Can I get a the RouteTemplate from AspNetCore FilterContext?

2.1k Views Asked by At

In AspNetCore, given a FilterContext, I'm looking to get a route template e.g. {controller}/{action}/{id?}

In Microsoft.AspNet.WebApi I could get the route template from: HttpControllerContext.RouteData.Route.RouteTemplate

In System.Web.Mvc I could get this from: ControllerContext.RouteData.Route as RouteBase

In AspNetCore there is: FilterContext.ActionDescriptor.AttributeRouteInfo.Template

However, not all routes are attribute routes.

Based on inspection if the attribute is not available, default routes and/or mapped routes can be assembled from: FilterContext.RouteData.Routers.OfType<Microsoft.AspNetCore.Routing.RouteBase>().First() but I'm looking for a documented or a simply better approach.

2

There are 2 best solutions below

1
On

Update (24 Jan 2021)
There is a much much simpler way of retrieving the RoutePattern directly via the HttpContext.

    FilterContext filterContext;
    var endpoint = filterContext.HttpContext.GetEndpoint() as RouteEndpoint;
    var template = endpoint?.RoutePattern?.RawText;
    if (template is null)
        throw new Exception("No route template found, that's absurd");
    Console.WriteLine(template);

GetEndpoint() is an extension method provided in EndpointHttpContextExtensions class inside Microsoft.AspNetCore.Http namespace

Old Answer (Too much work)

All the route builders for an ASP.NET Core app (at least for 3.1) are exposed and registered via IEndpointRouteBuilder, but unfortunately, this is not registered with the DI container, so you can't acquire it directly.The only places where I have seen this interface being exposed, are in the middlewares. So you can build a collection or dictionary out of one of those middlewares, and then use that for your purposes.
e.g

Program.cs

Extension class to build your endpoint collection / dictionary

    internal static class IEndpointRouteBuilderExtensions
    {
        internal static void BuildMap(this IEndpointRouteBuilder endpoints)
        {
            foreach (var item in endpoints.DataSources)
                foreach (RouteEndpoint endpoint in item.Endpoints)
                {
                    /* This is needed for controllers with overloaded actions
                     * Use the RoutePattern.Parameters here 
                     * to generate a unique display name for the route 
                     * instead of this list hack 
                     */
                    
                    if (Program.RouteTemplateMap.TryGetValue(endpoint.DisplayName, out var overloadedRoutes))
                        overloadedRoutes.Add(endpoint.RoutePattern.RawText);
                    else
                        Program.RouteTemplateMap.Add(endpoint.DisplayName, new List<string>() { endpoint.RoutePattern.RawText });
                }
        }
    }

    public class Program
    {
        internal static readonly Dictionary<string, List<string>> RouteTemplateMap = new Dictionary<string, List<string>>();
        /* Rest of things */
    }

Startup.cs

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            /* all other middlewares */
            app.UseEndpoints(endpoints =>
            {
                
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");

                //Use this at the last middlware exposing IEndpointRouteBuilder so that all the routes are built by this point
                endpoints.BuildMap();
            });
        }

And then you can use that Dictionary or Collection, to retrieve the Route Template from the FilterContext.

    FilterContext filterContext;
    Program.RouteTemplateMap.TryGetValue(filterContext.ActionDescriptor.DisplayName, out var template);
    if (template is null)
        throw new Exception("No route template found, that's absurd");

    /* Use the ActionDescriptor.Parameters here 
     * to figure out which overloaded action was called exactly */
    Console.WriteLine(string.Join('\n', template));

To tackle the case of overloaded actions, a list of strings is used for route template (instead of just a string in the Dictionary)
You can use the ActionDescriptor.Parameters in conjunction with RoutePattern.Parameters to generate a unique display name for that route.

0
On

These are the assembled versions, but still looking for a better answer.

AspNetCore 2.0

    FilterContext context;

    string routeTemplate = context.ActionDescriptor.AttributeRouteInfo?.Template;

    if (routeTemplate == null) 
    {
        // manually mapped routes or default routes
        // todo is there a better way, not 100% sure that this is correct either
        // https://github.com/aspnet/Routing/blob/1b0258ab8fccff1306e350fd036d05c3110bbc8e/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs
        IEnumerable<string> segments = context.RouteData.Routers.OfType<Microsoft.AspNetCore.Routing.RouteBase>()
            .FirstOrDefault()?.ParsedTemplate.Segments.Select(s => string.Join(string.Empty, s.Parts
                .Select(p => p.IsParameter ? $"{{{(p.IsCatchAll ? "*" : string.Empty)}{p.Name}{(p.IsOptional ? "?" : string.Empty)}}}" : p.Text)));

        if (segments != null)
        {
            routeTemplate = string.Join("/", segments);
        }
    }

AspNetCore 3.0 with Endpoint Routing

            RoutePattern routePattern = null;

            var endpointFeature = context.HttpContext.Features[typeof(Microsoft.AspNetCore.Http.Features.IEndpointFeature)]
                                           as Microsoft.AspNetCore.Http.Features.IEndpointFeature;
            var endpoint = endpointFeature?.Endpoint;

            if (endpoint != null)
            {
                routePattern = (endpoint as RouteEndpoint)?.RoutePattern;
            }

            string formatRoutePart(RoutePatternPart part)
            {
                if (part.IsParameter)
                {
                    RoutePatternParameterPart p = (RoutePatternParameterPart)part;
                    return $"{{{(p.IsCatchAll ? "*" : string.Empty)}{p.Name}{(p.IsSeparator ? " ? " : string.Empty)}}}";
                }
                else if (part.IsLiteral)
                {
                    RoutePatternLiteralPart p = (RoutePatternLiteralPart)part;
                    return p.Content;
                }
                else if(part.IsSeparator)
                {
                    RoutePatternSeparatorPart p = (RoutePatternSeparatorPart)part;
                    return p.Content;
                }
                else
                {
                    throw new NotSupportedException("Unknown Route PatterPart");
                }
            }

            if (routePattern != null)
            {
                // https://github.com/aspnet/Routing/blob/1b0258ab8fccff1306e350fd036d05c3110bbc8e/src/Microsoft.AspNetCore.Routing/Template/TemplatePart.cs
                routeString = string.Join("/", routePattern.PathSegments.SelectMany(s => s.Parts).Select(p => formatRoutePart(p)));
            }