From 59db8143dae4eabff79d56dcd387839cfd0d3436 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 26 Jan 2015 18:03:28 -0800 Subject: [PATCH] Updating CompilerCache to use IMemoryCache and file system watcher to expire files in razor file cache. Add a functional test to ensure the compiler cache does not get initialized until the first request to a View. Fixes #1708 --- Mvc.sln | 41 +++++++--- .../Compilation/CompilerCache.cs | 80 ++++++++++++------- .../Compilation/CompilerCacheEntry.cs | 5 ++ .../Razor/PreCompileViews/RazorPreCompiler.cs | 7 +- .../PrecompilationTest.cs | 35 ++++++++ .../RazorCompilerCacheTest.cs | 48 +++++++++++ .../project.json | 1 + .../TestFileProvider.cs | 16 +++- .../TestFileTrigger.cs | 20 +++++ .../Compilation/CompilerCacheTest.cs | 6 +- .../project.json | 3 +- .../Controllers/HomeController.cs | 11 +++ .../Views/Home/Index.cshtml | 2 +- .../Views/Home/Layout.cshtml | 2 +- .../Views/Home/_ViewStart.cshtml | 3 +- .../Views/Test/Index.cshtml | 1 + .../Views/ViewStartDelete/Index.cshtml | 2 + .../Views/ViewStartDelete/_ViewStart.cshtml | 1 + .../CompilerCacheController.cs | 36 +++++++++ .../RazorCompilerCacheWebSite/Index.cshtml | 1 + .../RazorCompilerCacheWebSite.kproj | 20 +++++ .../CompilerCacheInitialiedService.cs | 10 +++ .../Services/CustomCompilerCache.cs | 19 +++++ .../RazorCompilerCacheWebSite/Startup.cs | 26 ++++++ .../RazorCompilerCacheWebSite/project.json | 19 +++++ .../wwwroot/readme.md | 1 + 26 files changed, 366 insertions(+), 50 deletions(-) create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/RazorCompilerCacheTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileTrigger.cs create mode 100644 test/WebSites/PrecompilationWebSite/Views/Test/Index.cshtml create mode 100644 test/WebSites/PrecompilationWebSite/Views/ViewStartDelete/Index.cshtml create mode 100644 test/WebSites/PrecompilationWebSite/Views/ViewStartDelete/_ViewStart.cshtml create mode 100644 test/WebSites/RazorCompilerCacheWebSite/CompilerCacheController.cs create mode 100644 test/WebSites/RazorCompilerCacheWebSite/Index.cshtml create mode 100644 test/WebSites/RazorCompilerCacheWebSite/RazorCompilerCacheWebSite.kproj create mode 100644 test/WebSites/RazorCompilerCacheWebSite/Services/CompilerCacheInitialiedService.cs create mode 100644 test/WebSites/RazorCompilerCacheWebSite/Services/CustomCompilerCache.cs create mode 100644 test/WebSites/RazorCompilerCacheWebSite/Startup.cs create mode 100644 test/WebSites/RazorCompilerCacheWebSite/project.json create mode 100644 test/WebSites/RazorCompilerCacheWebSite/wwwroot/readme.md diff --git a/Mvc.sln b/Mvc.sln index 2e42a3e926..87891aba29 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22512.0 +VisualStudioVersion = 14.0.22604.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -132,6 +132,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Xml.Te EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "FormatFilterWebSite", "test\WebSites\FormatFilterWebSite\FormatFilterWebSite.kproj", "{AC9BE567-540E-4C70-90C2-AAF021307A80}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "RazorCompilerCacheWebSite", "test\WebSites\RazorCompilerCacheWebSite\RazorCompilerCacheWebSite.kproj", "{42C5D417-4060-48F4-BB28-E9E179007779}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -694,18 +696,6 @@ Global {BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Release|Mixed Platforms.Build.0 = Release|Any CPU {BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Release|x86.ActiveCfg = Release|Any CPU {BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Release|x86.Build.0 = Release|Any CPU - {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|x86.ActiveCfg = Debug|Any CPU - {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|x86.Build.0 = Debug|Any CPU - {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|Any CPU.Build.0 = Release|Any CPU - {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|x86.ActiveCfg = Release|Any CPU - {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|x86.Build.0 = Release|Any CPU {0449D6D2-BE1B-4E29-8E1B-444420802C03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0449D6D2-BE1B-4E29-8E1B-444420802C03}.Debug|Any CPU.Build.0 = Debug|Any CPU {0449D6D2-BE1B-4E29-8E1B-444420802C03}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -730,6 +720,18 @@ Global {C3123A70-41C4-4122-AD1C-D35DF8958DD7}.Release|Mixed Platforms.Build.0 = Release|Any CPU {C3123A70-41C4-4122-AD1C-D35DF8958DD7}.Release|x86.ActiveCfg = Release|Any CPU {C3123A70-41C4-4122-AD1C-D35DF8958DD7}.Release|x86.Build.0 = Release|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|x86.ActiveCfg = Debug|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Debug|x86.Build.0 = Debug|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|Any CPU.Build.0 = Release|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|x86.ActiveCfg = Release|Any CPU + {87AB84B2-22C1-43C6-BB8A-1D327B446FB0}.Release|x86.Build.0 = Release|Any CPU {22019146-BDFA-442E-8C8E-345FB9644578}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {22019146-BDFA-442E-8C8E-345FB9644578}.Debug|Any CPU.Build.0 = Debug|Any CPU {22019146-BDFA-442E-8C8E-345FB9644578}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -754,6 +756,18 @@ Global {AC9BE567-540E-4C70-90C2-AAF021307A80}.Release|Mixed Platforms.Build.0 = Release|Any CPU {AC9BE567-540E-4C70-90C2-AAF021307A80}.Release|x86.ActiveCfg = Release|Any CPU {AC9BE567-540E-4C70-90C2-AAF021307A80}.Release|x86.Build.0 = Release|Any CPU + {42C5D417-4060-48F4-BB28-E9E179007779}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42C5D417-4060-48F4-BB28-E9E179007779}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42C5D417-4060-48F4-BB28-E9E179007779}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {42C5D417-4060-48F4-BB28-E9E179007779}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {42C5D417-4060-48F4-BB28-E9E179007779}.Debug|x86.ActiveCfg = Debug|Any CPU + {42C5D417-4060-48F4-BB28-E9E179007779}.Debug|x86.Build.0 = Debug|Any CPU + {42C5D417-4060-48F4-BB28-E9E179007779}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42C5D417-4060-48F4-BB28-E9E179007779}.Release|Any CPU.Build.0 = Release|Any CPU + {42C5D417-4060-48F4-BB28-E9E179007779}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {42C5D417-4060-48F4-BB28-E9E179007779}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {42C5D417-4060-48F4-BB28-E9E179007779}.Release|x86.ActiveCfg = Release|Any CPU + {42C5D417-4060-48F4-BB28-E9E179007779}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -818,5 +832,6 @@ Global {87AB84B2-22C1-43C6-BB8A-1D327B446FB0} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {22019146-BDFA-442E-8C8E-345FB9644578} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {AC9BE567-540E-4C70-90C2-AAF021307A80} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {42C5D417-4060-48F4-BB28-E9E179007779} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs index b11d3784fd..0c3ab8b8a6 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs @@ -2,19 +2,21 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; using Microsoft.AspNet.FileProviders; +using Microsoft.Framework.Cache.Memory; namespace Microsoft.AspNet.Mvc.Razor { - /// + /// + /// Caches the result of runtime compilation of Razor files for the duration of the app lifetime. + /// public class CompilerCache : ICompilerCache { - private readonly ConcurrentDictionary _cache; private readonly IFileProvider _fileProvider; + private readonly IMemoryCache _cache; /// /// Initializes a new instance of populated with precompiled views @@ -33,36 +35,39 @@ namespace Microsoft.AspNet.Mvc.Razor } // Internal for unit testing - internal CompilerCache(IEnumerable viewCollections, IFileProvider fileProvider) + internal CompilerCache(IEnumerable razorFileInfoCollection, + IFileProvider fileProvider) { _fileProvider = fileProvider; - _cache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var viewCollection in viewCollections) + _cache = new MemoryCache(new MemoryCacheOptions { ListenForMemoryPressure = false }); + var cacheEntries = new List(); + foreach (var viewCollection in razorFileInfoCollection) { + var containingAssembly = viewCollection.GetType().GetTypeInfo().Assembly; foreach (var fileInfo in viewCollection.FileInfos) { - var containingAssembly = viewCollection.GetType().GetTypeInfo().Assembly; 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. - _cache.TryAdd(NormalizePath(fileInfo.RelativePath), cacheEntry); + _cache.Set(NormalizePath(fileInfo.RelativePath), cacheEntry, PopulateCacheSetContext); + + cacheEntries.Add(cacheEntry); } } // Set up ViewStarts - foreach (var entry in _cache) + foreach (var entry in cacheEntries) { - var viewStartLocations = ViewStartUtility.GetViewStartLocations(entry.Key); + var viewStartLocations = ViewStartUtility.GetViewStartLocations(entry.RelativePath); foreach (var location in viewStartLocations) { - CompilerCacheEntry viewStartEntry; - if (_cache.TryGetValue(location, out viewStartEntry)) + var viewStartEntry = _cache.Get(location); + if (viewStartEntry != null) { // Add the the composite _ViewStart entry as a dependency. - entry.Value.AssociatedViewStartEntry = viewStartEntry; + entry.AssociatedViewStartEntry = viewStartEntry; break; } } @@ -105,14 +110,19 @@ namespace Microsoft.AspNet.Mvc.Razor Func compile, out CompilationResult result) { - CompilerCacheEntry cacheEntry; var normalizedPath = NormalizePath(relativeFileInfo.RelativePath); - if (!_cache.TryGetValue(normalizedPath, out cacheEntry)) + var cacheEntry = _cache.Get(normalizedPath); + if (cacheEntry == null) { return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result); } - else + 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 _ViewStart 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 = relativeFileInfo.FileInfo; if (cacheEntry.Length != fileInfo.Length) { @@ -129,6 +139,9 @@ namespace Microsoft.AspNet.Mvc.Razor if (cacheEntry.LastModified == fileInfo.LastModified) { result = CompilationResult.Successful(cacheEntry.CompiledType); + // 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 cacheEntry; } @@ -139,9 +152,10 @@ namespace Microsoft.AspNet.Mvc.Razor StringComparison.Ordinal)) { // Cache hit, but we need to update the entry. - // Assigning to LastModified is an atomic operation and will result in a safe race if it is - // being concurrently read and written or updated concurrently. + // Assigning to LastModified and IsValidatedPreCompiled are atomic operations and will result in safe races + // if the entry is being concurrently read or updated. cacheEntry.LastModified = fileInfo.LastModified; + cacheEntry.IsValidatedPreCompiled = true; result = CompilationResult.Successful(cacheEntry.CompiledType); return cacheEntry; @@ -150,6 +164,9 @@ namespace Microsoft.AspNet.Mvc.Razor // it's not a match, recompile return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result); } + + result = CompilationResult.Successful(cacheEntry.CompiledType); + return cacheEntry; } private CompilerCacheEntry OnCacheMiss(RelativeFileInfo file, @@ -159,15 +176,24 @@ namespace Microsoft.AspNet.Mvc.Razor { result = compile(file); - var cacheEntry = new CompilerCacheEntry(file, result.CompiledType) - { - AssociatedViewStartEntry = GetCompositeViewStartEntry(normalizedPath, compile) - }; + var cacheEntry = new CompilerCacheEntry(file, result.CompiledType); - // The cache is a concurrent dictionary, so concurrent addition to it with the same key would result in a - // safe race. - _cache[normalizedPath] = cacheEntry; - return cacheEntry; + // Concurrent addition to MemoryCache with the same key result in safe race. + return _cache.Set(normalizedPath, cacheEntry, PopulateCacheSetContext); + } + + private CompilerCacheEntry PopulateCacheSetContext(ICacheSetContext cacheSetContext) + { + var entry = (CompilerCacheEntry)cacheSetContext.State; + cacheSetContext.AddExpirationTrigger(_fileProvider.Watch(entry.RelativePath)); + + var viewStartLocations = ViewStartUtility.GetViewStartLocations(cacheSetContext.Key); + foreach (var location in viewStartLocations) + { + cacheSetContext.AddExpirationTrigger(_fileProvider.Watch(location)); + } + + return entry; } private bool AssociatedViewStartsChanged(CompilerCacheEntry entry, diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs index 598e23412a..5e51be5605 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs @@ -82,5 +82,10 @@ namespace Microsoft.AspNet.Mvc.Razor /// depends on. /// public CompilerCacheEntry AssociatedViewStartEntry { 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/Razor/PreCompileViews/RazorPreCompiler.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs index e4451ef060..326e6b5ad8 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs @@ -167,13 +167,16 @@ namespace Microsoft.AspNet.Mvc.Razor if (fullTypeName != null) { - var hash = RazorFileHash.GetHash(fileInfo.FileInfo, RazorFileHash.HashAlgorithmVersion1); + var hashAlgorithmVersion = RazorFileHash.HashAlgorithmVersion1; + var hash = RazorFileHash.GetHash(fileInfo.FileInfo, hashAlgorithmVersion); var razorFileInfo = new RazorFileInfo { RelativePath = fileInfo.RelativePath, LastModified = fileInfo.FileInfo.LastModified, Length = fileInfo.FileInfo.Length, - FullTypeName = fullTypeName + FullTypeName = fullTypeName, + Hash = hash, + HashAlgorithmVersion = hashAlgorithmVersion }; return new PrecompilationCacheEntry(razorFileInfo, syntaxTree); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/PrecompilationTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/PrecompilationTest.cs index 307fb7751e..b07461b97d 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/PrecompilationTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/PrecompilationTest.cs @@ -167,6 +167,41 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal(expected, responseContent.Trim()); } + [Fact] + public async Task DeletingPrecompiledViewStart_PriorToFirstRequestToAView_CausesViewToBeRecompiled() + { + // Arrange + var expected = typeof(Startup).GetTypeInfo().Assembly.GetName().ToString(); + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + var applicationEnvironment = _services.GetRequiredService(); + + var viewsDirectory = Path.Combine(applicationEnvironment.ApplicationBasePath, "Views", "ViewStartDelete"); + var viewStartPath = Path.Combine(viewsDirectory, "_ViewStart.cshtml"); + var viewStartContent = File.ReadAllText(viewStartPath); + + // 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 + var response2 = await client.GetStringAsync("http://localhost/Home/ViewStartDeletedPriorToFirstRequest"); + + // Assert - 2 + Assert.NotEqual(expected, response2.Trim()); + } + finally + { + File.WriteAllText(viewStartPath, viewStartContent); + } + } + private static Task TouchFile(string viewsDir, string file) { File.AppendAllText(Path.Combine(viewsDir, file), " "); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorCompilerCacheTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorCompilerCacheTest.cs new file mode 100644 index 0000000000..c7aa0c0159 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorCompilerCacheTest.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using RazorCompilerCacheWebSite; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class RazorCompilerCacheTest + { + private readonly IServiceProvider _provider = TestHelper.CreateServices(nameof(RazorCompilerCacheWebSite)); + private readonly Action _app = new Startup().Configure; + + [Fact] + public async Task CompilerCache_IsNotInitializedUntilFirstViewRequest() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + client.BaseAddress = new Uri("http://localhost"); + + // Act - 1 + // Visit a sampling of controller actions that do not produce ViewResult + var result1 = await client.GetAsync("/file"); + var result2 = await client.GetAsync("/statuscode"); + var result3 = await client.GetStringAsync("/cache-status"); + + // Assert - 1 + Assert.Equal(HttpStatusCode.OK, result1.StatusCode); + Assert.Equal(HttpStatusCode.OK, result2.StatusCode); + // Ensure the cache was not initialized. + Assert.Equal(bool.FalseString, result3); + + // Act - 2 + var result4 = await client.GetStringAsync("/view"); + var result5 = await client.GetStringAsync("/cache-status"); + + // Assert - 2 + Assert.Equal("Hello from view!", result4); + Assert.Equal(bool.TrueString, result5); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index 0d419bf356..b39ba2771f 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -26,6 +26,7 @@ "PrecompilationWebSite": "1.0.0", "RoutingWebSite": "1.0.0", "RazorWebSite": "1.0.0", + "RazorCompilerCacheWebSite": "1.0.0", "RazorInstrumentationWebsite": "1.0.0", "RazorViewEngineOptionsWebsite": "1.0.0", "RequestServicesWebSite": "1.0.0", diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileProvider.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileProvider.cs index 319be33af5..db6859de33 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileProvider.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileProvider.cs @@ -13,6 +13,8 @@ namespace Microsoft.AspNet.Mvc.Razor { private readonly Dictionary _lookup = new Dictionary(StringComparer.Ordinal); + private readonly Dictionary _fileTriggers = + new Dictionary(StringComparer.Ordinal); public IDirectoryContents GetDirectoryContents(string subpath) { @@ -56,7 +58,19 @@ namespace Microsoft.AspNet.Mvc.Razor public IExpirationTrigger Watch(string filter) { - throw new NotImplementedException(); + TestFileTrigger trigger; + if (!_fileTriggers.TryGetValue(filter, out trigger) || trigger.IsExpired) + { + trigger = new TestFileTrigger(); + _fileTriggers[filter] = trigger; + } + + return trigger; + } + + public TestFileTrigger GetTrigger(string filter) + { + return _fileTriggers[filter]; } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileTrigger.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileTrigger.cs new file mode 100644 index 0000000000..48ba37da23 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileTrigger.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Framework.Expiration.Interfaces; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class TestFileTrigger : IExpirationTrigger + { + public bool ActiveExpirationCallbacks { get; } = false; + + public bool IsExpired { get; set; } + + public IDisposable RegisterExpirationCallback(Action callback, object state) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs index 9a7be03ae3..9ff9e6aefb 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs @@ -262,7 +262,8 @@ namespace Microsoft.AspNet.Mvc.Razor Assert.Equal(typeof(PreCompile), actual1.CompiledType); // Act 2 - fileProvider.AddFile("Views\\_ViewStart.cshtml", ""); + var viewStartTrigger = fileProvider.GetTrigger("Views\\_ViewStart.cshtml"); + viewStartTrigger.IsExpired = true; var actual2 = cache.GetOrAdd(relativeFile, compile: _ => CompilationResult.Successful(expectedType)); @@ -318,7 +319,8 @@ namespace Microsoft.AspNet.Mvc.Razor Assert.Equal(typeof(PreCompile), actual1.CompiledType); // Act 2 - fileProvider.DeleteFile(viewStartFileInfo.PhysicalPath); + var trigger = fileProvider.GetTrigger(viewStartFileInfo.PhysicalPath); + trigger.IsExpired = true; var actual2 = cache.GetOrAdd(fileInfo, compile: _ => CompilationResult.Successful(expectedType)); diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/project.json b/test/Microsoft.AspNet.Mvc.Razor.Test/project.json index 707efbf704..e24804ba00 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/project.json +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/project.json @@ -2,7 +2,8 @@ "code": [ "**/*.cs", "../Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileProvider.cs", - "../Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileInfo.cs" + "../Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileInfo.cs", + "../Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileTrigger.cs" ], "dependencies": { "Microsoft.AspNet.Mvc.Razor": "6.0.0-*", diff --git a/test/WebSites/PrecompilationWebSite/Controllers/HomeController.cs b/test/WebSites/PrecompilationWebSite/Controllers/HomeController.cs index 7fca420d2e..1dba8f1f28 100644 --- a/test/WebSites/PrecompilationWebSite/Controllers/HomeController.cs +++ b/test/WebSites/PrecompilationWebSite/Controllers/HomeController.cs @@ -16,5 +16,16 @@ namespace PrecompilationWebSite.Controllers { return View("~/Views/ViewsConsumingCompilationOptions/Index"); } + + public IActionResult ViewStartDeletedPriorToFirstRequest() + { + return View("~/Views/ViewStartDelete/Index"); + } + + [HttpGet("/Test")] + public IActionResult TestView() + { + return View("~/Views/Test/Index"); + } } } diff --git a/test/WebSites/PrecompilationWebSite/Views/Home/Index.cshtml b/test/WebSites/PrecompilationWebSite/Views/Home/Index.cshtml index 679f69ca75..77a9e96d2e 100644 --- a/test/WebSites/PrecompilationWebSite/Views/Home/Index.cshtml +++ b/test/WebSites/PrecompilationWebSite/Views/Home/Index.cshtml @@ -1 +1 @@ -index:@GetType().GetTypeInfo().Assembly.GetName() \ No newline at end of file +index:@GetType().GetTypeInfo().Assembly.GetName() \ No newline at end of file diff --git a/test/WebSites/PrecompilationWebSite/Views/Home/Layout.cshtml b/test/WebSites/PrecompilationWebSite/Views/Home/Layout.cshtml index 1a03bb0226..c068404da0 100644 --- a/test/WebSites/PrecompilationWebSite/Views/Home/Layout.cshtml +++ b/test/WebSites/PrecompilationWebSite/Views/Home/Layout.cshtml @@ -1,2 +1,2 @@ Layout:@GetType().GetTypeInfo().Assembly.FullName -@RenderBody() \ No newline at end of file +@RenderBody() \ No newline at end of file diff --git a/test/WebSites/PrecompilationWebSite/Views/Home/_ViewStart.cshtml b/test/WebSites/PrecompilationWebSite/Views/Home/_ViewStart.cshtml index 71a6696b86..984c6ea0eb 100644 --- a/test/WebSites/PrecompilationWebSite/Views/Home/_ViewStart.cshtml +++ b/test/WebSites/PrecompilationWebSite/Views/Home/_ViewStart.cshtml @@ -1,4 +1,3 @@ @using System.Reflection -@{ Layout = "/views/Home/Layout.cshtml";} +@{ Layout = "/Views/Home/Layout.cshtml";} _viewstart:@GetType().GetTypeInfo().Assembly.FullName - \ No newline at end of file diff --git a/test/WebSites/PrecompilationWebSite/Views/Test/Index.cshtml b/test/WebSites/PrecompilationWebSite/Views/Test/Index.cshtml new file mode 100644 index 0000000000..6d5e026376 --- /dev/null +++ b/test/WebSites/PrecompilationWebSite/Views/Test/Index.cshtml @@ -0,0 +1 @@ +Test \ No newline at end of file diff --git a/test/WebSites/PrecompilationWebSite/Views/ViewStartDelete/Index.cshtml b/test/WebSites/PrecompilationWebSite/Views/ViewStartDelete/Index.cshtml new file mode 100644 index 0000000000..ab695678b9 --- /dev/null +++ b/test/WebSites/PrecompilationWebSite/Views/ViewStartDelete/Index.cshtml @@ -0,0 +1,2 @@ +@using System.Reflection; +@GetType().GetTypeInfo().Assembly.FullName \ No newline at end of file diff --git a/test/WebSites/PrecompilationWebSite/Views/ViewStartDelete/_ViewStart.cshtml b/test/WebSites/PrecompilationWebSite/Views/ViewStartDelete/_ViewStart.cshtml new file mode 100644 index 0000000000..c7989b8b7a --- /dev/null +++ b/test/WebSites/PrecompilationWebSite/Views/ViewStartDelete/_ViewStart.cshtml @@ -0,0 +1 @@ +_ViewStart content \ No newline at end of file diff --git a/test/WebSites/RazorCompilerCacheWebSite/CompilerCacheController.cs b/test/WebSites/RazorCompilerCacheWebSite/CompilerCacheController.cs new file mode 100644 index 0000000000..aa5c44f9b8 --- /dev/null +++ b/test/WebSites/RazorCompilerCacheWebSite/CompilerCacheController.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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; + +namespace RazorCompilerCacheWebSite +{ + public class CompilerCacheController : Controller + { + [HttpGet("/cache-status")] + [Produces("text/plain")] + public ContentResult GetCompilerCacheInitializationStatus( + [FromServices] CompilerCacheInitialiedService service) + { + return Content(service.Initialized.ToString()); + } + + [HttpGet("/statuscode")] + public IActionResult StatusCodeAction() + { + return new EmptyResult(); + } + + [HttpGet("/file")] + public IActionResult FileAction() + { + return File("readme.md", "application/text"); + } + + [HttpGet("/view")] + public ViewResult Index() + { + return View("~/Index"); + } + } +} diff --git a/test/WebSites/RazorCompilerCacheWebSite/Index.cshtml b/test/WebSites/RazorCompilerCacheWebSite/Index.cshtml new file mode 100644 index 0000000000..b83b16fcf7 --- /dev/null +++ b/test/WebSites/RazorCompilerCacheWebSite/Index.cshtml @@ -0,0 +1 @@ +Hello from view! \ No newline at end of file diff --git a/test/WebSites/RazorCompilerCacheWebSite/RazorCompilerCacheWebSite.kproj b/test/WebSites/RazorCompilerCacheWebSite/RazorCompilerCacheWebSite.kproj new file mode 100644 index 0000000000..120a5a7809 --- /dev/null +++ b/test/WebSites/RazorCompilerCacheWebSite/RazorCompilerCacheWebSite.kproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 42c5d417-4060-48f4-bb28-e9e179007779 + + + + + + + 2.0 + 42473 + + + \ No newline at end of file diff --git a/test/WebSites/RazorCompilerCacheWebSite/Services/CompilerCacheInitialiedService.cs b/test/WebSites/RazorCompilerCacheWebSite/Services/CompilerCacheInitialiedService.cs new file mode 100644 index 0000000000..ded4ec6ae0 --- /dev/null +++ b/test/WebSites/RazorCompilerCacheWebSite/Services/CompilerCacheInitialiedService.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace RazorCompilerCacheWebSite +{ + public class CompilerCacheInitialiedService + { + public bool Initialized { get; set; } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorCompilerCacheWebSite/Services/CustomCompilerCache.cs b/test/WebSites/RazorCompilerCacheWebSite/Services/CustomCompilerCache.cs new file mode 100644 index 0000000000..21f9df3873 --- /dev/null +++ b/test/WebSites/RazorCompilerCacheWebSite/Services/CustomCompilerCache.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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 Microsoft.AspNet.Mvc.Razor; + +namespace RazorCompilerCacheWebSite +{ + public class CustomCompilerCache : CompilerCache + { + public CustomCompilerCache(IAssemblyProvider assemblyProvider, + IRazorFileProviderCache fileProvider, + CompilerCacheInitialiedService cacheInitializedService) + : base(assemblyProvider, fileProvider) + { + cacheInitializedService.Initialized = true; + } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorCompilerCacheWebSite/Startup.cs b/test/WebSites/RazorCompilerCacheWebSite/Startup.cs new file mode 100644 index 0000000000..cdff69443f --- /dev/null +++ b/test/WebSites/RazorCompilerCacheWebSite/Startup.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Mvc.Razor; +using Microsoft.Framework.DependencyInjection; + +namespace RazorCompilerCacheWebSite +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + var configuration = app.GetTestConfiguration(); + + app.UseServices(services => + { + services.AddMvc(configuration); + services.AddSingleton(); + services.AddSingleton(); + }); + + app.UseMvc(); + } + } +} diff --git a/test/WebSites/RazorCompilerCacheWebSite/project.json b/test/WebSites/RazorCompilerCacheWebSite/project.json new file mode 100644 index 0000000000..c582a1382f --- /dev/null +++ b/test/WebSites/RazorCompilerCacheWebSite/project.json @@ -0,0 +1,19 @@ +{ + "commands": { + "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:5001", + "kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5000" + }, + "dependencies": { + "Kestrel": "1.0.0-*", + "Microsoft.AspNet.Mvc": "6.0.0-*", + "Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.AspNet.StaticFiles": "1.0.0-*" + }, + "frameworks": { + "aspnet50": { }, + "aspnetcore50": { } + }, + "webroot": "wwwroot" +} diff --git a/test/WebSites/RazorCompilerCacheWebSite/wwwroot/readme.md b/test/WebSites/RazorCompilerCacheWebSite/wwwroot/readme.md new file mode 100644 index 0000000000..0927feefdb --- /dev/null +++ b/test/WebSites/RazorCompilerCacheWebSite/wwwroot/readme.md @@ -0,0 +1 @@ +A functional test website to verify that the Razor compiler cache gets lazily initialized. \ No newline at end of file