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 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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
/// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
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.
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue