Adding PageActionDescriptorProvider

Fixes #5353
This commit is contained in:
Pranav K 2016-10-24 17:43:16 -07:00
parent 5b2a4aecb6
commit 52ee9afc31
19 changed files with 941 additions and 42 deletions

View File

@ -99,6 +99,33 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
};
}
/// <summary>
/// Combines the prefix and route template for an attribute route.
/// </summary>
/// <param name="prefix">The prefix.</param>
/// <param name="template">The route template.</param>
/// <returns>The combined pattern.</returns>
public static string CombineTemplates(string prefix, string template)
{
var result = CombineCore(prefix, template);
return CleanTemplate(result);
}
/// <summary>
/// Determines if a template pattern can be used to override a prefix.
/// </summary>
/// <param name="template">The template.</param>
/// <returns><c>true</c> if this is an overriding template, <c>false</c> otherwise.</returns>
/// <remarks>
/// Route templates starting with "~/" or "/" can be used to override the prefix.
/// </remarks>
public static bool IsOverridePattern(string template)
{
return template != null &&
(template.StartsWith("~/", StringComparison.Ordinal) ||
template.StartsWith("/", StringComparison.Ordinal));
}
private static string ChooseName(
AttributeRouteModel left,
AttributeRouteModel right)
@ -113,12 +140,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
}
}
internal static string CombineTemplates(string left, string right)
{
var result = CombineCore(left, right);
return CleanTemplate(result);
}
private static string CombineCore(string left, string right)
{
if (left == null && right == null)
@ -143,13 +164,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
return left + "/" + right;
}
private static bool IsOverridePattern(string template)
{
return template != null &&
(template.StartsWith("~/", StringComparison.Ordinal) ||
template.StartsWith("/", StringComparison.Ordinal));
}
private static bool IsEmptyLeftSegment(string template)
{
return template == null ||

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Abstractions;
@ -14,7 +13,6 @@ using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing.Tree;
namespace Microsoft.AspNetCore.Mvc.Internal
{

View File

@ -1306,22 +1306,6 @@ namespace Microsoft.AspNetCore.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("MiddlewareFilter_ServiceResolutionFail"), p0, p1, p2, p3);
}
/// <summary>
/// Unable to create an instance of type '{0}'. The type specified in {1} must not be abstract and must have a parameterless constructor.
/// </summary>
internal static string MiddlewareFilterConfigurationProvider_CreateConfigureDelegate_CannotCreateType
{
get { return GetString("MiddlewareFilterConfigurationProvider_CreateConfigureDelegate_CannotCreateType"); }
}
/// <summary>
/// Unable to create an instance of type '{0}'. The type specified in {1} must not be abstract and must have a parameterless constructor.
/// </summary>
internal static string FormatMiddlewareFilterConfigurationProvider_CreateConfigureDelegate_CannotCreateType(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("MiddlewareFilterConfigurationProvider_CreateConfigureDelegate_CannotCreateType"), p0, p1);
}
/// <summary>
/// An {0} cannot be created without a valid instance of {1}.
/// </summary>
@ -1355,7 +1339,7 @@ namespace Microsoft.AspNetCore.Mvc.Core
}
/// <summary>
/// VaryByQueryKeys requires the response cache middleware.
/// '{0}' requires the response cache middleware.
/// </summary>
internal static string VaryByQueryKeys_Requires_ResponseCachingMiddleware
{
@ -1363,7 +1347,7 @@ namespace Microsoft.AspNetCore.Mvc.Core
}
/// <summary>
/// VaryByQueryKeys requires the response cache middleware.
/// '{0}' requires the response cache middleware.
/// </summary>
internal static string FormatVaryByQueryKeys_Requires_ResponseCachingMiddleware(object p0)
{
@ -1386,6 +1370,22 @@ namespace Microsoft.AspNetCore.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("CandidateResolver_DifferentCasedReference"), p0);
}
/// <summary>
/// Unable to create an instance of type '{0}'. The type specified in {1} must not be abstract and must have a parameterless constructor.
/// </summary>
internal static string MiddlewareFilterConfigurationProvider_CreateConfigureDelegate_CannotCreateType
{
get { return GetString("MiddlewareFilterConfigurationProvider_CreateConfigureDelegate_CannotCreateType"); }
}
/// <summary>
/// Unable to create an instance of type '{0}'. The type specified in {1} must not be abstract and must have a parameterless constructor.
/// </summary>
internal static string FormatMiddlewareFilterConfigurationProvider_CreateConfigureDelegate_CannotCreateType(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("MiddlewareFilterConfigurationProvider_CreateConfigureDelegate_CannotCreateType"), p0, p1);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -0,0 +1,17 @@
// 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>
/// Allows customization of the of the <see cref="PageModel"/>.
/// </summary>
public interface IPageModelConvention
{
/// <summary>
/// Called to apply the convention to the <see cref="PageModel"/>.
/// </summary>
/// <param name="model">The <see cref="PageModel"/>.</param>
void Apply(PageModel model);
}
}

View File

@ -0,0 +1,86 @@
// 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.Filters;
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
{
/// <summary>
/// Application model component for RazorPages.
/// </summary>
public class PageModel
{
/// <summary>
/// Initializes a new instance of <see cref="PageModel"/>.
/// </summary>
/// <param name="relativePath">The application relative path of the page.</param>
/// <param name="viewEnginePath">The path relative to the base path for page discovery.</param>
public PageModel(string relativePath, string viewEnginePath)
{
if (relativePath == null)
{
throw new ArgumentNullException(nameof(relativePath));
}
if (viewEnginePath == null)
{
throw new ArgumentNullException(nameof(viewEnginePath));
}
RelativePath = relativePath;
ViewEnginePath = viewEnginePath;
Filters = new List<IFilterMetadata>();
Properties = new Dictionary<object, object>();
Selectors = new List<SelectorModel>();
}
/// <summary>
/// A copy constructor for <see cref="PageModel"/>.
/// </summary>
/// <param name="other">The <see cref="PageModel"/> to copy from.</param>
public PageModel(PageModel other)
{
if (other == null)
{
throw new ArgumentNullException(nameof(other));
}
RelativePath = other.RelativePath;
ViewEnginePath = other.ViewEnginePath;
Filters = new List<IFilterMetadata>(other.Filters);
Properties = new Dictionary<object, object>(other.Properties);
Selectors = new List<SelectorModel>(other.Selectors.Select(m => new SelectorModel(m)));
}
/// <summary>
/// Gets or sets 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.
/// </summary>
public string ViewEnginePath { get; }
/// <summary>
/// Gets or sets the applicable <see cref="IFilterMetadata"/> instances.
/// </summary>
public IList<IFilterMetadata> Filters { get; }
/// <summary>
/// Stores arbitrary metadata properties associated with the <see cref="PageModel"/>.
/// </summary>
public IDictionary<object, object> Properties { get; }
/// <summary>
/// Gets or sets the <see cref="SelectorModel"/> instances.
/// </summary>
public IList<SelectorModel> Selectors { get; }
}
}

View File

@ -0,0 +1,127 @@
// 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.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.Evolution;
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 MvcOptions _mvcOptions;
private readonly RazorPagesOptions _pagesOptions;
public PageActionDescriptorProvider(
RazorProject project,
IOptions<MvcOptions> mvcOptionsAccessor,
IOptions<RazorPagesOptions> pagesOptionsAccessor)
{
_project = project;
_mvcOptions = mvcOptionsAccessor.Value;
_pagesOptions = pagesOptionsAccessor.Value;
}
public int Order { get; set; }
public void OnProvidersExecuting(ActionDescriptorProviderContext context)
{
foreach (var item in _project.EnumerateItems("/"))
{
if (item.Filename.StartsWith("_"))
{
// Pages like _PageImports should not be routable.
continue;
}
string template;
if (!PageDirectiveFeature.TryGetRouteTemplate(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);
}
}
public void OnProvidersExecuted(ActionDescriptorProviderContext context)
{
}
private void AddActionDescriptors(IList<ActionDescriptor> actions, RazorProjectItem item, string template)
{
var model = new PageModel(item.CombinedPath, item.PathWithoutExtension);
var routePrefix = item.BasePath == "/" ? item.PathWithoutExtension : item.BasePath + item.PathWithoutExtension;
model.Selectors.Add(CreateSelectorModel(routePrefix, template));
if (string.Equals(IndexFileName, item.Filename, StringComparison.OrdinalIgnoreCase))
{
model.Selectors.Add(CreateSelectorModel(item.BasePath, template));
}
for (var i = 0; i < _pagesOptions.Conventions.Count; i++)
{
_pagesOptions.Conventions[i].Apply(model);
}
var filters = new List<FilterDescriptor>(_mvcOptions.Filters.Count + model.Filters.Count);
for (var i = 0; i < _mvcOptions.Filters.Count; i++)
{
filters.Add(new FilterDescriptor(_mvcOptions.Filters[i], FilterScope.Global));
}
for (var i = 0; i < model.Filters.Count; i++)
{
filters.Add(new FilterDescriptor(model.Filters[i], FilterScope.Action));
}
foreach (var selector in model.Selectors)
{
actions.Add(new PageActionDescriptor()
{
AttributeRouteInfo = new AttributeRouteInfo()
{
Name = selector.AttributeRouteModel.Name,
Order = selector.AttributeRouteModel.Order ?? 0,
Template = selector.AttributeRouteModel.Template,
},
DisplayName = $"Page: {item.Path}",
FilterDescriptors = filters,
Properties = new Dictionary<object, object>(model.Properties),
RelativePath = item.CombinedPath,
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "page", item.PathWithoutExtension },
},
ViewEnginePath = item.Path,
});
}
}
private static SelectorModel CreateSelectorModel(string prefix, string template)
{
return new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Template = AttributeRouteModel.CombineTemplates(prefix, template),
}
};
}
}
}

View File

@ -0,0 +1,33 @@
// 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.Razor.Evolution;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
public static class PageDirectiveFeature
{
public static bool TryGetRouteTemplate(RazorProjectItem projectItem, out string template)
{
const string PageDirective = "@page";
string content;
using (var streamReader = new StreamReader(projectItem.Read()))
{
content = streamReader.ReadToEnd();
}
if (content.StartsWith(PageDirective, StringComparison.Ordinal))
{
var newLineIndex = content.IndexOf(Environment.NewLine, PageDirective.Length);
template = content.Substring(PageDirective.Length, newLineIndex - PageDirective.Length).Trim();
return true;
}
template = null;
return false;
}
}
}

View File

@ -0,0 +1,59 @@
// 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.IO;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.FileProviders;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class DefaultRazorProject : RazorProject
{
private const string RazorFileExtension = ".cshtml";
private readonly IFileProvider _provider;
public DefaultRazorProject(IFileProvider provider)
{
_provider = provider;
}
public override IEnumerable<RazorProjectItem> EnumerateItems(string path)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
if (path.Length == 0 || path[0] != '/')
{
throw new ArgumentException(Resources.RazorProject_PathMustStartWithForwardSlash);
}
return EnumerateFiles(_provider.GetDirectoryContents(path), path, "");
}
private IEnumerable<RazorProjectItem> EnumerateFiles(IDirectoryContents directory, string basePath, string prefix)
{
if (directory.Exists)
{
foreach (var file in directory)
{
if (file.IsDirectory)
{
var children = EnumerateFiles(_provider.GetDirectoryContents(file.PhysicalPath), basePath, prefix + "/" + file.Name);
foreach (var child in children)
{
yield return child;
}
}
else if (string.Equals(RazorFileExtension, Path.GetExtension(file.Name), StringComparison.OrdinalIgnoreCase))
{
yield return new DefaultRazorProjectItem(file, basePath, prefix + "/" + file.Name);
}
}
}
}
}
}

View File

@ -0,0 +1,32 @@
// 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.IO;
using Microsoft.AspNetCore.Razor.Evolution;
using Microsoft.Extensions.FileProviders;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
{
public class DefaultRazorProjectItem : RazorProjectItem
{
private readonly IFileInfo _fileInfo;
public DefaultRazorProjectItem(IFileInfo fileInfo, string basePath, string path)
{
_fileInfo = fileInfo;
BasePath = basePath;
Path = path;
}
public override string BasePath { get; }
public override string Path { get; }
public override string PhysicalPath => _fileInfo.PhysicalPath;
public override Stream Read()
{
return _fileInfo.CreateReadStream();
}
}
}

View File

@ -0,0 +1,16 @@
// 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.Diagnostics;
using Microsoft.AspNetCore.Mvc.Abstractions;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
[DebuggerDisplay("{" + nameof(ViewEnginePath) + "}")]
public class PageActionDescriptor : ActionDescriptor
{
public string RelativePath { get; set; }
public string ViewEnginePath { get; set; }
}
}

View File

@ -0,0 +1,62 @@
// <auto-generated />
namespace Microsoft.AspNetCore.Mvc.RazorPages
{
using System.Globalization;
using System.Reflection;
using System.Resources;
internal static class Resources
{
private static readonly ResourceManager _resourceManager
= new ResourceManager("Microsoft.AspNetCore.Mvc.RazorPages.Resources", typeof(Resources).GetTypeInfo().Assembly);
/// <summary>
/// The route for the page at '{0}' cannot start with / or ~/. Pages do not support overriding the file path of the page.
/// </summary>
internal static string PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable
{
get { return GetString("PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable"); }
}
/// <summary>
/// The route for the page at '{0}' cannot start with / or ~/. Pages do not support overriding the file path of the page.
/// </summary>
internal static string FormatPageActionDescriptorProvider_RouteTemplateCannotBeOverrideable(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable"), p0);
}
/// <summary>
/// Path must begin with a forward slash '/'.
/// </summary>
internal static string RazorProject_PathMustStartWithForwardSlash
{
get { return GetString("RazorProject_PathMustStartWithForwardSlash"); }
}
/// <summary>
/// Path must begin with a forward slash '/'.
/// </summary>
internal static string FormatRazorProject_PathMustStartWithForwardSlash()
{
return GetString("RazorProject_PathMustStartWithForwardSlash");
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
System.Diagnostics.Debug.Assert(value != null);
if (formatterNames != null)
{
for (var i = 0; i < formatterNames.Length; i++)
{
value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
}
}
return value;
}
}
}

View File

@ -0,0 +1,20 @@
// 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.ApplicationModels;
namespace Microsoft.AspNetCore.Mvc.RazorPages
{
/// <summary>
/// Provides configuration for RazorPages.
/// </summary>
public class RazorPagesOptions
{
/// <summary>
/// Gets a list of <see cref="IPageModelConvention"/> instances that will be applied to
/// the <see cref="PageModel"/> when discovering Razor Pages.
/// </summary>
public IList<IPageModelConvention> Conventions { get; } = new List<IPageModelConvention>();
}
}

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="PageActionDescriptorProvider_RouteTemplateCannotBeOverrideable" xml:space="preserve">
<value>The route for the page at '{0}' cannot start with / or ~/. Pages do not support overriding the file path of the page.</value>
</data>
<data name="RazorProject_PathMustStartWithForwardSlash" xml:space="preserve">
<value>Path must begin with a forward slash '/'.</value>
</data>
</root>

View File

@ -22,6 +22,7 @@
"xmlDoc": true
},
"dependencies": {
"Microsoft.AspNetCore.Mvc.ViewFeatures": { "target": "project" },
"Microsoft.AspNetCore.Razor.Evolution": "1.0.0-*",
"NETStandard.Library": "1.6.1-*"
},

View File

@ -139,7 +139,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
}
/// <summary>
/// Cannot determine an '{4}' attribute for {0}. A {0} with a specified '{1}' must not have an '{2}' or '{3}' attribute.
/// Cannot determine an '{4}' attribute for {0}. A {0} with a specified '{1}' must not have an '{2}', '{3}', or '{5}' attribute.
/// </summary>
internal static string FormTagHelper_CannotDetermineActionWithRouteAndActionOrControllerSpecified
{
@ -171,7 +171,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
}
/// <summary>
/// Cannot override the '{6}' attribute for &lt;{0}&gt;. &lt;{0}&gt; elements with a specified '{6}' must not have attributes starting with '{5}' or an '{1}', '{2}', '{3}', or '{4}' attribute.
/// Cannot override the '{7}' attribute for &lt;{0}&gt;. &lt;{0}&gt; elements with a specified '{7}' must not have attributes starting with '{6}' or an '{1}', '{2}', '{3}', '{4}', or '{5}' attribute.
/// </summary>
internal static string FormActionTagHelper_CannotOverrideFormAction
{
@ -179,7 +179,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
}
/// <summary>
/// Cannot override the '{6}' attribute for &lt;{0}&gt;. &lt;{0}&gt; elements with a specified '{7}' must not have attributes starting with '{6}' or an '{1}', '{2}', '{3}', '{4}', or '{5}' attribute.
/// Cannot override the '{7}' attribute for &lt;{0}&gt;. &lt;{0}&gt; elements with a specified '{7}' must not have attributes starting with '{6}' or an '{1}', '{2}', '{3}', '{4}', or '{5}' attribute.
/// </summary>
internal static string FormatFormActionTagHelper_CannotOverrideFormAction(object p0, object p1, object p2, object p3, object p4, object p5, object p6, object p7)
{
@ -187,7 +187,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
}
/// <summary>
/// Cannot determine a '{4}' attribute for &lt;{0}&gt;. &lt;{0}&gt; elements with a specified '{1}' must not have an '{2}' or '{3}' attribute.
/// Cannot determine a '{4}' attribute for &lt;{0}&gt;. &lt;{0}&gt; elements with a specified '{1}' must not have an '{2}', '{3}', or '{5}' attribute.
/// </summary>
internal static string FormActionTagHelper_CannotDetermineFormActionRouteActionOrControllerSpecified
{

View File

@ -14,7 +14,6 @@ using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing.Tree;
using Moq;
using Xunit;

View File

@ -0,0 +1,304 @@
// 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.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.RazorPages.Internal;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
using Microsoft.AspNetCore.Razor.Evolution;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
public class PageActionDescriptorProviderTest
{
[Fact]
public void GetDescriptors_DoesNotAddDescriptorsForFilesWithoutDirectives()
{
// Arrange
var razorProject = new Mock<RazorProject>();
razorProject.Setup(p => p.EnumerateItems("/"))
.Returns(new[]
{
GetProjectItem("/", "/Index.cshtml", "<h1>Hello world</h1>"),
});
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor<MvcOptions>(),
GetAccessor<RazorPagesOptions>());
var context = new ActionDescriptorProviderContext();
// Act
provider.OnProvidersExecuting(context);
// Assert
Assert.Empty(context.Results);
}
[Fact]
public void GetDescriptors_AddsDescriptorsForFileWithPageDirective()
{
// Arrange
var razorProject = new Mock<RazorProject>();
razorProject.Setup(p => p.EnumerateItems("/"))
.Returns(new[]
{
GetProjectItem("/", "/Test.cshtml", $"@page{Environment.NewLine}<h1>Hello world</h1>"),
});
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor<MvcOptions>(),
GetAccessor<RazorPagesOptions>());
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", descriptor.AttributeRouteInfo.Template);
}
[Fact]
public void GetDescriptors_AddsDescriptorsForFileWithPageDirectiveAndRouteTemplate()
{
// Arrange
var razorProject = new Mock<RazorProject>();
razorProject.Setup(p => p.EnumerateItems("/"))
.Returns(new[]
{
GetProjectItem("/", "/Test.cshtml", $"@page Home {Environment.NewLine}<h1>Hello world</h1>"),
});
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor<MvcOptions>(),
GetAccessor<RazorPagesOptions>());
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);
}
[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>(),
GetAccessor<RazorPagesOptions>());
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]
public void GetDescriptors_WithEmptyPageDirective_MapsIndexToEmptySegment()
{
// Arrange
var razorProject = new Mock<RazorProject>();
razorProject.Setup(p => p.EnumerateItems("/"))
.Returns(new[]
{
GetProjectItem("/About", "/Index.cshtml", $"@page {Environment.NewLine}"),
});
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor<MvcOptions>(),
GetAccessor<RazorPagesOptions>());
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("/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("/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<RazorPagesOptions>());
var context = new ActionDescriptorProviderContext();
// Act
provider.OnProvidersExecuting(context);
// Assert
Assert.Collection(context.Results,
result =>
{
var descriptor = Assert.IsType<PageActionDescriptor>(result);
Assert.Equal("/Catalog/Details/Index.cshtml", descriptor.RelativePath);
Assert.Equal("/Index", descriptor.RouteValues["page"]);
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("/Index", descriptor.RouteValues["page"]);
Assert.Equal("Catalog/Details/{id:int?}", descriptor.AttributeRouteInfo.Template);
});
}
[Fact]
public void GetDescriptors_AddsGlobalFilters()
{
// Arrange
var filter1 = Mock.Of<IFilterMetadata>();
var filter2 = Mock.Of<IFilterMetadata>();
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 provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor(options),
GetAccessor<RazorPagesOptions>());
var context = new ActionDescriptorProviderContext();
// Act
provider.OnProvidersExecuting(context);
// Assert
var result = Assert.Single(context.Results);
var descriptor = Assert.IsType<PageActionDescriptor>(result);
Assert.Collection(descriptor.FilterDescriptors,
filterDescriptor =>
{
Assert.Equal(FilterScope.Global, filterDescriptor.Scope);
Assert.Same(filter1, filterDescriptor.Filter);
},
filterDescriptor =>
{
Assert.Equal(FilterScope.Global, filterDescriptor.Scope);
Assert.Same(filter2, filterDescriptor.Filter);
});
}
[Fact]
public void GetDescriptors_AddsFiltersAddedByConvention()
{
// Arrange
var globalFilter = Mock.Of<IFilterMetadata>();
var localFilter = Mock.Of<IFilterMetadata>();
var options = new MvcOptions();
options.Filters.Add(globalFilter);
var convention = new Mock<IPageModelConvention>();
convention.Setup(c => c.Apply(It.IsAny<PageModel>()))
.Callback((PageModel model) =>
{
model.Filters.Add(localFilter);
});
var razorOptions = new RazorPagesOptions();
razorOptions.Conventions.Add(convention.Object);
var razorProject = new Mock<RazorProject>();
razorProject.Setup(p => p.EnumerateItems("/"))
.Returns(new[]
{
GetProjectItem("/", "/Home.cshtml", $"@page {Environment.NewLine}"),
});
var provider = new PageActionDescriptorProvider(
razorProject.Object,
GetAccessor(options),
GetAccessor(razorOptions));
var context = new ActionDescriptorProviderContext();
// Act
provider.OnProvidersExecuting(context);
// Assert
var result = Assert.Single(context.Results);
var descriptor = Assert.IsType<PageActionDescriptor>(result);
Assert.Collection(descriptor.FilterDescriptors,
filterDescriptor =>
{
Assert.Equal(FilterScope.Global, filterDescriptor.Scope);
Assert.Same(globalFilter, filterDescriptor.Filter);
},
filterDescriptor =>
{
Assert.Equal(FilterScope.Action, filterDescriptor.Scope);
Assert.Same(localFilter, filterDescriptor.Filter);
});
}
private static IOptions<TOptions> GetAccessor<TOptions>(TOptions options = null)
where TOptions : class, new()
{
var accessor = new Mock<IOptions<TOptions>>();
accessor.SetupGet(a => a.Value).Returns(options ?? new TOptions());
return accessor.Object;
}
private static RazorProjectItem GetProjectItem(string basePath, string path, string content)
{
var testFileInfo = new TestFileInfo
{
Content = content,
};
return new DefaultRazorProjectItem(testFileInfo, basePath, path);
}
}
}

View File

@ -4,7 +4,6 @@
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>0ab46520-f441-4e01-b444-08f4d23f8b1b</ProjectGuid>
@ -13,9 +12,11 @@
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>
</Project>

View File

@ -4,11 +4,15 @@
},
"dependencies": {
"dotnet-test-xunit": "2.2.0-*",
"Microsoft.AspNetCore.Testing": "1.2.0-*",
"Microsoft.AspNetCore.Mvc.RazorPages": "1.0.0-*",
"Microsoft.AspNetCore.Mvc.TestCommon": {
"target": "project"
},
"Microsoft.DotNet.InternalAbstractions": "1.0.0",
"Moq": "4.6.36-*",
"xunit": "2.2.0-*"
},
"testRunner": "xunit",
"frameworks": {
"netcoreapp1.1": {
"dependencies": {