Remove IRazorFileProviderCache and rely on file expiration triggers to

file change expiry.

Fixes #1969
This commit is contained in:
Pranav K 2015-02-06 05:31:11 -08:00
parent 4eb9c777ff
commit 6ef5518f8a
13 changed files with 376 additions and 706 deletions

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Reflection;
using Microsoft.AspNet.FileProviders;
using Microsoft.Framework.Cache.Memory;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc.Razor
{
@ -26,11 +27,10 @@ namespace Microsoft.AspNet.Mvc.Razor
/// An <see cref="IAssemblyProvider"/> representing the assemblies
/// used to search for pre-compiled views.
/// </param>
/// <param name="fileProvider">An <see cref="IRazorFileProviderCache"/> instance that represents the application's
/// file system.
/// </param>
public CompilerCache(IAssemblyProvider provider, IRazorFileProviderCache fileProvider)
: this(GetFileInfos(provider.CandidateAssemblies), fileProvider)
/// <param name="optionsAccessor">An accessor to the <see cref="RazorViewEngineOptions"/>.</param>
public CompilerCache(IAssemblyProvider provider,
IOptions<RazorViewEngineOptions> optionsAccessor)
: this(GetFileInfos(provider.CandidateAssemblies), optionsAccessor.Options.FileProvider)
{
}
@ -74,47 +74,34 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
internal static IEnumerable<RazorFileInfoCollection>
GetFileInfos(IEnumerable<Assembly> assemblies)
/// <inheritdoc />
public CompilerCacheResult GetOrAdd([NotNull] string relativePath,
[NotNull] Func<RelativeFileInfo, CompilationResult> compile)
{
return assemblies.SelectMany(a => a.ExportedTypes)
.Where(Match)
.Select(c => (RazorFileInfoCollection)Activator.CreateInstance(c));
}
private static bool Match(Type t)
{
var inAssemblyType = typeof(RazorFileInfoCollection);
if (inAssemblyType.IsAssignableFrom(t))
var result = GetOrAddCore(relativePath, compile);
if (result == null)
{
var hasParameterlessConstructor = t.GetConstructor(Type.EmptyTypes) != null;
return hasParameterlessConstructor
&& !t.GetTypeInfo().IsAbstract
&& !t.GetTypeInfo().ContainsGenericParameters;
return CompilerCacheResult.FileNotFound;
}
return false;
return new CompilerCacheResult(result.CompilationResult);
}
/// <inheritdoc />
public CompilationResult GetOrAdd([NotNull] RelativeFileInfo fileInfo,
[NotNull] Func<RelativeFileInfo, CompilationResult> compile)
private GetOrAddResult GetOrAddCore(string relativePath,
Func<RelativeFileInfo, CompilationResult> compile)
{
CompilationResult result;
var entry = GetOrAdd(fileInfo, compile, out result);
return result;
}
private CompilerCacheEntry GetOrAdd(RelativeFileInfo relativeFileInfo,
Func<RelativeFileInfo, CompilationResult> compile,
out CompilationResult result)
{
var normalizedPath = NormalizePath(relativeFileInfo.RelativePath);
var normalizedPath = NormalizePath(relativePath);
var cacheEntry = _cache.Get<CompilerCacheEntry>(normalizedPath);
if (cacheEntry == null)
{
return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result);
var fileInfo = _fileProvider.GetFileInfo(relativePath);
if (!fileInfo.Exists)
{
return null;
}
var relativeFileInfo = new RelativeFileInfo(fileInfo, relativePath);
return OnCacheMiss(relativeFileInfo, normalizedPath, compile);
}
else if (cacheEntry.IsPreCompiled && !cacheEntry.IsValidatedPreCompiled)
{
@ -123,26 +110,35 @@ namespace Microsoft.AspNet.Mvc.Razor
// 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 = _fileProvider.GetFileInfo(relativePath);
if (!fileInfo.Exists)
{
return null;
}
var relativeFileInfo = new RelativeFileInfo(fileInfo, relativePath);
if (cacheEntry.Length != fileInfo.Length)
{
// Recompile if the file lengths differ
return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result);
return OnCacheMiss(relativeFileInfo, normalizedPath, compile);
}
if (AssociatedViewStartsChanged(cacheEntry, compile))
{
// Recompile if the view starts have changed since the entry was created.
return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result);
return OnCacheMiss(relativeFileInfo, normalizedPath, compile);
}
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;
return new GetOrAddResult
{
CompilationResult = CompilationResult.Successful(cacheEntry.CompiledType),
CompilerCacheEntry = cacheEntry
};
}
// Timestamp doesn't match but it might be because of deployment, compare the hash.
@ -156,30 +152,39 @@ namespace Microsoft.AspNet.Mvc.Razor
// if the entry is being concurrently read or updated.
cacheEntry.LastModified = fileInfo.LastModified;
cacheEntry.IsValidatedPreCompiled = true;
result = CompilationResult.Successful(cacheEntry.CompiledType);
return cacheEntry;
return new GetOrAddResult
{
CompilationResult = CompilationResult.Successful(cacheEntry.CompiledType),
CompilerCacheEntry = cacheEntry
};
}
// it's not a match, recompile
return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result);
return OnCacheMiss(relativeFileInfo, normalizedPath, compile);
}
result = CompilationResult.Successful(cacheEntry.CompiledType);
return cacheEntry;
return new GetOrAddResult
{
CompilationResult = CompilationResult.Successful(cacheEntry.CompiledType),
CompilerCacheEntry = cacheEntry
};
}
private CompilerCacheEntry OnCacheMiss(RelativeFileInfo file,
string normalizedPath,
Func<RelativeFileInfo, CompilationResult> compile,
out CompilationResult result)
private GetOrAddResult OnCacheMiss(RelativeFileInfo file,
string normalizedPath,
Func<RelativeFileInfo, CompilationResult> compile)
{
result = compile(file);
var cacheEntry = new CompilerCacheEntry(file, result.CompiledType);
var compilationResult = compile(file);
// Concurrent addition to MemoryCache with the same key result in safe race.
return _cache.Set(normalizedPath, cacheEntry, PopulateCacheSetContext);
var cacheEntry = _cache.Set(normalizedPath,
new CompilerCacheEntry(file, compilationResult.CompiledType),
PopulateCacheSetContext);
return new GetOrAddResult
{
CompilationResult = compilationResult,
CompilerCacheEntry = cacheEntry
};
}
private CompilerCacheEntry PopulateCacheSetContext(ICacheSetContext cacheSetContext)
@ -212,12 +217,11 @@ namespace Microsoft.AspNet.Mvc.Razor
var viewStartLocations = ViewStartUtility.GetViewStartLocations(relativePath);
foreach (var viewStartLocation in viewStartLocations)
{
var viewStartFileInfo = _fileProvider.GetFileInfo(viewStartLocation);
if (viewStartFileInfo.Exists)
var getOrAddResult = GetOrAddCore(viewStartLocation, compile);
if (getOrAddResult != null)
{
var relativeFileInfo = new RelativeFileInfo(viewStartFileInfo, viewStartLocation);
CompilationResult result;
return GetOrAdd(relativeFileInfo, compile, out result);
// This is the nearest _ViewStart that exists on disk.
return getOrAddResult.CompilerCacheEntry;
}
}
@ -235,5 +239,36 @@ namespace Microsoft.AspNet.Mvc.Razor
return path;
}
internal static IEnumerable<RazorFileInfoCollection>
GetFileInfos(IEnumerable<Assembly> assemblies)
{
return assemblies.SelectMany(a => a.ExportedTypes)
.Where(Match)
.Select(c => (RazorFileInfoCollection)Activator.CreateInstance(c));
}
private static bool Match(Type t)
{
var inAssemblyType = typeof(RazorFileInfoCollection);
if (inAssemblyType.IsAssignableFrom(t))
{
var hasParameterlessConstructor = t.GetConstructor(Type.EmptyTypes) != null;
return hasParameterlessConstructor
&& !t.GetTypeInfo().IsAbstract
&& !t.GetTypeInfo().ContainsGenericParameters;
}
return false;
}
private class GetOrAddResult
{
public CompilerCacheEntry CompilerCacheEntry { get; set; }
public CompilationResult CompilationResult { get; set; }
}
}
}

View File

@ -0,0 +1,40 @@
// 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 Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Result of <see cref="ICompilerCache"/>.
/// </summary>
public class CompilerCacheResult
{
/// <summary>
/// Result of <see cref="ICompilerCache"/> when the specified file does not exist in the
/// file system.
/// </summary>
public static CompilerCacheResult FileNotFound { get; } = new CompilerCacheResult();
/// <summary>
/// Initializes a new instance of <see cref="CompilerCacheResult"/> with the specified
/// <see cref="CompilationResult"/>.
/// </summary>
/// <param name="compilationResult">The <see cref="Razor.CompilationResult"/> </param>
public CompilerCacheResult([NotNull] CompilationResult compilationResult)
{
CompilationResult = compilationResult;
}
/// <summary>
/// Initializes a new instance of <see cref="CompilerCacheResult"/> for a failed file lookup.
/// </summary>
protected CompilerCacheResult()
{
}
/// <summary>
/// The <see cref="Razor.CompilationResult"/>.
/// </summary>
/// <remarks>This property is null when file lookup failed.</remarks>
public CompilationResult CompilationResult { get; }
}
}

View File

@ -1,87 +0,0 @@
// 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.Collections.Concurrent;
using Microsoft.AspNet.FileProviders;
using Microsoft.Framework.Expiration.Interfaces;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Default implementation for the <see cref="IRazorFileProviderCache"/> interface that caches
/// the results of <see cref="RazorViewEngineOptions.FileProvider"/>.
/// </summary>
public class DefaultRazorFileProviderCache : IRazorFileProviderCache
{
private readonly ConcurrentDictionary<string, ExpiringFileInfo> _fileInfoCache =
new ConcurrentDictionary<string, ExpiringFileInfo>(StringComparer.Ordinal);
private readonly IFileProvider _fileProvider;
private readonly TimeSpan _offset;
/// <summary>
/// Initializes a new instance of <see cref="DefaultRazorFileProviderCache"/>.
/// </summary>
/// <param name="optionsAccessor">Accessor to <see cref="RazorViewEngineOptions"/>.</param>
public DefaultRazorFileProviderCache(IOptions<RazorViewEngineOptions> optionsAccessor)
{
_fileProvider = optionsAccessor.Options.FileProvider;
_offset = optionsAccessor.Options.ExpirationBeforeCheckingFilesOnDisk;
}
protected virtual DateTime UtcNow
{
get
{
return DateTime.UtcNow;
}
}
/// <inheritdoc />
public IDirectoryContents GetDirectoryContents(string subpath)
{
return _fileProvider.GetDirectoryContents(subpath);
}
/// <inheritdoc />
public IFileInfo GetFileInfo(string subpath)
{
ExpiringFileInfo expiringFileInfo;
var utcNow = UtcNow;
if (_fileInfoCache.TryGetValue(subpath, out expiringFileInfo) &&
expiringFileInfo.ValidUntil > utcNow)
{
return expiringFileInfo.FileInfo;
}
else
{
var fileInfo = _fileProvider.GetFileInfo(subpath);
expiringFileInfo = new ExpiringFileInfo()
{
FileInfo = fileInfo,
ValidUntil = _offset == TimeSpan.MaxValue ? DateTime.MaxValue : utcNow.Add(_offset),
};
_fileInfoCache[subpath] = expiringFileInfo;
return fileInfo;
}
}
/// <inheritdoc />
public IExpirationTrigger Watch(string filter)
{
return _fileProvider.Watch(filter);
}
private class ExpiringFileInfo
{
public IFileInfo FileInfo { get; set; }
public DateTime ValidUntil { get; set; }
}
}
}

View File

@ -14,10 +14,10 @@ namespace Microsoft.AspNet.Mvc.Razor
/// Get an existing compilation result, or create and add a new one if it is
/// not available in the cache or is expired.
/// </summary>
/// <param name="fileInfo">A <see cref="RelativeFileInfo"/> representing the file.</param>
/// <param name="relativePath">Application relative path to the file.</param>
/// <param name="compile">An delegate that will generate a compilation result.</param>
/// <returns>A cached <see cref="CompilationResult"/>.</returns>
CompilationResult GetOrAdd([NotNull] RelativeFileInfo fileInfo,
[NotNull] Func<RelativeFileInfo, CompilationResult> compile);
CompilerCacheResult GetOrAdd([NotNull] string relativePath,
[NotNull] Func<RelativeFileInfo, CompilationResult> compile);
}
}

View File

@ -1,15 +0,0 @@
// 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.FileProviders;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// An <see cref="IFileProvider"/> that caches the results of <see cref="IFileProvider.GetFileInfo(string)"/> for a
/// duration specified by <see cref="RazorViewEngineOptions.ExpirationBeforeCheckingFilesOnDisk"/>.
/// </summary>
public interface IRazorFileProviderCache : IFileProvider
{
}
}

View File

@ -9,42 +9,12 @@ using Microsoft.AspNet.Mvc.Razor.OptionDescriptors;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Provides programmatic configuration for the default <see cref="Microsoft.AspNet.Mvc.Rendering.IViewEngine"/>.
/// Provides programmatic configuration for the default <see cref="Rendering.IViewEngine"/>.
/// </summary>
public class RazorViewEngineOptions
{
private TimeSpan _expirationBeforeCheckingFilesOnDisk = TimeSpan.FromSeconds(2);
private IFileProvider _fileProvider;
/// <summary>
/// Gets or sets the <see cref="TimeSpan"/> that specifies the duration for which results of
/// <see cref="FileProvider"/> are cached by <see cref="DefaultRazorFileProviderCache"/>.
/// <see cref="DefaultRazorFileProviderCache"/> is used to query for file changes during Razor compilation.
/// </summary>
/// <remarks>
/// <see cref="TimeSpan"/> of <see cref="TimeSpan.Zero"/> or less, means no caching.
/// <see cref="TimeSpan"/> of <see cref="TimeSpan.MaxValue"/> means indefinite caching.
/// </remarks>
public TimeSpan ExpirationBeforeCheckingFilesOnDisk
{
get
{
return _expirationBeforeCheckingFilesOnDisk;
}
set
{
if (value.TotalMilliseconds < 0)
{
_expirationBeforeCheckingFilesOnDisk = TimeSpan.Zero;
}
else
{
_expirationBeforeCheckingFilesOnDisk = value;
}
}
}
/// <summary>
/// Get a <see cref="IList{T}"/> of descriptors for <see cref="IViewLocationExpander" />s used by this
/// application.

View File

@ -2,9 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.FileProviders;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.PageExecutionInstrumentation;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc.Razor
@ -17,19 +14,16 @@ namespace Microsoft.AspNet.Mvc.Razor
{
private readonly ITypeActivator _activator;
private readonly IServiceProvider _serviceProvider;
private readonly IRazorFileProviderCache _fileProviderCache;
private readonly ICompilerCache _compilerCache;
private IRazorCompilationService _razorcompilationService;
public VirtualPathRazorPageFactory(ITypeActivator typeActivator,
IServiceProvider serviceProvider,
ICompilerCache compilerCache,
IRazorFileProviderCache fileProviderCache)
ICompilerCache compilerCache)
{
_activator = typeActivator;
_serviceProvider = serviceProvider;
_compilerCache = compilerCache;
_fileProviderCache = fileProviderCache;
}
private IRazorCompilationService RazorCompilationService
@ -57,23 +51,19 @@ namespace Microsoft.AspNet.Mvc.Razor
relativePath = relativePath.Substring(1);
}
var fileInfo = _fileProviderCache.GetFileInfo(relativePath);
var result = _compilerCache.GetOrAdd(
relativePath,
RazorCompilationService.Compile);
if (fileInfo.Exists)
if (result == CompilerCacheResult.FileNotFound)
{
var relativeFileInfo = new RelativeFileInfo(fileInfo, relativePath);
var result = _compilerCache.GetOrAdd(
relativeFileInfo,
RazorCompilationService.Compile);
var page = (IRazorPage)_activator.CreateInstance(_serviceProvider, result.CompiledType);
page.Path = relativePath;
return page;
return null;
}
return null;
var page = (IRazorPage)_activator.CreateInstance(_serviceProvider, result.CompilationResult.CompiledType);
page.Path = relativePath;
return page;
}
}
}

View File

@ -107,13 +107,12 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Transient<IViewLocationExpanderProvider, DefaultViewLocationExpanderProvider>();
// Caches view locations that are valid for the lifetime of the application.
yield return describe.Singleton<IViewLocationCache, DefaultViewLocationCache>();
yield return describe.Singleton<IRazorFileProviderCache, DefaultRazorFileProviderCache>();
// The host is designed to be discarded after consumption and is very inexpensive to initialize.
yield return describe.Transient<IMvcRazorHost>(serviceProvider =>
{
var cachedFileProvider = serviceProvider.GetRequiredService<IRazorFileProviderCache>();
return new MvcRazorHost(cachedFileProvider);
var cachedFileProvider = serviceProvider.GetRequiredService<IOptions<RazorViewEngineOptions>>();
return new MvcRazorHost(cachedFileProvider.Options.FileProvider);
});
// Caches compilation artifacts across the lifetime of the application.

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNet.Mvc.Razor
private readonly Dictionary<string, TestFileTrigger> _fileTriggers =
new Dictionary<string, TestFileTrigger>(StringComparer.Ordinal);
public IDirectoryContents GetDirectoryContents(string subpath)
public virtual IDirectoryContents GetDirectoryContents(string subpath)
{
throw new NotSupportedException();
}
@ -44,7 +44,7 @@ namespace Microsoft.AspNet.Mvc.Razor
_lookup.Remove(path);
}
public IFileInfo GetFileInfo(string subpath)
public virtual IFileInfo GetFileInfo(string subpath)
{
if (_lookup.ContainsKey(subpath))
{
@ -56,7 +56,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
public IExpirationTrigger Watch(string filter)
public virtual IExpirationTrigger Watch(string filter)
{
TestFileTrigger trigger;
if (!_fileTriggers.TryGetValue(filter, out trigger) || trigger.IsExpired)

View File

@ -7,37 +7,137 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.AspNet.FileProviders;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor
{
public class CompilerCacheTest
{
private const string ViewPath = "view-path";
[Fact]
public void GetOrAdd_ReturnsFileNotFoundResult_IfFileIsNotFoundInFileSystem()
{
// Arrange
var fileProvider = new TestFileProvider();
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), fileProvider);
var type = GetType();
// Act
var result = cache.GetOrAdd("/some/path", _ => { throw new Exception("Shouldn't be called"); });
// Assert
Assert.Same(CompilerCacheResult.FileNotFound, result);
Assert.Null(result.CompilationResult);
}
[Fact]
public void GetOrAdd_ReturnsCompilationResultFromFactory()
{
// Arrange
var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), fileProvider);
var fileInfo = new TestFileInfo
{
LastModified = DateTime.FromFileTimeUtc(10000)
};
var type = GetType();
var expected = UncachedCompilationResult.Successful(type, "hello world");
var runtimeFileInfo = new RelativeFileInfo(fileInfo, "ab");
// Act
var actual = cache.GetOrAdd(runtimeFileInfo, _ => expected);
var result = cache.GetOrAdd(ViewPath, _ => expected);
// Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
var actual = result.CompilationResult;
Assert.NotNull(actual);
Assert.Same(expected, actual);
Assert.Equal("hello world", actual.CompiledContent);
Assert.Same(type, actual.CompiledType);
}
[Fact]
public void GetOrAdd_ReturnsFileNotFoundIfFileWasDeleted()
{
// Arrange
var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), fileProvider);
var type = typeof(RuntimeCompileIdentical);
var expected = UncachedCompilationResult.Successful(type, "hello world");
// Act 1
var result1 = cache.GetOrAdd(ViewPath, _ => expected);
// Assert 1
Assert.NotSame(CompilerCacheResult.FileNotFound, result1);
Assert.Same(expected, result1.CompilationResult);
// Act 2
// 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."); });
// Assert 2
Assert.Same(CompilerCacheResult.FileNotFound, result2);
Assert.Null(result2.CompilationResult);
}
[Fact]
public void GetOrAdd_ReturnsNewResultIfFileWasModified()
{
// Arrange
var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), fileProvider);
var type = typeof(RuntimeCompileIdentical);
var expected1 = UncachedCompilationResult.Successful(type, "hello world");
var expected2 = UncachedCompilationResult.Successful(type, "different content");
// Act 1
var result1 = cache.GetOrAdd(ViewPath, _ => expected1);
// Assert 1
Assert.NotSame(CompilerCacheResult.FileNotFound, result1);
Assert.Same(expected1, result1.CompilationResult);
// Act 2
fileProvider.GetTrigger(ViewPath).IsExpired = true;
var result2 = cache.GetOrAdd(ViewPath, _ => expected2);
// Assert 2
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
Assert.Same(expected2, result2.CompilationResult);
}
[Fact]
public void GetOrAdd_DoesNotQueryFileSystem_IfCachedFileTriggerWasNotSet()
{
// Arrange
var mockFileProvider = new Mock<TestFileProvider> { CallBase = true };
var fileProvider = mockFileProvider.Object;
fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), fileProvider);
var type = typeof(RuntimeCompileIdentical);
var expected = UncachedCompilationResult.Successful(type, "hello world");
// Act 1
var result1 = cache.GetOrAdd(ViewPath, _ => expected);
// Assert 1
Assert.NotSame(CompilerCacheResult.FileNotFound, result1);
Assert.Same(expected, result1.CompilationResult);
// Act 2
var result2 = cache.GetOrAdd(ViewPath, _ => { throw new Exception("shouldn't be called"); });
// Assert 2
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
Assert.IsType<CompilationResult>(result2.CompilationResult);
Assert.Same(type, result2.CompilationResult.CompiledType);
mockFileProvider.Verify(v => v.GetFileInfo(ViewPath), Times.Once());
}
private abstract class View
{
public abstract string Content { get; }
@ -87,7 +187,7 @@ namespace Microsoft.AspNet.Mvc.Razor
HashAlgorithmVersion = 1,
LastModified = DateTime.FromFileTimeUtc(10000),
Length = length,
RelativePath = "ab",
RelativePath = ViewPath,
});
}
@ -122,25 +222,17 @@ namespace Microsoft.AspNet.Mvc.Razor
LastModified = DateTime.FromFileTimeUtc(fileTimeUTC),
Content = instance.Content
};
var runtimeFileInfo = new RelativeFileInfo(fileInfo, "ab");
fileProvider.AddFile(ViewPath, fileInfo);
var precompiledContent = new PreCompile().Content;
var razorFileInfo = new RazorFileInfo
{
FullTypeName = typeof(PreCompile).FullName,
Hash = Crc32.Calculate(GetMemoryStream(precompiledContent)).ToString(CultureInfo.InvariantCulture),
HashAlgorithmVersion = 1,
LastModified = DateTime.FromFileTimeUtc(10000),
Length = Encoding.UTF8.GetByteCount(precompiledContent),
RelativePath = "ab",
};
// Act
var actual = cache.GetOrAdd(runtimeFileInfo,
var result = cache.GetOrAdd(ViewPath,
compile: _ => { throw new Exception("Shouldn't be called."); });
// Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
var actual = result.CompilationResult;
Assert.NotNull(actual);
Assert.Equal(typeof(PreCompile), actual.CompiledType);
}
@ -165,25 +257,16 @@ namespace Microsoft.AspNet.Mvc.Razor
LastModified = DateTime.FromFileTimeUtc(fileTimeUTC),
Content = instance.Content
};
var runtimeFileInfo = new RelativeFileInfo(fileInfo, "ab");
var precompiledContent = new PreCompile().Content;
var razorFileInfo = new RazorFileInfo
{
FullTypeName = typeof(PreCompile).FullName,
Hash = Crc32.Calculate(GetMemoryStream(precompiledContent)).ToString(CultureInfo.InvariantCulture),
HashAlgorithmVersion = 1,
LastModified = DateTime.FromFileTimeUtc(10000),
Length = Encoding.UTF8.GetByteCount(precompiledContent),
RelativePath = "ab",
};
fileProvider.AddFile(ViewPath, fileInfo);
// Act
var actual = cache.GetOrAdd(runtimeFileInfo,
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);
}
@ -203,7 +286,7 @@ namespace Microsoft.AspNet.Mvc.Razor
LastModified = lastModified,
Content = instance.Content
};
var runtimeFileInfo = new RelativeFileInfo(fileInfo, "ab");
fileProvider.AddFile(ViewPath, fileInfo);
var viewStartContent = "viewstart-content";
var viewStartFileInfo = new TestFileInfo
@ -227,13 +310,73 @@ namespace Microsoft.AspNet.Mvc.Razor
var cache = new CompilerCache(new[] { precompiledViews }, fileProvider);
// Act
var actual = cache.GetOrAdd(runtimeFileInfo,
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 precompiledView = precompiledViews.FileInfos[0];
var cache = new CompilerCache(new[] { precompiledViews }, fileProvider);
// Act
var result = cache.GetOrAdd(ViewPath,
compile: _ => { throw new Exception("shouldn't be invoked"); });
// Assert
Assert.Same(CompilerCacheResult.FileNotFound, result);
Assert.Null(result.CompilationResult);
}
[Fact]
public void GetOrAdd_DoesNotReadFileFromFileSystemAfterPrecompiledViewIsVerified()
{
// Arrange
var precompiledViews = new ViewCollection();
var mockFileProvider = new Mock<TestFileProvider> { CallBase = true };
var fileProvider = mockFileProvider.Object;
var precompiledView = precompiledViews.FileInfos[0];
var fileInfo = new TestFileInfo
{
Length = precompiledView.Length,
LastModified = precompiledView.LastModified,
};
fileProvider.AddFile(ViewPath, fileInfo);
var cache = new CompilerCache(new[] { precompiledViews }, fileProvider);
// Act 1
var result1 = cache.GetOrAdd(ViewPath,
compile: _ => { throw new Exception("shouldn't be invoked"); });
// 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());
// Act 2
var result2 = cache.GetOrAdd(ViewPath,
compile: _ => { throw new Exception("shouldn't be invoked"); });
// 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());
}
[Fact]
public void GetOrAdd_IgnoresCachedValueIfFileIsIdentical_ButViewStartWasAdedSinceTheCacheWasCreated()
{
@ -255,19 +398,25 @@ namespace Microsoft.AspNet.Mvc.Razor
var relativeFile = new RelativeFileInfo(testFile, testFile.PhysicalPath);
// Act 1
var actual1 = cache.GetOrAdd(relativeFile,
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 viewStartTrigger = fileProvider.GetTrigger("Views\\_ViewStart.cshtml");
viewStartTrigger.IsExpired = true;
var actual2 = cache.GetOrAdd(relativeFile,
var result2 = cache.GetOrAdd(testFile.PhysicalPath,
compile: _ => CompilationResult.Successful(expectedType));
// Assert 2
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
var actual2 = result2.CompilationResult;
Assert.NotNull(actual2);
Assert.Equal(expectedType, actual2.CompiledType);
}
@ -309,22 +458,27 @@ namespace Microsoft.AspNet.Mvc.Razor
viewCollection.Add(viewStart);
var cache = new CompilerCache(new[] { viewCollection }, fileProvider);
var fileInfo = new RelativeFileInfo(viewFileInfo, viewFileInfo.PhysicalPath);
// Act 1
var actual1 = cache.GetOrAdd(fileInfo,
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(viewStartFileInfo.PhysicalPath);
trigger.IsExpired = true;
var actual2 = cache.GetOrAdd(fileInfo,
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);
}
@ -385,28 +539,20 @@ namespace Microsoft.AspNet.Mvc.Razor
PhysicalPath = "Views\\home\\index.cshtml"
};
var runtimeFileInfo = new RelativeFileInfo(fileInfo, fileInfo.PhysicalPath);
var razorFileInfo = new RazorFileInfo
{
FullTypeName = typeof(PreCompile).FullName,
Hash = RazorFileHash.GetHash(fileInfo, hashAlgorithmVersion: 1),
HashAlgorithmVersion = 1,
LastModified = lastModified,
Length = Encoding.UTF8.GetByteCount(content),
RelativePath = fileInfo.PhysicalPath,
};
var fileProvider = new TestFileProvider();
fileProvider.AddFile(fileInfo.PhysicalPath, fileInfo);
fileProvider.AddFile(viewStartRazorFileInfo.RelativePath, viewStartFileInfo);
var viewCollection = new ViewCollection();
var cache = new CompilerCache(new[] { viewCollection }, fileProvider);
// Act
var actual = cache.GetOrAdd(runtimeFileInfo,
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);
}
@ -415,23 +561,28 @@ namespace Microsoft.AspNet.Mvc.Razor
{
// Arrange
var lastModified = DateTime.UtcNow;
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), new TestFileProvider());
var fileProvider = new TestFileProvider();
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), fileProvider);
var fileInfo = new TestFileInfo
{
PhysicalPath = "test",
LastModified = lastModified
};
fileProvider.AddFile("test", fileInfo);
var type = GetType();
var uncachedResult = UncachedCompilationResult.Successful(type, "hello world");
var runtimeFileInfo = new RelativeFileInfo(fileInfo, "test");
// Act
cache.GetOrAdd(runtimeFileInfo, _ => uncachedResult);
var actual1 = cache.GetOrAdd(runtimeFileInfo, _ => uncachedResult);
var actual2 = cache.GetOrAdd(runtimeFileInfo, _ => uncachedResult);
cache.GetOrAdd("test", _ => uncachedResult);
var result1 = cache.GetOrAdd("test", _ => uncachedResult);
var result2 = cache.GetOrAdd("test", _ => uncachedResult);
// Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result1);
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
var actual1 = result1.CompilationResult;
var actual2 = result2.CompilationResult;
Assert.NotSame(uncachedResult, actual1);
Assert.NotSame(uncachedResult, actual2);
var result = Assert.IsType<CompilationResult>(actual1);

View File

@ -1,414 +0,0 @@
// 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.Collections.Generic;
using Microsoft.AspNet.FileProviders;
using Microsoft.Framework.OptionsModel;
using Microsoft.Framework.Expiration.Interfaces;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor
{
public class DefaultRazorFileProviderCacheTest
{
private const string FileName = "myView.cshtml";
public DummyFileProvider TestFileProvider { get; } = new DummyFileProvider();
public IOptions<RazorViewEngineOptions> OptionsAccessor
{
get
{
var options = new RazorViewEngineOptions
{
FileProvider = TestFileProvider
};
var mock = new Mock<IOptions<RazorViewEngineOptions>>(MockBehavior.Strict);
mock.Setup(oa => oa.Options).Returns(options);
return mock.Object;
}
}
public ControllableExpiringFileInfoCache GetCache(IOptions<RazorViewEngineOptions> optionsAccessor)
{
return new ControllableExpiringFileInfoCache(optionsAccessor);
}
public void CreateFile(string fileName)
{
var fileInfo = new TestFileInfo()
{
Name = fileName,
LastModified = DateTime.Now,
};
TestFileProvider.AddFile(fileInfo);
}
public void Sleep(ControllableExpiringFileInfoCache cache, int offsetMilliseconds)
{
cache.Sleep(offsetMilliseconds);
}
public void Sleep(IOptions<RazorViewEngineOptions> accessor, ControllableExpiringFileInfoCache cache, int offsetMilliSeconds)
{
var baseMilliSeconds = (int)accessor.Options.ExpirationBeforeCheckingFilesOnDisk.TotalMilliseconds;
cache.Sleep(baseMilliSeconds + offsetMilliSeconds);
}
public void SetExpiration(IOptions<RazorViewEngineOptions> accessor, TimeSpan expiration)
{
accessor.Options.ExpirationBeforeCheckingFilesOnDisk = expiration;
}
[Fact]
public void VerifyDefaultOptionsAreSetupCorrectly()
{
var optionsAccessor = OptionsAccessor;
// Assert
Assert.Equal(2000, optionsAccessor.Options.ExpirationBeforeCheckingFilesOnDisk.TotalMilliseconds);
}
[Fact]
public void GettingFileInfoReturnsTheSameDataWithDefaultOptions()
{
// Arrange
var cache = GetCache(OptionsAccessor);
CreateFile(FileName);
// Act
var fileInfo1 = cache.GetFileInfo(FileName);
var fileInfo2 = cache.GetFileInfo(FileName);
// Assert
Assert.True(fileInfo1.Exists);
Assert.True(fileInfo1.Exists);
Assert.Same(fileInfo1, fileInfo2);
Assert.Equal(FileName, fileInfo1.Name);
}
[Fact]
public void GettingFileInfoReturnsTheSameDataWithDefaultOptionsEvenWhenFilesHaveChanged()
{
// Arrange
var cache = GetCache(OptionsAccessor);
CreateFile(FileName);
// Act
var fileInfo1 = cache.GetFileInfo(FileName);
CreateFile(FileName);
var fileInfo2 = cache.GetFileInfo(FileName);
// Assert
Assert.Same(fileInfo1, fileInfo2);
Assert.Equal(fileInfo1.LastModified, fileInfo2.LastModified);
Assert.Equal(FileName, fileInfo1.Name);
Assert.Equal(FileName, fileInfo2.Name);
}
[Fact]
public void GettingFileInfoReturnsNewDataWithDefaultOptionsAfterExpirationAndFileChange()
{
var optionsAccessor = OptionsAccessor;
// Arrange
var cache = GetCache(optionsAccessor);
CreateFile(FileName);
// Act
var fileInfo1 = cache.GetFileInfo(FileName);
Sleep(optionsAccessor, cache, 500);
CreateFile(FileName);
var fileInfo2 = cache.GetFileInfo(FileName);
// Assert
Assert.NotSame(fileInfo1, fileInfo2);
Assert.Equal(FileName, fileInfo1.Name);
Assert.Equal(FileName, fileInfo2.Name);
}
[Fact]
public void GettingFileInfoReturnsNewDataWithDefaultOptionsAfterExpiration()
{
// Arrange
var optionsAccessor = OptionsAccessor;
var cache = GetCache(optionsAccessor);
CreateFile(FileName);
// Act
var fileInfo1 = cache.GetFileInfo(FileName);
Sleep(optionsAccessor, cache, 500);
var fileInfo2 = cache.GetFileInfo(FileName);
// Assert
Assert.NotSame(fileInfo1, fileInfo2);
Assert.Equal(fileInfo1.LastModified, fileInfo2.LastModified);
Assert.Equal(FileName, fileInfo1.Name);
Assert.Equal(FileName, fileInfo2.Name);
}
public static IEnumerable<object[]> ImmediateExpirationTimespans
{
get
{
yield return new object[]
{
TimeSpan.FromSeconds(0.0)
};
yield return new object[]
{
TimeSpan.FromSeconds(-1.0)
};
yield return new object[]
{
TimeSpan.MinValue
};
}
}
[Theory]
[MemberData(nameof(ImmediateExpirationTimespans))]
public void GettingFileInfoReturnsNewDataWithCustomImmediateExpiration(TimeSpan expiration)
{
// Arrange
var optionsAccessor = OptionsAccessor;
SetExpiration(optionsAccessor, expiration);
string FileName = "myfile4.cshtml";
var cache = GetCache(optionsAccessor);
CreateFile(FileName);
// Act
var fileInfo1 = cache.GetFileInfo(FileName);
var fileInfo2 = cache.GetFileInfo(FileName);
// Assert
Assert.NotSame(fileInfo1, fileInfo2);
Assert.Equal(fileInfo1.LastModified, fileInfo2.LastModified);
Assert.Equal(FileName, fileInfo1.Name);
Assert.Equal(FileName, fileInfo2.Name);
}
public static IEnumerable<object[]> CustomExpirationTimespans
{
get
{
yield return new object[]
{
TimeSpan.FromSeconds(1.0)
};
yield return new object[]
{
TimeSpan.FromSeconds(3.0)
};
}
}
[Theory]
[MemberData(nameof(CustomExpirationTimespans))]
public void GettingFileInfoReturnsNewDataWithCustomExpiration(TimeSpan expiration)
{
// Arrange
var optionsAccessor = OptionsAccessor;
SetExpiration(optionsAccessor, expiration);
string FileName = "myfile5.cshtml";
var cache = GetCache(optionsAccessor);
CreateFile(FileName);
// Act
var fileInfo1 = cache.GetFileInfo(FileName);
Sleep(optionsAccessor, cache, 500);
var fileInfo2 = cache.GetFileInfo(FileName);
// Assert
Assert.NotSame(fileInfo1, fileInfo2);
Assert.Equal(FileName, fileInfo1.Name);
}
[Theory]
[MemberData(nameof(CustomExpirationTimespans))]
public void GettingFileInfoReturnsSameDataWithCustomExpiration(TimeSpan expiration)
{
// Arrange
var optionsAccessor = OptionsAccessor;
SetExpiration(optionsAccessor, expiration);
string FileName = "myfile6.cshtml";
var cache = GetCache(optionsAccessor);
CreateFile(FileName);
// Act
var fileInfo1 = cache.GetFileInfo(FileName);
Sleep(optionsAccessor, cache, -500);
var fileInfo2 = cache.GetFileInfo(FileName);
// Assert
Assert.Same(fileInfo1, fileInfo2);
Assert.Equal(FileName, fileInfo1.Name);
}
[Fact]
public void GettingFileInfoReturnsSameDataWithMaxExpiration()
{
// Arrange
var optionsAccessor = OptionsAccessor;
SetExpiration(optionsAccessor, TimeSpan.MaxValue);
string FileName = "myfile7.cshtml";
var cache = GetCache(optionsAccessor);
CreateFile(FileName);
// Act
var fileInfo1 = cache.GetFileInfo(FileName);
Sleep(cache, 2500);
var fileInfo2 = cache.GetFileInfo(FileName);
// Assert
Assert.Same(fileInfo1, fileInfo2);
Assert.Equal(FileName, fileInfo1.Name);
}
[Fact]
public void GetDirectoryInfo_PassesThroughToUnderlyingFileProvider()
{
// Arrange
var fileProvider = new Mock<IFileProvider>();
var expected = Mock.Of<IDirectoryContents>();
fileProvider.Setup(f => f.GetDirectoryContents("/test-path"))
.Returns(expected)
.Verifiable();
var options = new RazorViewEngineOptions
{
FileProvider = fileProvider.Object
};
var accessor = new Mock<IOptions<RazorViewEngineOptions>>();
accessor.SetupGet(a => a.Options)
.Returns(options);
var cachedFileProvider = new DefaultRazorFileProviderCache(accessor.Object);
// Act
var result = cachedFileProvider.GetDirectoryContents("/test-path");
// Assert
Assert.Same(expected, result);
fileProvider.Verify();
}
public class ControllableExpiringFileInfoCache : DefaultRazorFileProviderCache
{
public ControllableExpiringFileInfoCache(IOptions<RazorViewEngineOptions> optionsAccessor)
: base(optionsAccessor)
{
}
private DateTime? _internalUtcNow { get; set; }
protected override DateTime UtcNow
{
get
{
if (_internalUtcNow == null)
{
_internalUtcNow = base.UtcNow;
}
return _internalUtcNow.Value.AddTicks(1);
}
}
public void Sleep(int milliSeconds)
{
if (milliSeconds <= 0)
{
throw new InvalidOperationException();
}
_internalUtcNow = UtcNow.AddMilliseconds(milliSeconds);
}
}
public class DummyFileProvider : IFileProvider
{
private Dictionary<string, IFileInfo> _fileInfos = new Dictionary<string, IFileInfo>(StringComparer.OrdinalIgnoreCase);
public void AddFile(IFileInfo fileInfo)
{
if (_fileInfos.ContainsKey(fileInfo.Name))
{
_fileInfos[fileInfo.Name] = fileInfo;
}
else
{
_fileInfos.Add(fileInfo.Name, fileInfo);
}
}
public IDirectoryContents GetDirectoryContents(string subpath)
{
throw new NotImplementedException();
}
public IFileInfo GetFileInfo(string subpath)
{
IFileInfo knownInfo;
if (_fileInfos.TryGetValue(subpath, out knownInfo))
{
return new TestFileInfo
{
Name = knownInfo.Name,
LastModified = knownInfo.LastModified,
};
}
else
{
return new NotFoundFileInfo(subpath);
}
}
public IExpirationTrigger Watch(string filter)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -12,7 +12,7 @@ namespace Microsoft.AspNet.Mvc.Razor
public class RazorViewEngineOptionsTest
{
[Fact]
public void FileProviderThrows_IfNullIsAsseigned()
public void FileProviderThrows_IfNullIsAssigned()
{
// Arrange
var options = new RazorViewEngineOptions();
@ -27,18 +27,18 @@ namespace Microsoft.AspNet.Mvc.Razor
{
// Arrange
var services = new ServiceCollection().AddOptions();
var timeSpan = new TimeSpan(400);
var fileProvider = new TestFileProvider();
// Act
services.ConfigureRazorViewEngineOptions(options => {
options.ExpirationBeforeCheckingFilesOnDisk = timeSpan;
services.ConfigureRazorViewEngineOptions(options =>
{
options.FileProvider = fileProvider;
});
var serviceProvider = services.BuildServiceProvider();
// Assert
var accessor = serviceProvider.GetRequiredService<IOptions<RazorViewEngineOptions>>();
var expiration = Assert.IsType<TimeSpan>(accessor.Options.ExpirationBeforeCheckingFilesOnDisk);
Assert.Equal(timeSpan, expiration);
Assert.Same(fileProvider, accessor.Options.FileProvider);
}
}
}

View File

@ -3,15 +3,16 @@
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Razor;
using Microsoft.Framework.OptionsModel;
namespace RazorCompilerCacheWebSite
{
public class CustomCompilerCache : CompilerCache
{
public CustomCompilerCache(IAssemblyProvider assemblyProvider,
IRazorFileProviderCache fileProvider,
IOptions<RazorViewEngineOptions> optionsAccessor,
CompilerCacheInitialiedService cacheInitializedService)
: base(assemblyProvider, fileProvider)
: base(assemblyProvider, optionsAccessor)
{
cacheInitializedService.Initialized = true;
}