Getting the route template for a given Controller and Action

Issue

I’m building an application with an angularjs front end and ASP.NET back end. In order for my angular services to make requests, I need to construct a list of url templates given a list of ASP.NET controllers and actions, which I can then send to the client.

The sort of result I’m looking for would be something along the lines of:

[RoutePrefix("my-controller")]
public class MyController : ControllerBase
{
    [Route("my-action/{arg1}/{arg2}")]
    public ActionResult MyAction(int arg1, string arg2) {
        // ...
    }
}

// Elsewhere in my code
GetRouteTemplate("MyController", "MyAction") // => "my-controller/my-action/{arg1}/{arg2}"

I would prefer not to hard code these into the front end, as any change to the routing would break the angular code, so I’m looking for a way to generate them.

My first attempt was using reflection to get all the action methods, and then calling Url.Action to get the urls. I put this in my base controller class:

protected Dictionary<String, String> GetUrlTemplates()
{
    var controllerName = this
        .GetType()
        .Name;

    var actionNames = this
        .GetType()
        .GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)
        .Where(m => !m.IsDefined(typeof(NonActionAttribute), false))
        .Select(m => m.Name)
        .Distinct()
        .ToList();

    return actionNames 
        .ToDictionary(
            actionName => actionName,
            actionName => Url.Action(actionName, controllerName));
}

This is alright for any actions with don’t require arguments, but fails to return the correct route otherwise.

My next attempt was to try to pull the templates directly out of the RouteTable:

protected Dictionary<String, String> GetUrlTemplates()
{
    var controllerName = this
        .GetType()
        .Name;

    var actions = this
        .GetType()
        .GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)
        .Where(m => !m.IsDefined(typeof(NonActionAttribute), false))
        .Select(m => m.Name)
        .Distinct()
        .ToList();

    return RouteTable.Routes
        .OfType<LinkGenerationRoute>()
        .Select(lgr => new {
            url = lgr.Url,
            routeAction = lgr.DataTokens["MS_DirectRouteActions"][0]
        })
        .Where(o => o.routeAction.ControllerDescriptor.ControllerName == controllerName
            && actions.Contains(o.routeAction.ActionName))
        .ToDictionary(
            o => o.routeAction.ActionName,
            o => o.url);
}

This doesn’t work because LinkGenerationRoute is an internal class, so unless there’s another way to access these values in the route table, this also looks like a dead end.

Both of these attempts are a bit ugly and seem like the wrong approach, but I can’t see any other way to go about it. Surely generating url templates for the front end is quite a common task – Is there a “correct” way to get url templates in ASP.NET? Am I fundamentally approaching this issue in the wrong way? Thanks.

Solution

In the end I decided to pull the route templates directly from the RoutePrefix and Route attributes, which is fine for this project seeing as all of the routes are configured this way. I still feel like there should be an easier/less fragile solution though – if anyone responds with a better answer I’ll accept it.

This is the method I added to my base controller:

protected Dictionary<String, String> GetRouteTemplates(Type controllerType, params String[] actions)
{
    var routePrefix = controllerType
        .GetCustomAttribute<RoutePrefixAttribute>()
        .Prefix;

    var routeTemplates = controllerType
        .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
        .Where(m => !actions.Any() || actions.Contains(m.Name))
        .Where(m => !m.IsDefined(typeof(NonActionAttribute), false) && m.IsDefined(typeof(RouteAttribute), false))
        .Select(m => new {m.Name, m.GetCustomAttribute<RouteAttribute>().Template})
        .ToDictionary(r => r.Name, r => $"/{routePrefix}/{r.Template}");

    return routeTemplates;
}

And on any controller I want to return Url templates:

[Route("urls")]
public JsonResult Urls(Int32 organisationId)
{
    var routeTemplates = GetRouteTemplates(GetType());

    return Json(routeTemplates);
}

Answered By – Alfie Woodland

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published