parent
c8cabde1f1
commit
dfa085afaf
|
|
@ -98,8 +98,8 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
/// The value of <see cref="ActionName"/> is considered an implicit route value corresponding
|
/// The value of <see cref="ActionName"/> is considered an implicit route value corresponding
|
||||||
/// to the key <c>action</c> and the value of <see cref="ControllerModel.ControllerName"/> is
|
/// to the key <c>action</c> and the value of <see cref="ControllerModel.ControllerName"/> is
|
||||||
/// considered an implicit route value corresponding to the key <c>controller</c>. These entries
|
/// considered an implicit route value corresponding to the key <c>controller</c>. These entries
|
||||||
/// will be added to <see cref="ActionDescriptor.RouteValues"/>, but will not be visible in
|
/// will be implicitly added to <see cref="ActionDescriptor.RouteValues"/> when the action
|
||||||
/// <see cref="RouteValues"/>.
|
/// descriptor is created, but will not be visible in <see cref="RouteValues"/>.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Entries in <see cref="RouteValues"/> can override entries in
|
/// Entries in <see cref="RouteValues"/> can override entries in
|
||||||
|
|
|
||||||
|
|
@ -333,6 +333,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
{
|
{
|
||||||
return _options.ViewLocationFormats;
|
return _options.ViewLocationFormats;
|
||||||
}
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(context.AreaName) &&
|
||||||
|
!string.IsNullOrEmpty(context.PageName))
|
||||||
|
{
|
||||||
|
return _options.AreaPageViewLocationFormats;
|
||||||
|
}
|
||||||
else if (!string.IsNullOrEmpty(context.PageName))
|
else if (!string.IsNullOrEmpty(context.PageName))
|
||||||
{
|
{
|
||||||
return _options.PageViewLocationFormats;
|
return _options.PageViewLocationFormats;
|
||||||
|
|
|
||||||
|
|
@ -112,12 +112,47 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
/// /Pages/Account/Manage/{0}.cshtml
|
/// /Pages/Account/Manage/{0}.cshtml
|
||||||
/// /Pages/Account/{0}.cshtml
|
/// /Pages/Account/{0}.cshtml
|
||||||
/// /Pages/{0}.cshtml
|
/// /Pages/{0}.cshtml
|
||||||
/// /Views/Shared/{0}.cshtml
|
|
||||||
/// /Pages/Shared/{0}.cshtml
|
/// /Pages/Shared/{0}.cshtml
|
||||||
|
/// /Views/Shared/{0}.cshtml
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public IList<string> PageViewLocationFormats { get; } = new List<string>();
|
public IList<string> PageViewLocationFormats { get; } = new List<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the locations where <see cref="RazorViewEngine"/> will search for views (such as layouts and partials)
|
||||||
|
/// when searched from the context of rendering a Razor Page within an area.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Locations are format strings (see https://msdn.microsoft.com/en-us/library/txafckwd.aspx) which may contain
|
||||||
|
/// the following format items:
|
||||||
|
/// </para>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>
|
||||||
|
/// <description>{0} - View Name</description>
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// <description>{1} - Page Name</description>
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// <description>{2} - Area Name</description>
|
||||||
|
/// </item>
|
||||||
|
/// </list>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="AreaPageViewLocationFormats"/> work in tandem with a view location expander to perform hierarchical
|
||||||
|
/// path lookups. For instance, given a Page like /Areas/Account/Pages/Manage/User.cshtml using /Areas as the area pages root and
|
||||||
|
/// /Pages as the root, the view engine will search for views in the following locations:
|
||||||
|
///
|
||||||
|
/// /Areas/Accounts/Pages/Manage/{0}.cshtml
|
||||||
|
/// /Areas/Accounts/Pages/{0}.cshtml
|
||||||
|
/// /Areas/Accounts/Pages/Shared/{0}.cshtml
|
||||||
|
/// /Areas/Accounts/Views/Shared/{0}.cshtml
|
||||||
|
/// /Pages/Shared/{0}.cshtml
|
||||||
|
/// /Views/Shared/{0}.cshtml
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public IList<string> AreaPageViewLocationFormats { get; } = new List<string>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the <see cref="MetadataReference" /> instances that should be included in Razor compilation, along with
|
/// Gets the <see cref="MetadataReference" /> instances that should be included in Razor compilation, along with
|
||||||
/// those discovered by <see cref="MetadataReferenceFeatureProvider" />s.
|
/// those discovered by <see cref="MetadataReferenceFeatureProvider" />s.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
{
|
{
|
||||||
|
|
@ -19,21 +22,12 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
/// <param name="viewEnginePath">The path relative to the base path for page discovery.</param>
|
/// <param name="viewEnginePath">The path relative to the base path for page discovery.</param>
|
||||||
public PageRouteModel(string relativePath, string viewEnginePath)
|
public PageRouteModel(string relativePath, string viewEnginePath)
|
||||||
{
|
{
|
||||||
if (relativePath == null)
|
RelativePath = relativePath ?? throw new ArgumentNullException(nameof(relativePath));
|
||||||
{
|
ViewEnginePath = viewEnginePath ?? throw new ArgumentNullException(nameof(viewEnginePath));
|
||||||
throw new ArgumentNullException(nameof(relativePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewEnginePath == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(viewEnginePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
RelativePath = relativePath;
|
|
||||||
ViewEnginePath = viewEnginePath;
|
|
||||||
|
|
||||||
Properties = new Dictionary<object, object>();
|
Properties = new Dictionary<object, object>();
|
||||||
Selectors = new List<SelectorModel>();
|
Selectors = new List<SelectorModel>();
|
||||||
|
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -52,6 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
|
|
||||||
Properties = new Dictionary<object, object>(other.Properties);
|
Properties = new Dictionary<object, object>(other.Properties);
|
||||||
Selectors = new List<SelectorModel>(other.Selectors.Select(m => new SelectorModel(m)));
|
Selectors = new List<SelectorModel>(other.Selectors.Select(m => new SelectorModel(m)));
|
||||||
|
RouteValues = new Dictionary<string, string>(other.RouteValues, StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -62,6 +57,9 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the path relative to the base path for page discovery.
|
/// Gets the path relative to the base path for page discovery.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// For area pages, this path is calculated relative to the <see cref="RazorPagesOptions.RootDirectory"/> of the specific area.
|
||||||
|
/// </remarks>
|
||||||
public string ViewEnginePath { get; }
|
public string ViewEnginePath { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -73,5 +71,18 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
/// Gets the <see cref="SelectorModel"/> instances.
|
/// Gets the <see cref="SelectorModel"/> instances.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<SelectorModel> Selectors { get; }
|
public IList<SelectorModel> Selectors { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a collection of route values that must be present in the <see cref="RouteData.Values"/>
|
||||||
|
/// for the corresponding page to be selected.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The value of <see cref="ViewEnginePath"/> is considered an implicit route value corresponding
|
||||||
|
/// to the key <c>page</c>. These entries will be implicitly added to <see cref="ActionDescriptor.RouteValues"/>
|
||||||
|
/// when the action descriptor is created, but will not be visible in <see cref="RouteValues"/>.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public IDictionary<string, string> RouteValues { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
|
|
||||||
foreach (var selector in model.Selectors)
|
foreach (var selector in model.Selectors)
|
||||||
{
|
{
|
||||||
actions.Add(new PageActionDescriptor()
|
var descriptor = new PageActionDescriptor()
|
||||||
{
|
{
|
||||||
AttributeRouteInfo = new AttributeRouteInfo()
|
AttributeRouteInfo = new AttributeRouteInfo()
|
||||||
{
|
{
|
||||||
|
|
@ -87,12 +87,23 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
FilterDescriptors = Array.Empty<FilterDescriptor>(),
|
FilterDescriptors = Array.Empty<FilterDescriptor>(),
|
||||||
Properties = new Dictionary<object, object>(model.Properties),
|
Properties = new Dictionary<object, object>(model.Properties),
|
||||||
RelativePath = model.RelativePath,
|
RelativePath = model.RelativePath,
|
||||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
{ "page", model.ViewEnginePath },
|
|
||||||
},
|
|
||||||
ViewEnginePath = model.ViewEnginePath,
|
ViewEnginePath = model.ViewEnginePath,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
foreach (var kvp in model.RouteValues)
|
||||||
|
{
|
||||||
|
if (!descriptor.RouteValues.ContainsKey(kvp.Key))
|
||||||
|
{
|
||||||
|
descriptor.RouteValues.Add(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!descriptor.RouteValues.ContainsKey("page"))
|
||||||
|
{
|
||||||
|
descriptor.RouteValues.Add("page", model.ViewEnginePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.Add(descriptor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Razor;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
|
|
@ -17,14 +19,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
private readonly object _cacheLock = new object();
|
private readonly object _cacheLock = new object();
|
||||||
private readonly ApplicationPartManager _applicationManager;
|
private readonly ApplicationPartManager _applicationManager;
|
||||||
private readonly RazorPagesOptions _pagesOptions;
|
private readonly RazorPagesOptions _pagesOptions;
|
||||||
|
private readonly ILogger<CompiledPageRouteModelProvider> _logger;
|
||||||
private List<PageRouteModel> _cachedModels;
|
private List<PageRouteModel> _cachedModels;
|
||||||
|
|
||||||
public CompiledPageRouteModelProvider(
|
public CompiledPageRouteModelProvider(
|
||||||
ApplicationPartManager applicationManager,
|
ApplicationPartManager applicationManager,
|
||||||
IOptions<RazorPagesOptions> pagesOptionsAccessor)
|
IOptions<RazorPagesOptions> pagesOptionsAccessor,
|
||||||
|
ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
_applicationManager = applicationManager;
|
_applicationManager = applicationManager;
|
||||||
_pagesOptions = pagesOptionsAccessor.Value;
|
_pagesOptions = pagesOptionsAccessor.Value;
|
||||||
|
_logger = loggerFactory.CreateLogger<CompiledPageRouteModelProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Order => -1000;
|
public int Order => -1000;
|
||||||
|
|
@ -58,26 +63,69 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
rootDirectory = rootDirectory + "/";
|
rootDirectory = rootDirectory + "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var areaRootDirectory = _pagesOptions.AreaRootDirectory;
|
||||||
|
if (!areaRootDirectory.EndsWith("/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
areaRootDirectory = areaRootDirectory + "/";
|
||||||
|
}
|
||||||
|
|
||||||
var cachedApplicationModels = new List<PageRouteModel>();
|
var cachedApplicationModels = new List<PageRouteModel>();
|
||||||
foreach (var viewDescriptor in GetViewDescriptors(_applicationManager))
|
foreach (var viewDescriptor in GetViewDescriptors(_applicationManager))
|
||||||
{
|
{
|
||||||
if (!viewDescriptor.RelativePath.StartsWith(rootDirectory, StringComparison.OrdinalIgnoreCase))
|
PageRouteModel model = null;
|
||||||
|
if (viewDescriptor.RelativePath.StartsWith(rootDirectory, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
continue;
|
model = GetPageRouteModel(rootDirectory, viewDescriptor);
|
||||||
|
}
|
||||||
|
else if (_pagesOptions.EnableAreas && viewDescriptor.RelativePath.StartsWith(areaRootDirectory, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
model = GetAreaPageRouteModel(areaRootDirectory, viewDescriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
var viewEnginePath = GetViewEnginePath(rootDirectory, viewDescriptor.RelativePath);
|
if (model != null)
|
||||||
var model = new PageRouteModel(viewDescriptor.RelativePath, viewEnginePath);
|
{
|
||||||
var pageAttribute = (RazorPageAttribute)viewDescriptor.ViewAttribute;
|
cachedApplicationModels.Add(model);
|
||||||
PageSelectorModel.PopulateDefaults(model, pageAttribute.RouteTemplate);
|
}
|
||||||
|
|
||||||
cachedApplicationModels.Add(model);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_cachedModels = cachedApplicationModels;
|
_cachedModels = cachedApplicationModels;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private PageRouteModel GetPageRouteModel(string rootDirectory, CompiledViewDescriptor viewDescriptor)
|
||||||
|
{
|
||||||
|
var viewEnginePath = GetRootTrimmedPath(rootDirectory, viewDescriptor.RelativePath);
|
||||||
|
if (viewEnginePath.EndsWith(RazorViewEngine.ViewExtension, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
viewEnginePath = viewEnginePath.Substring(0, viewEnginePath.Length - RazorViewEngine.ViewExtension.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
var model = new PageRouteModel(viewDescriptor.RelativePath, viewEnginePath);
|
||||||
|
var pageAttribute = (RazorPageAttribute)viewDescriptor.ViewAttribute;
|
||||||
|
PageSelectorModel.PopulateDefaults(model, viewEnginePath, pageAttribute.RouteTemplate);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PageRouteModel GetAreaPageRouteModel(string areaRootDirectory, CompiledViewDescriptor viewDescriptor)
|
||||||
|
{
|
||||||
|
var rootTrimmedPath = GetRootTrimmedPath(areaRootDirectory, viewDescriptor.RelativePath);
|
||||||
|
|
||||||
|
if (PageSelectorModel.TryParseAreaPath(_pagesOptions, rootTrimmedPath, _logger, out var result))
|
||||||
|
{
|
||||||
|
var model = new PageRouteModel(viewDescriptor.RelativePath, result.viewEnginePath)
|
||||||
|
{
|
||||||
|
RouteValues = { ["area"] = result.areaName },
|
||||||
|
};
|
||||||
|
var pageAttribute = (RazorPageAttribute)viewDescriptor.ViewAttribute;
|
||||||
|
PageSelectorModel.PopulateDefaults(model, result.pageRoute, pageAttribute.RouteTemplate);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We were unable to parse the path to match the format we expect /Areas/AreaName/Pages/PagePath.cshtml
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the sequence of <see cref="CompiledViewDescriptor"/> from <paramref name="applicationManager"/>.
|
/// Gets the sequence of <see cref="CompiledViewDescriptor"/> from <paramref name="applicationManager"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -96,20 +144,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
return viewsFeature.ViewDescriptors.Where(d => d.IsPrecompiled && d.ViewAttribute is RazorPageAttribute);
|
return viewsFeature.ViewDescriptors.Where(d => d.IsPrecompiled && d.ViewAttribute is RazorPageAttribute);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetViewEnginePath(string rootDirectory, string path)
|
private string GetRootTrimmedPath(string rootDirectory, string path)
|
||||||
{
|
{
|
||||||
var endIndex = path.LastIndexOf('.');
|
|
||||||
if (endIndex == -1)
|
|
||||||
{
|
|
||||||
endIndex = path.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// rootDirectory = "/Pages/AllMyPages/"
|
// rootDirectory = "/Pages/AllMyPages/"
|
||||||
// path = "/Pages/AllMyPages/Home.cshtml"
|
// path = "/Pages/AllMyPages/Home.cshtml"
|
||||||
// Result = "/Home"
|
// Result = "/Home.cshtml"
|
||||||
var startIndex = rootDirectory.Length - 1;
|
var startIndex = rootDirectory.Length - 1;
|
||||||
|
return path.Substring(startIndex);
|
||||||
return path.Substring(startIndex, endIndex - startIndex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
|
|
@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
public class PageActionDescriptorChangeProvider : IActionDescriptorChangeProvider
|
public class PageActionDescriptorChangeProvider : IActionDescriptorChangeProvider
|
||||||
{
|
{
|
||||||
private readonly IFileProvider _fileProvider;
|
private readonly IFileProvider _fileProvider;
|
||||||
private readonly string _searchPattern;
|
private readonly string[] _searchPatterns;
|
||||||
private readonly string[] _additionalFilesToTrack;
|
private readonly string[] _additionalFilesToTrack;
|
||||||
|
|
||||||
public PageActionDescriptorChangeProvider(
|
public PageActionDescriptorChangeProvider(
|
||||||
|
|
@ -45,29 +46,61 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
Debug.Assert(!string.IsNullOrEmpty(rootDirectory));
|
Debug.Assert(!string.IsNullOrEmpty(rootDirectory));
|
||||||
rootDirectory = rootDirectory.TrimEnd('/');
|
rootDirectory = rootDirectory.TrimEnd('/');
|
||||||
|
|
||||||
var importFileAtPagesRoot = rootDirectory + "/" + templateEngine.Options.ImportsFileName;
|
// Search pattern that matches all cshtml files under the Pages RootDirectory
|
||||||
_additionalFilesToTrack = templateEngine.GetImportItems(importFileAtPagesRoot)
|
var pagesRootSearchPattern = rootDirectory + "/**/*.cshtml";
|
||||||
.Select(item => item.FilePath)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
_searchPattern = rootDirectory + "/**/*.cshtml";
|
// pagesRootSearchPattern will miss _ViewImports outside the RootDirectory despite these influencing
|
||||||
|
// compilation. e.g. when RootDirectory = /Dir1/Dir2, the search pattern will ignore changes to
|
||||||
|
// [/_ViewImports.cshtml, /Dir1/_ViewImports.cshtml]. We need to additionally account for these.
|
||||||
|
var importFileAtPagesRoot = rootDirectory + "/" + templateEngine.Options.ImportsFileName;
|
||||||
|
var additionalImportFilePaths = templateEngine.GetImportItems(importFileAtPagesRoot)
|
||||||
|
.Select(item => item.FilePath);
|
||||||
|
|
||||||
|
if (razorPagesOptions.Value.EnableAreas)
|
||||||
|
{
|
||||||
|
var areaRootDirectory = razorPagesOptions.Value.AreaRootDirectory;
|
||||||
|
Debug.Assert(!string.IsNullOrEmpty(areaRootDirectory));
|
||||||
|
areaRootDirectory = areaRootDirectory.TrimEnd('/');
|
||||||
|
|
||||||
|
// Search pattern that matches all cshtml files under the Pages AreaRootDirectory
|
||||||
|
var areaRootSearchPattern = areaRootDirectory + "/**/*.cshtml";
|
||||||
|
|
||||||
|
var importFileAtAreaPagesRoot = areaRootDirectory + "/" + templateEngine.Options.ImportsFileName;
|
||||||
|
var importPathsOutsideAreaPagesRoot = templateEngine.GetImportItems(importFileAtAreaPagesRoot)
|
||||||
|
.Select(item => item.FilePath);
|
||||||
|
|
||||||
|
additionalImportFilePaths = additionalImportFilePaths
|
||||||
|
.Concat(importPathsOutsideAreaPagesRoot)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
_searchPatterns = new[]
|
||||||
|
{
|
||||||
|
pagesRootSearchPattern,
|
||||||
|
areaRootSearchPattern
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_searchPatterns = new[] { pagesRootSearchPattern, };
|
||||||
|
}
|
||||||
|
|
||||||
|
_additionalFilesToTrack = additionalImportFilePaths.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IChangeToken GetChangeToken()
|
public IChangeToken GetChangeToken()
|
||||||
{
|
{
|
||||||
var wildcardChangeToken = _fileProvider.Watch(_searchPattern);
|
var changeTokens = new IChangeToken[_additionalFilesToTrack.Length + _searchPatterns.Length];
|
||||||
if (_additionalFilesToTrack.Length == 0)
|
|
||||||
{
|
|
||||||
return wildcardChangeToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
var changeTokens = new IChangeToken[_additionalFilesToTrack.Length + 1];
|
|
||||||
for (var i = 0; i < _additionalFilesToTrack.Length; i++)
|
for (var i = 0; i < _additionalFilesToTrack.Length; i++)
|
||||||
{
|
{
|
||||||
changeTokens[i] = _fileProvider.Watch(_additionalFilesToTrack[i]);
|
changeTokens[i] = _fileProvider.Watch(_additionalFilesToTrack[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeTokens[changeTokens.Length - 1] = wildcardChangeToken;
|
for (var i = 0; i < _searchPatterns.Length; i++)
|
||||||
|
{
|
||||||
|
var wildcardChangeToken = _fileProvider.Watch(_searchPatterns[i]);
|
||||||
|
changeTokens[_additionalFilesToTrack.Length + i] = wildcardChangeToken;
|
||||||
|
}
|
||||||
|
|
||||||
return new CompositeChangeToken(changeTokens);
|
return new CompositeChangeToken(changeTokens);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
private static readonly Action<ILogger, string, string, Exception> _handlerMethodExecuted;
|
private static readonly Action<ILogger, string, string, Exception> _handlerMethodExecuted;
|
||||||
private static readonly Action<ILogger, object, Exception> _pageFilterShortCircuit;
|
private static readonly Action<ILogger, object, Exception> _pageFilterShortCircuit;
|
||||||
private static readonly Action<ILogger, string, string[], Exception> _malformedPageDirective;
|
private static readonly Action<ILogger, string, string[], Exception> _malformedPageDirective;
|
||||||
|
private static readonly Action<ILogger, string, string, string, string, Exception> _unsupportedAreaPath;
|
||||||
private static readonly Action<ILogger, Type, Exception> _notMostEffectiveFilter;
|
private static readonly Action<ILogger, Type, Exception> _notMostEffectiveFilter;
|
||||||
private static readonly Action<ILogger, string, string, string, Exception> _beforeExecutingMethodOnFilter;
|
private static readonly Action<ILogger, string, string, string, Exception> _beforeExecutingMethodOnFilter;
|
||||||
private static readonly Action<ILogger, string, string, string, Exception> _afterExecutingMethodOnFilter;
|
private static readonly Action<ILogger, string, string, string, Exception> _afterExecutingMethodOnFilter;
|
||||||
|
|
@ -61,6 +62,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
LogLevel.Trace,
|
LogLevel.Trace,
|
||||||
2,
|
2,
|
||||||
"{FilterType}: After executing {Method} on filter {Filter}.");
|
"{FilterType}: After executing {Method} on filter {Filter}.");
|
||||||
|
|
||||||
|
_unsupportedAreaPath = LoggerMessage.Define<string, string, string, string>(
|
||||||
|
LogLevel.Warning,
|
||||||
|
1,
|
||||||
|
"The page at '{FilePath}' is located under the area root directory '{AreaRootDirectory}' but does not follow the path format '{AreaRootDirectory}{RootDirectory}/Directory/FileName.cshtml");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void ExecutingHandlerMethod(this ILogger logger, PageContext context, HandlerMethodDescriptor handler, object[] arguments)
|
public static void ExecutingHandlerMethod(this ILogger logger, PageContext context, HandlerMethodDescriptor handler, object[] arguments)
|
||||||
|
|
@ -133,5 +139,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
{
|
{
|
||||||
_notMostEffectiveFilter(logger, policyType, null);
|
_notMostEffectiveFilter(logger, policyType, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void UnsupportedAreaPath(this ILogger logger, RazorPagesOptions options, string filePath)
|
||||||
|
{
|
||||||
|
if (logger.IsEnabled(LogLevel.Warning))
|
||||||
|
{
|
||||||
|
_unsupportedAreaPath(logger, filePath, options.AreaRootDirectory, options.AreaRootDirectory, options.RootDirectory, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,23 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Razor;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
{
|
{
|
||||||
public static class PageSelectorModel
|
public static class PageSelectorModel
|
||||||
{
|
{
|
||||||
private const string IndexFileName = "Index.cshtml";
|
private static readonly string IndexFileName = "Index" + RazorViewEngine.ViewExtension;
|
||||||
|
|
||||||
public static void PopulateDefaults(PageRouteModel model, string routeTemplate)
|
public static void PopulateDefaults(PageRouteModel model, string pageRoute, string routeTemplate)
|
||||||
{
|
{
|
||||||
|
model.RouteValues.Add("page", model.ViewEnginePath);
|
||||||
|
|
||||||
if (AttributeRouteModel.IsOverridePattern(routeTemplate))
|
if (AttributeRouteModel.IsOverridePattern(routeTemplate))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(string.Format(
|
throw new InvalidOperationException(string.Format(
|
||||||
|
|
@ -20,7 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
model.RelativePath));
|
model.RelativePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectorModel = CreateSelectorModel(model.ViewEnginePath, routeTemplate);
|
var selectorModel = CreateSelectorModel(pageRoute, routeTemplate);
|
||||||
model.Selectors.Add(selectorModel);
|
model.Selectors.Add(selectorModel);
|
||||||
|
|
||||||
var fileName = Path.GetFileName(model.RelativePath);
|
var fileName = Path.GetFileName(model.RelativePath);
|
||||||
|
|
@ -30,20 +36,62 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
// force outgoing routes to match to the path sans /Index.
|
// force outgoing routes to match to the path sans /Index.
|
||||||
selectorModel.AttributeRouteModel.SuppressLinkGeneration = true;
|
selectorModel.AttributeRouteModel.SuppressLinkGeneration = true;
|
||||||
|
|
||||||
var parentDirectoryPath = model.ViewEnginePath;
|
var index = pageRoute.LastIndexOf('/');
|
||||||
var index = parentDirectoryPath.LastIndexOf('/');
|
var parentDirectoryPath = index == -1 ?
|
||||||
if (index == -1)
|
string.Empty :
|
||||||
{
|
pageRoute.Substring(0, index);
|
||||||
parentDirectoryPath = string.Empty;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
parentDirectoryPath = parentDirectoryPath.Substring(0, index);
|
|
||||||
}
|
|
||||||
model.Selectors.Add(CreateSelectorModel(parentDirectoryPath, routeTemplate));
|
model.Selectors.Add(CreateSelectorModel(parentDirectoryPath, routeTemplate));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryParseAreaPath(
|
||||||
|
RazorPagesOptions razorPagesOptions,
|
||||||
|
string path,
|
||||||
|
ILogger logger,
|
||||||
|
out (string areaName, string viewEnginePath, string pageRoute) result)
|
||||||
|
{
|
||||||
|
// path = "/Products/Pages/Manage/Home.cshtml"
|
||||||
|
// Result = ("Products", "/Manage/Home", "/Products/Manage/Home")
|
||||||
|
|
||||||
|
result = default;
|
||||||
|
Debug.Assert(path.StartsWith("/", StringComparison.Ordinal));
|
||||||
|
|
||||||
|
var areaEndIndex = path.IndexOf('/', startIndex: 1);
|
||||||
|
if (areaEndIndex == -1 || areaEndIndex == path.Length)
|
||||||
|
{
|
||||||
|
logger.UnsupportedAreaPath(razorPagesOptions, path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize the pages root directory so that it has a
|
||||||
|
var normalizedPagesRootDirectory = razorPagesOptions.RootDirectory.TrimStart('/');
|
||||||
|
if (!normalizedPagesRootDirectory.EndsWith("/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
normalizedPagesRootDirectory += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Compare(path, areaEndIndex + 1, normalizedPagesRootDirectory, 0, normalizedPagesRootDirectory.Length, StringComparison.OrdinalIgnoreCase) != 0)
|
||||||
|
{
|
||||||
|
logger.UnsupportedAreaPath(razorPagesOptions, path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var areaName = path.Substring(1, areaEndIndex - 1);
|
||||||
|
|
||||||
|
var pagePathIndex = areaEndIndex + normalizedPagesRootDirectory.Length;
|
||||||
|
Debug.Assert(path.EndsWith(RazorViewEngine.ViewExtension), $"{path} does not end in extension '{RazorViewEngine.ViewExtension}'.");
|
||||||
|
|
||||||
|
var pageName = path.Substring(pagePathIndex, path.Length - pagePathIndex - RazorViewEngine.ViewExtension.Length);
|
||||||
|
|
||||||
|
var builder = new InplaceStringBuilder(areaEndIndex + pageName.Length);
|
||||||
|
builder.Append(path, 0, areaEndIndex);
|
||||||
|
builder.Append(pageName);
|
||||||
|
var pageRoute = builder.ToString();
|
||||||
|
|
||||||
|
result = (areaName, pageName, pageRoute);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static SelectorModel CreateSelectorModel(string prefix, string template)
|
private static SelectorModel CreateSelectorModel(string prefix, string template)
|
||||||
{
|
{
|
||||||
return new SelectorModel
|
return new SelectorModel
|
||||||
|
|
|
||||||
|
|
@ -27,15 +27,37 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
|
|
||||||
var rootDirectory = _pagesOptions.RootDirectory;
|
var rootDirectory = _pagesOptions.RootDirectory;
|
||||||
Debug.Assert(!string.IsNullOrEmpty(rootDirectory));
|
Debug.Assert(!string.IsNullOrEmpty(rootDirectory));
|
||||||
var defaultPageSearchPath = CombinePath(rootDirectory, "{1}/{0}");
|
var defaultPageSearchPath = CombinePath(rootDirectory, "{1}/{0}" + RazorViewEngine.ViewExtension);
|
||||||
options.PageViewLocationFormats.Add(defaultPageSearchPath);
|
options.PageViewLocationFormats.Add(defaultPageSearchPath);
|
||||||
|
|
||||||
// /Pages/Shared/{0}.cshtml
|
// /Pages/Shared/{0}.cshtml
|
||||||
var pagesSharedDirectory = CombinePath(rootDirectory, "Shared/{0}");
|
var pagesSharedDirectory = CombinePath(rootDirectory, "Shared/{0}" + RazorViewEngine.ViewExtension);
|
||||||
options.PageViewLocationFormats.Add(pagesSharedDirectory);
|
options.PageViewLocationFormats.Add(pagesSharedDirectory);
|
||||||
|
|
||||||
options.PageViewLocationFormats.Add("/Views/Shared/{0}" + RazorViewEngine.ViewExtension);
|
options.PageViewLocationFormats.Add("/Views/Shared/{0}" + RazorViewEngine.ViewExtension);
|
||||||
|
|
||||||
|
Debug.Assert(!string.IsNullOrEmpty(_pagesOptions.AreaRootDirectory));
|
||||||
|
var areaDirectory = CombinePath(_pagesOptions.AreaRootDirectory, "{2}");
|
||||||
|
// Areas/{2}/Pages/
|
||||||
|
var areaPagesDirectory = CombinePath(areaDirectory, rootDirectory);
|
||||||
|
|
||||||
|
// Areas/{2}/Pages/{1}/{0}.cshtml
|
||||||
|
// Areas/{2}/Pages/Shared/{0}.cshtml
|
||||||
|
// Areas/{2}/Views/Shared/{0}.cshtml
|
||||||
|
// Pages/Shared/{0}.cshtml
|
||||||
|
// Views/Shared/{0}.cshtml
|
||||||
|
var areaSearchPath = CombinePath(areaPagesDirectory, "{1}/{0}" + RazorViewEngine.ViewExtension);
|
||||||
|
options.AreaPageViewLocationFormats.Add(areaSearchPath);
|
||||||
|
|
||||||
|
var areaPagesSharedSearchPath = CombinePath(areaPagesDirectory, "Shared/{0}" + RazorViewEngine.ViewExtension);
|
||||||
|
options.AreaPageViewLocationFormats.Add(areaPagesSharedSearchPath);
|
||||||
|
|
||||||
|
var areaViewsSharedSearchPath = CombinePath(areaDirectory, "Views/Shared/{0}" + RazorViewEngine.ViewExtension);
|
||||||
|
options.AreaPageViewLocationFormats.Add(areaViewsSharedSearchPath);
|
||||||
|
|
||||||
|
options.AreaPageViewLocationFormats.Add(pagesSharedDirectory);
|
||||||
|
options.AreaPageViewLocationFormats.Add("/Views/Shared/{0}" + RazorViewEngine.ViewExtension);
|
||||||
|
|
||||||
options.ViewLocationFormats.Add(pagesSharedDirectory);
|
options.ViewLocationFormats.Add(pagesSharedDirectory);
|
||||||
options.AreaViewLocationFormats.Add(pagesSharedDirectory);
|
options.AreaViewLocationFormats.Add(pagesSharedDirectory);
|
||||||
|
|
||||||
|
|
@ -44,12 +66,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
|
|
||||||
private static string CombinePath(string path1, string path2)
|
private static string CombinePath(string path1, string path2)
|
||||||
{
|
{
|
||||||
if (path1.EndsWith("/", StringComparison.Ordinal))
|
if (path1.EndsWith("/", StringComparison.Ordinal) || path2.StartsWith("/", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return path1 + path2 + RazorViewEngine.ViewExtension;
|
return path1 + path2;
|
||||||
|
}
|
||||||
|
else if (path1.EndsWith("/", StringComparison.Ordinal) && path2.StartsWith("/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return path1 + path2.Substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return path1 + "/" + path2 + RazorViewEngine.ViewExtension;
|
return path1 + "/" + path2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,21 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnProvidersExecuting(PageRouteModelProviderContext context)
|
public void OnProvidersExecuting(PageRouteModelProviderContext context)
|
||||||
|
{
|
||||||
|
AddPageModels(context);
|
||||||
|
|
||||||
|
if (_pagesOptions.EnableAreas)
|
||||||
|
{
|
||||||
|
AddAreaPageModels(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddPageModels(PageRouteModelProviderContext context)
|
||||||
{
|
{
|
||||||
foreach (var item in _project.EnumerateItems(_pagesOptions.RootDirectory))
|
foreach (var item in _project.EnumerateItems(_pagesOptions.RootDirectory))
|
||||||
{
|
{
|
||||||
if (item.FileName.StartsWith("_"))
|
if (!IsRouteable(item))
|
||||||
{
|
{
|
||||||
// Pages like _ViewImports should not be routable.
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,29 +60,71 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsAlreadyRegistered(context, item))
|
var routeModel = new PageRouteModel(
|
||||||
|
relativePath: item.CombinedPath,
|
||||||
|
viewEnginePath: item.FilePathWithoutExtension);
|
||||||
|
|
||||||
|
if (IsAlreadyRegistered(context, routeModel))
|
||||||
{
|
{
|
||||||
// The CompiledPageRouteModelProvider (or another provider) already registered a PageRoute for this path.
|
// The CompiledPageRouteModelProvider (or another provider) already registered a PageRoute for this path.
|
||||||
// Don't register a duplicate entry for this route.
|
// Don't register a duplicate entry for this route.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var routeModel = new PageRouteModel(
|
PageSelectorModel.PopulateDefaults(routeModel, routeModel.ViewEnginePath, routeTemplate);
|
||||||
relativePath: item.CombinedPath,
|
|
||||||
viewEnginePath: item.FilePathWithoutExtension);
|
|
||||||
PageSelectorModel.PopulateDefaults(routeModel, routeTemplate);
|
|
||||||
|
|
||||||
context.RouteModels.Add(routeModel);
|
context.RouteModels.Add(routeModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsAlreadyRegistered(PageRouteModelProviderContext context, RazorProjectItem projectItem)
|
private void AddAreaPageModels(PageRouteModelProviderContext context)
|
||||||
|
{
|
||||||
|
foreach (var item in _project.EnumerateItems(_pagesOptions.AreaRootDirectory))
|
||||||
|
{
|
||||||
|
if (!IsRouteable(item))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PageDirectiveFeature.TryGetPageDirective(_logger, item, out var routeTemplate))
|
||||||
|
{
|
||||||
|
// .cshtml pages without @page are not RazorPages.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PageSelectorModel.TryParseAreaPath(_pagesOptions, item.FilePath, _logger, out var areaResult))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var routeModel = new PageRouteModel(
|
||||||
|
relativePath: item.CombinedPath,
|
||||||
|
viewEnginePath: areaResult.viewEnginePath)
|
||||||
|
{
|
||||||
|
RouteValues =
|
||||||
|
{
|
||||||
|
["area"] = areaResult.areaName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (IsAlreadyRegistered(context, routeModel))
|
||||||
|
{
|
||||||
|
// The CompiledPageRouteModelProvider (or another provider) already registered a PageRoute for this path.
|
||||||
|
// Don't register a duplicate entry for this route.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
PageSelectorModel.PopulateDefaults(routeModel, areaResult.pageRoute, routeTemplate);
|
||||||
|
context.RouteModels.Add(routeModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsAlreadyRegistered(PageRouteModelProviderContext context, PageRouteModel routeModel)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < context.RouteModels.Count; i++)
|
for (var i = 0; i < context.RouteModels.Count; i++)
|
||||||
{
|
{
|
||||||
var routeModel = context.RouteModels[i];
|
var existingRouteModel = context.RouteModels[i];
|
||||||
if (string.Equals(routeModel.ViewEnginePath, projectItem.FilePathWithoutExtension, StringComparison.OrdinalIgnoreCase) &&
|
if (string.Equals(existingRouteModel.ViewEnginePath, routeModel.ViewEnginePath, StringComparison.OrdinalIgnoreCase) &&
|
||||||
string.Equals(routeModel.RelativePath, projectItem.CombinedPath, StringComparison.OrdinalIgnoreCase))
|
string.Equals(existingRouteModel.RelativePath, existingRouteModel.RelativePath, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -81,5 +132,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsRouteable(RazorProjectItem item)
|
||||||
|
{
|
||||||
|
// Pages like _ViewImports should not be routable.
|
||||||
|
return !item.FileName.StartsWith("_", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.RazorPages
|
namespace Microsoft.AspNetCore.Mvc.RazorPages
|
||||||
{
|
{
|
||||||
[DebuggerDisplay("{" + nameof(ViewEnginePath) + "}")]
|
[DebuggerDisplay(nameof(DebuggerDisplayString))]
|
||||||
public class PageActionDescriptor : ActionDescriptor
|
public class PageActionDescriptor : ActionDescriptor
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -60,5 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
||||||
base.DisplayName = value;
|
base.DisplayName = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string DebuggerDisplayString() => $"{{ViewEnginePath = {nameof(ViewEnginePath)}, RelativePath = {nameof(RelativePath)}}}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.RazorPages
|
namespace Microsoft.AspNetCore.Mvc.RazorPages
|
||||||
|
|
@ -13,6 +12,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
||||||
public class RazorPagesOptions
|
public class RazorPagesOptions
|
||||||
{
|
{
|
||||||
private string _root = "/Pages";
|
private string _root = "/Pages";
|
||||||
|
private string _areasRoot = "/Areas";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a collection of <see cref="IPageConvention"/> instances that are applied during
|
/// Gets a collection of <see cref="IPageConvention"/> instances that are applied during
|
||||||
|
|
@ -42,5 +42,41 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
||||||
_root = value;
|
_root = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value that determines if areas are enabled for Razor Pages.
|
||||||
|
/// Defaults to <c>true</c>.
|
||||||
|
/// <para>
|
||||||
|
/// When enabled, any Razor Page under the directory structure <c>/{AreaRootDirectory}/AreaName/{RootDirectory}/</c>
|
||||||
|
/// will be associated with an area with the name <c>AreaName</c>.
|
||||||
|
/// <seealso cref="AreaRootDirectory"/>
|
||||||
|
/// <seealso cref="RootDirectory"/>
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableAreas { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application relative path used as the root of discovery for Razor Page files associated with areas.
|
||||||
|
/// Defaults to the <c>/Areas</c> directory under application root.
|
||||||
|
/// <seealso cref="EnableAreas" />
|
||||||
|
/// </summary>
|
||||||
|
public string AreaRootDirectory
|
||||||
|
{
|
||||||
|
get => _areasRoot;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value[0] != '/')
|
||||||
|
{
|
||||||
|
throw new ArgumentException(Resources.PathMustBeRootRelativePath, nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
_areasRoot = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -1556,6 +1557,49 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
||||||
"Specify a root relative path with a leading '/' to generate a URL outside of a Razor Page.", ex.Message);
|
"Specify a root relative path with a leading '/' to generate a URL outside of a Razor Page.", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Page_UsesAreaValueFromRouteValueIfSpecified()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
UrlRouteContext actual = null;
|
||||||
|
var routeData = new RouteData
|
||||||
|
{
|
||||||
|
Values =
|
||||||
|
{
|
||||||
|
{ "page", "ambient-page" },
|
||||||
|
{ "area", "ambient-area" },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var actionContext = new ActionContext
|
||||||
|
{
|
||||||
|
RouteData = routeData,
|
||||||
|
};
|
||||||
|
|
||||||
|
var urlHelper = CreateMockUrlHelper(actionContext);
|
||||||
|
urlHelper.Setup(h => h.RouteUrl(It.IsAny<UrlRouteContext>()))
|
||||||
|
.Callback((UrlRouteContext context) => actual = context);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
string page = null;
|
||||||
|
urlHelper.Object.Page(page, values: new { area = "specified-area" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
urlHelper.Verify();
|
||||||
|
Assert.NotNull(actual);
|
||||||
|
Assert.Null(actual.RouteName);
|
||||||
|
Assert.Collection(Assert.IsType<RouteValueDictionary>(actual.Values).OrderBy(v => v.Key),
|
||||||
|
value =>
|
||||||
|
{
|
||||||
|
Assert.Equal("area", value.Key);
|
||||||
|
Assert.Equal("specified-area", value.Value);
|
||||||
|
},
|
||||||
|
value =>
|
||||||
|
{
|
||||||
|
Assert.Equal("page", value.Key);
|
||||||
|
Assert.Equal("ambient-page", value.Value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static Mock<IUrlHelper> CreateMockUrlHelper(ActionContext context = null)
|
private static Mock<IUrlHelper> CreateMockUrlHelper(ActionContext context = null)
|
||||||
{
|
{
|
||||||
if (context == null)
|
if (context == null)
|
||||||
|
|
|
||||||
|
|
@ -280,7 +280,7 @@ Hello from page";
|
||||||
public async Task Pages_ReturnsFromPagesSharedDirectory()
|
public async Task Pages_ReturnsFromPagesSharedDirectory()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = "Hello from Pages/Shared";
|
var expected = "Hello from /Pages/Shared/";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await Client.GetStringAsync("/SearchInPages");
|
var response = await Client.GetStringAsync("/SearchInPages");
|
||||||
|
|
@ -288,5 +288,87 @@ Hello from page";
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(expected, response.Trim());
|
Assert.Equal(expected, response.Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PagesInAreas_Work()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected = "Hello from a page in Accounts area";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await Client.GetStringAsync("/Accounts/About");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, response.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PagesInAreas_CanHaveRouteTemplates()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected = "The id is 42";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await Client.GetStringAsync("/Accounts/PageWithRouteTemplate/42");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, response.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PagesInAreas_CanGenerateLinksToControllersAndPages()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected =
|
||||||
|
@"<a href=""/Accounts/Manage/RenderPartials"">Link inside area</a>
|
||||||
|
<a href=""/Products/List/old/20"">Link to external area</a>
|
||||||
|
<a href=""/Accounts"">Link to area action</a>
|
||||||
|
<a href=""/Admin"">Link to non-area page</a>";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await Client.GetStringAsync("/Accounts/PageWithLinks");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, response.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PagesInAreas_CanGenerateRelativeLinks()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected =
|
||||||
|
@"<a href=""/Accounts/PageWithRouteTemplate/1"">Parent directory</a>
|
||||||
|
<a href=""/Accounts/Manage/RenderPartials"">Sibling directory</a>
|
||||||
|
<a href=""/Products/List"">Go back to root of different area</a>";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await Client.GetStringAsync("/Accounts/RelativeLinks");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, response.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PagesInAreas_CanDiscoverViewsFromAreaAndSharedDirectories()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected =
|
||||||
|
@"Layout in /Views/Shared
|
||||||
|
Partial in /Areas/Accounts/Pages/Manage/
|
||||||
|
|
||||||
|
Partial in /Areas/Accounts/Pages/
|
||||||
|
|
||||||
|
Partial in /Areas/Accounts/Pages/
|
||||||
|
|
||||||
|
Partial in /Areas/Accounts/Views/Shared/
|
||||||
|
|
||||||
|
Hello from /Pages/Shared/";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await Client.GetStringAsync("/Accounts/Manage/RenderPartials");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, response.Trim());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,65 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
Assert.Equal(expected, actual.ToArray());
|
Assert.Equal(expected, actual.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/Index", new [] { "/Areas/{2}/Pages/{0}.cshtml" })]
|
||||||
|
[InlineData("/Manage/User", new [] { "/Areas/{2}/Pages/Manage/{0}.cshtml", "/Areas/{2}/Pages/{0}.cshtml" })]
|
||||||
|
public void ExpandLocations_ExpandsAreaPaths(string pageName, string[] expected)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = CreateContext(pageName: pageName);
|
||||||
|
var locations = new[]
|
||||||
|
{
|
||||||
|
"/Areas/{2}/Pages/{1}/{0}.cshtml",
|
||||||
|
};
|
||||||
|
|
||||||
|
var expander = new PageViewLocationExpander();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var actual = expander.ExpandViewLocations(context, locations);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, actual.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExpandLocations_ExpandsAreaPaths_MultipleLocations()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = CreateContext(pageName: "/Customers/Edit");
|
||||||
|
var locations = new[]
|
||||||
|
{
|
||||||
|
"/Areas/{2}/Pages/{1}/{0}.cshtml",
|
||||||
|
"/Areas/{2}/Pages/Shared/{0}.cshtml",
|
||||||
|
"/Areas/{2}/Views/Shared/{0}.cshtml",
|
||||||
|
"/Areas/{2}/Pages/Shared/{0}.cshtml",
|
||||||
|
"/User/Customized/{1}/{0}.cshtml",
|
||||||
|
"/Views/Shared/{0}.cshtml",
|
||||||
|
"/Pages/Shared/{0}.cshtml",
|
||||||
|
};
|
||||||
|
|
||||||
|
var expected = new[]
|
||||||
|
{
|
||||||
|
"/Areas/{2}/Pages/Customers/{0}.cshtml",
|
||||||
|
"/Areas/{2}/Pages/{0}.cshtml",
|
||||||
|
"/Areas/{2}/Pages/Shared/{0}.cshtml",
|
||||||
|
"/Areas/{2}/Views/Shared/{0}.cshtml",
|
||||||
|
"/Areas/{2}/Pages/Shared/{0}.cshtml",
|
||||||
|
"/User/Customized/Customers/{0}.cshtml",
|
||||||
|
"/User/Customized/{0}.cshtml",
|
||||||
|
"/Views/Shared/{0}.cshtml",
|
||||||
|
"/Pages/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)
|
private ViewLocationExpanderContext CreateContext(string viewName = "_LoginPartial.cshtml", string pageName = null)
|
||||||
{
|
{
|
||||||
var actionContext = new ActionContext
|
var actionContext = new ActionContext
|
||||||
|
|
@ -158,7 +217,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
||||||
isMainPage: true)
|
isMainPage: true)
|
||||||
{
|
{
|
||||||
Values = new Dictionary<string, string>(),
|
Values = new Dictionary<string, string>(),
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -37,6 +39,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
Assert.Equal("/About", result.ViewEnginePath);
|
Assert.Equal("/About", result.ViewEnginePath);
|
||||||
Assert.Collection(result.Selectors,
|
Assert.Collection(result.Selectors,
|
||||||
selector => Assert.Equal("About", selector.AttributeRouteModel.Template));
|
selector => Assert.Equal("About", selector.AttributeRouteModel.Template));
|
||||||
|
Assert.Collection(result.RouteValues.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("page", kvp.Key);
|
||||||
|
Assert.Equal("/About", kvp.Value);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
result =>
|
result =>
|
||||||
{
|
{
|
||||||
|
|
@ -44,6 +52,126 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
Assert.Equal("/Home", result.ViewEnginePath);
|
Assert.Equal("/Home", result.ViewEnginePath);
|
||||||
Assert.Collection(result.Selectors,
|
Assert.Collection(result.Selectors,
|
||||||
selector => Assert.Equal("Home/some-prefix", selector.AttributeRouteModel.Template));
|
selector => Assert.Equal("Home/some-prefix", selector.AttributeRouteModel.Template));
|
||||||
|
Assert.Collection(result.RouteValues.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("page", kvp.Key);
|
||||||
|
Assert.Equal("/Home", kvp.Value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnProvidersExecuting_AddsModelsForCompiledAreaPages()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var descriptors = new[]
|
||||||
|
{
|
||||||
|
GetDescriptor("/Features/Products/Files/About.cshtml"),
|
||||||
|
GetDescriptor("/Features/Products/Files/Manage/Index.cshtml"),
|
||||||
|
GetDescriptor("/Features/Products/Files/Manage/Edit.cshtml", "{id}"),
|
||||||
|
};
|
||||||
|
var options = new RazorPagesOptions
|
||||||
|
{
|
||||||
|
EnableAreas = true,
|
||||||
|
AreaRootDirectory = "/Features",
|
||||||
|
RootDirectory = "/Files",
|
||||||
|
};
|
||||||
|
var provider = new TestCompiledPageRouteModelProvider(descriptors, options);
|
||||||
|
var context = new PageRouteModelProviderContext();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
provider.OnProvidersExecuting(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(context.RouteModels,
|
||||||
|
result =>
|
||||||
|
{
|
||||||
|
Assert.Equal("/Features/Products/Files/About.cshtml", result.RelativePath);
|
||||||
|
Assert.Equal("/About", result.ViewEnginePath);
|
||||||
|
Assert.Collection(result.Selectors,
|
||||||
|
selector => Assert.Equal("Products/About", selector.AttributeRouteModel.Template));
|
||||||
|
Assert.Collection(result.RouteValues.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("area", kvp.Key);
|
||||||
|
Assert.Equal("Products", kvp.Value);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("page", kvp.Key);
|
||||||
|
Assert.Equal("/About", kvp.Value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
result =>
|
||||||
|
{
|
||||||
|
Assert.Equal("/Features/Products/Files/Manage/Index.cshtml", result.RelativePath);
|
||||||
|
Assert.Equal("/Manage/Index", result.ViewEnginePath);
|
||||||
|
Assert.Collection(result.Selectors,
|
||||||
|
selector => Assert.Equal("Products/Manage/Index", selector.AttributeRouteModel.Template),
|
||||||
|
selector => Assert.Equal("Products/Manage", selector.AttributeRouteModel.Template));
|
||||||
|
Assert.Collection(result.RouteValues.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("area", kvp.Key);
|
||||||
|
Assert.Equal("Products", kvp.Value);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("page", kvp.Key);
|
||||||
|
Assert.Equal("/Manage/Index", kvp.Value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
result =>
|
||||||
|
{
|
||||||
|
Assert.Equal("/Features/Products/Files/Manage/Edit.cshtml", result.RelativePath);
|
||||||
|
Assert.Equal("/Manage/Edit", result.ViewEnginePath);
|
||||||
|
Assert.Collection(result.Selectors,
|
||||||
|
selector => Assert.Equal("Products/Manage/Edit/{id}", selector.AttributeRouteModel.Template));
|
||||||
|
Assert.Collection(result.RouteValues.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("area", kvp.Key);
|
||||||
|
Assert.Equal("Products", kvp.Value);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("page", kvp.Key);
|
||||||
|
Assert.Equal("/Manage/Edit", kvp.Value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnProvidersExecuting_DoesNotAddsModelsForAreaPages_IfFeatureIsDisabled()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var descriptors = new[]
|
||||||
|
{
|
||||||
|
GetDescriptor("/Pages/About.cshtml"),
|
||||||
|
GetDescriptor("/Areas/Accounts/Pages/Home.cshtml"),
|
||||||
|
};
|
||||||
|
var options = new RazorPagesOptions { EnableAreas = false };
|
||||||
|
var provider = new TestCompiledPageRouteModelProvider(descriptors, options);
|
||||||
|
var context = new PageRouteModelProviderContext();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
provider.OnProvidersExecuting(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(context.RouteModels,
|
||||||
|
result =>
|
||||||
|
{
|
||||||
|
Assert.Equal("/Pages/About.cshtml", result.RelativePath);
|
||||||
|
Assert.Equal("/About", result.ViewEnginePath);
|
||||||
|
Assert.Collection(result.Selectors,
|
||||||
|
selector => Assert.Equal("About", selector.AttributeRouteModel.Template));
|
||||||
|
Assert.Collection(result.RouteValues.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("page", kvp.Key);
|
||||||
|
Assert.Equal("/About", kvp.Value);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,7 +277,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
private readonly IEnumerable<CompiledViewDescriptor> _descriptors;
|
private readonly IEnumerable<CompiledViewDescriptor> _descriptors;
|
||||||
|
|
||||||
public TestCompiledPageRouteModelProvider(IEnumerable<CompiledViewDescriptor> descriptors, RazorPagesOptions options)
|
public TestCompiledPageRouteModelProvider(IEnumerable<CompiledViewDescriptor> descriptors, RazorPagesOptions options)
|
||||||
: base(new ApplicationPartManager(), Options.Create(options))
|
: base(new ApplicationPartManager(), Options.Create(options), NullLoggerFactory.Instance)
|
||||||
{
|
{
|
||||||
_descriptors = descriptors;
|
_descriptors = descriptors;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var fileProvider = new Mock<IFileProvider>();
|
var fileProvider = new Mock<IFileProvider>();
|
||||||
|
fileProvider.Setup(f => f.Watch(It.IsAny<string>()))
|
||||||
|
.Returns(Mock.Of<IChangeToken>());
|
||||||
var accessor = Mock.Of<IRazorViewEngineFileProviderAccessor>(a => a.FileProvider == fileProvider.Object);
|
var accessor = Mock.Of<IRazorViewEngineFileProviderAccessor>(a => a.FileProvider == fileProvider.Object);
|
||||||
|
|
||||||
var templateEngine = new RazorTemplateEngine(
|
var templateEngine = new RazorTemplateEngine(
|
||||||
|
|
@ -41,6 +43,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var fileProvider = new Mock<IFileProvider>();
|
var fileProvider = new Mock<IFileProvider>();
|
||||||
|
fileProvider.Setup(f => f.Watch(It.IsAny<string>()))
|
||||||
|
.Returns(Mock.Of<IChangeToken>());
|
||||||
var accessor = Mock.Of<IRazorViewEngineFileProviderAccessor>(a => a.FileProvider == fileProvider.Object);
|
var accessor = Mock.Of<IRazorViewEngineFileProviderAccessor>(a => a.FileProvider == fileProvider.Object);
|
||||||
|
|
||||||
var templateEngine = new RazorTemplateEngine(
|
var templateEngine = new RazorTemplateEngine(
|
||||||
|
|
@ -59,7 +63,57 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetChangeToken_WatchesViewImportsOutsidePagesRoot()
|
public void GetChangeToken_WatchesFilesUnderAreaRoot()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var fileProvider = new Mock<IFileProvider>();
|
||||||
|
fileProvider.Setup(f => f.Watch(It.IsAny<string>()))
|
||||||
|
.Returns(Mock.Of<IChangeToken>());
|
||||||
|
var accessor = Mock.Of<IRazorViewEngineFileProviderAccessor>(a => a.FileProvider == fileProvider.Object);
|
||||||
|
|
||||||
|
var templateEngine = new RazorTemplateEngine(
|
||||||
|
RazorEngine.Create(),
|
||||||
|
new FileProviderRazorProject(accessor));
|
||||||
|
var options = Options.Create(new RazorPagesOptions { EnableAreas = true });
|
||||||
|
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, options);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var changeToken = changeProvider.GetChangeToken();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
fileProvider.Verify(f => f.Watch("/Areas/**/*.cshtml"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/areas-base-dir")]
|
||||||
|
[InlineData("/areas-base-dir/")]
|
||||||
|
public void GetChangeToken_WatchesFilesUnderCustomAreaRoot(string rootDirectory)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var fileProvider = new Mock<IFileProvider>();
|
||||||
|
fileProvider.Setup(f => f.Watch(It.IsAny<string>()))
|
||||||
|
.Returns(Mock.Of<IChangeToken>());
|
||||||
|
var accessor = Mock.Of<IRazorViewEngineFileProviderAccessor>(a => a.FileProvider == fileProvider.Object);
|
||||||
|
|
||||||
|
var templateEngine = new RazorTemplateEngine(
|
||||||
|
RazorEngine.Create(),
|
||||||
|
new FileProviderRazorProject(accessor));
|
||||||
|
var options = Options.Create(new RazorPagesOptions
|
||||||
|
{
|
||||||
|
EnableAreas = true,
|
||||||
|
AreaRootDirectory = rootDirectory,
|
||||||
|
});
|
||||||
|
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, options);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var changeToken = changeProvider.GetChangeToken();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
fileProvider.Verify(f => f.Watch("/areas-base-dir/**/*.cshtml"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetChangeToken_WatchesViewImportsOutsidePagesRoot_WhenPagesRootIsNested()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var fileProvider = new TestFileProvider();
|
var fileProvider = new TestFileProvider();
|
||||||
|
|
@ -81,5 +135,55 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
changeToken => Assert.Same(fileProvider.GetChangeToken("/_ViewImports.cshtml"), changeToken),
|
changeToken => Assert.Same(fileProvider.GetChangeToken("/_ViewImports.cshtml"), changeToken),
|
||||||
changeToken => Assert.Same(fileProvider.GetChangeToken("/dir1/dir2/**/*.cshtml"), changeToken));
|
changeToken => Assert.Same(fileProvider.GetChangeToken("/dir1/dir2/**/*.cshtml"), changeToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetChangeToken_WatchesViewImportsOutsidePagesRoot_WhenAreaPagesRootIsNested()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var fileProvider = new TestFileProvider();
|
||||||
|
var accessor = Mock.Of<IRazorViewEngineFileProviderAccessor>(a => a.FileProvider == fileProvider);
|
||||||
|
|
||||||
|
var templateEngine = new RazorTemplateEngine(
|
||||||
|
RazorEngine.Create(),
|
||||||
|
new FileProviderRazorProject(accessor));
|
||||||
|
templateEngine.Options.ImportsFileName = "_ViewImports.cshtml";
|
||||||
|
var options = Options.Create(new RazorPagesOptions());
|
||||||
|
options.Value.RootDirectory = "/dir1/dir2";
|
||||||
|
options.Value.AreaRootDirectory = "/dir3/dir4";
|
||||||
|
options.Value.EnableAreas = true;
|
||||||
|
|
||||||
|
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, options);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var compositeChangeToken = Assert.IsType<CompositeChangeToken>(changeProvider.GetChangeToken());
|
||||||
|
Assert.Collection(compositeChangeToken.ChangeTokens,
|
||||||
|
changeToken => Assert.Same(fileProvider.GetChangeToken("/dir1/_ViewImports.cshtml"), changeToken),
|
||||||
|
changeToken => Assert.Same(fileProvider.GetChangeToken("/_ViewImports.cshtml"), changeToken),
|
||||||
|
changeToken => Assert.Same(fileProvider.GetChangeToken("/dir3/_ViewImports.cshtml"), changeToken),
|
||||||
|
changeToken => Assert.Same(fileProvider.GetChangeToken("/dir1/dir2/**/*.cshtml"), changeToken),
|
||||||
|
changeToken => Assert.Same(fileProvider.GetChangeToken("/dir3/dir4/**/*.cshtml"), changeToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetChangeToken_WatchesViewImportsOutsidePagesRoot_WhenAreaFeatureIsDisabled()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var fileProvider = new TestFileProvider();
|
||||||
|
var accessor = Mock.Of<IRazorViewEngineFileProviderAccessor>(a => a.FileProvider == fileProvider);
|
||||||
|
|
||||||
|
var templateEngine = new RazorTemplateEngine(
|
||||||
|
RazorEngine.Create(),
|
||||||
|
new FileProviderRazorProject(accessor));
|
||||||
|
templateEngine.Options.ImportsFileName = "_ViewImports.cshtml";
|
||||||
|
var options = Options.Create(new RazorPagesOptions { EnableAreas = false });
|
||||||
|
|
||||||
|
var changeProvider = new PageActionDescriptorChangeProvider(templateEngine, accessor, options);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var compositeChangeToken = Assert.IsType<CompositeChangeToken>(changeProvider.GetChangeToken());
|
||||||
|
Assert.Collection(compositeChangeToken.ChangeTokens,
|
||||||
|
changeToken => Assert.Same(fileProvider.GetChangeToken("/_ViewImports.cshtml"), changeToken),
|
||||||
|
changeToken => Assert.Same(fileProvider.GetChangeToken("/Pages/**/*.cshtml"), changeToken));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
// 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.Extensions.Logging.Abstractions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
|
{
|
||||||
|
public class PageSelectorModelTest
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/Areas/About.cshtml")]
|
||||||
|
[InlineData("/Areas/MyArea/Index.cshtml")]
|
||||||
|
public void TryParseAreaPath_ReturnsFalse_IfPathDoesNotConform(string path)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var options = new RazorPagesOptions();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var success = PageSelectorModel.TryParseAreaPath(options, path, NullLogger.Instance, out _);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(success);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/MyArea/Views/About.cshtml")]
|
||||||
|
[InlineData("/MyArea/SubDir/Pages/Index.cshtml")]
|
||||||
|
[InlineData("/MyArea/NotPages/SubDir/About.cshtml")]
|
||||||
|
public void TryParseAreaPath_ReturnsFalse_IfPathDoesNotBelongToRootDirectory(string path)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var options = new RazorPagesOptions();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var success = PageSelectorModel.TryParseAreaPath(options, path, NullLogger.Instance, out _);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(success);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/MyArea/Pages/Index.cshtml", "MyArea", "/Index", "/MyArea/Index")]
|
||||||
|
[InlineData("/Accounts/Pages/Manage/Edit.cshtml", "Accounts", "/Manage/Edit", "/Accounts/Manage/Edit")]
|
||||||
|
public void TryParseAreaPath_ParsesAreaPath(
|
||||||
|
string path,
|
||||||
|
string expectedArea,
|
||||||
|
string expectedViewEnginePath,
|
||||||
|
string expectedRoute)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var options = new RazorPagesOptions();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var success = PageSelectorModel.TryParseAreaPath(options, path, NullLogger.Instance, out var result);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(success);
|
||||||
|
Assert.Equal(expectedArea, result.areaName);
|
||||||
|
Assert.Equal(expectedViewEnginePath, result.viewEnginePath);
|
||||||
|
Assert.Equal(expectedRoute, result.pageRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/MyArea/Dir1/Dir2/Index.cshtml", "MyArea", "/Index", "/MyArea/Index")]
|
||||||
|
[InlineData("/Accounts/Dir1/Dir2/Manage/Edit.cshtml", "Accounts", "/Manage/Edit", "/Accounts/Manage/Edit")]
|
||||||
|
public void TryParseAreaPath_ParsesAreaPath_WithMultiLevelRootDirectory(
|
||||||
|
string path,
|
||||||
|
string expectedArea,
|
||||||
|
string expectedViewEnginePath,
|
||||||
|
string expectedRoute)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var options = new RazorPagesOptions
|
||||||
|
{
|
||||||
|
RootDirectory = "/Dir1/Dir2"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var success = PageSelectorModel.TryParseAreaPath(options, path, NullLogger.Instance, out var result);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(success);
|
||||||
|
Assert.Equal(expectedArea, result.areaName);
|
||||||
|
Assert.Equal(expectedViewEnginePath, result.viewEnginePath);
|
||||||
|
Assert.Equal(expectedRoute, result.pageRoute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -62,6 +62,60 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
Assert.Equal(expected, viewEngineOptions.PageViewLocationFormats);
|
Assert.Equal(expected, viewEngineOptions.PageViewLocationFormats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Configure_AddsAreaPageViewLocationFormats()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected = new[]
|
||||||
|
{
|
||||||
|
"/Areas/{2}/Pages/{1}/{0}.cshtml",
|
||||||
|
"/Areas/{2}/Pages/Shared/{0}.cshtml",
|
||||||
|
"/Areas/{2}/Views/Shared/{0}.cshtml",
|
||||||
|
"/Pages/Shared/{0}.cshtml",
|
||||||
|
"/Views/Shared/{0}.cshtml",
|
||||||
|
};
|
||||||
|
|
||||||
|
var razorPagesOptions = new RazorPagesOptions();
|
||||||
|
var viewEngineOptions = GetViewEngineOptions();
|
||||||
|
var setup = new RazorPagesRazorViewEngineOptionsSetup(
|
||||||
|
Options.Create(razorPagesOptions));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
setup.Configure(viewEngineOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, viewEngineOptions.AreaPageViewLocationFormats);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Configure_WithCustomRoots_AddsAreaPageViewLocationFormats()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expected = new[]
|
||||||
|
{
|
||||||
|
"/Features/{2}/RazorFiles/{1}/{0}.cshtml",
|
||||||
|
"/Features/{2}/RazorFiles/Shared/{0}.cshtml",
|
||||||
|
"/Features/{2}/Views/Shared/{0}.cshtml",
|
||||||
|
"/RazorFiles/Shared/{0}.cshtml",
|
||||||
|
"/Views/Shared/{0}.cshtml",
|
||||||
|
};
|
||||||
|
|
||||||
|
var razorPagesOptions = new RazorPagesOptions
|
||||||
|
{
|
||||||
|
AreaRootDirectory = "/Features",
|
||||||
|
RootDirectory = "/RazorFiles/",
|
||||||
|
};
|
||||||
|
var viewEngineOptions = GetViewEngineOptions();
|
||||||
|
var setup = new RazorPagesRazorViewEngineOptionsSetup(
|
||||||
|
Options.Create(razorPagesOptions));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
setup.Configure(viewEngineOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expected, viewEngineOptions.AreaPageViewLocationFormats);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Configure_AddsSharedPagesDirectoryToViewLocationFormats()
|
public void Configure_AddsSharedPagesDirectoryToViewLocationFormats()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor;
|
using Microsoft.AspNetCore.Mvc.Razor;
|
||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
|
@ -42,6 +43,131 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||||
Assert.Equal("/Pages/Home", model.ViewEnginePath);
|
Assert.Equal("/Pages/Home", model.ViewEnginePath);
|
||||||
Assert.Collection(model.Selectors,
|
Assert.Collection(model.Selectors,
|
||||||
selector => Assert.Equal("Pages/Home", selector.AttributeRouteModel.Template));
|
selector => Assert.Equal("Pages/Home", selector.AttributeRouteModel.Template));
|
||||||
|
Assert.Collection(model.RouteValues.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("page", kvp.Key);
|
||||||
|
Assert.Equal("/Pages/Home", kvp.Value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnProvidersExecuting_AddsPagesUnderAreas()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var fileProvider = new TestFileProvider();
|
||||||
|
var file1 = fileProvider.AddFile("Categories.cshtml", "@page");
|
||||||
|
var file2 = fileProvider.AddFile("Index.cshtml", "@page");
|
||||||
|
var file3 = fileProvider.AddFile("List.cshtml", "@page \"{sortOrder?}\"");
|
||||||
|
var file4 = fileProvider.AddFile("_ViewStart.cshtml", "@page");
|
||||||
|
var manageDir = fileProvider.AddDirectoryContent("/Areas/Products/Pages/Manage", new[] { file1 });
|
||||||
|
var pagesDir = fileProvider.AddDirectoryContent("/Areas/Products/Pages", new IFileInfo[] { manageDir, file2, file3, file4 });
|
||||||
|
var productsDir = fileProvider.AddDirectoryContent("/Areas/Products", new[] { pagesDir });
|
||||||
|
var areasDir = fileProvider.AddDirectoryContent("/Areas", new[] { productsDir });
|
||||||
|
var rootDir = fileProvider.AddDirectoryContent("/", new[] { areasDir });
|
||||||
|
|
||||||
|
var project = new TestRazorProject(fileProvider);
|
||||||
|
|
||||||
|
var optionsManager = Options.Create(new RazorPagesOptions { EnableAreas = true });
|
||||||
|
var provider = new RazorProjectPageRouteModelProvider(project, optionsManager, NullLoggerFactory.Instance);
|
||||||
|
var context = new PageRouteModelProviderContext();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
provider.OnProvidersExecuting(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(context.RouteModels,
|
||||||
|
model =>
|
||||||
|
{
|
||||||
|
Assert.Equal("/Areas/Products/Pages/Manage/Categories.cshtml", model.RelativePath);
|
||||||
|
Assert.Equal("/Manage/Categories", model.ViewEnginePath);
|
||||||
|
Assert.Collection(model.Selectors,
|
||||||
|
selector => Assert.Equal("Products/Manage/Categories", selector.AttributeRouteModel.Template));
|
||||||
|
Assert.Collection(model.RouteValues.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("area", kvp.Key);
|
||||||
|
Assert.Equal("Products", kvp.Value);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("page", kvp.Key);
|
||||||
|
Assert.Equal("/Manage/Categories", kvp.Value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
model =>
|
||||||
|
{
|
||||||
|
Assert.Equal("/Areas/Products/Pages/Index.cshtml", model.RelativePath);
|
||||||
|
Assert.Equal("/Index", model.ViewEnginePath);
|
||||||
|
Assert.Collection(model.Selectors,
|
||||||
|
selector => Assert.Equal("Products/Index", selector.AttributeRouteModel.Template),
|
||||||
|
selector => Assert.Equal("Products", selector.AttributeRouteModel.Template));
|
||||||
|
Assert.Collection(model.RouteValues.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("area", kvp.Key);
|
||||||
|
Assert.Equal("Products", kvp.Value);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("page", kvp.Key);
|
||||||
|
Assert.Equal("/Index", kvp.Value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
model =>
|
||||||
|
{
|
||||||
|
Assert.Equal("/Areas/Products/Pages/List.cshtml", model.RelativePath);
|
||||||
|
Assert.Equal("/List", model.ViewEnginePath);
|
||||||
|
Assert.Collection(model.Selectors,
|
||||||
|
selector => Assert.Equal("Products/List/{sortOrder?}", selector.AttributeRouteModel.Template));
|
||||||
|
Assert.Collection(model.RouteValues.OrderBy(k => k.Key),
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("area", kvp.Key);
|
||||||
|
Assert.Equal("Products", kvp.Value);
|
||||||
|
},
|
||||||
|
kvp =>
|
||||||
|
{
|
||||||
|
Assert.Equal("page", kvp.Key);
|
||||||
|
Assert.Equal("/List", kvp.Value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnProvidersExecuting_DoesNotAddPagesUnderAreas_WhenFeatureIsDisabled()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var fileProvider = new TestFileProvider();
|
||||||
|
var file1 = fileProvider.AddFile("Categories.cshtml", "@page");
|
||||||
|
var file2 = fileProvider.AddFile("Index.cshtml", "@page");
|
||||||
|
var file3 = fileProvider.AddFile("List.cshtml", "@page \"{sortOrder?}\"");
|
||||||
|
var file4 = fileProvider.AddFile("About.cshtml", "@page");
|
||||||
|
var manageDir = fileProvider.AddDirectoryContent("/Areas/Products/Pages/Manage", new[] { file1 });
|
||||||
|
var areaPagesDir = fileProvider.AddDirectoryContent("/Areas/Products/Pages", new IFileInfo[] { manageDir, file2, file3, });
|
||||||
|
var productsDir = fileProvider.AddDirectoryContent("/Areas/Products", new[] { areaPagesDir });
|
||||||
|
var areasDir = fileProvider.AddDirectoryContent("/Areas", new[] { productsDir });
|
||||||
|
var pagesDir = fileProvider.AddDirectoryContent("/Pages", new[] { file4 });
|
||||||
|
var rootDir = fileProvider.AddDirectoryContent("/", new[] { areasDir, pagesDir });
|
||||||
|
|
||||||
|
var project = new TestRazorProject(fileProvider);
|
||||||
|
|
||||||
|
var optionsManager = Options.Create(new RazorPagesOptions { EnableAreas = false });
|
||||||
|
var provider = new RazorProjectPageRouteModelProvider(project, optionsManager, NullLoggerFactory.Instance);
|
||||||
|
var context = new PageRouteModelProviderContext();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
provider.OnProvidersExecuting(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(context.RouteModels,
|
||||||
|
model =>
|
||||||
|
{
|
||||||
|
Assert.Equal("/Pages/About.cshtml", model.RelativePath);
|
||||||
|
Assert.Equal("/About", model.ViewEnginePath);
|
||||||
|
Assert.Collection(model.Selectors,
|
||||||
|
selector => Assert.Equal("About", selector.AttributeRouteModel.Template));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -417,6 +417,68 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
generator.Verify();
|
generator.Verify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessAsync_AddsAreaToRouteValuesAndCallsPageLinkWithExpectedParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = new TagHelperContext(
|
||||||
|
tagName: "a",
|
||||||
|
allAttributes: new TagHelperAttributeList(
|
||||||
|
Enumerable.Empty<TagHelperAttribute>()),
|
||||||
|
items: new Dictionary<object, object>(),
|
||||||
|
uniqueId: "test");
|
||||||
|
var output = new TagHelperOutput(
|
||||||
|
"a",
|
||||||
|
attributes: new TagHelperAttributeList(),
|
||||||
|
getChildContentAsync: (useCachedResult, encoder) =>
|
||||||
|
{
|
||||||
|
var tagHelperContent = new DefaultTagHelperContent();
|
||||||
|
tagHelperContent.SetContent("Something");
|
||||||
|
return Task.FromResult<TagHelperContent>(tagHelperContent);
|
||||||
|
});
|
||||||
|
output.Content.SetContent(string.Empty);
|
||||||
|
|
||||||
|
var generator = new Mock<IHtmlGenerator>();
|
||||||
|
generator
|
||||||
|
.Setup(mock => mock.GeneratePageLink(
|
||||||
|
It.IsAny<ViewContext>(),
|
||||||
|
string.Empty,
|
||||||
|
"/User/Home/Index",
|
||||||
|
"page-handler",
|
||||||
|
"http",
|
||||||
|
"contoso.com",
|
||||||
|
"hello=world",
|
||||||
|
It.IsAny<object>(),
|
||||||
|
null))
|
||||||
|
.Callback((ViewContext v, string linkText, string pageName, string pageHandler, string protocol, string hostname, string fragment, object routeValues, object htmlAttributes) =>
|
||||||
|
{
|
||||||
|
var rvd = Assert.IsType<RouteValueDictionary>(routeValues);
|
||||||
|
Assert.Collection(rvd.OrderBy(item => item.Key),
|
||||||
|
item =>
|
||||||
|
{
|
||||||
|
Assert.Equal("area", item.Key);
|
||||||
|
Assert.Equal("test-area", item.Value);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Returns(new TagBuilder("a"))
|
||||||
|
.Verifiable();
|
||||||
|
var anchorTagHelper = new AnchorTagHelper(generator.Object)
|
||||||
|
{
|
||||||
|
Page = "/User/Home/Index",
|
||||||
|
Area = "test-area",
|
||||||
|
PageHandler = "page-handler",
|
||||||
|
Fragment = "hello=world",
|
||||||
|
Host = "contoso.com",
|
||||||
|
Protocol = "http",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await anchorTagHelper.ProcessAsync(context, output);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
generator.Verify();
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("Action")]
|
[InlineData("Action")]
|
||||||
[InlineData("Controller")]
|
[InlineData("Controller")]
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
@ -441,6 +440,71 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
Assert.Empty(output.Content.GetContent());
|
Assert.Empty(output.Content.GetContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessAsync_WithPageAndArea_CallsUrlHelperWithExpectedValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var context = new TagHelperContext(
|
||||||
|
tagName: "form-action",
|
||||||
|
allAttributes: new TagHelperAttributeList(),
|
||||||
|
items: new Dictionary<object, object>(),
|
||||||
|
uniqueId: "test");
|
||||||
|
var output = new TagHelperOutput(
|
||||||
|
"submit",
|
||||||
|
attributes: new TagHelperAttributeList(),
|
||||||
|
getChildContentAsync: (useCachedResult, encoder) =>
|
||||||
|
{
|
||||||
|
return Task.FromResult<TagHelperContent>(new DefaultTagHelperContent());
|
||||||
|
});
|
||||||
|
|
||||||
|
var urlHelper = new Mock<IUrlHelper>();
|
||||||
|
urlHelper
|
||||||
|
.Setup(mock => mock.RouteUrl(It.IsAny<UrlRouteContext>()))
|
||||||
|
.Callback<UrlRouteContext>(routeContext =>
|
||||||
|
{
|
||||||
|
var rvd = Assert.IsType<RouteValueDictionary>(routeContext.Values);
|
||||||
|
Assert.Collection(
|
||||||
|
rvd.OrderBy(item => item.Key),
|
||||||
|
item =>
|
||||||
|
{
|
||||||
|
Assert.Equal("area", item.Key);
|
||||||
|
Assert.Equal("test-area", item.Value);
|
||||||
|
},
|
||||||
|
item =>
|
||||||
|
{
|
||||||
|
Assert.Equal("page", item.Key);
|
||||||
|
Assert.Equal("/my-page", item.Value);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Returns("admin/dashboard/index")
|
||||||
|
.Verifiable();
|
||||||
|
|
||||||
|
var viewContext = new ViewContext
|
||||||
|
{
|
||||||
|
RouteData = new RouteData(),
|
||||||
|
};
|
||||||
|
|
||||||
|
urlHelper.SetupGet(h => h.ActionContext)
|
||||||
|
.Returns(viewContext);
|
||||||
|
var urlHelperFactory = new Mock<IUrlHelperFactory>(MockBehavior.Strict);
|
||||||
|
urlHelperFactory
|
||||||
|
.Setup(f => f.GetUrlHelper(viewContext))
|
||||||
|
.Returns(urlHelper.Object);
|
||||||
|
|
||||||
|
var tagHelper = new FormActionTagHelper(urlHelperFactory.Object)
|
||||||
|
{
|
||||||
|
Area = "test-area",
|
||||||
|
Page = "/my-page",
|
||||||
|
ViewContext = viewContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await tagHelper.ProcessAsync(context, output);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
urlHelper.Verify();
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("button", "Action")]
|
[InlineData("button", "Action")]
|
||||||
[InlineData("button", "Controller")]
|
[InlineData("button", "Controller")]
|
||||||
|
|
|
||||||
|
|
@ -900,6 +900,64 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
generator.Verify();
|
generator.Verify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessAsync_WithPageAndArea_InvokesGeneratePageForm()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var viewContext = CreateViewContext();
|
||||||
|
var context = new TagHelperContext(
|
||||||
|
tagName: "form",
|
||||||
|
allAttributes: new TagHelperAttributeList(
|
||||||
|
Enumerable.Empty<TagHelperAttribute>()),
|
||||||
|
items: new Dictionary<object, object>(),
|
||||||
|
uniqueId: "test");
|
||||||
|
var output = new TagHelperOutput(
|
||||||
|
"form",
|
||||||
|
attributes: new TagHelperAttributeList(),
|
||||||
|
getChildContentAsync: (useCachedResult, encoder) =>
|
||||||
|
{
|
||||||
|
var tagHelperContent = new DefaultTagHelperContent();
|
||||||
|
tagHelperContent.SetContent("Something");
|
||||||
|
return Task.FromResult<TagHelperContent>(tagHelperContent);
|
||||||
|
});
|
||||||
|
var generator = new Mock<IHtmlGenerator>(MockBehavior.Strict);
|
||||||
|
generator
|
||||||
|
.Setup(mock => mock.GeneratePageForm(
|
||||||
|
viewContext,
|
||||||
|
"/Home/Admin/Post",
|
||||||
|
"page-handler",
|
||||||
|
It.IsAny<object>(),
|
||||||
|
"hello-world",
|
||||||
|
null,
|
||||||
|
null))
|
||||||
|
.Callback((ViewContext _, string pageName, string pageHandler, object routeValues, string fragment, string method, object htmlAttributes) =>
|
||||||
|
{
|
||||||
|
var rvd = Assert.IsType<RouteValueDictionary>(routeValues);
|
||||||
|
Assert.Collection(
|
||||||
|
rvd.OrderBy(item => item.Key),
|
||||||
|
item =>
|
||||||
|
{
|
||||||
|
Assert.Equal("area", item.Key);
|
||||||
|
Assert.Equal("test-area", item.Value);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Returns(new TagBuilder("form"))
|
||||||
|
.Verifiable();
|
||||||
|
var formTagHelper = new FormTagHelper(generator.Object)
|
||||||
|
{
|
||||||
|
Antiforgery = false,
|
||||||
|
ViewContext = viewContext,
|
||||||
|
Page = "/Home/Admin/Post",
|
||||||
|
PageHandler = "page-handler",
|
||||||
|
Fragment = "hello-world",
|
||||||
|
Area = "test-area",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await formTagHelper.ProcessAsync(context, output);
|
||||||
|
generator.Verify();
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(true, "<input />")]
|
[InlineData(true, "<input />")]
|
||||||
[InlineData(false, "")]
|
[InlineData(false, "")]
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,17 @@ namespace Microsoft.Extensions.Primitives
|
||||||
{
|
{
|
||||||
public class TestFileChangeToken : IChangeToken
|
public class TestFileChangeToken : IChangeToken
|
||||||
{
|
{
|
||||||
|
public TestFileChangeToken(string filter = "")
|
||||||
|
{
|
||||||
|
Filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
public bool ActiveChangeCallbacks => false;
|
public bool ActiveChangeCallbacks => false;
|
||||||
|
|
||||||
public bool HasChanged { get; set; }
|
public bool HasChanged { get; set; }
|
||||||
|
|
||||||
|
public string Filter { get; }
|
||||||
|
|
||||||
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
|
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
|
||||||
{
|
{
|
||||||
return new NullDisposable();
|
return new NullDisposable();
|
||||||
|
|
@ -22,5 +29,7 @@ namespace Microsoft.Extensions.Primitives
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString() => Filter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
|
|
||||||
public virtual TestFileChangeToken AddChangeToken(string filter)
|
public virtual TestFileChangeToken AddChangeToken(string filter)
|
||||||
{
|
{
|
||||||
var changeToken = new TestFileChangeToken();
|
var changeToken = new TestFileChangeToken(filter);
|
||||||
_fileTriggers[filter] = changeToken;
|
_fileTriggers[filter] = changeToken;
|
||||||
|
|
||||||
return changeToken;
|
return changeToken;
|
||||||
|
|
@ -84,10 +84,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
|
|
||||||
public virtual IChangeToken Watch(string filter)
|
public virtual IChangeToken Watch(string filter)
|
||||||
{
|
{
|
||||||
TestFileChangeToken changeToken;
|
if (!_fileTriggers.TryGetValue(filter, out var changeToken) || changeToken.HasChanged)
|
||||||
if (!_fileTriggers.TryGetValue(filter, out changeToken) || changeToken.HasChanged)
|
|
||||||
{
|
{
|
||||||
changeToken = new TestFileChangeToken();
|
changeToken = new TestFileChangeToken(filter);
|
||||||
_fileTriggers[filter] = changeToken;
|
_fileTriggers[filter] = changeToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
// 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 RazorPagesWebSite
|
||||||
|
{
|
||||||
|
[Area("Accounts")]
|
||||||
|
public class HomeController : Controller
|
||||||
|
{
|
||||||
|
public IActionResult Index() => View();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
@page
|
||||||
|
Hello from a page in Accounts area
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
@page
|
||||||
|
@{
|
||||||
|
Layout = "_GlobalLayout";
|
||||||
|
}
|
||||||
|
<partial name="_PartialInManage" />
|
||||||
|
<partial name="_PartialInAreaPagesRoot" />
|
||||||
|
<partial name="../_PartialInAreaPagesRoot" />
|
||||||
|
<partial name="_PartialInAreasSharedViews" />
|
||||||
|
<partial name="_FileInShared" />
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Partial in /Areas/Accounts/Pages/Manage/
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
@page
|
||||||
|
<a asp-page="/Manage/RenderPartials">Link inside area</a>
|
||||||
|
<a asp-page="/List" asp-area="Products" asp-route-sort="old" asp-route-top="20">Link to external area</a>
|
||||||
|
<a asp-controller="Home" asp-action="Index">Link to area action</a>
|
||||||
|
<a asp-page="/Admin/Index" asp-area="">Link to non-area page</a>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
@page "{id}"
|
||||||
|
@functions
|
||||||
|
{
|
||||||
|
[BindProperty(SupportsGet = true)]
|
||||||
|
public string Id { get; set; }
|
||||||
|
}
|
||||||
|
The id is @Id
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
@page
|
||||||
|
<a asp-page="../PageWithRouteTemplate" asp-route-id="1">Parent directory</a>
|
||||||
|
<a asp-page="../Manage/RenderPartials">Sibling directory</a>
|
||||||
|
<a asp-page="../List" asp-area="Products">Go back to root of different area</a>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Partial in /Areas/Accounts/Pages/
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<a asp-page="/PageWithRouteTemplate" asp-route-id="10">Link to page inside current area</a>
|
||||||
|
<a asp-area="Products" asp-page="/List" asp-route-sort="best">Link to page in different area</a>
|
||||||
|
<a asp-page="/Routes/Sibling">Link to page outside area</a>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Partial in /Areas/Accounts/Views/Shared/
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
@page "{sort?}/{top?}"
|
||||||
|
|
@ -1 +1 @@
|
||||||
Hello from Pages/Shared
|
Hello from /Pages/Shared/
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ namespace RazorPagesWebSite
|
||||||
.UseKestrel()
|
.UseKestrel()
|
||||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
.UseIISIntegration()
|
.UseIISIntegration()
|
||||||
.UseStartup<Startup>()
|
.UseStartup<StartupWithBasePath>()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
host.Run();
|
host.Run();
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ namespace RazorPagesWebSite
|
||||||
.AddCookieTempDataProvider()
|
.AddCookieTempDataProvider()
|
||||||
.AddRazorPagesOptions(options =>
|
.AddRazorPagesOptions(options =>
|
||||||
{
|
{
|
||||||
|
options.EnableAreas = true;
|
||||||
options.Conventions.AuthorizePage("/Conventions/Auth");
|
options.Conventions.AuthorizePage("/Conventions/Auth");
|
||||||
options.Conventions.AuthorizeFolder("/Conventions/AuthFolder");
|
options.Conventions.AuthorizeFolder("/Conventions/AuthFolder");
|
||||||
});
|
});
|
||||||
|
|
@ -28,7 +29,10 @@ namespace RazorPagesWebSite
|
||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
|
||||||
app.UseMvc();
|
app.UseMvc(routes =>
|
||||||
|
{
|
||||||
|
routes.MapRoute("areaRoute", "{area:exists}/{controller=Home}/{action=Index}");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
Layout in /Views/Shared
|
||||||
|
@RenderBody()
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
@using Microsoft.AspNetCore.Mvc.RazorPages
|
@using Microsoft.AspNetCore.Mvc.RazorPages
|
||||||
@if (((PageActionDescriptor)ViewContext.ActionDescriptor).ViewEnginePath == "/WithViewStart/ViewStartAtRoot")
|
@if (ViewContext.ActionDescriptor is PageActionDescriptor actionDescriptor && actionDescriptor.ViewEnginePath == "/WithViewStart/ViewStartAtRoot")
|
||||||
{
|
{
|
||||||
<text>Hello from _ViewStart at root
|
<text>Hello from _ViewStart at root
|
||||||
</text>
|
</text>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue