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 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal 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 @@ namespace Microsoft.AspNetCore.Mvc.Razor 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 de837275c9..da6ef0d766 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs @@ -396,11 +396,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor 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]); } } @@ -408,9 +409,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor { // 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); } @@ -427,17 +428,17 @@ namespace Microsoft.AspNetCore.Mvc.Razor 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]); } } @@ -479,22 +480,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor 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 @@ Partial that does not specify Layout 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 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal // 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 54cec8e7a7..632e01b489 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs @@ -8,6 +8,7 @@ using System.Threading; 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; @@ -169,15 +170,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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); @@ -205,11 +206,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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); @@ -334,15 +335,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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); @@ -370,7 +371,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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, @@ -398,7 +399,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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, @@ -427,7 +428,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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, @@ -453,7 +454,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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, @@ -481,7 +482,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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, @@ -509,7 +510,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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, @@ -537,7 +538,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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, @@ -565,10 +566,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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, @@ -629,10 +630,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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, @@ -693,7 +694,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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" }; @@ -746,6 +747,37 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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() { @@ -754,11 +786,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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); @@ -795,7 +827,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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); @@ -838,16 +870,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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); @@ -886,20 +918,20 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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); @@ -993,7 +1025,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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[] { @@ -1031,7 +1063,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test // 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); @@ -1131,7 +1163,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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(); @@ -1210,7 +1242,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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); @@ -1339,10 +1371,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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( @@ -1375,7 +1409,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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, @@ -1403,7 +1437,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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, @@ -1431,7 +1465,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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, @@ -1761,11 +1795,25 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test 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 a7ca3fc77e..cd5e8835a2 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewTest.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; 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; @@ -255,10 +256,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor 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/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs index aea39e4532..70b86ff7c6 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Razor.Internal; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.Rendering; @@ -181,10 +182,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal razorPageFactoryProvider .Setup(f => f.CreateFactory("/Home/Path1/_ViewStart.cshtml")) - .Returns(new RazorPageFactoryResult(factory1, new IChangeToken[0])); + .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), factory1)); razorPageFactoryProvider .Setup(f => f.CreateFactory("/_ViewStart.cshtml")) - .Returns(new RazorPageFactoryResult(factory2, new[] { Mock.Of() })); + .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), factory2)); var fileProvider = new TestFileProvider(); fileProvider.AddFile("/Home/Path1/_ViewStart.cshtml", "content1"); @@ -215,7 +216,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal Assert.Equal(new[] { factory2, factory1 }, entry.ViewStartFactories); } - [Fact] public void OnProvidersExecuting_CachesEntries() { @@ -347,19 +347,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var mock = new Mock(MockBehavior.Strict); mock .Setup(p => p.CreateFactory("/Pages/Level1/Level2/_ViewStart.cshtml")) - .Returns(new RazorPageFactoryResult(() => null, new List())) + .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null)) .Verifiable(); mock .Setup(p => p.CreateFactory("/Pages/Level1/_ViewStart.cshtml")) - .Returns(new RazorPageFactoryResult(() => null, new List())) + .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null)) .Verifiable(); mock .Setup(p => p.CreateFactory("/Pages/_ViewStart.cshtml")) - .Returns(new RazorPageFactoryResult(() => null, new List())) + .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null)) .Verifiable(); mock .Setup(p => p.CreateFactory("/_ViewStart.cshtml")) - .Returns(new RazorPageFactoryResult(() => null, new List())) + .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null)) .Verifiable(); var razorPageFactoryProvider = mock.Object; @@ -398,13 +398,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var pageFactory = new Mock(); pageFactory .Setup(f => f.CreateFactory("/Views/Deeper/_ViewStart.cshtml")) - .Returns(new RazorPageFactoryResult(() => null, new IChangeToken[0])); + .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null)); pageFactory .Setup(f => f.CreateFactory("/Views/_ViewStart.cshtml")) - .Returns(new RazorPageFactoryResult(new IChangeToken[0])); + .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), razorPageFactory: null)); pageFactory .Setup(f => f.CreateFactory("/_ViewStart.cshtml")) - .Returns(new RazorPageFactoryResult(() => null, new IChangeToken[0])); + .Returns(new RazorPageFactoryResult(new CompiledViewDescriptor(), () => null)); // No files var fileProvider = new TestFileProvider(); 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 @@ namespace RazorWebSite $"{nameof(RazorWebSite)}.EmbeddedViews")); options.FileProviders.Add(updateableFileProvider); options.ViewLocationExpanders.Add(new NonMainPageViewLocationExpander()); + options.ViewLocationExpanders.Add(new ForwardSlashExpander()); }) .AddViewOptions(options => { @@ -51,6 +52,7 @@ namespace RazorWebSite 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