Compilation of Views should be affected by changes to _ViewStart files

that are applicable to the view.

Fixes #974
This commit is contained in:
Pranav K 2014-10-29 10:15:04 -07:00
parent 5b1eae494e
commit 275d03a958
23 changed files with 890 additions and 265 deletions

View File

@ -6,6 +6,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Microsoft.AspNet.FileSystems;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
@ -13,22 +14,30 @@ namespace Microsoft.AspNet.Mvc.Razor
public class CompilerCache : ICompilerCache public class CompilerCache : ICompilerCache
{ {
private readonly ConcurrentDictionary<string, CompilerCacheEntry> _cache; private readonly ConcurrentDictionary<string, CompilerCacheEntry> _cache;
private static readonly Type[] EmptyType = new Type[0]; private readonly IFileSystem _fileSystem;
/// <summary> /// <summary>
/// Sets up the runtime compilation cache. /// Initializes a new instance of <see cref="CompilerCache"/> populated with precompiled views
/// discovered using <paramref name="provider"/>.
/// </summary> /// </summary>
/// <param name="provider"> /// <param name="provider">
/// 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>
public CompilerCache([NotNull] IAssemblyProvider provider) /// <param name="fileSystem">An <see cref="IRazorFileSystemCache"/> instance that represents the application's
: this(GetFileInfos(provider.CandidateAssemblies)) /// file system.
/// </param>
public CompilerCache(IAssemblyProvider provider, IRazorFileSystemCache fileSystem)
: this (GetFileInfos(provider.CandidateAssemblies), fileSystem)
{ {
} }
internal CompilerCache(IEnumerable<RazorFileInfoCollection> viewCollections) : this() // Internal for unit testing
internal CompilerCache(IEnumerable<RazorFileInfoCollection> viewCollections, IFileSystem fileSystem)
{ {
_fileSystem = fileSystem;
_cache = new ConcurrentDictionary<string, CompilerCacheEntry>(StringComparer.OrdinalIgnoreCase);
foreach (var viewCollection in viewCollections) foreach (var viewCollection in viewCollections)
{ {
foreach (var fileInfo in viewCollection.FileInfos) foreach (var fileInfo in viewCollection.FileInfos)
@ -42,11 +51,22 @@ namespace Microsoft.AspNet.Mvc.Razor
_cache.TryAdd(NormalizePath(fileInfo.RelativePath), cacheEntry); _cache.TryAdd(NormalizePath(fileInfo.RelativePath), cacheEntry);
} }
} }
}
internal CompilerCache() // Set up ViewStarts
{ foreach (var entry in _cache)
_cache = new ConcurrentDictionary<string, CompilerCacheEntry>(StringComparer.OrdinalIgnoreCase); {
var viewStartLocations = ViewStartUtility.GetViewStartLocations(entry.Key);
foreach (var location in viewStartLocations)
{
CompilerCacheEntry viewStartEntry;
if (_cache.TryGetValue(location, out viewStartEntry))
{
// Add the the composite _ViewStart entry as a dependency.
entry.Value.AssociatedViewStartEntry = viewStartEntry;
break;
}
}
}
} }
internal static IEnumerable<RazorFileInfoCollection> internal static IEnumerable<RazorFileInfoCollection>
@ -62,7 +82,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var inAssemblyType = typeof(RazorFileInfoCollection); var inAssemblyType = typeof(RazorFileInfoCollection);
if (inAssemblyType.IsAssignableFrom(t)) if (inAssemblyType.IsAssignableFrom(t))
{ {
var hasParameterlessConstructor = t.GetConstructor(EmptyType) != null; var hasParameterlessConstructor = t.GetConstructor(Type.EmptyTypes) != null;
return hasParameterlessConstructor return hasParameterlessConstructor
&& !t.GetTypeInfo().IsAbstract && !t.GetTypeInfo().IsAbstract
@ -74,56 +94,114 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <inheritdoc /> /// <inheritdoc />
public CompilationResult GetOrAdd([NotNull] RelativeFileInfo fileInfo, public CompilationResult GetOrAdd([NotNull] RelativeFileInfo fileInfo,
[NotNull] Func<CompilationResult> compile) [NotNull] Func<RelativeFileInfo, CompilationResult> compile)
{ {
CompilerCacheEntry cacheEntry; CompilationResult result;
if (!_cache.TryGetValue(NormalizePath(fileInfo.RelativePath), out cacheEntry)) var entry = GetOrAdd(fileInfo, compile, out result);
{
return OnCacheMiss(fileInfo, compile);
}
else
{
if (cacheEntry.Length != fileInfo.FileInfo.Length)
{
// Recompile if the file lengths differ
return OnCacheMiss(fileInfo, compile);
}
if (cacheEntry.LastModified == fileInfo.FileInfo.LastModified)
{
// Match, not update needed
return CompilationResult.Successful(cacheEntry.CompiledType);
}
var hash = RazorFileHash.GetHash(fileInfo.FileInfo);
// Timestamp doesn't match but it might be because of deployment, compare the hash.
if (cacheEntry.IsPreCompiled &&
string.Equals(cacheEntry.Hash, hash, StringComparison.Ordinal))
{
// Cache hit, but we need to update the entry
return OnCacheMiss(fileInfo,
() => CompilationResult.Successful(cacheEntry.CompiledType));
}
// it's not a match, recompile
return OnCacheMiss(fileInfo, compile);
}
}
private CompilationResult OnCacheMiss(RelativeFileInfo file,
Func<CompilationResult> compile)
{
var result = compile();
var cacheEntry = new CompilerCacheEntry(file, result.CompiledType);
_cache[NormalizePath(file.RelativePath)] = cacheEntry;
return result; return result;
} }
private string NormalizePath(string path) private CompilerCacheEntry GetOrAdd(RelativeFileInfo relativeFileInfo,
Func<RelativeFileInfo, CompilationResult> compile,
out CompilationResult result)
{ {
CompilerCacheEntry cacheEntry;
var normalizedPath = NormalizePath(relativeFileInfo.RelativePath);
if (!_cache.TryGetValue(normalizedPath, out cacheEntry))
{
return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result);
}
else
{
var fileInfo = relativeFileInfo.FileInfo;
if (cacheEntry.Length != fileInfo.Length)
{
// Recompile if the file lengths differ
return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result);
}
if (AssociatedViewStartsChanged(cacheEntry, compile))
{
// Recompile if the view starts have changed since the entry was created.
return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result);
}
if (cacheEntry.LastModified == fileInfo.LastModified)
{
result = CompilationResult.Successful(cacheEntry.CompiledType);
return 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), StringComparison.Ordinal))
{
// Cache hit, but we need to update the entry.
// Assigning to LastModified is an atomic operation and will result in a safe race if it is
// being concurrently read and written or updated concurrently.
cacheEntry.LastModified = fileInfo.LastModified;
result = CompilationResult.Successful(cacheEntry.CompiledType);
return cacheEntry;
}
// it's not a match, recompile
return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result);
}
}
private CompilerCacheEntry OnCacheMiss(RelativeFileInfo file,
string normalizedPath,
Func<RelativeFileInfo, CompilationResult> compile,
out CompilationResult result)
{
result = compile(file);
var cacheEntry = new CompilerCacheEntry(file, result.CompiledType)
{
AssociatedViewStartEntry = GetCompositeViewStartEntry(normalizedPath, compile)
};
// The cache is a concurrent dictionary, so concurrent addition to it with the same key would result in a
// safe race.
_cache[normalizedPath] = cacheEntry;
return cacheEntry;
}
private bool AssociatedViewStartsChanged(CompilerCacheEntry entry,
Func<RelativeFileInfo, CompilationResult> compile)
{
var viewStartEntry = GetCompositeViewStartEntry(entry.RelativePath, compile);
return entry.AssociatedViewStartEntry != viewStartEntry;
}
// Returns the entry for the nearest _ViewStart that the file inherits directives from. Since _ViewStart
// entries are affected by other _ViewStart entries that are in the path hierarchy, the returned value
// represents the composite result of performing a cache check on individual _ViewStart entries.
private CompilerCacheEntry GetCompositeViewStartEntry(string relativePath,
Func<RelativeFileInfo, CompilationResult> compile)
{
var viewStartLocations = ViewStartUtility.GetViewStartLocations(relativePath);
foreach (var viewStartLocation in viewStartLocations)
{
var viewStartFileInfo = _fileSystem.GetFileInfo(viewStartLocation);
if (viewStartFileInfo.Exists)
{
var relativeFileInfo = new RelativeFileInfo(viewStartFileInfo, viewStartLocation);
CompilationResult result;
return GetOrAdd(relativeFileInfo, compile, out result);
}
}
// No _ViewStarts discovered.
return null;
}
private static string NormalizePath(string path)
{
// We need to allow for scenarios where the application was precompiled on a machine with forward slashes
// but is being run in one with backslashes (or vice versa). To this effect, we'll normalize paths to
// use backslashes for lookups and storage in the dictionary.
path = path.Replace('/', '\\'); path = path.Replace('/', '\\');
path = path.TrimStart('\\'); path = path.TrimStart('\\');

View File

@ -53,9 +53,9 @@ namespace Microsoft.AspNet.Mvc.Razor
public long Length { get; private set; } public long Length { get; private set; }
/// <summary> /// <summary>
/// Gets the last modified <see cref="DateTime"/> for the file that was compiled at the time of compilation. /// Gets or sets the last modified <see cref="DateTime"/> for the file at the time of compilation.
/// </summary> /// </summary>
public DateTime LastModified { get; private set; } public DateTime LastModified { get; set; }
/// <summary> /// <summary>
/// Gets the file hash, should only be available for pre compiled files. /// Gets the file hash, should only be available for pre compiled files.
@ -66,5 +66,11 @@ namespace Microsoft.AspNet.Mvc.Razor
/// Gets a flag that indicates if the file is precompiled. /// Gets a flag that indicates if the file is precompiled.
/// </summary> /// </summary>
public bool IsPreCompiled { get { return Hash != null; } } public bool IsPreCompiled { get { return Hash != null; } }
/// <summary>
/// Gets or sets the <see cref="CompilerCacheEntry"/> for the nearest ViewStart that the compiled type
/// depends on.
/// </summary>
public CompilerCacheEntry AssociatedViewStartEntry { get; set; }
} }
} }

View File

@ -9,9 +9,10 @@ using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
/// <summary> /// <summary>
/// A default implementation for the <see cref="IFileInfoCache"/> interface. /// Default implementation for the <see cref="IRazorFileSystemCache"/> interface that caches
/// the results of <see cref="RazorViewEngineOptions.FileSystem"/>.
/// </summary> /// </summary>
public class ExpiringFileInfoCache : IFileInfoCache public class DefaultRazorFileSystemCache : IRazorFileSystemCache
{ {
private readonly ConcurrentDictionary<string, ExpiringFileInfo> _fileInfoCache = private readonly ConcurrentDictionary<string, ExpiringFileInfo> _fileInfoCache =
new ConcurrentDictionary<string, ExpiringFileInfo>(StringComparer.Ordinal); new ConcurrentDictionary<string, ExpiringFileInfo>(StringComparer.Ordinal);
@ -19,7 +20,11 @@ namespace Microsoft.AspNet.Mvc.Razor
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly TimeSpan _offset; private readonly TimeSpan _offset;
public ExpiringFileInfoCache(IOptions<RazorViewEngineOptions> optionsAccessor) /// <summary>
/// Initializes a new instance of <see cref="DefaultRazorFileSystemCache"/>.
/// </summary>
/// <param name="optionsAccessor">Accessor to <see cref="RazorViewEngineOptions"/>.</param>
public DefaultRazorFileSystemCache(IOptions<RazorViewEngineOptions> optionsAccessor)
{ {
_fileSystem = optionsAccessor.Options.FileSystem; _fileSystem = optionsAccessor.Options.FileSystem;
_offset = optionsAccessor.Options.ExpirationBeforeCheckingFilesOnDisk; _offset = optionsAccessor.Options.ExpirationBeforeCheckingFilesOnDisk;
@ -34,21 +39,25 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
/// <inheritdoc /> /// <inheritdoc />
public IFileInfo GetFileInfo([NotNull] string virtualPath) public IDirectoryContents GetDirectoryContents(string subpath)
{ {
IFileInfo fileInfo; return _fileSystem.GetDirectoryContents(subpath);
ExpiringFileInfo expiringFileInfo; }
/// <inheritdoc />
public IFileInfo GetFileInfo(string subpath)
{
ExpiringFileInfo expiringFileInfo;
var utcNow = UtcNow; var utcNow = UtcNow;
if (_fileInfoCache.TryGetValue(virtualPath, out expiringFileInfo) if (_fileInfoCache.TryGetValue(subpath, out expiringFileInfo) &&
&& expiringFileInfo.ValidUntil > utcNow) expiringFileInfo.ValidUntil > utcNow)
{ {
fileInfo = expiringFileInfo.FileInfo; return expiringFileInfo.FileInfo;
} }
else else
{ {
fileInfo = _fileSystem.GetFileInfo(virtualPath); var fileInfo = _fileSystem.GetFileInfo(subpath);
expiringFileInfo = new ExpiringFileInfo() expiringFileInfo = new ExpiringFileInfo()
{ {
@ -56,10 +65,10 @@ namespace Microsoft.AspNet.Mvc.Razor
ValidUntil = _offset == TimeSpan.MaxValue ? DateTime.MaxValue : utcNow.Add(_offset), ValidUntil = _offset == TimeSpan.MaxValue ? DateTime.MaxValue : utcNow.Add(_offset),
}; };
_fileInfoCache.AddOrUpdate(virtualPath, expiringFileInfo, (a, b) => expiringFileInfo); _fileInfoCache[subpath] = expiringFileInfo;
}
return fileInfo; return fileInfo;
}
} }
private class ExpiringFileInfo private class ExpiringFileInfo

View File

@ -6,18 +6,18 @@ using System;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
/// <summary> /// <summary>
/// Caches the result of runtime compilation for the duration of the app lifetime. /// Caches the result of runtime compilation of Razor files for the duration of the app lifetime.
/// </summary> /// </summary>
public interface ICompilerCache public interface ICompilerCache
{ {
/// <summary> /// <summary>
/// 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. /// not available in the cache or is expired.
/// </summary> /// </summary>
/// <param name="fileInfo">A <see cref="RelativeFileInfo"/> representing the file.</param> /// <param name="fileInfo">A <see cref="RelativeFileInfo"/> representing 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, CompilationResult GetOrAdd([NotNull] RelativeFileInfo fileInfo,
[NotNull] Func<CompilationResult> compile); [NotNull] Func<RelativeFileInfo, CompilationResult> compile);
} }
} }

View File

@ -1,20 +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.FileSystems;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// Provides cached access to file infos.
/// </summary>
public interface IFileInfoCache
{
/// <summary>
/// Returns a cached <see cref="IFileInfo" /> for a given path.
/// </summary>
/// <param name="virtualPath">The virtual path.</param>
/// <returns>The cached <see cref="IFileInfo"/>.</returns>
IFileInfo GetFileInfo(string virtualPath);
}
}

View File

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

View File

@ -17,7 +17,9 @@ namespace Microsoft.AspNet.Mvc.Razor
private IFileSystem _fileSystem; private IFileSystem _fileSystem;
/// <summary> /// <summary>
/// Controls the <see cref="ExpiringFileInfoCache" /> caching behavior. /// Gets or sets the <see cref="TimeSpan"/> that specifies the duration for which results of
/// <see cref="FileSystem"/> are cached by <see cref="DefaultRazorFileSystemCache"/>.
/// <see cref="DefaultRazorFileSystemCache"/> is used to query for file changes during Razor compilation.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <see cref="TimeSpan"/> of <see cref="TimeSpan.Zero"/> or less, means no caching. /// <see cref="TimeSpan"/> of <see cref="TimeSpan.Zero"/> or less, means no caching.

View File

@ -99,7 +99,7 @@ namespace __ASP_ASSEMBLY
" info = new " " info = new "
+ nameof(RazorFileInfo) + @" + nameof(RazorFileInfo) + @"
{{ {{
" + nameof(RazorFileInfo.LastModified) + @" = DateTime.FromFileTimeUtc({0:D}), " + nameof(RazorFileInfo.LastModified) + @" = DateTime.FromFileTimeUtc({0:D}).ToLocalTime(),
" + nameof(RazorFileInfo.Length) + @" = {1:D}, " + nameof(RazorFileInfo.Length) + @" = {1:D},
" + nameof(RazorFileInfo.RelativePath) + @" = @""{2}"", " + nameof(RazorFileInfo.RelativePath) + @" = @""{2}"",
" + nameof(RazorFileInfo.FullTypeName) + @" = @""{3}"", " + nameof(RazorFileInfo.FullTypeName) + @" = @""{3}"",

View File

@ -99,12 +99,8 @@ namespace Microsoft.AspNet.Mvc.Razor
else if (Path.GetExtension(fileInfo.Name) else if (Path.GetExtension(fileInfo.Name)
.Equals(FileExtension, StringComparison.OrdinalIgnoreCase)) .Equals(FileExtension, StringComparison.OrdinalIgnoreCase))
{ {
var info = new RelativeFileInfo() var relativePath = Path.Combine(currentPath, fileInfo.Name);
{ var info = new RelativeFileInfo(fileInfo, relativePath);
FileInfo = fileInfo,
RelativePath = Path.Combine(currentPath, fileInfo.Name),
};
yield return info; yield return info;
} }
} }

View File

@ -1,13 +1,41 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.FileSystems; using Microsoft.AspNet.FileSystems;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
/// <summary>
/// A container type that represents <see cref="IFileInfo"/> along with the application base relative path
/// for a file in the file system.
/// </summary>
public class RelativeFileInfo public class RelativeFileInfo
{ {
public IFileInfo FileInfo { get; set; } /// <summary>
public string RelativePath { get; set; } /// Initializes a new instance of <see cref="RelativeFileInfo"/>.
/// </summary>
/// <param name="fileInfo"><see cref="IFileInfo"/> for the file.</param>
/// <param name="relativePath">Path of the file relative to the application base.</param>
public RelativeFileInfo([NotNull] IFileInfo fileInfo, string relativePath)
{
if (string.IsNullOrEmpty(relativePath))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(relativePath));
}
FileInfo = fileInfo;
RelativePath = relativePath;
}
/// <summary>
/// Gets the <see cref="IFileInfo"/> associated with this instance of <see cref="RelativeFileInfo"/>.
/// </summary>
public IFileInfo FileInfo { get; }
/// <summary>
/// Gets the path of the file relative to the application base.
/// </summary>
public string RelativePath { get; }
} }
} }

View File

@ -2,6 +2,7 @@
// 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.FileSystems;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.PageExecutionInstrumentation; using Microsoft.AspNet.PageExecutionInstrumentation;
using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection;
@ -16,19 +17,19 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
private readonly ITypeActivator _activator; private readonly ITypeActivator _activator;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IFileInfoCache _fileInfoCache; private readonly IRazorFileSystemCache _fileSystemCache;
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,
IFileInfoCache fileInfoCache) IRazorFileSystemCache fileSystemCache)
{ {
_activator = typeActivator; _activator = typeActivator;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_compilerCache = compilerCache; _compilerCache = compilerCache;
_fileInfoCache = fileInfoCache; _fileSystemCache = fileSystemCache;
} }
private IRazorCompilationService RazorCompilationService private IRazorCompilationService RazorCompilationService
@ -56,19 +57,15 @@ namespace Microsoft.AspNet.Mvc.Razor
relativePath = relativePath.Substring(1); relativePath = relativePath.Substring(1);
} }
var fileInfo = _fileInfoCache.GetFileInfo(relativePath); var fileInfo = _fileSystemCache.GetFileInfo(relativePath);
if (fileInfo.Exists) if (fileInfo.Exists)
{ {
var relativeFileInfo = new RelativeFileInfo() var relativeFileInfo = new RelativeFileInfo(fileInfo, relativePath);
{
FileInfo = fileInfo,
RelativePath = relativePath,
};
var result = _compilerCache.GetOrAdd( var result = _compilerCache.GetOrAdd(
relativeFileInfo, relativeFileInfo,
() => RazorCompilationService.Compile(relativeFileInfo)); RazorCompilationService.Compile);
var page = (IRazorPage)_activator.CreateInstance(_serviceProvider, result.CompiledType); var page = (IRazorPage)_activator.CreateInstance(_serviceProvider, result.CompiledType);
page.Path = relativePath; page.Path = relativePath;
@ -78,10 +75,5 @@ namespace Microsoft.AspNet.Mvc.Razor
return null; return null;
} }
private static bool IsInstrumentationEnabled(HttpContext context)
{
return context.GetFeature<IPageExecutionListenerFeature>() != null;
}
} }
} }

View File

@ -108,16 +108,20 @@ 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<IFileInfoCache, ExpiringFileInfoCache>(); yield return describe.Singleton<IRazorFileSystemCache, DefaultRazorFileSystemCache>();
// 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 optionsAccessor = serviceProvider.GetRequiredService<IOptions<RazorViewEngineOptions>>(); var cachedFileSystem = serviceProvider.GetRequiredService<IRazorFileSystemCache>();
return new MvcRazorHost(optionsAccessor.Options.FileSystem); return new MvcRazorHost(cachedFileSystem);
}); });
// Caches compilation artifacts across the lifetime of the application.
yield return describe.Singleton<ICompilerCache, CompilerCache>(); yield return describe.Singleton<ICompilerCache, CompilerCache>();
// This caches compilation related details that are valid across the lifetime of the application
// and is required to be a singleton.
yield return describe.Singleton<ICompilationService, RoslynCompilationService>(); yield return describe.Singleton<ICompilationService, RoslynCompilationService>();
// Both the compiler cache and roslyn compilation service hold on the compilation related // Both the compiler cache and roslyn compilation service hold on the compilation related

View File

@ -2,17 +2,22 @@
// 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.IO;
using System.Linq;
using System.Net; using System.Net;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Builder; using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost; using Microsoft.AspNet.TestHost;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Runtime;
using Xunit; using Xunit;
namespace Microsoft.AspNet.Mvc.FunctionalTests namespace Microsoft.AspNet.Mvc.FunctionalTests
{ {
public class PrecompilationTest public class PrecompilationTest
{ {
private static readonly TimeSpan _cacheDelayInterval = TimeSpan.FromSeconds(2);
private readonly IServiceProvider _services = TestHelper.CreateServices("PrecompilationWebSite"); private readonly IServiceProvider _services = TestHelper.CreateServices("PrecompilationWebSite");
private readonly Action<IApplicationBuilder> _app = new PrecompilationWebSite.Startup().Configure; private readonly Action<IApplicationBuilder> _app = new PrecompilationWebSite.Startup().Configure;
@ -20,20 +25,155 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
public async Task PrecompiledView_RendersCorrectly() public async Task PrecompiledView_RendersCorrectly()
{ {
// Arrange // Arrange
var applicationEnvironment = _services.GetRequiredService<IApplicationEnvironment>();
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 viewstartContent = File.ReadAllText(Path.Combine(viewsDirectory, "_viewstart.cshtml"));
var server = TestServer.Create(_services, _app); var server = TestServer.Create(_services, _app);
var client = server.CreateClient(); var client = server.CreateClient();
// 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.
var expectedContent = typeof(PrecompilationWebSite.Startup).GetTypeInfo().Assembly.GetName().ToString(); var assemblyName = typeof(PrecompilationWebSite.Startup).GetTypeInfo().Assembly.GetName().ToString();
// Act try
var response = await client.GetAsync("http://localhost/Home/Index"); {
var responseContent = await response.Content.ReadAsStringAsync(); // Act - 1
var response = await client.GetAsync("http://localhost/Home/Index");
var responseContent = await response.Content.ReadAsStringAsync();
// Assert // Assert - 1
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(expectedContent, responseContent); var parsedResponse1 = new ParsedResponse(responseContent);
Assert.Equal(assemblyName, parsedResponse1.ViewStart);
Assert.Equal(assemblyName, parsedResponse1.Layout);
Assert.Equal(assemblyName, parsedResponse1.Index);
// Act - 2
// Touch the Layout file and verify it is now dynamically compiled.
await TouchFile(viewsDirectory, "Layout.cshtml");
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
// Assert - 2
var response2 = new ParsedResponse(responseContent);
Assert.NotEqual(assemblyName, response2.Layout);
Assert.Equal(assemblyName, response2.ViewStart);
Assert.Equal(assemblyName, response2.Index);
// Act - 3
// Touch the _ViewStart file and verify it causes all files to recompile.
await TouchFile(viewsDirectory, "_viewstart.cshtml");
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
// Assert - 3
var response3 = new ParsedResponse(responseContent);
Assert.NotEqual(assemblyName, response3.ViewStart);
Assert.NotEqual(assemblyName, response3.Index);
Assert.NotEqual(response2.Layout, response3.Layout);
// Act - 4
// 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 - 4
var response4 = new ParsedResponse(responseContent);
// Layout and _ViewStart should not have changed.
Assert.Equal(response3.Layout, response4.Layout);
Assert.Equal(response3.ViewStart, response4.ViewStart);
Assert.NotEqual(response3.Index, response4.Index);
// Act - 5
// Touch the _ViewStart file. This time, we'll verify the Non-precompiled -> Non-precompiled workflow.
await TouchFile(viewsDirectory, "_viewstart.cshtml");
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
// Assert - 5
var response5 = new ParsedResponse(responseContent);
// Everything should've recompiled.
Assert.NotEqual(response4.ViewStart, response5.ViewStart);
Assert.NotEqual(response4.Index, response5.Index);
Assert.NotEqual(response4.Layout, response5.Layout);
// Act - 6
// Add a new _ViewStart file
File.WriteAllText(Path.Combine(viewsDirectory, "..", "_viewstart.cshtml"), string.Empty);
await Task.Delay(_cacheDelayInterval);
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
// Assert - 6
// Everything should've recompiled.
var response6 = new ParsedResponse(responseContent);
Assert.NotEqual(response5.ViewStart, response6.ViewStart);
Assert.NotEqual(response5.Index, response6.Index);
Assert.NotEqual(response5.Layout, response6.Layout);
// Act - 7
// Remove new _ViewStart file
File.Delete(Path.Combine(viewsDirectory, "..", "_viewstart.cshtml"));
await Task.Delay(_cacheDelayInterval);
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
// Refetch and verify we get cached types
responseContent = await client.GetStringAsync("http://localhost/Home/Index");
// Assert - 7
var response8 = new ParsedResponse(responseContent);
Assert.Equal(response7.ViewStart, response8.ViewStart);
Assert.Equal(response7.Index, response8.Index);
Assert.Equal(response7.Layout, response8.Layout);
}
finally
{
File.WriteAllText(Path.Combine(viewsDirectory, "Layout.cshtml"), layoutContent);
File.WriteAllText(Path.Combine(viewsDirectory, "Index.cshtml"), indexContent);
File.WriteAllText(Path.Combine(viewsDirectory, "_viewstart.cshtml"), viewstartContent);
}
} }
private static Task TouchFile(string viewsDir, string file)
{
File.AppendAllText(Path.Combine(viewsDir, file), " ");
// Delay to ensure we don't hit the cached file system.
return Task.Delay(_cacheDelayInterval);
}
private sealed class ParsedResponse
{
public ParsedResponse(string responseContent)
{
var results = responseContent.Split(new[] { Environment.NewLine },
StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.ToArray();
Assert.True(results[0].StartsWith("Layout:"));
Layout = results[0].Substring("Layout:".Length);
Assert.True(results[1].StartsWith("_viewstart:"));
ViewStart = results[1].Substring("_viewstart:".Length);
Assert.True(results[2].StartsWith("index:"));
Index = results[2].Substring("index:".Length);
}
public string Layout { get; }
public string ViewStart { get; }
public string Index { get; }
}
} }
} }

View File

@ -0,0 +1,70 @@
// 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.IO;
using System.Text;
using Microsoft.AspNet.FileSystems;
using Microsoft.Framework.Expiration.Interfaces;
namespace Microsoft.AspNet.Mvc.Razor
{
public class TestFileInfo : IFileInfo
{
private string _content;
public bool IsDirectory { get; } = false;
public DateTime LastModified { get; set; }
public long Length { get; set; }
public string Name { get; set; }
public string PhysicalPath { get; set; }
public string Content
{
get { return _content; }
set
{
_content = value;
Length = Encoding.UTF8.GetByteCount(Content);
}
}
public bool Exists
{
get { return true; }
}
public bool IsReadOnly
{
get
{
throw new NotSupportedException();
}
}
public Stream CreateReadStream()
{
var bytes = Encoding.UTF8.GetBytes(Content);
return new MemoryStream(bytes);
}
public void WriteContent(byte[] content)
{
throw new NotSupportedException();
}
public void Delete()
{
throw new NotSupportedException();
}
public IExpirationTrigger CreateFileChangeTrigger()
{
throw new NotSupportedException();
}
}
}

View File

@ -4,9 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text;
using Microsoft.AspNet.FileSystems; using Microsoft.AspNet.FileSystems;
using Moq;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
@ -17,26 +15,30 @@ namespace Microsoft.AspNet.Mvc.Razor
public IDirectoryContents GetDirectoryContents(string subpath) public IDirectoryContents GetDirectoryContents(string subpath)
{ {
throw new NotImplementedException(); throw new NotSupportedException();
} }
public void AddFile(string path, string contents) public void AddFile(string path, string contents)
{ {
var fileInfo = new Mock<IFileInfo>(); var fileInfo = new TestFileInfo
fileInfo.Setup(f => f.CreateReadStream()) {
.Returns(() => new MemoryStream(Encoding.UTF8.GetBytes(contents))); Content = contents,
fileInfo.SetupGet(f => f.PhysicalPath) PhysicalPath = path,
.Returns(path); Name = Path.GetFileName(path),
fileInfo.SetupGet(f => f.Name) LastModified = DateTime.UtcNow,
.Returns(Path.GetFileName(path)); };
fileInfo.SetupGet(f => f.Exists)
.Returns(true); AddFile(path, fileInfo);
AddFile(path, fileInfo.Object);
} }
public void AddFile(string path, IFileInfo contents) public void AddFile(string path, TestFileInfo contents)
{ {
_lookup.Add(path, contents); _lookup[path] = contents;
}
public void DeleteFile(string path)
{
_lookup.Remove(path);
} }
public IFileInfo GetFileInfo(string subpath) public IFileInfo GetFileInfo(string subpath)
@ -50,5 +52,6 @@ namespace Microsoft.AspNet.Mvc.Razor
return new NotFoundFileInfo(subpath); return new NotFoundFileInfo(subpath);
} }
} }
} }
} }

View File

@ -4,12 +4,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using Microsoft.AspNet.FileSystems;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNet.Mvc.Razor.Test namespace Microsoft.AspNet.Mvc.Razor
{ {
public class CompilerCacheTest public class CompilerCacheTest
{ {
@ -17,24 +16,20 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
public void GetOrAdd_ReturnsCompilationResultFromFactory() public void GetOrAdd_ReturnsCompilationResultFromFactory()
{ {
// Arrange // Arrange
var cache = new CompilerCache(); var fileSystem = new TestFileSystem();
var fileInfo = new Mock<IFileInfo>(); var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), fileSystem);
var fileInfo = new TestFileInfo
fileInfo {
.SetupGet(i => i.LastModified) LastModified = DateTime.FromFileTimeUtc(10000)
.Returns(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() var runtimeFileInfo = new RelativeFileInfo(fileInfo, "ab");
{
FileInfo = fileInfo.Object,
RelativePath = "ab",
};
// Act // Act
var actual = cache.GetOrAdd(runtimeFileInfo, () => expected); var actual = cache.GetOrAdd(runtimeFileInfo, _ => expected);
// Assert // Assert
Assert.Same(expected, actual); Assert.Same(expected, actual);
@ -75,15 +70,16 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
private class ViewCollection : RazorFileInfoCollection private class ViewCollection : RazorFileInfoCollection
{ {
private readonly List<RazorFileInfo> _fileInfos = new List<RazorFileInfo>();
public ViewCollection() public ViewCollection()
{ {
var fileInfos = new List<RazorFileInfo>(); FileInfos = _fileInfos;
FileInfos = fileInfos;
var content = new PreCompile().Content; var content = new PreCompile().Content;
var length = Encoding.UTF8.GetByteCount(content); var length = Encoding.UTF8.GetByteCount(content);
fileInfos.Add(new RazorFileInfo() Add(new RazorFileInfo()
{ {
FullTypeName = typeof(PreCompile).FullName, FullTypeName = typeof(PreCompile).FullName,
Hash = RazorFileHash.GetHash(GetMemoryStream(content)), Hash = RazorFileHash.GetHash(GetMemoryStream(content)),
@ -92,6 +88,11 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
RelativePath = "ab", RelativePath = "ab",
}); });
} }
public void Add(RazorFileInfo fileInfo)
{
_fileInfos.Add(fileInfo);
}
} }
private static Stream GetMemoryStream(string content) private static Stream GetMemoryStream(string content)
@ -102,81 +103,324 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
} }
[Theory] [Theory]
[InlineData(typeof(RuntimeCompileIdentical), 10000, false)] [InlineData(10000)]
[InlineData(typeof(RuntimeCompileIdentical), 11000, false)] [InlineData(11000)]
[InlineData(typeof(RuntimeCompileDifferent), 10000, false)] // expected failure: same time and length [InlineData(10000)] // expected failure: same time and length
[InlineData(typeof(RuntimeCompileDifferent), 11000, true)] public void GetOrAdd_UsesFilesFromCache_IfTimestampDiffers_ButContentAndLengthAreTheSame(long fileTimeUTC)
[InlineData(typeof(RuntimeCompileDifferentLength), 10000, true)]
[InlineData(typeof(RuntimeCompileDifferentLength), 10000, true)]
public void FileWithTheSameLengthAndDifferentTime_DoesNot_OverridesPrecompilation(
Type resultViewType,
long fileTimeUTC,
bool swapsPreCompile)
{ {
// Arrange // Arrange
var instance = (View)Activator.CreateInstance(resultViewType); var instance = new RuntimeCompileIdentical();
var length = Encoding.UTF8.GetByteCount(instance.Content); var length = Encoding.UTF8.GetByteCount(instance.Content);
var collection = new ViewCollection(); var collection = new ViewCollection();
var cache = new CompilerCache(new[] { new ViewCollection() }); var fileSystem = new TestFileSystem();
var cache = new CompilerCache(new[] { new ViewCollection() }, fileSystem);
var fileInfo = new Mock<IFileInfo>(); var fileInfo = new TestFileInfo
fileInfo
.SetupGet(i => i.Length)
.Returns(length);
fileInfo
.SetupGet(i => i.LastModified)
.Returns(DateTime.FromFileTimeUtc(fileTimeUTC));
fileInfo.Setup(i => i.CreateReadStream())
.Returns(GetMemoryStream(instance.Content));
var preCompileType = typeof(PreCompile);
var runtimeFileInfo = new RelativeFileInfo()
{ {
FileInfo = fileInfo.Object, Length = length,
LastModified = DateTime.FromFileTimeUtc(fileTimeUTC),
Content = instance.Content
};
var runtimeFileInfo = new RelativeFileInfo(fileInfo, "ab");
var precompiledContent = new PreCompile().Content;
var razorFileInfo = new RazorFileInfo
{
FullTypeName = typeof(PreCompile).FullName,
Hash = RazorFileHash.GetHash(GetMemoryStream(precompiledContent)),
LastModified = DateTime.FromFileTimeUtc(10000),
Length = Encoding.UTF8.GetByteCount(precompiledContent),
RelativePath = "ab", RelativePath = "ab",
}; };
// Act // Act
var actual = cache.GetOrAdd(runtimeFileInfo, var actual = cache.GetOrAdd(runtimeFileInfo,
compile: () => CompilationResult.Successful(resultViewType)); compile: _ => { throw new Exception("Shouldn't be called."); });
// Assert // Assert
if (swapsPreCompile) 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 collection = new ViewCollection();
var fileSystem = new TestFileSystem();
var cache = new CompilerCache(new[] { new ViewCollection() }, fileSystem);
var fileInfo = new TestFileInfo
{ {
Assert.Equal(actual.CompiledType, resultViewType); Length = length,
} LastModified = DateTime.FromFileTimeUtc(fileTimeUTC),
else Content = instance.Content
};
var runtimeFileInfo = new RelativeFileInfo(fileInfo, "ab");
var precompiledContent = new PreCompile().Content;
var razorFileInfo = new RazorFileInfo
{ {
Assert.Equal(actual.CompiledType, typeof(PreCompile)); FullTypeName = typeof(PreCompile).FullName,
Hash = RazorFileHash.GetHash(GetMemoryStream(precompiledContent)),
LastModified = DateTime.FromFileTimeUtc(10000),
Length = Encoding.UTF8.GetByteCount(precompiledContent),
RelativePath = "ab",
};
// Act
var actual = cache.GetOrAdd(runtimeFileInfo,
compile: _ => CompilationResult.Successful(resultViewType));
// Assert
Assert.Equal(resultViewType, actual.CompiledType);
}
[Fact]
public void GetOrAdd_UsesValueFromCache_IfViewStartHasNotChanged()
{
// Arrange
var instance = (View)Activator.CreateInstance(typeof(PreCompile));
var length = Encoding.UTF8.GetByteCount(instance.Content);
var fileSystem = new TestFileSystem();
var lastModified = DateTime.UtcNow;
var fileInfo = new TestFileInfo
{
Length = length,
LastModified = lastModified,
Content = instance.Content
};
var runtimeFileInfo = new RelativeFileInfo(fileInfo, "ab");
var viewStartContent = "viewstart-content";
var viewStartFileInfo = new TestFileInfo
{
Content = viewStartContent,
LastModified = DateTime.UtcNow
};
fileSystem.AddFile("_ViewStart.cshtml", viewStartFileInfo);
var viewStartRazorFileInfo = new RazorFileInfo
{
Hash = RazorFileHash.GetHash(GetMemoryStream(viewStartContent)),
LastModified = viewStartFileInfo.LastModified,
Length = viewStartFileInfo.Length,
RelativePath = "_ViewStart.cshtml",
FullTypeName = typeof(RuntimeCompileIdentical).FullName
};
var precompiledViews = new ViewCollection();
precompiledViews.Add(viewStartRazorFileInfo);
var cache = new CompilerCache(new[] { precompiledViews }, fileSystem);
// Act
var actual = cache.GetOrAdd(runtimeFileInfo,
compile: _ => { throw new Exception("shouldn't be invoked"); });
// Assert
Assert.Equal(typeof(PreCompile), actual.CompiledType);
}
[Fact]
public void GetOrAdd_IgnoresCachedValueIfFileIsIdentical_ButViewStartWasAdedSinceTheCacheWasCreated()
{
// Arrange
var expectedType = typeof(RuntimeCompileDifferent);
var lastModified = DateTime.UtcNow;
var fileSystem = new TestFileSystem();
var collection = new ViewCollection();
var precompiledFile = collection.FileInfos[0];
precompiledFile.RelativePath = "Views\\home\\index.cshtml";
var cache = new CompilerCache(new[] { collection }, fileSystem);
var testFile = new TestFileInfo
{
Content = new PreCompile().Content,
LastModified = precompiledFile.LastModified,
PhysicalPath = precompiledFile.RelativePath
};
fileSystem.AddFile(precompiledFile.RelativePath, testFile);
var relativeFile = new RelativeFileInfo(testFile, testFile.PhysicalPath);
// Act 1
var actual1 = cache.GetOrAdd(relativeFile,
compile: _ => { throw new Exception("should not be called"); });
// Assert 1
Assert.Equal(typeof(PreCompile), actual1.CompiledType);
// Act 2
fileSystem.AddFile("Views\\_ViewStart.cshtml", "");
var actual2 = cache.GetOrAdd(relativeFile,
compile: _ => CompilationResult.Successful(expectedType));
// Assert 2
Assert.Equal(expectedType, actual2.CompiledType);
}
[Fact]
public void GetOrAdd_IgnoresCachedValueIfFileIsIdentical_ButViewStartWasDeletedSinceCacheWasCreated()
{
// Arrange
var expectedType = typeof(RuntimeCompileDifferent);
var lastModified = DateTime.UtcNow;
var fileSystem = new TestFileSystem();
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
};
fileSystem.AddFile(viewFileInfo.PhysicalPath, viewFileInfo);
var viewStartFileInfo = new TestFileInfo
{
PhysicalPath = "Views\\_ViewStart.cshtml",
Content = "viewstart-content",
LastModified = lastModified
};
var viewStart = new RazorFileInfo
{
FullTypeName = typeof(RuntimeCompileIdentical).FullName,
RelativePath = viewStartFileInfo.PhysicalPath,
LastModified = viewStartFileInfo.LastModified,
Hash = RazorFileHash.GetHash(viewStartFileInfo),
Length = viewStartFileInfo.Length
};
fileSystem.AddFile(viewStartFileInfo.PhysicalPath, viewStartFileInfo);
viewCollection.Add(viewStart);
var cache = new CompilerCache(new[] { viewCollection }, fileSystem);
var fileInfo = new RelativeFileInfo(viewFileInfo, viewFileInfo.PhysicalPath);
// Act 1
var actual1 = cache.GetOrAdd(fileInfo,
compile: _ => { throw new Exception("should not be called"); });
// Assert 1
Assert.Equal(typeof(PreCompile), actual1.CompiledType);
// Act 2
fileSystem.DeleteFile(viewStartFileInfo.PhysicalPath);
var actual2 = cache.GetOrAdd(fileInfo,
compile: _ => CompilationResult.Successful(expectedType));
// Assert 2
Assert.Equal(expectedType, actual2.CompiledType);
}
public static IEnumerable<object[]> GetOrAdd_IgnoresCachedValue_IfViewStartWasChangedSinceCacheWasCreatedData
{
get
{
var viewStartContent = "viewstart-content";
var contentStream = GetMemoryStream(viewStartContent);
var lastModified = DateTime.UtcNow;
int length = Encoding.UTF8.GetByteCount(viewStartContent);
var path = "Views\\_ViewStart.cshtml";
var razorFileInfo = new RazorFileInfo
{
Hash = RazorFileHash.GetHash(contentStream),
LastModified = lastModified,
Length = length,
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_IfViewStartWasChangedSinceCacheWasCreatedData))]
public void GetOrAdd_IgnoresCachedValue_IfViewStartWasChangedSinceCacheWasCreated(
RazorFileInfo viewStartRazorFileInfo, TestFileInfo viewStartFileInfo)
{
// 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 runtimeFileInfo = new RelativeFileInfo(fileInfo, fileInfo.PhysicalPath);
var razorFileInfo = new RazorFileInfo
{
FullTypeName = typeof(PreCompile).FullName,
Hash = RazorFileHash.GetHash(fileInfo),
LastModified = lastModified,
Length = Encoding.UTF8.GetByteCount(content),
RelativePath = fileInfo.PhysicalPath,
};
var fileSystem = new TestFileSystem();
fileSystem.AddFile(viewStartRazorFileInfo.RelativePath, viewStartFileInfo);
var viewCollection = new ViewCollection();
var cache = new CompilerCache(new[] { viewCollection }, fileSystem);
// Act
var actual = cache.GetOrAdd(runtimeFileInfo,
compile: _ => CompilationResult.Successful(expectedType));
// Assert
Assert.Equal(expectedType, actual.CompiledType);
}
[Fact] [Fact]
public void GetOrAdd_DoesNotCacheCompiledContent_OnCallsAfterInitial() public void GetOrAdd_DoesNotCacheCompiledContent_OnCallsAfterInitial()
{ {
// Arrange // Arrange
var lastModified = DateTime.UtcNow; var lastModified = DateTime.UtcNow;
var cache = new CompilerCache(); var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), new TestFileSystem());
var fileInfo = new Mock<IFileInfo>(); var fileInfo = new TestFileInfo
fileInfo.SetupGet(f => f.PhysicalPath) {
.Returns("test"); PhysicalPath = "test",
fileInfo.SetupGet(f => f.LastModified) LastModified = lastModified
.Returns(lastModified); };
var type = GetType(); var type = GetType();
var uncachedResult = UncachedCompilationResult.Successful(type, "hello world"); var uncachedResult = UncachedCompilationResult.Successful(type, "hello world");
var runtimeFileInfo = new RelativeFileInfo() var runtimeFileInfo = new RelativeFileInfo(fileInfo, "test");
{
FileInfo = fileInfo.Object,
RelativePath = "test",
};
// Act // Act
cache.GetOrAdd(runtimeFileInfo, () => uncachedResult); cache.GetOrAdd(runtimeFileInfo, _ => uncachedResult);
var actual1 = cache.GetOrAdd(runtimeFileInfo, () => uncachedResult); var actual1 = cache.GetOrAdd(runtimeFileInfo, _ => uncachedResult);
var actual2 = cache.GetOrAdd(runtimeFileInfo, () => uncachedResult); var actual2 = cache.GetOrAdd(runtimeFileInfo, _ => uncachedResult);
// Assert // Assert
Assert.NotSame(uncachedResult, actual1); Assert.NotSame(uncachedResult, actual1);

View File

@ -3,16 +3,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using Microsoft.AspNet.FileSystems; using Microsoft.AspNet.FileSystems;
using Microsoft.Framework.Expiration.Interfaces;
using Microsoft.Framework.OptionsModel; using Microsoft.Framework.OptionsModel;
using Moq; using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
public class ExpiringFileInfoCacheTest public class DefaultRazorFileSystemCacheTest
{ {
private const string FileName = "myView.cshtml"; private const string FileName = "myView.cshtml";
@ -41,7 +39,7 @@ namespace Microsoft.AspNet.Mvc.Razor
public void CreateFile(string fileName) public void CreateFile(string fileName)
{ {
var fileInfo = new DummyFileInfo() var fileInfo = new TestFileInfo()
{ {
Name = fileName, Name = fileName,
LastModified = DateTime.Now, LastModified = DateTime.Now,
@ -89,6 +87,9 @@ namespace Microsoft.AspNet.Mvc.Razor
var fileInfo2 = cache.GetFileInfo(FileName); var fileInfo2 = cache.GetFileInfo(FileName);
// Assert // Assert
Assert.True(fileInfo1.Exists);
Assert.True(fileInfo1.Exists);
Assert.Same(fileInfo1, fileInfo2); Assert.Same(fileInfo1, fileInfo2);
Assert.Equal(FileName, fileInfo1.Name); Assert.Equal(FileName, fileInfo1.Name);
@ -306,7 +307,34 @@ namespace Microsoft.AspNet.Mvc.Razor
Assert.Equal(FileName, fileInfo1.Name); Assert.Equal(FileName, fileInfo1.Name);
} }
public class ControllableExpiringFileInfoCache : ExpiringFileInfoCache [Fact]
public void GetDirectoryInfo_PassesThroughToUnderlyingFileSystem()
{
// Arrange
var fileSystem = new Mock<IFileSystem>();
var expected = Mock.Of<IDirectoryContents>();
fileSystem.Setup(f => f.GetDirectoryContents("/test-path"))
.Returns(expected)
.Verifiable();
var options = new RazorViewEngineOptions
{
FileSystem = fileSystem.Object
};
var accessor = new Mock<IOptions<RazorViewEngineOptions>>();
accessor.SetupGet(a => a.Options)
.Returns(options);
var cachedFileSystem = new DefaultRazorFileSystemCache(accessor.Object);
// Act
var result = cachedFileSystem.GetDirectoryContents("/test-path");
// Assert
Assert.Same(expected, result);
fileSystem.Verify();
}
public class ControllableExpiringFileInfoCache : DefaultRazorFileSystemCache
{ {
public ControllableExpiringFileInfoCache(IOptions<RazorViewEngineOptions> optionsAccessor) public ControllableExpiringFileInfoCache(IOptions<RazorViewEngineOptions> optionsAccessor)
: base(optionsAccessor) : base(optionsAccessor)
@ -338,7 +366,6 @@ namespace Microsoft.AspNet.Mvc.Razor
_internalUtcNow = UtcNow.AddMilliseconds(milliSeconds); _internalUtcNow = UtcNow.AddMilliseconds(milliSeconds);
} }
} }
public class DummyFileSystem : IFileSystem public class DummyFileSystem : IFileSystem
{ {
private Dictionary<string, IFileInfo> _fileInfos = new Dictionary<string, IFileInfo>(StringComparer.OrdinalIgnoreCase); private Dictionary<string, IFileInfo> _fileInfos = new Dictionary<string, IFileInfo>(StringComparer.OrdinalIgnoreCase);
@ -365,7 +392,7 @@ namespace Microsoft.AspNet.Mvc.Razor
IFileInfo knownInfo; IFileInfo knownInfo;
if (_fileInfos.TryGetValue(subpath, out knownInfo)) if (_fileInfos.TryGetValue(subpath, out knownInfo))
{ {
return new DummyFileInfo() return new TestFileInfo
{ {
Name = knownInfo.Name, Name = knownInfo.Name,
LastModified = knownInfo.LastModified, LastModified = knownInfo.LastModified,
@ -382,48 +409,5 @@ namespace Microsoft.AspNet.Mvc.Razor
throw new NotImplementedException(); throw new NotImplementedException();
} }
} }
public class DummyFileInfo : IFileInfo
{
public DateTime LastModified { get; set; }
public string Name { get; set; }
public long Length { get { throw new NotImplementedException(); } }
public bool IsDirectory { get { throw new NotImplementedException(); } }
public string PhysicalPath { get { throw new NotImplementedException(); } }
public bool Exists
{
get
{
throw new NotImplementedException();
}
}
public bool IsReadOnly
{
get
{
throw new NotImplementedException();
}
}
public Stream CreateReadStream() { throw new NotImplementedException(); }
public void WriteContent(byte[] content)
{
throw new NotImplementedException();
}
public void Delete()
{
throw new NotImplementedException();
}
public IExpirationTrigger CreateFileChangeTrigger()
{
throw new NotImplementedException();
}
}
} }
} }

View File

@ -34,11 +34,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
var razorService = new RazorCompilationService(compiler.Object, host.Object); var razorService = new RazorCompilationService(compiler.Object, host.Object);
var relativeFileInfo = new RelativeFileInfo() var relativeFileInfo = new RelativeFileInfo(fileInfo.Object, @"Views\index\home.cshtml");
{
FileInfo = fileInfo.Object,
RelativePath = @"Views\index\home.cshtml",
};
// Act // Act
razorService.Compile(relativeFileInfo); razorService.Compile(relativeFileInfo);
@ -47,6 +43,73 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
host.Verify(); host.Verify();
} }
[Fact]
public void Compile_ReturnsFailedResultIfParseFails()
{
// Arrange
var generatorResult = new GeneratorResults(
new Block(
new BlockBuilder { Type = BlockType.Comment }),
new RazorError[] { new RazorError("some message", 1, 1, 1, 1) },
new CodeBuilderResult("", new LineMapping[0]),
new CodeTree());
var host = new Mock<IMvcRazorHost>();
host.Setup(h => h.GenerateCode(It.IsAny<string>(), It.IsAny<Stream>()))
.Returns(generatorResult)
.Verifiable();
var fileInfo = new Mock<IFileInfo>();
fileInfo.Setup(f => f.CreateReadStream())
.Returns(Stream.Null);
var compiler = new Mock<ICompilationService>(MockBehavior.Strict);
var relativeFileInfo = new RelativeFileInfo(fileInfo.Object, @"Views\index\home.cshtml");
var razorService = new RazorCompilationService(compiler.Object, host.Object);
// Act
var result = razorService.Compile(relativeFileInfo);
// Assert
var ex = Assert.Throws<CompilationFailedException>(() => result.CompiledType);
Assert.Equal("some message", Assert.Single(ex.Messages).Message);
host.Verify();
}
[Fact]
public void Compile_ReturnsResultFromCompilationServiceIfParseSucceeds()
{
// Arrange
var code = "compiled-content";
var generatorResult = new GeneratorResults(
new Block(
new BlockBuilder { Type = BlockType.Comment }),
new RazorError[0],
new CodeBuilderResult(code, new LineMapping[0]),
new CodeTree());
var host = new Mock<IMvcRazorHost>();
host.Setup(h => h.GenerateCode(It.IsAny<string>(), It.IsAny<Stream>()))
.Returns(generatorResult);
var fileInfo = new Mock<IFileInfo>();
fileInfo.Setup(f => f.CreateReadStream())
.Returns(Stream.Null);
var compilationResult = CompilationResult.Successful(typeof(object));
var compiler = new Mock<ICompilationService>();
compiler.Setup(c => c.Compile(fileInfo.Object, code))
.Returns(compilationResult)
.Verifiable();
var relativeFileInfo = new RelativeFileInfo(fileInfo.Object, @"Views\index\home.cshtml");
var razorService = new RazorCompilationService(compiler.Object, host.Object);
// Act
var result = razorService.Compile(relativeFileInfo);
// Assert
Assert.Same(compilationResult, result);
compiler.Verify();
}
private static GeneratorResults GetGeneratorResult() private static GeneratorResults GetGeneratorResult()
{ {
return new GeneratorResults( return new GeneratorResults(

View File

@ -1,4 +1,9 @@
{ {
"code": [
"**/*.cs",
"../Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileSystem.cs",
"../Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileInfo.cs"
],
"dependencies": { "dependencies": {
"Microsoft.AspNet.Mvc.Razor": "6.0.0-*", "Microsoft.AspNet.Mvc.Razor": "6.0.0-*",
"Microsoft.AspNet.Testing": "1.0.0-*", "Microsoft.AspNet.Testing": "1.0.0-*",

View File

@ -0,0 +1 @@
*.cshtml

View File

@ -1,2 +1 @@
@using System.Reflection index:@GetType().GetTypeInfo().Assembly.GetName()
@(GetType().GetTypeInfo().Assembly.GetName())

View File

@ -0,0 +1,2 @@
Layout:@GetType().GetTypeInfo().Assembly.FullName
@RenderBody()

View File

@ -0,0 +1,4 @@
@using System.Reflection
@{ Layout = "/views/Home/Layout.cshtml";}
_viewstart:@GetType().GetTypeInfo().Assembly.FullName