Compilation of Views should be affected by changes to _ViewStart files
that are applicable to the view. Fixes #974
This commit is contained in:
parent
5b1eae494e
commit
275d03a958
|
|
@ -6,6 +6,7 @@ using System.Collections.Concurrent;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNet.FileSystems;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
|
|
@ -13,22 +14,30 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
public class CompilerCache : ICompilerCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, CompilerCacheEntry> _cache;
|
||||
private static readonly Type[] EmptyType = new Type[0];
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
/// <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>
|
||||
/// <param name="provider">
|
||||
/// An <see cref="IAssemblyProvider"/> representing the assemblies
|
||||
/// used to search for pre-compiled views.
|
||||
/// </param>
|
||||
public CompilerCache([NotNull] IAssemblyProvider provider)
|
||||
: this(GetFileInfos(provider.CandidateAssemblies))
|
||||
/// <param name="fileSystem">An <see cref="IRazorFileSystemCache"/> instance that represents the application's
|
||||
/// 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 fileInfo in viewCollection.FileInfos)
|
||||
|
|
@ -42,11 +51,22 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
_cache.TryAdd(NormalizePath(fileInfo.RelativePath), cacheEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal CompilerCache()
|
||||
{
|
||||
_cache = new ConcurrentDictionary<string, CompilerCacheEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
// Set up ViewStarts
|
||||
foreach (var entry in _cache)
|
||||
{
|
||||
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>
|
||||
|
|
@ -62,7 +82,7 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
var inAssemblyType = typeof(RazorFileInfoCollection);
|
||||
if (inAssemblyType.IsAssignableFrom(t))
|
||||
{
|
||||
var hasParameterlessConstructor = t.GetConstructor(EmptyType) != null;
|
||||
var hasParameterlessConstructor = t.GetConstructor(Type.EmptyTypes) != null;
|
||||
|
||||
return hasParameterlessConstructor
|
||||
&& !t.GetTypeInfo().IsAbstract
|
||||
|
|
@ -74,56 +94,114 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
|
||||
/// <inheritdoc />
|
||||
public CompilationResult GetOrAdd([NotNull] RelativeFileInfo fileInfo,
|
||||
[NotNull] Func<CompilationResult> compile)
|
||||
[NotNull] Func<RelativeFileInfo, CompilationResult> compile)
|
||||
{
|
||||
CompilerCacheEntry cacheEntry;
|
||||
if (!_cache.TryGetValue(NormalizePath(fileInfo.RelativePath), out cacheEntry))
|
||||
{
|
||||
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;
|
||||
|
||||
CompilationResult result;
|
||||
var entry = GetOrAdd(fileInfo, compile, out 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.TrimStart('\\');
|
||||
|
||||
|
|
|
|||
|
|
@ -53,9 +53,9 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
public long Length { get; private set; }
|
||||
|
||||
/// <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>
|
||||
public DateTime LastModified { get; private set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ using Microsoft.Framework.OptionsModel;
|
|||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
/// <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>
|
||||
public class ExpiringFileInfoCache : IFileInfoCache
|
||||
public class DefaultRazorFileSystemCache : IRazorFileSystemCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ExpiringFileInfo> _fileInfoCache =
|
||||
new ConcurrentDictionary<string, ExpiringFileInfo>(StringComparer.Ordinal);
|
||||
|
|
@ -19,7 +20,11 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
private readonly IFileSystem _fileSystem;
|
||||
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;
|
||||
_offset = optionsAccessor.Options.ExpirationBeforeCheckingFilesOnDisk;
|
||||
|
|
@ -34,21 +39,25 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFileInfo GetFileInfo([NotNull] string virtualPath)
|
||||
public IDirectoryContents GetDirectoryContents(string subpath)
|
||||
{
|
||||
IFileInfo fileInfo;
|
||||
ExpiringFileInfo expiringFileInfo;
|
||||
return _fileSystem.GetDirectoryContents(subpath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFileInfo GetFileInfo(string subpath)
|
||||
{
|
||||
ExpiringFileInfo expiringFileInfo;
|
||||
var utcNow = UtcNow;
|
||||
|
||||
if (_fileInfoCache.TryGetValue(virtualPath, out expiringFileInfo)
|
||||
&& expiringFileInfo.ValidUntil > utcNow)
|
||||
if (_fileInfoCache.TryGetValue(subpath, out expiringFileInfo) &&
|
||||
expiringFileInfo.ValidUntil > utcNow)
|
||||
{
|
||||
fileInfo = expiringFileInfo.FileInfo;
|
||||
return expiringFileInfo.FileInfo;
|
||||
}
|
||||
else
|
||||
{
|
||||
fileInfo = _fileSystem.GetFileInfo(virtualPath);
|
||||
var fileInfo = _fileSystem.GetFileInfo(subpath);
|
||||
|
||||
expiringFileInfo = new ExpiringFileInfo()
|
||||
{
|
||||
|
|
@ -56,10 +65,10 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
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
|
||||
|
|
@ -6,18 +6,18 @@ using System;
|
|||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
/// <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>
|
||||
public interface ICompilerCache
|
||||
{
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <param name="fileInfo">A <see cref="RelativeFileInfo"/> representing the file.</param>
|
||||
/// <param name="compile">An delegate that will generate a compilation result.</param>
|
||||
/// <returns>A cached <see cref="CompilationResult"/>.</returns>
|
||||
CompilationResult GetOrAdd([NotNull] RelativeFileInfo fileInfo,
|
||||
[NotNull] Func<CompilationResult> compile);
|
||||
[NotNull] Func<RelativeFileInfo, CompilationResult> compile);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,9 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
private IFileSystem _fileSystem;
|
||||
|
||||
/// <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>
|
||||
/// <remarks>
|
||||
/// <see cref="TimeSpan"/> of <see cref="TimeSpan.Zero"/> or less, means no caching.
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ namespace __ASP_ASSEMBLY
|
|||
" info = new "
|
||||
+ nameof(RazorFileInfo) + @"
|
||||
{{
|
||||
" + nameof(RazorFileInfo.LastModified) + @" = DateTime.FromFileTimeUtc({0:D}),
|
||||
" + nameof(RazorFileInfo.LastModified) + @" = DateTime.FromFileTimeUtc({0:D}).ToLocalTime(),
|
||||
" + nameof(RazorFileInfo.Length) + @" = {1:D},
|
||||
" + nameof(RazorFileInfo.RelativePath) + @" = @""{2}"",
|
||||
" + nameof(RazorFileInfo.FullTypeName) + @" = @""{3}"",
|
||||
|
|
|
|||
|
|
@ -99,12 +99,8 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
else if (Path.GetExtension(fileInfo.Name)
|
||||
.Equals(FileExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var info = new RelativeFileInfo()
|
||||
{
|
||||
FileInfo = fileInfo,
|
||||
RelativePath = Path.Combine(currentPath, fileInfo.Name),
|
||||
};
|
||||
|
||||
var relativePath = Path.Combine(currentPath, fileInfo.Name);
|
||||
var info = new RelativeFileInfo(fileInfo, relativePath);
|
||||
yield return info;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,41 @@
|
|||
// 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 Microsoft.AspNet.FileSystems;
|
||||
|
||||
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 IFileInfo FileInfo { get; set; }
|
||||
public string RelativePath { get; set; }
|
||||
/// <summary>
|
||||
/// 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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// 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.Http;
|
||||
using Microsoft.AspNet.PageExecutionInstrumentation;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
|
|
@ -16,19 +17,19 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
{
|
||||
private readonly ITypeActivator _activator;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IFileInfoCache _fileInfoCache;
|
||||
private readonly IRazorFileSystemCache _fileSystemCache;
|
||||
private readonly ICompilerCache _compilerCache;
|
||||
private IRazorCompilationService _razorcompilationService;
|
||||
|
||||
public VirtualPathRazorPageFactory(ITypeActivator typeActivator,
|
||||
IServiceProvider serviceProvider,
|
||||
ICompilerCache compilerCache,
|
||||
IFileInfoCache fileInfoCache)
|
||||
IRazorFileSystemCache fileSystemCache)
|
||||
{
|
||||
_activator = typeActivator;
|
||||
_serviceProvider = serviceProvider;
|
||||
_compilerCache = compilerCache;
|
||||
_fileInfoCache = fileInfoCache;
|
||||
_fileSystemCache = fileSystemCache;
|
||||
}
|
||||
|
||||
private IRazorCompilationService RazorCompilationService
|
||||
|
|
@ -56,19 +57,15 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
relativePath = relativePath.Substring(1);
|
||||
}
|
||||
|
||||
var fileInfo = _fileInfoCache.GetFileInfo(relativePath);
|
||||
var fileInfo = _fileSystemCache.GetFileInfo(relativePath);
|
||||
|
||||
if (fileInfo.Exists)
|
||||
{
|
||||
var relativeFileInfo = new RelativeFileInfo()
|
||||
{
|
||||
FileInfo = fileInfo,
|
||||
RelativePath = relativePath,
|
||||
};
|
||||
var relativeFileInfo = new RelativeFileInfo(fileInfo, relativePath);
|
||||
|
||||
var result = _compilerCache.GetOrAdd(
|
||||
relativeFileInfo,
|
||||
() => RazorCompilationService.Compile(relativeFileInfo));
|
||||
RazorCompilationService.Compile);
|
||||
|
||||
var page = (IRazorPage)_activator.CreateInstance(_serviceProvider, result.CompiledType);
|
||||
page.Path = relativePath;
|
||||
|
|
@ -78,10 +75,5 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsInstrumentationEnabled(HttpContext context)
|
||||
{
|
||||
return context.GetFeature<IPageExecutionListenerFeature>() != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,16 +108,20 @@ namespace Microsoft.AspNet.Mvc
|
|||
yield return describe.Transient<IViewLocationExpanderProvider, DefaultViewLocationExpanderProvider>();
|
||||
// Caches view locations that are valid for the lifetime of the application.
|
||||
yield return describe.Singleton<IViewLocationCache, DefaultViewLocationCache>();
|
||||
yield return describe.Singleton<IFileInfoCache, ExpiringFileInfoCache>();
|
||||
yield return describe.Singleton<IRazorFileSystemCache, DefaultRazorFileSystemCache>();
|
||||
|
||||
// The host is designed to be discarded after consumption and is very inexpensive to initialize.
|
||||
yield return describe.Transient<IMvcRazorHost>(serviceProvider =>
|
||||
{
|
||||
var optionsAccessor = serviceProvider.GetRequiredService<IOptions<RazorViewEngineOptions>>();
|
||||
return new MvcRazorHost(optionsAccessor.Options.FileSystem);
|
||||
var cachedFileSystem = serviceProvider.GetRequiredService<IRazorFileSystemCache>();
|
||||
return new MvcRazorHost(cachedFileSystem);
|
||||
});
|
||||
|
||||
// Caches compilation artifacts across the lifetime of the application.
|
||||
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>();
|
||||
|
||||
// Both the compiler cache and roslyn compilation service hold on the compilation related
|
||||
|
|
|
|||
|
|
@ -2,17 +2,22 @@
|
|||
// 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.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Builder;
|
||||
using Microsoft.AspNet.TestHost;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
using Microsoft.Framework.Runtime;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||
{
|
||||
public class PrecompilationTest
|
||||
{
|
||||
private static readonly TimeSpan _cacheDelayInterval = TimeSpan.FromSeconds(2);
|
||||
private readonly IServiceProvider _services = TestHelper.CreateServices("PrecompilationWebSite");
|
||||
private readonly Action<IApplicationBuilder> _app = new PrecompilationWebSite.Startup().Configure;
|
||||
|
||||
|
|
@ -20,20 +25,155 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
public async Task PrecompiledView_RendersCorrectly()
|
||||
{
|
||||
// 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 client = server.CreateClient();
|
||||
|
||||
// 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.
|
||||
var expectedContent = typeof(PrecompilationWebSite.Startup).GetTypeInfo().Assembly.GetName().ToString();
|
||||
var assemblyName = typeof(PrecompilationWebSite.Startup).GetTypeInfo().Assembly.GetName().ToString();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/Home/Index");
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
try
|
||||
{
|
||||
// Act - 1
|
||||
var response = await client.GetAsync("http://localhost/Home/Index");
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(expectedContent, responseContent);
|
||||
// Assert - 1
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
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; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.AspNet.FileSystems;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
|
|
@ -17,26 +15,30 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
|
||||
public IDirectoryContents GetDirectoryContents(string subpath)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void AddFile(string path, string contents)
|
||||
{
|
||||
var fileInfo = new Mock<IFileInfo>();
|
||||
fileInfo.Setup(f => f.CreateReadStream())
|
||||
.Returns(() => new MemoryStream(Encoding.UTF8.GetBytes(contents)));
|
||||
fileInfo.SetupGet(f => f.PhysicalPath)
|
||||
.Returns(path);
|
||||
fileInfo.SetupGet(f => f.Name)
|
||||
.Returns(Path.GetFileName(path));
|
||||
fileInfo.SetupGet(f => f.Exists)
|
||||
.Returns(true);
|
||||
AddFile(path, fileInfo.Object);
|
||||
var fileInfo = new TestFileInfo
|
||||
{
|
||||
Content = contents,
|
||||
PhysicalPath = path,
|
||||
Name = Path.GetFileName(path),
|
||||
LastModified = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
AddFile(path, fileInfo);
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
@ -50,5 +52,6 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
return new NotFoundFileInfo(subpath);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,11 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNet.FileSystems;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor.Test
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
public class CompilerCacheTest
|
||||
{
|
||||
|
|
@ -17,24 +16,20 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
public void GetOrAdd_ReturnsCompilationResultFromFactory()
|
||||
{
|
||||
// Arrange
|
||||
var cache = new CompilerCache();
|
||||
var fileInfo = new Mock<IFileInfo>();
|
||||
|
||||
fileInfo
|
||||
.SetupGet(i => i.LastModified)
|
||||
.Returns(DateTime.FromFileTimeUtc(10000));
|
||||
var fileSystem = new TestFileSystem();
|
||||
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), fileSystem);
|
||||
var fileInfo = new TestFileInfo
|
||||
{
|
||||
LastModified = DateTime.FromFileTimeUtc(10000)
|
||||
};
|
||||
|
||||
var type = GetType();
|
||||
var expected = UncachedCompilationResult.Successful(type, "hello world");
|
||||
|
||||
var runtimeFileInfo = new RelativeFileInfo()
|
||||
{
|
||||
FileInfo = fileInfo.Object,
|
||||
RelativePath = "ab",
|
||||
};
|
||||
var runtimeFileInfo = new RelativeFileInfo(fileInfo, "ab");
|
||||
|
||||
// Act
|
||||
var actual = cache.GetOrAdd(runtimeFileInfo, () => expected);
|
||||
var actual = cache.GetOrAdd(runtimeFileInfo, _ => expected);
|
||||
|
||||
// Assert
|
||||
Assert.Same(expected, actual);
|
||||
|
|
@ -75,15 +70,16 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
|
||||
private class ViewCollection : RazorFileInfoCollection
|
||||
{
|
||||
private readonly List<RazorFileInfo> _fileInfos = new List<RazorFileInfo>();
|
||||
|
||||
public ViewCollection()
|
||||
{
|
||||
var fileInfos = new List<RazorFileInfo>();
|
||||
FileInfos = fileInfos;
|
||||
FileInfos = _fileInfos;
|
||||
|
||||
var content = new PreCompile().Content;
|
||||
var length = Encoding.UTF8.GetByteCount(content);
|
||||
|
||||
fileInfos.Add(new RazorFileInfo()
|
||||
Add(new RazorFileInfo()
|
||||
{
|
||||
FullTypeName = typeof(PreCompile).FullName,
|
||||
Hash = RazorFileHash.GetHash(GetMemoryStream(content)),
|
||||
|
|
@ -92,6 +88,11 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
RelativePath = "ab",
|
||||
});
|
||||
}
|
||||
|
||||
public void Add(RazorFileInfo fileInfo)
|
||||
{
|
||||
_fileInfos.Add(fileInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream GetMemoryStream(string content)
|
||||
|
|
@ -102,81 +103,324 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(RuntimeCompileIdentical), 10000, false)]
|
||||
[InlineData(typeof(RuntimeCompileIdentical), 11000, false)]
|
||||
[InlineData(typeof(RuntimeCompileDifferent), 10000, false)] // expected failure: same time and length
|
||||
[InlineData(typeof(RuntimeCompileDifferent), 11000, true)]
|
||||
[InlineData(typeof(RuntimeCompileDifferentLength), 10000, true)]
|
||||
[InlineData(typeof(RuntimeCompileDifferentLength), 10000, true)]
|
||||
public void FileWithTheSameLengthAndDifferentTime_DoesNot_OverridesPrecompilation(
|
||||
Type resultViewType,
|
||||
long fileTimeUTC,
|
||||
bool swapsPreCompile)
|
||||
[InlineData(10000)]
|
||||
[InlineData(11000)]
|
||||
[InlineData(10000)] // expected failure: same time and length
|
||||
public void GetOrAdd_UsesFilesFromCache_IfTimestampDiffers_ButContentAndLengthAreTheSame(long fileTimeUTC)
|
||||
{
|
||||
// Arrange
|
||||
var instance = (View)Activator.CreateInstance(resultViewType);
|
||||
var instance = new RuntimeCompileIdentical();
|
||||
var length = Encoding.UTF8.GetByteCount(instance.Content);
|
||||
|
||||
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>();
|
||||
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()
|
||||
var fileInfo = new TestFileInfo
|
||||
{
|
||||
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",
|
||||
};
|
||||
|
||||
// Act
|
||||
var actual = cache.GetOrAdd(runtimeFileInfo,
|
||||
compile: () => CompilationResult.Successful(resultViewType));
|
||||
compile: _ => { throw new Exception("Shouldn't be called."); });
|
||||
|
||||
// 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);
|
||||
}
|
||||
else
|
||||
Length = length,
|
||||
LastModified = DateTime.FromFileTimeUtc(fileTimeUTC),
|
||||
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]
|
||||
public void GetOrAdd_DoesNotCacheCompiledContent_OnCallsAfterInitial()
|
||||
{
|
||||
// Arrange
|
||||
var lastModified = DateTime.UtcNow;
|
||||
var cache = new CompilerCache();
|
||||
var fileInfo = new Mock<IFileInfo>();
|
||||
fileInfo.SetupGet(f => f.PhysicalPath)
|
||||
.Returns("test");
|
||||
fileInfo.SetupGet(f => f.LastModified)
|
||||
.Returns(lastModified);
|
||||
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), new TestFileSystem());
|
||||
var fileInfo = new TestFileInfo
|
||||
{
|
||||
PhysicalPath = "test",
|
||||
LastModified = lastModified
|
||||
};
|
||||
var type = GetType();
|
||||
var uncachedResult = UncachedCompilationResult.Successful(type, "hello world");
|
||||
|
||||
var runtimeFileInfo = new RelativeFileInfo()
|
||||
{
|
||||
FileInfo = fileInfo.Object,
|
||||
RelativePath = "test",
|
||||
};
|
||||
var runtimeFileInfo = new RelativeFileInfo(fileInfo, "test");
|
||||
|
||||
// Act
|
||||
cache.GetOrAdd(runtimeFileInfo, () => uncachedResult);
|
||||
var actual1 = cache.GetOrAdd(runtimeFileInfo, () => uncachedResult);
|
||||
var actual2 = cache.GetOrAdd(runtimeFileInfo, () => uncachedResult);
|
||||
cache.GetOrAdd(runtimeFileInfo, _ => uncachedResult);
|
||||
var actual1 = cache.GetOrAdd(runtimeFileInfo, _ => uncachedResult);
|
||||
var actual2 = cache.GetOrAdd(runtimeFileInfo, _ => uncachedResult);
|
||||
|
||||
// Assert
|
||||
Assert.NotSame(uncachedResult, actual1);
|
||||
|
|
|
|||
|
|
@ -3,16 +3,14 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.AspNet.FileSystems;
|
||||
using Microsoft.Framework.Expiration.Interfaces;
|
||||
using Microsoft.Framework.OptionsModel;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Razor
|
||||
{
|
||||
public class ExpiringFileInfoCacheTest
|
||||
public class DefaultRazorFileSystemCacheTest
|
||||
{
|
||||
private const string FileName = "myView.cshtml";
|
||||
|
||||
|
|
@ -41,7 +39,7 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
|
||||
public void CreateFile(string fileName)
|
||||
{
|
||||
var fileInfo = new DummyFileInfo()
|
||||
var fileInfo = new TestFileInfo()
|
||||
{
|
||||
Name = fileName,
|
||||
LastModified = DateTime.Now,
|
||||
|
|
@ -89,6 +87,9 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
var fileInfo2 = cache.GetFileInfo(FileName);
|
||||
|
||||
// Assert
|
||||
Assert.True(fileInfo1.Exists);
|
||||
Assert.True(fileInfo1.Exists);
|
||||
|
||||
Assert.Same(fileInfo1, fileInfo2);
|
||||
|
||||
Assert.Equal(FileName, fileInfo1.Name);
|
||||
|
|
@ -306,7 +307,34 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
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)
|
||||
: base(optionsAccessor)
|
||||
|
|
@ -338,7 +366,6 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
_internalUtcNow = UtcNow.AddMilliseconds(milliSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
public class DummyFileSystem : IFileSystem
|
||||
{
|
||||
private Dictionary<string, IFileInfo> _fileInfos = new Dictionary<string, IFileInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
|
|
@ -365,7 +392,7 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
IFileInfo knownInfo;
|
||||
if (_fileInfos.TryGetValue(subpath, out knownInfo))
|
||||
{
|
||||
return new DummyFileInfo()
|
||||
return new TestFileInfo
|
||||
{
|
||||
Name = knownInfo.Name,
|
||||
LastModified = knownInfo.LastModified,
|
||||
|
|
@ -382,48 +409,5 @@ namespace Microsoft.AspNet.Mvc.Razor
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,11 +34,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
|
||||
var razorService = new RazorCompilationService(compiler.Object, host.Object);
|
||||
|
||||
var relativeFileInfo = new RelativeFileInfo()
|
||||
{
|
||||
FileInfo = fileInfo.Object,
|
||||
RelativePath = @"Views\index\home.cshtml",
|
||||
};
|
||||
var relativeFileInfo = new RelativeFileInfo(fileInfo.Object, @"Views\index\home.cshtml");
|
||||
|
||||
// Act
|
||||
razorService.Compile(relativeFileInfo);
|
||||
|
|
@ -47,6 +43,73 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
|
|||
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()
|
||||
{
|
||||
return new GeneratorResults(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
{
|
||||
"code": [
|
||||
"**/*.cs",
|
||||
"../Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileSystem.cs",
|
||||
"../Microsoft.AspNet.Mvc.Razor.Host.Test/TestFileInfo.cs"
|
||||
],
|
||||
"dependencies": {
|
||||
"Microsoft.AspNet.Mvc.Razor": "6.0.0-*",
|
||||
"Microsoft.AspNet.Testing": "1.0.0-*",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
*.cshtml
|
||||
|
|
@ -1,2 +1 @@
|
|||
@using System.Reflection
|
||||
@(GetType().GetTypeInfo().Assembly.GetName())
|
||||
index:@GetType().GetTypeInfo().Assembly.GetName()
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
Layout:@GetType().GetTypeInfo().Assembly.FullName
|
||||
@RenderBody()
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
@using System.Reflection
|
||||
@{ Layout = "/views/Home/Layout.cshtml";}
|
||||
_viewstart:@GetType().GetTypeInfo().Assembly.FullName
|
||||
|
||||
Loading…
Reference in New Issue