From 1124eb50167f245ca486dbcba2df208ed27a106e Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 7 Jun 2017 10:16:26 -0700 Subject: [PATCH] Perform case insensitive lookups for precompiled views --- .../Internal/RazorViewCompiler.cs | 51 +++++++++++--- .../Properties/Resources.Designer.cs | 14 ++++ .../Resources.resx | 3 + .../Internal/RazorViewCompilerTest.cs | 67 ++++++++++++++++++- 4 files changed, 125 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs index eb4da99fd7..4ba321f8be 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs @@ -27,6 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal { private readonly object _initializeLock = new object(); private readonly object _cacheLock = new object(); + private readonly Dictionary> _precompiledViewLookup; private readonly ConcurrentDictionary _normalizedPathLookup; private readonly IFileProvider _fileProvider; private readonly RazorTemplateEngine _templateEngine; @@ -82,12 +83,23 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal _normalizedPathLookup = new ConcurrentDictionary(StringComparer.Ordinal); _cache = new MemoryCache(new MemoryCacheOptions()); + _precompiledViewLookup = new Dictionary>( + 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)); } } @@ -99,17 +111,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 cachedResult)) + string normalizedPath = null; + Task 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; } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs index 3790719784..3ac504edc8 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs @@ -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); + /// + /// The following precompiled view paths differ only in case, which is not supported: + /// + internal static string RazorViewCompiler_ViewPathsDifferOnlyInCase + { + get => GetString("RazorViewCompiler_ViewPathsDifferOnlyInCase"); + } + + /// + /// The following precompiled view paths differ only in case, which is not supported: + /// + internal static string FormatRazorViewCompiler_ViewPathsDifferOnlyInCase() + => GetString("RazorViewCompiler_ViewPathsDifferOnlyInCase"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx index 13325e77e3..777ba9eec7 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx @@ -191,4 +191,7 @@ The property '{0}' of '{1}' must not be null. + + The following precompiled view paths differ only in case, which is not supported: + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerTest.cs index 62550fd260..0abaceed4b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerTest.cs @@ -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(() => 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(); - if (referenceManager == null) + if (referenceManager == null) { var applicationPartManager = new ApplicationPartManager(); var assembly = typeof(RazorViewCompilerTest).Assembly;