Add support for page precompilation

This commit is contained in:
Pranav K 2017-04-04 11:17:36 -07:00
parent e766a259ed
commit 8ed55107e7
25 changed files with 981 additions and 307 deletions

View File

@ -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
}
}
}

View File

@ -3,9 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.ApplicationParts;
namespace Microsoft.AspNetCore.Mvc.Razor.Compilation namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
@ -15,9 +13,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
/// </summary> /// </summary>
public class ViewsFeatureProvider : IApplicationFeatureProvider<ViewsFeature> public class ViewsFeatureProvider : IApplicationFeatureProvider<ViewsFeature>
{ {
/// <summary>
/// Gets the suffix for the view assembly.
/// </summary>
public static readonly string PrecompiledViewsAssemblySuffix = ".PrecompiledViews"; public static readonly string PrecompiledViewsAssemblySuffix = ".PrecompiledViews";
/// <summary> /// <summary>
@ -30,19 +25,19 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
/// </summary> /// </summary>
public static readonly string ViewInfoContainerTypeName = "__PrecompiledViewCollection"; public static readonly string ViewInfoContainerTypeName = "__PrecompiledViewCollection";
private static readonly string FullyQualifiedManifestTypeName = ViewInfoContainerNamespace + "." + ViewInfoContainerTypeName;
/// <inheritdoc /> /// <inheritdoc />
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewsFeature feature) public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewsFeature feature)
{ {
foreach (var assemblyPart in parts.OfType<AssemblyPart>()) foreach (var assemblyPart in parts.OfType<AssemblyPart>())
{ {
var viewInfoContainerTypeName = GetViewInfoContainerType(assemblyPart); var viewContainer = GetManifest(assemblyPart);
if (viewInfoContainerTypeName == null) if (viewContainer == null)
{ {
continue; continue;
} }
var viewContainer = (ViewInfoContainer)Activator.CreateInstance(viewInfoContainerTypeName);
foreach (var item in viewContainer.ViewInfos) foreach (var item in viewContainer.ViewInfos)
{ {
feature.Views[item.Path] = item.Type; feature.Views[item.Path] = item.Type;
@ -55,40 +50,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
/// </summary> /// </summary>
/// <param name="assemblyPart">The <see cref="AssemblyPart"/>.</param> /// <param name="assemblyPart">The <see cref="AssemblyPart"/>.</param>
/// <returns>The <see cref="ViewInfoContainer"/> <see cref="Type"/>.</returns> /// <returns>The <see cref="ViewInfoContainer"/> <see cref="Type"/>.</returns>
protected virtual Type GetViewInfoContainerType(AssemblyPart assemblyPart) protected virtual ViewInfoContainer GetManifest(AssemblyPart assemblyPart)
{ {
#if NETSTANDARD1_6 var type = CompiledViewManfiest.GetManifestType(assemblyPart, FullyQualifiedManifestTypeName);
if (!assemblyPart.Assembly.IsDynamic && !string.IsNullOrEmpty(assemblyPart.Assembly.Location)) if (type != null)
{ {
var precompiledAssemblyFileName = assemblyPart.Assembly.GetName().Name return (ViewInfoContainer)Activator.CreateInstance(type);
+ 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.
}
}
} }
#elif NET46
#else
#error target frameworks needs to be updated.
#endif
var precompiledAssemblyName = new AssemblyName(assemblyPart.Assembly.FullName); return null;
precompiledAssemblyName.Name = precompiledAssemblyName.Name + PrecompiledViewsAssemblySuffix;
var typeName = $"{ViewInfoContainerNamespace}.{ViewInfoContainerTypeName},{precompiledAssemblyName}";
return Type.GetType(typeName);
} }
} }
} }

View File

@ -135,10 +135,8 @@ namespace Microsoft.Extensions.DependencyInjection
// DependencyContextRazorViewEngineOptionsSetup needs to run after RazorViewEngineOptionsSetup. // DependencyContextRazorViewEngineOptionsSetup needs to run after RazorViewEngineOptionsSetup.
// The ordering of the following two lines is important to ensure this behavior. // The ordering of the following two lines is important to ensure this behavior.
#pragma warning disable 0618
services.TryAddEnumerable( services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<RazorViewEngineOptions>, RazorViewEngineOptionsSetup>()); ServiceDescriptor.Transient<IConfigureOptions<RazorViewEngineOptions>, RazorViewEngineOptionsSetup>());
#pragma warning restore 0618
services.TryAddEnumerable( services.TryAddEnumerable(
ServiceDescriptor.Transient< ServiceDescriptor.Transient<
IConfigureOptions<RazorViewEngineOptions>, IConfigureOptions<RazorViewEngineOptions>,

View File

@ -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
{
/// <summary>
/// Builds or modifies an <see cref="PageApplicationModelProviderContext"/> for Razor Page discovery.
/// </summary>
public interface IPageApplicationModelProvider
{
/// <summary>
/// Gets the order value for determining the order of execution of providers. Providers execute in
/// ascending numeric value of the <see cref="Order"/> property.
/// </summary>
/// <remarks>
/// <para>
/// Providers are executed in an ordering determined by an ascending sort of the <see cref="Order"/> property.
/// A provider with a lower numeric value of <see cref="Order"/> will have its
/// <see cref="OnProvidersExecuting"/> called before that of a provider with a higher numeric value of
/// <see cref="Order"/>. The <see cref="OnProvidersExecuted"/> method is called in the reverse ordering after
/// all calls to <see cref="OnProvidersExecuting"/>. A provider with a lower numeric value of
/// <see cref="Order"/> will have its <see cref="OnProvidersExecuted"/> method called after that of a provider
/// with a higher numeric value of <see cref="Order"/>.
/// </para>
/// <para>
/// If two providers have the same numeric value of <see cref="Order"/>, then their relative execution order
/// is undefined.
/// </para>
/// </remarks>
int Order { get; }
/// <summary>
/// Executed for the first pass of building <see cref="PageApplicationModel"/> instances. See <see cref="Order"/>.
/// </summary>
/// <param name="context">The <see cref="PageApplicationModelProviderContext"/>.</param>
void OnProvidersExecuting(PageApplicationModelProviderContext context);
/// <summary>
/// Executed for the second pass of building <see cref="PageApplicationModel"/> instances. See <see cref="Order"/>.
/// </summary>
/// <param name="context">The <see cref="PageApplicationModelProviderContext"/>.</param>
void OnProvidersExecuted(PageApplicationModelProviderContext context);
}
}

View File

@ -59,17 +59,17 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
} }
/// <summary> /// <summary>
/// Gets or sets the application root relative path for the page. /// Gets the application root relative path for the page.
/// </summary> /// </summary>
public string RelativePath { get; } public string RelativePath { get; }
/// <summary> /// <summary>
/// Gets or sets the path relative to the base path for page discovery. /// Gets the path relative to the base path for page discovery.
/// </summary> /// </summary>
public string ViewEnginePath { get; } public string ViewEnginePath { get; }
/// <summary> /// <summary>
/// Gets or sets the applicable <see cref="IFilterMetadata"/> instances. /// Gets the applicable <see cref="IFilterMetadata"/> instances.
/// </summary> /// </summary>
public IList<IFilterMetadata> Filters { get; } public IList<IFilterMetadata> Filters { get; }
@ -79,7 +79,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
public IDictionary<object, object> Properties { get; } public IDictionary<object, object> Properties { get; }
/// <summary> /// <summary>
/// Gets or sets the <see cref="SelectorModel"/> instances. /// Gets the <see cref="SelectorModel"/> instances.
/// </summary> /// </summary>
public IList<SelectorModel> Selectors { get; } public IList<SelectorModel> Selectors { get; }
} }

View File

@ -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
{
/// <summary>
/// A context object for <see cref="IPageApplicationModelProvider"/>.
/// </summary>
public class PageApplicationModelProviderContext
{
/// <summary>
/// Gets the <see cref="PageApplicationModel"/> instances.
/// </summary>
public IList<PageApplicationModel> Results { get; } = new List<PageApplicationModel>();
}
}

View File

@ -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<CompiledPageInfo> CompiledPages { get; } = new List<CompiledPageInfo>();
}
}

View File

@ -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
{
/// <summary>
/// An <see cref="IApplicationFeatureProvider{TFeature}"/> for <see cref="ViewsFeature"/>.
/// </summary>
public class CompiledPageFeatureProvider : IApplicationFeatureProvider<ViewsFeature>
{
/// <summary>
/// Gets the namespace for the <see cref="ViewInfoContainer"/> type in the view assembly.
/// </summary>
public static readonly string CompiledPageManifestNamespace = "AspNetCore";
/// <summary>
/// Gets the type name for the view collection type in the view assembly.
/// </summary>
public static readonly string CompiledPageManifestTypeName = "__CompiledRazorPagesManifest";
private static readonly string FullyQualifiedManifestTypeName =
CompiledPageManifestNamespace + "." + CompiledPageManifestTypeName;
/// <inheritdoc />
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewsFeature feature)
{
foreach (var item in GetCompiledPageInfo(parts))
{
feature.Views.Add(item.Path, item.CompiledType);
}
}
/// <summary>
/// Gets the sequence of <see cref="CompiledPageInfo"/> from <paramref name="parts"/>.
/// </summary>
/// <param name="parts">The <see cref="ApplicationPart"/>s</param>
/// <returns>The sequence of <see cref="CompiledPageInfo"/>.</returns>
public static IEnumerable<CompiledPageInfo> GetCompiledPageInfo(IEnumerable<ApplicationPart> parts)
{
return parts.OfType<AssemblyPart>()
.Select(part => CompiledViewManfiest.GetManifestType(part, FullyQualifiedManifestTypeName))
.Where(type => type != null)
.Select(type => (CompiledPageManifest)Activator.CreateInstance(type))
.SelectMany(manifest => manifest.CompiledPages);
}
}
}

View File

@ -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; }
}
}

View File

@ -2,9 +2,11 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Mvc.RazorPages.Internal; using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
@ -24,13 +26,16 @@ namespace Microsoft.Extensions.DependencyInjection
} }
builder.AddRazorViewEngine(); builder.AddRazorViewEngine();
AddFeatureProviders(builder);
AddServices(builder.Services); AddServices(builder.Services);
return builder; return builder;
} }
public static IMvcCoreBuilder AddRazorPages( public static IMvcCoreBuilder AddRazorPages(
this IMvcCoreBuilder builder, this IMvcCoreBuilder builder,
Action<RazorViewEngineOptions> setupAction) Action<RazorPagesOptions> setupAction)
{ {
if (builder == null) if (builder == null)
{ {
@ -43,6 +48,8 @@ namespace Microsoft.Extensions.DependencyInjection
} }
builder.AddRazorViewEngine(); builder.AddRazorViewEngine();
AddFeatureProviders(builder);
AddServices(builder.Services); AddServices(builder.Services);
builder.Services.Configure(setupAction); builder.Services.Configure(setupAction);
@ -50,6 +57,14 @@ namespace Microsoft.Extensions.DependencyInjection
return builder; return builder;
} }
private static void AddFeatureProviders(IMvcCoreBuilder builder)
{
if (!builder.PartManager.FeatureProviders.OfType<CompiledPageFeatureProvider>().Any())
{
builder.PartManager.FeatureProviders.Add(new CompiledPageFeatureProvider());
}
}
// Internal for testing. // Internal for testing.
internal static void AddServices(IServiceCollection services) internal static void AddServices(IServiceCollection services)
{ {
@ -57,10 +72,14 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddEnumerable( services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<RazorPagesOptions>, RazorPagesOptionsSetup>()); ServiceDescriptor.Transient<IConfigureOptions<RazorPagesOptions>, RazorPagesOptionsSetup>());
// Action Invoker // Action description and invocation
services.TryAddEnumerable( services.TryAddEnumerable(
ServiceDescriptor.Singleton<IActionDescriptorProvider, PageActionDescriptorProvider>()); ServiceDescriptor.Singleton<IActionDescriptorProvider, PageActionDescriptorProvider>());
services.TryAddSingleton<IActionDescriptorChangeProvider, PageActionDescriptorChangeProvider>(); services.TryAddSingleton<IActionDescriptorChangeProvider, PageActionDescriptorChangeProvider>();
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPageApplicationModelProvider, RazorProjectPageApplicationModelProvider>());
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPageApplicationModelProvider, CompiledPageApplicationModelProvider>());
services.TryAddEnumerable( services.TryAddEnumerable(
ServiceDescriptor.Singleton<IActionInvokerProvider, PageActionInvokerProvider>()); ServiceDescriptor.Singleton<IActionInvokerProvider, PageActionInvokerProvider>());

View File

@ -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
{
/// <summary>
/// Initializes a new instance of <see cref="CompiledPageManifest"/>.
/// </summary>
/// <param name="pages">The sequence of <see cref="CompiledPageInfo"/>.</param>
public CompiledPageManifest(IReadOnlyList<CompiledPageInfo> pages)
{
CompiledPages = pages;
}
/// <summary>
/// The <see cref="IReadOnlyList{T}"/> of <see cref="CompiledPageInfo"/>.
/// </summary>
public IReadOnlyList<CompiledPageInfo> CompiledPages { get; }
}
}

View File

@ -3,29 +3,27 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{ {
public class PageActionDescriptorProvider : IActionDescriptorProvider public class PageActionDescriptorProvider : IActionDescriptorProvider
{ {
private static readonly string IndexFileName = "Index.cshtml"; private readonly List<IPageApplicationModelProvider> _applicationModelProviders;
private readonly RazorProject _project;
private readonly MvcOptions _mvcOptions; private readonly MvcOptions _mvcOptions;
private readonly RazorPagesOptions _pagesOptions; private readonly RazorPagesOptions _pagesOptions;
public PageActionDescriptorProvider( public PageActionDescriptorProvider(
RazorProject project, IEnumerable<IPageApplicationModelProvider> pageMetadataProviders,
IOptions<MvcOptions> mvcOptionsAccessor, IOptions<MvcOptions> mvcOptionsAccessor,
IOptions<RazorPagesOptions> pagesOptionsAccessor) IOptions<RazorPagesOptions> pagesOptionsAccessor)
{ {
_project = project; _applicationModelProviders = pageMetadataProviders.OrderBy(p => p.Order).ToList();
_mvcOptions = mvcOptionsAccessor.Value; _mvcOptions = mvcOptionsAccessor.Value;
_pagesOptions = pagesOptionsAccessor.Value; _pagesOptions = pagesOptionsAccessor.Value;
} }
@ -34,57 +32,37 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public void OnProvidersExecuting(ActionDescriptorProviderContext context) 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("_")) AddActionDescriptors(context.Results, pageApplicationModels[i]);
{
// 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);
} }
} }
protected IList<PageApplicationModel> 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) public void OnProvidersExecuted(ActionDescriptorProviderContext context)
{ {
} }
private void AddActionDescriptors(IList<ActionDescriptor> actions, RazorProjectItem item, string template) private void AddActionDescriptors(IList<ActionDescriptor> 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++) for (var i = 0; i < _pagesOptions.Conventions.Count; i++)
{ {
_pagesOptions.Conventions[i].Apply(model); _pagesOptions.Conventions[i].Apply(model);
@ -111,28 +89,17 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
Order = selector.AttributeRouteModel.Order ?? 0, Order = selector.AttributeRouteModel.Order ?? 0,
Template = selector.AttributeRouteModel.Template, Template = selector.AttributeRouteModel.Template,
}, },
DisplayName = $"Page: {item.Path}", DisplayName = $"Page: {model.ViewEnginePath}",
FilterDescriptors = filters, FilterDescriptors = filters,
Properties = new Dictionary<object, object>(model.Properties), Properties = new Dictionary<object, object>(model.Properties),
RelativePath = item.CombinedPath, RelativePath = model.RelativePath,
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) RouteValues = new Dictionary<string, string>(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),
}
};
}
} }
} }

View File

@ -16,16 +16,25 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
throw new ArgumentNullException(nameof(projectItem)); throw new ArgumentNullException(nameof(projectItem));
} }
return TryGetPageDirective(projectItem.Read, out template);
}
public static bool TryGetPageDirective(Func<Stream> streamFactory, out string template)
{
if (streamFactory == null)
{
throw new ArgumentNullException(nameof(streamFactory));
}
const string PageDirective = "@page"; const string PageDirective = "@page";
var stream = projectItem.Read(); string content;
using (var streamReader = new StreamReader(streamFactory()))
string content = null;
using (var streamReader = new StreamReader(stream))
{ {
do do
{ {
content = streamReader.ReadLine(); content = streamReader.ReadLine();
} while (content != null && string.IsNullOrWhiteSpace(content)); } while (content != null && string.IsNullOrWhiteSpace(content));
content = content?.Trim(); content = content?.Trim();
} }

View File

@ -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<PageApplicationModel> _cachedApplicationModels;
public CompiledPageApplicationModelProvider(
ApplicationPartManager applicationManager,
IOptions<RazorPagesOptions> 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<PageApplicationModel>();
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<CompiledPageInfo> 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);
}
}
}

View File

@ -216,13 +216,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
ViewStartFileName); ViewStartFileName);
foreach (var item in viewStartItems) foreach (var item in viewStartItems)
{ {
if (item.Exists) var factoryResult = _razorPageFactoryProvider.CreateFactory(item.Path);
if (factoryResult.Success)
{ {
var factoryResult = _razorPageFactoryProvider.CreateFactory(item.Path); viewStartFactories.Insert(0, factoryResult.RazorPageFactory);
if (factoryResult.Success)
{
viewStartFactories.Insert(0, factoryResult.RazorPageFactory);
}
} }
} }

View File

@ -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),
}
};
}
}
}

View File

@ -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<RazorPagesOptions> 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);
}
}
}
}

View File

@ -121,8 +121,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
_containerLookup = containerLookup; _containerLookup = containerLookup;
} }
protected override Type GetViewInfoContainerType(AssemblyPart assemblyPart) => protected override ViewInfoContainer GetManifest(AssemblyPart assemblyPart)
_containerLookup[assemblyPart]; {
var type = _containerLookup[assemblyPart];
return (ViewInfoContainer)Activator.CreateInstance(type);
}
} }
private class ViewInfoContainer1 : ViewInfoContainer private class ViewInfoContainer1 : ViewInfoContainer

View File

@ -19,17 +19,12 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
public class PageActionDescriptorProviderTest public class PageActionDescriptorProviderTest
{ {
[Fact] [Fact]
public void GetDescriptors_DoesNotAddDescriptorsForFilesWithoutDirectives() public void GetDescriptors_DoesNotAddDescriptorsIfNoApplicationModelsAreDiscovered()
{ {
// Arrange // Arrange
var razorProject = new Mock<RazorProject>(); var applicationModelProvider = new TestPageApplicationModelProvider();
razorProject.Setup(p => p.EnumerateItems("/"))
.Returns(new[]
{
GetProjectItem("/", "/Index.cshtml", "<h1>Hello world</h1>"),
});
var provider = new PageActionDescriptorProvider( var provider = new PageActionDescriptorProvider(
razorProject.Object, new[] { applicationModelProvider },
GetAccessor<MvcOptions>(), GetAccessor<MvcOptions>(),
GetRazorPagesOptions()); GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext(); var context = new ActionDescriptorProviderContext();
@ -42,17 +37,25 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
} }
[Fact] [Fact]
public void GetDescriptors_AddsDescriptorsForFileWithPageDirective() public void GetDescriptors_AddsDescriptorsForModelWithSelector()
{ {
// Arrange // Arrange
var razorProject = new Mock<RazorProject>(); var model = new PageApplicationModel("/Test.cshtml", "/Test")
razorProject.Setup(p => p.EnumerateItems("/")) {
.Returns(new[] Selectors =
{ {
GetProjectItem("/", "/Test.cshtml", $"@page{Environment.NewLine}<h1>Hello world</h1>"), new SelectorModel
}); {
AttributeRouteModel = new AttributeRouteModel
{
Template = "/Test/{id:int?}",
}
}
}
};
var applicationModelProvider = new TestPageApplicationModelProvider(model);
var provider = new PageActionDescriptorProvider( var provider = new PageActionDescriptorProvider(
razorProject.Object, new[] { applicationModelProvider },
GetAccessor<MvcOptions>(), GetAccessor<MvcOptions>(),
GetRazorPagesOptions()); GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext(); var context = new ActionDescriptorProviderContext();
@ -65,53 +68,49 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var descriptor = Assert.IsType<PageActionDescriptor>(result); var descriptor = Assert.IsType<PageActionDescriptor>(result);
Assert.Equal("/Test.cshtml", descriptor.RelativePath); Assert.Equal("/Test.cshtml", descriptor.RelativePath);
Assert.Equal("/Test", descriptor.RouteValues["page"]); Assert.Equal("/Test", descriptor.RouteValues["page"]);
Assert.Equal("Test", descriptor.AttributeRouteInfo.Template); Assert.Equal("/Test/{id:int?}", descriptor.AttributeRouteInfo.Template);
} }
[Fact] [Fact]
public void GetDescriptors_AddsDescriptorsForFileWithPageDirectiveAndRouteTemplate() public void GetDescriptors_AddsActionDescriptorForEachSelector()
{ {
// Arrange // Arrange
var razorProject = new Mock<RazorProject>(); var applicationModelProvider = new TestPageApplicationModelProvider(
razorProject.Setup(p => p.EnumerateItems("/")) new PageApplicationModel("/base-path/Test.cshtml", "/base-path/Test")
.Returns(new[]
{ {
GetProjectItem("/", "/Test.cshtml", $"@page \"Home\" {Environment.NewLine}<h1>Hello world</h1>"), Selectors =
}); {
var provider = new PageActionDescriptorProvider( CreateSelectorModel("base-path/Test/Home")
razorProject.Object, }
GetAccessor<MvcOptions>(), },
GetRazorPagesOptions()); new PageApplicationModel("/base-path/Index.cshtml", "/base-path/Index")
var context = new ActionDescriptorProviderContext();
// Act
provider.OnProvidersExecuting(context);
// Assert
var result = Assert.Single(context.Results);
var descriptor = Assert.IsType<PageActionDescriptor>(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<RazorProject>(MockBehavior.Strict);
razorProject.Setup(p => p.EnumerateItems("/"))
.Returns(new[]
{ {
GetProjectItem("/", "/base-path/Test.cshtml", $"@page \"Home\" {Environment.NewLine}<h1>Hello world</h1>"), Selectors =
GetProjectItem("/", "/base-path/Index.cshtml", $"@page {Environment.NewLine}"), {
GetProjectItem("/", "/base-path/Admin/Index.cshtml", $"@page{Environment.NewLine}"), CreateSelectorModel("base-path/Index"),
GetProjectItem("/", "/base-path/Admin/User.cshtml", $"@page{Environment.NewLine}"), 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 options = GetRazorPagesOptions();
var provider = new PageActionDescriptorProvider( var provider = new PageActionDescriptorProvider(
razorProject.Object, new[] { applicationModelProvider },
GetAccessor<MvcOptions>(), GetAccessor<MvcOptions>(),
options); options);
var context = new ActionDescriptorProviderContext(); var context = new ActionDescriptorProviderContext();
@ -123,122 +122,39 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
Assert.Collection(context.Results, Assert.Collection(context.Results,
result => Assert.Equal("base-path/Test/Home", result.AttributeRouteInfo.Template), result => Assert.Equal("base-path/Test/Home", result.AttributeRouteInfo.Template),
result => Assert.Equal("base-path/Index", 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/Index", result.AttributeRouteInfo.Template),
result => Assert.Equal("base-path/Admin", result.AttributeRouteInfo.Template), result => Assert.Equal("base-path/Admin", result.AttributeRouteInfo.Template),
result => Assert.Equal("base-path/Admin/User", result.AttributeRouteInfo.Template)); result => Assert.Equal("base-path/Admin/User", result.AttributeRouteInfo.Template));
} }
[Fact] private static SelectorModel CreateSelectorModel(string template)
public void GetDescriptors_UsesBasePathOption_WhenGeneratingRouteTemplate()
{ {
// Arrange return new SelectorModel
var razorProject = new Mock<RazorProject>(MockBehavior.Strict); {
razorProject.Setup(p => p.EnumerateItems("/base-path")) AttributeRouteModel = new AttributeRouteModel
.Returns(new[]
{ {
GetProjectItem("/base-path", "/Test.cshtml", $"@page \"Home\" {Environment.NewLine}<h1>Hello world</h1>"), Template = template,
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<MvcOptions>(),
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>();
razorProject.Setup(p => p.EnumerateItems("/"))
.Returns(new[]
{
GetProjectItem("/", "/Test.cshtml", $"@page \"{template}\" {Environment.NewLine}<h1>Hello world</h1>"),
});
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor<MvcOptions>(),
GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext();
// Act and Assert
var ex = Assert.Throws<InvalidOperationException>(() => 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);
} }
[Fact] [Fact]
public void GetDescriptors_WithEmptyPageDirective_MapsIndexToEmptySegment() public void GetDescriptors_AddsMultipleDescriptorsForPageWithMultipleSelectors()
{ {
// Arrange // Arrange
var razorProject = new Mock<RazorProject>(); var applicationModelProvider = new TestPageApplicationModelProvider(
razorProject.Setup(p => p.EnumerateItems("/")) new PageApplicationModel("/Catalog/Details/Index.cshtml", "/Catalog/Details/Index")
.Returns(new[]
{ {
GetProjectItem("", "/About/Index.cshtml", $"@page {Environment.NewLine}"), Selectors =
{
CreateSelectorModel("/Catalog/Details/Index/{id:int?}"),
CreateSelectorModel("/Catalog/Details/{id:int?}"),
},
}); });
var provider = new PageActionDescriptorProvider( var provider = new PageActionDescriptorProvider(
razorProject.Object, new[] { applicationModelProvider },
GetAccessor<MvcOptions>(),
GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext();
// Act
provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(context.Results,
result =>
{
var descriptor = Assert.IsType<PageActionDescriptor>(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<PageActionDescriptor>(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>();
razorProject.Setup(p => p.EnumerateItems("/"))
.Returns(new[]
{
GetProjectItem("", "/Catalog/Details/Index.cshtml", $"@page \"{{id:int?}}\" {Environment.NewLine}"),
});
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor<MvcOptions>(), GetAccessor<MvcOptions>(),
GetRazorPagesOptions()); GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext(); var context = new ActionDescriptorProviderContext();
@ -253,14 +169,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var descriptor = Assert.IsType<PageActionDescriptor>(result); var descriptor = Assert.IsType<PageActionDescriptor>(result);
Assert.Equal("/Catalog/Details/Index.cshtml", descriptor.RelativePath); Assert.Equal("/Catalog/Details/Index.cshtml", descriptor.RelativePath);
Assert.Equal("/Catalog/Details/Index", descriptor.RouteValues["page"]); 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 => result =>
{ {
var descriptor = Assert.IsType<PageActionDescriptor>(result); var descriptor = Assert.IsType<PageActionDescriptor>(result);
Assert.Equal("/Catalog/Details/Index.cshtml", descriptor.RelativePath); Assert.Equal("/Catalog/Details/Index.cshtml", descriptor.RelativePath);
Assert.Equal("/Catalog/Details/Index", descriptor.RouteValues["page"]); 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 // Arrange
var options = new MvcOptions(); var options = new MvcOptions();
var razorProject = new Mock<RazorProject>(); var applicationModelProvider = new TestPageApplicationModelProvider(CreateModel());
razorProject.Setup(p => p.EnumerateItems("/"))
.Returns(new[]
{
GetProjectItem("/", "/Home.cshtml", $"@page {Environment.NewLine}"),
});
var provider = new PageActionDescriptorProvider( var provider = new PageActionDescriptorProvider(
razorProject.Object, new[] { applicationModelProvider },
GetAccessor(options), GetAccessor(options),
GetRazorPagesOptions()); GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext(); var context = new ActionDescriptorProviderContext();
@ -310,14 +221,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var options = new MvcOptions(); var options = new MvcOptions();
options.Filters.Add(filter1); options.Filters.Add(filter1);
options.Filters.Add(filter2); options.Filters.Add(filter2);
var razorProject = new Mock<RazorProject>(); var applicationModelProvider = new TestPageApplicationModelProvider(CreateModel());
razorProject.Setup(p => p.EnumerateItems("/"))
.Returns(new[]
{
GetProjectItem("/", "/Home.cshtml", $"@page {Environment.NewLine}"),
});
var provider = new PageActionDescriptorProvider( var provider = new PageActionDescriptorProvider(
razorProject.Object, new[] { applicationModelProvider },
GetAccessor(options), GetAccessor(options),
GetRazorPagesOptions()); GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext(); var context = new ActionDescriptorProviderContext();
@ -368,15 +274,9 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
}); });
var razorOptions = GetRazorPagesOptions(); var razorOptions = GetRazorPagesOptions();
razorOptions.Value.Conventions.Add(convention.Object); razorOptions.Value.Conventions.Add(convention.Object);
var applicationModelProvider = new TestPageApplicationModelProvider(CreateModel());
var razorProject = new Mock<RazorProject>();
razorProject.Setup(p => p.EnumerateItems("/"))
.Returns(new[]
{
GetProjectItem("/", "/Home.cshtml", $"@page {Environment.NewLine}"),
});
var provider = new PageActionDescriptorProvider( var provider = new PageActionDescriptorProvider(
razorProject.Object, new[] { applicationModelProvider },
GetAccessor(options), GetAccessor(options),
razorOptions); razorOptions);
var context = new ActionDescriptorProviderContext(); 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<TOptions> GetAccessor<TOptions>(TOptions options = null) private static IOptions<TOptions> GetAccessor<TOptions>(TOptions options = null)
where TOptions : class, new() where TOptions : class, new()
{ {
@ -432,5 +349,30 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
return new DefaultRazorProjectItem(testFileInfo, basePath, path); return new DefaultRazorProjectItem(testFileInfo, basePath, path);
} }
private class TestPageApplicationModelProvider : IPageApplicationModelProvider
{
private readonly PageApplicationModel[] _models;
public TestPageApplicationModelProvider(params PageApplicationModel[] models)
{
_models = models ?? Array.Empty<PageApplicationModel>();
}
public int Order => 0;
public void OnProvidersExecuted(PageApplicationModelProviderContext context)
{
}
public void OnProvidersExecuting(PageApplicationModelProviderContext context)
{
foreach (var model in _models)
{
context.Results.Add(model);
}
}
}
} }
} }

View File

@ -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<InvalidOperationException>(() => provider.OnProvidersExecuting(context));
Assert.Equal("The route for the page at '/Pages/Home.cshtml' cannot start with / or ~/. Pages do not support overriding the file path of the page.",
ex.Message);
}
public class TestCompiledPageApplicationModelProvider : CompiledPageApplicationModelProvider
{
private readonly IEnumerable<CompiledPageInfo> _info;
public TestCompiledPageApplicationModelProvider(IEnumerable<CompiledPageInfo> info, RazorPagesOptions options)
: base(new ApplicationPartManager(), new TestOptionsManager<RazorPagesOptions>(options))
{
_info = info;
}
protected override IEnumerable<CompiledPageInfo> GetCompiledPages() => _info;
}
}
}

View File

@ -723,8 +723,10 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
} }
[Fact] [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 // Arrange
var descriptor = new PageActionDescriptor() var descriptor = new PageActionDescriptor()
{ {
@ -739,6 +741,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
var actionDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>(); var actionDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection); actionDescriptorProvider.Setup(p => p.ActionDescriptors).Returns(descriptorCollection);
var pageFactory = new Mock<IRazorPageFactoryProvider>();
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 // No files
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
var razorProject = new TestRazorProject(fileProvider); var razorProject = new TestRazorProject(fileProvider);
@ -748,16 +758,16 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
actionDescriptorProvider.Object, actionDescriptorProvider.Object,
pageProvider: null, pageProvider: null,
modelProvider: null, modelProvider: null,
razorPageFactoryProvider: CreateRazorPageFactoryProvider(), razorPageFactoryProvider: pageFactory.Object,
razorProject: razorProject); razorProject: razorProject);
var compiledDescriptor = CreateCompiledPageActionDescriptor(descriptor); var compiledDescriptor = CreateCompiledPageActionDescriptor(descriptor);
// Act // Act
var factories = invokerProvider.GetViewStartFactories(compiledDescriptor); var factories = invokerProvider.GetViewStartFactories(compiledDescriptor).ToList();
// Assert // Assert
Assert.Empty(factories); Assert.Equal(2, factories.Count);
} }
private IRazorPageFactoryProvider CreateRazorPageFactoryProvider() private IRazorPageFactoryProvider CreateRazorPageFactoryProvider()

View File

@ -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<RazorPagesOptions>();
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<RazorPagesOptions>();
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<RazorPagesOptions>();
optionsManager.Value.RootDirectory = "/";
var provider = new RazorProjectPageApplicationModelProvider(project, optionsManager);
var context = new PageApplicationModelProviderContext();
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => 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<RazorPagesOptions>();
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<RazorPagesOptions>();
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);
});
}
}
}

View File

@ -213,7 +213,8 @@ namespace Microsoft.AspNetCore.Mvc
feature => Assert.IsType<ControllerFeatureProvider>(feature), feature => Assert.IsType<ControllerFeatureProvider>(feature),
feature => Assert.IsType<ViewComponentFeatureProvider>(feature), feature => Assert.IsType<ViewComponentFeatureProvider>(feature),
feature => Assert.IsType<MetadataReferenceFeatureProvider>(feature), feature => Assert.IsType<MetadataReferenceFeatureProvider>(feature),
feature => Assert.IsType<ViewsFeatureProvider>(feature)); feature => Assert.IsType<ViewsFeatureProvider>(feature),
feature => Assert.IsType<CompiledPageFeatureProvider>(feature));
} }
[Fact] [Fact]
@ -419,6 +420,14 @@ namespace Microsoft.AspNetCore.Mvc
typeof(JsonPatchOperationsArrayProvider), typeof(JsonPatchOperationsArrayProvider),
} }
}, },
{
typeof(IPageApplicationModelProvider),
new[]
{
typeof(CompiledPageApplicationModelProvider),
typeof(RazorProjectPageApplicationModelProvider),
}
},
}; };
} }
} }

View File

@ -1,23 +1,41 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
namespace Microsoft.AspNetCore.Mvc.TestCommon namespace Microsoft.AspNetCore.Mvc.TestCommon
{ {
public class TestDirectoryContent : IDirectoryContents public class TestDirectoryContent : IDirectoryContents, IFileInfo
{ {
private readonly IEnumerable<IFileInfo> _files; private readonly IEnumerable<IFileInfo> _files;
public TestDirectoryContent(IEnumerable<IFileInfo> files) public TestDirectoryContent(string name, IEnumerable<IFileInfo> files)
{ {
Name = name;
_files = files; _files = files;
} }
public bool Exists => true; 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<IFileInfo> GetEnumerator() => _files.GetEnumerator(); public IEnumerator<IFileInfo> GetEnumerator() => _files.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

View File

@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
public TestDirectoryContent AddDirectoryContent(string path, IEnumerable<IFileInfo> files) public TestDirectoryContent AddDirectoryContent(string path, IEnumerable<IFileInfo> files)
{ {
var directoryContent = new TestDirectoryContent(files); var directoryContent = new TestDirectoryContent(Path.GetFileName(path), files);
_directoryContentsLookup[path] = directoryContent; _directoryContentsLookup[path] = directoryContent;
return directoryContent; return directoryContent;
} }