diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs index 966b9effca..2220c1fe58 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs @@ -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 { /// - /// 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. /// 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 populated with precompiled views /// discovered using . /// - /// The that provides assemblies - /// for precompiled view discovery. + /// The sequence of that provides + /// information for precompiled view discovery. /// The . /// An accessor to the . - public CompilerCache(IAssemblyProvider assemblyProvider, - IAssemblyLoadContextAccessor loadContextAccessor, - IOptions optionsAccessor) - : this(GetFileInfos(assemblyProvider.CandidateAssemblies), - loadContextAccessor.GetLoadContext(RazorFileInfoCollectionType.Assembly), + public CompilerCache( + IEnumerable razorFileInfoCollections, + IAssemblyLoadContextAccessor loadContextAccessor, + IOptions optionsAccessor) + : this(razorFileInfoCollections, + loadContextAccessor.GetLoadContext(RazorHostAssembly), optionsAccessor.Options.FileProvider) { } - internal CompilerCache(IEnumerable razorFileInfoCollections, - IAssemblyLoadContext loadContext, - IFileProvider fileProvider) + internal CompilerCache( + IEnumerable razorFileInfoCollections, + IAssemblyLoadContext loadContext, + IFileProvider fileProvider) { _fileProvider = fileProvider; _cache = new MemoryCache(new MemoryCacheOptions { CompactOnMemoryPressure = false }); - var cacheEntries = new List(); 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(location); - if (globalFileEntry != null) - { - // Add the composite _ViewImports entry as a dependency. - entry.AssociatedGlobalFileEntry = globalFileEntry; - break; - } + _cache.Set(normalizedPath, cacheEntry); } } } /// - public CompilerCacheResult GetOrAdd([NotNull] string relativePath, - [NotNull] Func compile) - { - var result = GetOrAddCore(relativePath, compile); - if (result == null) - { - return CompilerCacheResult.FileNotFound; - } - - return new CompilerCacheResult(result.CompilationResult); - } - - private GetOrAddResult GetOrAddCore(string relativePath, - Func compile) + public CompilerCacheResult GetOrAdd( + [NotNull] string relativePath, + [NotNull] Func compile) { var normalizedPath = NormalizePath(relativePath); - var cacheEntry = _cache.Get(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 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( - 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 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 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 GetFileInfos(IEnumerable 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; } - } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs deleted file mode 100644 index dac6e16a70..0000000000 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs +++ /dev/null @@ -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 -{ - /// - /// An entry in that contain metadata about precompiled and dynamically compiled file. - /// - public class CompilerCacheEntry - { - /// - /// Initializes a new instance of for a file that was precompiled. - /// - /// Metadata about the precompiled file. - /// The compiled . - 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; - } - - /// - /// Initializes a new instance of for a file that was dynamically compiled. - /// - /// Metadata about the file that was compiled. - /// The compiled . - public CompilerCacheEntry([NotNull] RelativeFileInfo info, [NotNull] Type compiledType) - { - CompiledType = compiledType; - RelativePath = info.RelativePath; - Length = info.FileInfo.Length; - LastModified = info.FileInfo.LastModified; - } - - /// - /// Gets the produced as a result of compilation. - /// - public Type CompiledType { get; } - - /// - /// Gets the path of the compiled file relative to the root of the application. - /// - public string RelativePath { get; } - - /// - /// Gets the size of file (in bytes) on disk. - /// - public long Length { get; } - - /// - /// Gets or sets the last modified for the file at the time of compilation. - /// - public DateTimeOffset LastModified { get; set; } - - /// - /// Gets the file hash, should only be available for pre compiled files. - /// - public string Hash { get; } - - /// - /// Gets the version of the hash algorithm used to generate . - /// - /// - /// This value is only initialized for precompiled views. - /// - public int HashAlgorithmVersion { get; } - - /// - /// Gets a flag that indicates if the file is precompiled. - /// - public bool IsPreCompiled { get; } - - /// - /// Gets or sets the for the nearest _ViewImports that the compiled type - /// depends on. - /// - public CompilerCacheEntry AssociatedGlobalFileEntry { get; set; } - - /// - /// Gets or sets a flag that determines if the validity of this cache entry was performed at runtime. - /// - public bool IsValidatedPreCompiled { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Razor/DependencyInjection/MvcRazorMvcBuilderExtensions.cs b/src/Microsoft.AspNet.Mvc.Razor/DependencyInjection/MvcRazorMvcBuilderExtensions.cs index 7124572613..10fb5efc11 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/DependencyInjection/MvcRazorMvcBuilderExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/DependencyInjection/MvcRazorMvcBuilderExtensions.cs @@ -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 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(); } + + private static IEnumerable GetFileInfoCollections(IEnumerable assemblies) => + assemblies + .SelectMany(assembly => assembly.ExportedTypes) + .Where(IsValidRazorFileInfoCollection) + .Select(Activator.CreateInstance) + .Cast(); + + internal static bool IsValidRazorFileInfoCollection(Type type) => + RazorFileInfoCollectionType.IsAssignableFrom(type) && + !type.GetTypeInfo().IsAbstract && + !type.GetTypeInfo().ContainsGenericParameters; } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Precompilation/RazorFileInfoCollection.cs b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/RazorFileInfoCollection.cs index 3ff7e92ca8..438d1d9afa 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Precompilation/RazorFileInfoCollection.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Precompilation/RazorFileInfoCollection.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Precompilation public string SymbolsResourceName { get; protected set; } /// - /// Gets the of s. + /// Gets the of s. /// public IReadOnlyList FileInfos { get; protected set; } diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/PrecompilationTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/PrecompilationTest.cs index 23ea26a4c1..c3ded05252 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/PrecompilationTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/PrecompilationTest.cs @@ -41,10 +41,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var applicationEnvironment = serviceProvider.GetRequiredService(); 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(); - - 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() diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs index c500050d1d..fc1388ffbd 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs @@ -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(); + public static TheoryData ViewImportsPaths => + new TheoryData + { + 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(), 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(), 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(), 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(), 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(), 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(), 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 { 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 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 { - 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(), 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(result1.CompilationResult); - var actual2 = Assert.IsType(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 : RazorFileInfoCollection - { - - } - - private class ParameterConstructorRazorFileInfoCollection : RazorFileInfoCollection - { - public ParameterConstructorRazorFileInfoCollection(string value) - { - } - } - - private class NonSubTypeRazorFileInfoCollection : Controller - { - - } - - private class ViewCollection : RazorFileInfoCollection - { - private readonly List _fileInfos = new List(); - - 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; } } } diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/DependencyInjection/MvcRazorMvcBuilderExtensionsTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/DependencyInjection/MvcRazorMvcBuilderExtensionsTest.cs new file mode 100644 index 0000000000..46c33cff8d --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/DependencyInjection/MvcRazorMvcBuilderExtensionsTest.cs @@ -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 : RazorFileInfoCollection + { + } + + private class ParameterConstructorRazorFileInfoCollection : RazorFileInfoCollection + { + public ParameterConstructorRazorFileInfoCollection(string value) + { + } + } + + private class NonSubTypeRazorFileInfoCollection : Controller + { + } + + private class ViewCollection : RazorFileInfoCollection + { + } + } +} diff --git a/test/WebSites/PrecompilationWebSite/Startup.cs b/test/WebSites/PrecompilationWebSite/Startup.cs index ef3ad786c1..001e659e9f 100644 --- a/test/WebSites/PrecompilationWebSite/Startup.cs +++ b/test/WebSites/PrecompilationWebSite/Startup.cs @@ -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) diff --git a/test/WebSites/RazorCompilerCacheWebSite/Services/CustomCompilerCache.cs b/test/WebSites/RazorCompilerCacheWebSite/Services/CustomCompilerCache.cs index b3a77cfb06..701f418cb6 100644 --- a/test/WebSites/RazorCompilerCacheWebSite/Services/CustomCompilerCache.cs +++ b/test/WebSites/RazorCompilerCacheWebSite/Services/CustomCompilerCache.cs @@ -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 fileInfoCollection, IAssemblyLoadContextAccessor loadContextAccessor, IOptions optionsAccessor, CompilerCacheInitialiedService cacheInitializedService) - : base(assemblyProvider, loadContextAccessor, optionsAccessor) + : base(fileInfoCollection, loadContextAccessor, optionsAccessor) { cacheInitializedService.Initialized = true; }