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 _cacheLock = new object();
private readonly Dictionary<string, Task<CompiledViewDescriptor>> _precompiledViewLookup;
private readonly ConcurrentDictionary<string, string> _normalizedPathLookup;
private readonly IFileProvider _fileProvider;
private readonly RazorTemplateEngine _templateEngine;
@ -81,12 +82,23 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
_normalizedPathLookup = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
_cache = new MemoryCache(new MemoryCacheOptions());
_precompiledViewLookup = new Dictionary<string, Task<CompiledViewDescriptor>>(
precompiledViews.Count,
StringComparer.OrdinalIgnoreCase);
foreach (var precompiledView in precompiledViews)
{
_cache.Set(
precompiledView.RelativePath,
Task.FromResult(precompiledView),
new MemoryCacheEntryOptions { Priority = CacheItemPriority.NeverRemove });
if (_precompiledViewLookup.TryGetValue(precompiledView.RelativePath, out var otherValue))
{
var message = string.Join(
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));
}
// Lookup precompiled views first.
// 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.
if (!_cache.TryGetValue(relativePath, out Task<CompiledViewDescriptor> cachedResult))
string normalizedPath = null;
Task<CompiledViewDescriptor> cachedResult;
if (_precompiledViewLookup.Count > 0)
{
var normalizedPath = GetNormalizedPath(relativePath);
if (!_cache.TryGetValue(normalizedPath, out cachedResult))
if (_precompiledViewLookup.TryGetValue(relativePath, 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;
}

View File

@ -350,6 +350,20 @@ namespace Microsoft.AspNetCore.Mvc.Razor
internal static string FormatPropertyMustBeSet(object p0, object 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)
{
var value = _resourceManager.GetString(name);

View File

@ -191,4 +191,7 @@
<data name="PropertyMustBeSet" xml:space="preserve">
<value>The property '{0}' of '{1}' must not be null.</value>
</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>

View File

@ -16,6 +16,27 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
{
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]
public async Task CompileAsync_ReturnsResultWithNullAttribute_IfFileIsNotFoundInFileSystem()
{
@ -178,6 +199,49 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
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]
public async Task CompileAsync_DoesNotRecompile_IfFileTriggerWasSetForPrecompiledView()
{
@ -200,7 +264,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
Assert.Same(precompiledView, result);
}
[Fact]
public async Task GetOrAdd_AllowsConcurrentCompilationOfMultipleRazorPages()
{
@ -465,7 +528,7 @@ this should fail";
fileProvider = fileProvider ?? new TestFileProvider();
compilationCallback = compilationCallback ?? (_ => { });
var options = new TestOptionsManager<RazorViewEngineOptions>();
if (referenceManager == null)
if (referenceManager == null)
{
var applicationPartManager = new ApplicationPartManager();
var assembly = typeof(RazorViewCompilerTest).Assembly;