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.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('\\');

View File

@ -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; }
}
}

View File

@ -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

View File

@ -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);
}
}

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;
/// <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.

View File

@ -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}"",

View File

@ -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;
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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; }
}
}
}

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.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);
}
}
}
}

View File

@ -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);

View File

@ -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();
}
}
}
}

View File

@ -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(

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": {
"Microsoft.AspNet.Mvc.Razor": "6.0.0-*",
"Microsoft.AspNet.Testing": "1.0.0-*",

View File

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

View File

@ -1,2 +1 @@
@using System.Reflection
@(GetType().GetTypeInfo().Assembly.GetName())
index:@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