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.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
/// </summary>
public class ViewsFeatureProvider : IApplicationFeatureProvider<ViewsFeature>
{
/// <summary>
/// Gets the suffix for the view assembly.
/// </summary>
public static readonly string PrecompiledViewsAssemblySuffix = ".PrecompiledViews";
/// <summary>
@ -30,19 +25,19 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
/// </summary>
public static readonly string ViewInfoContainerTypeName = "__PrecompiledViewCollection";
private static readonly string FullyQualifiedManifestTypeName = ViewInfoContainerNamespace + "." + ViewInfoContainerTypeName;
/// <inheritdoc />
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewsFeature feature)
{
foreach (var assemblyPart in parts.OfType<AssemblyPart>())
{
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
/// </summary>
/// <param name="assemblyPart">The <see cref="AssemblyPart"/>.</param>
/// <returns>The <see cref="ViewInfoContainer"/> <see cref="Type"/>.</returns>
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;
}
}
}

View File

@ -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<IConfigureOptions<RazorViewEngineOptions>, RazorViewEngineOptionsSetup>());
#pragma warning restore 0618
services.TryAddEnumerable(
ServiceDescriptor.Transient<
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>
/// Gets or sets the application root relative path for the page.
/// Gets the application root relative path for the page.
/// </summary>
public string RelativePath { get; }
/// <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>
public string ViewEnginePath { get; }
/// <summary>
/// Gets or sets the applicable <see cref="IFilterMetadata"/> instances.
/// Gets the applicable <see cref="IFilterMetadata"/> instances.
/// </summary>
public IList<IFilterMetadata> Filters { get; }
@ -79,7 +79,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
public IDictionary<object, object> Properties { get; }
/// <summary>
/// Gets or sets the <see cref="SelectorModel"/> instances.
/// Gets the <see cref="SelectorModel"/> instances.
/// </summary>
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.
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<RazorViewEngineOptions> setupAction)
Action<RazorPagesOptions> 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<CompiledPageFeatureProvider>().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<IConfigureOptions<RazorPagesOptions>, RazorPagesOptionsSetup>());
// Action Invoker
// Action description and invocation
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IActionDescriptorProvider, PageActionDescriptorProvider>());
services.TryAddSingleton<IActionDescriptorChangeProvider, PageActionDescriptorChangeProvider>();
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPageApplicationModelProvider, RazorProjectPageApplicationModelProvider>());
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPageApplicationModelProvider, CompiledPageApplicationModelProvider>());
services.TryAddEnumerable(
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.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<IPageApplicationModelProvider> _applicationModelProviders;
private readonly MvcOptions _mvcOptions;
private readonly RazorPagesOptions _pagesOptions;
public PageActionDescriptorProvider(
RazorProject project,
IEnumerable<IPageApplicationModelProvider> pageMetadataProviders,
IOptions<MvcOptions> mvcOptionsAccessor,
IOptions<RazorPagesOptions> 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<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)
{
}
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++)
{
_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<object, object>(model.Properties),
RelativePath = item.CombinedPath,
RelativePath = model.RelativePath,
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));
}
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";
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();
}

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

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

View File

@ -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>();
razorProject.Setup(p => p.EnumerateItems("/"))
.Returns(new[]
{
GetProjectItem("/", "/Index.cshtml", "<h1>Hello world</h1>"),
});
var applicationModelProvider = new TestPageApplicationModelProvider();
var provider = new PageActionDescriptorProvider(
razorProject.Object,
new[] { applicationModelProvider },
GetAccessor<MvcOptions>(),
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>();
razorProject.Setup(p => p.EnumerateItems("/"))
.Returns(new[]
var model = new PageApplicationModel("/Test.cshtml", "/Test")
{
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(
razorProject.Object,
new[] { applicationModelProvider },
GetAccessor<MvcOptions>(),
GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext();
@ -65,53 +68,49 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var descriptor = Assert.IsType<PageActionDescriptor>(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>();
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}<h1>Hello world</h1>"),
});
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor<MvcOptions>(),
GetRazorPagesOptions());
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[]
Selectors =
{
CreateSelectorModel("base-path/Test/Home")
}
},
new PageApplicationModel("/base-path/Index.cshtml", "/base-path/Index")
{
GetProjectItem("/", "/base-path/Test.cshtml", $"@page \"Home\" {Environment.NewLine}<h1>Hello world</h1>"),
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<MvcOptions>(),
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<RazorProject>(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}<h1>Hello world</h1>"),
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);
Template = template,
}
};
}
[Fact]
public void GetDescriptors_WithEmptyPageDirective_MapsIndexToEmptySegment()
public void GetDescriptors_AddsMultipleDescriptorsForPageWithMultipleSelectors()
{
// Arrange
var razorProject = new Mock<RazorProject>();
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<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,
new[] { applicationModelProvider },
GetAccessor<MvcOptions>(),
GetRazorPagesOptions());
var context = new ActionDescriptorProviderContext();
@ -253,14 +169,14 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
var descriptor = Assert.IsType<PageActionDescriptor>(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<PageActionDescriptor>(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>();
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>();
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>();
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<TOptions> GetAccessor<TOptions>(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<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]
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<IActionDescriptorCollectionProvider>();
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
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()

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

View File

@ -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<IFileInfo> _files;
public TestDirectoryContent(IEnumerable<IFileInfo> files)
public TestDirectoryContent(string name, IEnumerable<IFileInfo> 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<IFileInfo> GetEnumerator() => _files.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

View File

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