Refactor PageRouteModel generation

This commit is contained in:
Pranav K 2018-01-12 11:51:22 -08:00
parent 66c13ae5e5
commit 094b61dfc6
8 changed files with 562 additions and 414 deletions

View File

@ -1,2 +1,2 @@
version:2.1.0-preview1-15661
commithash:c9349d4c8a495d3085d9b879214d80f2f45e2193
version:2.1.0-preview1-15670
commithash:49176144e03c3015d83b21e3f1d0ce093c05ecc3

View File

@ -22,37 +22,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
private readonly RazorPagesOptions _pagesOptions;
private readonly RazorTemplateEngine _templateEngine;
private readonly ILogger<CompiledPageRouteModelProvider> _logger;
private readonly PageRouteModelFactory _routeModelFactory;
public CompiledPageRouteModelProvider(
ApplicationPartManager applicationManager,
IOptions<RazorPagesOptions> pagesOptionsAccessor,
RazorTemplateEngine templateEngine,
ILoggerFactory loggerFactory)
ILogger<CompiledPageRouteModelProvider> logger)
{
if (applicationManager == null)
{
throw new ArgumentNullException(nameof(applicationManager));
}
if (pagesOptionsAccessor == null)
{
throw new ArgumentNullException(nameof(pagesOptionsAccessor));
}
if (templateEngine == null)
{
throw new ArgumentNullException(nameof(templateEngine));
}
if (loggerFactory == null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}
_applicationManager = applicationManager;
_pagesOptions = pagesOptionsAccessor.Value;
_templateEngine = templateEngine;
_logger = loggerFactory.CreateLogger<CompiledPageRouteModelProvider>();
_applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));
_pagesOptions = pagesOptionsAccessor?.Value ?? throw new ArgumentNullException(nameof(pagesOptionsAccessor));
_templateEngine = templateEngine ?? throw new ArgumentNullException(nameof(templateEngine));
_logger = logger ?? throw new ArgumentNullException(nameof(templateEngine));
_routeModelFactory = new PageRouteModelFactory(_pagesOptions, _logger);
}
public int Order => -1000;
@ -64,7 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
throw new ArgumentNullException(nameof(context));
}
CreateModels(context.RouteModels);
CreateModels(context);
}
public void OnProvidersExecuted(PageRouteModelProviderContext context)
@ -75,7 +57,25 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
}
}
private void CreateModels(IList<PageRouteModel> results)
/// <summary>
/// Gets the sequence of <see cref="CompiledViewDescriptor"/> from <paramref name="applicationManager"/>.
/// </summary>
/// <param name="applicationManager">The <see cref="ApplicationPartManager"/>s</param>
/// <returns>The sequence of <see cref="CompiledViewDescriptor"/>.</returns>
protected virtual IEnumerable<CompiledViewDescriptor> GetViewDescriptors(ApplicationPartManager applicationManager)
{
if (applicationManager == null)
{
throw new ArgumentNullException(nameof(applicationManager));
}
var viewsFeature = new ViewsFeature();
applicationManager.PopulateFeature(viewsFeature);
return viewsFeature.ViewDescriptors.Where(d => d.IsPrecompiled && d.ViewAttribute is RazorPageAttribute);
}
private void CreateModels(PageRouteModelProviderContext context)
{
var rootDirectory = _pagesOptions.RootDirectory;
if (!rootDirectory.EndsWith("/", StringComparison.Ordinal))
@ -97,84 +97,25 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
continue;
}
PageRouteModel model = null;
var pageAttribute = (RazorPageAttribute)viewDescriptor.ViewAttribute;
PageRouteModel routeModel = null;
// When RootDirectory and AreaRootDirectory overlap (e.g. RootDirectory = '/', AreaRootDirectory = '/Areas'), we
// only want to allow a page to be associated with the area route.
if (_pagesOptions.AllowAreas && viewDescriptor.RelativePath.StartsWith(areaRootDirectory, StringComparison.OrdinalIgnoreCase))
{
model = GetAreaPageRouteModel(areaRootDirectory, viewDescriptor);
routeModel = _routeModelFactory.CreateAreaRouteModel(viewDescriptor.RelativePath, pageAttribute.RouteTemplate);
}
else if (viewDescriptor.RelativePath.StartsWith(rootDirectory, StringComparison.OrdinalIgnoreCase))
{
model = GetPageRouteModel(rootDirectory, viewDescriptor);
routeModel = _routeModelFactory.CreateRouteModel(pageAttribute.Path, pageAttribute.RouteTemplate);
}
if (model != null)
if (routeModel != null)
{
results.Add(model);
context.RouteModels.Add(routeModel);
}
}
}
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>
/// <param name="applicationManager">The <see cref="ApplicationPartManager"/>s</param>
/// <returns>The sequence of <see cref="CompiledViewDescriptor"/>.</returns>
protected virtual IEnumerable<CompiledViewDescriptor> GetViewDescriptors(ApplicationPartManager applicationManager)
{
if (applicationManager == null)
{
throw new ArgumentNullException(nameof(applicationManager));
}
var viewsFeature = new ViewsFeature();
applicationManager.PopulateFeature(viewsFeature);
return viewsFeature.ViewDescriptors.Where(d => d.IsPrecompiled && d.ViewAttribute is RazorPageAttribute);
}
private string GetRootTrimmedPath(string rootDirectory, string path)
{
// rootDirectory = "/Pages/AllMyPages/"
// path = "/Pages/AllMyPages/Home.cshtml"
// Result = "/Home.cshtml"
var startIndex = rootDirectory.Length - 1;
return path.Substring(startIndex);
}
}
}

View File

@ -0,0 +1,196 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using 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
{
internal class PageRouteModelFactory
{
private static readonly string IndexFileName = "Index" + RazorViewEngine.ViewExtension;
private readonly RazorPagesOptions _options;
private readonly ILogger _logger;
private readonly string _normalizedRootDirectory;
private readonly string _normalizedAreaRootDirectory;
public PageRouteModelFactory(
RazorPagesOptions options,
ILogger logger)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_normalizedRootDirectory = NormalizeDirectory(options.RootDirectory);
_normalizedAreaRootDirectory = NormalizeDirectory(options.AreaRootDirectory);
}
public PageRouteModel CreateRouteModel(string relativePath, string routeTemplate)
{
var viewEnginePath = GetViewEnginePath(_normalizedRootDirectory, relativePath);
var routeModel = new PageRouteModel(relativePath, viewEnginePath);
PopulateRouteModel(routeModel, viewEnginePath, routeTemplate);
return routeModel;
}
public PageRouteModel CreateAreaRouteModel(string relativePath, string routeTemplate)
{
if (!TryParseAreaPath(relativePath, out var areaResult))
{
return null;
}
var routeModel = new PageRouteModel(relativePath, areaResult.viewEnginePath);
var routePrefix = CreateAreaRoute(areaResult.areaName, areaResult.viewEnginePath);
PopulateRouteModel(routeModel, routePrefix, routeTemplate);
routeModel.RouteValues["area"] = areaResult.areaName;
return routeModel;
}
private static void PopulateRouteModel(PageRouteModel model, string pageRoute, string routeTemplate)
{
model.RouteValues.Add("page", model.ViewEnginePath);
if (AttributeRouteModel.IsOverridePattern(routeTemplate))
{
throw new InvalidOperationException(string.Format(
Resources.PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable,
model.RelativePath));
}
var selectorModel = CreateSelectorModel(pageRoute, routeTemplate);
model.Selectors.Add(selectorModel);
var fileName = Path.GetFileName(model.RelativePath);
if (string.Equals(IndexFileName, fileName, StringComparison.OrdinalIgnoreCase))
{
// For pages ending in /Index.cshtml, we want to allow incoming routing, but
// force outgoing routes to match to the path sans /Index.
selectorModel.AttributeRouteModel.SuppressLinkGeneration = true;
var index = pageRoute.LastIndexOf('/');
var parentDirectoryPath = index == -1 ?
string.Empty :
pageRoute.Substring(0, index);
model.Selectors.Add(CreateSelectorModel(parentDirectoryPath, routeTemplate));
}
}
// Internal for unit testing
internal bool TryParseAreaPath(
string relativePath,
out (string areaName, string viewEnginePath) result)
{
// path = "/Areas/Products/Pages/Manage/Home.cshtml"
// Result ("Products", "/Manage/Home")
result = default;
Debug.Assert(relativePath.StartsWith("/", StringComparison.Ordinal));
// Parse the area root directory.
var areaRootEndIndex = relativePath.IndexOf('/', startIndex: 1);
if (areaRootEndIndex == -1 ||
areaRootEndIndex >= relativePath.Length - 1 || // There's at least one token after the area root.
!relativePath.StartsWith(_normalizedAreaRootDirectory, StringComparison.OrdinalIgnoreCase)) // The path must start with area root.
{
_logger.UnsupportedAreaPath(_options, relativePath);
return false;
}
// The first directory that follows the area root is the area name.
var areaEndIndex = relativePath.IndexOf('/', startIndex: areaRootEndIndex + 1);
if (areaEndIndex == -1 || areaEndIndex == relativePath.Length)
{
_logger.UnsupportedAreaPath(_options, relativePath);
return false;
}
var areaName = relativePath.Substring(areaRootEndIndex + 1, areaEndIndex - areaRootEndIndex - 1);
string viewEnginePath;
if (_options.RootDirectory == "/")
{
// When RootDirectory is "/", every thing past the area name is the page path.
Debug.Assert(relativePath.EndsWith(RazorViewEngine.ViewExtension), $"{relativePath} does not end in extension '{RazorViewEngine.ViewExtension}'.");
viewEnginePath = relativePath.Substring(areaEndIndex, relativePath.Length - areaEndIndex - RazorViewEngine.ViewExtension.Length);
}
else
{
// Normalize the pages root directory so that it has a trailing slash. This ensures we're looking at a directory delimiter
// and not just the area name occuring as part of a segment.
Debug.Assert(_options.RootDirectory.StartsWith("/", StringComparison.Ordinal));
// If the pages root has a value i.e. it's not the app root "/", ensure that the area path contains this value.
if (string.Compare(relativePath, areaEndIndex, _normalizedRootDirectory, 0, _normalizedRootDirectory.Length, StringComparison.OrdinalIgnoreCase) != 0)
{
_logger.UnsupportedAreaPath(_options, relativePath);
return false;
}
// Include the trailing slash of the root directory at the start of the viewEnginePath
var pageNameIndex = areaEndIndex + _normalizedRootDirectory.Length - 1;
viewEnginePath = relativePath.Substring(pageNameIndex, relativePath.Length - pageNameIndex - RazorViewEngine.ViewExtension.Length);
}
result = (areaName, viewEnginePath);
return true;
}
private string GetViewEnginePath(string rootDirectory, string path)
{
// rootDirectory = "/Pages/AllMyPages/"
// path = "/Pages/AllMyPages/Home.cshtml"
// Result = "/Home"
Debug.Assert(path.StartsWith(rootDirectory, StringComparison.OrdinalIgnoreCase));
Debug.Assert(path.EndsWith(RazorViewEngine.ViewExtension, StringComparison.OrdinalIgnoreCase));
var startIndex = rootDirectory.Length - 1;
var endIndex = path.Length - RazorViewEngine.ViewExtension.Length;
return path.Substring(startIndex, endIndex - startIndex);
}
private static string CreateAreaRoute(string areaName, string viewEnginePath)
{
// AreaName = Products, ViewEnginePath = /List/Categories
// Result = /Products/List/Categories
Debug.Assert(!string.IsNullOrEmpty(areaName));
Debug.Assert(!string.IsNullOrEmpty(viewEnginePath));
Debug.Assert(viewEnginePath.StartsWith("/", StringComparison.Ordinal));
var builder = new InplaceStringBuilder(1 + areaName.Length + viewEnginePath.Length);
builder.Append('/');
builder.Append(areaName);
builder.Append(viewEnginePath);
return builder.ToString();
}
private static SelectorModel CreateSelectorModel(string prefix, string routeTemplate)
{
return new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Template = AttributeRouteModel.CombineTemplates(prefix, routeTemplate),
}
};
}
private static string NormalizeDirectory(string directory)
{
Debug.Assert(directory.StartsWith("/", StringComparison.Ordinal));
if (directory.Length > 1 && !directory.EndsWith("/", StringComparison.Ordinal))
{
return directory + "/";
}
return directory;
}
}
}

View File

@ -1,119 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using 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 static readonly string IndexFileName = "Index" + RazorViewEngine.ViewExtension;
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(
Resources.PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable,
model.RelativePath));
}
var selectorModel = CreateSelectorModel(pageRoute, routeTemplate);
model.Selectors.Add(selectorModel);
var fileName = Path.GetFileName(model.RelativePath);
if (string.Equals(IndexFileName, fileName, StringComparison.OrdinalIgnoreCase))
{
// For pages ending in /Index.cshtml, we want to allow incoming routing, but
// force outgoing routes to match to the path sans /Index.
selectorModel.AttributeRouteModel.SuppressLinkGeneration = true;
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));
// 1. Parse the area name. This will be the first token we encounter.
var areaEndIndex = path.IndexOf('/', startIndex: 1);
if (areaEndIndex == -1 || areaEndIndex == path.Length)
{
logger.UnsupportedAreaPath(razorPagesOptions, path);
return false;
}
var areaName = path.Substring(1, areaEndIndex - 1);
string pageName;
if (razorPagesOptions.RootDirectory == "/")
{
// When RootDirectory is "/", every thing past the area name is the page path.
Debug.Assert(path.EndsWith(RazorViewEngine.ViewExtension), $"{path} does not end in extension '{RazorViewEngine.ViewExtension}'.");
pageName = path.Substring(areaEndIndex, path.Length - areaEndIndex - RazorViewEngine.ViewExtension.Length);
}
else
{
// Normalize the pages root directory so that it has a trailing slash. This ensures we're looking at a directory delimiter
// and not just the area name occuring as part of a segment.
Debug.Assert(razorPagesOptions.RootDirectory.StartsWith("/", StringComparison.Ordinal));
var normalizedPagesRootDirectory = razorPagesOptions.RootDirectory.Substring(1);
if (!normalizedPagesRootDirectory.EndsWith("/", StringComparison.Ordinal))
{
normalizedPagesRootDirectory += "/";
}
Debug.Assert(normalizedPagesRootDirectory.Length > 0);
// If the pages root has a value i.e. it's not the app root "/", ensure that the area path contains this value.
if (string.Compare(path, areaEndIndex + 1, normalizedPagesRootDirectory, 0, normalizedPagesRootDirectory.Length, StringComparison.OrdinalIgnoreCase) != 0)
{
logger.UnsupportedAreaPath(razorPagesOptions, path);
return false;
}
var pageNameIndex = areaEndIndex + normalizedPagesRootDirectory.Length;
pageName = path.Substring(pageNameIndex, path.Length - pageNameIndex - 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
{
AttributeRouteModel = new AttributeRouteModel
{
Template = AttributeRouteModel.CombineTemplates(prefix, template),
}
};
}
}
}

View File

@ -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.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Razor.Language;
@ -14,7 +15,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
private readonly RazorProject _project;
private readonly RazorPagesOptions _pagesOptions;
private readonly ILogger _logger;
private readonly PageRouteModelFactory _routeModelFactory;
private readonly ILogger<RazorProjectPageRouteModelProvider> _logger;
public RazorProjectPageRouteModelProvider(
RazorProject razorProject,
@ -24,12 +26,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
_project = razorProject;
_pagesOptions = pagesOptionsAccessor.Value;
_logger = loggerFactory.CreateLogger<RazorProjectPageRouteModelProvider>();
_routeModelFactory = new PageRouteModelFactory(_pagesOptions, _logger);
}
/// <remarks>
/// Ordered to execute after <see cref="CompiledPageRouteModelProvider"/>.
/// </remarks>
public int Order => -1000 + 10;
public int Order => -1000 + 10;
public void OnProvidersExecuted(PageRouteModelProviderContext context)
{
@ -40,6 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
// When RootDirectory and AreaRootDirectory overlap, e.g. RootDirectory = /, AreaRootDirectoryy = /Areas;
// we need to ensure that the page is only route-able via the area route. By adding area routes first,
// we'll ensure non area routes get skipped when it encounters an IsAlreadyRegistered check.
if (_pagesOptions.AllowAreas)
{
AddAreaPageModels(context);
@ -64,10 +68,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
}
var relativePath = item.CombinedPath;
if (IsAlreadyRegistered(context, relativePath))
if (context.RouteModels.Any(m => string.Equals(relativePath, m.RelativePath, StringComparison.OrdinalIgnoreCase)))
{
// A route for this file was already registered either by the CompiledPageRouteModel or as an area route.
// by this provider. Skip registering an additional entry.
// Note: We're comparing duplicates based on root-relative paths. This eliminates a page from being discovered
// by overlapping area and non-area routes where ViewEnginePath would be different.
continue;
}
@ -85,9 +92,11 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
continue;
}
var routeModel = new PageRouteModel(relativePath, viewEnginePath: item.FilePathWithoutExtension);
PageSelectorModel.PopulateDefaults(routeModel, routeModel.ViewEnginePath, routeTemplate);
context.RouteModels.Add(routeModel);
var routeModel = _routeModelFactory.CreateRouteModel(relativePath, routeTemplate);
if (routeModel != null)
{
context.RouteModels.Add(routeModel);
}
}
}
@ -101,7 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
}
var relativePath = item.CombinedPath;
if (IsAlreadyRegistered(context, relativePath))
if (context.RouteModels.Any(m => string.Equals(relativePath, m.RelativePath, StringComparison.OrdinalIgnoreCase)))
{
// A route for this file was already registered either by the CompiledPageRouteModel.
// Skip registering an additional entry.
@ -114,36 +123,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
continue;
}
if (!PageSelectorModel.TryParseAreaPath(_pagesOptions, item.FilePath, _logger, out var areaResult))
var routeModel = _routeModelFactory.CreateAreaRouteModel(relativePath, routeTemplate);
if (routeModel != null)
{
continue;
}
var routeModel = new PageRouteModel(relativePath, viewEnginePath: areaResult.viewEnginePath)
{
RouteValues =
{
["area"] = areaResult.areaName,
},
};
PageSelectorModel.PopulateDefaults(routeModel, areaResult.pageRoute, routeTemplate);
context.RouteModels.Add(routeModel);
}
}
private bool IsAlreadyRegistered(PageRouteModelProviderContext context, string relativePath)
{
for (var i = 0; i < context.RouteModels.Count; i++)
{
var existingRouteModel = context.RouteModels[i];
if (string.Equals(relativePath, existingRouteModel.RelativePath, StringComparison.OrdinalIgnoreCase))
{
return true;
context.RouteModels.Add(routeModel);
}
}
return false;
}
private static bool IsRouteable(RazorProjectItem item)

View File

@ -24,42 +24,21 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class CompiledPageRouteModelProviderTest
{
public CompiledPageRouteModelProviderTest()
{
FileProvider = new TestFileProvider();
Project = new FileProviderRazorProject(
Mock.Of<IRazorViewEngineFileProviderAccessor>(a => a.FileProvider == FileProvider),
Mock.Of<IHostingEnvironment>(e => e.ContentRootPath == "BasePath"));
TemplateEngine = new RazorTemplateEngine(RazorEngine.Create(), Project);
PagesOptions = new RazorPagesOptions();
Provider = new TestCompiledPageRouteModelProvider(new ApplicationPartManager(), Options.Create(PagesOptions), TemplateEngine, NullLoggerFactory.Instance);
}
public TestFileProvider FileProvider { get; }
public RazorProject Project { get; }
public RazorTemplateEngine TemplateEngine { get; }
public RazorPagesOptions PagesOptions { get; }
public TestCompiledPageRouteModelProvider Provider { get; }
[Fact]
public void OnProvidersExecuting_AddsModelsForCompiledViews()
{
// Arrange
Provider.Descriptors.AddRange(new[]
var descriptors = new[]
{
CreateVersion_2_0_Descriptor("/Pages/About.cshtml"),
CreateVersion_2_0_Descriptor("/Pages/Home.cshtml", "some-prefix"),
});
};
var provider = CreateProvider(descriptors: descriptors);
var context = new PageRouteModelProviderContext();
// Act
Provider.OnProvidersExecuting(context);
provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(
@ -100,16 +79,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public void OnProvidersExecuting_AddsModelsForCompiledViews_Version_2_1()
{
// Arrange
Provider.Descriptors.AddRange(new[]
var descriptors = new[]
{
CreateVersion_2_1_Descriptor("/Pages/About.cshtml"),
CreateVersion_2_1_Descriptor("/Pages/Home.cshtml", "some-prefix"),
});
};
var provider = CreateProvider(descriptors: descriptors);
var context = new PageRouteModelProviderContext();
// Act
Provider.OnProvidersExecuting(context);
provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(
@ -150,20 +130,22 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public void OnProvidersExecuting_ValidatesChecksum_RejectsPageWhenContentDoesntMatch()
{
// Arrange
Provider.Descriptors.AddRange(new[]
var descriptors = new[]
{
CreateVersion_2_1_Descriptor("/Pages/About.cshtml", metadata: new object[]
{
new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), "/Pages/About.cshtml"),
}),
});
};
FileProvider.AddFile("/Pages/About.cshtml", "some other content");
var fileProvider = new TestFileProvider();
fileProvider.AddFile("/Pages/About.cshtml", "some other content");
var provider = CreateProvider(descriptors: descriptors, fileProvider: fileProvider);
var context = new PageRouteModelProviderContext();
// Act
Provider.OnProvidersExecuting(context);
provider.OnProvidersExecuting(context);
// Assert
Assert.Empty(context.RouteModels);
@ -173,22 +155,24 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public void OnProvidersExecuting_ValidatesChecksum_AcceptsPageWhenContentMatches()
{
// Arrange
Provider.Descriptors.AddRange(new[]
var descriptors = new[]
{
CreateVersion_2_1_Descriptor("/Pages/About.cshtml", metadata: new object[]
{
new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), "/Pages/About.cshtml"),
new RazorSourceChecksumAttribute("SHA1", GetChecksum("some import"), "/Pages/_ViewImports.cshtml"),
}),
});
};
FileProvider.AddFile("/Pages/About.cshtml", "some content");
FileProvider.AddFile("/Pages/_ViewImports.cshtml", "some import");
var fileProvider = new TestFileProvider();
fileProvider.AddFile("/Pages/About.cshtml", "some content");
fileProvider.AddFile("/Pages/_ViewImports.cshtml", "some import");
var provider = CreateProvider(descriptors: descriptors, fileProvider: fileProvider);
var context = new PageRouteModelProviderContext();
// Act
Provider.OnProvidersExecuting(context);
provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(
@ -200,21 +184,23 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public void OnProvidersExecuting_ValidatesChecksum_SkipsValidationWhenMainSourceMissing()
{
// Arrange
Provider.Descriptors.AddRange(new[]
var descriptors = new[]
{
CreateVersion_2_1_Descriptor("/Pages/About.cshtml", metadata: new object[]
{
new RazorSourceChecksumAttribute("SHA1", GetChecksum("some content"), "/Pages/About.cshtml"),
new RazorSourceChecksumAttribute("SHA1", GetChecksum("some import"), "/Pages/_ViewImports.cshtml"),
}),
});
};
FileProvider.AddFile("/Pages/_ViewImports.cshtml", "some other import");
var fileProvider = new TestFileProvider();
fileProvider.AddFile("/Pages/_ViewImports.cshtml", "some other import");
var provider = CreateProvider(descriptors: descriptors, fileProvider: fileProvider);
var context = new PageRouteModelProviderContext();
// Act
Provider.OnProvidersExecuting(context);
provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(
@ -226,21 +212,25 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public void OnProvidersExecuting_AddsModelsForCompiledAreaPages()
{
// Arrange
Provider.Descriptors.AddRange(new[]
var descriptors = new[]
{
CreateVersion_2_0_Descriptor("/Features/Products/Files/About.cshtml"),
CreateVersion_2_0_Descriptor("/Features/Products/Files/Manage/Index.cshtml"),
CreateVersion_2_0_Descriptor("/Features/Products/Files/Manage/Edit.cshtml", "{id}"),
});
};
PagesOptions.AllowAreas = true;
PagesOptions.AreaRootDirectory = "/Features";
PagesOptions.RootDirectory = "/Files";
var options = new RazorPagesOptions
{
AllowAreas = true,
AreaRootDirectory = "/Features",
RootDirectory = "/Files",
};
var provider = CreateProvider(options: options, descriptors: descriptors);
var context = new PageRouteModelProviderContext();
// Act
Provider.OnProvidersExecuting(context);
provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(
@ -311,18 +301,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public void OnProvidersExecuting_DoesNotAddsModelsForAreaPages_IfFeatureIsDisabled()
{
// Arrange
Provider.Descriptors.AddRange(new[]
var descriptors = new[]
{
CreateVersion_2_0_Descriptor("/Pages/About.cshtml"),
CreateVersion_2_0_Descriptor("/Areas/Accounts/Pages/Home.cshtml"),
});
};
PagesOptions.AllowAreas = false;
var options = new RazorPagesOptions { AllowAreas = false };
var provider = CreateProvider(options: options, descriptors: descriptors);
var context = new PageRouteModelProviderContext();
// Act
Provider.OnProvidersExecuting(context);
provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(
@ -348,21 +339,25 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public void OnProvidersExecuting_DoesNotAddAreaAndNonAreaRoutesForAPage()
{
// Arrange
Provider.Descriptors.AddRange(new[]
var descriptors = new[]
{
CreateVersion_2_0_Descriptor("/Areas/Accounts/Manage/Home.cshtml"),
CreateVersion_2_0_Descriptor("/Areas/About.cshtml"),
CreateVersion_2_0_Descriptor("/Contact.cshtml"),
});
};
PagesOptions.AllowAreas = true;
PagesOptions.AreaRootDirectory = "/Areas";
PagesOptions.RootDirectory = "/";
var options = new RazorPagesOptions
{
AllowAreas = true,
AreaRootDirectory = "/Areas",
RootDirectory = "/",
};
var provider = CreateProvider(options: options, descriptors: descriptors);
var context = new PageRouteModelProviderContext();
// Act
Provider.OnProvidersExecuting(context);
provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(
@ -408,18 +403,18 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPage_WithIndexAtRoot()
{
// Arrange
Provider.Descriptors.AddRange(new[]
var descriptors = new[]
{
CreateVersion_2_0_Descriptor("/Pages/Index.cshtml"),
CreateVersion_2_0_Descriptor("/Pages/Admin/Index.cshtml", "some-template"),
});
PagesOptions.RootDirectory = "/";
};
var options = new RazorPagesOptions { RootDirectory = "/" };
var provider = CreateProvider(options: options, descriptors: descriptors);
var context = new PageRouteModelProviderContext();
// Act
Provider.OnProvidersExecuting(context);
provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(
@ -448,16 +443,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPage()
{
// Arrange
Provider.Descriptors.AddRange(new[]
var descriptors = new[]
{
CreateVersion_2_0_Descriptor("/Pages/Index.cshtml"),
CreateVersion_2_0_Descriptor("/Pages/Admin/Index.cshtml", "some-template"),
});
};
var provider = CreateProvider(descriptors: descriptors);
var context = new PageRouteModelProviderContext();
// Act
Provider.OnProvidersExecuting(context);
provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(
@ -486,21 +482,45 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
public void OnProvidersExecuting_ThrowsIfRouteTemplateHasOverridePattern()
{
// Arrange
Provider.Descriptors.AddRange(new[]
var descriptors = new[]
{
CreateVersion_2_0_Descriptor("/Pages/Index.cshtml"),
CreateVersion_2_0_Descriptor("/Pages/Home.cshtml", "/some-prefix"),
});
};
var provider = CreateProvider(descriptors: descriptors);
var context = new PageRouteModelProviderContext();
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => Provider.OnProvidersExecuting(context));
var exception = Assert.Throws<InvalidOperationException>(() => provider.OnProvidersExecuting(context));
Assert.Equal(
"The route for the page at '/Pages/Home.cshtml' cannot start with / or ~/. Pages do not support overriding the file path of the page.",
exception.Message);
}
private TestCompiledPageRouteModelProvider CreateProvider(
RazorPagesOptions options = null,
IList<CompiledViewDescriptor> descriptors = null,
TestFileProvider fileProvider = null)
{
options = options ?? new RazorPagesOptions();
fileProvider = fileProvider ?? new TestFileProvider();
var project = new FileProviderRazorProject(
Mock.Of<IRazorViewEngineFileProviderAccessor>(a => a.FileProvider == fileProvider),
Mock.Of<IHostingEnvironment>(e => e.ContentRootPath == "BasePath"));
var templateEngine = new RazorTemplateEngine(RazorEngine.Create(), project);
var provider = new TestCompiledPageRouteModelProvider(
new ApplicationPartManager(),
Options.Create(options),
templateEngine,
NullLogger<CompiledPageRouteModelProvider>.Instance);
provider.Descriptors.AddRange(descriptors ?? Array.Empty<CompiledViewDescriptor>());
return provider;
}
private static CompiledViewDescriptor CreateVersion_2_0_Descriptor(string path, string routeTemplate = "")
{
return new CompiledViewDescriptor
@ -529,8 +549,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
ApplicationPartManager partManager,
IOptions<RazorPagesOptions> options,
RazorTemplateEngine templateEngine,
ILoggerFactory loggerFactory)
: base(partManager, options, templateEngine, loggerFactory)
ILogger<CompiledPageRouteModelProvider> logger)
: base(partManager, options, templateEngine, logger)
{
}
@ -539,4 +559,4 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
protected override IEnumerable<CompiledViewDescriptor> GetViewDescriptors(ApplicationPartManager applicationManager) => Descriptors;
}
}
}
}

View File

@ -0,0 +1,219 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class PageRouteModelFactoryTest
{
[Fact]
public void CreateRouteModel_AddsSelector()
{
// Arrange
var relativePath = "/Pages/Users/Profile.cshtml";
var options = new RazorPagesOptions();
var routeModelFactory = new PageRouteModelFactory(options, NullLogger.Instance);
// Act
var routeModel = routeModelFactory.CreateRouteModel(relativePath, "{id?}");
// Assert
Assert.Equal(relativePath, routeModel.RelativePath);
Assert.Equal("/Users/Profile", routeModel.ViewEnginePath);
Assert.Collection(
routeModel.Selectors,
selector => Assert.Equal("Users/Profile/{id?}", selector.AttributeRouteModel.Template));
Assert.Collection(
routeModel.RouteValues,
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Users/Profile", kvp.Value);
});
}
[Fact]
public void CreateRouteModel_AddsMultipleSelectorsForIndexPage()
{
// Arrange
var relativePath = "/Pages/Users/Profile/Index.cshtml";
var options = new RazorPagesOptions();
var routeModelFactory = new PageRouteModelFactory(options, NullLogger.Instance);
// Act
var routeModel = routeModelFactory.CreateRouteModel(relativePath, "{id?}");
// Assert
Assert.Equal(relativePath, routeModel.RelativePath);
Assert.Equal("/Users/Profile/Index", routeModel.ViewEnginePath);
Assert.Collection(
routeModel.Selectors,
selector => Assert.Equal("Users/Profile/Index/{id?}", selector.AttributeRouteModel.Template),
selector => Assert.Equal("Users/Profile/{id?}", selector.AttributeRouteModel.Template));
Assert.Collection(
routeModel.RouteValues,
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Users/Profile/Index", kvp.Value);
});
}
[Fact]
public void CreateAreaRouteModel_AddsSelector()
{
// Arrange
var relativePath = "/Areas/TestArea/Pages/Users/Profile.cshtml";
var options = new RazorPagesOptions();
var routeModelFactory = new PageRouteModelFactory(options, NullLogger.Instance);
// Act
var routeModel = routeModelFactory.CreateAreaRouteModel(relativePath, "{id?}");
// Assert
Assert.Equal(relativePath, routeModel.RelativePath);
Assert.Equal("/Users/Profile", routeModel.ViewEnginePath);
Assert.Collection(
routeModel.Selectors,
selector => Assert.Equal("TestArea/Users/Profile/{id?}", selector.AttributeRouteModel.Template));
Assert.Collection(
routeModel.RouteValues.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal("TestArea", kvp.Value);
},
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Users/Profile", kvp.Value);
});
}
[Fact]
public void CreateAreaRouteModel_AddsMultipleSelectorsForIndexPage()
{
// Arrange
var relativePath = "/Areas/TestArea/Pages/Users/Profile/Index.cshtml";
var options = new RazorPagesOptions();
var routeModelFactory = new PageRouteModelFactory(options, NullLogger.Instance);
// Act
var routeModel = routeModelFactory.CreateAreaRouteModel(relativePath, "{id?}");
// Assert
Assert.Equal(relativePath, routeModel.RelativePath);
Assert.Equal("/Users/Profile/Index", routeModel.ViewEnginePath);
Assert.Collection(
routeModel.Selectors,
selector => Assert.Equal("TestArea/Users/Profile/Index/{id?}", selector.AttributeRouteModel.Template),
selector => Assert.Equal("TestArea/Users/Profile/{id?}", selector.AttributeRouteModel.Template));
Assert.Collection(
routeModel.RouteValues.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal("TestArea", kvp.Value);
},
kvp =>
{
Assert.Equal("page", kvp.Key);
Assert.Equal("/Users/Profile/Index", kvp.Value);
});
}
[ConditionalTheory]
[FrameworkSkipCondition(RuntimeFrameworks.CLR, SkipReason = "Fails due to dotnet/standard#567")]
[InlineData("/Areas/About.cshtml")]
[InlineData("/Areas/MyArea/Index.cshtml")]
public void TryParseAreaPath_ReturnsFalse_IfPathDoesNotConform(string path)
{
// Arrange
var options = new RazorPagesOptions();
var routeModelFactory = new PageRouteModelFactory(options, NullLogger.Instance);
// Act
var success = routeModelFactory.TryParseAreaPath(path, out _);
// Assert
Assert.False(success);
}
[ConditionalTheory]
[FrameworkSkipCondition(RuntimeFrameworks.CLR, SkipReason = "Fails due to dotnet/standard#567")]
[InlineData("/Areas/MyArea/Views/About.cshtml")]
[InlineData("/Areas/MyArea/SubDir/Pages/Index.cshtml")]
[InlineData("/Areas/MyArea/NotPages/SubDir/About.cshtml")]
public void TryParseAreaPath_ReturnsFalse_IfPathDoesNotBelongToRootDirectory(string path)
{
// Arrange
var options = new RazorPagesOptions();
var routeModelFactory = new PageRouteModelFactory(options, NullLogger.Instance);
// Act
var success = routeModelFactory.TryParseAreaPath(path, out _);
// Assert
Assert.False(success);
}
[ConditionalTheory]
[FrameworkSkipCondition(RuntimeFrameworks.CLR, SkipReason = "Fails due to dotnet/standard#567")]
[InlineData("/Areas/MyArea/Pages/Index.cshtml", "MyArea", "/Index")]
[InlineData("/Areas/Accounts/Pages/Manage/Edit.cshtml", "Accounts", "/Manage/Edit")]
public void TryParseAreaPath_ParsesAreaPath(
string path,
string expectedArea,
string expectedViewEnginePath)
{
// Arrange
var options = new RazorPagesOptions();
var routeModelFactory = new PageRouteModelFactory(options, NullLogger.Instance);
// Act
var success = routeModelFactory.TryParseAreaPath(path, out var result);
// Assert
Assert.True(success);
Assert.Equal(expectedArea, result.areaName);
Assert.Equal(expectedViewEnginePath, result.viewEnginePath);
}
[ConditionalTheory]
[FrameworkSkipCondition(RuntimeFrameworks.CLR, SkipReason = "Fails due to dotnet/standard#567")]
[InlineData("/Areas/MyArea/Dir1/Dir2/Index.cshtml", "MyArea", "/Index")]
[InlineData("/Areas/Accounts/Dir1/Dir2/Manage/Edit.cshtml", "Accounts", "/Manage/Edit")]
public void TryParseAreaPath_ParsesAreaPath_WithMultiLevelRootDirectory(
string path,
string expectedArea,
string expectedViewEnginePath)
{
// Arrange
var options = new RazorPagesOptions
{
RootDirectory = "/Dir1/Dir2"
};
var routeModelFactory = new PageRouteModelFactory(options, NullLogger.Instance);
// Act
var success = routeModelFactory.TryParseAreaPath(path, out var result);
// Assert
Assert.True(success);
Assert.Equal(expectedArea, result.areaName);
Assert.Equal(expectedViewEnginePath, result.viewEnginePath);
}
}
}

View File

@ -1,94 +0,0 @@
// 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.Testing.xunit;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class PageSelectorModelTest
{
[ConditionalTheory]
[FrameworkSkipCondition(RuntimeFrameworks.CLR, SkipReason = "Fails due to dotnet/standard#567")]
[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);
}
[ConditionalTheory]
[FrameworkSkipCondition(RuntimeFrameworks.CLR, SkipReason = "Fails due to dotnet/standard#567")]
[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);
}
[ConditionalTheory]
[FrameworkSkipCondition(RuntimeFrameworks.CLR, SkipReason = "Fails due to dotnet/standard#567")]
[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);
}
[ConditionalTheory]
[FrameworkSkipCondition(RuntimeFrameworks.CLR, SkipReason = "Fails due to dotnet/standard#567")]
[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);
}
}
}