Remove IRazorFileProviderCache and rely on file expiration triggers to
file change expiry. Fixes #1969
This commit is contained in:
parent
4eb9c777ff
commit
6ef5518f8a
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue