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 System.Reflection;
using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.FileProviders;
using Microsoft.Framework.Cache.Memory; using Microsoft.Framework.Cache.Memory;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
@ -26,11 +27,10 @@ namespace Microsoft.AspNet.Mvc.Razor
/// An <see cref="IAssemblyProvider"/> representing the assemblies /// An <see cref="IAssemblyProvider"/> representing the assemblies
/// used to search for pre-compiled views. /// used to search for pre-compiled views.
/// </param> /// </param>
/// <param name="fileProvider">An <see cref="IRazorFileProviderCache"/> instance that represents the application's /// <param name="optionsAccessor">An accessor to the <see cref="RazorViewEngineOptions"/>.</param>
/// file system. public CompilerCache(IAssemblyProvider provider,
/// </param> IOptions<RazorViewEngineOptions> optionsAccessor)
public CompilerCache(IAssemblyProvider provider, IRazorFileProviderCache fileProvider) : this(GetFileInfos(provider.CandidateAssemblies), optionsAccessor.Options.FileProvider)
: this(GetFileInfos(provider.CandidateAssemblies), fileProvider)
{ {
} }
@ -74,47 +74,34 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
} }
internal static IEnumerable<RazorFileInfoCollection> /// <inheritdoc />
GetFileInfos(IEnumerable<Assembly> assemblies) public CompilerCacheResult GetOrAdd([NotNull] string relativePath,
[NotNull] Func<RelativeFileInfo, CompilationResult> compile)
{ {
return assemblies.SelectMany(a => a.ExportedTypes) var result = GetOrAddCore(relativePath, compile);
.Where(Match) if (result == null)
.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 CompilerCacheResult.FileNotFound;
return hasParameterlessConstructor
&& !t.GetTypeInfo().IsAbstract
&& !t.GetTypeInfo().ContainsGenericParameters;
} }
return false; return new CompilerCacheResult(result.CompilationResult);
} }
/// <inheritdoc /> private GetOrAddResult GetOrAddCore(string relativePath,
public CompilationResult GetOrAdd([NotNull] RelativeFileInfo fileInfo, Func<RelativeFileInfo, CompilationResult> compile)
[NotNull] Func<RelativeFileInfo, CompilationResult> compile)
{ {
CompilationResult result; var normalizedPath = NormalizePath(relativePath);
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 cacheEntry = _cache.Get<CompilerCacheEntry>(normalizedPath); var cacheEntry = _cache.Get<CompilerCacheEntry>(normalizedPath);
if (cacheEntry == null) 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) 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 // 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. // 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) if (cacheEntry.Length != fileInfo.Length)
{ {
// Recompile if the file lengths differ // Recompile if the file lengths differ
return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result); return OnCacheMiss(relativeFileInfo, normalizedPath, compile);
} }
if (AssociatedViewStartsChanged(cacheEntry, compile)) if (AssociatedViewStartsChanged(cacheEntry, compile))
{ {
// Recompile if the view starts have changed since the entry was created. // 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) if (cacheEntry.LastModified == fileInfo.LastModified)
{ {
result = CompilationResult.Successful(cacheEntry.CompiledType);
// Assigning to IsValidatedPreCompiled is an atomic operation and will result in a safe race // Assigning to IsValidatedPreCompiled is an atomic operation and will result in a safe race
// if it is being concurrently updated and read. // if it is being concurrently updated and read.
cacheEntry.IsValidatedPreCompiled = true; 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. // 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. // if the entry is being concurrently read or updated.
cacheEntry.LastModified = fileInfo.LastModified; cacheEntry.LastModified = fileInfo.LastModified;
cacheEntry.IsValidatedPreCompiled = true; cacheEntry.IsValidatedPreCompiled = true;
result = CompilationResult.Successful(cacheEntry.CompiledType); return new GetOrAddResult
{
return cacheEntry; CompilationResult = CompilationResult.Successful(cacheEntry.CompiledType),
CompilerCacheEntry = cacheEntry
};
} }
// it's not a match, recompile // it's not a match, recompile
return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result); return OnCacheMiss(relativeFileInfo, normalizedPath, compile);
} }
result = CompilationResult.Successful(cacheEntry.CompiledType); return new GetOrAddResult
return cacheEntry; {
CompilationResult = CompilationResult.Successful(cacheEntry.CompiledType),
CompilerCacheEntry = cacheEntry
};
} }
private CompilerCacheEntry OnCacheMiss(RelativeFileInfo file, private GetOrAddResult OnCacheMiss(RelativeFileInfo file,
string normalizedPath, string normalizedPath,
Func<RelativeFileInfo, CompilationResult> compile, Func<RelativeFileInfo, CompilationResult> compile)
out CompilationResult result)
{ {
result = compile(file); var compilationResult = compile(file);
var cacheEntry = new CompilerCacheEntry(file, result.CompiledType);
// Concurrent addition to MemoryCache with the same key result in safe race. // 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) private CompilerCacheEntry PopulateCacheSetContext(ICacheSetContext cacheSetContext)
@ -212,12 +217,11 @@ namespace Microsoft.AspNet.Mvc.Razor
var viewStartLocations = ViewStartUtility.GetViewStartLocations(relativePath); var viewStartLocations = ViewStartUtility.GetViewStartLocations(relativePath);
foreach (var viewStartLocation in viewStartLocations) foreach (var viewStartLocation in viewStartLocations)
{ {
var viewStartFileInfo = _fileProvider.GetFileInfo(viewStartLocation); var getOrAddResult = GetOrAddCore(viewStartLocation, compile);
if (viewStartFileInfo.Exists) if (getOrAddResult != null)
{ {
var relativeFileInfo = new RelativeFileInfo(viewStartFileInfo, viewStartLocation); // This is the nearest _ViewStart that exists on disk.
CompilationResult result; return getOrAddResult.CompilerCacheEntry;
return GetOrAdd(relativeFileInfo, compile, out result);
} }
} }
@ -235,5 +239,36 @@ namespace Microsoft.AspNet.Mvc.Razor
return path; 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 /// Get an existing compilation result, or create and add a new one if it is
/// not available in the cache or is expired. /// not available in the cache or is expired.
/// </summary> /// </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> /// <param name="compile">An delegate that will generate a compilation result.</param>
/// <returns>A cached <see cref="CompilationResult"/>.</returns> /// <returns>A cached <see cref="CompilationResult"/>.</returns>
CompilationResult GetOrAdd([NotNull] RelativeFileInfo fileInfo, CompilerCacheResult GetOrAdd([NotNull] string relativePath,
[NotNull] Func<RelativeFileInfo, CompilationResult> compile); [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 namespace Microsoft.AspNet.Mvc.Razor
{ {
/// <summary> /// <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> /// </summary>
public class RazorViewEngineOptions public class RazorViewEngineOptions
{ {
private TimeSpan _expirationBeforeCheckingFilesOnDisk = TimeSpan.FromSeconds(2);
private IFileProvider _fileProvider; 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> /// <summary>
/// Get a <see cref="IList{T}"/> of descriptors for <see cref="IViewLocationExpander" />s used by this /// Get a <see cref="IList{T}"/> of descriptors for <see cref="IViewLocationExpander" />s used by this
/// application. /// 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using Microsoft.AspNet.FileProviders;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.PageExecutionInstrumentation;
using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
@ -17,19 +14,16 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
private readonly ITypeActivator _activator; private readonly ITypeActivator _activator;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IRazorFileProviderCache _fileProviderCache;
private readonly ICompilerCache _compilerCache; private readonly ICompilerCache _compilerCache;
private IRazorCompilationService _razorcompilationService; private IRazorCompilationService _razorcompilationService;
public VirtualPathRazorPageFactory(ITypeActivator typeActivator, public VirtualPathRazorPageFactory(ITypeActivator typeActivator,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ICompilerCache compilerCache, ICompilerCache compilerCache)
IRazorFileProviderCache fileProviderCache)
{ {
_activator = typeActivator; _activator = typeActivator;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_compilerCache = compilerCache; _compilerCache = compilerCache;
_fileProviderCache = fileProviderCache;
} }
private IRazorCompilationService RazorCompilationService private IRazorCompilationService RazorCompilationService
@ -57,23 +51,19 @@ namespace Microsoft.AspNet.Mvc.Razor
relativePath = relativePath.Substring(1); 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); return null;
var result = _compilerCache.GetOrAdd(
relativeFileInfo,
RazorCompilationService.Compile);
var page = (IRazorPage)_activator.CreateInstance(_serviceProvider, result.CompiledType);
page.Path = relativePath;
return page;
} }
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>(); yield return describe.Transient<IViewLocationExpanderProvider, DefaultViewLocationExpanderProvider>();
// Caches view locations that are valid for the lifetime of the application. // Caches view locations that are valid for the lifetime of the application.
yield return describe.Singleton<IViewLocationCache, DefaultViewLocationCache>(); 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. // The host is designed to be discarded after consumption and is very inexpensive to initialize.
yield return describe.Transient<IMvcRazorHost>(serviceProvider => yield return describe.Transient<IMvcRazorHost>(serviceProvider =>
{ {
var cachedFileProvider = serviceProvider.GetRequiredService<IRazorFileProviderCache>(); var cachedFileProvider = serviceProvider.GetRequiredService<IOptions<RazorViewEngineOptions>>();
return new MvcRazorHost(cachedFileProvider); return new MvcRazorHost(cachedFileProvider.Options.FileProvider);
}); });
// Caches compilation artifacts across the lifetime of the application. // 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 = private readonly Dictionary<string, TestFileTrigger> _fileTriggers =
new Dictionary<string, TestFileTrigger>(StringComparer.Ordinal); new Dictionary<string, TestFileTrigger>(StringComparer.Ordinal);
public IDirectoryContents GetDirectoryContents(string subpath) public virtual IDirectoryContents GetDirectoryContents(string subpath)
{ {
throw new NotSupportedException(); throw new NotSupportedException();
} }
@ -44,7 +44,7 @@ namespace Microsoft.AspNet.Mvc.Razor
_lookup.Remove(path); _lookup.Remove(path);
} }
public IFileInfo GetFileInfo(string subpath) public virtual IFileInfo GetFileInfo(string subpath)
{ {
if (_lookup.ContainsKey(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; TestFileTrigger trigger;
if (!_fileTriggers.TryGetValue(filter, out trigger) || trigger.IsExpired) if (!_fileTriggers.TryGetValue(filter, out trigger) || trigger.IsExpired)

View File

@ -7,37 +7,137 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Microsoft.AspNet.FileProviders;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
public class CompilerCacheTest 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] [Fact]
public void GetOrAdd_ReturnsCompilationResultFromFactory() public void GetOrAdd_ReturnsCompilationResultFromFactory()
{ {
// Arrange // Arrange
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), fileProvider); var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), fileProvider);
var fileInfo = new TestFileInfo
{
LastModified = DateTime.FromFileTimeUtc(10000)
};
var type = GetType(); var type = GetType();
var expected = UncachedCompilationResult.Successful(type, "hello world"); var expected = UncachedCompilationResult.Successful(type, "hello world");
var runtimeFileInfo = new RelativeFileInfo(fileInfo, "ab");
// Act // Act
var actual = cache.GetOrAdd(runtimeFileInfo, _ => expected); var result = cache.GetOrAdd(ViewPath, _ => expected);
// Assert // Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
var actual = result.CompilationResult;
Assert.NotNull(actual);
Assert.Same(expected, actual); Assert.Same(expected, actual);
Assert.Equal("hello world", actual.CompiledContent); Assert.Equal("hello world", actual.CompiledContent);
Assert.Same(type, actual.CompiledType); 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 private abstract class View
{ {
public abstract string Content { get; } public abstract string Content { get; }
@ -87,7 +187,7 @@ namespace Microsoft.AspNet.Mvc.Razor
HashAlgorithmVersion = 1, HashAlgorithmVersion = 1,
LastModified = DateTime.FromFileTimeUtc(10000), LastModified = DateTime.FromFileTimeUtc(10000),
Length = length, Length = length,
RelativePath = "ab", RelativePath = ViewPath,
}); });
} }
@ -122,25 +222,17 @@ namespace Microsoft.AspNet.Mvc.Razor
LastModified = DateTime.FromFileTimeUtc(fileTimeUTC), LastModified = DateTime.FromFileTimeUtc(fileTimeUTC),
Content = instance.Content Content = instance.Content
}; };
fileProvider.AddFile(ViewPath, fileInfo);
var runtimeFileInfo = new RelativeFileInfo(fileInfo, "ab");
var precompiledContent = new PreCompile().Content; 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 // Act
var actual = cache.GetOrAdd(runtimeFileInfo, var result = cache.GetOrAdd(ViewPath,
compile: _ => { throw new Exception("Shouldn't be called."); }); compile: _ => { throw new Exception("Shouldn't be called."); });
// Assert // Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
var actual = result.CompilationResult;
Assert.NotNull(actual);
Assert.Equal(typeof(PreCompile), actual.CompiledType); Assert.Equal(typeof(PreCompile), actual.CompiledType);
} }
@ -165,25 +257,16 @@ namespace Microsoft.AspNet.Mvc.Razor
LastModified = DateTime.FromFileTimeUtc(fileTimeUTC), LastModified = DateTime.FromFileTimeUtc(fileTimeUTC),
Content = instance.Content Content = instance.Content
}; };
fileProvider.AddFile(ViewPath, fileInfo);
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",
};
// Act // Act
var actual = cache.GetOrAdd(runtimeFileInfo, var result = cache.GetOrAdd(ViewPath,
compile: _ => CompilationResult.Successful(resultViewType)); compile: _ => CompilationResult.Successful(resultViewType));
// Assert // Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
var actual = result.CompilationResult;
Assert.NotNull(actual);
Assert.Equal(resultViewType, actual.CompiledType); Assert.Equal(resultViewType, actual.CompiledType);
} }
@ -203,7 +286,7 @@ namespace Microsoft.AspNet.Mvc.Razor
LastModified = lastModified, LastModified = lastModified,
Content = instance.Content Content = instance.Content
}; };
var runtimeFileInfo = new RelativeFileInfo(fileInfo, "ab"); fileProvider.AddFile(ViewPath, fileInfo);
var viewStartContent = "viewstart-content"; var viewStartContent = "viewstart-content";
var viewStartFileInfo = new TestFileInfo var viewStartFileInfo = new TestFileInfo
@ -227,13 +310,73 @@ namespace Microsoft.AspNet.Mvc.Razor
var cache = new CompilerCache(new[] { precompiledViews }, fileProvider); var cache = new CompilerCache(new[] { precompiledViews }, fileProvider);
// Act // Act
var actual = cache.GetOrAdd(runtimeFileInfo, var result = cache.GetOrAdd(ViewPath,
compile: _ => { throw new Exception("shouldn't be invoked"); }); compile: _ => { throw new Exception("shouldn't be invoked"); });
// Assert // Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
var actual = result.CompilationResult;
Assert.NotNull(actual);
Assert.Equal(typeof(PreCompile), actual.CompiledType); 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] [Fact]
public void GetOrAdd_IgnoresCachedValueIfFileIsIdentical_ButViewStartWasAdedSinceTheCacheWasCreated() public void GetOrAdd_IgnoresCachedValueIfFileIsIdentical_ButViewStartWasAdedSinceTheCacheWasCreated()
{ {
@ -255,19 +398,25 @@ namespace Microsoft.AspNet.Mvc.Razor
var relativeFile = new RelativeFileInfo(testFile, testFile.PhysicalPath); var relativeFile = new RelativeFileInfo(testFile, testFile.PhysicalPath);
// Act 1 // Act 1
var actual1 = cache.GetOrAdd(relativeFile, var result1 = cache.GetOrAdd(testFile.PhysicalPath,
compile: _ => { throw new Exception("should not be called"); }); compile: _ => { throw new Exception("should not be called"); });
// Assert 1 // Assert 1
Assert.NotSame(CompilerCacheResult.FileNotFound, result1);
var actual1 = result1.CompilationResult;
Assert.NotNull(actual1);
Assert.Equal(typeof(PreCompile), actual1.CompiledType); Assert.Equal(typeof(PreCompile), actual1.CompiledType);
// Act 2 // Act 2
var viewStartTrigger = fileProvider.GetTrigger("Views\\_ViewStart.cshtml"); var viewStartTrigger = fileProvider.GetTrigger("Views\\_ViewStart.cshtml");
viewStartTrigger.IsExpired = true; viewStartTrigger.IsExpired = true;
var actual2 = cache.GetOrAdd(relativeFile, var result2 = cache.GetOrAdd(testFile.PhysicalPath,
compile: _ => CompilationResult.Successful(expectedType)); compile: _ => CompilationResult.Successful(expectedType));
// Assert 2 // Assert 2
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
var actual2 = result2.CompilationResult;
Assert.NotNull(actual2);
Assert.Equal(expectedType, actual2.CompiledType); Assert.Equal(expectedType, actual2.CompiledType);
} }
@ -309,22 +458,27 @@ namespace Microsoft.AspNet.Mvc.Razor
viewCollection.Add(viewStart); viewCollection.Add(viewStart);
var cache = new CompilerCache(new[] { viewCollection }, fileProvider); var cache = new CompilerCache(new[] { viewCollection }, fileProvider);
var fileInfo = new RelativeFileInfo(viewFileInfo, viewFileInfo.PhysicalPath);
// Act 1 // Act 1
var actual1 = cache.GetOrAdd(fileInfo, var result1 = cache.GetOrAdd(viewFileInfo.PhysicalPath,
compile: _ => { throw new Exception("should not be called"); }); compile: _ => { throw new Exception("should not be called"); });
// Assert 1 // Assert 1
Assert.NotSame(CompilerCacheResult.FileNotFound, result1);
var actual1 = result1.CompilationResult;
Assert.NotNull(actual1);
Assert.Equal(typeof(PreCompile), actual1.CompiledType); Assert.Equal(typeof(PreCompile), actual1.CompiledType);
// Act 2 // Act 2
var trigger = fileProvider.GetTrigger(viewStartFileInfo.PhysicalPath); var trigger = fileProvider.GetTrigger(viewStartFileInfo.PhysicalPath);
trigger.IsExpired = true; trigger.IsExpired = true;
var actual2 = cache.GetOrAdd(fileInfo, var result2 = cache.GetOrAdd(viewFileInfo.PhysicalPath,
compile: _ => CompilationResult.Successful(expectedType)); compile: _ => CompilationResult.Successful(expectedType));
// Assert 2 // Assert 2
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
var actual2 = result2.CompilationResult;
Assert.NotNull(actual2);
Assert.Equal(expectedType, actual2.CompiledType); Assert.Equal(expectedType, actual2.CompiledType);
} }
@ -385,28 +539,20 @@ namespace Microsoft.AspNet.Mvc.Razor
PhysicalPath = "Views\\home\\index.cshtml" 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(); var fileProvider = new TestFileProvider();
fileProvider.AddFile(fileInfo.PhysicalPath, fileInfo);
fileProvider.AddFile(viewStartRazorFileInfo.RelativePath, viewStartFileInfo); fileProvider.AddFile(viewStartRazorFileInfo.RelativePath, viewStartFileInfo);
var viewCollection = new ViewCollection(); var viewCollection = new ViewCollection();
var cache = new CompilerCache(new[] { viewCollection }, fileProvider); var cache = new CompilerCache(new[] { viewCollection }, fileProvider);
// Act // Act
var actual = cache.GetOrAdd(runtimeFileInfo, var result = cache.GetOrAdd(fileInfo.PhysicalPath,
compile: _ => CompilationResult.Successful(expectedType)); compile: _ => CompilationResult.Successful(expectedType));
// Assert // Assert
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
var actual = result.CompilationResult;
Assert.NotNull(actual);
Assert.Equal(expectedType, actual.CompiledType); Assert.Equal(expectedType, actual.CompiledType);
} }
@ -415,23 +561,28 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
// Arrange // Arrange
var lastModified = DateTime.UtcNow; 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 var fileInfo = new TestFileInfo
{ {
PhysicalPath = "test", PhysicalPath = "test",
LastModified = lastModified LastModified = lastModified
}; };
fileProvider.AddFile("test", fileInfo);
var type = GetType(); var type = GetType();
var uncachedResult = UncachedCompilationResult.Successful(type, "hello world"); var uncachedResult = UncachedCompilationResult.Successful(type, "hello world");
var runtimeFileInfo = new RelativeFileInfo(fileInfo, "test");
// Act // Act
cache.GetOrAdd(runtimeFileInfo, _ => uncachedResult); cache.GetOrAdd("test", _ => uncachedResult);
var actual1 = cache.GetOrAdd(runtimeFileInfo, _ => uncachedResult); var result1 = cache.GetOrAdd("test", _ => uncachedResult);
var actual2 = cache.GetOrAdd(runtimeFileInfo, _ => uncachedResult); var result2 = cache.GetOrAdd("test", _ => uncachedResult);
// Assert // 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, actual1);
Assert.NotSame(uncachedResult, actual2); Assert.NotSame(uncachedResult, actual2);
var result = Assert.IsType<CompilationResult>(actual1); 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 public class RazorViewEngineOptionsTest
{ {
[Fact] [Fact]
public void FileProviderThrows_IfNullIsAsseigned() public void FileProviderThrows_IfNullIsAssigned()
{ {
// Arrange // Arrange
var options = new RazorViewEngineOptions(); var options = new RazorViewEngineOptions();
@ -27,18 +27,18 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
// Arrange // Arrange
var services = new ServiceCollection().AddOptions(); var services = new ServiceCollection().AddOptions();
var timeSpan = new TimeSpan(400); var fileProvider = new TestFileProvider();
// Act // Act
services.ConfigureRazorViewEngineOptions(options => { services.ConfigureRazorViewEngineOptions(options =>
options.ExpirationBeforeCheckingFilesOnDisk = timeSpan; {
options.FileProvider = fileProvider;
}); });
var serviceProvider = services.BuildServiceProvider(); var serviceProvider = services.BuildServiceProvider();
// Assert // Assert
var accessor = serviceProvider.GetRequiredService<IOptions<RazorViewEngineOptions>>(); var accessor = serviceProvider.GetRequiredService<IOptions<RazorViewEngineOptions>>();
var expiration = Assert.IsType<TimeSpan>(accessor.Options.ExpirationBeforeCheckingFilesOnDisk); Assert.Same(fileProvider, accessor.Options.FileProvider);
Assert.Equal(timeSpan, expiration);
} }
} }
} }

View File

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