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
|
||||
/// 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
|
||||
/// will be added to <see cref="ActionDescriptor.RouteValues"/>, but will not be visible in
|
||||
/// <see cref="RouteValues"/>.
|
||||
/// 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>
|
||||
/// <para>
|
||||
/// Entries in <see cref="RouteValues"/> can override entries in
|
||||
|
|
|
|||
|
|
@ -333,6 +333,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
{
|
||||
return _options.ViewLocationFormats;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(context.AreaName) &&
|
||||
!string.IsNullOrEmpty(context.PageName))
|
||||
{
|
||||
return _options.AreaPageViewLocationFormats;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(context.PageName))
|
||||
{
|
||||
return _options.PageViewLocationFormats;
|
||||
|
|
|
|||
|
|
@ -112,12 +112,47 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
/// /Pages/Account/Manage/{0}.cshtml
|
||||
/// /Pages/Account/{0}.cshtml
|
||||
/// /Pages/{0}.cshtml
|
||||
/// /Views/Shared/{0}.cshtml
|
||||
/// /Pages/Shared/{0}.cshtml
|
||||
/// /Views/Shared/{0}.cshtml
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
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>
|
||||
/// Gets the <see cref="MetadataReference" /> instances that should be included in Razor compilation, along with
|
||||
/// those discovered by <see cref="MetadataReferenceFeatureProvider" />s.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
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>
|
||||
public PageRouteModel(string relativePath, string viewEnginePath)
|
||||
{
|
||||
if (relativePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(relativePath));
|
||||
}
|
||||
|
||||
if (viewEnginePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(viewEnginePath));
|
||||
}
|
||||
|
||||
RelativePath = relativePath;
|
||||
ViewEnginePath = viewEnginePath;
|
||||
RelativePath = relativePath ?? throw new ArgumentNullException(nameof(relativePath));
|
||||
ViewEnginePath = viewEnginePath ?? throw new ArgumentNullException(nameof(viewEnginePath));
|
||||
|
||||
Properties = new Dictionary<object, object>();
|
||||
Selectors = new List<SelectorModel>();
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -52,6 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
|||
|
||||
Properties = new Dictionary<object, object>(other.Properties);
|
||||
Selectors = new List<SelectorModel>(other.Selectors.Select(m => new SelectorModel(m)));
|
||||
RouteValues = new Dictionary<string, string>(other.RouteValues, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -62,6 +57,9 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
|||
/// <summary>
|
||||
/// Gets the path relative to the base path for page discovery.
|
||||
/// </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; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -73,5 +71,18 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
|||
/// Gets the <see cref="SelectorModel"/> instances.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
actions.Add(new PageActionDescriptor()
|
||||
var descriptor = new PageActionDescriptor()
|
||||
{
|
||||
AttributeRouteInfo = new AttributeRouteInfo()
|
||||
{
|
||||
|
|
@ -87,12 +87,23 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
FilterDescriptors = Array.Empty<FilterDescriptor>(),
|
||||
Properties = new Dictionary<object, object>(model.Properties),
|
||||
RelativePath = model.RelativePath,
|
||||
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "page", 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 Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||
|
|
@ -17,14 +19,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
private readonly object _cacheLock = new object();
|
||||
private readonly ApplicationPartManager _applicationManager;
|
||||
private readonly RazorPagesOptions _pagesOptions;
|
||||
private readonly ILogger<CompiledPageRouteModelProvider> _logger;
|
||||
private List<PageRouteModel> _cachedModels;
|
||||
|
||||
public CompiledPageRouteModelProvider(
|
||||
ApplicationPartManager applicationManager,
|
||||
IOptions<RazorPagesOptions> pagesOptionsAccessor)
|
||||
IOptions<RazorPagesOptions> pagesOptionsAccessor,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_applicationManager = applicationManager;
|
||||
_pagesOptions = pagesOptionsAccessor.Value;
|
||||
_logger = loggerFactory.CreateLogger<CompiledPageRouteModelProvider>();
|
||||
}
|
||||
|
||||
public int Order => -1000;
|
||||
|
|
@ -58,26 +63,69 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
rootDirectory = rootDirectory + "/";
|
||||
}
|
||||
|
||||
var areaRootDirectory = _pagesOptions.AreaRootDirectory;
|
||||
if (!areaRootDirectory.EndsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
areaRootDirectory = areaRootDirectory + "/";
|
||||
}
|
||||
|
||||
var cachedApplicationModels = new List<PageRouteModel>();
|
||||
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);
|
||||
var model = new PageRouteModel(viewDescriptor.RelativePath, viewEnginePath);
|
||||
var pageAttribute = (RazorPageAttribute)viewDescriptor.ViewAttribute;
|
||||
PageSelectorModel.PopulateDefaults(model, pageAttribute.RouteTemplate);
|
||||
|
||||
cachedApplicationModels.Add(model);
|
||||
if (model != null)
|
||||
{
|
||||
cachedApplicationModels.Add(model);
|
||||
}
|
||||
}
|
||||
|
||||
_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>
|
||||
/// Gets the sequence of <see cref="CompiledViewDescriptor"/> from <paramref name="applicationManager"/>.
|
||||
/// </summary>
|
||||
|
|
@ -96,20 +144,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
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/"
|
||||
// path = "/Pages/AllMyPages/Home.cshtml"
|
||||
// Result = "/Home"
|
||||
// Result = "/Home.cshtml"
|
||||
var startIndex = rootDirectory.Length - 1;
|
||||
|
||||
return path.Substring(startIndex, endIndex - startIndex);
|
||||
return path.Substring(startIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
|
|
@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
public class PageActionDescriptorChangeProvider : IActionDescriptorChangeProvider
|
||||
{
|
||||
private readonly IFileProvider _fileProvider;
|
||||
private readonly string _searchPattern;
|
||||
private readonly string[] _searchPatterns;
|
||||
private readonly string[] _additionalFilesToTrack;
|
||||
|
||||
public PageActionDescriptorChangeProvider(
|
||||
|
|
@ -45,29 +46,61 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
Debug.Assert(!string.IsNullOrEmpty(rootDirectory));
|
||||
rootDirectory = rootDirectory.TrimEnd('/');
|
||||
|
||||
var importFileAtPagesRoot = rootDirectory + "/" + templateEngine.Options.ImportsFileName;
|
||||
_additionalFilesToTrack = templateEngine.GetImportItems(importFileAtPagesRoot)
|
||||
.Select(item => item.FilePath)
|
||||
.ToArray();
|
||||
// Search pattern that matches all cshtml files under the Pages RootDirectory
|
||||
var pagesRootSearchPattern = rootDirectory + "/**/*.cshtml";
|
||||
|
||||
_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()
|
||||
{
|
||||
var wildcardChangeToken = _fileProvider.Watch(_searchPattern);
|
||||
if (_additionalFilesToTrack.Length == 0)
|
||||
{
|
||||
return wildcardChangeToken;
|
||||
}
|
||||
|
||||
var changeTokens = new IChangeToken[_additionalFilesToTrack.Length + 1];
|
||||
var changeTokens = new IChangeToken[_additionalFilesToTrack.Length + _searchPatterns.Length];
|
||||
for (var i = 0; i < _additionalFilesToTrack.Length; 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
private static readonly Action<ILogger, string, string, Exception> _handlerMethodExecuted;
|
||||
private static readonly Action<ILogger, object, Exception> _pageFilterShortCircuit;
|
||||
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, string, string, string, Exception> _beforeExecutingMethodOnFilter;
|
||||
private static readonly Action<ILogger, string, string, string, Exception> _afterExecutingMethodOnFilter;
|
||||
|
|
@ -61,6 +62,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
LogLevel.Trace,
|
||||
2,
|
||||
"{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)
|
||||
|
|
@ -133,5 +139,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
{
|
||||
_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.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
||||
{
|
||||
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))
|
||||
{
|
||||
throw new InvalidOperationException(string.Format(
|
||||
|
|
@ -20,7 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
model.RelativePath));
|
||||
}
|
||||
|
||||
var selectorModel = CreateSelectorModel(model.ViewEnginePath, routeTemplate);
|
||||
var selectorModel = CreateSelectorModel(pageRoute, routeTemplate);
|
||||
model.Selectors.Add(selectorModel);
|
||||
|
||||
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.
|
||||
selectorModel.AttributeRouteModel.SuppressLinkGeneration = true;
|
||||
|
||||
var parentDirectoryPath = model.ViewEnginePath;
|
||||
var index = parentDirectoryPath.LastIndexOf('/');
|
||||
if (index == -1)
|
||||
{
|
||||
parentDirectoryPath = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
parentDirectoryPath = parentDirectoryPath.Substring(0, index);
|
||||
}
|
||||
var index = pageRoute.LastIndexOf('/');
|
||||
var parentDirectoryPath = index == -1 ?
|
||||
string.Empty :
|
||||
pageRoute.Substring(0, index);
|
||||
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)
|
||||
{
|
||||
return new SelectorModel
|
||||
|
|
|
|||
|
|
@ -27,15 +27,37 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
|
||||
var rootDirectory = _pagesOptions.RootDirectory;
|
||||
Debug.Assert(!string.IsNullOrEmpty(rootDirectory));
|
||||
var defaultPageSearchPath = CombinePath(rootDirectory, "{1}/{0}");
|
||||
var defaultPageSearchPath = CombinePath(rootDirectory, "{1}/{0}" + RazorViewEngine.ViewExtension);
|
||||
options.PageViewLocationFormats.Add(defaultPageSearchPath);
|
||||
|
||||
// /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("/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.AreaViewLocationFormats.Add(pagesSharedDirectory);
|
||||
|
||||
|
|
@ -44,12 +66,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
|
||||
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)
|
||||
{
|
||||
AddPageModels(context);
|
||||
|
||||
if (_pagesOptions.EnableAreas)
|
||||
{
|
||||
AddAreaPageModels(context);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddPageModels(PageRouteModelProviderContext context)
|
||||
{
|
||||
foreach (var item in _project.EnumerateItems(_pagesOptions.RootDirectory))
|
||||
{
|
||||
if (item.FileName.StartsWith("_"))
|
||||
if (!IsRouteable(item))
|
||||
{
|
||||
// Pages like _ViewImports should not be routable.
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -51,29 +60,71 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
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.
|
||||
// Don't register a duplicate entry for this route.
|
||||
continue;
|
||||
}
|
||||
|
||||
var routeModel = new PageRouteModel(
|
||||
relativePath: item.CombinedPath,
|
||||
viewEnginePath: item.FilePathWithoutExtension);
|
||||
PageSelectorModel.PopulateDefaults(routeModel, routeTemplate);
|
||||
|
||||
PageSelectorModel.PopulateDefaults(routeModel, routeModel.ViewEnginePath, routeTemplate);
|
||||
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++)
|
||||
{
|
||||
var routeModel = context.RouteModels[i];
|
||||
if (string.Equals(routeModel.ViewEnginePath, projectItem.FilePathWithoutExtension, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(routeModel.RelativePath, projectItem.CombinedPath, StringComparison.OrdinalIgnoreCase))
|
||||
var existingRouteModel = context.RouteModels[i];
|
||||
if (string.Equals(existingRouteModel.ViewEnginePath, routeModel.ViewEnginePath, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(existingRouteModel.RelativePath, existingRouteModel.RelativePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -81,5 +132,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
|
||||
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
|
||||
{
|
||||
[DebuggerDisplay("{" + nameof(ViewEnginePath) + "}")]
|
||||
[DebuggerDisplay(nameof(DebuggerDisplayString))]
|
||||
public class PageActionDescriptor : ActionDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -60,5 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
|||
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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.RazorPages
|
||||
|
|
@ -13,6 +12,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
|||
public class RazorPagesOptions
|
||||
{
|
||||
private string _root = "/Pages";
|
||||
private string _areasRoot = "/Areas";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="IPageConvention"/> instances that are applied during
|
||||
|
|
@ -42,5 +42,41 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
|
|||
_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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
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);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
if (context == null)
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@ Hello from page";
|
|||
public async Task Pages_ReturnsFromPagesSharedDirectory()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "Hello from Pages/Shared";
|
||||
var expected = "Hello from /Pages/Shared/";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetStringAsync("/SearchInPages");
|
||||
|
|
@ -288,5 +288,87 @@ Hello from page";
|
|||
// Assert
|
||||
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());
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
var actionContext = new ActionContext
|
||||
|
|
@ -158,7 +217,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
|
|||
isMainPage: true)
|
||||
{
|
||||
Values = new Dictionary<string, string>(),
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -37,6 +39,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
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);
|
||||
});
|
||||
},
|
||||
result =>
|
||||
{
|
||||
|
|
@ -44,6 +52,126 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
Assert.Equal("/Home", result.ViewEnginePath);
|
||||
Assert.Collection(result.Selectors,
|
||||
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;
|
||||
|
||||
public TestCompiledPageRouteModelProvider(IEnumerable<CompiledViewDescriptor> descriptors, RazorPagesOptions options)
|
||||
: base(new ApplicationPartManager(), Options.Create(options))
|
||||
: base(new ApplicationPartManager(), Options.Create(options), NullLoggerFactory.Instance)
|
||||
{
|
||||
_descriptors = descriptors;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
{
|
||||
// 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(
|
||||
|
|
@ -41,6 +43,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
{
|
||||
// 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(
|
||||
|
|
@ -59,7 +63,57 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
}
|
||||
|
||||
[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
|
||||
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("/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);
|
||||
}
|
||||
|
||||
[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]
|
||||
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.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
|
@ -42,6 +43,131 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
|
|||
Assert.Equal("/Pages/Home", model.ViewEnginePath);
|
||||
Assert.Collection(model.Selectors,
|
||||
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();
|
||||
}
|
||||
|
||||
[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]
|
||||
[InlineData("Action")]
|
||||
[InlineData("Controller")]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
|
@ -441,6 +440,71 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
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]
|
||||
[InlineData("button", "Action")]
|
||||
[InlineData("button", "Controller")]
|
||||
|
|
|
|||
|
|
@ -900,6 +900,64 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
|||
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]
|
||||
[InlineData(true, "<input />")]
|
||||
[InlineData(false, "")]
|
||||
|
|
|
|||
|
|
@ -7,10 +7,17 @@ namespace Microsoft.Extensions.Primitives
|
|||
{
|
||||
public class TestFileChangeToken : IChangeToken
|
||||
{
|
||||
public TestFileChangeToken(string filter = "")
|
||||
{
|
||||
Filter = filter;
|
||||
}
|
||||
|
||||
public bool ActiveChangeCallbacks => false;
|
||||
|
||||
public bool HasChanged { get; set; }
|
||||
|
||||
public string Filter { get; }
|
||||
|
||||
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var changeToken = new TestFileChangeToken();
|
||||
var changeToken = new TestFileChangeToken(filter);
|
||||
_fileTriggers[filter] = changeToken;
|
||||
|
||||
return changeToken;
|
||||
|
|
@ -84,10 +84,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
|||
|
||||
public virtual IChangeToken Watch(string filter)
|
||||
{
|
||||
TestFileChangeToken changeToken;
|
||||
if (!_fileTriggers.TryGetValue(filter, out changeToken) || changeToken.HasChanged)
|
||||
if (!_fileTriggers.TryGetValue(filter, out var changeToken) || changeToken.HasChanged)
|
||||
{
|
||||
changeToken = new TestFileChangeToken();
|
||||
changeToken = new TestFileChangeToken(filter);
|
||||
_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()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseIISIntegration()
|
||||
.UseStartup<Startup>()
|
||||
.UseStartup<StartupWithBasePath>()
|
||||
.Build();
|
||||
|
||||
host.Run();
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ namespace RazorPagesWebSite
|
|||
.AddCookieTempDataProvider()
|
||||
.AddRazorPagesOptions(options =>
|
||||
{
|
||||
options.EnableAreas = true;
|
||||
options.Conventions.AuthorizePage("/Conventions/Auth");
|
||||
options.Conventions.AuthorizeFolder("/Conventions/AuthFolder");
|
||||
});
|
||||
|
|
@ -28,7 +29,10 @@ namespace RazorPagesWebSite
|
|||
|
||||
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,6 +1,6 @@
|
|||
@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>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue