From 094b61dfc69c2b1e3bd68e4b7f3e342170b377fe Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 12 Jan 2018 11:51:22 -0800 Subject: [PATCH] Refactor PageRouteModel generation --- korebuild-lock.txt | 4 +- .../CompiledPageRouteModelProvider.cs | 127 +++------- .../Internal/PageRouteModelFactory.cs | 196 ++++++++++++++++ .../Internal/PageSelectorModel.cs | 119 ---------- .../RazorProjectPageRouteModelProvider.cs | 55 ++--- .../CompiledPageRouteModelProviderTest.cs | 162 +++++++------ .../Internal/PageRouteModelFactoryTest.cs | 219 ++++++++++++++++++ .../Internal/PageSelectorModelTest.cs | 94 -------- 8 files changed, 562 insertions(+), 414 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteModelFactory.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSelectorModel.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageRouteModelFactoryTest.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageSelectorModelTest.cs diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 2146d006d7..181b38dcfe 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview1-15661 -commithash:c9349d4c8a495d3085d9b879214d80f2f45e2193 +version:2.1.0-preview1-15670 +commithash:49176144e03c3015d83b21e3f1d0ce093c05ecc3 diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageRouteModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageRouteModelProvider.cs index 032f868abd..5e9cd2d38c 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageRouteModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageRouteModelProvider.cs @@ -22,37 +22,19 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal private readonly RazorPagesOptions _pagesOptions; private readonly RazorTemplateEngine _templateEngine; private readonly ILogger _logger; + private readonly PageRouteModelFactory _routeModelFactory; public CompiledPageRouteModelProvider( ApplicationPartManager applicationManager, IOptions pagesOptionsAccessor, RazorTemplateEngine templateEngine, - ILoggerFactory loggerFactory) + ILogger 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(); + _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 results) + /// + /// Gets the sequence of from . + /// + /// The s + /// The sequence of . + protected virtual IEnumerable 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; - } - - /// - /// Gets the sequence of from . - /// - /// The s - /// The sequence of . - protected virtual IEnumerable 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); - } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteModelFactory.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteModelFactory.cs new file mode 100644 index 0000000000..0b75e6a22d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageRouteModelFactory.cs @@ -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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSelectorModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSelectorModel.cs deleted file mode 100644 index c0ee83d144..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSelectorModel.cs +++ /dev/null @@ -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), - } - }; - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorProjectPageRouteModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorProjectPageRouteModelProvider.cs index d57f471aa8..a6201331bd 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorProjectPageRouteModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorProjectPageRouteModelProvider.cs @@ -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 _logger; public RazorProjectPageRouteModelProvider( RazorProject razorProject, @@ -24,12 +26,13 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal _project = razorProject; _pagesOptions = pagesOptionsAccessor.Value; _logger = loggerFactory.CreateLogger(); + _routeModelFactory = new PageRouteModelFactory(_pagesOptions, _logger); } - + /// /// Ordered to execute after . /// - 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) diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageRouteModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageRouteModelProviderTest.cs index 3aeebad54f..011681f152 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageRouteModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageRouteModelProviderTest.cs @@ -24,42 +24,21 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { public class CompiledPageRouteModelProviderTest { - public CompiledPageRouteModelProviderTest() - { - FileProvider = new TestFileProvider(); - Project = new FileProviderRazorProject( - Mock.Of(a => a.FileProvider == FileProvider), - Mock.Of(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(() => Provider.OnProvidersExecuting(context)); + var exception = Assert.Throws(() => 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 descriptors = null, + TestFileProvider fileProvider = null) + { + options = options ?? new RazorPagesOptions(); + fileProvider = fileProvider ?? new TestFileProvider(); + var project = new FileProviderRazorProject( + Mock.Of(a => a.FileProvider == fileProvider), + Mock.Of(e => e.ContentRootPath == "BasePath")); + var templateEngine = new RazorTemplateEngine(RazorEngine.Create(), project); + + var provider = new TestCompiledPageRouteModelProvider( + new ApplicationPartManager(), + Options.Create(options), + templateEngine, + NullLogger.Instance); + + provider.Descriptors.AddRange(descriptors ?? Array.Empty()); + + 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 options, RazorTemplateEngine templateEngine, - ILoggerFactory loggerFactory) - : base(partManager, options, templateEngine, loggerFactory) + ILogger logger) + : base(partManager, options, templateEngine, logger) { } @@ -539,4 +559,4 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal protected override IEnumerable GetViewDescriptors(ApplicationPartManager applicationManager) => Descriptors; } } -} +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageRouteModelFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageRouteModelFactoryTest.cs new file mode 100644 index 0000000000..7e8aeeb734 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageRouteModelFactoryTest.cs @@ -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); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageSelectorModelTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageSelectorModelTest.cs deleted file mode 100644 index 7979a3f952..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageSelectorModelTest.cs +++ /dev/null @@ -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); - } - } -}