Merge pull request #6376 from aspnet/rel/2.0.0-preview2

Perform case insensitive lookups for precompiled views
This commit is contained in:
Pranav K 2017-06-07 15:19:42 -07:00 committed by GitHub
commit 8662422e18
4 changed files with 125 additions and 10 deletions

View File

@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{ {
private readonly object _initializeLock = new object(); private readonly object _initializeLock = new object();
private readonly object _cacheLock = new object(); private readonly object _cacheLock = new object();
private readonly Dictionary<string, Task<CompiledViewDescriptor>> _precompiledViewLookup;
private readonly ConcurrentDictionary<string, string> _normalizedPathLookup; private readonly ConcurrentDictionary<string, string> _normalizedPathLookup;
private readonly IFileProvider _fileProvider; private readonly IFileProvider _fileProvider;
private readonly RazorTemplateEngine _templateEngine; private readonly RazorTemplateEngine _templateEngine;
@ -81,12 +82,23 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
_normalizedPathLookup = new ConcurrentDictionary<string, string>(StringComparer.Ordinal); _normalizedPathLookup = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
_cache = new MemoryCache(new MemoryCacheOptions()); _cache = new MemoryCache(new MemoryCacheOptions());
_precompiledViewLookup = new Dictionary<string, Task<CompiledViewDescriptor>>(
precompiledViews.Count,
StringComparer.OrdinalIgnoreCase);
foreach (var precompiledView in precompiledViews) foreach (var precompiledView in precompiledViews)
{ {
_cache.Set( if (_precompiledViewLookup.TryGetValue(precompiledView.RelativePath, out var otherValue))
precompiledView.RelativePath, {
Task.FromResult(precompiledView), var message = string.Join(
new MemoryCacheEntryOptions { Priority = CacheItemPriority.NeverRemove }); Environment.NewLine,
Resources.RazorViewCompiler_ViewPathsDifferOnlyInCase,
otherValue.Result.RelativePath,
precompiledView.RelativePath);
throw new InvalidOperationException(message);
}
_precompiledViewLookup.Add(precompiledView.RelativePath, Task.FromResult(precompiledView));
} }
} }
@ -98,17 +110,40 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
throw new ArgumentNullException(nameof(relativePath)); throw new ArgumentNullException(nameof(relativePath));
} }
// Lookup precompiled views first.
// Attempt to lookup the cache entry using the passed in path. This will succeed if the path is already // Attempt to lookup the cache entry using the passed in path. This will succeed if the path is already
// normalized and a cache entry exists. // normalized and a cache entry exists.
if (!_cache.TryGetValue(relativePath, out Task<CompiledViewDescriptor> cachedResult)) string normalizedPath = null;
Task<CompiledViewDescriptor> cachedResult;
if (_precompiledViewLookup.Count > 0)
{ {
var normalizedPath = GetNormalizedPath(relativePath); if (_precompiledViewLookup.TryGetValue(relativePath, out cachedResult))
if (!_cache.TryGetValue(normalizedPath, out cachedResult))
{ {
cachedResult = CreateCacheEntry(normalizedPath); return cachedResult;
}
normalizedPath = GetNormalizedPath(relativePath);
if (_precompiledViewLookup.TryGetValue(normalizedPath, out cachedResult))
{
return cachedResult;
} }
} }
if (_cache.TryGetValue(relativePath, out cachedResult))
{
return cachedResult;
}
normalizedPath = normalizedPath ?? GetNormalizedPath(relativePath);
if (_cache.TryGetValue(normalizedPath, out cachedResult))
{
return cachedResult;
}
// Entry does not exist. Attempt to create one.
cachedResult = CreateCacheEntry(normalizedPath);
return cachedResult; return cachedResult;
} }

View File

@ -350,6 +350,20 @@ namespace Microsoft.AspNetCore.Mvc.Razor
internal static string FormatPropertyMustBeSet(object p0, object p1) internal static string FormatPropertyMustBeSet(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("PropertyMustBeSet"), p0, p1); => string.Format(CultureInfo.CurrentCulture, GetString("PropertyMustBeSet"), p0, p1);
/// <summary>
/// The following precompiled view paths differ only in case, which is not supported:
/// </summary>
internal static string RazorViewCompiler_ViewPathsDifferOnlyInCase
{
get => GetString("RazorViewCompiler_ViewPathsDifferOnlyInCase");
}
/// <summary>
/// The following precompiled view paths differ only in case, which is not supported:
/// </summary>
internal static string FormatRazorViewCompiler_ViewPathsDifferOnlyInCase()
=> GetString("RazorViewCompiler_ViewPathsDifferOnlyInCase");
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -191,4 +191,7 @@
<data name="PropertyMustBeSet" xml:space="preserve"> <data name="PropertyMustBeSet" xml:space="preserve">
<value>The property '{0}' of '{1}' must not be null.</value> <value>The property '{0}' of '{1}' must not be null.</value>
</data> </data>
<data name="RazorViewCompiler_ViewPathsDifferOnlyInCase" xml:space="preserve">
<value>The following precompiled view paths differ only in case, which is not supported:</value>
</data>
</root> </root>

View File

@ -16,6 +16,27 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{ {
public class RazorViewCompilerTest public class RazorViewCompilerTest
{ {
[Fact]
public void Constructor_ThrowsIfMultiplePrecompiledViewsHavePathsDifferingOnlyInCase()
{
// Arrange
var fileProvider = new TestFileProvider();
var precompiledViews = new[]
{
new CompiledViewDescriptor { RelativePath = "/Views/Home/About.cshtml" },
new CompiledViewDescriptor { RelativePath = "/Views/home/About.cshtml" },
};
var message = string.Join(
Environment.NewLine,
"The following precompiled view paths differ only in case, which is not supported:",
precompiledViews[0].RelativePath,
precompiledViews[1].RelativePath);
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => GetViewCompiler(fileProvider, precompiledViews: precompiledViews));
Assert.Equal(message, ex.Message);
}
[Fact] [Fact]
public async Task CompileAsync_ReturnsResultWithNullAttribute_IfFileIsNotFoundInFileSystem() public async Task CompileAsync_ReturnsResultWithNullAttribute_IfFileIsNotFoundInFileSystem()
{ {
@ -178,6 +199,49 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
Assert.Same(precompiledView, result); Assert.Same(precompiledView, result);
} }
[Theory]
[InlineData("/views/home/index.cshtml")]
[InlineData("/VIEWS/HOME/INDEX.CSHTML")]
[InlineData("/viEws/HoME/inDex.cshtml")]
public async Task CompileAsync_PerformsCaseInsensitiveLookupsForPrecompiledViews(string lookupPath)
{
// Arrange
var path = "/Views/Home/Index.cshtml";
var fileProvider = new TestFileProvider();
var fileInfo = fileProvider.AddFile(path, "some content");
var precompiledView = new CompiledViewDescriptor
{
RelativePath = path,
};
var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
// Act
var result = await viewCompiler.CompileAsync(lookupPath);
// Assert
Assert.Same(precompiledView, result);
}
[Fact]
public async Task CompileAsync_PerformsCaseInsensitiveLookupsForPrecompiledViews_WithNonNormalizedPaths()
{
// Arrange
var path = "/Views/Home/Index.cshtml";
var fileProvider = new TestFileProvider();
var fileInfo = fileProvider.AddFile(path, "some content");
var precompiledView = new CompiledViewDescriptor
{
RelativePath = path,
};
var viewCompiler = GetViewCompiler(fileProvider, precompiledViews: new[] { precompiledView });
// Act
var result = await viewCompiler.CompileAsync("Views\\Home\\Index.cshtml");
// Assert
Assert.Same(precompiledView, result);
}
[Fact] [Fact]
public async Task CompileAsync_DoesNotRecompile_IfFileTriggerWasSetForPrecompiledView() public async Task CompileAsync_DoesNotRecompile_IfFileTriggerWasSetForPrecompiledView()
{ {
@ -200,7 +264,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
Assert.Same(precompiledView, result); Assert.Same(precompiledView, result);
} }
[Fact] [Fact]
public async Task GetOrAdd_AllowsConcurrentCompilationOfMultipleRazorPages() public async Task GetOrAdd_AllowsConcurrentCompilationOfMultipleRazorPages()
{ {
@ -465,7 +528,7 @@ this should fail";
fileProvider = fileProvider ?? new TestFileProvider(); fileProvider = fileProvider ?? new TestFileProvider();
compilationCallback = compilationCallback ?? (_ => { }); compilationCallback = compilationCallback ?? (_ => { });
var options = new TestOptionsManager<RazorViewEngineOptions>(); var options = new TestOptionsManager<RazorViewEngineOptions>();
if (referenceManager == null) if (referenceManager == null)
{ {
var applicationPartManager = new ApplicationPartManager(); var applicationPartManager = new ApplicationPartManager();
var assembly = typeof(RazorViewCompilerTest).Assembly; var assembly = typeof(RazorViewCompilerTest).Assembly;