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
This commit is contained in:
Pranav K 2015-01-26 18:03:28 -08:00
parent 19ca77ac2d
commit 59db8143da
26 changed files with 366 additions and 50 deletions

41
Mvc.sln
View File

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14 # Visual Studio 14
VisualStudioVersion = 14.0.22512.0 VisualStudioVersion = 14.0.22604.0
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject EndProject
@ -132,6 +132,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Xml.Te
EndProject EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "FormatFilterWebSite", "test\WebSites\FormatFilterWebSite\FormatFilterWebSite.kproj", "{AC9BE567-540E-4C70-90C2-AAF021307A80}" Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "FormatFilterWebSite", "test\WebSites\FormatFilterWebSite\FormatFilterWebSite.kproj", "{AC9BE567-540E-4C70-90C2-AAF021307A80}"
EndProject EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "RazorCompilerCacheWebSite", "test\WebSites\RazorCompilerCacheWebSite\RazorCompilerCacheWebSite.kproj", "{42C5D417-4060-48F4-BB28-E9E179007779}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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|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.ActiveCfg = Release|Any CPU
{BDEEBE09-C0C4-433C-B0B8-8478C9776996}.Release|x86.Build.0 = 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.ActiveCfg = Debug|Any CPU
{0449D6D2-BE1B-4E29-8E1B-444420802C03}.Debug|Any CPU.Build.0 = 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 {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|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.ActiveCfg = Release|Any CPU
{C3123A70-41C4-4122-AD1C-D35DF8958DD7}.Release|x86.Build.0 = 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.ActiveCfg = Debug|Any CPU
{22019146-BDFA-442E-8C8E-345FB9644578}.Debug|Any CPU.Build.0 = 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 {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|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.ActiveCfg = Release|Any CPU
{AC9BE567-540E-4C70-90C2-AAF021307A80}.Release|x86.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -818,5 +832,6 @@ Global
{87AB84B2-22C1-43C6-BB8A-1D327B446FB0} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {87AB84B2-22C1-43C6-BB8A-1D327B446FB0} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{22019146-BDFA-442E-8C8E-345FB9644578} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {22019146-BDFA-442E-8C8E-345FB9644578} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{AC9BE567-540E-4C70-90C2-AAF021307A80} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {AC9BE567-540E-4C70-90C2-AAF021307A80} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{42C5D417-4060-48F4-BB28-E9E179007779} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -2,19 +2,21 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.FileProviders;
using Microsoft.Framework.Cache.Memory;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
/// <inheritdoc /> /// <summary>
/// Caches the result of runtime compilation of Razor files for the duration of the app lifetime.
/// </summary>
public class CompilerCache : ICompilerCache public class CompilerCache : ICompilerCache
{ {
private readonly ConcurrentDictionary<string, CompilerCacheEntry> _cache;
private readonly IFileProvider _fileProvider; private readonly IFileProvider _fileProvider;
private readonly IMemoryCache _cache;
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="CompilerCache"/> populated with precompiled views /// Initializes a new instance of <see cref="CompilerCache"/> populated with precompiled views
@ -33,36 +35,39 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
// Internal for unit testing // Internal for unit testing
internal CompilerCache(IEnumerable<RazorFileInfoCollection> viewCollections, IFileProvider fileProvider) internal CompilerCache(IEnumerable<RazorFileInfoCollection> razorFileInfoCollection,
IFileProvider fileProvider)
{ {
_fileProvider = fileProvider; _fileProvider = fileProvider;
_cache = new ConcurrentDictionary<string, CompilerCacheEntry>(StringComparer.OrdinalIgnoreCase); _cache = new MemoryCache(new MemoryCacheOptions { ListenForMemoryPressure = false });
var cacheEntries = new List<CompilerCacheEntry>();
foreach (var viewCollection in viewCollections) foreach (var viewCollection in razorFileInfoCollection)
{ {
var containingAssembly = viewCollection.GetType().GetTypeInfo().Assembly;
foreach (var fileInfo in viewCollection.FileInfos) foreach (var fileInfo in viewCollection.FileInfos)
{ {
var containingAssembly = viewCollection.GetType().GetTypeInfo().Assembly;
var viewType = containingAssembly.GetType(fileInfo.FullTypeName); var viewType = containingAssembly.GetType(fileInfo.FullTypeName);
var cacheEntry = new CompilerCacheEntry(fileInfo, viewType); var cacheEntry = new CompilerCacheEntry(fileInfo, viewType);
// There shouldn't be any duplicates and if there are any the first will win. // 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. // 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 // 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) foreach (var location in viewStartLocations)
{ {
CompilerCacheEntry viewStartEntry; var viewStartEntry = _cache.Get<CompilerCacheEntry>(location);
if (_cache.TryGetValue(location, out viewStartEntry)) if (viewStartEntry != null)
{ {
// Add the the composite _ViewStart entry as a dependency. // Add the the composite _ViewStart entry as a dependency.
entry.Value.AssociatedViewStartEntry = viewStartEntry; entry.AssociatedViewStartEntry = viewStartEntry;
break; break;
} }
} }
@ -105,14 +110,19 @@ namespace Microsoft.AspNet.Mvc.Razor
Func<RelativeFileInfo, CompilationResult> compile, Func<RelativeFileInfo, CompilationResult> compile,
out CompilationResult result) out CompilationResult result)
{ {
CompilerCacheEntry cacheEntry;
var normalizedPath = NormalizePath(relativeFileInfo.RelativePath); var normalizedPath = NormalizePath(relativeFileInfo.RelativePath);
if (!_cache.TryGetValue(normalizedPath, out cacheEntry)) var cacheEntry = _cache.Get<CompilerCacheEntry>(normalizedPath);
if (cacheEntry == null)
{ {
return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result); 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; var fileInfo = relativeFileInfo.FileInfo;
if (cacheEntry.Length != fileInfo.Length) if (cacheEntry.Length != fileInfo.Length)
{ {
@ -129,6 +139,9 @@ namespace Microsoft.AspNet.Mvc.Razor
if (cacheEntry.LastModified == fileInfo.LastModified) if (cacheEntry.LastModified == fileInfo.LastModified)
{ {
result = CompilationResult.Successful(cacheEntry.CompiledType); 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; return cacheEntry;
} }
@ -139,9 +152,10 @@ namespace Microsoft.AspNet.Mvc.Razor
StringComparison.Ordinal)) StringComparison.Ordinal))
{ {
// Cache hit, but we need to update the entry. // 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 // Assigning to LastModified and IsValidatedPreCompiled are atomic operations and will result in safe races
// being concurrently read and written or updated concurrently. // if the entry is being concurrently read or updated.
cacheEntry.LastModified = fileInfo.LastModified; cacheEntry.LastModified = fileInfo.LastModified;
cacheEntry.IsValidatedPreCompiled = true;
result = CompilationResult.Successful(cacheEntry.CompiledType); result = CompilationResult.Successful(cacheEntry.CompiledType);
return cacheEntry; return cacheEntry;
@ -150,6 +164,9 @@ namespace Microsoft.AspNet.Mvc.Razor
// it's not a match, recompile // it's not a match, recompile
return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result); return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result);
} }
result = CompilationResult.Successful(cacheEntry.CompiledType);
return cacheEntry;
} }
private CompilerCacheEntry OnCacheMiss(RelativeFileInfo file, private CompilerCacheEntry OnCacheMiss(RelativeFileInfo file,
@ -159,15 +176,24 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
result = compile(file); result = compile(file);
var cacheEntry = new CompilerCacheEntry(file, result.CompiledType) var cacheEntry = new CompilerCacheEntry(file, result.CompiledType);
{
AssociatedViewStartEntry = GetCompositeViewStartEntry(normalizedPath, compile)
};
// The cache is a concurrent dictionary, so concurrent addition to it with the same key would result in a // Concurrent addition to MemoryCache with the same key result in safe race.
// safe race. return _cache.Set(normalizedPath, cacheEntry, PopulateCacheSetContext);
_cache[normalizedPath] = cacheEntry; }
return cacheEntry;
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, private bool AssociatedViewStartsChanged(CompilerCacheEntry entry,

View File

@ -82,5 +82,10 @@ namespace Microsoft.AspNet.Mvc.Razor
/// depends on. /// depends on.
/// </summary> /// </summary>
public CompilerCacheEntry AssociatedViewStartEntry { get; set; } public CompilerCacheEntry AssociatedViewStartEntry { 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

@ -167,13 +167,16 @@ namespace Microsoft.AspNet.Mvc.Razor
if (fullTypeName != null) 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 var razorFileInfo = new RazorFileInfo
{ {
RelativePath = fileInfo.RelativePath, RelativePath = fileInfo.RelativePath,
LastModified = fileInfo.FileInfo.LastModified, LastModified = fileInfo.FileInfo.LastModified,
Length = fileInfo.FileInfo.Length, Length = fileInfo.FileInfo.Length,
FullTypeName = fullTypeName FullTypeName = fullTypeName,
Hash = hash,
HashAlgorithmVersion = hashAlgorithmVersion
}; };
return new PrecompilationCacheEntry(razorFileInfo, syntaxTree); return new PrecompilationCacheEntry(razorFileInfo, syntaxTree);

View File

@ -167,6 +167,41 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(expected, responseContent.Trim()); 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<IApplicationEnvironment>();
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) private static Task TouchFile(string viewsDir, string file)
{ {
File.AppendAllText(Path.Combine(viewsDir, file), " "); File.AppendAllText(Path.Combine(viewsDir, file), " ");

View File

@ -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<IApplicationBuilder> _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);
}
}
}

View File

@ -26,6 +26,7 @@
"PrecompilationWebSite": "1.0.0", "PrecompilationWebSite": "1.0.0",
"RoutingWebSite": "1.0.0", "RoutingWebSite": "1.0.0",
"RazorWebSite": "1.0.0", "RazorWebSite": "1.0.0",
"RazorCompilerCacheWebSite": "1.0.0",
"RazorInstrumentationWebsite": "1.0.0", "RazorInstrumentationWebsite": "1.0.0",
"RazorViewEngineOptionsWebsite": "1.0.0", "RazorViewEngineOptionsWebsite": "1.0.0",
"RequestServicesWebSite": "1.0.0", "RequestServicesWebSite": "1.0.0",

View File

@ -13,6 +13,8 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
private readonly Dictionary<string, IFileInfo> _lookup = private readonly Dictionary<string, IFileInfo> _lookup =
new Dictionary<string, IFileInfo>(StringComparer.Ordinal); new Dictionary<string, IFileInfo>(StringComparer.Ordinal);
private readonly Dictionary<string, TestFileTrigger> _fileTriggers =
new Dictionary<string, TestFileTrigger>(StringComparer.Ordinal);
public IDirectoryContents GetDirectoryContents(string subpath) public IDirectoryContents GetDirectoryContents(string subpath)
{ {
@ -56,7 +58,19 @@ namespace Microsoft.AspNet.Mvc.Razor
public IExpirationTrigger Watch(string filter) 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];
} }
} }
} }

View File

@ -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<object> callback, object state)
{
throw new NotImplementedException();
}
}
}

View File

@ -262,7 +262,8 @@ namespace Microsoft.AspNet.Mvc.Razor
Assert.Equal(typeof(PreCompile), actual1.CompiledType); Assert.Equal(typeof(PreCompile), actual1.CompiledType);
// Act 2 // Act 2
fileProvider.AddFile("Views\\_ViewStart.cshtml", ""); var viewStartTrigger = fileProvider.GetTrigger("Views\\_ViewStart.cshtml");
viewStartTrigger.IsExpired = true;
var actual2 = cache.GetOrAdd(relativeFile, var actual2 = cache.GetOrAdd(relativeFile,
compile: _ => CompilationResult.Successful(expectedType)); compile: _ => CompilationResult.Successful(expectedType));
@ -318,7 +319,8 @@ namespace Microsoft.AspNet.Mvc.Razor
Assert.Equal(typeof(PreCompile), actual1.CompiledType); Assert.Equal(typeof(PreCompile), actual1.CompiledType);
// Act 2 // Act 2
fileProvider.DeleteFile(viewStartFileInfo.PhysicalPath); var trigger = fileProvider.GetTrigger(viewStartFileInfo.PhysicalPath);
trigger.IsExpired = true;
var actual2 = cache.GetOrAdd(fileInfo, var actual2 = cache.GetOrAdd(fileInfo,
compile: _ => CompilationResult.Successful(expectedType)); compile: _ => CompilationResult.Successful(expectedType));

View File

@ -2,7 +2,8 @@
"code": [ "code": [
"**/*.cs", "**/*.cs",
"../Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileProvider.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": { "dependencies": {
"Microsoft.AspNet.Mvc.Razor": "6.0.0-*", "Microsoft.AspNet.Mvc.Razor": "6.0.0-*",

View File

@ -16,5 +16,16 @@ namespace PrecompilationWebSite.Controllers
{ {
return View("~/Views/ViewsConsumingCompilationOptions/Index"); return View("~/Views/ViewsConsumingCompilationOptions/Index");
} }
public IActionResult ViewStartDeletedPriorToFirstRequest()
{
return View("~/Views/ViewStartDelete/Index");
}
[HttpGet("/Test")]
public IActionResult TestView()
{
return View("~/Views/Test/Index");
}
} }
} }

View File

@ -1 +1 @@
index:@GetType().GetTypeInfo().Assembly.GetName() index:@GetType().GetTypeInfo().Assembly.GetName()

View File

@ -1,2 +1,2 @@
Layout:@GetType().GetTypeInfo().Assembly.FullName Layout:@GetType().GetTypeInfo().Assembly.FullName
@RenderBody() @RenderBody()

View File

@ -1,4 +1,3 @@
@using System.Reflection @using System.Reflection
@{ Layout = "/views/Home/Layout.cshtml";} @{ Layout = "/Views/Home/Layout.cshtml";}
_viewstart:@GetType().GetTypeInfo().Assembly.FullName _viewstart:@GetType().GetTypeInfo().Assembly.FullName

View File

@ -0,0 +1 @@
Test

View File

@ -0,0 +1,2 @@
@using System.Reflection;
@GetType().GetTypeInfo().Assembly.FullName

View File

@ -0,0 +1 @@
_ViewStart content

View File

@ -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");
}
}
}

View File

@ -0,0 +1 @@
Hello from view!

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="__ToolsVersion__" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>42c5d417-4060-48f4-bb28-e9e179007779</ProjectGuid>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>42473</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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<ICompilerCache, CustomCompilerCache>();
services.AddSingleton<CompilerCacheInitialiedService>();
});
app.UseMvc();
}
}
}

View File

@ -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"
}

View File

@ -0,0 +1 @@
A functional test website to verify that the Razor compiler cache gets lazily initialized.