diff --git a/build/dependencies.props b/build/dependencies.props
index a2f537f41d..7c65067dfa 100644
--- a/build/dependencies.props
+++ b/build/dependencies.props
@@ -48,8 +48,8 @@
3.0.0-alpha1-10617
3.0.0-alpha1-10617
3.0.0-alpha1-10617
- 3.0.0-alpha1-10617
- 3.0.0-alpha1-10617
+ 3.0.0-a-alpha1-master-builder-17073
+ 3.0.0-a-alpha1-master-builder-17073
3.0.0-alpha1-10617
3.0.0-alpha1-10617
3.0.0-alpha1-10617
diff --git a/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationAppBuilderExtensions.cs b/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationAppBuilderExtensions.cs
new file mode 100644
index 0000000000..6a2fa48911
--- /dev/null
+++ b/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationAppBuilderExtensions.cs
@@ -0,0 +1,21 @@
+// 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 MvcSandbox.AuthorizationMiddleware;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ public static class AuthorizationAppBuilderExtensions
+ {
+ public static IApplicationBuilder UseAuthorization(this IApplicationBuilder app)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
+ return app.UseMiddleware();
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationEndpointConventionBuilder.cs b/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationEndpointConventionBuilder.cs
new file mode 100644
index 0000000000..5fb990e1f3
--- /dev/null
+++ b/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationEndpointConventionBuilder.cs
@@ -0,0 +1,16 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Routing;
+
+namespace MvcSandbox.AuthorizationMiddleware
+{
+ public static class AuthorizationEndpointConventionBuilder
+ {
+ public static T RequireAuthorization(this T builder, params string[] roles) where T : IEndpointConventionBuilder
+ {
+ builder.Apply(model => model.Metadata.Add(new AuthorizeMetadataAttribute(roles)));
+ return builder;
+ }
+ }
+}
diff --git a/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationMiddleware.cs b/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationMiddleware.cs
new file mode 100644
index 0000000000..23a5e45f3f
--- /dev/null
+++ b/samples/MvcSandbox/AuthorizationMiddleware/AuthorizationMiddleware.cs
@@ -0,0 +1,52 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
+
+namespace MvcSandbox.AuthorizationMiddleware
+{
+ public class AuthorizationMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ public AuthorizationMiddleware(RequestDelegate next)
+ {
+ if (next == null)
+ {
+ throw new ArgumentNullException(nameof(next));
+ }
+
+ _next = next;
+ }
+
+ public async Task Invoke(HttpContext httpContext)
+ {
+ var endpoint = httpContext.Features.Get()?.Endpoint;
+ var metadata = endpoint?.Metadata?.GetMetadata();
+
+ // Only run authorization if endpoint has metadata
+ if (metadata != null)
+ {
+ // Check if role querystring value is a valid role
+ if (!httpContext.Request.Query.TryGetValue("role", out var role) ||
+ !metadata.Roles.Contains(role.ToString(), StringComparer.OrdinalIgnoreCase))
+ {
+ httpContext.Response.StatusCode = 401;
+ httpContext.Response.ContentType = "text/plain";
+ await httpContext.Response.WriteAsync($"Unauthorized access to '{endpoint.DisplayName}'.");
+ return;
+ }
+ }
+
+ await _next(httpContext);
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/MvcSandbox/AuthorizationMiddleware/AuthorizeMetadataAttribute.cs b/samples/MvcSandbox/AuthorizationMiddleware/AuthorizeMetadataAttribute.cs
new file mode 100644
index 0000000000..7c95327d0d
--- /dev/null
+++ b/samples/MvcSandbox/AuthorizationMiddleware/AuthorizeMetadataAttribute.cs
@@ -0,0 +1,20 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MvcSandbox.AuthorizationMiddleware
+{
+ public class AuthorizeMetadataAttribute : Attribute
+ {
+ public AuthorizeMetadataAttribute(string[] roles)
+ {
+ Roles = roles;
+ }
+
+ public string[] Roles { get; }
+ }
+}
diff --git a/samples/MvcSandbox/Controllers/HomeController.cs b/samples/MvcSandbox/Controllers/HomeController.cs
index 2aa4ff6829..87406bce53 100644
--- a/samples/MvcSandbox/Controllers/HomeController.cs
+++ b/samples/MvcSandbox/Controllers/HomeController.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Mvc;
+using MvcSandbox.AuthorizationMiddleware;
namespace MvcSandbox.Controllers
{
diff --git a/samples/MvcSandbox/Controllers/LoginController.cs b/samples/MvcSandbox/Controllers/LoginController.cs
new file mode 100644
index 0000000000..5110850fbf
--- /dev/null
+++ b/samples/MvcSandbox/Controllers/LoginController.cs
@@ -0,0 +1,16 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Mvc;
+
+namespace MvcSandbox.Controllers
+{
+ [Route("[controller]/[action]")]
+ public class LoginController : Controller
+ {
+ public IActionResult Index()
+ {
+ return View();
+ }
+ }
+}
diff --git a/samples/MvcSandbox/HealthChecks/HealthChecksEndpointRouteBuilderExtensions.cs b/samples/MvcSandbox/HealthChecks/HealthChecksEndpointRouteBuilderExtensions.cs
new file mode 100644
index 0000000000..b89acdd2b4
--- /dev/null
+++ b/samples/MvcSandbox/HealthChecks/HealthChecksEndpointRouteBuilderExtensions.cs
@@ -0,0 +1,28 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ public static class HealthChecksEndpointRouteBuilderExtensions
+ {
+ private static readonly Random _random = new Random();
+
+ public static IEndpointConventionBuilder MapHealthChecks(this IEndpointRouteBuilder builder, string pattern)
+ {
+ return builder.MapGet(
+ pattern,
+ async httpContext =>
+ {
+ await httpContext.Response.WriteAsync(_random.Next() % 2 == 0 ? "Up!" : "Down!");
+ });
+ }
+ }
+}
diff --git a/samples/MvcSandbox/Startup.cs b/samples/MvcSandbox/Startup.cs
index d9f96bf08b..12c1291669 100644
--- a/samples/MvcSandbox/Startup.cs
+++ b/samples/MvcSandbox/Startup.cs
@@ -1,11 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using MvcSandbox.AuthorizationMiddleware;
namespace MvcSandbox
{
@@ -20,14 +27,46 @@ namespace MvcSandbox
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app)
{
- app.UseDeveloperExceptionPage();
- app.UseStaticFiles();
- app.UseMvc(routes =>
+ app.UseEndpointRouting(builder =>
{
- routes.MapRoute(
+ builder.MapGet(
+ requestDelegate: WriteEndpoints,
+ pattern: "/endpoints",
+ displayName: "Home");
+
+ builder.MapMvcRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
+
+ builder.MapMvcControllers();
+ builder.MapRazorPages();
+
+ builder.MapHealthChecks("/healthz");
});
+
+ app.UseDeveloperExceptionPage();
+ app.UseStaticFiles();
+
+ app.UseAuthorization();
+
+ app.UseEndpoint();
+ }
+
+ private static Task WriteEndpoints(HttpContext httpContext)
+ {
+ var dataSource = httpContext.RequestServices.GetRequiredService();
+
+ var sb = new StringBuilder();
+ sb.AppendLine("Endpoints:");
+ foreach (var endpoint in dataSource.Endpoints.OfType().OrderBy(e => e.RoutePattern.RawText, StringComparer.OrdinalIgnoreCase))
+ {
+ sb.AppendLine($"- {endpoint.RoutePattern.RawText} '{endpoint.DisplayName}'");
+ }
+
+ var response = httpContext.Response;
+ response.StatusCode = 200;
+ response.ContentType = "text/plain";
+ return response.WriteAsync(sb.ToString());
}
public static void Main(string[] args)
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Builder/DefaultEndpointConventionBuilder.cs b/src/Microsoft.AspNetCore.Mvc.Core/Builder/DefaultEndpointConventionBuilder.cs
new file mode 100644
index 0000000000..8427ba5c3b
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Builder/DefaultEndpointConventionBuilder.cs
@@ -0,0 +1,24 @@
+// 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.Routing;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ internal class DefaultEndpointConventionBuilder : IEndpointConventionBuilder
+ {
+ public DefaultEndpointConventionBuilder()
+ {
+ Conventions = new List>();
+ }
+
+ public List> Conventions { get; }
+
+ public void Apply(Action convention)
+ {
+ Conventions.Add(convention);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs
index 06e26ed407..6cde8eccd5 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcApplicationBuilderExtensions.cs
@@ -90,9 +90,7 @@ namespace Microsoft.AspNetCore.Builder
if (options.Value.EnableEndpointRouting)
{
var mvcEndpointDataSource = app.ApplicationServices
- .GetRequiredService>()
- .OfType()
- .First();
+ .GetRequiredService();
var parameterPolicyFactory = app.ApplicationServices
.GetRequiredService();
@@ -122,11 +120,21 @@ namespace Microsoft.AspNetCore.Builder
}
}
+ // Include all controllers with attribute routing and Razor pages
+ var defaultEndpointConventionBuilder = new DefaultEndpointConventionBuilder();
+ mvcEndpointDataSource.AttributeRoutingConventionResolvers.Add((actionDescriptor) =>
+ {
+ return defaultEndpointConventionBuilder;
+ });
+
if (!app.Properties.TryGetValue(EndpointRoutingRegisteredKey, out _))
{
// Matching middleware has not been registered yet
// For back-compat register middleware so an endpoint is matched and then immediately used
- app.UseEndpointRouting();
+ app.UseEndpointRouting(routerBuilder =>
+ {
+ routerBuilder.DataSources.Add(mvcEndpointDataSource);
+ });
}
return app.UseEndpoint();
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs
index 161189b871..289a1a2f34 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs
@@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Routing.Patterns;
namespace Microsoft.AspNetCore.Builder
{
- internal class MvcEndpointInfo
+ internal class MvcEndpointInfo : DefaultEndpointConventionBuilder
{
public MvcEndpointInfo(
string name,
@@ -43,6 +43,7 @@ namespace Microsoft.AspNetCore.Builder
public string Name { get; }
public string Pattern { get; }
+ public Type ControllerType { get; set; }
// Non-inline defaults
public RouteValueDictionary Defaults { get; }
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointRouteBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointRouteBuilderExtensions.cs
new file mode 100644
index 0000000000..a3bd2d9951
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointRouteBuilderExtensions.cs
@@ -0,0 +1,148 @@
+// 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.Mvc;
+using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.AspNetCore.Mvc.Internal;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ public static class MvcEndpointRouteBuilderExtensions
+ {
+ public static IEndpointConventionBuilder MapMvcControllers(
+ this IEndpointRouteBuilder routeBuilder)
+ {
+ return MapMvcControllers(routeBuilder);
+ }
+
+ public static IEndpointConventionBuilder MapMvcControllers(
+ this IEndpointRouteBuilder routeBuilder) where TController : ControllerBase
+ {
+ var mvcEndpointDataSource = routeBuilder.DataSources.OfType().FirstOrDefault();
+
+ if (mvcEndpointDataSource == null)
+ {
+ mvcEndpointDataSource = routeBuilder.ServiceProvider.GetRequiredService();
+ routeBuilder.DataSources.Add(mvcEndpointDataSource);
+ }
+
+ var conventionBuilder = new DefaultEndpointConventionBuilder();
+
+ mvcEndpointDataSource.AttributeRoutingConventionResolvers.Add(actionDescriptor =>
+ {
+ if (actionDescriptor is ControllerActionDescriptor controllerActionDescriptor &&
+ typeof(TController).IsAssignableFrom(controllerActionDescriptor.ControllerTypeInfo))
+ {
+ return conventionBuilder;
+ }
+
+ return null;
+ });
+
+ return conventionBuilder;
+ }
+
+ public static IEndpointConventionBuilder MapMvcRoute(
+ this IEndpointRouteBuilder routeBuilder,
+ string name,
+ string template)
+ {
+ return MapMvcRoute(routeBuilder, name, template, defaults: null);
+ }
+
+ public static IEndpointConventionBuilder MapMvcRoute(
+ this IEndpointRouteBuilder routeBuilder,
+ string name,
+ string template,
+ object defaults)
+ {
+ return MapMvcRoute(routeBuilder, name, template, defaults, constraints: null);
+ }
+
+ public static IEndpointConventionBuilder MapMvcRoute(
+ this IEndpointRouteBuilder routeBuilder,
+ string name,
+ string template,
+ object defaults,
+ object constraints)
+ {
+ return MapMvcRoute(routeBuilder, name, template, defaults, constraints, dataTokens: null);
+ }
+
+ public static IEndpointConventionBuilder MapMvcRoute(
+ this IEndpointRouteBuilder routeBuilder,
+ string name,
+ string template,
+ object defaults,
+ object constraints,
+ object dataTokens)
+ {
+ return MapMvcRoute(routeBuilder, name, template, defaults, constraints, dataTokens);
+ }
+
+ public static IEndpointConventionBuilder MapMvcRoute(
+ this IEndpointRouteBuilder routeBuilder,
+ string name,
+ string template) where TController : ControllerBase
+ {
+ return MapMvcRoute(routeBuilder, name, template, defaults: null);
+ }
+
+ public static IEndpointConventionBuilder MapMvcRoute(
+ this IEndpointRouteBuilder routeBuilder,
+ string name,
+ string template,
+ object defaults) where TController : ControllerBase
+ {
+ return MapMvcRoute(routeBuilder, name, template, defaults, constraints: null);
+ }
+
+ public static IEndpointConventionBuilder MapMvcRoute(
+ this IEndpointRouteBuilder routeBuilder,
+ string name,
+ string template,
+ object defaults,
+ object constraints) where TController : ControllerBase
+ {
+ return MapMvcRoute(routeBuilder, name, template, defaults, constraints, dataTokens: null);
+ }
+
+ public static IEndpointConventionBuilder MapMvcRoute(
+ this IEndpointRouteBuilder routeBuilder,
+ string name,
+ string template,
+ object defaults,
+ object constraints,
+ object dataTokens) where TController : ControllerBase
+ {
+ var mvcEndpointDataSource = routeBuilder.DataSources.OfType().FirstOrDefault();
+
+ if (mvcEndpointDataSource == null)
+ {
+ mvcEndpointDataSource = routeBuilder.ServiceProvider.GetRequiredService();
+ routeBuilder.DataSources.Add(mvcEndpointDataSource);
+ }
+
+ var endpointInfo = new MvcEndpointInfo(
+ name,
+ template,
+ new RouteValueDictionary(defaults),
+ new RouteValueDictionary(constraints),
+ new RouteValueDictionary(dataTokens),
+ routeBuilder.ServiceProvider.GetRequiredService());
+
+ endpointInfo.ControllerType = typeof(TController);
+
+ mvcEndpointDataSource.ConventionalEndpointInfos.Add(endpointInfo);
+
+ return endpointInfo;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
index 95a4afb21b..d645322384 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
@@ -270,8 +270,7 @@ namespace Microsoft.Extensions.DependencyInjection
//
// Endpoint Routing / Endpoints
//
- services.TryAddEnumerable(
- ServiceDescriptor.Singleton());
+ services.TryAddSingleton();
services.TryAddSingleton();
//
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs
index d371d02f79..c0e5215259 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs
@@ -10,6 +10,7 @@ using System.Threading;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
@@ -57,6 +58,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
_parameterPolicyFactory = parameterPolicyFactory;
ConventionalEndpointInfos = new List();
+ AttributeRoutingConventionResolvers = new List>();
// IMPORTANT: this needs to be the last thing we do in the constructor. Change notifications can happen immediately!
//
@@ -72,6 +74,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public List ConventionalEndpointInfos { get; }
+ public List> AttributeRoutingConventionResolvers { get; }
+
public override IReadOnlyList Endpoints
{
get
@@ -134,6 +138,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// - Home/Login
foreach (var endpointInfo in ConventionalEndpointInfos)
{
+ if (endpointInfo.ControllerType != null &&
+ endpointInfo.ControllerType != typeof(ControllerBase))
+ {
+ if (!ValidateControllerConstraint(action, endpointInfo))
+ {
+ // Action descriptor does not belong to a controller of the specified type
+ continue;
+ }
+ }
+
// An 'endpointInfo' is applicable if:
// 1. it has a parameter (or default value) for 'required' non-null route value
// 2. it does not have a parameter (or default value) for 'required' null route value
@@ -164,11 +178,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal
endpointInfo.DataTokens,
endpointInfo.ParameterPolicies,
suppressLinkGeneration: false,
- suppressPathMatching: false);
+ suppressPathMatching: false,
+ endpointInfo.Conventions);
}
}
else
{
+ var conventionBuilder = ResolveActionConventionBuilder(action);
+ if (conventionBuilder == null)
+ {
+ // No convention builder for this action
+ // Do not create an endpoint for it
+ continue;
+ }
+
var attributeRoutePattern = RoutePatternFactory.Parse(action.AttributeRouteInfo.Template);
CreateEndpoints(
@@ -183,7 +206,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
dataTokens: null,
allParameterPolicies: null,
action.AttributeRouteInfo.SuppressLinkGeneration,
- action.AttributeRouteInfo.SuppressPathMatching);
+ action.AttributeRouteInfo.SuppressPathMatching,
+ conventionBuilder.Conventions);
}
}
@@ -205,6 +229,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
}
+ private DefaultEndpointConventionBuilder ResolveActionConventionBuilder(ActionDescriptor action)
+ {
+ foreach (var filter in AttributeRoutingConventionResolvers)
+ {
+ var conventionBuilder = filter(action);
+ if (conventionBuilder != null)
+ {
+ return conventionBuilder;
+ }
+ }
+
+ return null;
+ }
+
+ private static bool ValidateControllerConstraint(ActionDescriptor action, MvcEndpointInfo endpointInfo)
+ {
+ if (action is ControllerActionDescriptor controllerActionDescriptor)
+ {
+ return endpointInfo.ControllerType.IsAssignableFrom(controllerActionDescriptor.ControllerTypeInfo);
+ }
+
+ return false;
+ }
+
// CreateEndpoints processes the route pattern, replacing area/controller/action parameters with endpoint values
// Because of default values it is possible for a route pattern to resolve to multiple endpoints
private int CreateEndpoints(
@@ -219,7 +267,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
RouteValueDictionary dataTokens,
IDictionary> allParameterPolicies,
bool suppressLinkGeneration,
- bool suppressPathMatching)
+ bool suppressPathMatching,
+ List> conventions)
{
var newPathSegments = routePattern.PathSegments.ToList();
@@ -245,7 +294,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
routeOrder++,
dataTokens,
suppressLinkGeneration,
- suppressPathMatching);
+ suppressPathMatching,
+ conventions);
endpoints.Add(subEndpoint);
}
@@ -314,7 +364,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
routeOrder++,
dataTokens,
suppressLinkGeneration,
- suppressPathMatching);
+ suppressPathMatching,
+ conventions);
endpoints.Add(endpoint);
return routeOrder;
@@ -448,7 +499,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
int order,
RouteValueDictionary dataTokens,
bool suppressLinkGeneration,
- bool suppressPathMatching)
+ bool suppressPathMatching,
+ List> conventions)
{
RequestDelegate requestDelegate = (context) =>
{
@@ -463,7 +515,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var defaults = new RouteValueDictionary(nonInlineDefaults);
EnsureRequiredValuesInDefaults(action.RouteValues, defaults);
- var metadataCollection = BuildEndpointMetadata(
+ var model = new RouteEndpointModel(requestDelegate, RoutePatternFactory.Pattern(patternRawText, defaults, parameterPolicies: null, segments), order);
+
+ AddEndpointMetadata(
+ model.Metadata,
action,
routeName,
new RouteValueDictionary(action.RouteValues),
@@ -471,17 +526,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal
suppressLinkGeneration,
suppressPathMatching);
- var endpoint = new RouteEndpoint(
- requestDelegate,
- RoutePatternFactory.Pattern(patternRawText, defaults, parameterPolicies: null, segments),
- order,
- metadataCollection,
- action.DisplayName);
+ model.DisplayName = action.DisplayName;
- return endpoint;
+ if (conventions != null)
+ {
+ foreach (var convention in conventions)
+ {
+ convention(model);
+ }
+ }
+
+ return (RouteEndpoint)model.Build();
}
- private static EndpointMetadataCollection BuildEndpointMetadata(
+ private static void AddEndpointMetadata(
+ IList