* Remove support for updateable precompiled views.

* Allow precompiled views to be served when source file does not exist in
  file system.
* Cache results for views that do not exist on disk.

Fixes #2462 and fixes #2796.
Partially addresses #2551
This commit is contained in:
Pranav K 2015-08-05 16:50:26 -07:00
parent 982213e9e0
commit dfacd2543b
9 changed files with 313 additions and 941 deletions

View File

@ -3,23 +3,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.FileProviders;
using Microsoft.AspNet.Mvc.Razor.Precompilation;
using Microsoft.Dnx.Runtime;
using Microsoft.Framework.Caching.Memory;
using Microsoft.Framework.Internal;
using Microsoft.Framework.OptionsModel;
using Microsoft.Dnx.Runtime;
namespace Microsoft.AspNet.Mvc.Razor.Compilation
{
/// <summary>
/// Caches the result of runtime compilation of Razor files for the duration of the app lifetime.
/// Caches the result of runtime compilation of Razor files for the duration of the application lifetime.
/// </summary>
public class CompilerCache : ICompilerCache
{
private static readonly TypeInfo RazorFileInfoCollectionType = typeof(RazorFileInfoCollection).GetTypeInfo();
private static readonly Assembly RazorHostAssembly = typeof(CompilerCache).GetTypeInfo().Assembly;
private readonly IFileProvider _fileProvider;
private readonly IMemoryCache _cache;
@ -27,178 +26,79 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
/// Initializes a new instance of <see cref="CompilerCache"/> populated with precompiled views
/// discovered using <paramref name="provider"/>.
/// </summary>
/// <param name="assemblyProvider">The <see cref="IAssemblyProvider"/> that provides assemblies
/// for precompiled view discovery.</param>
/// <param name="razorFileInfoCollections">The sequence of <see cref="RazorFileInfoCollection"/> that provides
/// information for precompiled view discovery.</param>
/// <param name="loaderContextAccessor">The <see cref="IAssemblyLoadContextAccessor"/>.</param>
/// <param name="optionsAccessor">An accessor to the <see cref="RazorViewEngineOptions"/>.</param>
public CompilerCache(IAssemblyProvider assemblyProvider,
IAssemblyLoadContextAccessor loadContextAccessor,
IOptions<RazorViewEngineOptions> optionsAccessor)
: this(GetFileInfos(assemblyProvider.CandidateAssemblies),
loadContextAccessor.GetLoadContext(RazorFileInfoCollectionType.Assembly),
public CompilerCache(
IEnumerable<RazorFileInfoCollection> razorFileInfoCollections,
IAssemblyLoadContextAccessor loadContextAccessor,
IOptions<RazorViewEngineOptions> optionsAccessor)
: this(razorFileInfoCollections,
loadContextAccessor.GetLoadContext(RazorHostAssembly),
optionsAccessor.Options.FileProvider)
{
}
internal CompilerCache(IEnumerable<RazorFileInfoCollection> razorFileInfoCollections,
IAssemblyLoadContext loadContext,
IFileProvider fileProvider)
internal CompilerCache(
IEnumerable<RazorFileInfoCollection> razorFileInfoCollections,
IAssemblyLoadContext loadContext,
IFileProvider fileProvider)
{
_fileProvider = fileProvider;
_cache = new MemoryCache(new MemoryCacheOptions { CompactOnMemoryPressure = false });
var cacheEntries = new List<CompilerCacheEntry>();
foreach (var viewCollection in razorFileInfoCollections)
{
var containingAssembly = viewCollection.LoadAssembly(loadContext);
foreach (var fileInfo in viewCollection.FileInfos)
{
var viewType = containingAssembly.GetType(fileInfo.FullTypeName);
var cacheEntry = new CompilerCacheEntry(fileInfo, viewType);
// There shouldn't be any duplicates and if there are any the first will win.
// If the result doesn't match the one on disk its going to recompile anyways.
var cacheEntry = new CompilerCacheResult(CompilationResult.Successful(viewType));
var normalizedPath = NormalizePath(fileInfo.RelativePath);
_cache.Set(
normalizedPath,
cacheEntry,
GetMemoryCacheEntryOptions(fileInfo.RelativePath));
cacheEntries.Add(cacheEntry);
}
}
// Set up _ViewImports
foreach (var entry in cacheEntries)
{
var globalFileLocations = ViewHierarchyUtility.GetViewImportsLocations(entry.RelativePath);
foreach (var location in globalFileLocations)
{
var globalFileEntry = _cache.Get<CompilerCacheEntry>(location);
if (globalFileEntry != null)
{
// Add the composite _ViewImports entry as a dependency.
entry.AssociatedGlobalFileEntry = globalFileEntry;
break;
}
_cache.Set(normalizedPath, cacheEntry);
}
}
}
/// <inheritdoc />
public CompilerCacheResult GetOrAdd([NotNull] string relativePath,
[NotNull] Func<RelativeFileInfo, CompilationResult> compile)
{
var result = GetOrAddCore(relativePath, compile);
if (result == null)
{
return CompilerCacheResult.FileNotFound;
}
return new CompilerCacheResult(result.CompilationResult);
}
private GetOrAddResult GetOrAddCore(string relativePath,
Func<RelativeFileInfo, CompilationResult> compile)
public CompilerCacheResult GetOrAdd(
[NotNull] string relativePath,
[NotNull] Func<RelativeFileInfo, CompilationResult> compile)
{
var normalizedPath = NormalizePath(relativePath);
var cacheEntry = _cache.Get<CompilerCacheEntry>(normalizedPath);
if (cacheEntry == null)
CompilerCacheResult cacheResult;
if (!_cache.TryGetValue(normalizedPath, out cacheResult))
{
var fileInfo = _fileProvider.GetFileInfo(relativePath);
MemoryCacheEntryOptions cacheEntryOptions;
CompilerCacheResult cacheResultToCache;
if (!fileInfo.Exists)
{
return null;
cacheResultToCache = CompilerCacheResult.FileNotFound;
cacheResult = CompilerCacheResult.FileNotFound;
cacheEntryOptions = new MemoryCacheEntryOptions();
cacheEntryOptions.AddExpirationTrigger(_fileProvider.Watch(relativePath));
}
var relativeFileInfo = new RelativeFileInfo(fileInfo, relativePath);
return OnCacheMiss(relativeFileInfo, normalizedPath, compile);
}
else if (cacheEntry.IsPreCompiled && !cacheEntry.IsValidatedPreCompiled)
{
// For precompiled views, the first time the entry is read, we need to ensure that no changes were made
// either to the file associated with this entry, or any _ViewImports associated with it between the time
// the View was precompiled and the time EnsureInitialized was called. For later iterations, we can
// rely on expiration triggers ensuring the validity of the entry.
var fileInfo = _fileProvider.GetFileInfo(relativePath);
if (!fileInfo.Exists)
else
{
return null;
var relativeFileInfo = new RelativeFileInfo(fileInfo, relativePath);
var compilationResult = compile(relativeFileInfo).EnsureSuccessful();
cacheEntryOptions = GetMemoryCacheEntryOptions(relativePath);
// By default the CompilationResult returned by IRoslynCompiler is an instance of
// UncachedCompilationResult. This type has the generated code as a string property and do not want
// to cache it. We'll instead cache the unwrapped result.
cacheResultToCache = new CompilerCacheResult(
CompilationResult.Successful(compilationResult.CompiledType));
cacheResult = new CompilerCacheResult(compilationResult);
}
var relativeFileInfo = new RelativeFileInfo(fileInfo, relativePath);
if (cacheEntry.Length != fileInfo.Length)
{
// Recompile if the file lengths differ
return OnCacheMiss(relativeFileInfo, normalizedPath, compile);
}
if (AssociatedGlobalFilesChanged(cacheEntry, compile))
{
// Recompile if _ViewImports have changed since the entry was created.
return OnCacheMiss(relativeFileInfo, normalizedPath, compile);
}
if (cacheEntry.LastModified == fileInfo.LastModified)
{
// Assigning to IsValidatedPreCompiled is an atomic operation and will result in a safe race
// if it is being concurrently updated and read.
cacheEntry.IsValidatedPreCompiled = true;
return new GetOrAddResult
{
CompilationResult = CompilationResult.Successful(cacheEntry.CompiledType),
CompilerCacheEntry = cacheEntry
};
}
// Timestamp doesn't match but it might be because of deployment, compare the hash.
if (cacheEntry.IsPreCompiled &&
string.Equals(cacheEntry.Hash,
RazorFileHash.GetHash(fileInfo, cacheEntry.HashAlgorithmVersion),
StringComparison.Ordinal))
{
// Cache hit, but we need to update the entry.
// Assigning to LastModified and IsValidatedPreCompiled are atomic operations and will result in safe race
// if the entry is being concurrently read or updated.
cacheEntry.LastModified = fileInfo.LastModified;
cacheEntry.IsValidatedPreCompiled = true;
return new GetOrAddResult
{
CompilationResult = CompilationResult.Successful(cacheEntry.CompiledType),
CompilerCacheEntry = cacheEntry
};
}
// it's not a match, recompile
return OnCacheMiss(relativeFileInfo, normalizedPath, compile);
_cache.Set(normalizedPath, cacheResultToCache, cacheEntryOptions);
}
return new GetOrAddResult
{
CompilationResult = CompilationResult.Successful(cacheEntry.CompiledType),
CompilerCacheEntry = cacheEntry
};
}
private GetOrAddResult OnCacheMiss(RelativeFileInfo file,
string normalizedPath,
Func<RelativeFileInfo, CompilationResult> compile)
{
var compilationResult = compile(file).EnsureSuccessful();
// Concurrent addition to MemoryCache with the same key result in safe race.
var compilerCacheEntry = new CompilerCacheEntry(file, compilationResult.CompiledType);
var cacheEntry = _cache.Set<CompilerCacheEntry>(
normalizedPath,
compilerCacheEntry,
GetMemoryCacheEntryOptions(compilerCacheEntry.RelativePath));
return new GetOrAddResult
{
CompilationResult = compilationResult,
CompilerCacheEntry = cacheEntry
};
return cacheResult;
}
private MemoryCacheEntryOptions GetMemoryCacheEntryOptions(string relativePath)
@ -211,37 +111,10 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
{
options.AddExpirationTrigger(_fileProvider.Watch(location));
}
return options;
}
private bool AssociatedGlobalFilesChanged(CompilerCacheEntry entry,
Func<RelativeFileInfo, CompilationResult> compile)
{
var globalFileEntry = GetCompositeGlobalFileEntry(entry.RelativePath, compile);
return entry.AssociatedGlobalFileEntry != globalFileEntry;
}
// Returns the entry for the nearest _ViewImports that the file inherits directives from. Since _ViewImports
// entries are affected by other _ViewImports entries that are in the path hierarchy, the returned value
// represents the composite result of performing a cache check on individual _ViewImports entries.
private CompilerCacheEntry GetCompositeGlobalFileEntry(string relativePath,
Func<RelativeFileInfo, CompilationResult> compile)
{
var viewImportsLocations = ViewHierarchyUtility.GetViewImportsLocations(relativePath);
foreach (var viewImports in viewImportsLocations)
{
var getOrAddResult = GetOrAddCore(viewImports, compile);
if (getOrAddResult != null)
{
// This is the nearest _ViewImports that exists on disk.
return getOrAddResult.CompilerCacheEntry;
}
}
// No _ViewImports discovered.
return null;
}
private static string NormalizePath(string path)
{
// We need to allow for scenarios where the application was precompiled on a machine with forward slashes
@ -252,34 +125,5 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
return path;
}
internal static IEnumerable<RazorFileInfoCollection> GetFileInfos(IEnumerable<Assembly> assemblies)
{
return assemblies.SelectMany(a => a.ExportedTypes)
.Where(Match)
.Select(c => (RazorFileInfoCollection)Activator.CreateInstance(c));
}
internal static bool Match(Type t)
{
var inAssemblyType = typeof(RazorFileInfoCollection);
if (inAssemblyType.IsAssignableFrom(t))
{
var hasParameterlessConstructor = t.GetConstructor(Type.EmptyTypes) != null;
return hasParameterlessConstructor
&& !t.GetTypeInfo().IsAbstract
&& !t.GetTypeInfo().ContainsGenericParameters;
}
return false;
}
private class GetOrAddResult
{
public CompilerCacheEntry CompilerCacheEntry { get; set; }
public CompilationResult CompilationResult { get; set; }
}
}
}

View File

@ -1,93 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Mvc.Razor.Precompilation;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.Razor.Compilation
{
/// <summary>
/// An entry in <see cref="ICompilerCache"/> that contain metadata about precompiled and dynamically compiled file.
/// </summary>
public class CompilerCacheEntry
{
/// <summary>
/// Initializes a new instance of <see cref="CompilerCacheEntry"/> for a file that was precompiled.
/// </summary>
/// <param name="info">Metadata about the precompiled file.</param>
/// <param name="compiledType">The compiled <see cref="Type"/>.</param>
public CompilerCacheEntry([NotNull] RazorFileInfo info, [NotNull] Type compiledType)
{
CompiledType = compiledType;
RelativePath = info.RelativePath;
Length = info.Length;
LastModified = info.LastModified;
Hash = info.Hash;
HashAlgorithmVersion = info.HashAlgorithmVersion;
IsPreCompiled = true;
}
/// <summary>
/// Initializes a new instance of <see cref="CompilerCacheEntry"/> for a file that was dynamically compiled.
/// </summary>
/// <param name="info">Metadata about the file that was compiled.</param>
/// <param name="compiledType">The compiled <see cref="Type"/>.</param>
public CompilerCacheEntry([NotNull] RelativeFileInfo info, [NotNull] Type compiledType)
{
CompiledType = compiledType;
RelativePath = info.RelativePath;
Length = info.FileInfo.Length;
LastModified = info.FileInfo.LastModified;
}
/// <summary>
/// Gets the <see cref="Type"/> produced as a result of compilation.
/// </summary>
public Type CompiledType { get; }
/// <summary>
/// Gets the path of the compiled file relative to the root of the application.
/// </summary>
public string RelativePath { get; }
/// <summary>
/// Gets the size of file (in bytes) on disk.
/// </summary>
public long Length { get; }
/// <summary>
/// Gets or sets the last modified <see cref="DateTimeOffset"/> for the file at the time of compilation.
/// </summary>
public DateTimeOffset LastModified { get; set; }
/// <summary>
/// Gets the file hash, should only be available for pre compiled files.
/// </summary>
public string Hash { get; }
/// <summary>
/// Gets the version of the hash algorithm used to generate <see cref="Hash"/>.
/// </summary>
/// <remarks>
/// This value is only initialized for precompiled views.
/// </remarks>
public int HashAlgorithmVersion { get; }
/// <summary>
/// Gets a flag that indicates if the file is precompiled.
/// </summary>
public bool IsPreCompiled { get; }
/// <summary>
/// Gets or sets the <see cref="CompilerCacheEntry"/> for the nearest _ViewImports that the compiled type
/// depends on.
/// </summary>
public CompilerCacheEntry AssociatedGlobalFileEntry { get; set; }
/// <summary>
/// Gets or sets a flag that determines if the validity of this cache entry was performed at runtime.
/// </summary>
public bool IsValidatedPreCompiled { get; set; }
}
}

View File

@ -2,10 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Razor;
using Microsoft.AspNet.Mvc.Razor.Compilation;
using Microsoft.AspNet.Mvc.Razor.Directives;
using Microsoft.AspNet.Mvc.Razor.Precompilation;
using Microsoft.Framework.Caching.Memory;
using Microsoft.Framework.Internal;
using Microsoft.Framework.OptionsModel;
@ -14,6 +18,8 @@ namespace Microsoft.Framework.DependencyInjection
{
public static class MvcRazorMvcBuilderExtensions
{
private static readonly Type RazorFileInfoCollectionType = typeof(RazorFileInfoCollection);
public static IMvcBuilder AddRazorViewEngine([NotNull] this IMvcBuilder builder)
{
builder.AddViews();
@ -36,6 +42,28 @@ namespace Microsoft.Framework.DependencyInjection
return builder;
}
public static IMvcBuilder AddPrecompiledRazorViews(
[NotNull] this IMvcBuilder builder,
[NotNull] params Assembly[] assemblies)
{
AddRazorViewEngine(builder);
var razorFileInfos = GetFileInfoCollections(assemblies);
builder.Services.TryAddEnumerable(ServiceDescriptor.Instance(razorFileInfos));
return builder;
}
public static IServiceCollection AddPrecompiledRazorViews(
[NotNull] this IServiceCollection collection,
[NotNull] params Assembly[] assemblies)
{
var razorFileInfos = GetFileInfoCollections(assemblies);
collection.TryAddEnumerable(ServiceDescriptor.Instance(razorFileInfos));
return collection;
}
public static IMvcBuilder ConfigureRazorViewEngine(
[NotNull] this IMvcBuilder builder,
[NotNull] Action<RazorViewEngineOptions> setupAction)
@ -91,5 +119,17 @@ namespace Microsoft.Framework.DependencyInjection
// Consumed by the Cache tag helper to cache results across the lifetime of the application.
services.TryAddSingleton<IMemoryCache, MemoryCache>();
}
private static IEnumerable<RazorFileInfoCollection> GetFileInfoCollections(IEnumerable<Assembly> assemblies) =>
assemblies
.SelectMany(assembly => assembly.ExportedTypes)
.Where(IsValidRazorFileInfoCollection)
.Select(Activator.CreateInstance)
.Cast<RazorFileInfoCollection>();
internal static bool IsValidRazorFileInfoCollection(Type type) =>
RazorFileInfoCollectionType.IsAssignableFrom(type) &&
!type.GetTypeInfo().IsAbstract &&
!type.GetTypeInfo().ContainsGenericParameters;
}
}

View File

@ -25,7 +25,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Precompilation
public string SymbolsResourceName { get; protected set; }
/// <summary>
/// Gets the <see cref="IReadOnlyList{T}{T}"/> of <see cref="RazorFileInfo"/>s.
/// Gets the <see cref="IReadOnlyList{T}"/> of <see cref="RazorFileInfo"/>s.
/// </summary>
public IReadOnlyList<RazorFileInfo> FileInfos { get; protected set; }

View File

@ -41,10 +41,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var applicationEnvironment = serviceProvider.GetRequiredService<IApplicationEnvironment>();
var viewsDirectory = Path.Combine(applicationEnvironment.ApplicationBasePath, "Views", "Home");
var layoutContent = File.ReadAllText(Path.Combine(viewsDirectory, "Layout.cshtml"));
var indexContent = File.ReadAllText(Path.Combine(viewsDirectory, "Index.cshtml"));
var viewstartContent = File.ReadAllText(Path.Combine(viewsDirectory, "_ViewStart.cshtml"));
var globalContent = File.ReadAllText(Path.Combine(viewsDirectory, "_ViewImports.cshtml"));
// We will render a view that writes the fully qualified name of the Assembly containing the type of
// the view. If the view is precompiled, this assembly will be PrecompilationWebsite.
@ -64,103 +61,19 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.StartsWith(assemblyNamePrefix, parsedResponse1.Index);
// Act - 2
// Touch the Layout file and verify it is now dynamically compiled.
await TouchFile(viewsDirectory, "Layout.cshtml");
// Touch the Index file and verify it remains unaffected.
await TouchFile(viewsDirectory, "Index.cshtml");
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
// Assert - 2
var response2 = new ParsedResponse(responseContent);
Assert.StartsWith(assemblyNamePrefix, response2.ViewStart);
Assert.StartsWith(assemblyNamePrefix, response2.Index);
Assert.DoesNotContain(assemblyNamePrefix, response2.Layout);
// Act - 3
// Touch the _ViewStart file and verify it is is dynamically compiled.
await TouchFile(viewsDirectory, "_ViewStart.cshtml");
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
// Assert - 3
var response3 = new ParsedResponse(responseContent);
Assert.NotEqual(assemblyNamePrefix, response3.ViewStart);
Assert.Equal(response2.Index, response3.Index);
Assert.Equal(response2.Layout, response3.Layout);
// Act - 4
// Touch the _ViewImports file and verify it causes all files to recompile.
await TouchFile(viewsDirectory, "_ViewImports.cshtml");
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
// Assert - 4
var response4 = new ParsedResponse(responseContent);
Assert.NotEqual(response3.ViewStart, response4.ViewStart);
Assert.NotEqual(response3.Index, response4.Index);
Assert.NotEqual(response3.Layout, response4.Layout);
// Act - 5
// Touch Index file and verify it is the only page that recompiles.
await TouchFile(viewsDirectory, "Index.cshtml");
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
// Assert - 5
var response5 = new ParsedResponse(responseContent);
// Layout and _ViewStart should not have changed.
Assert.Equal(response4.Layout, response5.Layout);
Assert.Equal(response4.ViewStart, response5.ViewStart);
Assert.NotEqual(response4.Index, response5.Index);
// Act - 6
// Touch the _ViewImports file. This time, we'll verify the Non-precompiled -> Non-precompiled workflow.
await TouchFile(viewsDirectory, "_ViewImports.cshtml");
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
// Assert - 6
var response6 = new ParsedResponse(responseContent);
// Everything should've recompiled.
Assert.NotEqual(response5.ViewStart, response6.ViewStart);
Assert.NotEqual(response5.Index, response6.Index);
Assert.NotEqual(response5.Layout, response6.Layout);
// Act - 7
// Add a new _ViewImports file
var newViewImports = await TouchFile(Path.GetDirectoryName(viewsDirectory), "_ViewImports.cshtml");
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
// Assert - 7
// Everything should've recompiled.
var response7 = new ParsedResponse(responseContent);
Assert.NotEqual(response6.ViewStart, response7.ViewStart);
Assert.NotEqual(response6.Index, response7.Index);
Assert.NotEqual(response6.Layout, response7.Layout);
// Act - 8
// Remove new _ViewImports file
File.Delete(newViewImports);
await Task.Delay(_cacheDelayInterval);
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
// Assert - 8
// Everything should've recompiled.
var response8 = new ParsedResponse(responseContent);
Assert.NotEqual(response7.ViewStart, response8.ViewStart);
Assert.NotEqual(response7.Index, response8.Index);
Assert.NotEqual(response7.Layout, response8.Layout);
// Act - 9
// Refetch and verify we get cached types
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
// Assert - 9
var response9 = new ParsedResponse(responseContent);
Assert.Equal(response8.ViewStart, response9.ViewStart);
Assert.Equal(response8.Index, response9.Index);
Assert.Equal(response8.Layout, response9.Layout);
Assert.StartsWith(assemblyNamePrefix, response2.Layout);
}
finally
{
File.WriteAllText(Path.Combine(viewsDirectory, "Layout.cshtml"), layoutContent.TrimEnd(' '));
File.WriteAllText(Path.Combine(viewsDirectory, "Index.cshtml"), indexContent.TrimEnd(' '));
File.WriteAllText(Path.Combine(viewsDirectory, "_ViewStart.cshtml"), viewstartContent.TrimEnd(' '));
File.WriteAllText(Path.Combine(viewsDirectory, "_ViewImports.cshtml"), globalContent.TrimEnd(' '));
}
}
@ -188,51 +101,6 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.StartsWith(expected, responseContent.Trim());
}
[ConditionalTheory]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
public async Task DeletingPrecompiledGlobalFile_PriorToFirstRequestToAView_CausesViewToBeRecompiled()
{
// Arrange
var expected = GetAssemblyNamePrefix();
IServiceCollection serviceCollection = null;
var server = TestHelper.CreateServer(_app, SiteName, services =>
{
_configureServices(services);
serviceCollection = services;
});
var client = server.CreateClient();
var serviceProvider = serviceCollection.BuildServiceProvider();
var applicationEnvironment = serviceProvider.GetRequiredService<IApplicationEnvironment>();
var viewsDirectory = Path.Combine(applicationEnvironment.ApplicationBasePath,
"Views",
"ViewImportsDelete");
var globalPath = Path.Combine(viewsDirectory, "_ViewImports.cshtml");
var globalContent = File.ReadAllText(globalPath);
// Act - 1
// Query the Test view so we know the compiler cache gets populated.
var response = await client.GetStringAsync("/Test");
// Assert - 1
Assert.Equal("Test", response.Trim());
try
{
// Act - 2
File.Delete(globalPath);
var response2 = await client.GetStringAsync("http://localhost/Home/GlobalDeletedPriorToFirstRequest");
// Assert - 2
Assert.DoesNotContain(expected, response2.Trim());
}
finally
{
File.WriteAllText(globalPath, globalContent);
}
}
[ConditionalTheory]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
public async Task TagHelpersFromTheApplication_CanBeAdded()

View File

@ -3,15 +3,10 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.AspNet.FileProviders;
using Microsoft.AspNet.Mvc.Razor.Internal;
using Microsoft.AspNet.Mvc.Razor.Precompilation;
using Microsoft.AspNet.Testing.xunit;
using Microsoft.Dnx.Runtime;
using Moq;
using Xunit;
@ -20,19 +15,27 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
{
public class CompilerCacheTest
{
private const string ViewPath = "view-path";
private const string ViewPath = "Views/Home/Index.cshtml";
private const string PrecompiledViewsPath = "Views/Home/Precompiled.cshtml";
private readonly IAssemblyLoadContext TestLoadContext = Mock.Of<IAssemblyLoadContext>();
public static TheoryData ViewImportsPaths =>
new TheoryData<string>
{
Path.Combine("Views", "Home", "_ViewImports.cshtml"),
Path.Combine("Views", "_ViewImports.cshtml"),
"_ViewImports.cshtml",
};
[Fact]
public void GetOrAdd_ReturnsFileNotFoundResult_IfFileIsNotFoundInFileSystem()
{
// Arrange
var fileProvider = new TestFileProvider();
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
var type = GetType();
// Act
var result = cache.GetOrAdd("/some/path", _ => { throw new Exception("Shouldn't be called"); });
var result = cache.GetOrAdd("/some/path", ThrowsIfCalled);
// Assert
Assert.Same(CompilerCacheResult.FileNotFound, result);
@ -46,7 +49,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
var type = GetType();
var type = typeof(TestView);
var expected = UncachedCompilationResult.Successful(type, "hello world");
// Act
@ -68,7 +71,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
var type = typeof(RuntimeCompileIdentical);
var type = typeof(TestView);
var expected = UncachedCompilationResult.Successful(type, "hello world");
// Act 1
@ -82,7 +85,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
// Delete the file from the file system and set it's expiration trigger.
fileProvider.DeleteFile(ViewPath);
fileProvider.GetTrigger(ViewPath).IsExpired = true;
var result2 = cache.GetOrAdd(ViewPath, _ => { throw new Exception("shouldn't be called."); });
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
// Assert 2
Assert.Same(CompilerCacheResult.FileNotFound, result2);
@ -96,9 +99,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
var type = typeof(RuntimeCompileIdentical);
var expected1 = UncachedCompilationResult.Successful(type, "hello world");
var expected2 = UncachedCompilationResult.Successful(type, "different content");
var expected1 = UncachedCompilationResult.Successful(typeof(TestView), "hello world");
var expected2 = UncachedCompilationResult.Successful(typeof(DifferentView), "different content");
// Act 1
var result1 = cache.GetOrAdd(ViewPath, _ => expected1);
@ -108,12 +110,55 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
Assert.Same(expected1, result1.CompilationResult);
// Act 2
fileProvider.GetTrigger(ViewPath).IsExpired = true;
var result2 = cache.GetOrAdd(ViewPath, _ => expected2);
// Verify we're getting cached results.
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
// Assert 2
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
Assert.Same(expected2, result2.CompilationResult);
Assert.Same(expected1.CompiledType, result2.CompilationResult.CompiledType);
// Act 3
fileProvider.GetTrigger(ViewPath).IsExpired = true;
var result3 = cache.GetOrAdd(ViewPath, _ => expected2);
// Assert 3
Assert.NotSame(CompilerCacheResult.FileNotFound, result3);
Assert.Same(expected2, result3.CompilationResult);
}
[Theory]
[MemberData(nameof(ViewImportsPaths))]
public void GetOrAdd_ReturnsNewResult_IfAncestorViewImportsWereModified(string globalImportPath)
{
// Arrange
var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
var expected1 = UncachedCompilationResult.Successful(typeof(TestView), "hello world");
var expected2 = UncachedCompilationResult.Successful(typeof(DifferentView), "different content");
// Act 1
var result1 = cache.GetOrAdd(ViewPath, _ => expected1);
// Assert 1
Assert.NotSame(CompilerCacheResult.FileNotFound, result1);
Assert.Same(expected1, result1.CompilationResult);
// Act 2
// Verify we're getting cached results.
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
// Assert 2
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
Assert.Same(expected1.CompiledType, result2.CompilationResult.CompiledType);
// Act 3
fileProvider.GetTrigger(globalImportPath).IsExpired = true;
var result3 = cache.GetOrAdd(ViewPath, _ => expected2);
// Assert 2
Assert.NotSame(CompilerCacheResult.FileNotFound, result3);
Assert.Same(expected2, result3.CompilationResult);
}
[Fact]
@ -124,7 +169,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
var fileProvider = mockFileProvider.Object;
fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
var type = typeof(RuntimeCompileIdentical);
var type = typeof(TestView);
var expected = UncachedCompilationResult.Successful(type, "hello world");
// Act 1
@ -135,7 +180,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
Assert.Same(expected, result1.CompilationResult);
// Act 2
var result2 = cache.GetOrAdd(ViewPath, _ => { throw new Exception("shouldn't be called"); });
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
// Assert 2
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
@ -144,543 +189,120 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
mockFileProvider.Verify(v => v.GetFileInfo(ViewPath), Times.Once());
}
private abstract class View
[Fact]
public void GetOrAdd_UsesViewsSpecifiedFromRazorFileInfoCollection()
{
public abstract string Content { get; }
// Arrange
var fileProvider = new TestFileProvider();
var cache = new CompilerCache(new[] { new TestViewCollection() }, TestLoadContext, fileProvider);
// Act
var result = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled);
// Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
Assert.Same(typeof(PreCompile), result.CompilationResult.CompiledType);
}
private class PreCompile : View
[Fact]
public void GetOrAdd_DoesNotRecompile_IfFileTriggerWasSetForPrecompiledFile()
{
public override string Content { get { return "Hello World it's @DateTime.Now"; } }
}
// Arrange
var fileProvider = new TestFileProvider();
var cache = new CompilerCache(new[] { new TestViewCollection() }, TestLoadContext, fileProvider);
private class RuntimeCompileIdentical : View
{
public override string Content { get { return new PreCompile().Content; } }
}
// Act
fileProvider.Watch(PrecompiledViewsPath);
fileProvider.GetTrigger(PrecompiledViewsPath).IsExpired = true;
var result = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled);
private class RuntimeCompileDifferent : View
{
public override string Content { get { return new PreCompile().Content.Substring(1) + " "; } }
}
private class RuntimeCompileDifferentLength : View
{
public override string Content
{
get
{
return new PreCompile().Content + " longer because it was modified at runtime";
}
}
}
private static Stream GetMemoryStream(string content)
{
var bytes = Encoding.UTF8.GetBytes(content);
return new MemoryStream(bytes);
// Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
Assert.Same(typeof(PreCompile), result.CompilationResult.CompiledType);
}
[Theory]
[InlineData(10000)]
[InlineData(11000)]
public void GetOrAdd_UsesFilesFromCache_IfTimestampDiffers_ButContentAndLengthAreTheSame(long fileTimeUTC)
[MemberData(nameof(ViewImportsPaths))]
public void GetOrAdd_DoesNotRecompile_IfFileTriggerWasSetForViewImports(string globalImportPath)
{
// Arrange
var instance = new RuntimeCompileIdentical();
var length = Encoding.UTF8.GetByteCount(instance.Content);
var fileProvider = new TestFileProvider();
var cache = new CompilerCache(new[] { new ViewCollection() }, TestLoadContext, fileProvider);
var fileInfo = new TestFileInfo
{
Length = length,
LastModified = DateTime.FromFileTimeUtc(fileTimeUTC),
Content = instance.Content
};
fileProvider.AddFile(ViewPath, fileInfo);
var cache = new CompilerCache(new[] { new TestViewCollection() }, TestLoadContext, fileProvider);
// Act
var result = cache.GetOrAdd(ViewPath,
compile: _ => { throw new Exception("Shouldn't be called."); });
fileProvider.Watch(globalImportPath);
fileProvider.GetTrigger(globalImportPath).IsExpired = true;
var result = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled);
// Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
var actual = result.CompilationResult;
Assert.NotNull(actual);
Assert.Equal(typeof(PreCompile), actual.CompiledType);
}
[Theory]
[InlineData(typeof(RuntimeCompileDifferent), 11000)]
[InlineData(typeof(RuntimeCompileDifferentLength), 10000)]
[InlineData(typeof(RuntimeCompileDifferentLength), 11000)]
public void GetOrAdd_RecompilesFile_IfContentAndLengthAreChanged(
Type resultViewType,
long fileTimeUTC)
{
// Arrange
var instance = (View)Activator.CreateInstance(resultViewType);
var length = Encoding.UTF8.GetByteCount(instance.Content);
var fileProvider = new TestFileProvider();
var cache = new CompilerCache(new[] { new ViewCollection() }, TestLoadContext, fileProvider);
var fileInfo = new TestFileInfo
{
Length = length,
LastModified = DateTime.FromFileTimeUtc(fileTimeUTC),
Content = instance.Content
};
fileProvider.AddFile(ViewPath, fileInfo);
// Act
var result = cache.GetOrAdd(ViewPath,
compile: _ => CompilationResult.Successful(resultViewType));
// Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
var actual = result.CompilationResult;
Assert.NotNull(actual);
Assert.Equal(resultViewType, actual.CompiledType);
Assert.Same(typeof(PreCompile), result.CompilationResult.CompiledType);
}
[Fact]
public void GetOrAdd_UsesValueFromCache_IfGlobalHasNotChanged()
public void GetOrAdd_ReturnsRuntimeCompiledAndPrecompiledViews()
{
// Arrange
var instance = new PreCompile();
var length = Encoding.UTF8.GetByteCount(instance.Content);
var fileProvider = new TestFileProvider();
var lastModified = DateTime.UtcNow;
var fileInfo = new TestFileInfo
{
Length = length,
LastModified = lastModified,
Content = instance.Content
};
fileProvider.AddFile(ViewPath, fileInfo);
var globalContent = "global-content";
var globalFileInfo = new TestFileInfo
{
Content = globalContent,
LastModified = DateTime.UtcNow
};
fileProvider.AddFile("_ViewImports.cshtml", globalFileInfo);
var globalRazorFileInfo = new RazorFileInfo
{
Hash = Crc32.Calculate(GetMemoryStream(globalContent)).ToString(CultureInfo.InvariantCulture),
HashAlgorithmVersion = 1,
LastModified = globalFileInfo.LastModified,
Length = globalFileInfo.Length,
RelativePath = "_ViewImports.cshtml",
FullTypeName = typeof(RuntimeCompileIdentical).FullName
};
var precompiledViews = new ViewCollection();
precompiledViews.Add(globalRazorFileInfo);
var cache = new CompilerCache(new[] { precompiledViews }, TestLoadContext, fileProvider);
// Act
var result = cache.GetOrAdd(ViewPath,
compile: _ => { throw new Exception("shouldn't be invoked"); });
// Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
var actual = result.CompilationResult;
Assert.NotNull(actual);
Assert.Equal(typeof(PreCompile), actual.CompiledType);
}
[Fact]
public void GetOrAdd_ReturnsFileNotFoundResult_IfPrecompiledViewWasRemovedFromFileSystem()
{
// Arrange
var precompiledViews = new ViewCollection();
var fileProvider = new TestFileProvider();
var cache = new CompilerCache(new[] { precompiledViews }, TestLoadContext, fileProvider);
// Act
var result = cache.GetOrAdd(ViewPath,
compile: _ => { throw new Exception("shouldn't be invoked"); });
// Assert
Assert.Same(CompilerCacheResult.FileNotFound, result);
Assert.Null(result.CompilationResult);
}
[Fact]
public void GetOrAdd_DoesNotReadFileFromFileSystemAfterPrecompiledViewIsVerified()
{
// Arrange
var precompiledViews = new ViewCollection();
var mockFileProvider = new Mock<TestFileProvider> { CallBase = true };
var fileProvider = mockFileProvider.Object;
var precompiledView = precompiledViews.FileInfos[0];
var fileInfo = new TestFileInfo
{
Length = precompiledView.Length,
LastModified = precompiledView.LastModified,
};
fileProvider.AddFile(ViewPath, fileInfo);
var cache = new CompilerCache(new[] { precompiledViews }, TestLoadContext, fileProvider);
fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(new[] { new TestViewCollection() }, TestLoadContext, fileProvider);
var expected = CompilationResult.Successful(typeof(TestView));
// Act 1
var result1 = cache.GetOrAdd(ViewPath,
compile: _ => { throw new Exception("shouldn't be invoked"); });
var result1 = cache.GetOrAdd(ViewPath, _ => expected);
// Assert 1
Assert.NotSame(CompilerCacheResult.FileNotFound, result1);
var actual1 = result1.CompilationResult;
Assert.NotNull(actual1);
Assert.Equal(typeof(PreCompile), actual1.CompiledType);
mockFileProvider.Verify(v => v.GetFileInfo(ViewPath), Times.Once());
Assert.Same(expected, result1.CompilationResult);
// Act 2
var result2 = cache.GetOrAdd(ViewPath,
compile: _ => { throw new Exception("shouldn't be invoked"); });
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
// Assert 2
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
var actual2 = result2.CompilationResult;
Assert.NotNull(actual2);
Assert.Equal(typeof(PreCompile), actual2.CompiledType);
mockFileProvider.Verify(v => v.GetFileInfo(ViewPath), Times.Once());
}
Assert.Same(typeof(TestView), result2.CompilationResult.CompiledType);
[ConditionalTheory]
// Skipping for now since this is going to change.
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
public void GetOrAdd_IgnoresCachedValueIfFileIsIdentical_ButViewImportsWasAdedSinceTheCacheWasCreated()
{
// Arrange
var expectedType = typeof(RuntimeCompileDifferent);
var fileProvider = new TestFileProvider();
var collection = new ViewCollection();
var precompiledFile = collection.FileInfos[0];
precompiledFile.RelativePath = "Views\\home\\index.cshtml";
var cache = new CompilerCache(new[] { collection }, TestLoadContext, fileProvider);
var testFile = new TestFileInfo
{
Content = new PreCompile().Content,
LastModified = precompiledFile.LastModified,
PhysicalPath = precompiledFile.RelativePath
};
fileProvider.AddFile(precompiledFile.RelativePath, testFile);
var relativeFile = new RelativeFileInfo(testFile, testFile.PhysicalPath);
// Act 3
var result3 = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled);
// Act 1
var result1 = cache.GetOrAdd(testFile.PhysicalPath,
compile: _ => { throw new Exception("should not be called"); });
// Assert 1
Assert.NotSame(CompilerCacheResult.FileNotFound, result1);
var actual1 = result1.CompilationResult;
Assert.NotNull(actual1);
Assert.Equal(typeof(PreCompile), actual1.CompiledType);
// Act 2
var globalTrigger = fileProvider.GetTrigger("Views\\_ViewImports.cshtml");
globalTrigger.IsExpired = true;
var result2 = cache.GetOrAdd(testFile.PhysicalPath,
compile: _ => CompilationResult.Successful(expectedType));
// Assert 2
// Assert 3
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
var actual2 = result2.CompilationResult;
Assert.NotNull(actual2);
Assert.Equal(expectedType, actual2.CompiledType);
Assert.Same(typeof(PreCompile), result3.CompilationResult.CompiledType);
}
[ConditionalTheory]
// Skipping for now since this is going to change.
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
public void GetOrAdd_IgnoresCachedValueIfFileIsIdentical_ButGlobalWasDeletedSinceCacheWasCreated()
private class TestView
{
// Arrange
var expectedType = typeof(RuntimeCompileDifferent);
var lastModified = DateTime.UtcNow;
var fileProvider = new TestFileProvider();
var viewCollection = new ViewCollection();
var precompiledView = viewCollection.FileInfos[0];
precompiledView.RelativePath = "Views\\Index.cshtml";
var viewFileInfo = new TestFileInfo
{
Content = new PreCompile().Content,
LastModified = precompiledView.LastModified,
PhysicalPath = precompiledView.RelativePath
};
fileProvider.AddFile(viewFileInfo.PhysicalPath, viewFileInfo);
var globalFileInfo = new TestFileInfo
{
PhysicalPath = "Views\\_ViewImports.cshtml",
Content = "viewstart-content",
LastModified = lastModified
};
var globalFile = new RazorFileInfo
{
FullTypeName = typeof(RuntimeCompileIdentical).FullName,
RelativePath = globalFileInfo.PhysicalPath,
LastModified = globalFileInfo.LastModified,
Hash = RazorFileHash.GetHash(globalFileInfo, hashAlgorithmVersion: 1),
HashAlgorithmVersion = 1,
Length = globalFileInfo.Length
};
fileProvider.AddFile(globalFileInfo.PhysicalPath, globalFileInfo);
viewCollection.Add(globalFile);
var cache = new CompilerCache(new[] { viewCollection }, TestLoadContext, fileProvider);
// Act 1
var result1 = cache.GetOrAdd(viewFileInfo.PhysicalPath,
compile: _ => { throw new Exception("should not be called"); });
// Assert 1
Assert.NotSame(CompilerCacheResult.FileNotFound, result1);
var actual1 = result1.CompilationResult;
Assert.NotNull(actual1);
Assert.Equal(typeof(PreCompile), actual1.CompiledType);
// Act 2
var trigger = fileProvider.GetTrigger(globalFileInfo.PhysicalPath);
trigger.IsExpired = true;
var result2 = cache.GetOrAdd(viewFileInfo.PhysicalPath,
compile: _ => CompilationResult.Successful(expectedType));
// Assert 2
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
var actual2 = result2.CompilationResult;
Assert.NotNull(actual2);
Assert.Equal(expectedType, actual2.CompiledType);
}
public static IEnumerable<object[]> GetOrAdd_IgnoresCachedValue_IfGlobalWasChangedSinceCacheWasCreatedData
private class PreCompile
{
get
{
var globalContent = "global-content";
var contentStream = GetMemoryStream(globalContent);
var lastModified = DateTime.UtcNow;
int length = Encoding.UTF8.GetByteCount(globalContent);
var path = "Views\\_ViewImports.cshtml";
}
var razorFileInfo = new RazorFileInfo
public class DifferentView
{
}
private CompilationResult ThrowsIfCalled(RelativeFileInfo file)
{
throw new Exception("Shouldn't be called");
}
private class TestViewCollection : RazorFileInfoCollection
{
public TestViewCollection()
{
FileInfos = new List<RazorFileInfo>
{
Hash = Crc32.Calculate(contentStream).ToString(CultureInfo.InvariantCulture),
HashAlgorithmVersion = 1,
LastModified = lastModified,
Length = length,
RelativePath = path
new RazorFileInfo
{
FullTypeName = typeof(PreCompile).FullName,
RelativePath = PrecompiledViewsPath,
}
};
// Length does not match
var testFileInfo1 = new TestFileInfo
{
Length = 7732
};
yield return new object[] { razorFileInfo, testFileInfo1 };
// Content and last modified do not match
var testFileInfo2 = new TestFileInfo
{
Length = length,
Content = "viewstart-modified",
LastModified = lastModified.AddSeconds(100),
};
yield return new object[] { razorFileInfo, testFileInfo2 };
}
}
[Theory]
[MemberData(nameof(GetOrAdd_IgnoresCachedValue_IfGlobalWasChangedSinceCacheWasCreatedData))]
public void GetOrAdd_IgnoresCachedValue_IfGlobalFileWasChangedSinceCacheWasCreated(
RazorFileInfo viewStartRazorFileInfo, IFileInfo globalFileInfo)
{
// Arrange
var expectedType = typeof(RuntimeCompileDifferent);
var lastModified = DateTime.UtcNow;
var viewStartLastModified = DateTime.UtcNow;
var content = "some content";
var fileInfo = new TestFileInfo
{
Length = 1020,
Content = content,
LastModified = lastModified,
PhysicalPath = "Views\\home\\index.cshtml"
};
var fileProvider = new TestFileProvider();
fileProvider.AddFile(fileInfo.PhysicalPath, fileInfo);
fileProvider.AddFile(viewStartRazorFileInfo.RelativePath, globalFileInfo);
var viewCollection = new ViewCollection();
var cache = new CompilerCache(new[] { viewCollection }, TestLoadContext, fileProvider);
// Act
var result = cache.GetOrAdd(fileInfo.PhysicalPath,
compile: _ => CompilationResult.Successful(expectedType));
// Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
var actual = result.CompilationResult;
Assert.NotNull(actual);
Assert.Equal(expectedType, actual.CompiledType);
}
[Fact]
public void GetOrAdd_DoesNotCacheCompiledContent_OnCallsAfterInitial()
{
// Arrange
var lastModified = DateTime.UtcNow;
var fileProvider = new TestFileProvider();
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
var fileInfo = new TestFileInfo
{
PhysicalPath = "test",
LastModified = lastModified
};
fileProvider.AddFile("test", fileInfo);
var type = GetType();
var uncachedResult = UncachedCompilationResult.Successful(type, "hello world");
// Act
cache.GetOrAdd("test", _ => uncachedResult);
var result1 = cache.GetOrAdd("test", _ => { throw new Exception("shouldn't be called."); });
var result2 = cache.GetOrAdd("test", _ => { throw new Exception("shouldn't be called."); });
// Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result1);
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
var actual1 = Assert.IsType<CompilationResult>(result1.CompilationResult);
var actual2 = Assert.IsType<CompilationResult>(result2.CompilationResult);
Assert.NotSame(uncachedResult, actual1);
Assert.NotSame(uncachedResult, actual2);
Assert.Same(type, actual1.CompiledType);
Assert.Same(type, actual2.CompiledType);
}
[Fact]
public void Match_ReturnsFalse_IfTypeIsAbstract()
{
// Arrange
var type = typeof(AbstractRazorFileInfoCollection);
// Act
var result = CompilerCache.Match(type);
// Assert
Assert.False(result);
}
[Fact]
public void Match_ReturnsFalse_IfTypeHasGenericParameters()
{
// Arrange
var type = typeof(GenericRazorFileInfoCollection<>);
// Act
var result = CompilerCache.Match(type);
// Assert
Assert.False(result);
}
[Fact]
public void Match_ReturnsFalse_IfTypeDoesNotHaveDefaultConstructor()
{
// Arrange
var type = typeof(ParameterConstructorRazorFileInfoCollection);
// Act
var result = CompilerCache.Match(type);
// Assert
Assert.False(result);
}
[Fact]
public void Match_ReturnsFalse_IfTypeDoesNotDeriveFromRazorFileInfoCollection()
{
// Arrange
var type = typeof(NonSubTypeRazorFileInfoCollection);
// Act
var result = CompilerCache.Match(type);
// Assert
Assert.False(result);
}
[Fact]
public void Match_ReturnsTrue_IfTypeDerivesFromRazorFileInfoCollection()
{
// Arrange
var type = typeof(ViewCollection);
// Act
var result = CompilerCache.Match(type);
// Assert
Assert.True(result);
}
private abstract class AbstractRazorFileInfoCollection : RazorFileInfoCollection
{
}
private class GenericRazorFileInfoCollection<TVal> : RazorFileInfoCollection
{
}
private class ParameterConstructorRazorFileInfoCollection : RazorFileInfoCollection
{
public ParameterConstructorRazorFileInfoCollection(string value)
{
}
}
private class NonSubTypeRazorFileInfoCollection : Controller
{
}
private class ViewCollection : RazorFileInfoCollection
{
private readonly List<RazorFileInfo> _fileInfos = new List<RazorFileInfo>();
public ViewCollection()
{
FileInfos = _fileInfos;
var content = new PreCompile().Content;
var length = Encoding.UTF8.GetByteCount(content);
Add(new RazorFileInfo()
{
FullTypeName = typeof(PreCompile).FullName,
Hash = Crc32.Calculate(GetMemoryStream(content)).ToString(CultureInfo.InvariantCulture),
HashAlgorithmVersion = 1,
LastModified = DateTime.FromFileTimeUtc(10000),
Length = length,
RelativePath = ViewPath,
});
}
public void Add(RazorFileInfo fileInfo)
{
_fileInfos.Add(fileInfo);
}
public override Assembly LoadAssembly(IAssemblyLoadContext loadContext)
{
return typeof(ViewCollection).Assembly;
return typeof(TestViewCollection).Assembly;
}
}
}

View File

@ -0,0 +1,87 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Razor.Precompilation;
using Xunit;
namespace Microsoft.Framework.DependencyInjection
{
public class MvcRazorMvcBuilderExtensionsTest
{
[Fact]
public void IsValidRazorFileInfoCollection_ReturnsFalse_IfTypeIsAbstract()
{
// Arrange
var type = typeof(AbstractRazorFileInfoCollection);
// Act
var result = MvcRazorMvcBuilderExtensions.IsValidRazorFileInfoCollection(type);
// Assert
Assert.False(result);
}
[Fact]
public void IsValidRazorFileInfoCollection_ReturnsFalse_IfTypeHasGenericParameters()
{
// Arrange
var type = typeof(GenericRazorFileInfoCollection<>);
// Act
var result = MvcRazorMvcBuilderExtensions.IsValidRazorFileInfoCollection(type);
// Assert
Assert.False(result);
}
[Fact]
public void IsValidRazorFileInfoCollection_ReturnsFalse_IfTypeDoesNotDeriveFromRazorFileInfoCollection()
{
// Arrange
var type = typeof(NonSubTypeRazorFileInfoCollection);
// Act
var result = MvcRazorMvcBuilderExtensions.IsValidRazorFileInfoCollection(type);
// Assert
Assert.False(result);
}
[Theory]
[InlineData(typeof(ParameterConstructorRazorFileInfoCollection))]
[InlineData(typeof(ViewCollection))]
public void IsValidRazorFileInfoCollection_ReturnsTrue_IfTypeDerivesFromRazorFileInfoCollection(Type type)
{
// Act
var result = MvcRazorMvcBuilderExtensions.IsValidRazorFileInfoCollection(type);
// Assert
Assert.True(result);
}
private abstract class AbstractRazorFileInfoCollection : RazorFileInfoCollection
{
}
private class GenericRazorFileInfoCollection<TVal> : RazorFileInfoCollection
{
}
private class ParameterConstructorRazorFileInfoCollection : RazorFileInfoCollection
{
public ParameterConstructorRazorFileInfoCollection(string value)
{
}
}
private class NonSubTypeRazorFileInfoCollection : Controller
{
}
private class ViewCollection : RazorFileInfoCollection
{
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Reflection;
using Microsoft.AspNet.Builder;
using Microsoft.Framework.DependencyInjection;
@ -12,7 +13,9 @@ namespace PrecompilationWebSite
public void ConfigureServices(IServiceCollection services)
{
// Add MVC services to the services container
services.AddMvc();
services
.AddMvc()
.AddPrecompiledRazorViews(GetType().GetTypeInfo().Assembly);
}
public void Configure(IApplicationBuilder app)

View File

@ -1,21 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
using System.Collections.Generic;
using Microsoft.AspNet.Mvc.Razor;
using Microsoft.AspNet.Mvc.Razor.Compilation;
using Microsoft.Framework.OptionsModel;
using Microsoft.AspNet.Mvc.Razor.Precompilation;
using Microsoft.Dnx.Runtime;
using Microsoft.Framework.OptionsModel;
namespace RazorCompilerCacheWebSite
{
public class CustomCompilerCache : CompilerCache
{
public CustomCompilerCache(IAssemblyProvider assemblyProvider,
public CustomCompilerCache(IEnumerable<RazorFileInfoCollection> fileInfoCollection,
IAssemblyLoadContextAccessor loadContextAccessor,
IOptions<RazorViewEngineOptions> optionsAccessor,
CompilerCacheInitialiedService cacheInitializedService)
: base(assemblyProvider, loadContextAccessor, optionsAccessor)
: base(fileInfoCollection, loadContextAccessor, optionsAccessor)
{
cacheInitializedService.Initialized = true;
}