* Remove support for updateable precompiled views.
* Allow precompiled views to be served when source file does not exist in file system. * Cache results for views that do not exist on disk. Fixes #2462 and fixes #2796. Partially addresses #2551
This commit is contained in:
parent
982213e9e0
commit
dfacd2543b
|
|
@ -3,23 +3,22 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.AspNet.FileProviders;
|
using Microsoft.AspNet.FileProviders;
|
||||||
using Microsoft.AspNet.Mvc.Razor.Precompilation;
|
using Microsoft.AspNet.Mvc.Razor.Precompilation;
|
||||||
|
using Microsoft.Dnx.Runtime;
|
||||||
using Microsoft.Framework.Caching.Memory;
|
using Microsoft.Framework.Caching.Memory;
|
||||||
using Microsoft.Framework.Internal;
|
using Microsoft.Framework.Internal;
|
||||||
using Microsoft.Framework.OptionsModel;
|
using Microsoft.Framework.OptionsModel;
|
||||||
using Microsoft.Dnx.Runtime;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Razor.Compilation
|
namespace Microsoft.AspNet.Mvc.Razor.Compilation
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Caches the result of runtime compilation of Razor files for the duration of the app lifetime.
|
/// Caches the result of runtime compilation of Razor files for the duration of the application lifetime.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CompilerCache : ICompilerCache
|
public class CompilerCache : ICompilerCache
|
||||||
{
|
{
|
||||||
private static readonly TypeInfo RazorFileInfoCollectionType = typeof(RazorFileInfoCollection).GetTypeInfo();
|
private static readonly Assembly RazorHostAssembly = typeof(CompilerCache).GetTypeInfo().Assembly;
|
||||||
private readonly IFileProvider _fileProvider;
|
private readonly IFileProvider _fileProvider;
|
||||||
private readonly IMemoryCache _cache;
|
private readonly IMemoryCache _cache;
|
||||||
|
|
||||||
|
|
@ -27,178 +26,79 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
|
||||||
/// Initializes a new instance of <see cref="CompilerCache"/> populated with precompiled views
|
/// Initializes a new instance of <see cref="CompilerCache"/> populated with precompiled views
|
||||||
/// discovered using <paramref name="provider"/>.
|
/// discovered using <paramref name="provider"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="assemblyProvider">The <see cref="IAssemblyProvider"/> that provides assemblies
|
/// <param name="razorFileInfoCollections">The sequence of <see cref="RazorFileInfoCollection"/> that provides
|
||||||
/// for precompiled view discovery.</param>
|
/// information for precompiled view discovery.</param>
|
||||||
/// <param name="loaderContextAccessor">The <see cref="IAssemblyLoadContextAccessor"/>.</param>
|
/// <param name="loaderContextAccessor">The <see cref="IAssemblyLoadContextAccessor"/>.</param>
|
||||||
/// <param name="optionsAccessor">An accessor to the <see cref="RazorViewEngineOptions"/>.</param>
|
/// <param name="optionsAccessor">An accessor to the <see cref="RazorViewEngineOptions"/>.</param>
|
||||||
public CompilerCache(IAssemblyProvider assemblyProvider,
|
public CompilerCache(
|
||||||
IAssemblyLoadContextAccessor loadContextAccessor,
|
IEnumerable<RazorFileInfoCollection> razorFileInfoCollections,
|
||||||
IOptions<RazorViewEngineOptions> optionsAccessor)
|
IAssemblyLoadContextAccessor loadContextAccessor,
|
||||||
: this(GetFileInfos(assemblyProvider.CandidateAssemblies),
|
IOptions<RazorViewEngineOptions> optionsAccessor)
|
||||||
loadContextAccessor.GetLoadContext(RazorFileInfoCollectionType.Assembly),
|
: this(razorFileInfoCollections,
|
||||||
|
loadContextAccessor.GetLoadContext(RazorHostAssembly),
|
||||||
optionsAccessor.Options.FileProvider)
|
optionsAccessor.Options.FileProvider)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
internal CompilerCache(IEnumerable<RazorFileInfoCollection> razorFileInfoCollections,
|
internal CompilerCache(
|
||||||
IAssemblyLoadContext loadContext,
|
IEnumerable<RazorFileInfoCollection> razorFileInfoCollections,
|
||||||
IFileProvider fileProvider)
|
IAssemblyLoadContext loadContext,
|
||||||
|
IFileProvider fileProvider)
|
||||||
{
|
{
|
||||||
_fileProvider = fileProvider;
|
_fileProvider = fileProvider;
|
||||||
_cache = new MemoryCache(new MemoryCacheOptions { CompactOnMemoryPressure = false });
|
_cache = new MemoryCache(new MemoryCacheOptions { CompactOnMemoryPressure = false });
|
||||||
|
|
||||||
var cacheEntries = new List<CompilerCacheEntry>();
|
|
||||||
foreach (var viewCollection in razorFileInfoCollections)
|
foreach (var viewCollection in razorFileInfoCollections)
|
||||||
{
|
{
|
||||||
var containingAssembly = viewCollection.LoadAssembly(loadContext);
|
var containingAssembly = viewCollection.LoadAssembly(loadContext);
|
||||||
foreach (var fileInfo in viewCollection.FileInfos)
|
foreach (var fileInfo in viewCollection.FileInfos)
|
||||||
{
|
{
|
||||||
var viewType = containingAssembly.GetType(fileInfo.FullTypeName);
|
var viewType = containingAssembly.GetType(fileInfo.FullTypeName);
|
||||||
var cacheEntry = new CompilerCacheEntry(fileInfo, viewType);
|
var cacheEntry = new CompilerCacheResult(CompilationResult.Successful(viewType));
|
||||||
|
|
||||||
// There shouldn't be any duplicates and if there are any the first will win.
|
|
||||||
// If the result doesn't match the one on disk its going to recompile anyways.
|
|
||||||
var normalizedPath = NormalizePath(fileInfo.RelativePath);
|
var normalizedPath = NormalizePath(fileInfo.RelativePath);
|
||||||
_cache.Set(
|
_cache.Set(normalizedPath, cacheEntry);
|
||||||
normalizedPath,
|
|
||||||
cacheEntry,
|
|
||||||
GetMemoryCacheEntryOptions(fileInfo.RelativePath));
|
|
||||||
|
|
||||||
cacheEntries.Add(cacheEntry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up _ViewImports
|
|
||||||
foreach (var entry in cacheEntries)
|
|
||||||
{
|
|
||||||
var globalFileLocations = ViewHierarchyUtility.GetViewImportsLocations(entry.RelativePath);
|
|
||||||
foreach (var location in globalFileLocations)
|
|
||||||
{
|
|
||||||
var globalFileEntry = _cache.Get<CompilerCacheEntry>(location);
|
|
||||||
if (globalFileEntry != null)
|
|
||||||
{
|
|
||||||
// Add the composite _ViewImports entry as a dependency.
|
|
||||||
entry.AssociatedGlobalFileEntry = globalFileEntry;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public CompilerCacheResult GetOrAdd([NotNull] string relativePath,
|
public CompilerCacheResult GetOrAdd(
|
||||||
[NotNull] Func<RelativeFileInfo, CompilationResult> compile)
|
[NotNull] string relativePath,
|
||||||
{
|
[NotNull] Func<RelativeFileInfo, CompilationResult> compile)
|
||||||
var result = GetOrAddCore(relativePath, compile);
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
return CompilerCacheResult.FileNotFound;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CompilerCacheResult(result.CompilationResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
private GetOrAddResult GetOrAddCore(string relativePath,
|
|
||||||
Func<RelativeFileInfo, CompilationResult> compile)
|
|
||||||
{
|
{
|
||||||
var normalizedPath = NormalizePath(relativePath);
|
var normalizedPath = NormalizePath(relativePath);
|
||||||
var cacheEntry = _cache.Get<CompilerCacheEntry>(normalizedPath);
|
CompilerCacheResult cacheResult;
|
||||||
if (cacheEntry == null)
|
if (!_cache.TryGetValue(normalizedPath, out cacheResult))
|
||||||
{
|
{
|
||||||
var fileInfo = _fileProvider.GetFileInfo(relativePath);
|
var fileInfo = _fileProvider.GetFileInfo(relativePath);
|
||||||
|
MemoryCacheEntryOptions cacheEntryOptions;
|
||||||
|
CompilerCacheResult cacheResultToCache;
|
||||||
if (!fileInfo.Exists)
|
if (!fileInfo.Exists)
|
||||||
{
|
{
|
||||||
return null;
|
cacheResultToCache = CompilerCacheResult.FileNotFound;
|
||||||
|
cacheResult = CompilerCacheResult.FileNotFound;
|
||||||
|
|
||||||
|
cacheEntryOptions = new MemoryCacheEntryOptions();
|
||||||
|
cacheEntryOptions.AddExpirationTrigger(_fileProvider.Watch(relativePath));
|
||||||
}
|
}
|
||||||
|
else
|
||||||
var relativeFileInfo = new RelativeFileInfo(fileInfo, relativePath);
|
|
||||||
return OnCacheMiss(relativeFileInfo, normalizedPath, compile);
|
|
||||||
}
|
|
||||||
else if (cacheEntry.IsPreCompiled && !cacheEntry.IsValidatedPreCompiled)
|
|
||||||
{
|
|
||||||
// For precompiled views, the first time the entry is read, we need to ensure that no changes were made
|
|
||||||
// either to the file associated with this entry, or any _ViewImports associated with it between the time
|
|
||||||
// the View was precompiled and the time EnsureInitialized was called. For later iterations, we can
|
|
||||||
// rely on expiration triggers ensuring the validity of the entry.
|
|
||||||
|
|
||||||
var fileInfo = _fileProvider.GetFileInfo(relativePath);
|
|
||||||
if (!fileInfo.Exists)
|
|
||||||
{
|
{
|
||||||
return null;
|
var relativeFileInfo = new RelativeFileInfo(fileInfo, relativePath);
|
||||||
|
var compilationResult = compile(relativeFileInfo).EnsureSuccessful();
|
||||||
|
cacheEntryOptions = GetMemoryCacheEntryOptions(relativePath);
|
||||||
|
|
||||||
|
// By default the CompilationResult returned by IRoslynCompiler is an instance of
|
||||||
|
// UncachedCompilationResult. This type has the generated code as a string property and do not want
|
||||||
|
// to cache it. We'll instead cache the unwrapped result.
|
||||||
|
cacheResultToCache = new CompilerCacheResult(
|
||||||
|
CompilationResult.Successful(compilationResult.CompiledType));
|
||||||
|
cacheResult = new CompilerCacheResult(compilationResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
var relativeFileInfo = new RelativeFileInfo(fileInfo, relativePath);
|
_cache.Set(normalizedPath, cacheResultToCache, cacheEntryOptions);
|
||||||
if (cacheEntry.Length != fileInfo.Length)
|
|
||||||
{
|
|
||||||
// Recompile if the file lengths differ
|
|
||||||
return OnCacheMiss(relativeFileInfo, normalizedPath, compile);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AssociatedGlobalFilesChanged(cacheEntry, compile))
|
|
||||||
{
|
|
||||||
// Recompile if _ViewImports have changed since the entry was created.
|
|
||||||
return OnCacheMiss(relativeFileInfo, normalizedPath, compile);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cacheEntry.LastModified == fileInfo.LastModified)
|
|
||||||
{
|
|
||||||
// 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 new GetOrAddResult
|
|
||||||
{
|
|
||||||
CompilationResult = CompilationResult.Successful(cacheEntry.CompiledType),
|
|
||||||
CompilerCacheEntry = cacheEntry
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timestamp doesn't match but it might be because of deployment, compare the hash.
|
|
||||||
if (cacheEntry.IsPreCompiled &&
|
|
||||||
string.Equals(cacheEntry.Hash,
|
|
||||||
RazorFileHash.GetHash(fileInfo, cacheEntry.HashAlgorithmVersion),
|
|
||||||
StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
// Cache hit, but we need to update the entry.
|
|
||||||
// Assigning to LastModified and IsValidatedPreCompiled are atomic operations and will result in safe race
|
|
||||||
// if the entry is being concurrently read or updated.
|
|
||||||
cacheEntry.LastModified = fileInfo.LastModified;
|
|
||||||
cacheEntry.IsValidatedPreCompiled = true;
|
|
||||||
return new GetOrAddResult
|
|
||||||
{
|
|
||||||
CompilationResult = CompilationResult.Successful(cacheEntry.CompiledType),
|
|
||||||
CompilerCacheEntry = cacheEntry
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// it's not a match, recompile
|
|
||||||
return OnCacheMiss(relativeFileInfo, normalizedPath, compile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new GetOrAddResult
|
return cacheResult;
|
||||||
{
|
|
||||||
CompilationResult = CompilationResult.Successful(cacheEntry.CompiledType),
|
|
||||||
CompilerCacheEntry = cacheEntry
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private GetOrAddResult OnCacheMiss(RelativeFileInfo file,
|
|
||||||
string normalizedPath,
|
|
||||||
Func<RelativeFileInfo, CompilationResult> compile)
|
|
||||||
{
|
|
||||||
var compilationResult = compile(file).EnsureSuccessful();
|
|
||||||
|
|
||||||
// Concurrent addition to MemoryCache with the same key result in safe race.
|
|
||||||
var compilerCacheEntry = new CompilerCacheEntry(file, compilationResult.CompiledType);
|
|
||||||
var cacheEntry = _cache.Set<CompilerCacheEntry>(
|
|
||||||
normalizedPath,
|
|
||||||
compilerCacheEntry,
|
|
||||||
GetMemoryCacheEntryOptions(compilerCacheEntry.RelativePath));
|
|
||||||
|
|
||||||
return new GetOrAddResult
|
|
||||||
{
|
|
||||||
CompilationResult = compilationResult,
|
|
||||||
CompilerCacheEntry = cacheEntry
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private MemoryCacheEntryOptions GetMemoryCacheEntryOptions(string relativePath)
|
private MemoryCacheEntryOptions GetMemoryCacheEntryOptions(string relativePath)
|
||||||
|
|
@ -211,37 +111,10 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
|
||||||
{
|
{
|
||||||
options.AddExpirationTrigger(_fileProvider.Watch(location));
|
options.AddExpirationTrigger(_fileProvider.Watch(location));
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool AssociatedGlobalFilesChanged(CompilerCacheEntry entry,
|
|
||||||
Func<RelativeFileInfo, CompilationResult> compile)
|
|
||||||
{
|
|
||||||
var globalFileEntry = GetCompositeGlobalFileEntry(entry.RelativePath, compile);
|
|
||||||
return entry.AssociatedGlobalFileEntry != globalFileEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the entry for the nearest _ViewImports that the file inherits directives from. Since _ViewImports
|
|
||||||
// entries are affected by other _ViewImports entries that are in the path hierarchy, the returned value
|
|
||||||
// represents the composite result of performing a cache check on individual _ViewImports entries.
|
|
||||||
private CompilerCacheEntry GetCompositeGlobalFileEntry(string relativePath,
|
|
||||||
Func<RelativeFileInfo, CompilationResult> compile)
|
|
||||||
{
|
|
||||||
var viewImportsLocations = ViewHierarchyUtility.GetViewImportsLocations(relativePath);
|
|
||||||
foreach (var viewImports in viewImportsLocations)
|
|
||||||
{
|
|
||||||
var getOrAddResult = GetOrAddCore(viewImports, compile);
|
|
||||||
if (getOrAddResult != null)
|
|
||||||
{
|
|
||||||
// This is the nearest _ViewImports that exists on disk.
|
|
||||||
return getOrAddResult.CompilerCacheEntry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No _ViewImports discovered.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizePath(string path)
|
private static string NormalizePath(string path)
|
||||||
{
|
{
|
||||||
// We need to allow for scenarios where the application was precompiled on a machine with forward slashes
|
// We need to allow for scenarios where the application was precompiled on a machine with forward slashes
|
||||||
|
|
@ -252,34 +125,5 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
internal 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; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using Microsoft.AspNet.Mvc.Razor.Precompilation;
|
|
||||||
using Microsoft.Framework.Internal;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Razor.Compilation
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// An entry in <see cref="ICompilerCache"/> that contain metadata about precompiled and dynamically compiled file.
|
|
||||||
/// </summary>
|
|
||||||
public class CompilerCacheEntry
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of <see cref="CompilerCacheEntry"/> for a file that was precompiled.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="info">Metadata about the precompiled file.</param>
|
|
||||||
/// <param name="compiledType">The compiled <see cref="Type"/>.</param>
|
|
||||||
public CompilerCacheEntry([NotNull] RazorFileInfo info, [NotNull] Type compiledType)
|
|
||||||
{
|
|
||||||
CompiledType = compiledType;
|
|
||||||
RelativePath = info.RelativePath;
|
|
||||||
Length = info.Length;
|
|
||||||
LastModified = info.LastModified;
|
|
||||||
Hash = info.Hash;
|
|
||||||
HashAlgorithmVersion = info.HashAlgorithmVersion;
|
|
||||||
IsPreCompiled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of <see cref="CompilerCacheEntry"/> for a file that was dynamically compiled.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="info">Metadata about the file that was compiled.</param>
|
|
||||||
/// <param name="compiledType">The compiled <see cref="Type"/>.</param>
|
|
||||||
public CompilerCacheEntry([NotNull] RelativeFileInfo info, [NotNull] Type compiledType)
|
|
||||||
{
|
|
||||||
CompiledType = compiledType;
|
|
||||||
RelativePath = info.RelativePath;
|
|
||||||
Length = info.FileInfo.Length;
|
|
||||||
LastModified = info.FileInfo.LastModified;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the <see cref="Type"/> produced as a result of compilation.
|
|
||||||
/// </summary>
|
|
||||||
public Type CompiledType { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the path of the compiled file relative to the root of the application.
|
|
||||||
/// </summary>
|
|
||||||
public string RelativePath { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the size of file (in bytes) on disk.
|
|
||||||
/// </summary>
|
|
||||||
public long Length { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the last modified <see cref="DateTimeOffset"/> for the file at the time of compilation.
|
|
||||||
/// </summary>
|
|
||||||
public DateTimeOffset LastModified { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the file hash, should only be available for pre compiled files.
|
|
||||||
/// </summary>
|
|
||||||
public string Hash { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the version of the hash algorithm used to generate <see cref="Hash"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This value is only initialized for precompiled views.
|
|
||||||
/// </remarks>
|
|
||||||
public int HashAlgorithmVersion { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a flag that indicates if the file is precompiled.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsPreCompiled { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the <see cref="CompilerCacheEntry"/> for the nearest _ViewImports that the compiled type
|
|
||||||
/// depends on.
|
|
||||||
/// </summary>
|
|
||||||
public CompilerCacheEntry AssociatedGlobalFileEntry { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a flag that determines if the validity of this cache entry was performed at runtime.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsValidatedPreCompiled { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,10 +2,14 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using Microsoft.AspNet.Mvc;
|
using Microsoft.AspNet.Mvc;
|
||||||
using Microsoft.AspNet.Mvc.Razor;
|
using Microsoft.AspNet.Mvc.Razor;
|
||||||
using Microsoft.AspNet.Mvc.Razor.Compilation;
|
using Microsoft.AspNet.Mvc.Razor.Compilation;
|
||||||
using Microsoft.AspNet.Mvc.Razor.Directives;
|
using Microsoft.AspNet.Mvc.Razor.Directives;
|
||||||
|
using Microsoft.AspNet.Mvc.Razor.Precompilation;
|
||||||
using Microsoft.Framework.Caching.Memory;
|
using Microsoft.Framework.Caching.Memory;
|
||||||
using Microsoft.Framework.Internal;
|
using Microsoft.Framework.Internal;
|
||||||
using Microsoft.Framework.OptionsModel;
|
using Microsoft.Framework.OptionsModel;
|
||||||
|
|
@ -14,6 +18,8 @@ namespace Microsoft.Framework.DependencyInjection
|
||||||
{
|
{
|
||||||
public static class MvcRazorMvcBuilderExtensions
|
public static class MvcRazorMvcBuilderExtensions
|
||||||
{
|
{
|
||||||
|
private static readonly Type RazorFileInfoCollectionType = typeof(RazorFileInfoCollection);
|
||||||
|
|
||||||
public static IMvcBuilder AddRazorViewEngine([NotNull] this IMvcBuilder builder)
|
public static IMvcBuilder AddRazorViewEngine([NotNull] this IMvcBuilder builder)
|
||||||
{
|
{
|
||||||
builder.AddViews();
|
builder.AddViews();
|
||||||
|
|
@ -36,6 +42,28 @@ namespace Microsoft.Framework.DependencyInjection
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IMvcBuilder AddPrecompiledRazorViews(
|
||||||
|
[NotNull] this IMvcBuilder builder,
|
||||||
|
[NotNull] params Assembly[] assemblies)
|
||||||
|
{
|
||||||
|
AddRazorViewEngine(builder);
|
||||||
|
|
||||||
|
var razorFileInfos = GetFileInfoCollections(assemblies);
|
||||||
|
builder.Services.TryAddEnumerable(ServiceDescriptor.Instance(razorFileInfos));
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddPrecompiledRazorViews(
|
||||||
|
[NotNull] this IServiceCollection collection,
|
||||||
|
[NotNull] params Assembly[] assemblies)
|
||||||
|
{
|
||||||
|
var razorFileInfos = GetFileInfoCollections(assemblies);
|
||||||
|
collection.TryAddEnumerable(ServiceDescriptor.Instance(razorFileInfos));
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
public static IMvcBuilder ConfigureRazorViewEngine(
|
public static IMvcBuilder ConfigureRazorViewEngine(
|
||||||
[NotNull] this IMvcBuilder builder,
|
[NotNull] this IMvcBuilder builder,
|
||||||
[NotNull] Action<RazorViewEngineOptions> setupAction)
|
[NotNull] Action<RazorViewEngineOptions> setupAction)
|
||||||
|
|
@ -91,5 +119,17 @@ namespace Microsoft.Framework.DependencyInjection
|
||||||
// Consumed by the Cache tag helper to cache results across the lifetime of the application.
|
// Consumed by the Cache tag helper to cache results across the lifetime of the application.
|
||||||
services.TryAddSingleton<IMemoryCache, MemoryCache>();
|
services.TryAddSingleton<IMemoryCache, MemoryCache>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<RazorFileInfoCollection> GetFileInfoCollections(IEnumerable<Assembly> assemblies) =>
|
||||||
|
assemblies
|
||||||
|
.SelectMany(assembly => assembly.ExportedTypes)
|
||||||
|
.Where(IsValidRazorFileInfoCollection)
|
||||||
|
.Select(Activator.CreateInstance)
|
||||||
|
.Cast<RazorFileInfoCollection>();
|
||||||
|
|
||||||
|
internal static bool IsValidRazorFileInfoCollection(Type type) =>
|
||||||
|
RazorFileInfoCollectionType.IsAssignableFrom(type) &&
|
||||||
|
!type.GetTypeInfo().IsAbstract &&
|
||||||
|
!type.GetTypeInfo().ContainsGenericParameters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Precompilation
|
||||||
public string SymbolsResourceName { get; protected set; }
|
public string SymbolsResourceName { get; protected set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the <see cref="IReadOnlyList{T}{T}"/> of <see cref="RazorFileInfo"/>s.
|
/// Gets the <see cref="IReadOnlyList{T}"/> of <see cref="RazorFileInfo"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<RazorFileInfo> FileInfos { get; protected set; }
|
public IReadOnlyList<RazorFileInfo> FileInfos { get; protected set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
var applicationEnvironment = serviceProvider.GetRequiredService<IApplicationEnvironment>();
|
var applicationEnvironment = serviceProvider.GetRequiredService<IApplicationEnvironment>();
|
||||||
|
|
||||||
var viewsDirectory = Path.Combine(applicationEnvironment.ApplicationBasePath, "Views", "Home");
|
var viewsDirectory = Path.Combine(applicationEnvironment.ApplicationBasePath, "Views", "Home");
|
||||||
var layoutContent = File.ReadAllText(Path.Combine(viewsDirectory, "Layout.cshtml"));
|
|
||||||
var indexContent = File.ReadAllText(Path.Combine(viewsDirectory, "Index.cshtml"));
|
var indexContent = File.ReadAllText(Path.Combine(viewsDirectory, "Index.cshtml"));
|
||||||
var viewstartContent = File.ReadAllText(Path.Combine(viewsDirectory, "_ViewStart.cshtml"));
|
|
||||||
var globalContent = File.ReadAllText(Path.Combine(viewsDirectory, "_ViewImports.cshtml"));
|
|
||||||
|
|
||||||
// We will render a view that writes the fully qualified name of the Assembly containing the type of
|
// We will render a view that writes the fully qualified name of the Assembly containing the type of
|
||||||
// the view. If the view is precompiled, this assembly will be PrecompilationWebsite.
|
// the view. If the view is precompiled, this assembly will be PrecompilationWebsite.
|
||||||
|
|
@ -64,103 +61,19 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
Assert.StartsWith(assemblyNamePrefix, parsedResponse1.Index);
|
Assert.StartsWith(assemblyNamePrefix, parsedResponse1.Index);
|
||||||
|
|
||||||
// Act - 2
|
// Act - 2
|
||||||
// Touch the Layout file and verify it is now dynamically compiled.
|
// Touch the Index file and verify it remains unaffected.
|
||||||
await TouchFile(viewsDirectory, "Layout.cshtml");
|
await TouchFile(viewsDirectory, "Index.cshtml");
|
||||||
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
|
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
|
||||||
|
|
||||||
// Assert - 2
|
// Assert - 2
|
||||||
var response2 = new ParsedResponse(responseContent);
|
var response2 = new ParsedResponse(responseContent);
|
||||||
Assert.StartsWith(assemblyNamePrefix, response2.ViewStart);
|
Assert.StartsWith(assemblyNamePrefix, response2.ViewStart);
|
||||||
Assert.StartsWith(assemblyNamePrefix, response2.Index);
|
Assert.StartsWith(assemblyNamePrefix, response2.Index);
|
||||||
Assert.DoesNotContain(assemblyNamePrefix, response2.Layout);
|
Assert.StartsWith(assemblyNamePrefix, response2.Layout);
|
||||||
|
|
||||||
// Act - 3
|
|
||||||
// Touch the _ViewStart file and verify it is is dynamically compiled.
|
|
||||||
await TouchFile(viewsDirectory, "_ViewStart.cshtml");
|
|
||||||
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
|
|
||||||
|
|
||||||
// Assert - 3
|
|
||||||
var response3 = new ParsedResponse(responseContent);
|
|
||||||
Assert.NotEqual(assemblyNamePrefix, response3.ViewStart);
|
|
||||||
Assert.Equal(response2.Index, response3.Index);
|
|
||||||
Assert.Equal(response2.Layout, response3.Layout);
|
|
||||||
|
|
||||||
// Act - 4
|
|
||||||
// Touch the _ViewImports file and verify it causes all files to recompile.
|
|
||||||
await TouchFile(viewsDirectory, "_ViewImports.cshtml");
|
|
||||||
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
|
|
||||||
|
|
||||||
// Assert - 4
|
|
||||||
var response4 = new ParsedResponse(responseContent);
|
|
||||||
Assert.NotEqual(response3.ViewStart, response4.ViewStart);
|
|
||||||
Assert.NotEqual(response3.Index, response4.Index);
|
|
||||||
Assert.NotEqual(response3.Layout, response4.Layout);
|
|
||||||
|
|
||||||
// Act - 5
|
|
||||||
// Touch Index file and verify it is the only page that recompiles.
|
|
||||||
await TouchFile(viewsDirectory, "Index.cshtml");
|
|
||||||
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
|
|
||||||
|
|
||||||
// Assert - 5
|
|
||||||
var response5 = new ParsedResponse(responseContent);
|
|
||||||
// Layout and _ViewStart should not have changed.
|
|
||||||
Assert.Equal(response4.Layout, response5.Layout);
|
|
||||||
Assert.Equal(response4.ViewStart, response5.ViewStart);
|
|
||||||
Assert.NotEqual(response4.Index, response5.Index);
|
|
||||||
|
|
||||||
// Act - 6
|
|
||||||
// Touch the _ViewImports file. This time, we'll verify the Non-precompiled -> Non-precompiled workflow.
|
|
||||||
await TouchFile(viewsDirectory, "_ViewImports.cshtml");
|
|
||||||
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
|
|
||||||
|
|
||||||
// Assert - 6
|
|
||||||
var response6 = new ParsedResponse(responseContent);
|
|
||||||
// Everything should've recompiled.
|
|
||||||
Assert.NotEqual(response5.ViewStart, response6.ViewStart);
|
|
||||||
Assert.NotEqual(response5.Index, response6.Index);
|
|
||||||
Assert.NotEqual(response5.Layout, response6.Layout);
|
|
||||||
|
|
||||||
// Act - 7
|
|
||||||
// Add a new _ViewImports file
|
|
||||||
var newViewImports = await TouchFile(Path.GetDirectoryName(viewsDirectory), "_ViewImports.cshtml");
|
|
||||||
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
|
|
||||||
|
|
||||||
// Assert - 7
|
|
||||||
// Everything should've recompiled.
|
|
||||||
var response7 = new ParsedResponse(responseContent);
|
|
||||||
Assert.NotEqual(response6.ViewStart, response7.ViewStart);
|
|
||||||
Assert.NotEqual(response6.Index, response7.Index);
|
|
||||||
Assert.NotEqual(response6.Layout, response7.Layout);
|
|
||||||
|
|
||||||
// Act - 8
|
|
||||||
// Remove new _ViewImports file
|
|
||||||
File.Delete(newViewImports);
|
|
||||||
await Task.Delay(_cacheDelayInterval);
|
|
||||||
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
|
|
||||||
|
|
||||||
// Assert - 8
|
|
||||||
// Everything should've recompiled.
|
|
||||||
var response8 = new ParsedResponse(responseContent);
|
|
||||||
Assert.NotEqual(response7.ViewStart, response8.ViewStart);
|
|
||||||
Assert.NotEqual(response7.Index, response8.Index);
|
|
||||||
Assert.NotEqual(response7.Layout, response8.Layout);
|
|
||||||
|
|
||||||
// Act - 9
|
|
||||||
// Refetch and verify we get cached types
|
|
||||||
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
|
|
||||||
|
|
||||||
// Assert - 9
|
|
||||||
var response9 = new ParsedResponse(responseContent);
|
|
||||||
Assert.Equal(response8.ViewStart, response9.ViewStart);
|
|
||||||
Assert.Equal(response8.Index, response9.Index);
|
|
||||||
Assert.Equal(response8.Layout, response9.Layout);
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
File.WriteAllText(Path.Combine(viewsDirectory, "Layout.cshtml"), layoutContent.TrimEnd(' '));
|
|
||||||
File.WriteAllText(Path.Combine(viewsDirectory, "Index.cshtml"), indexContent.TrimEnd(' '));
|
File.WriteAllText(Path.Combine(viewsDirectory, "Index.cshtml"), indexContent.TrimEnd(' '));
|
||||||
File.WriteAllText(Path.Combine(viewsDirectory, "_ViewStart.cshtml"), viewstartContent.TrimEnd(' '));
|
|
||||||
File.WriteAllText(Path.Combine(viewsDirectory, "_ViewImports.cshtml"), globalContent.TrimEnd(' '));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,51 +101,6 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
Assert.StartsWith(expected, responseContent.Trim());
|
Assert.StartsWith(expected, responseContent.Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConditionalTheory]
|
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
|
|
||||||
public async Task DeletingPrecompiledGlobalFile_PriorToFirstRequestToAView_CausesViewToBeRecompiled()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var expected = GetAssemblyNamePrefix();
|
|
||||||
IServiceCollection serviceCollection = null;
|
|
||||||
var server = TestHelper.CreateServer(_app, SiteName, services =>
|
|
||||||
{
|
|
||||||
_configureServices(services);
|
|
||||||
serviceCollection = services;
|
|
||||||
});
|
|
||||||
var client = server.CreateClient();
|
|
||||||
|
|
||||||
var serviceProvider = serviceCollection.BuildServiceProvider();
|
|
||||||
var applicationEnvironment = serviceProvider.GetRequiredService<IApplicationEnvironment>();
|
|
||||||
|
|
||||||
var viewsDirectory = Path.Combine(applicationEnvironment.ApplicationBasePath,
|
|
||||||
"Views",
|
|
||||||
"ViewImportsDelete");
|
|
||||||
var globalPath = Path.Combine(viewsDirectory, "_ViewImports.cshtml");
|
|
||||||
var globalContent = File.ReadAllText(globalPath);
|
|
||||||
|
|
||||||
// Act - 1
|
|
||||||
// Query the Test view so we know the compiler cache gets populated.
|
|
||||||
var response = await client.GetStringAsync("/Test");
|
|
||||||
|
|
||||||
// Assert - 1
|
|
||||||
Assert.Equal("Test", response.Trim());
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Act - 2
|
|
||||||
File.Delete(globalPath);
|
|
||||||
var response2 = await client.GetStringAsync("http://localhost/Home/GlobalDeletedPriorToFirstRequest");
|
|
||||||
|
|
||||||
// Assert - 2
|
|
||||||
Assert.DoesNotContain(expected, response2.Trim());
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
File.WriteAllText(globalPath, globalContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[ConditionalTheory]
|
[ConditionalTheory]
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
|
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
|
||||||
public async Task TagHelpersFromTheApplication_CanBeAdded()
|
public async Task TagHelpersFromTheApplication_CanBeAdded()
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,10 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
|
||||||
using Microsoft.AspNet.FileProviders;
|
|
||||||
using Microsoft.AspNet.Mvc.Razor.Internal;
|
|
||||||
using Microsoft.AspNet.Mvc.Razor.Precompilation;
|
using Microsoft.AspNet.Mvc.Razor.Precompilation;
|
||||||
using Microsoft.AspNet.Testing.xunit;
|
|
||||||
using Microsoft.Dnx.Runtime;
|
using Microsoft.Dnx.Runtime;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -20,19 +15,27 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
|
||||||
{
|
{
|
||||||
public class CompilerCacheTest
|
public class CompilerCacheTest
|
||||||
{
|
{
|
||||||
private const string ViewPath = "view-path";
|
private const string ViewPath = "Views/Home/Index.cshtml";
|
||||||
|
private const string PrecompiledViewsPath = "Views/Home/Precompiled.cshtml";
|
||||||
private readonly IAssemblyLoadContext TestLoadContext = Mock.Of<IAssemblyLoadContext>();
|
private readonly IAssemblyLoadContext TestLoadContext = Mock.Of<IAssemblyLoadContext>();
|
||||||
|
|
||||||
|
public static TheoryData ViewImportsPaths =>
|
||||||
|
new TheoryData<string>
|
||||||
|
{
|
||||||
|
Path.Combine("Views", "Home", "_ViewImports.cshtml"),
|
||||||
|
Path.Combine("Views", "_ViewImports.cshtml"),
|
||||||
|
"_ViewImports.cshtml",
|
||||||
|
};
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetOrAdd_ReturnsFileNotFoundResult_IfFileIsNotFoundInFileSystem()
|
public void GetOrAdd_ReturnsFileNotFoundResult_IfFileIsNotFoundInFileSystem()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var fileProvider = new TestFileProvider();
|
var fileProvider = new TestFileProvider();
|
||||||
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
|
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
|
||||||
var type = GetType();
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = cache.GetOrAdd("/some/path", _ => { throw new Exception("Shouldn't be called"); });
|
var result = cache.GetOrAdd("/some/path", ThrowsIfCalled);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(CompilerCacheResult.FileNotFound, result);
|
Assert.Same(CompilerCacheResult.FileNotFound, result);
|
||||||
|
|
@ -46,7 +49,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
|
||||||
var fileProvider = new TestFileProvider();
|
var fileProvider = new TestFileProvider();
|
||||||
fileProvider.AddFile(ViewPath, "some content");
|
fileProvider.AddFile(ViewPath, "some content");
|
||||||
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
|
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
|
||||||
var type = GetType();
|
var type = typeof(TestView);
|
||||||
var expected = UncachedCompilationResult.Successful(type, "hello world");
|
var expected = UncachedCompilationResult.Successful(type, "hello world");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -68,7 +71,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
|
||||||
var fileProvider = new TestFileProvider();
|
var fileProvider = new TestFileProvider();
|
||||||
fileProvider.AddFile(ViewPath, "some content");
|
fileProvider.AddFile(ViewPath, "some content");
|
||||||
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
|
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
|
||||||
var type = typeof(RuntimeCompileIdentical);
|
var type = typeof(TestView);
|
||||||
var expected = UncachedCompilationResult.Successful(type, "hello world");
|
var expected = UncachedCompilationResult.Successful(type, "hello world");
|
||||||
|
|
||||||
// Act 1
|
// Act 1
|
||||||
|
|
@ -82,7 +85,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
|
||||||
// Delete the file from the file system and set it's expiration trigger.
|
// Delete the file from the file system and set it's expiration trigger.
|
||||||
fileProvider.DeleteFile(ViewPath);
|
fileProvider.DeleteFile(ViewPath);
|
||||||
fileProvider.GetTrigger(ViewPath).IsExpired = true;
|
fileProvider.GetTrigger(ViewPath).IsExpired = true;
|
||||||
var result2 = cache.GetOrAdd(ViewPath, _ => { throw new Exception("shouldn't be called."); });
|
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
|
||||||
|
|
||||||
// Assert 2
|
// Assert 2
|
||||||
Assert.Same(CompilerCacheResult.FileNotFound, result2);
|
Assert.Same(CompilerCacheResult.FileNotFound, result2);
|
||||||
|
|
@ -96,9 +99,8 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
|
||||||
var fileProvider = new TestFileProvider();
|
var fileProvider = new TestFileProvider();
|
||||||
fileProvider.AddFile(ViewPath, "some content");
|
fileProvider.AddFile(ViewPath, "some content");
|
||||||
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
|
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
|
||||||
var type = typeof(RuntimeCompileIdentical);
|
var expected1 = UncachedCompilationResult.Successful(typeof(TestView), "hello world");
|
||||||
var expected1 = UncachedCompilationResult.Successful(type, "hello world");
|
var expected2 = UncachedCompilationResult.Successful(typeof(DifferentView), "different content");
|
||||||
var expected2 = UncachedCompilationResult.Successful(type, "different content");
|
|
||||||
|
|
||||||
// Act 1
|
// Act 1
|
||||||
var result1 = cache.GetOrAdd(ViewPath, _ => expected1);
|
var result1 = cache.GetOrAdd(ViewPath, _ => expected1);
|
||||||
|
|
@ -108,12 +110,55 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
|
||||||
Assert.Same(expected1, result1.CompilationResult);
|
Assert.Same(expected1, result1.CompilationResult);
|
||||||
|
|
||||||
// Act 2
|
// Act 2
|
||||||
fileProvider.GetTrigger(ViewPath).IsExpired = true;
|
// Verify we're getting cached results.
|
||||||
var result2 = cache.GetOrAdd(ViewPath, _ => expected2);
|
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
|
||||||
|
|
||||||
// Assert 2
|
// Assert 2
|
||||||
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
|
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
|
||||||
Assert.Same(expected2, result2.CompilationResult);
|
Assert.Same(expected1.CompiledType, result2.CompilationResult.CompiledType);
|
||||||
|
|
||||||
|
// Act 3
|
||||||
|
fileProvider.GetTrigger(ViewPath).IsExpired = true;
|
||||||
|
var result3 = cache.GetOrAdd(ViewPath, _ => expected2);
|
||||||
|
|
||||||
|
// Assert 3
|
||||||
|
Assert.NotSame(CompilerCacheResult.FileNotFound, result3);
|
||||||
|
Assert.Same(expected2, result3.CompilationResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ViewImportsPaths))]
|
||||||
|
public void GetOrAdd_ReturnsNewResult_IfAncestorViewImportsWereModified(string globalImportPath)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var fileProvider = new TestFileProvider();
|
||||||
|
fileProvider.AddFile(ViewPath, "some content");
|
||||||
|
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
|
||||||
|
var expected1 = UncachedCompilationResult.Successful(typeof(TestView), "hello world");
|
||||||
|
var expected2 = UncachedCompilationResult.Successful(typeof(DifferentView), "different content");
|
||||||
|
|
||||||
|
// Act 1
|
||||||
|
var result1 = cache.GetOrAdd(ViewPath, _ => expected1);
|
||||||
|
|
||||||
|
// Assert 1
|
||||||
|
Assert.NotSame(CompilerCacheResult.FileNotFound, result1);
|
||||||
|
Assert.Same(expected1, result1.CompilationResult);
|
||||||
|
|
||||||
|
// Act 2
|
||||||
|
// Verify we're getting cached results.
|
||||||
|
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
|
||||||
|
|
||||||
|
// Assert 2
|
||||||
|
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
|
||||||
|
Assert.Same(expected1.CompiledType, result2.CompilationResult.CompiledType);
|
||||||
|
|
||||||
|
// Act 3
|
||||||
|
fileProvider.GetTrigger(globalImportPath).IsExpired = true;
|
||||||
|
var result3 = cache.GetOrAdd(ViewPath, _ => expected2);
|
||||||
|
|
||||||
|
// Assert 2
|
||||||
|
Assert.NotSame(CompilerCacheResult.FileNotFound, result3);
|
||||||
|
Assert.Same(expected2, result3.CompilationResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -124,7 +169,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
|
||||||
var fileProvider = mockFileProvider.Object;
|
var fileProvider = mockFileProvider.Object;
|
||||||
fileProvider.AddFile(ViewPath, "some content");
|
fileProvider.AddFile(ViewPath, "some content");
|
||||||
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
|
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
|
||||||
var type = typeof(RuntimeCompileIdentical);
|
var type = typeof(TestView);
|
||||||
var expected = UncachedCompilationResult.Successful(type, "hello world");
|
var expected = UncachedCompilationResult.Successful(type, "hello world");
|
||||||
|
|
||||||
// Act 1
|
// Act 1
|
||||||
|
|
@ -135,7 +180,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
|
||||||
Assert.Same(expected, result1.CompilationResult);
|
Assert.Same(expected, result1.CompilationResult);
|
||||||
|
|
||||||
// Act 2
|
// Act 2
|
||||||
var result2 = cache.GetOrAdd(ViewPath, _ => { throw new Exception("shouldn't be called"); });
|
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
|
||||||
|
|
||||||
// Assert 2
|
// Assert 2
|
||||||
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
|
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
|
||||||
|
|
@ -144,543 +189,120 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
|
||||||
mockFileProvider.Verify(v => v.GetFileInfo(ViewPath), Times.Once());
|
mockFileProvider.Verify(v => v.GetFileInfo(ViewPath), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
private abstract class View
|
[Fact]
|
||||||
|
public void GetOrAdd_UsesViewsSpecifiedFromRazorFileInfoCollection()
|
||||||
{
|
{
|
||||||
public abstract string Content { get; }
|
// Arrange
|
||||||
|
var fileProvider = new TestFileProvider();
|
||||||
|
var cache = new CompilerCache(new[] { new TestViewCollection() }, TestLoadContext, fileProvider);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
|
||||||
|
Assert.Same(typeof(PreCompile), result.CompilationResult.CompiledType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PreCompile : View
|
[Fact]
|
||||||
|
public void GetOrAdd_DoesNotRecompile_IfFileTriggerWasSetForPrecompiledFile()
|
||||||
{
|
{
|
||||||
public override string Content { get { return "Hello World it's @DateTime.Now"; } }
|
// Arrange
|
||||||
}
|
var fileProvider = new TestFileProvider();
|
||||||
|
var cache = new CompilerCache(new[] { new TestViewCollection() }, TestLoadContext, fileProvider);
|
||||||
|
|
||||||
private class RuntimeCompileIdentical : View
|
// Act
|
||||||
{
|
fileProvider.Watch(PrecompiledViewsPath);
|
||||||
public override string Content { get { return new PreCompile().Content; } }
|
fileProvider.GetTrigger(PrecompiledViewsPath).IsExpired = true;
|
||||||
}
|
var result = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled);
|
||||||
|
|
||||||
private class RuntimeCompileDifferent : View
|
// Assert
|
||||||
{
|
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
|
||||||
public override string Content { get { return new PreCompile().Content.Substring(1) + " "; } }
|
Assert.Same(typeof(PreCompile), result.CompilationResult.CompiledType);
|
||||||
}
|
|
||||||
|
|
||||||
private class RuntimeCompileDifferentLength : View
|
|
||||||
{
|
|
||||||
public override string Content
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return new PreCompile().Content + " longer because it was modified at runtime";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Stream GetMemoryStream(string content)
|
|
||||||
{
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(content);
|
|
||||||
|
|
||||||
return new MemoryStream(bytes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(10000)]
|
[MemberData(nameof(ViewImportsPaths))]
|
||||||
[InlineData(11000)]
|
public void GetOrAdd_DoesNotRecompile_IfFileTriggerWasSetForViewImports(string globalImportPath)
|
||||||
public void GetOrAdd_UsesFilesFromCache_IfTimestampDiffers_ButContentAndLengthAreTheSame(long fileTimeUTC)
|
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var instance = new RuntimeCompileIdentical();
|
|
||||||
var length = Encoding.UTF8.GetByteCount(instance.Content);
|
|
||||||
var fileProvider = new TestFileProvider();
|
var fileProvider = new TestFileProvider();
|
||||||
var cache = new CompilerCache(new[] { new ViewCollection() }, TestLoadContext, fileProvider);
|
var cache = new CompilerCache(new[] { new TestViewCollection() }, TestLoadContext, fileProvider);
|
||||||
var fileInfo = new TestFileInfo
|
|
||||||
{
|
|
||||||
Length = length,
|
|
||||||
LastModified = DateTime.FromFileTimeUtc(fileTimeUTC),
|
|
||||||
Content = instance.Content
|
|
||||||
};
|
|
||||||
fileProvider.AddFile(ViewPath, fileInfo);
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = cache.GetOrAdd(ViewPath,
|
fileProvider.Watch(globalImportPath);
|
||||||
compile: _ => { throw new Exception("Shouldn't be called."); });
|
fileProvider.GetTrigger(globalImportPath).IsExpired = true;
|
||||||
|
var result = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
|
Assert.NotSame(CompilerCacheResult.FileNotFound, result);
|
||||||
var actual = result.CompilationResult;
|
Assert.Same(typeof(PreCompile), result.CompilationResult.CompiledType);
|
||||||
Assert.NotNull(actual);
|
|
||||||
Assert.Equal(typeof(PreCompile), actual.CompiledType);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(typeof(RuntimeCompileDifferent), 11000)]
|
|
||||||
[InlineData(typeof(RuntimeCompileDifferentLength), 10000)]
|
|
||||||
[InlineData(typeof(RuntimeCompileDifferentLength), 11000)]
|
|
||||||
public void GetOrAdd_RecompilesFile_IfContentAndLengthAreChanged(
|
|
||||||
Type resultViewType,
|
|
||||||
long fileTimeUTC)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var instance = (View)Activator.CreateInstance(resultViewType);
|
|
||||||
var length = Encoding.UTF8.GetByteCount(instance.Content);
|
|
||||||
var fileProvider = new TestFileProvider();
|
|
||||||
var cache = new CompilerCache(new[] { new ViewCollection() }, TestLoadContext, fileProvider);
|
|
||||||
|
|
||||||
var fileInfo = new TestFileInfo
|
|
||||||
{
|
|
||||||
Length = length,
|
|
||||||
LastModified = DateTime.FromFileTimeUtc(fileTimeUTC),
|
|
||||||
Content = instance.Content
|
|
||||||
};
|
|
||||||
fileProvider.AddFile(ViewPath, fileInfo);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetOrAdd_UsesValueFromCache_IfGlobalHasNotChanged()
|
public void GetOrAdd_ReturnsRuntimeCompiledAndPrecompiledViews()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var instance = new PreCompile();
|
|
||||||
var length = Encoding.UTF8.GetByteCount(instance.Content);
|
|
||||||
var fileProvider = new TestFileProvider();
|
var fileProvider = new TestFileProvider();
|
||||||
|
fileProvider.AddFile(ViewPath, "some content");
|
||||||
var lastModified = DateTime.UtcNow;
|
var cache = new CompilerCache(new[] { new TestViewCollection() }, TestLoadContext, fileProvider);
|
||||||
|
var expected = CompilationResult.Successful(typeof(TestView));
|
||||||
var fileInfo = new TestFileInfo
|
|
||||||
{
|
|
||||||
Length = length,
|
|
||||||
LastModified = lastModified,
|
|
||||||
Content = instance.Content
|
|
||||||
};
|
|
||||||
fileProvider.AddFile(ViewPath, fileInfo);
|
|
||||||
|
|
||||||
var globalContent = "global-content";
|
|
||||||
var globalFileInfo = new TestFileInfo
|
|
||||||
{
|
|
||||||
Content = globalContent,
|
|
||||||
LastModified = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
fileProvider.AddFile("_ViewImports.cshtml", globalFileInfo);
|
|
||||||
var globalRazorFileInfo = new RazorFileInfo
|
|
||||||
{
|
|
||||||
Hash = Crc32.Calculate(GetMemoryStream(globalContent)).ToString(CultureInfo.InvariantCulture),
|
|
||||||
HashAlgorithmVersion = 1,
|
|
||||||
LastModified = globalFileInfo.LastModified,
|
|
||||||
Length = globalFileInfo.Length,
|
|
||||||
RelativePath = "_ViewImports.cshtml",
|
|
||||||
FullTypeName = typeof(RuntimeCompileIdentical).FullName
|
|
||||||
};
|
|
||||||
var precompiledViews = new ViewCollection();
|
|
||||||
precompiledViews.Add(globalRazorFileInfo);
|
|
||||||
var cache = new CompilerCache(new[] { precompiledViews }, TestLoadContext, fileProvider);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
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 cache = new CompilerCache(new[] { precompiledViews }, TestLoadContext, 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 }, TestLoadContext, fileProvider);
|
|
||||||
|
|
||||||
// Act 1
|
// Act 1
|
||||||
var result1 = cache.GetOrAdd(ViewPath,
|
var result1 = cache.GetOrAdd(ViewPath, _ => expected);
|
||||||
compile: _ => { throw new Exception("shouldn't be invoked"); });
|
|
||||||
|
|
||||||
// Assert 1
|
// Assert 1
|
||||||
Assert.NotSame(CompilerCacheResult.FileNotFound, result1);
|
Assert.Same(expected, result1.CompilationResult);
|
||||||
var actual1 = result1.CompilationResult;
|
|
||||||
Assert.NotNull(actual1);
|
|
||||||
Assert.Equal(typeof(PreCompile), actual1.CompiledType);
|
|
||||||
mockFileProvider.Verify(v => v.GetFileInfo(ViewPath), Times.Once());
|
|
||||||
|
|
||||||
// Act 2
|
// Act 2
|
||||||
var result2 = cache.GetOrAdd(ViewPath,
|
var result2 = cache.GetOrAdd(ViewPath, ThrowsIfCalled);
|
||||||
compile: _ => { throw new Exception("shouldn't be invoked"); });
|
|
||||||
|
|
||||||
// Assert 2
|
// Assert 2
|
||||||
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
|
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
|
||||||
var actual2 = result2.CompilationResult;
|
Assert.Same(typeof(TestView), result2.CompilationResult.CompiledType);
|
||||||
Assert.NotNull(actual2);
|
|
||||||
Assert.Equal(typeof(PreCompile), actual2.CompiledType);
|
|
||||||
mockFileProvider.Verify(v => v.GetFileInfo(ViewPath), Times.Once());
|
|
||||||
}
|
|
||||||
|
|
||||||
[ConditionalTheory]
|
// Act 3
|
||||||
// Skipping for now since this is going to change.
|
var result3 = cache.GetOrAdd(PrecompiledViewsPath, ThrowsIfCalled);
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
|
|
||||||
public void GetOrAdd_IgnoresCachedValueIfFileIsIdentical_ButViewImportsWasAdedSinceTheCacheWasCreated()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var expectedType = typeof(RuntimeCompileDifferent);
|
|
||||||
var fileProvider = new TestFileProvider();
|
|
||||||
var collection = new ViewCollection();
|
|
||||||
var precompiledFile = collection.FileInfos[0];
|
|
||||||
precompiledFile.RelativePath = "Views\\home\\index.cshtml";
|
|
||||||
var cache = new CompilerCache(new[] { collection }, TestLoadContext, fileProvider);
|
|
||||||
var testFile = new TestFileInfo
|
|
||||||
{
|
|
||||||
Content = new PreCompile().Content,
|
|
||||||
LastModified = precompiledFile.LastModified,
|
|
||||||
PhysicalPath = precompiledFile.RelativePath
|
|
||||||
};
|
|
||||||
fileProvider.AddFile(precompiledFile.RelativePath, testFile);
|
|
||||||
var relativeFile = new RelativeFileInfo(testFile, testFile.PhysicalPath);
|
|
||||||
|
|
||||||
// Act 1
|
// Assert 3
|
||||||
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 globalTrigger = fileProvider.GetTrigger("Views\\_ViewImports.cshtml");
|
|
||||||
globalTrigger.IsExpired = true;
|
|
||||||
var result2 = cache.GetOrAdd(testFile.PhysicalPath,
|
|
||||||
compile: _ => CompilationResult.Successful(expectedType));
|
|
||||||
|
|
||||||
// Assert 2
|
|
||||||
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
|
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
|
||||||
var actual2 = result2.CompilationResult;
|
Assert.Same(typeof(PreCompile), result3.CompilationResult.CompiledType);
|
||||||
Assert.NotNull(actual2);
|
|
||||||
Assert.Equal(expectedType, actual2.CompiledType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConditionalTheory]
|
private class TestView
|
||||||
// Skipping for now since this is going to change.
|
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
|
|
||||||
public void GetOrAdd_IgnoresCachedValueIfFileIsIdentical_ButGlobalWasDeletedSinceCacheWasCreated()
|
|
||||||
{
|
{
|
||||||
// Arrange
|
|
||||||
var expectedType = typeof(RuntimeCompileDifferent);
|
|
||||||
var lastModified = DateTime.UtcNow;
|
|
||||||
var fileProvider = new TestFileProvider();
|
|
||||||
|
|
||||||
var viewCollection = new ViewCollection();
|
|
||||||
var precompiledView = viewCollection.FileInfos[0];
|
|
||||||
precompiledView.RelativePath = "Views\\Index.cshtml";
|
|
||||||
var viewFileInfo = new TestFileInfo
|
|
||||||
{
|
|
||||||
Content = new PreCompile().Content,
|
|
||||||
LastModified = precompiledView.LastModified,
|
|
||||||
PhysicalPath = precompiledView.RelativePath
|
|
||||||
};
|
|
||||||
fileProvider.AddFile(viewFileInfo.PhysicalPath, viewFileInfo);
|
|
||||||
|
|
||||||
var globalFileInfo = new TestFileInfo
|
|
||||||
{
|
|
||||||
PhysicalPath = "Views\\_ViewImports.cshtml",
|
|
||||||
Content = "viewstart-content",
|
|
||||||
LastModified = lastModified
|
|
||||||
};
|
|
||||||
var globalFile = new RazorFileInfo
|
|
||||||
{
|
|
||||||
FullTypeName = typeof(RuntimeCompileIdentical).FullName,
|
|
||||||
RelativePath = globalFileInfo.PhysicalPath,
|
|
||||||
LastModified = globalFileInfo.LastModified,
|
|
||||||
Hash = RazorFileHash.GetHash(globalFileInfo, hashAlgorithmVersion: 1),
|
|
||||||
HashAlgorithmVersion = 1,
|
|
||||||
Length = globalFileInfo.Length
|
|
||||||
};
|
|
||||||
fileProvider.AddFile(globalFileInfo.PhysicalPath, globalFileInfo);
|
|
||||||
|
|
||||||
viewCollection.Add(globalFile);
|
|
||||||
var cache = new CompilerCache(new[] { viewCollection }, TestLoadContext, fileProvider);
|
|
||||||
|
|
||||||
// Act 1
|
|
||||||
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(globalFileInfo.PhysicalPath);
|
|
||||||
trigger.IsExpired = true;
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<object[]> GetOrAdd_IgnoresCachedValue_IfGlobalWasChangedSinceCacheWasCreatedData
|
private class PreCompile
|
||||||
{
|
{
|
||||||
get
|
}
|
||||||
{
|
|
||||||
var globalContent = "global-content";
|
|
||||||
var contentStream = GetMemoryStream(globalContent);
|
|
||||||
var lastModified = DateTime.UtcNow;
|
|
||||||
int length = Encoding.UTF8.GetByteCount(globalContent);
|
|
||||||
var path = "Views\\_ViewImports.cshtml";
|
|
||||||
|
|
||||||
var razorFileInfo = new RazorFileInfo
|
public class DifferentView
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompilationResult ThrowsIfCalled(RelativeFileInfo file)
|
||||||
|
{
|
||||||
|
throw new Exception("Shouldn't be called");
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestViewCollection : RazorFileInfoCollection
|
||||||
|
{
|
||||||
|
public TestViewCollection()
|
||||||
|
{
|
||||||
|
FileInfos = new List<RazorFileInfo>
|
||||||
{
|
{
|
||||||
Hash = Crc32.Calculate(contentStream).ToString(CultureInfo.InvariantCulture),
|
new RazorFileInfo
|
||||||
HashAlgorithmVersion = 1,
|
{
|
||||||
LastModified = lastModified,
|
FullTypeName = typeof(PreCompile).FullName,
|
||||||
Length = length,
|
RelativePath = PrecompiledViewsPath,
|
||||||
RelativePath = path
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Length does not match
|
|
||||||
var testFileInfo1 = new TestFileInfo
|
|
||||||
{
|
|
||||||
Length = 7732
|
|
||||||
};
|
|
||||||
|
|
||||||
yield return new object[] { razorFileInfo, testFileInfo1 };
|
|
||||||
|
|
||||||
// Content and last modified do not match
|
|
||||||
var testFileInfo2 = new TestFileInfo
|
|
||||||
{
|
|
||||||
Length = length,
|
|
||||||
Content = "viewstart-modified",
|
|
||||||
LastModified = lastModified.AddSeconds(100),
|
|
||||||
};
|
|
||||||
|
|
||||||
yield return new object[] { razorFileInfo, testFileInfo2 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[MemberData(nameof(GetOrAdd_IgnoresCachedValue_IfGlobalWasChangedSinceCacheWasCreatedData))]
|
|
||||||
public void GetOrAdd_IgnoresCachedValue_IfGlobalFileWasChangedSinceCacheWasCreated(
|
|
||||||
RazorFileInfo viewStartRazorFileInfo, IFileInfo globalFileInfo)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var expectedType = typeof(RuntimeCompileDifferent);
|
|
||||||
var lastModified = DateTime.UtcNow;
|
|
||||||
var viewStartLastModified = DateTime.UtcNow;
|
|
||||||
var content = "some content";
|
|
||||||
var fileInfo = new TestFileInfo
|
|
||||||
{
|
|
||||||
Length = 1020,
|
|
||||||
Content = content,
|
|
||||||
LastModified = lastModified,
|
|
||||||
PhysicalPath = "Views\\home\\index.cshtml"
|
|
||||||
};
|
|
||||||
|
|
||||||
var fileProvider = new TestFileProvider();
|
|
||||||
fileProvider.AddFile(fileInfo.PhysicalPath, fileInfo);
|
|
||||||
fileProvider.AddFile(viewStartRazorFileInfo.RelativePath, globalFileInfo);
|
|
||||||
var viewCollection = new ViewCollection();
|
|
||||||
var cache = new CompilerCache(new[] { viewCollection }, TestLoadContext, fileProvider);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetOrAdd_DoesNotCacheCompiledContent_OnCallsAfterInitial()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var lastModified = DateTime.UtcNow;
|
|
||||||
var fileProvider = new TestFileProvider();
|
|
||||||
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
|
|
||||||
var fileInfo = new TestFileInfo
|
|
||||||
{
|
|
||||||
PhysicalPath = "test",
|
|
||||||
LastModified = lastModified
|
|
||||||
};
|
|
||||||
fileProvider.AddFile("test", fileInfo);
|
|
||||||
var type = GetType();
|
|
||||||
var uncachedResult = UncachedCompilationResult.Successful(type, "hello world");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
cache.GetOrAdd("test", _ => uncachedResult);
|
|
||||||
var result1 = cache.GetOrAdd("test", _ => { throw new Exception("shouldn't be called."); });
|
|
||||||
var result2 = cache.GetOrAdd("test", _ => { throw new Exception("shouldn't be called."); });
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotSame(CompilerCacheResult.FileNotFound, result1);
|
|
||||||
Assert.NotSame(CompilerCacheResult.FileNotFound, result2);
|
|
||||||
|
|
||||||
var actual1 = Assert.IsType<CompilationResult>(result1.CompilationResult);
|
|
||||||
var actual2 = Assert.IsType<CompilationResult>(result2.CompilationResult);
|
|
||||||
Assert.NotSame(uncachedResult, actual1);
|
|
||||||
Assert.NotSame(uncachedResult, actual2);
|
|
||||||
Assert.Same(type, actual1.CompiledType);
|
|
||||||
Assert.Same(type, actual2.CompiledType);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Match_ReturnsFalse_IfTypeIsAbstract()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var type = typeof(AbstractRazorFileInfoCollection);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = CompilerCache.Match(type);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.False(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Match_ReturnsFalse_IfTypeHasGenericParameters()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var type = typeof(GenericRazorFileInfoCollection<>);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = CompilerCache.Match(type);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.False(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Match_ReturnsFalse_IfTypeDoesNotHaveDefaultConstructor()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var type = typeof(ParameterConstructorRazorFileInfoCollection);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = CompilerCache.Match(type);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.False(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Match_ReturnsFalse_IfTypeDoesNotDeriveFromRazorFileInfoCollection()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var type = typeof(NonSubTypeRazorFileInfoCollection);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = CompilerCache.Match(type);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.False(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Match_ReturnsTrue_IfTypeDerivesFromRazorFileInfoCollection()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var type = typeof(ViewCollection);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = CompilerCache.Match(type);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
private abstract class AbstractRazorFileInfoCollection : RazorFileInfoCollection
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private class GenericRazorFileInfoCollection<TVal> : RazorFileInfoCollection
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ParameterConstructorRazorFileInfoCollection : RazorFileInfoCollection
|
|
||||||
{
|
|
||||||
public ParameterConstructorRazorFileInfoCollection(string value)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class NonSubTypeRazorFileInfoCollection : Controller
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ViewCollection : RazorFileInfoCollection
|
|
||||||
{
|
|
||||||
private readonly List<RazorFileInfo> _fileInfos = new List<RazorFileInfo>();
|
|
||||||
|
|
||||||
public ViewCollection()
|
|
||||||
{
|
|
||||||
FileInfos = _fileInfos;
|
|
||||||
|
|
||||||
var content = new PreCompile().Content;
|
|
||||||
var length = Encoding.UTF8.GetByteCount(content);
|
|
||||||
|
|
||||||
Add(new RazorFileInfo()
|
|
||||||
{
|
|
||||||
FullTypeName = typeof(PreCompile).FullName,
|
|
||||||
Hash = Crc32.Calculate(GetMemoryStream(content)).ToString(CultureInfo.InvariantCulture),
|
|
||||||
HashAlgorithmVersion = 1,
|
|
||||||
LastModified = DateTime.FromFileTimeUtc(10000),
|
|
||||||
Length = length,
|
|
||||||
RelativePath = ViewPath,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(RazorFileInfo fileInfo)
|
|
||||||
{
|
|
||||||
_fileInfos.Add(fileInfo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Assembly LoadAssembly(IAssemblyLoadContext loadContext)
|
public override Assembly LoadAssembly(IAssemblyLoadContext loadContext)
|
||||||
{
|
{
|
||||||
return typeof(ViewCollection).Assembly;
|
return typeof(TestViewCollection).Assembly;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Microsoft.AspNet.Mvc;
|
||||||
|
using Microsoft.AspNet.Mvc.Razor.Precompilation;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Framework.DependencyInjection
|
||||||
|
{
|
||||||
|
public class MvcRazorMvcBuilderExtensionsTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void IsValidRazorFileInfoCollection_ReturnsFalse_IfTypeIsAbstract()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(AbstractRazorFileInfoCollection);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = MvcRazorMvcBuilderExtensions.IsValidRazorFileInfoCollection(type);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsValidRazorFileInfoCollection_ReturnsFalse_IfTypeHasGenericParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(GenericRazorFileInfoCollection<>);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = MvcRazorMvcBuilderExtensions.IsValidRazorFileInfoCollection(type);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsValidRazorFileInfoCollection_ReturnsFalse_IfTypeDoesNotDeriveFromRazorFileInfoCollection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var type = typeof(NonSubTypeRazorFileInfoCollection);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = MvcRazorMvcBuilderExtensions.IsValidRazorFileInfoCollection(type);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(ParameterConstructorRazorFileInfoCollection))]
|
||||||
|
[InlineData(typeof(ViewCollection))]
|
||||||
|
public void IsValidRazorFileInfoCollection_ReturnsTrue_IfTypeDerivesFromRazorFileInfoCollection(Type type)
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = MvcRazorMvcBuilderExtensions.IsValidRazorFileInfoCollection(type);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract class AbstractRazorFileInfoCollection : RazorFileInfoCollection
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GenericRazorFileInfoCollection<TVal> : RazorFileInfoCollection
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ParameterConstructorRazorFileInfoCollection : RazorFileInfoCollection
|
||||||
|
{
|
||||||
|
public ParameterConstructorRazorFileInfoCollection(string value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NonSubTypeRazorFileInfoCollection : Controller
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ViewCollection : RazorFileInfoCollection
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// 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.Reflection;
|
||||||
using Microsoft.AspNet.Builder;
|
using Microsoft.AspNet.Builder;
|
||||||
using Microsoft.Framework.DependencyInjection;
|
using Microsoft.Framework.DependencyInjection;
|
||||||
|
|
||||||
|
|
@ -12,7 +13,9 @@ namespace PrecompilationWebSite
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
// Add MVC services to the services container
|
// Add MVC services to the services container
|
||||||
services.AddMvc();
|
services
|
||||||
|
.AddMvc()
|
||||||
|
.AddPrecompiledRazorViews(GetType().GetTypeInfo().Assembly);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Configure(IApplicationBuilder app)
|
public void Configure(IApplicationBuilder app)
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,22 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// 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 Microsoft.AspNet.Mvc;
|
using System.Collections.Generic;
|
||||||
using Microsoft.AspNet.Mvc.Razor;
|
using Microsoft.AspNet.Mvc.Razor;
|
||||||
using Microsoft.AspNet.Mvc.Razor.Compilation;
|
using Microsoft.AspNet.Mvc.Razor.Compilation;
|
||||||
using Microsoft.Framework.OptionsModel;
|
using Microsoft.AspNet.Mvc.Razor.Precompilation;
|
||||||
using Microsoft.Dnx.Runtime;
|
using Microsoft.Dnx.Runtime;
|
||||||
|
using Microsoft.Framework.OptionsModel;
|
||||||
|
|
||||||
namespace RazorCompilerCacheWebSite
|
namespace RazorCompilerCacheWebSite
|
||||||
{
|
{
|
||||||
public class CustomCompilerCache : CompilerCache
|
public class CustomCompilerCache : CompilerCache
|
||||||
{
|
{
|
||||||
public CustomCompilerCache(IAssemblyProvider assemblyProvider,
|
public CustomCompilerCache(IEnumerable<RazorFileInfoCollection> fileInfoCollection,
|
||||||
IAssemblyLoadContextAccessor loadContextAccessor,
|
IAssemblyLoadContextAccessor loadContextAccessor,
|
||||||
IOptions<RazorViewEngineOptions> optionsAccessor,
|
IOptions<RazorViewEngineOptions> optionsAccessor,
|
||||||
CompilerCacheInitialiedService cacheInitializedService)
|
CompilerCacheInitialiedService cacheInitializedService)
|
||||||
: base(assemblyProvider, loadContextAccessor, optionsAccessor)
|
: base(fileInfoCollection, loadContextAccessor, optionsAccessor)
|
||||||
{
|
{
|
||||||
cacheInitializedService.Initialized = true;
|
cacheInitializedService.Initialized = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue