Implement view search for pages

The View Engine now needs to know about pages :(. This isn't ideal but the
view engine needs to know what set of search paths to use. This was
already hardcoded for controllers vs controllers + areas. It felt right to
further hardcode instead of introduce a wierd abstraction that we only
use.

Additionally pages use a view location expander to implement an ascending
directory search.
This commit is contained in:
Ryan Nowak 2017-04-06 22:32:31 -07:00
parent c56b64fc41
commit a8eb5bee70
17 changed files with 547 additions and 14 deletions

View File

@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
viewName,
controllerName: null,
areaName: null,
pageName: null,
isMainPage: isMainPage,
values: null)
{
@ -35,18 +36,21 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
/// <param name="viewName">The view name.</param>
/// <param name="controllerName">The controller name.</param>
/// <param name="areaName">The area name.</param>
/// <param name="pageName">The page name.</param>
/// <param name="isMainPage">Determines if the page being found is the main page for an action.</param>
/// <param name="values">Values from <see cref="IViewLocationExpander"/> instances.</param>
public ViewLocationCacheKey(
string viewName,
string controllerName,
string areaName,
string pageName,
bool isMainPage,
IReadOnlyDictionary<string, string> values)
{
ViewName = viewName;
ControllerName = controllerName;
AreaName = areaName;
PageName = pageName;
IsMainPage = isMainPage;
ViewLocationExpanderValues = values;
}
@ -66,6 +70,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
/// </summary>
public string AreaName { get; }
/// <summary>
/// Gets the page name.
/// </summary>
public string PageName { get; }
/// <summary>
/// Determines if the page being found is the main page for an action.
/// </summary>
@ -82,7 +91,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
if (IsMainPage != y.IsMainPage ||
!string.Equals(ViewName, y.ViewName, StringComparison.Ordinal) ||
!string.Equals(ControllerName, y.ControllerName, StringComparison.Ordinal) ||
!string.Equals(AreaName, y.AreaName, StringComparison.Ordinal))
!string.Equals(AreaName, y.AreaName, StringComparison.Ordinal) ||
!string.Equals(PageName, y.PageName, StringComparison.Ordinal))
{
return false;
}
@ -131,6 +141,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
hashCodeCombiner.Add(ViewName, StringComparer.Ordinal);
hashCodeCombiner.Add(ControllerName, StringComparer.Ordinal);
hashCodeCombiner.Add(AreaName, StringComparer.Ordinal);
hashCodeCombiner.Add(PageName, StringComparer.Ordinal);
if (ViewLocationExpanderValues != null)
{

View File

@ -8,7 +8,6 @@ using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Razor.Language;
@ -33,8 +32,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor
public static readonly string ViewExtension = ".cshtml";
private const string ViewStartFileName = "_ViewStart.cshtml";
private const string ControllerKey = "controller";
private const string AreaKey = "area";
private const string ControllerKey = "controller";
private const string PageKey = "page";
private const string ParentDirectoryToken = "..";
private static readonly TimeSpan _cacheExpirationDuration = TimeSpan.FromMinutes(20);
private static readonly char[] _pathSeparators = new[] { '/', '\\' };
@ -272,11 +273,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor
{
var controllerName = GetNormalizedRouteValue(actionContext, ControllerKey);
var areaName = GetNormalizedRouteValue(actionContext, AreaKey);
var razorPageName = GetNormalizedRouteValue(actionContext, PageKey);
var expanderContext = new ViewLocationExpanderContext(
actionContext,
pageName,
controllerName,
areaName,
razorPageName,
isMainPage);
Dictionary<string, string> expanderValues = null;
@ -296,6 +299,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
expanderContext.ViewName,
expanderContext.ControllerName,
expanderContext.AreaName,
expanderContext.PageName,
expanderContext.IsMainPage,
expanderValues);
@ -396,14 +400,35 @@ namespace Microsoft.AspNetCore.Mvc.Razor
return builder.ToString();
}
// internal for tests
internal IEnumerable<string> GetViewLocationFormats(ViewLocationExpanderContext context)
{
if (!string.IsNullOrEmpty(context.AreaName) &&
!string.IsNullOrEmpty(context.ControllerName))
{
return _options.AreaViewLocationFormats;
}
else if (!string.IsNullOrEmpty(context.ControllerName))
{
return _options.ViewLocationFormats;
}
else if (!string.IsNullOrEmpty(context.PageName))
{
return _options.PageViewLocationFormats;
}
else
{
// If we don't match one of these conditions, we'll just treat it like regular controller/action
// and use those search paths. This is what we did in 1.0.0 without giving much thought to it.
return _options.ViewLocationFormats;
}
}
private ViewLocationCacheResult OnCacheMiss(
ViewLocationExpanderContext expanderContext,
ViewLocationCacheKey cacheKey)
{
// Only use the area view location formats if we have an area token.
IEnumerable<string> viewLocations = !string.IsNullOrEmpty(expanderContext.AreaName) ?
_options.AreaViewLocationFormats :
_options.ViewLocationFormats;
var viewLocations = GetViewLocationFormats(expanderContext);
for (var i = 0; i < _options.ViewLocationExpanders.Count; i++)
{

View File

@ -92,6 +92,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
/// </remarks>
public IList<string> AreaViewLocationFormats { get; } = new List<string>();
public IList<string> PageViewLocationFormats { get; } = new List<string>();
/// <summary>
/// Gets the <see cref="MetadataReference" /> instances that should be included in Razor compilation, along with
/// those discovered by <see cref="MetadataReferenceFeatureProvider" />s.

View File

@ -18,12 +18,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor
/// <param name="viewName">The view name.</param>
/// <param name="controllerName">The controller name.</param>
/// <param name="areaName">The area name.</param>
/// <param name="pageName">The page name.</param>
/// <param name="isMainPage">Determines if the page being found is the main page for an action.</param>
public ViewLocationExpanderContext(
ActionContext actionContext,
string viewName,
string controllerName,
string areaName,
string pageName,
bool isMainPage)
{
if (actionContext == null)
@ -40,9 +42,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor
ViewName = viewName;
ControllerName = controllerName;
AreaName = areaName;
PageName = pageName;
IsMainPage = isMainPage;
}
/// <summary>
/// Gets the <see cref="Mvc.ActionContext"/> for the current executing action.
/// </summary>
@ -58,6 +61,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor
/// </summary>
public string ControllerName { get; }
/// <summary>
/// Gets the page name. This will be the value of the <c>page</c> route value when rendering a Page from the
/// Razor Pages framework. This value will be <c>null</c> if rendering a view as the result of a controller.
/// </summary>
public string PageName { get; }
/// <summary>
/// Gets the area name.
/// </summary>

View File

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
@ -71,6 +72,8 @@ namespace Microsoft.Extensions.DependencyInjection
// Options
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<RazorPagesOptions>, RazorPagesOptionsSetup>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<RazorViewEngineOptions>, RazorPagesRazorViewEngineOptionsSetup>());
// Action description and invocation
services.TryAddEnumerable(

View File

@ -0,0 +1,69 @@
// 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;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
public class PageViewLocationExpander : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
if (string.IsNullOrEmpty(context.PageName))
{
// Not a page - just act natural.
return viewLocations;
}
return ExpandPageHierarchy();
IEnumerable<string> ExpandPageHierarchy()
{
foreach (var location in viewLocations)
{
// For pages, we only handle the 'page' token when it's surrounded by slashes.
//
// Explanation:
// We need the ability to 'collapse' the segment which requires us to understand slashes.
// Imagine a path like /{1}/{0} - we might end up with //{0} if we don't do *something* with
// the slashes. Instead of picking on (leading or trailing), we choose both. This seems
// less arbitrary.
//
//
// So given a Page like /Account/Manage/Index using /Pages as the root, and the default set of
// search paths, this will produce the expanded paths:
//
// /Pages/Account/Manage/{0}.cshtml
// /Pages/Account/{0}.cshtml
// /Pages/{0}.cshtml
// /Views/Shared/{0}.cshtml
if (!location.Contains("/{1}/"))
{
// If the location doesn't have the 'page' replacement token just return it as-is.
yield return location;
continue;
}
// For locations with the 'page' token - expand them into an ascending directory search,
// but only up to the pages root.
//
// This is easy because the 'page' token already trims the root directory.
var end = context.PageName.Length;
while (end > 0 && (end = context.PageName.LastIndexOf('/', end - 1)) != -1)
{
// PageName always starts with `/`
yield return location.Replace("/{1}/", context.PageName.Substring(0, end + 1));
}
}
}
}
public void PopulateValues(ViewLocationExpanderContext context)
{
// The value we care about - 'page' is already part of the system. We don't need to add it manually.
}
}
}

View File

@ -0,0 +1,38 @@
// 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.Diagnostics;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class RazorPagesRazorViewEngineOptionsSetup : IConfigureOptions<RazorViewEngineOptions>
{
private readonly IOptions<RazorPagesOptions> _pagesOptions;
public RazorPagesRazorViewEngineOptionsSetup(IOptions<RazorPagesOptions> pagesOptions)
{
_pagesOptions = 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
{
options.PageViewLocationFormats.Add(_pagesOptions.Value.RootDirectory + "/{1}/{0}" + RazorViewEngine.ViewExtension);
}
options.PageViewLocationFormats.Add("/Views/Shared/{0}" + RazorViewEngine.ViewExtension);
options.ViewLocationExpanders.Add(new PageViewLocationExpander());
}
}
}

View File

@ -0,0 +1,63 @@
// 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.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
public class RazorPagesViewSearchTest : IClassFixture<MvcTestFixture<RazorPagesWebSite.Startup>>
{
public RazorPagesViewSearchTest(MvcTestFixture<RazorPagesWebSite.Startup> fixture)
{
Client = fixture.Client;
}
public HttpClient Client { get; }
[Fact]
public async Task Page_CanFindPartial_InCurrentDirectory()
{
// Arrange & Act
var content = await Client.GetStringAsync("http://localhost/Pages/ViewSearch?partial=_Sibling");
// Assert
Assert.Equal("Hello from sibling", content.Trim());
}
[Fact]
public async Task Page_CanFindPartial_InParentDirectory()
{
// Arrange & Act
var content = await Client.GetStringAsync("http://localhost/Pages/ViewSearch?partial=_Parent");
// Assert
Assert.Equal("Hello from parent", content.Trim());
}
[Fact]
public async Task Page_CanFindPartial_InRootDirectory()
{
// Arrange & Act
var content = await Client.GetStringAsync("http://localhost/Pages/ViewSearch?partial=_Root");
// Assert
Assert.Equal("Hello from root", content.Trim());
}
[Fact]
public async Task Page_CanFindPartial_InViewsSharedDirectory()
{
// Arrange & Act
var content = await Client.GetStringAsync("http://localhost/Pages/ViewSearch?partial=_Shared");
// Assert
Assert.Equal("Hello from shared", content.Trim());
}
}
}

View File

@ -137,6 +137,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
"testView",
"test-controller",
"",
null,
false);
var languageViewLocationExpander = new LanguageViewLocationExpander(format);
viewLocationExpanderContext.Values = new Dictionary<string, string>();
@ -161,6 +162,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
"testView",
"test-controller",
"test-area",
null,
false);
var languageViewLocationExpander = new LanguageViewLocationExpander();
viewLocationExpanderContext.Values = new Dictionary<string, string>();
@ -188,6 +190,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
"testView",
"test-controller",
"test-area",
null,
false);
var languageViewLocationExpander = new LanguageViewLocationExpander();
viewLocationExpanderContext.Values = new Dictionary<string, string>();

View File

@ -37,6 +37,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
{"controller", "bar"},
};
private static readonly Dictionary<string, object> _pageTestContext = new Dictionary<string, object>()
{
{"page", "/Accounts/Index"},
};
public static IEnumerable<string[]> AbsoluteViewPathData
{
get
@ -781,6 +786,43 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
pageFactory.Verify();
}
[Fact]
public void FindView_CachesValuesIfViewWasFound_ForPages()
{
// Arrange
var page = Mock.Of<IRazorPage>();
var pageFactory = new Mock<IRazorPageFactoryProvider>();
pageFactory
.Setup(p => p.CreateFactory("/Views/Shared/baz.cshtml"))
.Returns(new RazorPageFactoryResult(() => page, new IChangeToken[0]))
.Verifiable();
var viewEngine = CreateViewEngine(pageFactory.Object);
var context = GetActionContext(_pageTestContext);
// Act 1
var result1 = viewEngine.FindView(context, "baz", isMainPage: false);
// Assert 1
Assert.True(result1.Success);
var view1 = Assert.IsType<RazorView>(result1.View);
Assert.Same(page, view1.RazorPage);
pageFactory.Verify();
// Act 2
pageFactory
.Setup(p => p.CreateFactory(It.IsAny<string>()))
.Throws(new Exception("Shouldn't be called"));
var result2 = viewEngine.FindView(context, "baz", isMainPage: false);
// Assert 2
Assert.True(result2.Success);
var view2 = Assert.IsType<RazorView>(result2.View);
Assert.Same(page, view2.RazorPage);
pageFactory.Verify();
}
[Fact]
public void FindView_InvokesPageFactoryIfChangeTokenExpired()
{
@ -1608,6 +1650,108 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
}
}
[Fact]
public void GetViewLocationFormats_ForControllerWithoutArea_ReturnsDefaultSet()
{
// Arrange
var expected = new string[] { "expected", };
var viewEngine = new TestableRazorViewEngine(
Mock.Of<IRazorPageFactoryProvider>(),
GetOptionsAccessor(viewLocationFormats: expected));
var context = new ViewLocationExpanderContext(
new ActionContext(),
"Index.cshtml",
controllerName: "Home",
areaName: null,
pageName: "ignored",
isMainPage: true);
// Act
var actual = viewEngine.GetViewLocationFormats(context);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void GetViewLocationFormats_ForControllerWithArea_ReturnsAreaSet()
{
// Arrange
var expected = new string[] { "expected", };
var viewEngine = new TestableRazorViewEngine(
Mock.Of<IRazorPageFactoryProvider>(),
GetOptionsAccessor(areaViewLocationFormats: expected));
var context = new ViewLocationExpanderContext(
new ActionContext(),
"Index.cshtml",
controllerName: "Home",
areaName: "Admin",
pageName: "ignored",
isMainPage: true);
// Act
var actual = viewEngine.GetViewLocationFormats(context);
// Assert
Assert.Equal(expected, actual);
}
[Fact]
public void GetViewLocationFormats_ForPage_ReturnsPageSet()
{
// Arrange
var expected = new string[] { "expected", };
var viewEngine = new TestableRazorViewEngine(
Mock.Of<IRazorPageFactoryProvider>(),
GetOptionsAccessor(pageViewLocationFormats: expected));
var context = new ViewLocationExpanderContext(
new ActionContext(),
"Index.cshtml",
controllerName: null,
areaName: null,
pageName: "/Some/Page",
isMainPage: true);
// Act
var actual = viewEngine.GetViewLocationFormats(context);
// Assert
Assert.Equal(expected, actual);
}
// This isn't a real case we expect to hit in an app, just making sure we have a reasonable default
// for a weird configuration. In this case we preserve what we did in 1.0.0.
[Fact]
public void GetViewLocationFormats_NoRouteValues_ReturnsDefaultSet()
{
// Arrange
var expected = new string[] { "expected", };
var viewEngine = new TestableRazorViewEngine(
Mock.Of<IRazorPageFactoryProvider>(),
GetOptionsAccessor(viewLocationFormats: expected));
var context = new ViewLocationExpanderContext(
new ActionContext(),
"Index.cshtml",
controllerName: null,
areaName: null,
pageName: null,
isMainPage: true);
// Act
var actual = viewEngine.GetViewLocationFormats(context);
// Assert
Assert.Equal(expected, actual);
}
// Return RazorViewEngine with a page factory provider that is always successful.
private RazorViewEngine CreateSuccessfulViewEngine()
{
@ -1635,14 +1779,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
private static IOptions<RazorViewEngineOptions> GetOptionsAccessor(
IEnumerable<IViewLocationExpander> expanders = null,
IEnumerable<string> viewLocationFormats = null,
IEnumerable<string> areaViewLocationFormats = null)
IEnumerable<string> areaViewLocationFormats = null,
IEnumerable<string> pageViewLocationFormats = null)
{
#pragma warning disable 0618
var optionsSetup = new RazorViewEngineOptionsSetup(Mock.Of<IHostingEnvironment>());
#pragma warning restore 0618
var options = new RazorViewEngineOptions();
optionsSetup.Configure(options);
options.PageViewLocationFormats.Add("/Views/Shared/{0}.cshtml");
if (expanders != null)
{
@ -1672,6 +1816,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
}
}
if (pageViewLocationFormats != null)
{
options.PageViewLocationFormats.Clear();
foreach (var location in pageViewLocationFormats)
{
options.PageViewLocationFormats.Add(location);
}
}
var optionsAccessor = new Mock<IOptions<RazorViewEngineOptions>>();
optionsAccessor
.SetupGet(v => v.Value)

View File

@ -0,0 +1,140 @@
// 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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Razor.Language;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
public class PageViewLocationExpanderTest
{
[Fact]
public void PopulateValues_DoesNothing()
{
// Arrange
var context = CreateContext();
var expander = new PageViewLocationExpander();
// Act
expander.PopulateValues(context);
// Assert
Assert.Empty(context.Values);
}
[Fact]
public void ExpandLocations_NoOp_ForNonPage()
{
// Arrange
var context = CreateContext(pageName: null);
var locations = new string[]
{
"/ignore-me",
};
var expander = new PageViewLocationExpander();
// Act
var actual = expander.ExpandViewLocations(context, locations);
// Assert
Assert.Equal(locations, actual);
}
[Fact]
public void ExpandLocations_NoOp_WhenLocationDoesNotContainPageToken()
{
// Arrange
var context = CreateContext(pageName: null);
var locations = new string[]
{
"/ignore-me",
};
var expander = new PageViewLocationExpander();
// Act
var actual = expander.ExpandViewLocations(context, locations);
// Assert
Assert.Equal(locations, actual);
}
[Theory]
[InlineData("/Index", new string[] { "/{0}.cshtml" })]
[InlineData("/Edit", new string[] { "/{0}.cshtml" })]
[InlineData("/Customers/Add", new string[] { "/Customers/{0}.cshtml", "/{0}.cshtml" })]
public void ExpandLocations_ExpandsDirectories_WhenLocationContainsPage(
string pageName,
string[] expected)
{
// Arrange
var context = CreateContext(pageName: pageName);
var locations = new string[]
{
"/{1}/{0}.cshtml",
};
var expander = new PageViewLocationExpander();
// Act
var actual = expander.ExpandViewLocations(context, locations);
// Assert
Assert.Equal(expected, actual.ToArray());
}
[Fact]
public void ExpandLocations_ExpandsDirectories_MultipleLocations()
{
// Arrange
var context = CreateContext(pageName: "/Customers/Edit");
var locations = new string[]
{
"/Pages/{1}/{0}.cshtml",
"/More/Paths/{1}/{0}.cshtml",
"/Views/Shared/{0}.cshtml",
};
var expected = new string[]
{
"/Pages/Customers/{0}.cshtml",
"/Pages/{0}.cshtml",
"/More/Paths/Customers/{0}.cshtml",
"/More/Paths/{0}.cshtml",
"/Views/Shared/{0}.cshtml",
};
var expander = new PageViewLocationExpander();
// Act
var actual = expander.ExpandViewLocations(context, locations);
// Assert
Assert.Equal(expected, actual.ToArray());
}
private ViewLocationExpanderContext CreateContext(string viewName = "_LoginPartial.cshtml", string pageName = null)
{
return new ViewLocationExpanderContext(
new ActionContext(),
viewName,
controllerName: null,
areaName: null,
pageName: pageName,
isMainPage: true)
{
Values = new Dictionary<string, string>(),
};
}
}
}

View File

@ -23,6 +23,7 @@ using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
using Microsoft.AspNetCore.Mvc.TagHelpers;
@ -358,10 +359,16 @@ namespace Microsoft.AspNetCore.Mvc
typeof(IConfigureOptions<RazorViewEngineOptions>),
new[]
{
#pragma warning disable 0618
typeof(RazorViewEngineOptionsSetup),
#pragma warning restore 0618
typeof(DependencyContextRazorViewEngineOptionsSetup)
typeof(DependencyContextRazorViewEngineOptionsSetup),
typeof(RazorPagesRazorViewEngineOptionsSetup),
}
},
{
typeof(IConfigureOptions<RazorPagesOptions>),
new[]
{
typeof(RazorPagesOptionsSetup),
}
},
{

View File

@ -0,0 +1,5 @@
@page
@{
await Html.RenderPartialAsync(Request.Query["partial"]);
}

View File

@ -0,0 +1 @@
Hello from sibling

View File

@ -0,0 +1 @@
Hello from parent

View File

@ -0,0 +1 @@
Hello from shared

View File

@ -0,0 +1 @@
Hello from root