From 91da10a08f7e66dabb934dfcb9d5bf912dbeb430 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 26 Jun 2017 16:02:54 -0700 Subject: [PATCH] Normalize paths returned by view location expanders Fixes #6448 --- .../Compilation/CompiledViewDescriptor.cs | 3 + .../DefaultRazorPageFactoryProvider.cs | 4 +- .../RazorPageFactoryResult.cs | 64 +-------- .../RazorViewEngine.cs | 37 ++--- .../ViewEngineTests.cs | 16 +++ .../DefaultRazorPageFactoryProviderTest.cs | 6 +- .../RazorViewEngineTest.cs | 126 ++++++++++++------ .../RazorViewTest.cs | 4 +- .../Controllers/BackSlashController.cs | 12 ++ .../WebSites/RazorWebSite/RazorWebSite.csproj | 1 + .../Services/BackSlashExpander.cs | 27 ++++ test/WebSites/RazorWebSite/Startup.cs | 2 + .../Views/BackSlash/BackSlashView.cshtml | 6 + .../Views/BackSlash/_BackSlashPartial.cshtml | 1 + .../Views/BackSlash/_Layout.cshtml | 2 + 15 files changed, 183 insertions(+), 128 deletions(-) create mode 100644 test/WebSites/RazorWebSite/Controllers/BackSlashController.cs create mode 100644 test/WebSites/RazorWebSite/Services/BackSlashExpander.cs create mode 100644 test/WebSites/RazorWebSite/Views/BackSlash/BackSlashView.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/BackSlash/_BackSlashPartial.cshtml create mode 100644 test/WebSites/RazorWebSite/Views/BackSlash/_Layout.cshtml diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewDescriptor.cs index 34514536e7..c88a512af0 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewDescriptor.cs @@ -8,6 +8,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation { public class CompiledViewDescriptor { + /// + /// The normalized application relative path of the view. + /// public string RelativePath { get; set; } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorPageFactoryProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorPageFactoryProvider.cs index 09af95c9c1..0ffd390d52 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorPageFactoryProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorPageFactoryProvider.cs @@ -57,11 +57,11 @@ public RazorPageFactoryResult CreateFactory(string relativePath) var pageFactory = Expression .Lambda>(objectInitializeExpression) .Compile(); - return new RazorPageFactoryResult(pageFactory, viewDescriptor.ExpirationTokens, viewDescriptor.IsPrecompiled); + return new RazorPageFactoryResult(viewDescriptor, pageFactory); } else { - return new RazorPageFactoryResult(viewDescriptor.ExpirationTokens); + return new RazorPageFactoryResult(viewDescriptor, razorPageFactory: null); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageFactoryResult.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageFactoryResult.cs index 1d430ca4da..31f7b1ce94 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageFactoryResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorPageFactoryResult.cs @@ -2,8 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; -using Microsoft.Extensions.Primitives; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; namespace Microsoft.AspNetCore.Mvc.Razor { @@ -12,61 +11,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor /// public struct RazorPageFactoryResult { - /// - /// Initializes a new instance of with the - /// specified . - /// - /// One or more instances. - public RazorPageFactoryResult(IList expirationTokens) - { - if (expirationTokens == null) - { - throw new ArgumentNullException(nameof(expirationTokens)); - } - - ExpirationTokens = expirationTokens; - RazorPageFactory = null; - IsPrecompiled = false; - } - /// /// Initializes a new instance of with the /// specified factory. /// /// The factory. - /// One or more instances. + /// The . public RazorPageFactoryResult( - Func razorPageFactory, - IList expirationTokens) - : this(razorPageFactory, expirationTokens, isPrecompiled: false) + CompiledViewDescriptor viewDescriptor, + Func razorPageFactory) { - } - - /// - /// Initializes a new instance of with the - /// specified factory. - /// - /// The factory. - /// One or more instances. - /// true if the view is precompiled, false otherwise. - public RazorPageFactoryResult( - Func razorPageFactory, - IList expirationTokens, - bool isPrecompiled) - { - if (razorPageFactory == null) - { - throw new ArgumentNullException(nameof(razorPageFactory)); - } - - if (expirationTokens == null) - { - throw new ArgumentNullException(nameof(expirationTokens)); - } - + ViewDescriptor = viewDescriptor ?? throw new ArgumentNullException(nameof(viewDescriptor)); RazorPageFactory = razorPageFactory; - ExpirationTokens = expirationTokens; - IsPrecompiled = isPrecompiled; } /// @@ -76,19 +32,13 @@ public RazorPageFactoryResult( public Func RazorPageFactory { get; } /// - /// One or more s associated with this instance of - /// . + /// Gets the . /// - public IList ExpirationTokens { get; } + public CompiledViewDescriptor ViewDescriptor { get; } /// /// Gets a value that determines if the page was successfully located. /// public bool Success => RazorPageFactory != null; - - /// - /// Gets a value that determines if the view is precompiled. - /// - public bool IsPrecompiled { get; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs index 243a899f46..75e7382423 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs @@ -393,11 +393,12 @@ internal ViewLocationCacheResult CreateCacheResult( bool isMainPage) { var factoryResult = _pageFactory.CreateFactory(relativePath); - if (factoryResult.ExpirationTokens != null) + var viewDescriptor = factoryResult.ViewDescriptor; + if (viewDescriptor?.ExpirationTokens != null) { - for (var i = 0; i < factoryResult.ExpirationTokens.Count; i++) + for (var i = 0; i < viewDescriptor.ExpirationTokens.Count; i++) { - expirationTokens.Add(factoryResult.ExpirationTokens[i]); + expirationTokens.Add(viewDescriptor.ExpirationTokens[i]); } } @@ -405,9 +406,9 @@ internal ViewLocationCacheResult CreateCacheResult( { // Only need to lookup _ViewStarts for the main page. var viewStartPages = isMainPage ? - GetViewStartPages(relativePath, expirationTokens) : + GetViewStartPages(viewDescriptor.RelativePath, expirationTokens) : Array.Empty(); - if (factoryResult.IsPrecompiled) + if (viewDescriptor.IsPrecompiled) { _logger.PrecompiledViewFound(relativePath); } @@ -424,17 +425,17 @@ private IReadOnlyList GetViewStartPages( string path, HashSet expirationTokens) { - var applicationRelativePath = MakePathApplicationRelative(path); var viewStartPages = new List(); - foreach (var viewStartProjectItem in _razorProject.FindHierarchicalItems(applicationRelativePath, ViewStartFileName)) + foreach (var viewStartProjectItem in _razorProject.FindHierarchicalItems(path, ViewStartFileName)) { var result = _pageFactory.CreateFactory(viewStartProjectItem.Path); - if (result.ExpirationTokens != null) + var viewDescriptor = result.ViewDescriptor; + if (viewDescriptor?.ExpirationTokens != null) { - for (var i = 0; i < result.ExpirationTokens.Count; i++) + for (var i = 0; i < viewDescriptor.ExpirationTokens.Count; i++) { - expirationTokens.Add(result.ExpirationTokens[i]); + expirationTokens.Add(viewDescriptor.ExpirationTokens[i]); } } @@ -476,22 +477,6 @@ private static bool IsApplicationRelativePath(string name) return name[0] == '~' || name[0] == '/'; } - private string MakePathApplicationRelative(string path) - { - Debug.Assert(!string.IsNullOrEmpty(path)); - if (path[0] == '~') - { - path = path.Substring(1); - } - - if (path[0] != '/') - { - path = '/' + path; - } - - return path; - } - private static bool IsRelativePath(string name) { Debug.Assert(!string.IsNullOrEmpty(name)); diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ViewEngineTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ViewEngineTests.cs index cfa8378b50..af66f1ce37 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ViewEngineTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ViewEngineTests.cs @@ -476,5 +476,21 @@ public async Task RazorView_SetsViewPathAndExecutingPagePath() ignoreLineEndingDifferences: true); #endif } + + [Fact] + public async Task ViewEngine_NormalizesPathsReturnedByViewLocationExpanders() + { + // Arrange + var expected = +@"Layout +Page +Partial"; + + // Act + var responseContent = await Client.GetStringAsync("/BackSlash"); + + // Assert + Assert.Equal(expected, responseContent.Trim()); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorPageFactoryProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorPageFactoryProviderTest.cs index 8a8697c731..c216e4993a 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorPageFactoryProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorPageFactoryProviderTest.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal public class DefaultRazorPageFactoryProviderTest { [Fact] - public void CreateFactory_ReturnsExpirationTokensFromCompilerCache_ForUnsuccessfulResults() + public void CreateFactory_ReturnsViewDescriptor_ForUnsuccessfulResults() { // Arrange var path = "/file-does-not-exist"; @@ -39,11 +39,11 @@ public void CreateFactory_ReturnsExpirationTokensFromCompilerCache_ForUnsuccessf // Assert Assert.False(result.Success); - Assert.Equal(expirationTokens, result.ExpirationTokens); + Assert.Same(descriptor, result.ViewDescriptor); } [Fact] - public void CreateFactory_ReturnsExpirationTokensFromCompilerCache_ForSuccessfulResults() + public void CreateFactory_ReturnsViewDescriptor_ForSuccessfulResults() { // Arrange var relativePath = "/file-exists"; diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs index be2529dd93..5334cbe371 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewEngines; @@ -168,15 +169,15 @@ public void FindView_ReturnsRazorView_IfLookupWasSuccessful() pageFactory .Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml")) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => page)); pageFactory .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) - .Returns(new RazorPageFactoryResult(() => viewStart2, new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => viewStart2)); pageFactory .Setup(p => p.CreateFactory("/_ViewStart.cshtml")) - .Returns(new RazorPageFactoryResult(() => viewStart1, new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => viewStart1)); var viewEngine = CreateViewEngine(pageFactory.Object); var context = GetActionContext(_controllerTestContext); @@ -204,11 +205,11 @@ public void FindView_DoesNotExpireCachedResults_IfViewStartsExpire() pageFactory .Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml")) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => page)); pageFactory .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) - .Returns(new RazorPageFactoryResult(() => viewStart, new[] { changeToken })); + .Returns(GetPageFactoryResult(() => viewStart, new[] { changeToken })); var viewEngine = CreateViewEngine(pageFactory.Object); var context = GetActionContext(_controllerTestContext); @@ -333,15 +334,15 @@ public void FindView_IsMainPage_ReturnsRazorView_IfLookupWasSuccessful() pageFactory .Setup(p => p.CreateFactory("/Views/bar/test-view.cshtml")) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => page)); pageFactory .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) - .Returns(new RazorPageFactoryResult(() => viewStart2, new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => viewStart2)); pageFactory .Setup(p => p.CreateFactory("/_ViewStart.cshtml")) - .Returns(new RazorPageFactoryResult(() => viewStart1, new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => viewStart1)); var viewEngine = CreateViewEngine(pageFactory.Object); var context = GetActionContext(_controllerTestContext); @@ -369,7 +370,7 @@ public void FindView_UsesViewLocationFormat_IfRouteDoesNotContainArea() var page = Mock.Of(); pageFactory .Setup(p => p.CreateFactory("fake-path1/bar/test-view.rzr")) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) + .Returns(GetPageFactoryResult(() => page)) .Verifiable(); var viewEngine = new TestableRazorViewEngine( pageFactory.Object, @@ -397,7 +398,7 @@ public void FindView_UsesAreaViewLocationFormat_IfRouteContainsArea() var page = Mock.Of(); pageFactory .Setup(p => p.CreateFactory(expectedViewName)) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) + .Returns(GetPageFactoryResult(() => page)) .Verifiable(); var viewEngine = new TestableRazorViewEngine( pageFactory.Object, @@ -426,7 +427,7 @@ public void GetView_DoesNotUseViewLocationFormat_WithRelativePath_IfRouteDoesNot var page = Mock.Of(); pageFactory .Setup(p => p.CreateFactory(expectedViewName)) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) + .Returns(GetPageFactoryResult(() => page)) .Verifiable(); var viewEngine = new TestableRazorViewEngine( pageFactory.Object, @@ -452,7 +453,7 @@ public void GetView_DoesNotUseViewLocationFormat_WithRelativePath_IfRouteContain var page = Mock.Of(); pageFactory .Setup(p => p.CreateFactory(expectedViewName)) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) + .Returns(GetPageFactoryResult(() => page)) .Verifiable(); var viewEngine = new TestableRazorViewEngine( pageFactory.Object, @@ -480,7 +481,7 @@ public void GetView_UsesGivenPath_WithAppRelativePath(string viewName) var page = Mock.Of(); pageFactory .Setup(p => p.CreateFactory(viewName)) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) + .Returns(GetPageFactoryResult(() => page)) .Verifiable(); var viewEngine = new TestableRazorViewEngine( pageFactory.Object, @@ -508,7 +509,7 @@ public void GetView_ResolvesRelativeToCurrentPage_WithRelativePath(string viewNa var page = Mock.Of(); pageFactory .Setup(p => p.CreateFactory(expectedViewName)) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) + .Returns(GetPageFactoryResult(() => page)) .Verifiable(); var viewEngine = new TestableRazorViewEngine( pageFactory.Object, @@ -536,7 +537,7 @@ public void GetView_ResolvesRelativeToAppRoot_WithRelativePath_IfNoPageExecuting var page = Mock.Of(); pageFactory .Setup(p => p.CreateFactory(expectedViewName)) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) + .Returns(GetPageFactoryResult(() => page)) .Verifiable(); var viewEngine = new TestableRazorViewEngine( pageFactory.Object, @@ -564,10 +565,10 @@ public void FindView_CreatesDifferentCacheEntries_ForAreaViewsAndNonAreaViews(bo var nonAreaPage = Mock.Of(); pageFactory .Setup(p => p.CreateFactory("/Areas/Admin/Views/Home/Index.cshtml")) - .Returns(new RazorPageFactoryResult(() => areaPage, new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => areaPage)); pageFactory .Setup(p => p.CreateFactory("/Views/Home/Index.cshtml")) - .Returns(new RazorPageFactoryResult(() => nonAreaPage, new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => nonAreaPage)); var viewEngine = new TestableRazorViewEngine( pageFactory.Object, @@ -628,10 +629,10 @@ public void FindView_CreatesDifferentCacheEntries_ForDifferentAreas(bool isMainP var areaPage2 = Mock.Of(); pageFactory .Setup(p => p.CreateFactory("/Areas/Marketing/Views/Home/Index.cshtml")) - .Returns(new RazorPageFactoryResult(() => areaPage1, new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => areaPage1)); pageFactory .Setup(p => p.CreateFactory("/Areas/Sales/Views/Home/Index.cshtml")) - .Returns(new RazorPageFactoryResult(() => areaPage2, new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => areaPage2)); var viewEngine = new TestableRazorViewEngine( pageFactory.Object, @@ -692,7 +693,7 @@ public void FindView_UsesViewLocationExpandersToLocateViews( var pageFactory = new Mock(); pageFactory .Setup(p => p.CreateFactory("test-string/bar.cshtml")) - .Returns(new RazorPageFactoryResult(() => Mock.Of(), new IChangeToken[0])) + .Returns(GetPageFactoryResult(() => Mock.Of())) .Verifiable(); var expander1Result = new[] { "some-seed" }; @@ -745,6 +746,37 @@ public void FindView_UsesViewLocationExpandersToLocateViews( expander2.Verify(); } + [Fact] + public void FindView_NoramlizesPaths_ReturnedByViewLocationExpanders() + { + // Arrange + var pageFactory = new Mock(); + pageFactory + .Setup(p => p.CreateFactory(@"Views\Home\Index.cshtml")) + .Returns(GetPageFactoryResult(() => Mock.Of())) + .Verifiable(); + + var expander = new Mock(); + expander + .Setup(e => e.ExpandViewLocations( + It.IsAny(), + It.IsAny>())) + .Returns(new[] { @"Views\Home\Index.cshtml" }); + + var viewEngine = CreateViewEngine( + pageFactory.Object, + new[] { expander.Object }); + var context = GetActionContext(new Dictionary()); + + // Act + var result = viewEngine.FindView(context, "test-view", isMainPage: true); + + // Assert + Assert.True(result.Success); + Assert.IsAssignableFrom(result.View); + pageFactory.Verify(); + } + [Fact] public void FindView_CachesValuesIfViewWasFound() { @@ -753,11 +785,11 @@ public void FindView_CachesValuesIfViewWasFound() var pageFactory = new Mock(); pageFactory .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml")) - .Returns(new RazorPageFactoryResult(new IChangeToken[0])) + .Returns(GetPageFactoryResult(factory: null)) .Verifiable(); pageFactory .Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml")) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) + .Returns(GetPageFactoryResult(() => page)) .Verifiable(); var viewEngine = CreateViewEngine(pageFactory.Object); @@ -794,7 +826,7 @@ public void FindView_CachesValuesIfViewWasFound_ForPages() var pageFactory = new Mock(); pageFactory .Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml")) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) + .Returns(GetPageFactoryResult(() => page)) .Verifiable(); var viewEngine = CreateViewEngine(pageFactory.Object); @@ -837,16 +869,16 @@ public void FindView_InvokesPageFactoryIfChangeTokenExpired() pageFactory .InSequence(sequence) .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml")) - .Returns(new RazorPageFactoryResult(new[] { changeToken })); + .Returns(GetPageFactoryResult(factory: null, changeTokens: new[] { changeToken })); pageFactory .InSequence(sequence) .Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml")) - .Returns(new RazorPageFactoryResult(() => page1, new IChangeToken[0])) + .Returns(GetPageFactoryResult(() => page1)) .Verifiable(); pageFactory .InSequence(sequence) .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml")) - .Returns(new RazorPageFactoryResult(() => page2, new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => page2)); var viewEngine = CreateViewEngine(pageFactory.Object); var context = GetActionContext(_controllerTestContext); @@ -885,20 +917,20 @@ public void FindView_InvokesPageFactoryIfViewStartExpirationTokensHaveExpired() pageFactory .InSequence(sequence) .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml")) - .Returns(new RazorPageFactoryResult(() => page1, new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => page1)); pageFactory .InSequence(sequence) .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) - .Returns(new RazorPageFactoryResult(new[] { changeToken })) + .Returns(GetPageFactoryResult(factory: null, changeTokens: new[] { changeToken })) .Verifiable(); pageFactory .InSequence(sequence) .Setup(p => p.CreateFactory("/Views/bar/baz.cshtml")) - .Returns(new RazorPageFactoryResult(() => page2, new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => page2)); pageFactory .InSequence(sequence) .Setup(p => p.CreateFactory("/Views/_ViewStart.cshtml")) - .Returns(new RazorPageFactoryResult(() => viewStart, new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => viewStart)); var fileProvider = new TestFileProvider(); var razorProject = new FileProviderRazorProject(fileProvider); @@ -992,7 +1024,7 @@ public void FindView_InvokesViewLocationExpanders_IfChangeTokenExpires() var pageFactory = new Mock(); pageFactory .Setup(p => p.CreateFactory("viewlocation3")) - .Returns(new RazorPageFactoryResult(new[] { changeToken })); + .Returns(GetPageFactoryResult(factory: null, changeTokens: new[] { changeToken })); var expander = new Mock(); var expandedLocations = new[] { @@ -1030,7 +1062,7 @@ public void FindView_InvokesViewLocationExpanders_IfChangeTokenExpires() // Act - 2 pageFactory .Setup(p => p.CreateFactory("viewlocation3")) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => page)); cancellationTokenSource.Cancel(); result = viewEngine.FindView(context, "MyView", isMainPage: true); @@ -1130,7 +1162,7 @@ public void FindPage_UsesViewLocationExpander_ToExpandPaths( var pageFactory = new Mock(); pageFactory .Setup(p => p.CreateFactory("expanded-path/bar-layout")) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) + .Returns(GetPageFactoryResult(() => page)) .Verifiable(); var expander = new Mock(); @@ -1209,7 +1241,7 @@ public void FindPage_SelectsActionCaseInsensitively() var pageFactory = new Mock(); pageFactory .Setup(p => p.CreateFactory("/Views/Foo/details.cshtml")) - .Returns(new RazorPageFactoryResult(() => page.Object, new IChangeToken[0])) + .Returns(GetPageFactoryResult(() => page.Object)) .Verifiable(); var viewEngine = CreateViewEngine(pageFactory.Object); @@ -1338,10 +1370,12 @@ public void CreateCacheResult_LogsPrecompiledViewFound() var loggerFactory = new TestLoggerFactory(sink, enabled: true); var relativePath = "/Views/Foo/details.cshtml"; + var factoryResult = GetPageFactoryResult(() => Mock.Of()); + factoryResult.ViewDescriptor.IsPrecompiled = true; var pageFactory = new Mock(); pageFactory .Setup(p => p.CreateFactory(relativePath)) - .Returns(new RazorPageFactoryResult(() => Mock.Of(), new IChangeToken[0], isPrecompiled: true)) + .Returns(factoryResult) .Verifiable(); var viewEngine = new RazorViewEngine( @@ -1373,7 +1407,7 @@ public void GetPage_UsesGivenPath_WithAppRelativePath(string pageName) var page = Mock.Of(); pageFactory .Setup(p => p.CreateFactory(pageName)) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) + .Returns(GetPageFactoryResult(() => page)) .Verifiable(); var viewEngine = new TestableRazorViewEngine( pageFactory.Object, @@ -1401,7 +1435,7 @@ public void GetPage_ResolvesRelativeToCurrentPage_WithRelativePath(string pageNa var page = Mock.Of(); pageFactory .Setup(p => p.CreateFactory(expectedPageName)) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) + .Returns(GetPageFactoryResult(() => page)) .Verifiable(); var viewEngine = new TestableRazorViewEngine( pageFactory.Object, @@ -1429,7 +1463,7 @@ public void GetPage_ResolvesRelativeToAppRoot_WithRelativePath_IfNoPageExecuting var page = Mock.Of(); pageFactory .Setup(p => p.CreateFactory(expectedPageName)) - .Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0])) + .Returns(GetPageFactoryResult(() => page)) .Verifiable(); var viewEngine = new TestableRazorViewEngine( pageFactory.Object, @@ -1759,11 +1793,25 @@ private RazorViewEngine CreateSuccessfulViewEngine() var pageFactory = new Mock(MockBehavior.Strict); pageFactory .Setup(f => f.CreateFactory(It.IsAny())) - .Returns(new RazorPageFactoryResult(() => Mock.Of(), new IChangeToken[0])); + .Returns(GetPageFactoryResult(() => Mock.Of())); return CreateViewEngine(pageFactory.Object); } + private static RazorPageFactoryResult GetPageFactoryResult( + Func factory, + IList changeTokens = null, + string path = "/Views/Home/Index.cshtml") + { + var descriptor = new CompiledViewDescriptor + { + ExpirationTokens = changeTokens ?? Array.Empty(), + RelativePath = path, + }; + + return new RazorPageFactoryResult(descriptor, factory); + } + private TestableRazorViewEngine CreateViewEngine( IRazorPageFactoryProvider pageFactory = null, IEnumerable expanders = null, diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs index ce5dc500da..7655ac50ba 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; @@ -201,10 +202,11 @@ public async Task RenderAsync_AsPartial_ExecutesLayout_ButNotViewStartPages() v.Write("layout-content" + Environment.NewLine); v.RenderBodyPublic(); }); + var pageFactoryResult = new RazorPageFactoryResult(new CompiledViewDescriptor(), () => layout); var pageFactory = new Mock(); pageFactory .Setup(p => p.CreateFactory(LayoutPath)) - .Returns(new RazorPageFactoryResult(() => layout, new IChangeToken[0])); + .Returns(pageFactoryResult); var viewEngine = new Mock(MockBehavior.Strict); viewEngine diff --git a/test/WebSites/RazorWebSite/Controllers/BackSlashController.cs b/test/WebSites/RazorWebSite/Controllers/BackSlashController.cs new file mode 100644 index 0000000000..ca43a6f58d --- /dev/null +++ b/test/WebSites/RazorWebSite/Controllers/BackSlashController.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Mvc; + +namespace RazorWebSite.Controllers +{ + public class BackSlashController : Controller + { + public IActionResult Index() => View(@"Views\BackSlash\BackSlashView.cshtml"); + } +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/RazorWebSite.csproj b/test/WebSites/RazorWebSite/RazorWebSite.csproj index 6234f1c05f..d087f196fd 100644 --- a/test/WebSites/RazorWebSite/RazorWebSite.csproj +++ b/test/WebSites/RazorWebSite/RazorWebSite.csproj @@ -19,5 +19,6 @@ + diff --git a/test/WebSites/RazorWebSite/Services/BackSlashExpander.cs b/test/WebSites/RazorWebSite/Services/BackSlashExpander.cs new file mode 100644 index 0000000000..065d9a6413 --- /dev/null +++ b/test/WebSites/RazorWebSite/Services/BackSlashExpander.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace RazorWebSite +{ + public class ForwardSlashExpander : IViewLocationExpander + { + public void PopulateValues(ViewLocationExpanderContext context) + { + } + + public virtual IEnumerable ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable viewLocations) + { + if (context.ActionContext is ViewContext viewContext && (string)viewContext.ViewData["back-slash"] == "true") + { + return new[] { $@"Views\BackSlash\{context.ViewName}.cshtml" }; + + } + + return viewLocations; + } + } +} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Startup.cs b/test/WebSites/RazorWebSite/Startup.cs index a4c7f7b68e..4bb646bc5c 100644 --- a/test/WebSites/RazorWebSite/Startup.cs +++ b/test/WebSites/RazorWebSite/Startup.cs @@ -33,6 +33,7 @@ public void ConfigureServices(IServiceCollection services) $"{nameof(RazorWebSite)}.EmbeddedViews")); options.FileProviders.Add(updateableFileProvider); options.ViewLocationExpanders.Add(new NonMainPageViewLocationExpander()); + options.ViewLocationExpanders.Add(new ForwardSlashExpander()); }) .AddViewOptions(options => { @@ -51,6 +52,7 @@ public void ConfigureServices(IServiceCollection services) public void Configure(IApplicationBuilder app) { + app.UseDeveloperExceptionPage(); app.UseRequestLocalization(new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture("en-GB", "en-US"), diff --git a/test/WebSites/RazorWebSite/Views/BackSlash/BackSlashView.cshtml b/test/WebSites/RazorWebSite/Views/BackSlash/BackSlashView.cshtml new file mode 100644 index 0000000000..e28dd6e75f --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/BackSlash/BackSlashView.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["back-slash"] = "true"; + Layout = "_Layout"; +} +Page +@Html.Partial("_BackSlashPartial") \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/BackSlash/_BackSlashPartial.cshtml b/test/WebSites/RazorWebSite/Views/BackSlash/_BackSlashPartial.cshtml new file mode 100644 index 0000000000..8bfc4e8000 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/BackSlash/_BackSlashPartial.cshtml @@ -0,0 +1 @@ +Partial \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Views/BackSlash/_Layout.cshtml b/test/WebSites/RazorWebSite/Views/BackSlash/_Layout.cshtml new file mode 100644 index 0000000000..6bba921339 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/BackSlash/_Layout.cshtml @@ -0,0 +1,2 @@ +Layout +@RenderBody() \ No newline at end of file