diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngineOptions.cs b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngineOptions.cs index 6f5ed745bb..31813ee0ea 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngineOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngineOptions.cs @@ -113,6 +113,7 @@ public class RazorViewEngineOptions /// /Pages/Account/{0}.cshtml /// /Pages/{0}.cshtml /// /Views/Shared/{0}.cshtml + /// /Pages/Shared/{0}.cshtml /// /// public IList PageViewLocationFormats { get; } = new List(); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorPagesRazorViewEngineOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorPagesRazorViewEngineOptionsSetup.cs index d0dc97c670..fd73faba03 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorPagesRazorViewEngineOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorPagesRazorViewEngineOptionsSetup.cs @@ -1,6 +1,7 @@ // 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; using System.Diagnostics; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; @@ -10,29 +11,45 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { public class RazorPagesRazorViewEngineOptionsSetup : IConfigureOptions { - private readonly IOptions _pagesOptions; + private readonly RazorPagesOptions _pagesOptions; public RazorPagesRazorViewEngineOptionsSetup(IOptions pagesOptions) { - _pagesOptions = pagesOptions; + _pagesOptions = pagesOptions?.Value ?? throw new ArgumentNullException(nameof(pagesOptions)); } public void Configure(RazorViewEngineOptions options) { - Debug.Assert(_pagesOptions.Value.RootDirectory.Length > 0); - - if (_pagesOptions.Value.RootDirectory == "/") - { - options.PageViewLocationFormats.Add("/{1}/{0}" + RazorViewEngine.ViewExtension); - } - else + if (options == null) { - options.PageViewLocationFormats.Add(_pagesOptions.Value.RootDirectory + "/{1}/{0}" + RazorViewEngine.ViewExtension); + throw new ArgumentNullException(nameof(options)); } + var rootDirectory = _pagesOptions.RootDirectory; + Debug.Assert(!string.IsNullOrEmpty(rootDirectory)); + var defaultPageSearchPath = CombinePath(rootDirectory, "{1}/{0}"); + options.PageViewLocationFormats.Add(defaultPageSearchPath); + + // /Pages/Shared/{0}.cshtml + var pagesSharedDirectory = CombinePath(rootDirectory, "Shared/{0}"); + options.PageViewLocationFormats.Add(pagesSharedDirectory); + options.PageViewLocationFormats.Add("/Views/Shared/{0}" + RazorViewEngine.ViewExtension); + options.ViewLocationFormats.Add(pagesSharedDirectory); + options.AreaViewLocationFormats.Add(pagesSharedDirectory); + options.ViewLocationExpanders.Add(new PageViewLocationExpander()); } + + private static string CombinePath(string path1, string path2) + { + if (path1.EndsWith("/", StringComparison.Ordinal)) + { + return path1 + path2 + RazorViewEngine.ViewExtension; + } + + return path1 + "/" + path2 + RazorViewEngine.ViewExtension; + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs index 92bcfb86c9..cc17c5d784 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs @@ -274,5 +274,18 @@ public async Task PageRoute_UsingDefaultPageNameToRoute() // Assert Assert.Equal(expected, response.Trim()); } + + [Fact] + public async Task Pages_ReturnsFromPagesSharedDirectory() + { + // Arrange + var expected = "Hello from Pages/Shared"; + + // Act + var response = await Client.GetStringAsync("/SearchInPages"); + + // Assert + Assert.Equal(expected, response.Trim()); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ViewEngineTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ViewEngineTests.cs index 8b901b2977..8149c3bd12 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ViewEngineTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ViewEngineTests.cs @@ -513,5 +513,18 @@ public async Task ViewEngine_ResolvesPathsWithSlashesThatDoNotHaveExtensions() // Assert Assert.Equal(expected, responseContent.Trim()); } + + [Fact] + public async Task ViewEngine_DiscoversViewsFromPagesSharedDirectory() + { + // Arrange + var expected = "Hello from Pages/Shared"; + + // Act + var responseContent = await Client.GetStringAsync("/ViewEngine/SearchInPages"); + + // Assert + Assert.Equal(expected, responseContent.Trim()); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs index 11aedb7f04..e77430ee08 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/RazorViewEngineTest.cs @@ -1990,6 +1990,7 @@ private static IOptions GetOptionsAccessor( var options = new RazorViewEngineOptions(); optionsSetup.Configure(options); options.PageViewLocationFormats.Add("/Views/Shared/{0}.cshtml"); + options.PageViewLocationFormats.Add("/Pages/Shared/{0}.cshtml"); if (expanders != null) { diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorPagesRazorViewEngineOptionsSetupTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorPagesRazorViewEngineOptionsSetupTest.cs new file mode 100644 index 0000000000..35f36579b6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorPagesRazorViewEngineOptionsSetupTest.cs @@ -0,0 +1,142 @@ +// 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.Hosting; +using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class RazorPagesRazorViewEngineOptionsSetupTest + { + [Fact] + public void Configure_AddsPageViewLocationFormats_WhenPagesRootIsAppRoot() + { + // Arrange + var expected = new[] + { + "/{1}/{0}.cshtml", + "/Shared/{0}.cshtml", + "/Views/Shared/{0}.cshtml", + }; + + var razorPagesOptions = new RazorPagesOptions + { + RootDirectory = "/" + }; + var viewEngineOptions = GetViewEngineOptions(); + var setup = new RazorPagesRazorViewEngineOptionsSetup( + new TestOptionsManager(razorPagesOptions)); + + // Act + setup.Configure(viewEngineOptions); + + // Assert + Assert.Equal(expected, viewEngineOptions.PageViewLocationFormats); + } + + [Fact] + public void Configure_AddsPageViewLocationFormats_WithDefaultPagesRoot() + { + // Arrange + var expected = new[] + { + "/Pages/{1}/{0}.cshtml", + "/Pages/Shared/{0}.cshtml", + "/Views/Shared/{0}.cshtml", + }; + + var razorPagesOptions = new RazorPagesOptions(); + var viewEngineOptions = GetViewEngineOptions(); + var setup = new RazorPagesRazorViewEngineOptionsSetup( + new TestOptionsManager(razorPagesOptions)); + + // Act + setup.Configure(viewEngineOptions); + + // Assert + Assert.Equal(expected, viewEngineOptions.PageViewLocationFormats); + } + + [Fact] + public void Configure_AddsSharedPagesDirectoryToViewLocationFormats() + { + // Arrange + var expected = new[] + { + "/Views/{1}/{0}.cshtml", + "/Views/Shared/{0}.cshtml", + "/PagesRoot/Shared/{0}.cshtml", + }; + + var razorPagesOptions = new RazorPagesOptions + { + RootDirectory = "/PagesRoot", + }; + var viewEngineOptions = GetViewEngineOptions(); + var setup = new RazorPagesRazorViewEngineOptionsSetup( + new TestOptionsManager(razorPagesOptions)); + + // Act + setup.Configure(viewEngineOptions); + + // Assert + Assert.Equal(expected, viewEngineOptions.ViewLocationFormats); + } + + [Fact] + public void Configure_AddsSharedPagesDirectoryToAreaViewLocationFormats() + { + // Arrange + var expected = new[] + { + "/Areas/{2}/Views/{1}/{0}.cshtml", + "/Areas/{2}/Views/Shared/{0}.cshtml", + "/Views/Shared/{0}.cshtml", + "/PagesRoot/Shared/{0}.cshtml", + }; + + var razorPagesOptions = new RazorPagesOptions + { + RootDirectory = "/PagesRoot", + }; + var viewEngineOptions = GetViewEngineOptions(); + var setup = new RazorPagesRazorViewEngineOptionsSetup( + new TestOptionsManager(razorPagesOptions)); + + // Act + setup.Configure(viewEngineOptions); + + // Assert + Assert.Equal(expected, viewEngineOptions.AreaViewLocationFormats); + } + + [Fact] + public void Configure_RegistersPageViewLocationExpander() + { + // Arrange + var viewEngineOptions = GetViewEngineOptions(); + var setup = new RazorPagesRazorViewEngineOptionsSetup(new TestOptionsManager()); + + // Act + setup.Configure(viewEngineOptions); + + // Assert + Assert.Collection( + viewEngineOptions.ViewLocationExpanders, + expander => Assert.IsType(expander)); + } + + private static RazorViewEngineOptions GetViewEngineOptions() + { + var defaultSetup = new RazorViewEngineOptionsSetup(Mock.Of()); + var options = new RazorViewEngineOptions(); + defaultSetup.Configure(options); + + return options; + } + } +} diff --git a/test/WebSites/RazorPagesWebSite/Pages/FileFromShared b/test/WebSites/RazorPagesWebSite/Pages/FileFromShared new file mode 100644 index 0000000000..e9c9a4235a --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/FileFromShared @@ -0,0 +1 @@ +@Html.Partial("_FileInShared) \ No newline at end of file diff --git a/test/WebSites/RazorPagesWebSite/Pages/SearchInPages.cshtml b/test/WebSites/RazorPagesWebSite/Pages/SearchInPages.cshtml new file mode 100644 index 0000000000..9e0c846031 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/SearchInPages.cshtml @@ -0,0 +1,2 @@ +@page +@Html.Partial("_FileInShared") \ No newline at end of file diff --git a/test/WebSites/RazorPagesWebSite/Pages/Shared/_FileInShared.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Shared/_FileInShared.cshtml new file mode 100644 index 0000000000..cbc41653cb --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Shared/_FileInShared.cshtml @@ -0,0 +1 @@ +Hello from Pages/Shared \ No newline at end of file diff --git a/test/WebSites/RazorPagesWebSite/Views/Shared/_FileInShared.cshtml b/test/WebSites/RazorPagesWebSite/Views/Shared/_FileInShared.cshtml new file mode 100644 index 0000000000..0c110be233 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Views/Shared/_FileInShared.cshtml @@ -0,0 +1 @@ +@{ throw new Exception("This file should not be processed"); } \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs b/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs index 221a425cae..1a36f37741 100644 --- a/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs +++ b/test/WebSites/RazorWebSite/Controllers/ViewEngineController.cs @@ -77,5 +77,7 @@ public IActionResult ViewWithComponentThatHasViewStart() { return View(); } + + public IActionResult SearchInPages() => View(); } } \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Pages/Shared/_Layout.cshtml b/test/WebSites/RazorWebSite/Pages/Shared/_Layout.cshtml new file mode 100644 index 0000000000..0c110be233 --- /dev/null +++ b/test/WebSites/RazorWebSite/Pages/Shared/_Layout.cshtml @@ -0,0 +1 @@ +@{ throw new Exception("This file should not be processed"); } \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Pages/Shared/_SharedFromPages.cshtml b/test/WebSites/RazorWebSite/Pages/Shared/_SharedFromPages.cshtml new file mode 100644 index 0000000000..eebf127144 --- /dev/null +++ b/test/WebSites/RazorWebSite/Pages/Shared/_SharedFromPages.cshtml @@ -0,0 +1 @@ +Hello from Pages/Shared diff --git a/test/WebSites/RazorWebSite/Views/ViewEngine/SearchInPages.cshtml b/test/WebSites/RazorWebSite/Views/ViewEngine/SearchInPages.cshtml new file mode 100644 index 0000000000..3c2c59e986 --- /dev/null +++ b/test/WebSites/RazorWebSite/Views/ViewEngine/SearchInPages.cshtml @@ -0,0 +1 @@ +@Html.Partial("_SharedFromPages")