Add support for conventional routes with dispatching (#7928)

This commit is contained in:
James Newton-King 2018-06-20 09:02:52 +12:00 committed by GitHub
parent 58aa16ee69
commit 3547341762
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1264 additions and 306 deletions

View File

@ -2,6 +2,8 @@
// 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.Core;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Routing;
@ -75,15 +77,7 @@ namespace Microsoft.AspNetCore.Builder
throw new ArgumentNullException(nameof(configureRoutes));
}
// Verify if AddMvc was done before calling UseMvc
// We use the MvcMarkerService to make sure if all the services were added.
if (app.ApplicationServices.GetService(typeof(MvcMarkerService)) == null)
{
throw new InvalidOperationException(Resources.FormatUnableToFindServices(
nameof(IServiceCollection),
"AddMvc",
"ConfigureServices(...)"));
}
VerifyMvcIsRegistered(app);
var middlewarePipelineBuilder = app.ApplicationServices.GetRequiredService<MiddlewareFilterBuilder>();
middlewarePipelineBuilder.ApplicationBuilder = app.New();
@ -99,5 +93,51 @@ namespace Microsoft.AspNetCore.Builder
return app.UseRouter(routes.Build());
}
public static IApplicationBuilder UseMvcWithEndpoint(
this IApplicationBuilder app,
Action<MvcEndpointInfoBuilder> configureRoutes)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
if (configureRoutes == null)
{
throw new ArgumentNullException(nameof(configureRoutes));
}
VerifyMvcIsRegistered(app);
var mvcEndpointDataSource = app.ApplicationServices
.GetRequiredService<IEnumerable<EndpointDataSource>>()
.OfType<MvcEndpointDataSource>()
.First();
var constraintResolver = app.ApplicationServices.GetRequiredService<IInlineConstraintResolver>();
MvcEndpointInfoBuilder routeBuilder = new MvcEndpointInfoBuilder(constraintResolver);
configureRoutes(routeBuilder);
mvcEndpointDataSource.ConventionalEndpointInfos.AddRange(routeBuilder.EndpointInfos);
mvcEndpointDataSource.InitializeEndpoints();
return app.UseEndpoint();
}
private static void VerifyMvcIsRegistered(IApplicationBuilder app)
{
// Verify if AddMvc was done before calling UseMvc
// We use the MvcMarkerService to make sure if all the services were added.
if (app.ApplicationServices.GetService(typeof(MvcMarkerService)) == null)
{
throw new InvalidOperationException(Resources.FormatUnableToFindServices(
nameof(IServiceCollection),
"AddMvc",
"ConfigureServices(...)"));
}
}
}
}

View File

@ -0,0 +1,105 @@
// 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.Globalization;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Template;
namespace Microsoft.AspNetCore.Builder
{
public class MvcEndpointInfo
{
public MvcEndpointInfo(
string name,
string template,
RouteValueDictionary defaults,
IDictionary<string, object> constraints,
RouteValueDictionary dataTokens,
IInlineConstraintResolver constraintResolver)
{
Name = name;
Template = template ?? string.Empty;
DataTokens = dataTokens;
try
{
// Data we parse from the template will be used to fill in the rest of the constraints or
// defaults. The parser will throw for invalid routes.
ParsedTemplate = TemplateParser.Parse(template);
Constraints = GetConstraints(constraintResolver, ParsedTemplate, constraints);
Defaults = GetDefaults(ParsedTemplate, defaults);
}
catch (Exception exception)
{
throw new RouteCreationException(
string.Format(CultureInfo.CurrentCulture, "An error occurred while creating the route with name '{0}' and template '{1}'.", name, template), exception);
}
}
public string Name { get; }
public string Template { get; }
public RouteValueDictionary Defaults { get; }
public IDictionary<string, IRouteConstraint> Constraints { get; }
public RouteValueDictionary DataTokens { get; }
internal RouteTemplate ParsedTemplate { get; private set; }
private static IDictionary<string, IRouteConstraint> GetConstraints(
IInlineConstraintResolver inlineConstraintResolver,
RouteTemplate parsedTemplate,
IDictionary<string, object> constraints)
{
var constraintBuilder = new RouteConstraintBuilder(inlineConstraintResolver, parsedTemplate.TemplateText);
if (constraints != null)
{
foreach (var kvp in constraints)
{
constraintBuilder.AddConstraint(kvp.Key, kvp.Value);
}
}
foreach (var parameter in parsedTemplate.Parameters)
{
if (parameter.IsOptional)
{
constraintBuilder.SetOptional(parameter.Name);
}
foreach (var inlineConstraint in parameter.InlineConstraints)
{
constraintBuilder.AddResolvedConstraint(parameter.Name, inlineConstraint.Constraint);
}
}
return constraintBuilder.Build();
}
private static RouteValueDictionary GetDefaults(
RouteTemplate parsedTemplate,
RouteValueDictionary defaults)
{
var result = defaults == null ? new RouteValueDictionary() : new RouteValueDictionary(defaults);
foreach (var parameter in parsedTemplate.Parameters)
{
if (parameter.DefaultValue != null)
{
if (result.ContainsKey(parameter.Name))
{
throw new InvalidOperationException(
string.Format(CultureInfo.CurrentCulture, "The route parameter '{0}' has both an inline default value and an explicit default value specified. A route parameter cannot contain an inline default value when a default value is specified explicitly. Consider removing one of them.", parameter.Name));
}
else
{
result.Add(parameter.Name, parameter.DefaultValue);
}
}
}
return result;
}
}
}

View File

@ -0,0 +1,19 @@
// 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.Routing;
namespace Microsoft.AspNetCore.Builder
{
public class MvcEndpointInfoBuilder
{
public MvcEndpointInfoBuilder(IInlineConstraintResolver constraintResolver)
{
ConstraintResolver = constraintResolver;
}
public List<MvcEndpointInfo> EndpointInfos { get; } = new List<MvcEndpointInfo>();
public IInlineConstraintResolver ConstraintResolver { get; }
}
}

View File

@ -0,0 +1,229 @@
// 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.Core;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Provides extension methods for <see cref="MvcEndpointInfoBuilder" /> to add endpoints.
/// </summary>
public static class MvcEndpointInfoBuilderExtensions
{
#region MapEndpoint
/// <summary>
/// Adds a endpoint to the <see cref="MvcEndpointInfoBuilder" /> with the specified name and template.
/// </summary>
/// <param name="endpointBuilder">The <see cref="MvcEndpointInfoBuilder" /> to add the endpoint to.</param>
/// <param name="name">The name of the endpoint.</param>
/// <param name="template">The URL pattern of the endpoint.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static MvcEndpointInfoBuilder MapEndpoint(this MvcEndpointInfoBuilder endpointBuilder, string name, string template)
{
endpointBuilder.MapEndpoint(name, template, null);
return endpointBuilder;
}
/// <summary>
/// Adds a endpoint to the <see cref="MvcEndpointInfoBuilder" /> with the specified name, template, and default values.
/// </summary>
/// <param name="endpointBuilder">The <see cref="MvcEndpointInfoBuilder" /> to add the endpoint to.</param>
/// <param name="name">The name of the endpoint.</param>
/// <param name="template">The URL pattern of the endpoint.</param>
/// <param name="defaults">
/// An object that contains default values for endpoint parameters. The object's properties represent the names
/// and values of the default values.
/// </param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static MvcEndpointInfoBuilder MapEndpoint(this MvcEndpointInfoBuilder endpointBuilder, string name, string template, object defaults)
{
return endpointBuilder.MapEndpoint(name, template, defaults, null);
}
/// <summary>
/// Adds a endpoint to the <see cref="MvcEndpointInfoBuilder" /> with the specified name, template, default values, and
/// constraints.
/// </summary>
/// <param name="endpointBuilder">The <see cref="MvcEndpointInfoBuilder" /> to add the endpoint to.</param>
/// <param name="name">The name of the endpoint.</param>
/// <param name="template">The URL pattern of the endpoint.</param>
/// <param name="defaults">
/// An object that contains default values for endpoint parameters. The object's properties represent the names
/// and values of the default values.
/// </param>
/// <param name="constraints">
/// An object that contains constraints for the endpoint. The object's properties represent the names and values
/// of the constraints.
/// </param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static MvcEndpointInfoBuilder MapEndpoint(this MvcEndpointInfoBuilder endpointBuilder, string name, string template, object defaults, object constraints)
{
return endpointBuilder.MapEndpoint(name, template, defaults, constraints, null);
}
/// <summary>
/// Adds a endpoint to the <see cref="MvcEndpointInfoBuilder" /> with the specified name, template, default values, and
/// data tokens.
/// </summary>
/// <param name="endpointBuilder">The <see cref="MvcEndpointInfoBuilder" /> to add the endpoint to.</param>
/// <param name="name">The name of the endpoint.</param>
/// <param name="template">The URL pattern of the endpoint.</param>
/// <param name="defaults">
/// An object that contains default values for endpoint parameters. The object's properties represent the names
/// and values of the default values.
/// </param>
/// <param name="constraints">
/// An object that contains constraints for the endpoint. The object's properties represent the names and values
/// of the constraints.
/// </param>
/// <param name="dataTokens">
/// An object that contains data tokens for the endpoint. The object's properties represent the names and values
/// of the data tokens.
/// </param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static MvcEndpointInfoBuilder MapEndpoint(this MvcEndpointInfoBuilder endpointBuilder, string name, string template, object defaults, object constraints, object dataTokens)
{
endpointBuilder.EndpointInfos.Add(new MvcEndpointInfo(
name,
template,
new RouteValueDictionary(defaults),
new RouteValueDictionary(constraints),
new RouteValueDictionary(dataTokens),
endpointBuilder.ConstraintResolver));
return endpointBuilder;
}
#endregion
#region MapAreaEndpoint
/// <summary>
/// Adds a endpoint to the <see cref="MvcEndpointInfoBuilder"/> with the given MVC area with the specified
/// <paramref name="name"/>, <paramref name="areaName"/> and <paramref name="template"/>.
/// </summary>
/// <param name="endpointBuilder">The <see cref="MvcEndpointInfoBuilder"/> to add the endpoint to.</param>
/// <param name="name">The name of the endpoint.</param>
/// <param name="areaName">The MVC area name.</param>
/// <param name="template">The URL pattern of the endpoint.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static MvcEndpointInfoBuilder MapAreaEndpoint(
this MvcEndpointInfoBuilder endpointBuilder,
string name,
string areaName,
string template)
{
MapAreaEndpoint(endpointBuilder, name, areaName, template, defaults: null, constraints: null, dataTokens: null);
return endpointBuilder;
}
/// <summary>
/// Adds a endpoint to the <see cref="MvcEndpointInfoBuilder"/> with the given MVC area with the specified
/// <paramref name="name"/>, <paramref name="areaName"/>, <paramref name="template"/>, and
/// <paramref name="defaults"/>.
/// </summary>
/// <param name="endpointBuilder">The <see cref="MvcEndpointInfoBuilder"/> to add the endpoint to.</param>
/// <param name="name">The name of the endpoint.</param>
/// <param name="areaName">The MVC area name.</param>
/// <param name="template">The URL pattern of the endpoint.</param>
/// <param name="defaults">
/// An object that contains default values for endpoint parameters. The object's properties represent the
/// names and values of the default values.
/// </param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static MvcEndpointInfoBuilder MapAreaEndpoint(
this MvcEndpointInfoBuilder endpointBuilder,
string name,
string areaName,
string template,
object defaults)
{
MapAreaEndpoint(endpointBuilder, name, areaName, template, defaults, constraints: null, dataTokens: null);
return endpointBuilder;
}
/// <summary>
/// Adds a endpoint to the <see cref="MvcEndpointInfoBuilder"/> with the given MVC area with the specified
/// <paramref name="name"/>, <paramref name="areaName"/>, <paramref name="template"/>,
/// <paramref name="defaults"/>, and <paramref name="constraints"/>.
/// </summary>
/// <param name="endpointBuilder">The <see cref="MvcEndpointInfoBuilder"/> to add the endpoint to.</param>
/// <param name="name">The name of the endpoint.</param>
/// <param name="areaName">The MVC area name.</param>
/// <param name="template">The URL pattern of the endpoint.</param>
/// <param name="defaults">
/// An object that contains default values for endpoint parameters. The object's properties represent the
/// names and values of the default values.
/// </param>
/// <param name="constraints">
/// An object that contains constraints for the endpoint. The object's properties represent the names and
/// values of the constraints.
/// </param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static MvcEndpointInfoBuilder MapAreaEndpoint(
this MvcEndpointInfoBuilder endpointBuilder,
string name,
string areaName,
string template,
object defaults,
object constraints)
{
MapAreaEndpoint(endpointBuilder, name, areaName, template, defaults, constraints, dataTokens: null);
return endpointBuilder;
}
/// <summary>
/// Adds a endpoint to the <see cref="MvcEndpointInfoBuilder"/> with the given MVC area with the specified
/// <paramref name="name"/>, <paramref name="areaName"/>, <paramref name="template"/>,
/// <paramref name="defaults"/>, <paramref name="constraints"/>, and <paramref name="dataTokens"/>.
/// </summary>
/// <param name="endpointBuilder">The <see cref="MvcEndpointInfoBuilder"/> to add the endpoint to.</param>
/// <param name="name">The name of the endpoint.</param>
/// <param name="areaName">The MVC area name.</param>
/// <param name="template">The URL pattern of the endpoint.</param>
/// <param name="defaults">
/// An object that contains default values for endpoint parameters. The object's properties represent the
/// names and values of the default values.
/// </param>
/// <param name="constraints">
/// An object that contains constraints for the endpoint. The object's properties represent the names and
/// values of the constraints.
/// </param>
/// <param name="dataTokens">
/// An object that contains data tokens for the endpoint. The object's properties represent the names and
/// values of the data tokens.
/// </param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static MvcEndpointInfoBuilder MapAreaEndpoint(
this MvcEndpointInfoBuilder endpointBuilder,
string name,
string areaName,
string template,
object defaults,
object constraints,
object dataTokens)
{
if (endpointBuilder == null)
{
throw new ArgumentNullException(nameof(endpointBuilder));
}
if (string.IsNullOrEmpty(areaName))
{
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(areaName));
}
var defaultsDictionary = new RouteValueDictionary(defaults);
defaultsDictionary["area"] = defaultsDictionary["area"] ?? areaName;
var constraintsDictionary = new RouteValueDictionary(constraints);
constraintsDictionary["area"] = constraintsDictionary["area"] ?? new StringRouteConstraint(areaName);
endpointBuilder.MapEndpoint(name, template, defaultsDictionary, constraintsDictionary, dataTokens);
return endpointBuilder;
}
#endregion
}
}

View File

@ -4,11 +4,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.EndpointConstraints;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.Internal
@ -17,6 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
private readonly IActionDescriptorCollectionProvider _actions;
private readonly MvcEndpointInvokerFactory _invokerFactory;
private readonly IServiceProvider _serviceProvider;
private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders;
private readonly List<Endpoint> _endpoints;
@ -25,7 +30,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public MvcEndpointDataSource(
IActionDescriptorCollectionProvider actions,
MvcEndpointInvokerFactory invokerFactory,
IEnumerable<IActionDescriptorChangeProvider> actionDescriptorChangeProviders)
IEnumerable<IActionDescriptorChangeProvider> actionDescriptorChangeProviders,
IServiceProvider serviceProvider)
{
if (actions == null)
{
@ -42,70 +48,247 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(actionDescriptorChangeProviders));
}
if (serviceProvider == null)
{
throw new ArgumentNullException(nameof(serviceProvider));
}
_actions = actions;
_invokerFactory = invokerFactory;
_serviceProvider = serviceProvider;
_actionDescriptorChangeProviders = actionDescriptorChangeProviders.ToArray();
_endpoints = new List<Endpoint>();
InitializeEndpoints();
ConventionalEndpointInfos = new List<MvcEndpointInfo>();
}
private void InitializeEndpoints()
public void InitializeEndpoints()
{
// note: this code has haxxx. This will only work in some constrained scenarios
foreach (var action in _actions.ActionDescriptors.Items)
{
if (action.AttributeRouteInfo == null)
{
// Action does not have an attribute route
continue;
}
RequestDelegate invokerDelegate = (context) =>
{
var values = context.Features.Get<IEndpointFeature>().Values;
var routeData = new RouteData();
foreach (var kvp in values)
// Check each of the conventional templates to see if the action would be reachable
// If the action and template are compatible then create an endpoint with the
// area/controller/action parameter parts replaced with literals
//
// e.g. {controller}/{action} with HomeController.Index and HomeController.Login
// would result in endpoints:
// - Home/Index
// - Home/Login
foreach (var endpointInfo in ConventionalEndpointInfos)
{
routeData.Values.Add(kvp.Key, kvp.Value);
}
var actionRouteValues = action.RouteValues;
var endpointTemplateSegments = endpointInfo.ParsedTemplate.Segments;
var actionContext = new ActionContext(context, routeData, action);
var invoker = _invokerFactory.CreateInvoker(actionContext);
return invoker.InvokeAsync();
};
var metadata = new List<object>();
// Add filter descriptors to endpoint metadata
metadata.AddRange(action.FilterDescriptors.OrderBy(f => f, FilterDescriptorOrderComparer.Comparer).Select(f => f.Filter));
if (action.ActionConstraints != null && action.ActionConstraints.Count > 0)
{
foreach (var actionConstraint in action.ActionConstraints)
{
if (actionConstraint is HttpMethodActionConstraint httpMethodActionConstraint)
if (MatchRouteValue(action, endpointInfo, "Area")
&& MatchRouteValue(action, endpointInfo, "Controller")
&& MatchRouteValue(action, endpointInfo, "Action"))
{
metadata.Add(new HttpMethodEndpointConstraint(httpMethodActionConstraint.HttpMethods));
var newEndpointTemplate = TemplateParser.Parse(endpointInfo.Template);
for (var i = 0; i < newEndpointTemplate.Segments.Count; i++)
{
// Check if the template can be shortened because the remaining parameters are optional
//
// e.g. Matching template {controller=Home}/{action=Index}/{id?} against HomeController.Index
// can resolve to the following endpoints:
// - /Home/Index/{id?}
// - /Home
// - /
if (UseDefaultValuePlusRemainingSegementsOptional(i, action, endpointInfo, newEndpointTemplate))
{
var subTemplate = RouteTemplateWriter.ToString(newEndpointTemplate.Segments.Take(i));
var subEndpoint = CreateEndpoint(action, subTemplate, 0, endpointInfo);
_endpoints.Add(subEndpoint);
}
var segment = newEndpointTemplate.Segments[i];
for (var j = 0; j < segment.Parts.Count; j++)
{
var part = segment.Parts[j];
if (part.IsParameter && IsMvcParameter(part.Name))
{
// Replace parameter with literal value
segment.Parts[j] = TemplatePart.CreateLiteral(action.RouteValues[part.Name]);
}
}
}
var newTemplate = RouteTemplateWriter.ToString(newEndpointTemplate.Segments);
var endpoint = CreateEndpoint(action, newTemplate, 0, endpointInfo);
_endpoints.Add(endpoint);
}
}
}
var metadataCollection = new EndpointMetadataCollection(metadata);
_endpoints.Add(new MatcherEndpoint(
next => invokerDelegate,
action.AttributeRouteInfo.Template,
action.RouteValues,
action.AttributeRouteInfo.Order,
metadataCollection,
action.DisplayName,
new Address(action.AttributeRouteInfo.Name)));
else
{
var endpoint = CreateEndpoint(action, action.AttributeRouteInfo.Template, action.AttributeRouteInfo.Order, action.AttributeRouteInfo);
_endpoints.Add(endpoint);
}
}
}
private bool IsMvcParameter(string name)
{
if (string.Equals(name, "Area", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "Controller", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "Action", StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
private bool UseDefaultValuePlusRemainingSegementsOptional(int segmentIndex, ActionDescriptor action, MvcEndpointInfo endpointInfo, RouteTemplate template)
{
// Check whether the remaining segments are all optional and one or more of them is
// for area/controller/action and has a default value
var usedDefaultValue = false;
for (var i = segmentIndex; i < template.Segments.Count; i++)
{
var segment = template.Segments[i];
for (var j = 0; j < segment.Parts.Count; j++)
{
var part = segment.Parts[j];
if (part.IsOptional || part.IsOptionalSeperator || part.IsCatchAll)
{
continue;
}
if (part.IsParameter)
{
if (IsMvcParameter(part.Name))
{
if (endpointInfo.Defaults[part.Name] is string defaultValue
&& action.RouteValues.TryGetValue(part.Name, out var routeValue)
&& string.Equals(defaultValue, routeValue, StringComparison.OrdinalIgnoreCase))
{
usedDefaultValue = true;
continue;
}
}
}
// Stop because there is a non-optional/non-defaulted trailing value
return false;
}
}
return usedDefaultValue;
}
private bool MatchRouteValue(ActionDescriptor action, MvcEndpointInfo endpointInfo, string routeKey)
{
if (!action.RouteValues.TryGetValue(routeKey, out var actionValue) || string.IsNullOrWhiteSpace(actionValue))
{
// Action does not have a value for this routeKey, most likely because action is not in an area
// Check that the template does not have a parameter for the routeKey
var matchingParameter = endpointInfo.ParsedTemplate.Parameters.SingleOrDefault(p => string.Equals(p.Name, routeKey, StringComparison.OrdinalIgnoreCase));
if (matchingParameter == null)
{
return true;
}
}
else
{
if (endpointInfo.Defaults != null && string.Equals(actionValue, endpointInfo.Defaults[routeKey] as string, StringComparison.OrdinalIgnoreCase))
{
return true;
}
var matchingParameter = endpointInfo.ParsedTemplate.Parameters.SingleOrDefault(p => string.Equals(p.Name, routeKey, StringComparison.OrdinalIgnoreCase));
if (matchingParameter != null)
{
// Check that the value matches against constraints on that parameter
// e.g. For {controller:regex((Home|Login))} the controller value must match the regex
//
// REVIEW: This is really ugly
if (endpointInfo.Constraints.TryGetValue(routeKey, out var constraint)
&& !constraint.Match(new DefaultHttpContext() { RequestServices = _serviceProvider }, new DummyRouter(), routeKey, new RouteValueDictionary(action.RouteValues), RouteDirection.IncomingRequest))
{
// Did not match constraint
return false;
}
return true;
}
}
return false;
}
private class DummyRouter : IRouter
{
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return null;
}
public Task RouteAsync(RouteContext context)
{
return Task.CompletedTask;
}
}
private MatcherEndpoint CreateEndpoint(ActionDescriptor action, string template, int order, object source)
{
RequestDelegate invokerDelegate = (context) =>
{
var values = context.Features.Get<IEndpointFeature>().Values;
var routeData = new RouteData();
foreach (var kvp in values)
{
if (kvp.Value != null)
{
routeData.Values.Add(kvp.Key, kvp.Value);
}
}
var actionContext = new ActionContext(context, routeData, action);
var invoker = _invokerFactory.CreateInvoker(actionContext);
return invoker.InvokeAsync();
};
var metadata = new List<object>();
// REVIEW: Used for debugging. Consider removing before release
metadata.Add(source);
metadata.Add(action);
// Add filter descriptors to endpoint metadata
if (action.FilterDescriptors != null && action.FilterDescriptors.Count > 0)
{
metadata.AddRange(action.FilterDescriptors.OrderBy(f => f, FilterDescriptorOrderComparer.Comparer).Select(f => f.Filter));
}
if (action.ActionConstraints != null && action.ActionConstraints.Count > 0)
{
foreach (var actionConstraint in action.ActionConstraints)
{
if (actionConstraint is HttpMethodActionConstraint httpMethodActionConstraint)
{
metadata.Add(new HttpMethodEndpointConstraint(httpMethodActionConstraint.HttpMethods));
}
}
}
var metadataCollection = new EndpointMetadataCollection(metadata);
var endpoint = new MatcherEndpoint(
next => invokerDelegate,
template,
action.RouteValues,
order,
metadataCollection,
action.DisplayName,
address: null);
return endpoint;
}
private IChangeToken GetCompositeChangeToken()
{
if (_actionDescriptorChangeProviders.Length == 1)
@ -136,5 +319,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
public override IReadOnlyList<Endpoint> Endpoints => _endpoints;
public List<MvcEndpointInfo> ConventionalEndpointInfos { get; }
}
}

View File

@ -0,0 +1,56 @@
// 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 System.Linq;
using Microsoft.AspNetCore.Routing.Template;
namespace Microsoft.AspNetCore.Mvc.Internal
{
internal static class RouteTemplateWriter
{
public static string ToString(IEnumerable<TemplateSegment> routeSegments)
{
return string.Join("/", routeSegments.Select(s => ToString(s)));
}
private static string ToString(TemplateSegment templateSegment)
{
return string.Join(string.Empty, templateSegment.Parts.Select(p => ToString(p)));
}
private static string ToString(TemplatePart templatePart)
{
if (templatePart.IsParameter)
{
var partText = "{";
if (templatePart.IsCatchAll)
{
partText += "*";
}
partText += templatePart.Name;
foreach (var item in templatePart.InlineConstraints)
{
partText += ":";
partText += item.Constraint;
}
if (templatePart.DefaultValue != null)
{
partText += "=";
partText += templatePart.DefaultValue;
}
if (templatePart.IsOptional)
{
partText += "?";
}
partText += "}";
return partText;
}
else
{
return templatePart.Text;
}
}
}
}

View File

@ -29,5 +29,25 @@ namespace Microsoft.AspNetCore.Mvc.Core.Builder
"in the application startup code.",
exception.Message);
}
[Fact]
public void UseMvcWithEndpoint_ThrowsInvalidOperationException_IfMvcMarkerServiceIsNotRegistered()
{
// Arrange
var applicationBuilderMock = new Mock<IApplicationBuilder>();
applicationBuilderMock
.Setup(s => s.ApplicationServices)
.Returns(Mock.Of<IServiceProvider>());
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(
() => applicationBuilderMock.Object.UseMvcWithEndpoint(rb => { }));
Assert.Equal(
"Unable to find the required services. Please add all the required services by calling " +
"'IServiceCollection.AddMvc' inside the call to 'ConfigureServices(...)' " +
"in the application startup code.",
exception.Message);
}
}
}

View File

@ -0,0 +1,282 @@
// 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 System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Core.Test.Builder
{
public class MvcEndpointInfoBuilderExtensionsTest
{
#region MapAreaEndpoint
[Fact]
public void MapAreaEndpoint_Simple()
{
// Arrange
var builder = CreateEndpointBuilder();
// Act
builder.MapAreaEndpoint(name: null, areaName: "admin", template: "site/Admin/");
// Assert
var endpointInfo = Assert.Single(builder.EndpointInfos);
Assert.Null(endpointInfo.Name);
Assert.Equal("site/Admin/", endpointInfo.Template);
Assert.Collection(
endpointInfo.Constraints.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.IsType<StringRouteConstraint>(kvp.Value);
});
Assert.Empty(endpointInfo.DataTokens);
Assert.Collection(
endpointInfo.Defaults.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal("admin", kvp.Value);
});
}
[Fact]
public void MapAreaEndpoint_Defaults()
{
// Arrange
var builder = CreateEndpointBuilder();
// Act
builder.MapAreaEndpoint(
name: "admin_area",
areaName: "admin",
template: "site/Admin/",
defaults: new { action = "Home" });
// Assert
var endpointInfo = Assert.Single(builder.EndpointInfos);
Assert.Equal("admin_area", endpointInfo.Name);
Assert.Equal("site/Admin/", endpointInfo.Template);
Assert.Collection(
endpointInfo.Constraints.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.IsType<StringRouteConstraint>(kvp.Value);
});
Assert.Empty(endpointInfo.DataTokens);
Assert.Collection(
endpointInfo.Defaults.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("action", kvp.Key);
Assert.Equal("Home", kvp.Value);
},
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal("admin", kvp.Value);
});
}
[Fact]
public void MapAreaEndpoint_DefaultsAndConstraints()
{
// Arrange
var builder = CreateEndpointBuilder();
// Act
builder.MapAreaEndpoint(
name: "admin_area",
areaName: "admin",
template: "site/Admin/",
defaults: new { action = "Home" },
constraints: new { id = new IntRouteConstraint() });
// Assert
var endpointInfo = Assert.Single(builder.EndpointInfos);
Assert.Equal("admin_area", endpointInfo.Name);
Assert.Equal("site/Admin/", endpointInfo.Template);
Assert.Collection(
endpointInfo.Constraints.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.IsType<StringRouteConstraint>(kvp.Value);
},
kvp =>
{
Assert.Equal("id", kvp.Key);
Assert.IsType<IntRouteConstraint>(kvp.Value);
});
Assert.Empty(endpointInfo.DataTokens);
Assert.Collection(
endpointInfo.Defaults.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("action", kvp.Key);
Assert.Equal("Home", kvp.Value);
},
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal("admin", kvp.Value);
});
}
[Fact]
public void MapAreaEndpoint_DefaultsConstraintsAndDataTokens()
{
// Arrange
var builder = CreateEndpointBuilder();
// Act
builder.MapAreaEndpoint(
name: "admin_area",
areaName: "admin",
template: "site/Admin/",
defaults: new { action = "Home" },
constraints: new { id = new IntRouteConstraint() },
dataTokens: new { some_token = "hello" });
// Assert
var endpointInfo = Assert.Single(builder.EndpointInfos);
Assert.Equal("admin_area", endpointInfo.Name);
Assert.Equal("site/Admin/", endpointInfo.Template);
Assert.Collection(
endpointInfo.Constraints.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.IsType<StringRouteConstraint>(kvp.Value);
},
kvp =>
{
Assert.Equal("id", kvp.Key);
Assert.IsType<IntRouteConstraint>(kvp.Value);
});
Assert.Collection(
endpointInfo.DataTokens.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("some_token", kvp.Key);
Assert.Equal("hello", kvp.Value);
});
Assert.Collection(
endpointInfo.Defaults.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("action", kvp.Key);
Assert.Equal("Home", kvp.Value);
},
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal("admin", kvp.Value);
});
}
[Fact]
public void MapAreaEndpoint_DoesNotReplaceValuesForAreaIfAlreadyPresentInConstraintsOrDefaults()
{
// Arrange
var builder = CreateEndpointBuilder();
// Act
builder.MapAreaEndpoint(
name: "admin_area",
areaName: "admin",
template: "site/Admin/",
defaults: new { area = "Home" },
constraints: new { area = new IntRouteConstraint() },
dataTokens: new { some_token = "hello" });
// Assert
var endpointInfo = Assert.Single(builder.EndpointInfos);
Assert.Equal("admin_area", endpointInfo.Name);
Assert.Equal("site/Admin/", endpointInfo.Template);
Assert.Collection(
endpointInfo.Constraints.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.IsType<IntRouteConstraint>(kvp.Value);
});
Assert.Collection(
endpointInfo.DataTokens.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("some_token", kvp.Key);
Assert.Equal("hello", kvp.Value);
});
Assert.Collection(
endpointInfo.Defaults.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal("Home", kvp.Value);
});
}
[Fact]
public void MapAreaEndpoint_UsesPassedInAreaNameAsIs()
{
// Arrange
var builder = CreateEndpointBuilder();
var areaName = "user.admin";
// Act
builder.MapAreaEndpoint(name: null, areaName: areaName, template: "site/Admin/");
// Assert
var endpointInfo = Assert.Single(builder.EndpointInfos);
Assert.Null(endpointInfo.Name);
Assert.Equal("site/Admin/", endpointInfo.Template);
Assert.Collection(
endpointInfo.Constraints.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.IsType<StringRouteConstraint>(kvp.Value);
var values = new RouteValueDictionary(new { area = areaName });
var match = kvp.Value.Match(
new DefaultHttpContext(),
route: new Mock<IRouter>().Object,
routeKey: kvp.Key,
values: values,
routeDirection: RouteDirection.UrlGeneration);
Assert.True(match);
});
Assert.Empty(endpointInfo.DataTokens);
Assert.Collection(
endpointInfo.Defaults.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("area", kvp.Key);
Assert.Equal(kvp.Value, areaName);
});
}
#endregion
private MvcEndpointInfoBuilder CreateEndpointBuilder()
{
var builder = new MvcEndpointInfoBuilder(Mock.Of<IInlineConstraintResolver>());
return builder;
}
}
}

View File

@ -3,7 +3,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Abstractions;
@ -13,6 +15,7 @@ using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matchers;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;
@ -54,17 +57,16 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
}
}, 0));
var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object);
// Act
var dataSource = new MvcEndpointDataSource(
mockDescriptorProvider.Object,
new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())),
Array.Empty<IActionDescriptorChangeProvider>());
dataSource.InitializeEndpoints();
// Assert
var endpoint = Assert.Single(dataSource.Endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
object endpointValue = matcherEndpoint.Values["Name"];
var endpointValue = matcherEndpoint.Values["Name"];
Assert.Equal(routeValue, endpointValue);
Assert.Equal(displayName, matcherEndpoint.DisplayName);
@ -85,8 +87,8 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(m => m.Features).Returns(featureCollection);
var mockDescriptorProviderMock = new Mock<IActionDescriptorCollectionProvider>();
mockDescriptorProviderMock.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>
var descriptorProviderMock = new Mock<IActionDescriptorCollectionProvider>();
descriptorProviderMock.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>
{
new ActionDescriptor
{
@ -109,11 +111,12 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
var actionInvokerProviderMock = new Mock<IActionInvokerFactory>();
actionInvokerProviderMock.Setup(m => m.CreateInvoker(It.IsAny<ActionContext>())).Returns(actionInvokerMock.Object);
var dataSource = CreateMvcEndpointDataSource(
descriptorProviderMock.Object,
new MvcEndpointInvokerFactory(actionInvokerProviderMock.Object));
// Act
var dataSource = new MvcEndpointDataSource(
mockDescriptorProviderMock.Object,
new MvcEndpointInvokerFactory(actionInvokerProviderMock.Object),
Array.Empty<IActionDescriptorChangeProvider>());
dataSource.InitializeEndpoints();
// Assert
var endpoint = Assert.Single(dataSource.Endpoints);
@ -139,8 +142,8 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(m => m.Features).Returns(featureCollection);
var mockDescriptorProviderMock = new Mock<IActionDescriptorCollectionProvider>();
mockDescriptorProviderMock.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>(), 0));
var descriptorProviderMock = new Mock<IActionDescriptorCollectionProvider>();
descriptorProviderMock.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>(), 0));
var actionInvokerMock = new Mock<IActionInvoker>();
@ -154,8 +157,8 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
var changeProvider2Mock = new Mock<IActionDescriptorChangeProvider>();
changeProvider2Mock.Setup(m => m.GetChangeToken()).Returns(changeTokenMock.Object);
var dataSource = new MvcEndpointDataSource(
mockDescriptorProviderMock.Object,
var dataSource = CreateMvcEndpointDataSource(
descriptorProviderMock.Object,
new MvcEndpointInvokerFactory(actionInvokerProviderMock.Object),
new[] { changeProvider1Mock.Object, changeProvider2Mock.Object });
@ -166,5 +169,211 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
var compositeChangeToken = Assert.IsType<CompositeChangeToken>(changeToken);
Assert.Equal(2, compositeChangeToken.ChangeTokens.Count);
}
[Theory]
[InlineData("{controller}/{action}/{id?}", new[] { "TestController/TestAction/{id?}" })]
[InlineData("{controller}/{id?}", new string[] { })]
[InlineData("{action}/{id?}", new string[] { })]
[InlineData("{Controller}/{Action}/{id?}", new[] { "TestController/TestAction/{id?}" })]
[InlineData("{CONTROLLER}/{ACTION}/{id?}", new[] { "TestController/TestAction/{id?}" })]
[InlineData("{controller}/{action=TestAction}", new[] { "TestController", "TestController/TestAction" })]
[InlineData("{controller}/{action=TestAction}/{id?}", new[] { "TestController", "TestController/TestAction/{id?}" })]
[InlineData("{controller=TestController}/{action=TestAction}/{id?}", new[] { "", "TestController", "TestController/TestAction/{id?}" })]
[InlineData("{controller}/{action}/{*catchAll}", new[] { "TestController/TestAction/{*catchAll}" })]
[InlineData("{controller}/{action=TestAction}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{*catchAll}" })]
[InlineData("{controller}/{action=TestAction}/{id?}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{id?}/{*catchAll}" })]
//[InlineData("{controller}/{action}.{ext?}", new[] { "TestController/TestAction.{ext?}" })]
//[InlineData("{controller}/{action=TestAction}.{ext?}", new[] { "TestController", "TestController/TestAction.{ext?}" })]
public void InitializeEndpoints_SingleAction(string endpointInfoRoute, string[] finalEndpointTemplates)
{
// Arrange
var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>
{
CreateActionDescriptor("TestController", "TestAction")
}, 0));
var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute));
// Act
dataSource.InitializeEndpoints();
// Assert
var inspectors = finalEndpointTemplates
.Select(t => new Action<Endpoint>(e => Assert.Equal(t, Assert.IsType<MatcherEndpoint>(e).Template)))
.ToArray();
// Assert
Assert.Collection(dataSource.Endpoints, inspectors);
}
[Theory]
[InlineData("{area}/{controller}/{action}/{id?}", new[] { "TestArea/TestController/TestAction/{id?}" })]
[InlineData("{controller}/{action}/{id?}", new string[] { })]
[InlineData("{area=TestArea}/{controller}/{action}/{id?}", new[] { "TestArea/TestController/TestAction/{id?}" })]
[InlineData("{area=TestArea}/{controller}/{action=TestAction}/{id?}", new[] { "TestArea/TestController", "TestArea/TestController/TestAction/{id?}" })]
[InlineData("{area=TestArea}/{controller=TestController}/{action=TestAction}/{id?}", new[] { "", "TestArea", "TestArea/TestController", "TestArea/TestController/TestAction/{id?}" })]
[InlineData("{area:exists}/{controller}/{action}/{id?}", new[] { "TestArea/TestController/TestAction/{id?}" })]
public void InitializeEndpoints_AreaSingleAction(string endpointInfoRoute, string[] finalEndpointTemplates)
{
// Arrange
var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>
{
CreateActionDescriptor("TestController", "TestAction", "TestArea")
}, 0));
var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute));
// Act
dataSource.InitializeEndpoints();
// Assert
var inspectors = finalEndpointTemplates
.Select(t => new Action<Endpoint>(e => Assert.Equal(t, Assert.IsType<MatcherEndpoint>(e).Template)))
.ToArray();
// Assert
Assert.Collection(dataSource.Endpoints, inspectors);
}
[Fact]
public void InitializeEndpoints_SingleAction_WithActionDefault()
{
// Arrange
var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>
{
CreateActionDescriptor("TestController", "TestAction")
}, 0));
var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
string.Empty,
"{controller}/{action}",
new RouteValueDictionary(new { action = "TestAction" })));
// Act
dataSource.InitializeEndpoints();
// Assert
Assert.Collection(dataSource.Endpoints,
(e) => Assert.Equal("TestController", Assert.IsType<MatcherEndpoint>(e).Template),
(e) => Assert.Equal("TestController/TestAction", Assert.IsType<MatcherEndpoint>(e).Template));
}
[Fact]
public void InitializeEndpoints_MultipleActions_WithActionConstraint()
{
// Arrange
var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>
{
CreateActionDescriptor("TestController", "TestAction"),
CreateActionDescriptor("TestController", "TestAction1"),
CreateActionDescriptor("TestController", "TestAction2")
}, 0));
var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
string.Empty,
"{controller}/{action}",
constraints: new RouteValueDictionary(new { action = "(TestAction1|TestAction2)" })));
// Act
dataSource.InitializeEndpoints();
// Assert
Assert.Collection(dataSource.Endpoints,
(e) => Assert.Equal("TestController/TestAction1", Assert.IsType<MatcherEndpoint>(e).Template),
(e) => Assert.Equal("TestController/TestAction2", Assert.IsType<MatcherEndpoint>(e).Template));
}
[Theory]
[InlineData("{controller}/{action}", new[] { "TestController1/TestAction1", "TestController1/TestAction2", "TestController1/TestAction3", "TestController2/TestAction1" })]
[InlineData("{controller}/{action:regex((TestAction1|TestAction2))}", new[] { "TestController1/TestAction1", "TestController1/TestAction2", "TestController2/TestAction1" })]
public void InitializeEndpoints_MultipleActions(string endpointInfoRoute, string[] finalEndpointTemplates)
{
// Arrange
var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>
{
CreateActionDescriptor("TestController1", "TestAction1"),
CreateActionDescriptor("TestController1", "TestAction2"),
CreateActionDescriptor("TestController1", "TestAction3"),
CreateActionDescriptor("TestController2", "TestAction1")
}, 0));
var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(
string.Empty,
endpointInfoRoute));
// Act
dataSource.InitializeEndpoints();
var inspectors = finalEndpointTemplates
.Select(t => new Action<Endpoint>(e => Assert.Equal(t, Assert.IsType<MatcherEndpoint>(e).Template)))
.ToArray();
// Assert
Assert.Collection(dataSource.Endpoints, inspectors);
}
private MvcEndpointDataSource CreateMvcEndpointDataSource(
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider = null,
MvcEndpointInvokerFactory mvcEndpointInvokerFactory = null,
IEnumerable<IActionDescriptorChangeProvider> actionDescriptorChangeProviders = null)
{
if (actionDescriptorCollectionProvider == null)
{
var mockDescriptorProvider = new Mock<IActionDescriptorCollectionProvider>();
mockDescriptorProvider.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>(), 0));
actionDescriptorCollectionProvider = mockDescriptorProvider.Object;
}
var serviceProviderMock = new Mock<IServiceProvider>();
serviceProviderMock.Setup(m => m.GetService(typeof(IActionDescriptorCollectionProvider))).Returns(actionDescriptorCollectionProvider);
var dataSource = new MvcEndpointDataSource(
actionDescriptorCollectionProvider,
mvcEndpointInvokerFactory ?? new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())),
actionDescriptorChangeProviders ?? Array.Empty<IActionDescriptorChangeProvider>(),
serviceProviderMock.Object);
return dataSource;
}
private MvcEndpointInfo CreateEndpointInfo(
string name,
string template,
RouteValueDictionary defaults = null,
IDictionary<string, object> constraints = null,
RouteValueDictionary dataTokens = null)
{
var routeOptions = new RouteOptions();
var routeOptionsSetup = new MvcCoreRouteOptionsSetup();
routeOptionsSetup.Configure(routeOptions);
var constraintResolver = new DefaultInlineConstraintResolver(Options.Create<RouteOptions>(routeOptions));
return new MvcEndpointInfo(name, template, defaults, constraints, dataTokens, constraintResolver);
}
private ActionDescriptor CreateActionDescriptor(string controller, string action, string area = null)
{
return new ActionDescriptor
{
RouteValues =
{
["controller"] = controller,
["action"] = action,
["area"] = area
},
DisplayName = string.Empty,
};
}
}
}
}

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 Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Routing.Template;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
{
public class RouteTemplateWriterTests
{
[Theory]
[InlineData(@"")]
[InlineData(@"Literal")]
[InlineData(@"Literal1/Literal2")]
[InlineData(@"{controller}")]
[InlineData(@"{controller}/{action}")]
[InlineData(@"{controller}/{action}/{param:test(\?)?}")]
[InlineData(@"{param:test(\w,\w)=jsd}")]
[InlineData(@"some/url-{p1:int:test(3)=hello}/{p2=abc}/{p3?}")]
[InlineData(@"{param:test(abc:somevalue):name(test1:differentname=default-value}")]
[InlineData(@"api/Blog/{controller}/{action}/{id?}")]
[InlineData(@"{p1}.{p2}.{p3}")]
public void ToString_TemplateRoundtrips(string template)
{
var routeTemplate = TemplateParser.Parse(template);
var output = RouteTemplateWriter.ToString(routeTemplate.Segments);
Assert.Equal(template, output);
}
}
}

View File

@ -12,223 +12,5 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
: base(fixture)
{
}
[Fact(Skip = "Conventional routing WIP")]
public override Task ConventionalRoutedController_ActionIsReachable()
{
return Task.CompletedTask;
}
[Fact(Skip = "Conventional routing WIP")]
public override Task ConventionalRoutedController_ActionIsReachable_WithDefaults()
{
return Task.CompletedTask;
}
[Fact(Skip = "Conventional routing WIP")]
public override Task ConventionalRoutedController_NonActionIsNotReachable()
{
return Task.CompletedTask;
}
[Fact(Skip = "Conventional routing WIP")]
public override Task ConventionalRoutedController_InArea_ActionIsReachable()
{
return Task.CompletedTask;
}
[Fact(Skip = "Conventional routing WIP")]
public override Task ConventionalRoutedController_InArea_ActionBlockedByHttpMethod()
{
return Task.CompletedTask;
}
[Theory(Skip = "Conventional routing WIP")]
[InlineData("", "/Home/OptionalPath/default")]
[InlineData("CustomPath", "/Home/OptionalPath/CustomPath")]
public override Task ConventionalRoutedController_WithOptionalSegment(string optionalSegment, string expected)
{
return Task.CompletedTask;
}
[Theory(Skip = "URL generation WIP")]
[InlineData("http://localhost/api/v1/Maps")]
[InlineData("http://localhost/api/v2/Maps")]
public override Task AttributeRoutedAction_MultipleRouteAttributes_WorksWithNameAndOrder(string url)
{
return Task.CompletedTask;
}
[Fact(Skip = "URL generation WIP")]
public override Task AttributeRoutedAction_MultipleRouteAttributes_WorksWithOverrideRoutes()
{
return Task.CompletedTask;
}
[Theory(Skip = "URL generation WIP")]
[InlineData("http://localhost/api/v1/Maps/5", "PUT")]
[InlineData("http://localhost/api/v2/Maps/5", "PUT")]
[InlineData("http://localhost/api/v1/Maps/PartialUpdate/5", "PATCH")]
[InlineData("http://localhost/api/v2/Maps/PartialUpdate/5", "PATCH")]
public override Task AttributeRoutedAction_MultipleRouteAttributes_CombinesWithMultipleHttpAttributes(
string url,
string method)
{
return Task.CompletedTask;
}
[Theory(Skip = "URL generation WIP")]
[InlineData("http://localhost/Banks/Get/5")]
[InlineData("http://localhost/Bank/Get/5")]
public override Task AttributeRoutedAction_MultipleHttpAttributesAndTokenReplacement(string url)
{
return Task.CompletedTask;
}
[Theory(Skip = "URL generation WIP")]
[InlineData("PUT", "Bank")]
[InlineData("PATCH", "Bank")]
[InlineData("PUT", "Bank/Update")]
[InlineData("PATCH", "Bank/Update")]
public override Task AttributeRoutedAction_AcceptVerbsAndRouteTemplate_IsReachable(string verb, string path)
{
return Task.CompletedTask;
}
[Fact(Skip = "URL generation WIP")]
public override Task AttributeRoutedAction_LinkGeneration_OverrideActionOverridesOrderOnController()
{
return Task.CompletedTask;
}
[Fact(Skip = "URL generation WIP")]
public override Task AttributeRoutedAction_LinkGeneration_OrderOnActionOverridesOrderOnController()
{
return Task.CompletedTask;
}
[Fact(Skip = "URL generation WIP")]
public override Task AttributeRoutedAction_LinkToSelf()
{
return Task.CompletedTask;
}
[Fact(Skip = "URL generation WIP")]
public override Task AttributeRoutedAction_LinkWithAmbientController()
{
return Task.CompletedTask;
}
[Fact(Skip = "URL generation WIP")]
public override Task AttributeRoutedAction_LinkToAttributeRoutedController()
{
return Task.CompletedTask;
}
[Fact(Skip = "URL generation WIP")]
public override Task AttributeRoutedAction_LinkToConventionalController()
{
return Task.CompletedTask;
}
[Theory(Skip = "URL generation WIP")]
[InlineData("GET", "Get")]
[InlineData("PUT", "Put")]
public override Task AttributeRoutedAction_LinkWithName_WithNameInheritedFromControllerRoute(
string method,
string actionName)
{
return Task.CompletedTask;
}
[Fact(Skip = "URL generation WIP")]
public override Task AttributeRoutedAction_LinkWithName_WithNameOverrridenFromController()
{
return Task.CompletedTask;
}
[Fact(Skip = "URL generation WIP")]
public override Task AttributeRoutedAction_Link_WithNonEmptyActionRouteTemplateAndNoActionRouteName()
{
return Task.CompletedTask;
}
[Fact(Skip = "URL generation WIP")]
public override Task AttributeRoutedAction_LinkWithName_WithNonEmptyActionRouteTemplateAndActionRouteName()
{
return Task.CompletedTask;
}
[Fact(Skip = "Conventional routing WIP")]
public override Task ConventionalRoutedAction_LinkToArea()
{
return Task.CompletedTask;
}
[Fact(Skip = "Conventional routing WIP")]
public override Task ConventionalRoutedAction_InArea_ImplicitLinkToArea()
{
return Task.CompletedTask;
}
[Fact(Skip = "Conventional routing WIP")]
public override Task ConventionalRoutedAction_InArea_ExplicitLeaveArea()
{
return Task.CompletedTask;
}
[Fact(Skip = "Conventional routing WIP")]
public override Task ConventionalRoutedAction_InArea_StaysInArea()
{
return Task.CompletedTask;
}
[Fact(Skip = "URL generation WIP")]
public override Task AttributeRoutedAction_LinkToArea()
{
return Task.CompletedTask;
}
[Fact(Skip = "URL generation WIP")]
public override Task AttributeRoutedAction_InArea_ImplicitLinkToArea()
{
return Task.CompletedTask;
}
[Fact(Skip = "URL generation WIP")]
public override Task AttributeRoutedAction_InArea_ExplicitLeaveArea()
{
return Task.CompletedTask;
}
[Fact(Skip = "URL generation WIP")]
public override Task AttributeRoutedAction_InArea_StaysInArea_ActionDoesntExist()
{
return Task.CompletedTask;
}
[Fact(Skip = "URL generation WIP")]
public override Task AttributeRoutedAction_InArea_LinkToConventionalRoutedActionInArea()
{
return Task.CompletedTask;
}
[Fact(Skip = "Conventional routing WIP")]
public override Task ConventionalRoutedAction_InArea_LinkToAttributeRoutedActionInArea()
{
return Task.CompletedTask;
}
[Fact(Skip = "Conventional routing WIP")]
public override Task ConventionalRoutedAction_InArea_LinkToAnotherArea()
{
return Task.CompletedTask;
}
[Fact(Skip = "URL generation WIP")]
public override Task AttributeRoutedAction_InArea_LinkToAnotherArea()
{
return Task.CompletedTask;
}
}
}
}

View File

@ -33,26 +33,24 @@ namespace RoutingWebSite
{
app.UseDispatcher();
app.UseEndpoint();
app.UseMvcWithEndpoint(routes =>
{
routes.MapAreaEndpoint(
"flightRoute",
"adminRoute",
"{area:exists}/{controller}/{action}",
new { controller = "Home", action = "Index" },
new { area = "Travel" });
//app.UseMvcWithEndpoint(routes =>
//{
// routes.MapAreaEndpoint(
// "flightRoute",
// "adminRoute",
// "{area:exists}/{controller}/{action}",
// new { controller = "Home", action = "Index" },
// new { area = "Travel" });
routes.MapEndpoint(
"ActionAsMethod",
"{controller}/{action}",
defaults: new { controller = "Home", action = "Index" });
// routes.MapEndpoint(
// "ActionAsMethod",
// "{controller}/{action}",
// defaults: new { controller = "Home", action = "Index" });
// routes.MapEndpoint(
// "RouteWithOptionalSegment",
// "{controller}/{action}/{path?}");
//});
routes.MapEndpoint(
"RouteWithOptionalSegment",
"{controller}/{action}/{path?}");
});
}
}
}