diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewManfiest.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewManfiest.cs new file mode 100644 index 0000000000..622d5ee6f8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewManfiest.cs @@ -0,0 +1,60 @@ +// 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.IO; +using System.Reflection; +#if NETSTANDARD1_6 +using System.Runtime.Loader; +#endif +using Microsoft.AspNetCore.Mvc.ApplicationParts; + +namespace Microsoft.AspNetCore.Mvc.Razor.Compilation +{ + public static class CompiledViewManfiest + { + public static readonly string PrecompiledViewsAssemblySuffix = ".PrecompiledViews"; + + public static Type GetManifestType(AssemblyPart assemblyPart, string typeName) + { + EnsureFeatureAssembly(assemblyPart); + + var precompiledAssemblyName = new AssemblyName(assemblyPart.Assembly.FullName); + precompiledAssemblyName.Name = precompiledAssemblyName.Name + PrecompiledViewsAssemblySuffix; + + return Type.GetType($"{typeName},{precompiledAssemblyName}"); + } + + private static void EnsureFeatureAssembly(AssemblyPart assemblyPart) + { +#if NETSTANDARD1_6 + if (assemblyPart.Assembly.IsDynamic || string.IsNullOrEmpty(assemblyPart.Assembly.Location)) + { + return; + } + + var precompiledAssemblyFileName = assemblyPart.Assembly.GetName().Name + + PrecompiledViewsAssemblySuffix + + ".dll"; + var precompiledAssemblyFilePath = Path.Combine( + Path.GetDirectoryName(assemblyPart.Assembly.Location), + precompiledAssemblyFileName); + + if (File.Exists(precompiledAssemblyFilePath)) + { + try + { + AssemblyLoadContext.Default.LoadFromAssemblyPath(precompiledAssemblyFilePath); + } + catch (FileLoadException) + { + // Don't throw if assembly cannot be loaded. This can happen if the file is not a managed assembly. + } + } +#elif NET46 +#else +#error target frameworks needs to be updated. +#endif + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs index fd2a512b5d..a87b02dde3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Reflection; using Microsoft.AspNetCore.Mvc.ApplicationParts; namespace Microsoft.AspNetCore.Mvc.Razor.Compilation @@ -15,9 +13,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation /// public class ViewsFeatureProvider : IApplicationFeatureProvider { - /// - /// Gets the suffix for the view assembly. - /// public static readonly string PrecompiledViewsAssemblySuffix = ".PrecompiledViews"; /// @@ -30,19 +25,19 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation /// public static readonly string ViewInfoContainerTypeName = "__PrecompiledViewCollection"; + private static readonly string FullyQualifiedManifestTypeName = ViewInfoContainerNamespace + "." + ViewInfoContainerTypeName; + /// public void PopulateFeature(IEnumerable parts, ViewsFeature feature) { foreach (var assemblyPart in parts.OfType()) { - var viewInfoContainerTypeName = GetViewInfoContainerType(assemblyPart); - if (viewInfoContainerTypeName == null) + var viewContainer = GetManifest(assemblyPart); + if (viewContainer == null) { continue; } - var viewContainer = (ViewInfoContainer)Activator.CreateInstance(viewInfoContainerTypeName); - foreach (var item in viewContainer.ViewInfos) { feature.Views[item.Path] = item.Type; @@ -55,40 +50,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation /// /// The . /// The . - protected virtual Type GetViewInfoContainerType(AssemblyPart assemblyPart) + protected virtual ViewInfoContainer GetManifest(AssemblyPart assemblyPart) { -#if NETSTANDARD1_6 - if (!assemblyPart.Assembly.IsDynamic && !string.IsNullOrEmpty(assemblyPart.Assembly.Location)) + var type = CompiledViewManfiest.GetManifestType(assemblyPart, FullyQualifiedManifestTypeName); + if (type != null) { - var precompiledAssemblyFileName = assemblyPart.Assembly.GetName().Name - + PrecompiledViewsAssemblySuffix - + ".dll"; - var precompiledAssemblyFilePath = Path.Combine( - Path.GetDirectoryName(assemblyPart.Assembly.Location), - precompiledAssemblyFileName); - - if (File.Exists(precompiledAssemblyFilePath)) - { - try - { - System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(precompiledAssemblyFilePath); - } - catch (FileLoadException) - { - // Don't throw if assembly cannot be loaded. This can happen if the file is not a managed assembly. - } - } + return (ViewInfoContainer)Activator.CreateInstance(type); } -#elif NET46 -#else -#error target frameworks needs to be updated. -#endif - var precompiledAssemblyName = new AssemblyName(assemblyPart.Assembly.FullName); - precompiledAssemblyName.Name = precompiledAssemblyName.Name + PrecompiledViewsAssemblySuffix; - - var typeName = $"{ViewInfoContainerNamespace}.{ViewInfoContainerTypeName},{precompiledAssemblyName}"; - return Type.GetType(typeName); + return null; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs index 38c2f86735..c7875c3b22 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs @@ -135,10 +135,8 @@ namespace Microsoft.Extensions.DependencyInjection // DependencyContextRazorViewEngineOptionsSetup needs to run after RazorViewEngineOptionsSetup. // The ordering of the following two lines is important to ensure this behavior. -#pragma warning disable 0618 services.TryAddEnumerable( ServiceDescriptor.Transient, RazorViewEngineOptionsSetup>()); -#pragma warning restore 0618 services.TryAddEnumerable( ServiceDescriptor.Transient< IConfigureOptions, diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageApplicationModelProvider.cs new file mode 100644 index 0000000000..9296955e2e --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/IPageApplicationModelProvider.cs @@ -0,0 +1,44 @@ +// 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. + +namespace Microsoft.AspNetCore.Mvc.ApplicationModels +{ + /// + /// Builds or modifies an for Razor Page discovery. + /// + public interface IPageApplicationModelProvider + { + /// + /// Gets the order value for determining the order of execution of providers. Providers execute in + /// ascending numeric value of the property. + /// + /// + /// + /// Providers are executed in an ordering determined by an ascending sort of the property. + /// A provider with a lower numeric value of will have its + /// called before that of a provider with a higher numeric value of + /// . The method is called in the reverse ordering after + /// all calls to . A provider with a lower numeric value of + /// will have its method called after that of a provider + /// with a higher numeric value of . + /// + /// + /// If two providers have the same numeric value of , then their relative execution order + /// is undefined. + /// + /// + int Order { get; } + + /// + /// Executed for the first pass of building instances. See . + /// + /// The . + void OnProvidersExecuting(PageApplicationModelProviderContext context); + + /// + /// Executed for the second pass of building instances. See . + /// + /// The . + void OnProvidersExecuted(PageApplicationModelProviderContext context); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModel.cs index a5ff863070..dd075bc91b 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModel.cs @@ -59,17 +59,17 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels } /// - /// Gets or sets the application root relative path for the page. + /// Gets the application root relative path for the page. /// public string RelativePath { get; } /// - /// Gets or sets the path relative to the base path for page discovery. + /// Gets the path relative to the base path for page discovery. /// public string ViewEnginePath { get; } /// - /// Gets or sets the applicable instances. + /// Gets the applicable instances. /// public IList Filters { get; } @@ -79,7 +79,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels public IDictionary Properties { get; } /// - /// Gets or sets the instances. + /// Gets the instances. /// public IList Selectors { get; } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModelProviderContext.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModelProviderContext.cs new file mode 100644 index 0000000000..b73f88c168 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationModels/PageApplicationModelProviderContext.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Mvc.ApplicationModels +{ + /// + /// A context object for . + /// + public class PageApplicationModelProviderContext + { + /// + /// Gets the instances. + /// + public IList Results { get; } = new List(); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageFeature.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageFeature.cs new file mode 100644 index 0000000000..b2efd6ce9b --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageFeature.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + public class CompiledPageInfoFeature + { + public IList CompiledPages { get; } = new List(); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageFeatureProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageFeatureProvider.cs new file mode 100644 index 0000000000..582790f1b8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageFeatureProvider.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + /// + /// An for . + /// + public class CompiledPageFeatureProvider : IApplicationFeatureProvider + { + /// + /// Gets the namespace for the type in the view assembly. + /// + public static readonly string CompiledPageManifestNamespace = "AspNetCore"; + + /// + /// Gets the type name for the view collection type in the view assembly. + /// + public static readonly string CompiledPageManifestTypeName = "__CompiledRazorPagesManifest"; + + private static readonly string FullyQualifiedManifestTypeName = + CompiledPageManifestNamespace + "." + CompiledPageManifestTypeName; + + /// + public void PopulateFeature(IEnumerable parts, ViewsFeature feature) + { + foreach (var item in GetCompiledPageInfo(parts)) + { + feature.Views.Add(item.Path, item.CompiledType); + } + } + + /// + /// Gets the sequence of from . + /// + /// The s + /// The sequence of . + public static IEnumerable GetCompiledPageInfo(IEnumerable parts) + { + return parts.OfType() + .Select(part => CompiledViewManfiest.GetManifestType(part, FullyQualifiedManifestTypeName)) + .Where(type => type != null) + .Select(type => (CompiledPageManifest)Activator.CreateInstance(type)) + .SelectMany(manifest => manifest.CompiledPages); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageInfo.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageInfo.cs new file mode 100644 index 0000000000..5c6418e7a6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/ApplicationParts/CompiledPageInfo.cs @@ -0,0 +1,23 @@ +// 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; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + public class CompiledPageInfo + { + public CompiledPageInfo(string path, Type compiledType, string routePrefix) + { + Path = path; + CompiledType = compiledType; + RoutePrefix = routePrefix; + } + + public string Path { get; } + + public string RoutePrefix { get; } + + public Type CompiledType { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs index 1a2c4e1cae..f37261faf8 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/DependencyInjection/MvcRazorPagesMvcCoreBuilderExtensions.cs @@ -2,9 +2,11 @@ // 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.Abstractions; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.RazorPages.Internal; @@ -24,13 +26,16 @@ namespace Microsoft.Extensions.DependencyInjection } builder.AddRazorViewEngine(); + + AddFeatureProviders(builder); AddServices(builder.Services); + return builder; } public static IMvcCoreBuilder AddRazorPages( this IMvcCoreBuilder builder, - Action setupAction) + Action setupAction) { if (builder == null) { @@ -43,6 +48,8 @@ namespace Microsoft.Extensions.DependencyInjection } builder.AddRazorViewEngine(); + + AddFeatureProviders(builder); AddServices(builder.Services); builder.Services.Configure(setupAction); @@ -50,6 +57,14 @@ namespace Microsoft.Extensions.DependencyInjection return builder; } + private static void AddFeatureProviders(IMvcCoreBuilder builder) + { + if (!builder.PartManager.FeatureProviders.OfType().Any()) + { + builder.PartManager.FeatureProviders.Add(new CompiledPageFeatureProvider()); + } + } + // Internal for testing. internal static void AddServices(IServiceCollection services) { @@ -57,10 +72,14 @@ namespace Microsoft.Extensions.DependencyInjection services.TryAddEnumerable( ServiceDescriptor.Transient, RazorPagesOptionsSetup>()); - // Action Invoker + // Action description and invocation services.TryAddEnumerable( ServiceDescriptor.Singleton()); services.TryAddSingleton(); + services.TryAddEnumerable( + ServiceDescriptor.Singleton()); + services.TryAddEnumerable( + ServiceDescriptor.Singleton()); services.TryAddEnumerable( ServiceDescriptor.Singleton()); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/CompiledPageManifest.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/CompiledPageManifest.cs new file mode 100644 index 0000000000..20ea60c86d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/CompiledPageManifest.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ApplicationParts; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure +{ + public abstract class CompiledPageManifest + { + /// + /// Initializes a new instance of . + /// + /// The sequence of . + public CompiledPageManifest(IReadOnlyList pages) + { + CompiledPages = pages; + } + + /// + /// The of . + /// + public IReadOnlyList CompiledPages { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs index 646dd7697d..c99f472f98 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs @@ -3,29 +3,27 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; -using Microsoft.AspNetCore.Razor.Language; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { public class PageActionDescriptorProvider : IActionDescriptorProvider { - private static readonly string IndexFileName = "Index.cshtml"; - private readonly RazorProject _project; + private readonly List _applicationModelProviders; private readonly MvcOptions _mvcOptions; private readonly RazorPagesOptions _pagesOptions; public PageActionDescriptorProvider( - RazorProject project, + IEnumerable pageMetadataProviders, IOptions mvcOptionsAccessor, IOptions pagesOptionsAccessor) { - _project = project; + _applicationModelProviders = pageMetadataProviders.OrderBy(p => p.Order).ToList(); _mvcOptions = mvcOptionsAccessor.Value; _pagesOptions = pagesOptionsAccessor.Value; } @@ -34,57 +32,37 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure public void OnProvidersExecuting(ActionDescriptorProviderContext context) { - foreach (var item in _project.EnumerateItems(_pagesOptions.RootDirectory)) + var pageApplicationModels = BuildModel(); + + for (var i = 0; i < pageApplicationModels.Count; i++) { - if (item.FileName.StartsWith("_")) - { - // Files like _ViewImports.cshtml should not be routable. - continue; - } - - string template; - if (!PageDirectiveFeature.TryGetPageDirective(item, out template)) - { - // .cshtml pages without @page are not RazorPages. - continue; - } - - if (AttributeRouteModel.IsOverridePattern(template)) - { - throw new InvalidOperationException(string.Format( - Resources.PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable, - item.Path)); - } - - AddActionDescriptors(context.Results, item, template); + AddActionDescriptors(context.Results, pageApplicationModels[i]); } } + protected IList BuildModel() + { + var context = new PageApplicationModelProviderContext(); + + for (var i = 0; i < _applicationModelProviders.Count; i++) + { + _applicationModelProviders[i].OnProvidersExecuting(context); + } + + for (var i = _applicationModelProviders.Count - 1; i >= 0; i--) + { + _applicationModelProviders[i].OnProvidersExecuted(context); + } + + return context.Results; + } + public void OnProvidersExecuted(ActionDescriptorProviderContext context) { } - private void AddActionDescriptors(IList actions, RazorProjectItem item, string template) + private void AddActionDescriptors(IList actions, PageApplicationModel model) { - var model = new PageApplicationModel(item.CombinedPath, item.PathWithoutExtension); - var routePrefix = item.PathWithoutExtension; - model.Selectors.Add(CreateSelectorModel(routePrefix, template)); - - if (string.Equals(IndexFileName, item.FileName, StringComparison.OrdinalIgnoreCase)) - { - var parentDirectoryPath = item.Path; - var index = parentDirectoryPath.LastIndexOf('/'); - if (index == -1) - { - parentDirectoryPath = string.Empty; - } - else - { - parentDirectoryPath = parentDirectoryPath.Substring(0, index); - } - model.Selectors.Add(CreateSelectorModel(parentDirectoryPath, template)); - } - for (var i = 0; i < _pagesOptions.Conventions.Count; i++) { _pagesOptions.Conventions[i].Apply(model); @@ -111,28 +89,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure Order = selector.AttributeRouteModel.Order ?? 0, Template = selector.AttributeRouteModel.Template, }, - DisplayName = $"Page: {item.Path}", + DisplayName = $"Page: {model.ViewEnginePath}", FilterDescriptors = filters, Properties = new Dictionary(model.Properties), - RelativePath = item.CombinedPath, + RelativePath = model.RelativePath, RouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "page", item.PathWithoutExtension }, + { "page", model.ViewEnginePath}, }, - ViewEnginePath = item.Path, + ViewEnginePath = model.ViewEnginePath, }); } } - - private static SelectorModel CreateSelectorModel(string prefix, string template) - { - return new SelectorModel - { - AttributeRouteModel = new AttributeRouteModel - { - Template = AttributeRouteModel.CombineTemplates(prefix, template), - } - }; - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageDirectiveFeature.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageDirectiveFeature.cs index 0b55841886..85a04cb3aa 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageDirectiveFeature.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageDirectiveFeature.cs @@ -16,16 +16,25 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure throw new ArgumentNullException(nameof(projectItem)); } + return TryGetPageDirective(projectItem.Read, out template); + } + + public static bool TryGetPageDirective(Func streamFactory, out string template) + { + if (streamFactory == null) + { + throw new ArgumentNullException(nameof(streamFactory)); + } + const string PageDirective = "@page"; - var stream = projectItem.Read(); - - string content = null; - using (var streamReader = new StreamReader(stream)) + string content; + using (var streamReader = new StreamReader(streamFactory())) { do { content = streamReader.ReadLine(); + } while (content != null && string.IsNullOrWhiteSpace(content)); content = content?.Trim(); } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageApplicationModelProvider.cs new file mode 100644 index 0000000000..d71909c43f --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/CompiledPageApplicationModelProvider.cs @@ -0,0 +1,97 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class CompiledPageApplicationModelProvider : IPageApplicationModelProvider + { + private readonly object _cacheLock = new object(); + private readonly ApplicationPartManager _applicationManager; + private readonly RazorPagesOptions _pagesOptions; + private List _cachedApplicationModels; + + public CompiledPageApplicationModelProvider( + ApplicationPartManager applicationManager, + IOptions pagesOptionsAccessor) + { + _applicationManager = applicationManager; + _pagesOptions = pagesOptionsAccessor.Value; + } + + public int Order => -1000; + + public void OnProvidersExecuting(PageApplicationModelProviderContext context) + { + EnsureCache(); + for (var i = 0; i < _cachedApplicationModels.Count; i++) + { + var pageModel = _cachedApplicationModels[i]; + context.Results.Add(new PageApplicationModel(pageModel)); + } + } + + public void OnProvidersExecuted(PageApplicationModelProviderContext context) + { + } + + private void EnsureCache() + { + lock (_cacheLock) + { + if (_cachedApplicationModels != null) + { + return; + } + + var rootDirectory = _pagesOptions.RootDirectory; + if (!rootDirectory.EndsWith("/", StringComparison.Ordinal)) + { + rootDirectory = rootDirectory + "/"; + } + + var cachedApplicationModels = new List(); + var pages = GetCompiledPages(); + foreach (var page in pages) + { + if (!page.Path.StartsWith(rootDirectory)) + { + continue; + } + + var viewEnginePath = GetViewEnginePath(rootDirectory, page.Path); + var model = new PageApplicationModel(page.Path, viewEnginePath); + PageSelectorModel.PopulateDefaults(model, page.RoutePrefix); + + cachedApplicationModels.Add(model); + } + + _cachedApplicationModels = cachedApplicationModels; + } + } + + protected virtual IEnumerable GetCompiledPages() + => CompiledPageFeatureProvider.GetCompiledPageInfo(_applicationManager.ApplicationParts); + + private string GetViewEnginePath(string rootDirectory, string path) + { + var endIndex = path.LastIndexOf('.'); + if (endIndex == -1) + { + endIndex = path.Length; + } + + // rootDirectory = "/Pages/AllMyPages/" + // path = "/Pages/AllMyPages/Home.cshtml" + // Result = "/Home" + var startIndex = rootDirectory.Length - 1; + + return path.Substring(startIndex, endIndex - startIndex); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs index 30c5d4dc97..3e9228daf0 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionInvokerProvider.cs @@ -216,13 +216,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal ViewStartFileName); foreach (var item in viewStartItems) { - if (item.Exists) + var factoryResult = _razorPageFactoryProvider.CreateFactory(item.Path); + if (factoryResult.Success) { - var factoryResult = _razorPageFactoryProvider.CreateFactory(item.Path); - if (factoryResult.Success) - { - viewStartFactories.Insert(0, factoryResult.RazorPageFactory); - } + viewStartFactories.Insert(0, factoryResult.RazorPageFactory); } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSelectorModel.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSelectorModel.cs new file mode 100644 index 0000000000..4690d2e434 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSelectorModel.cs @@ -0,0 +1,53 @@ +// 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.IO; +using Microsoft.AspNetCore.Mvc.ApplicationModels; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public static class PageSelectorModel + { + private const string IndexFileName = "Index.cshtml"; + + public static void PopulateDefaults(PageApplicationModel model, string routeTemplate) + { + if (AttributeRouteModel.IsOverridePattern(routeTemplate)) + { + throw new InvalidOperationException(string.Format( + Resources.PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable, + model.RelativePath)); + } + + model.Selectors.Add(CreateSelectorModel(model.ViewEnginePath, routeTemplate)); + + var fileName = Path.GetFileName(model.RelativePath); + if (string.Equals(IndexFileName, fileName, StringComparison.OrdinalIgnoreCase)) + { + var parentDirectoryPath = model.ViewEnginePath; + var index = parentDirectoryPath.LastIndexOf('/'); + if (index == -1) + { + parentDirectoryPath = string.Empty; + } + else + { + parentDirectoryPath = parentDirectoryPath.Substring(0, index); + } + model.Selectors.Add(CreateSelectorModel(parentDirectoryPath, routeTemplate)); + } + } + + 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/RazorProjectPageApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorProjectPageApplicationModelProvider.cs new file mode 100644 index 0000000000..367d99d564 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/RazorProjectPageApplicationModelProvider.cs @@ -0,0 +1,55 @@ +// 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.ApplicationModels; +using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class RazorProjectPageApplicationModelProvider : IPageApplicationModelProvider + { + private readonly RazorProject _project; + private readonly RazorPagesOptions _pagesOptions; + + public RazorProjectPageApplicationModelProvider( + RazorProject razorProject, + IOptions pagesOptionsAccessor) + { + _project = razorProject; + _pagesOptions = pagesOptionsAccessor.Value; + } + + public int Order => -1000; + + public void OnProvidersExecuted(PageApplicationModelProviderContext context) + { + } + + public void OnProvidersExecuting(PageApplicationModelProviderContext context) + { + foreach (var item in _project.EnumerateItems(_pagesOptions.RootDirectory)) + { + if (item.FileName.StartsWith("_")) + { + // Pages like _ViewImports should not be routable. + continue; + } + + if (!PageDirectiveFeature.TryGetPageDirective(item, out var routeTemplate)) + { + // .cshtml pages without @page are not RazorPages. + continue; + } + + var pageApplicationModel = new PageApplicationModel( + relativePath: item.CombinedPath, + viewEnginePath: item.PathWithoutExtension); + PageSelectorModel.PopulateDefaults(pageApplicationModel, routeTemplate); + + context.Results.Add(pageApplicationModel); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs index 8952c6416b..5bc044dc80 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs @@ -121,8 +121,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation _containerLookup = containerLookup; } - protected override Type GetViewInfoContainerType(AssemblyPart assemblyPart) => - _containerLookup[assemblyPart]; + protected override ViewInfoContainer GetManifest(AssemblyPart assemblyPart) + { + var type = _containerLookup[assemblyPart]; + return (ViewInfoContainer)Activator.CreateInstance(type); + } } private class ViewInfoContainer1 : ViewInfoContainer diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs index 79ad7dff74..8aefcfa07c 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs @@ -19,17 +19,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure public class PageActionDescriptorProviderTest { [Fact] - public void GetDescriptors_DoesNotAddDescriptorsForFilesWithoutDirectives() + public void GetDescriptors_DoesNotAddDescriptorsIfNoApplicationModelsAreDiscovered() { // Arrange - var razorProject = new Mock(); - razorProject.Setup(p => p.EnumerateItems("/")) - .Returns(new[] - { - GetProjectItem("/", "/Index.cshtml", "

Hello world

"), - }); + var applicationModelProvider = new TestPageApplicationModelProvider(); var provider = new PageActionDescriptorProvider( - razorProject.Object, + new[] { applicationModelProvider }, GetAccessor(), GetRazorPagesOptions()); var context = new ActionDescriptorProviderContext(); @@ -42,17 +37,25 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure } [Fact] - public void GetDescriptors_AddsDescriptorsForFileWithPageDirective() + public void GetDescriptors_AddsDescriptorsForModelWithSelector() { // Arrange - var razorProject = new Mock(); - razorProject.Setup(p => p.EnumerateItems("/")) - .Returns(new[] + var model = new PageApplicationModel("/Test.cshtml", "/Test") + { + Selectors = { - GetProjectItem("/", "/Test.cshtml", $"@page{Environment.NewLine}

Hello world

"), - }); + new SelectorModel + { + AttributeRouteModel = new AttributeRouteModel + { + Template = "/Test/{id:int?}", + } + } + } + }; + var applicationModelProvider = new TestPageApplicationModelProvider(model); var provider = new PageActionDescriptorProvider( - razorProject.Object, + new[] { applicationModelProvider }, GetAccessor(), GetRazorPagesOptions()); var context = new ActionDescriptorProviderContext(); @@ -65,53 +68,49 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure var descriptor = Assert.IsType(result); Assert.Equal("/Test.cshtml", descriptor.RelativePath); Assert.Equal("/Test", descriptor.RouteValues["page"]); - Assert.Equal("Test", descriptor.AttributeRouteInfo.Template); + Assert.Equal("/Test/{id:int?}", descriptor.AttributeRouteInfo.Template); } [Fact] - public void GetDescriptors_AddsDescriptorsForFileWithPageDirectiveAndRouteTemplate() + public void GetDescriptors_AddsActionDescriptorForEachSelector() { // Arrange - var razorProject = new Mock(); - razorProject.Setup(p => p.EnumerateItems("/")) - .Returns(new[] + var applicationModelProvider = new TestPageApplicationModelProvider( + new PageApplicationModel("/base-path/Test.cshtml", "/base-path/Test") { - GetProjectItem("/", "/Test.cshtml", $"@page \"Home\" {Environment.NewLine}

Hello world

"), - }); - var provider = new PageActionDescriptorProvider( - razorProject.Object, - GetAccessor(), - GetRazorPagesOptions()); - var context = new ActionDescriptorProviderContext(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var result = Assert.Single(context.Results); - var descriptor = Assert.IsType(result); - Assert.Equal("/Test.cshtml", descriptor.RelativePath); - Assert.Equal("/Test", descriptor.RouteValues["page"]); - Assert.Equal("Test/Home", descriptor.AttributeRouteInfo.Template); - } - - [Fact] - public void GetDescriptors_GeneratesRouteTemplate() - { - // Arrange - var razorProject = new Mock(MockBehavior.Strict); - razorProject.Setup(p => p.EnumerateItems("/")) - .Returns(new[] + Selectors = + { + CreateSelectorModel("base-path/Test/Home") + } + }, + new PageApplicationModel("/base-path/Index.cshtml", "/base-path/Index") { - GetProjectItem("/", "/base-path/Test.cshtml", $"@page \"Home\" {Environment.NewLine}

Hello world

"), - GetProjectItem("/", "/base-path/Index.cshtml", $"@page {Environment.NewLine}"), - GetProjectItem("/", "/base-path/Admin/Index.cshtml", $"@page{Environment.NewLine}"), - GetProjectItem("/", "/base-path/Admin/User.cshtml", $"@page{Environment.NewLine}"), + Selectors = + { + CreateSelectorModel("base-path/Index"), + CreateSelectorModel("base-path/"), + } + }, + new PageApplicationModel("/base-path/Admin/Index.cshtml", "/base-path/Admin/Index") + { + Selectors = + { + CreateSelectorModel("base-path/Admin/Index"), + CreateSelectorModel("base-path/Admin"), + } + }, + new PageApplicationModel("/base-path/Admin/User.cshtml", "/base-path/Admin/User") + { + Selectors = + { + CreateSelectorModel("base-path/Admin/User"), + }, }); + var options = GetRazorPagesOptions(); var provider = new PageActionDescriptorProvider( - razorProject.Object, + new[] { applicationModelProvider }, GetAccessor(), options); var context = new ActionDescriptorProviderContext(); @@ -123,122 +122,39 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure Assert.Collection(context.Results, result => Assert.Equal("base-path/Test/Home", result.AttributeRouteInfo.Template), result => Assert.Equal("base-path/Index", result.AttributeRouteInfo.Template), - result => Assert.Equal("base-path", result.AttributeRouteInfo.Template), + result => Assert.Equal("base-path/", result.AttributeRouteInfo.Template), result => Assert.Equal("base-path/Admin/Index", result.AttributeRouteInfo.Template), result => Assert.Equal("base-path/Admin", result.AttributeRouteInfo.Template), result => Assert.Equal("base-path/Admin/User", result.AttributeRouteInfo.Template)); } - [Fact] - public void GetDescriptors_UsesBasePathOption_WhenGeneratingRouteTemplate() + private static SelectorModel CreateSelectorModel(string template) { - // Arrange - var razorProject = new Mock(MockBehavior.Strict); - razorProject.Setup(p => p.EnumerateItems("/base-path")) - .Returns(new[] + return new SelectorModel + { + AttributeRouteModel = new AttributeRouteModel { - GetProjectItem("/base-path", "/Test.cshtml", $"@page \"Home\" {Environment.NewLine}

Hello world

"), - GetProjectItem("/base-path", "/Index.cshtml", $"@page {Environment.NewLine}"), - GetProjectItem("/base-path", "/Admin/Index.cshtml", $"@page{Environment.NewLine}"), - GetProjectItem("/base-path", "/Admin/User.cshtml", $"@page{Environment.NewLine}"), - }); - var options = GetRazorPagesOptions(); - options.Value.RootDirectory = "/base-path"; - var provider = new PageActionDescriptorProvider( - razorProject.Object, - GetAccessor(), - options); - var context = new ActionDescriptorProviderContext(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - Assert.Collection(context.Results, - result => Assert.Equal("Test/Home", result.AttributeRouteInfo.Template), - result => Assert.Equal("Index", result.AttributeRouteInfo.Template), - result => Assert.Equal("", result.AttributeRouteInfo.Template), - result => Assert.Equal("Admin/Index", result.AttributeRouteInfo.Template), - result => Assert.Equal("Admin", result.AttributeRouteInfo.Template), - result => Assert.Equal("Admin/User", result.AttributeRouteInfo.Template)); - - } - - [Theory] - [InlineData("/Path1")] - [InlineData("~/Path1")] - public void GetDescriptors_ThrowsIfRouteTemplatesAreOverriden(string template) - { - // Arrange - var razorProject = new Mock(); - razorProject.Setup(p => p.EnumerateItems("/")) - .Returns(new[] - { - GetProjectItem("/", "/Test.cshtml", $"@page \"{template}\" {Environment.NewLine}

Hello world

"), - }); - var provider = new PageActionDescriptorProvider( - razorProject.Object, - GetAccessor(), - GetRazorPagesOptions()); - var context = new ActionDescriptorProviderContext(); - - // Act and Assert - var ex = Assert.Throws(() => provider.OnProvidersExecuting(context)); - Assert.Equal( - "The route for the page at '/Test.cshtml' cannot start with / or ~/. " + - "Pages do not support overriding the file path of the page.", - ex.Message); + Template = template, + } + }; } [Fact] - public void GetDescriptors_WithEmptyPageDirective_MapsIndexToEmptySegment() + public void GetDescriptors_AddsMultipleDescriptorsForPageWithMultipleSelectors() { // Arrange - var razorProject = new Mock(); - razorProject.Setup(p => p.EnumerateItems("/")) - .Returns(new[] + var applicationModelProvider = new TestPageApplicationModelProvider( + new PageApplicationModel("/Catalog/Details/Index.cshtml", "/Catalog/Details/Index") { - GetProjectItem("", "/About/Index.cshtml", $"@page {Environment.NewLine}"), + Selectors = + { + CreateSelectorModel("/Catalog/Details/Index/{id:int?}"), + CreateSelectorModel("/Catalog/Details/{id:int?}"), + }, }); + var provider = new PageActionDescriptorProvider( - razorProject.Object, - GetAccessor(), - GetRazorPagesOptions()); - var context = new ActionDescriptorProviderContext(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - Assert.Collection(context.Results, - result => - { - var descriptor = Assert.IsType(result); - Assert.Equal("/About/Index.cshtml", descriptor.RelativePath); - Assert.Equal("/About/Index", descriptor.RouteValues["page"]); - Assert.Equal("About/Index", descriptor.AttributeRouteInfo.Template); - }, - result => - { - var descriptor = Assert.IsType(result); - Assert.Equal("/About/Index.cshtml", descriptor.RelativePath); - Assert.Equal("/About/Index", descriptor.RouteValues["page"]); - Assert.Equal("About", descriptor.AttributeRouteInfo.Template); - }); - } - - [Fact] - public void GetDescriptors_WithNonEmptyPageDirective_MapsIndexToEmptySegment() - { - // Arrange - var razorProject = new Mock(); - razorProject.Setup(p => p.EnumerateItems("/")) - .Returns(new[] - { - GetProjectItem("", "/Catalog/Details/Index.cshtml", $"@page \"{{id:int?}}\" {Environment.NewLine}"), - }); - var provider = new PageActionDescriptorProvider( - razorProject.Object, + new[] { applicationModelProvider }, GetAccessor(), GetRazorPagesOptions()); var context = new ActionDescriptorProviderContext(); @@ -253,14 +169,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure var descriptor = Assert.IsType(result); Assert.Equal("/Catalog/Details/Index.cshtml", descriptor.RelativePath); Assert.Equal("/Catalog/Details/Index", descriptor.RouteValues["page"]); - Assert.Equal("Catalog/Details/Index/{id:int?}", descriptor.AttributeRouteInfo.Template); + Assert.Equal("/Catalog/Details/Index/{id:int?}", descriptor.AttributeRouteInfo.Template); }, result => { var descriptor = Assert.IsType(result); Assert.Equal("/Catalog/Details/Index.cshtml", descriptor.RelativePath); Assert.Equal("/Catalog/Details/Index", descriptor.RouteValues["page"]); - Assert.Equal("Catalog/Details/{id:int?}", descriptor.AttributeRouteInfo.Template); + Assert.Equal("/Catalog/Details/{id:int?}", descriptor.AttributeRouteInfo.Template); }); } @@ -269,14 +185,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure { // Arrange var options = new MvcOptions(); - var razorProject = new Mock(); - razorProject.Setup(p => p.EnumerateItems("/")) - .Returns(new[] - { - GetProjectItem("/", "/Home.cshtml", $"@page {Environment.NewLine}"), - }); + var applicationModelProvider = new TestPageApplicationModelProvider(CreateModel()); var provider = new PageActionDescriptorProvider( - razorProject.Object, + new[] { applicationModelProvider }, GetAccessor(options), GetRazorPagesOptions()); var context = new ActionDescriptorProviderContext(); @@ -310,14 +221,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure var options = new MvcOptions(); options.Filters.Add(filter1); options.Filters.Add(filter2); - var razorProject = new Mock(); - razorProject.Setup(p => p.EnumerateItems("/")) - .Returns(new[] - { - GetProjectItem("/", "/Home.cshtml", $"@page {Environment.NewLine}"), - }); + var applicationModelProvider = new TestPageApplicationModelProvider(CreateModel()); var provider = new PageActionDescriptorProvider( - razorProject.Object, + new[] { applicationModelProvider }, GetAccessor(options), GetRazorPagesOptions()); var context = new ActionDescriptorProviderContext(); @@ -368,15 +274,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure }); var razorOptions = GetRazorPagesOptions(); razorOptions.Value.Conventions.Add(convention.Object); - - var razorProject = new Mock(); - razorProject.Setup(p => p.EnumerateItems("/")) - .Returns(new[] - { - GetProjectItem("/", "/Home.cshtml", $"@page {Environment.NewLine}"), - }); + var applicationModelProvider = new TestPageApplicationModelProvider(CreateModel()); var provider = new PageActionDescriptorProvider( - razorProject.Object, + new[] { applicationModelProvider }, GetAccessor(options), razorOptions); var context = new ActionDescriptorProviderContext(); @@ -410,6 +310,23 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure }); } + private static PageApplicationModel CreateModel() + { + return new PageApplicationModel("/Home.cshtml", "/Home") + { + Selectors = + { + new SelectorModel + { + AttributeRouteModel = new AttributeRouteModel + { + Template = "Home", + } + } + } + }; + } + private static IOptions GetAccessor(TOptions options = null) where TOptions : class, new() { @@ -432,5 +349,30 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure return new DefaultRazorProjectItem(testFileInfo, basePath, path); } + + private class TestPageApplicationModelProvider : IPageApplicationModelProvider + { + private readonly PageApplicationModel[] _models; + + public TestPageApplicationModelProvider(params PageApplicationModel[] models) + { + _models = models ?? Array.Empty(); + } + + public int Order => 0; + + public void OnProvidersExecuted(PageApplicationModelProviderContext context) + { + } + + public void OnProvidersExecuting(PageApplicationModelProviderContext context) + { + foreach (var model in _models) + { + context.Results.Add(model); + } + + } + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageApplicationModelProviderTest.cs new file mode 100644 index 0000000000..5fcec76fa2 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/CompiledPageApplicationModelProviderTest.cs @@ -0,0 +1,113 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class CompiledPageApplicationModelProviderTest + { + [Fact] + public void OnProvidersExecuting_AddsModelsForCompiledViews() + { + // Arrange + var info = new[] + { + new CompiledPageInfo("/Pages/About.cshtml", typeof(object), routePrefix: string.Empty), + new CompiledPageInfo("/Pages/Home.cshtml", typeof(object), "some-prefix"), + }; + var provider = new TestCompiledPageApplicationModelProvider(info, new RazorPagesOptions()); + var context = new PageApplicationModelProviderContext(); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Collection(context.Results, + result => + { + Assert.Equal("/Pages/About.cshtml", result.RelativePath); + Assert.Equal("/Pages/About", result.ViewEnginePath); + Assert.Collection(result.Selectors, + selector => Assert.Equal("Pages/About", selector.AttributeRouteModel.Template)); + }, + result => + { + Assert.Equal("/Pages/Home.cshtml", result.RelativePath); + Assert.Equal("/Pages/Home", result.ViewEnginePath); + Assert.Collection(result.Selectors, + selector => Assert.Equal("Pages/Home/some-prefix", selector.AttributeRouteModel.Template)); + }); + } + + [Fact] + public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPage() + { + // Arrange + var info = new[] + { + new CompiledPageInfo("/Pages/Index.cshtml", typeof(object), routePrefix: string.Empty), + new CompiledPageInfo("/Pages/Admin/Index.cshtml", typeof(object), "some-template"), + }; + var provider = new TestCompiledPageApplicationModelProvider(info, new RazorPagesOptions()); + var context = new PageApplicationModelProviderContext(); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Collection(context.Results, + result => + { + Assert.Equal("/Pages/Index.cshtml", result.RelativePath); + Assert.Equal("/Pages/Index", result.ViewEnginePath); + Assert.Collection(result.Selectors, + selector => Assert.Equal("Pages/Index", selector.AttributeRouteModel.Template), + selector => Assert.Equal("Pages", selector.AttributeRouteModel.Template)); + }, + result => + { + Assert.Equal("/Pages/Admin/Index.cshtml", result.RelativePath); + Assert.Equal("/Pages/Admin/Index", result.ViewEnginePath); + Assert.Collection(result.Selectors, + selector => Assert.Equal("Pages/Admin/Index/some-template", selector.AttributeRouteModel.Template), + selector => Assert.Equal("Pages/Admin/some-template", selector.AttributeRouteModel.Template)); + }); + } + + [Fact] + public void OnProvidersExecuting_ThrowsIfRouteTemplateHasOverridePattern() + { + // Arrange + var info = new[] + { + new CompiledPageInfo("/Pages/Index.cshtml", typeof(object), routePrefix: string.Empty), + new CompiledPageInfo("/Pages/Home.cshtml", typeof(object), "/some-prefix"), + }; + var provider = new TestCompiledPageApplicationModelProvider(info, new RazorPagesOptions()); + var context = new PageApplicationModelProviderContext(); + + // Act & Assert + var ex = 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.", + ex.Message); + } + + public class TestCompiledPageApplicationModelProvider : CompiledPageApplicationModelProvider + { + private readonly IEnumerable _info; + + public TestCompiledPageApplicationModelProvider(IEnumerable info, RazorPagesOptions options) + : base(new ApplicationPartManager(), new TestOptionsManager(options)) + { + _info = info; + } + + protected override IEnumerable GetCompiledPages() => _info; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs index b3d389b30c..34a728e223 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionInvokerProviderTest.cs @@ -723,8 +723,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } [Fact] - public void GetViewStartFactories_NoFactoriesForMissingFiles() + public void GetViewStartFactories_ReturnsFactoriesForFilesThatDoNotExistInProject() { + // The factory provider might have access to _ViewStarts for files that do not exist on disk \ RazorProject. + // This test verifies that we query the factory provider correctly. // Arrange var descriptor = new PageActionDescriptor() { @@ -739,6 +741,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var actionDescriptorProvider = new Mock(); actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); + var pageFactory = new Mock(); + pageFactory.Setup(f => f.CreateFactory("/Views/Deeper/_ViewStart.cshtml")) + .Returns(new RazorPageFactoryResult(() => null, new IChangeToken[0])); + pageFactory.Setup(f => f.CreateFactory("/Views/_ViewStart.cshtml")) + .Returns(new RazorPageFactoryResult(new IChangeToken[0])); + pageFactory.Setup(f => f.CreateFactory("/_ViewStart.cshtml")) + .Returns(new RazorPageFactoryResult(() => null, new IChangeToken[0])); + // No files var fileProvider = new TestFileProvider(); var razorProject = new TestRazorProject(fileProvider); @@ -748,16 +758,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal actionDescriptorProvider.Object, pageProvider: null, modelProvider: null, - razorPageFactoryProvider: CreateRazorPageFactoryProvider(), + razorPageFactoryProvider: pageFactory.Object, razorProject: razorProject); var compiledDescriptor = CreateCompiledPageActionDescriptor(descriptor); // Act - var factories = invokerProvider.GetViewStartFactories(compiledDescriptor); + var factories = invokerProvider.GetViewStartFactories(compiledDescriptor).ToList(); // Assert - Assert.Empty(factories); + Assert.Equal(2, factories.Count); } private IRazorPageFactoryProvider CreateRazorPageFactoryProvider() diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageApplicationModelProviderTest.cs new file mode 100644 index 0000000000..3f8487e579 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/RazorProjectPageApplicationModelProviderTest.cs @@ -0,0 +1,179 @@ +// 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 Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.Extensions.FileProviders; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class RazorProjectPageApplicationModelProviderTest + { + [Fact] + public void OnProvidersExecuting_ReturnsPagesWithPageDirective() + { + // Arrange + var fileProvider = new TestFileProvider(); + var file1 = fileProvider.AddFile("/Pages/Home.cshtml", "@page"); + var file2 = fileProvider.AddFile("/Pages/Test.cshtml", "Hello world"); + + var dir1 = fileProvider.AddDirectoryContent("/Pages", new IFileInfo[] { file1, file2 }); + fileProvider.AddDirectoryContent("/", new[] { dir1 }); + + var project = new TestRazorProject(fileProvider); + + var optionsManager = new TestOptionsManager(); + optionsManager.Value.RootDirectory = "/"; + var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager); + var context = new PageApplicationModelProviderContext(); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Collection(context.Results, + model => + { + Assert.Equal("/Pages/Home.cshtml", model.RelativePath); + Assert.Equal("/Pages/Home", model.ViewEnginePath); + Assert.Collection(model.Selectors, + selector => Assert.Equal("Pages/Home", selector.AttributeRouteModel.Template)); + }); + } + + [Fact] + public void OnProvidersExecuting_AddsMultipleSelectorsForIndexPages() + { + // Arrange + var fileProvider = new TestFileProvider(); + var file1 = fileProvider.AddFile("/Pages/Index.cshtml", "@page"); + var file2 = fileProvider.AddFile("/Pages/Test.cshtml", "Hello world"); + var file3 = fileProvider.AddFile("/Pages/Admin/Index.cshtml", "@page \"test\""); + + var dir2 = fileProvider.AddDirectoryContent("/Pages/Admin", new[] { file3 }); + var dir1 = fileProvider.AddDirectoryContent("/Pages", new IFileInfo[] { dir2, file1, file2 }); + fileProvider.AddDirectoryContent("/", new[] { dir1 }); + + var project = new TestRazorProject(fileProvider); + + var optionsManager = new TestOptionsManager(); + optionsManager.Value.RootDirectory = "/"; + var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager); + var context = new PageApplicationModelProviderContext(); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Collection(context.Results, + model => + { + Assert.Equal("/Pages/Admin/Index.cshtml", model.RelativePath); + Assert.Equal("/Pages/Admin/Index", model.ViewEnginePath); + Assert.Collection(model.Selectors, + selector => Assert.Equal("Pages/Admin/Index/test", selector.AttributeRouteModel.Template), + selector => Assert.Equal("Pages/Admin/test", selector.AttributeRouteModel.Template)); + }, + model => + { + Assert.Equal("/Pages/Index.cshtml", model.RelativePath); + Assert.Equal("/Pages/Index", model.ViewEnginePath); + Assert.Collection(model.Selectors, + selector => Assert.Equal("Pages/Index", selector.AttributeRouteModel.Template), + selector => Assert.Equal("Pages", selector.AttributeRouteModel.Template)); + }); + } + + [Fact] + public void OnProvidersExecuting_ThrowsIfRouteTemplateHasOverridePattern() + { + // Arrange + var fileProvider = new TestFileProvider(); + var file = fileProvider.AddFile("/Index.cshtml", "@page \"/custom-route\""); + fileProvider.AddDirectoryContent("/", new[] { file }); + + var project = new TestRazorProject(fileProvider); + + var optionsManager = new TestOptionsManager(); + optionsManager.Value.RootDirectory = "/"; + var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager); + var context = new PageApplicationModelProviderContext(); + + // Act & Assert + var ex = Assert.Throws(() => provider.OnProvidersExecuting(context)); + Assert.Equal("The route for the page at '/Index.cshtml' cannot start with / or ~/. Pages do not support overriding the file path of the page.", + ex.Message); + } + + [Fact] + public void OnProvidersExecuting_SkipsPagesStartingWithUnderscore() + { + // Arrange + var fileProvider = new TestFileProvider(); + var dir1 = fileProvider.AddDirectoryContent("/Pages", + new[] + { + fileProvider.AddFile("/Pages/Home.cshtml", "@page"), + fileProvider.AddFile("/Pages/_Layout.cshtml", "@page") + }); + fileProvider.AddDirectoryContent("/", new[] { dir1 }); + + var project = new TestRazorProject(fileProvider); + + var optionsManager = new TestOptionsManager(); + optionsManager.Value.RootDirectory = "/"; + var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager); + var context = new PageApplicationModelProviderContext(); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Collection(context.Results, + model => + { + Assert.Equal("/Pages/Home.cshtml", model.RelativePath); + }); + } + + [Fact] + public void OnProvidersExecuting_DiscoversFilesUnderBasePath() + { + // Arrange + var fileProvider = new TestFileProvider(); + var dir1 = fileProvider.AddDirectoryContent("/Pages", + new[] + { + fileProvider.AddFile("/Pages/Index.cshtml", "@page"), + fileProvider.AddFile("/Pages/_Layout.cshtml", "@page") + }); + var dir2 = fileProvider.AddDirectoryContent("/NotPages", + new[] + { + fileProvider.AddFile("/NotPages/Index.cshtml", "@page"), + fileProvider.AddFile("/NotPages/_Layout.cshtml", "@page") + }); + var rootFile = fileProvider.AddFile("/Index.cshtml", "@page"); + fileProvider.AddDirectoryContent("/", new IFileInfo[] { rootFile, dir1, dir2 }); + + var project = new TestRazorProject(fileProvider); + + var optionsManager = new TestOptionsManager(); + optionsManager.Value.RootDirectory = "/Pages"; + var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager); + var context = new PageApplicationModelProviderContext(); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Collection(context.Results, + model => + { + Assert.Equal("/Pages/Index.cshtml", model.RelativePath); + }); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs index b0c72b7c55..7e131264b5 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs @@ -213,7 +213,8 @@ namespace Microsoft.AspNetCore.Mvc feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature), - feature => Assert.IsType(feature)); + feature => Assert.IsType(feature), + feature => Assert.IsType(feature)); } [Fact] @@ -419,6 +420,14 @@ namespace Microsoft.AspNetCore.Mvc typeof(JsonPatchOperationsArrayProvider), } }, + { + typeof(IPageApplicationModelProvider), + new[] + { + typeof(CompiledPageApplicationModelProvider), + typeof(RazorProjectPageApplicationModelProvider), + } + }, }; } } diff --git a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestDirectoryContent.cs b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestDirectoryContent.cs index d50a72c3bc..0ddb4f7823 100644 --- a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestDirectoryContent.cs +++ b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestDirectoryContent.cs @@ -1,23 +1,41 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections; using System.Collections.Generic; +using System.IO; using Microsoft.Extensions.FileProviders; namespace Microsoft.AspNetCore.Mvc.TestCommon { - public class TestDirectoryContent : IDirectoryContents + public class TestDirectoryContent : IDirectoryContents, IFileInfo { private readonly IEnumerable _files; - public TestDirectoryContent(IEnumerable files) + public TestDirectoryContent(string name, IEnumerable files) { + Name = name; _files = files; } public bool Exists => true; + public long Length => throw new NotSupportedException(); + + public string PhysicalPath => throw new NotSupportedException(); + + public string Name { get; } + + public DateTimeOffset LastModified => throw new NotSupportedException(); + + public bool IsDirectory => true; + + public Stream CreateReadStream() + { + throw new NotSupportedException(); + } + public IEnumerator GetEnumerator() => _files.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestFileProvider.cs b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestFileProvider.cs index d6efad2070..fb612c6a46 100644 --- a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestFileProvider.cs +++ b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestFileProvider.cs @@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor public TestDirectoryContent AddDirectoryContent(string path, IEnumerable files) { - var directoryContent = new TestDirectoryContent(files); + var directoryContent = new TestDirectoryContent(Path.GetFileName(path), files); _directoryContentsLookup[path] = directoryContent; return directoryContent; }