Implement ASP.NET Core style attribute based routing in ASP.NET MVC?

1 week ago 12
ARTICLE AD BOX

So I actually implemented it, but there is one last problem I need to resolve. My implementation can be summarized by these two classes:

Hooking into the ASP.NET MVC startup code to register TokenizedDirectRouteProvider:

public static class RouteCollectionExtensions { public static void MapMvcAttributeRoutesWithTokens(this RouteCollection routes, IInlineConstraintResolver constraintResolver = null) { if (routes == null) { throw new ArgumentNullException(nameof(routes)); } routes.MapMvcAttributeRoutes(constraintResolver ?? new DefaultInlineConstraintResolver(), new TokenizedDirectRouteProvider()); } }

And its usage:

public static class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapMvcAttributeRoutesWithTokens(); // Conventional route for SimpleController (global) routes.MapRoute( name: "Simple", url: "{controller}/{action}/{id}", defaults: null, constraints: new { controller = "Simple" }, namespaces: [typeof(WebProcessorLibrary.Controllers.SimpleController).Namespace] ); } }

Where the TokenizedDirectRouteProvider is:

internal class TokenizedDirectRouteProvider : DefaultDirectRouteProvider { protected override IReadOnlyList<RouteEntry> GetControllerDirectRoutes(ControllerDescriptor controllerDescriptor, IReadOnlyList<ActionDescriptor> actionDescriptors, IReadOnlyList<IDirectRouteFactory> factories, IInlineConstraintResolver constraintResolver) => [.. actionDescriptors.SelectMany(o => GetActionDirectRoutes(o, factories, constraintResolver))]; protected override IReadOnlyList<RouteEntry> GetActionDirectRoutes(ActionDescriptor actionDescriptor, IReadOnlyList<IDirectRouteFactory> factories, IInlineConstraintResolver constraintResolver) => base.GetActionDirectRoutes(actionDescriptor, [.. factories.Select(f => WrapDirectRouteFactory(f, actionDescriptor))], constraintResolver); private static IDirectRouteFactory WrapDirectRouteFactory(IDirectRouteFactory proto, ActionDescriptor actionDescriptor) => new TokenReplacingRouteFactory(proto, actionDescriptor); /// <summary> /// Wrapper factory that processes tokens in route templates before creating routes. /// </summary> private class TokenReplacingRouteFactory(IDirectRouteFactory m_innerFactory, ActionDescriptor m_actionDescriptor) : IDirectRouteFactory { public RouteEntry CreateRoute(DirectRouteFactoryContext context) { if (m_innerFactory is RouteAttribute routeAttr) { string template = routeAttr.Template; if (!string.IsNullOrEmpty(template)) { var areaName = GetAreaName(m_actionDescriptor.ControllerDescriptor); var hasAreaToken = template.Contains("[area]"); if (!string.IsNullOrEmpty(areaName) != hasAreaToken) { var msg = hasAreaToken ? $"The route template '{template}' has the [area] token, but the controller '{m_actionDescriptor.ControllerDescriptor.ControllerName}' is not in an area." : $"Controller '{m_actionDescriptor.ControllerDescriptor.ControllerName}' is in area '{areaName}', but route template '{template}' does not contain the required '[area]' token."; throw new InvalidOperationException(msg); } var processedTemplate = template .Replace("[controller]", m_actionDescriptor.ControllerDescriptor.ControllerName) .Replace("[action]", m_actionDescriptor.ActionName) .Replace("[area]", areaName); // If the template contains [area] token and doesn't start with ~, prepend ~ to make it absolute // This prevents ASP.NET Framework from adding the area prefix automatically if (hasAreaToken && !processedTemplate.StartsWith("~")) { processedTemplate = "~/" + processedTemplate; } if (processedTemplate != template) { LogEx.ForContext<TokenReplacingRouteFactory>().Debug("'{OriginalTemplate}' -> '{ProcessedTemplate}' for {Controller}.{Action}", template, processedTemplate, m_actionDescriptor.ControllerDescriptor.ControllerName, m_actionDescriptor.ActionName); IDirectRouteFactory newRouteFactory = new RouteAttribute(processedTemplate) { Name = routeAttr.Name, Order = routeAttr.Order }; // Create the route entry using the new factory var routeEntry = newRouteFactory.CreateRoute(context); if (areaName != null) { Debug.Assert(areaName.Equals(context.AreaPrefix)); Debug.Assert(areaName.Equals(routeEntry.Route.DataTokens["area"])); } return routeEntry; } } } return m_innerFactory.CreateRoute(context); } private string GetAreaName(ControllerDescriptor controllerDescriptor) { var routeAreaAttr = controllerDescriptor.GetCustomAttributes(typeof(RouteAreaAttribute), inherit: true) .OfType<RouteAreaAttribute>() .FirstOrDefault(); if (routeAreaAttr != null) { return routeAreaAttr.AreaName; } // Fallback: check the namespace for area name string controllerNamespace = controllerDescriptor.ControllerType.Namespace ?? string.Empty; var i = controllerNamespace.IndexOf(".Areas."); if (i >= 0) { int j = controllerNamespace.IndexOf('.', i + 7); if (j >= 0) { return controllerNamespace[(i + 7)..j]; } } return null; } } }

This allows me to use ASP.NET Core style attributes like [Route("[controller]/[action]/{id?}")].

My problem is the Controller.Initialize override:

[Route("[controller]/[action]/{id?}")] public class TestController: Controller { ... protected override void Initialize(RequestContext requestContext) { base.Initialize(requestContext); } }

Inside it I can access the route data like this:

var routeData = requestContext.RouteData; routeData.Values.TryGetValue("controller", out var controller); routeData.Values.TryGetValue("action", out var action); routeData.Values.TryGetValue("id", out var id);

The problem - when using my implementation of the attribute based routing the action and the id keys are absent. (The request in question includes id in the url path).

A controller which falls back to the convention based routing (SimpleController) does not have this problem - all the keys are present in its override of the Controller.Initialize method.

I am specifically interested in the Controller.Initialize method. And I cannot use Asp.Net Core itself, though I must have its attribute based routing.

How can this be fixed?

Read Entire Article