Add support for conventional routes with dispatching (#7928)
This commit is contained in:
parent
58aa16ee69
commit
3547341762
|
|
@ -2,6 +2,8 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Mvc.Core;
|
using Microsoft.AspNetCore.Mvc.Core;
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
@ -75,15 +77,7 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
throw new ArgumentNullException(nameof(configureRoutes));
|
throw new ArgumentNullException(nameof(configureRoutes));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify if AddMvc was done before calling UseMvc
|
VerifyMvcIsRegistered(app);
|
||||||
// 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(...)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
var middlewarePipelineBuilder = app.ApplicationServices.GetRequiredService<MiddlewareFilterBuilder>();
|
var middlewarePipelineBuilder = app.ApplicationServices.GetRequiredService<MiddlewareFilterBuilder>();
|
||||||
middlewarePipelineBuilder.ApplicationBuilder = app.New();
|
middlewarePipelineBuilder.ApplicationBuilder = app.New();
|
||||||
|
|
@ -99,5 +93,51 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
|
|
||||||
return app.UseRouter(routes.Build());
|
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(...)"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,11 +4,15 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
using Microsoft.AspNetCore.Routing.EndpointConstraints;
|
||||||
using Microsoft.AspNetCore.Routing.Matchers;
|
using Microsoft.AspNetCore.Routing.Matchers;
|
||||||
|
using Microsoft.AspNetCore.Routing.Template;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
|
|
@ -17,6 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
private readonly IActionDescriptorCollectionProvider _actions;
|
private readonly IActionDescriptorCollectionProvider _actions;
|
||||||
private readonly MvcEndpointInvokerFactory _invokerFactory;
|
private readonly MvcEndpointInvokerFactory _invokerFactory;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders;
|
private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders;
|
||||||
private readonly List<Endpoint> _endpoints;
|
private readonly List<Endpoint> _endpoints;
|
||||||
|
|
||||||
|
|
@ -25,7 +30,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
public MvcEndpointDataSource(
|
public MvcEndpointDataSource(
|
||||||
IActionDescriptorCollectionProvider actions,
|
IActionDescriptorCollectionProvider actions,
|
||||||
MvcEndpointInvokerFactory invokerFactory,
|
MvcEndpointInvokerFactory invokerFactory,
|
||||||
IEnumerable<IActionDescriptorChangeProvider> actionDescriptorChangeProviders)
|
IEnumerable<IActionDescriptorChangeProvider> actionDescriptorChangeProviders,
|
||||||
|
IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
if (actions == null)
|
if (actions == null)
|
||||||
{
|
{
|
||||||
|
|
@ -42,70 +48,247 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
throw new ArgumentNullException(nameof(actionDescriptorChangeProviders));
|
throw new ArgumentNullException(nameof(actionDescriptorChangeProviders));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (serviceProvider == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(serviceProvider));
|
||||||
|
}
|
||||||
|
|
||||||
_actions = actions;
|
_actions = actions;
|
||||||
_invokerFactory = invokerFactory;
|
_invokerFactory = invokerFactory;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
_actionDescriptorChangeProviders = actionDescriptorChangeProviders.ToArray();
|
_actionDescriptorChangeProviders = actionDescriptorChangeProviders.ToArray();
|
||||||
|
|
||||||
_endpoints = new List<Endpoint>();
|
_endpoints = new List<Endpoint>();
|
||||||
|
ConventionalEndpointInfos = new List<MvcEndpointInfo>();
|
||||||
InitializeEndpoints();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
foreach (var action in _actions.ActionDescriptors.Items)
|
||||||
{
|
{
|
||||||
if (action.AttributeRouteInfo == null)
|
if (action.AttributeRouteInfo == null)
|
||||||
{
|
{
|
||||||
// Action does not have an attribute route
|
// Check each of the conventional templates to see if the action would be reachable
|
||||||
continue;
|
// If the action and template are compatible then create an endpoint with the
|
||||||
}
|
// area/controller/action parameter parts replaced with literals
|
||||||
|
//
|
||||||
RequestDelegate invokerDelegate = (context) =>
|
// e.g. {controller}/{action} with HomeController.Index and HomeController.Login
|
||||||
{
|
// would result in endpoints:
|
||||||
var values = context.Features.Get<IEndpointFeature>().Values;
|
// - Home/Index
|
||||||
var routeData = new RouteData();
|
// - Home/Login
|
||||||
foreach (var kvp in values)
|
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);
|
if (MatchRouteValue(action, endpointInfo, "Area")
|
||||||
|
&& MatchRouteValue(action, endpointInfo, "Controller")
|
||||||
var invoker = _invokerFactory.CreateInvoker(actionContext);
|
&& MatchRouteValue(action, endpointInfo, "Action"))
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
var metadataCollection = new EndpointMetadataCollection(metadata);
|
{
|
||||||
|
var endpoint = CreateEndpoint(action, action.AttributeRouteInfo.Template, action.AttributeRouteInfo.Order, action.AttributeRouteInfo);
|
||||||
_endpoints.Add(new MatcherEndpoint(
|
_endpoints.Add(endpoint);
|
||||||
next => invokerDelegate,
|
}
|
||||||
action.AttributeRouteInfo.Template,
|
|
||||||
action.RouteValues,
|
|
||||||
action.AttributeRouteInfo.Order,
|
|
||||||
metadataCollection,
|
|
||||||
action.DisplayName,
|
|
||||||
new Address(action.AttributeRouteInfo.Name)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
private IChangeToken GetCompositeChangeToken()
|
||||||
{
|
{
|
||||||
if (_actionDescriptorChangeProviders.Length == 1)
|
if (_actionDescriptorChangeProviders.Length == 1)
|
||||||
|
|
@ -136,5 +319,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IReadOnlyList<Endpoint> Endpoints => _endpoints;
|
public override IReadOnlyList<Endpoint> Endpoints => _endpoints;
|
||||||
|
|
||||||
|
public List<MvcEndpointInfo> ConventionalEndpointInfos { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,5 +29,25 @@ namespace Microsoft.AspNetCore.Mvc.Core.Builder
|
||||||
"in the application startup code.",
|
"in the application startup code.",
|
||||||
exception.Message);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
|
|
@ -13,6 +15,7 @@ using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Microsoft.AspNetCore.Routing.Matchers;
|
using Microsoft.AspNetCore.Routing.Matchers;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -54,17 +57,16 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
|
||||||
}
|
}
|
||||||
}, 0));
|
}, 0));
|
||||||
|
|
||||||
|
var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var dataSource = new MvcEndpointDataSource(
|
dataSource.InitializeEndpoints();
|
||||||
mockDescriptorProvider.Object,
|
|
||||||
new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())),
|
|
||||||
Array.Empty<IActionDescriptorChangeProvider>());
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var endpoint = Assert.Single(dataSource.Endpoints);
|
var endpoint = Assert.Single(dataSource.Endpoints);
|
||||||
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
|
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
|
||||||
|
|
||||||
object endpointValue = matcherEndpoint.Values["Name"];
|
var endpointValue = matcherEndpoint.Values["Name"];
|
||||||
Assert.Equal(routeValue, endpointValue);
|
Assert.Equal(routeValue, endpointValue);
|
||||||
|
|
||||||
Assert.Equal(displayName, matcherEndpoint.DisplayName);
|
Assert.Equal(displayName, matcherEndpoint.DisplayName);
|
||||||
|
|
@ -85,8 +87,8 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
|
||||||
var httpContextMock = new Mock<HttpContext>();
|
var httpContextMock = new Mock<HttpContext>();
|
||||||
httpContextMock.Setup(m => m.Features).Returns(featureCollection);
|
httpContextMock.Setup(m => m.Features).Returns(featureCollection);
|
||||||
|
|
||||||
var mockDescriptorProviderMock = new Mock<IActionDescriptorCollectionProvider>();
|
var descriptorProviderMock = new Mock<IActionDescriptorCollectionProvider>();
|
||||||
mockDescriptorProviderMock.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>
|
descriptorProviderMock.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>
|
||||||
{
|
{
|
||||||
new ActionDescriptor
|
new ActionDescriptor
|
||||||
{
|
{
|
||||||
|
|
@ -109,11 +111,12 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
|
||||||
var actionInvokerProviderMock = new Mock<IActionInvokerFactory>();
|
var actionInvokerProviderMock = new Mock<IActionInvokerFactory>();
|
||||||
actionInvokerProviderMock.Setup(m => m.CreateInvoker(It.IsAny<ActionContext>())).Returns(actionInvokerMock.Object);
|
actionInvokerProviderMock.Setup(m => m.CreateInvoker(It.IsAny<ActionContext>())).Returns(actionInvokerMock.Object);
|
||||||
|
|
||||||
|
var dataSource = CreateMvcEndpointDataSource(
|
||||||
|
descriptorProviderMock.Object,
|
||||||
|
new MvcEndpointInvokerFactory(actionInvokerProviderMock.Object));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var dataSource = new MvcEndpointDataSource(
|
dataSource.InitializeEndpoints();
|
||||||
mockDescriptorProviderMock.Object,
|
|
||||||
new MvcEndpointInvokerFactory(actionInvokerProviderMock.Object),
|
|
||||||
Array.Empty<IActionDescriptorChangeProvider>());
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var endpoint = Assert.Single(dataSource.Endpoints);
|
var endpoint = Assert.Single(dataSource.Endpoints);
|
||||||
|
|
@ -139,8 +142,8 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
|
||||||
var httpContextMock = new Mock<HttpContext>();
|
var httpContextMock = new Mock<HttpContext>();
|
||||||
httpContextMock.Setup(m => m.Features).Returns(featureCollection);
|
httpContextMock.Setup(m => m.Features).Returns(featureCollection);
|
||||||
|
|
||||||
var mockDescriptorProviderMock = new Mock<IActionDescriptorCollectionProvider>();
|
var descriptorProviderMock = new Mock<IActionDescriptorCollectionProvider>();
|
||||||
mockDescriptorProviderMock.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>(), 0));
|
descriptorProviderMock.Setup(m => m.ActionDescriptors).Returns(new ActionDescriptorCollection(new List<ActionDescriptor>(), 0));
|
||||||
|
|
||||||
var actionInvokerMock = new Mock<IActionInvoker>();
|
var actionInvokerMock = new Mock<IActionInvoker>();
|
||||||
|
|
||||||
|
|
@ -154,8 +157,8 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
|
||||||
var changeProvider2Mock = new Mock<IActionDescriptorChangeProvider>();
|
var changeProvider2Mock = new Mock<IActionDescriptorChangeProvider>();
|
||||||
changeProvider2Mock.Setup(m => m.GetChangeToken()).Returns(changeTokenMock.Object);
|
changeProvider2Mock.Setup(m => m.GetChangeToken()).Returns(changeTokenMock.Object);
|
||||||
|
|
||||||
var dataSource = new MvcEndpointDataSource(
|
var dataSource = CreateMvcEndpointDataSource(
|
||||||
mockDescriptorProviderMock.Object,
|
descriptorProviderMock.Object,
|
||||||
new MvcEndpointInvokerFactory(actionInvokerProviderMock.Object),
|
new MvcEndpointInvokerFactory(actionInvokerProviderMock.Object),
|
||||||
new[] { changeProvider1Mock.Object, changeProvider2Mock.Object });
|
new[] { changeProvider1Mock.Object, changeProvider2Mock.Object });
|
||||||
|
|
||||||
|
|
@ -166,5 +169,211 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test.Internal
|
||||||
var compositeChangeToken = Assert.IsType<CompositeChangeToken>(changeToken);
|
var compositeChangeToken = Assert.IsType<CompositeChangeToken>(changeToken);
|
||||||
Assert.Equal(2, compositeChangeToken.ChangeTokens.Count);
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,223 +12,5 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
: base(fixture)
|
: 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -33,26 +33,24 @@ namespace RoutingWebSite
|
||||||
{
|
{
|
||||||
app.UseDispatcher();
|
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.MapEndpoint(
|
||||||
//{
|
"ActionAsMethod",
|
||||||
// routes.MapAreaEndpoint(
|
"{controller}/{action}",
|
||||||
// "flightRoute",
|
defaults: new { controller = "Home", action = "Index" });
|
||||||
// "adminRoute",
|
|
||||||
// "{area:exists}/{controller}/{action}",
|
|
||||||
// new { controller = "Home", action = "Index" },
|
|
||||||
// new { area = "Travel" });
|
|
||||||
|
|
||||||
// routes.MapEndpoint(
|
routes.MapEndpoint(
|
||||||
// "ActionAsMethod",
|
"RouteWithOptionalSegment",
|
||||||
// "{controller}/{action}",
|
"{controller}/{action}/{path?}");
|
||||||
// defaults: new { controller = "Home", action = "Index" });
|
});
|
||||||
|
|
||||||
// routes.MapEndpoint(
|
|
||||||
// "RouteWithOptionalSegment",
|
|
||||||
// "{controller}/{action}/{path?}");
|
|
||||||
//});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue