blog post image
Andrew Lock avatar

Andrew Lock

~5 min read

Using routing DataTokens in ASP.NET Core

ASP.NET Core uses routing to map incoming URLs to controllers and action methods, and also to generate URLs when you provide route parameters.

One of the lesser known features of the routing infrastructure is data tokens. These are additional values that can be associated with a particular route, but don't affect the process of URL matching or generation at all.

This post takes a brief look at data tokens and how to use them in your applications for providing supplementary information about a route, but generally speaking I recommend avoiding them if possible.

How to add data tokens to a route

Data tokens are specified when you define your global convention-based routes, in the call to UseMvc. For example, the following route adds a data token called Name to the default route:

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}",
        defaults: null,
        constraints: null,
        dataTokens: new { Name = "default_route" });
});

This route just adds the standard default conventional route to the route collection but it also specifies the Name data token. Note that due to the available overloads, you have to explicitly provide values for defaults and constraints.

This data token is functionally identical to the MapRoute version without dataTokens; the data tokens do not modify the way URLs are routed at all.

Accessing the data tokens from an action method

Whenever a route is used to map an incoming URL to an action method, the data tokens associated with the route are set. These can be accessed from the RouteData.DataTokens property on the Controller base class. This exposes the values as a RouteValueDictionary so you can access them by name. For example, you could retrieve and display the above data token as follows:

public class ProductController : Controller
{
    public string Index()
    {
        var nameTokenValue = (string)RouteData.DataTokens["Name"];
        return nameTokenValue;
    }
}

As you can see, the data token needs to be cast to the appropriate Type it was defined as, in this case string.

This behaviour is different to that of the route parameter values. Route values are stored as strings, so the values need be convertible to a string. Data tokens don't have this restriction, so you can store the values as any type you like and just cast when retrieving it.

Using data tokens to identify the selected route

So what can data tokens actually be used for? Well, fundamentally they are designed to help you associate state data with a specific route. The values aren't dynamic, so they don't change depending on the URL; instead, they are fixed for a given route.

This means you can use data tokens to determine which route was selected during routing. This may be useful if you have multiple routes that map to the same action method, and you need to know which route was selected.

Consider the following couple of routes. They are for two different URLs, but they match to the same action method, HomeController.Index:

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "otherRoute",
        template: "fancy-other-route",
        defaults: new { controller = "Home", action = "Index" },
        constraints: null,
        dataTokens: new { routeOrigin = new RouteOrigin { Name = "fancy route" } });

    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}",
        defaults: null,
        constraints: null,
        dataTokens: new { routeOrigin = new RouteOrigin { Name = "default route" } });
});

Both routes set a data token of type RouteOrigin which is just a simple class, to demonstrate that data tokens can be complex types:

public class RouteOrigin
{
    public string Name { get; set; }
}

So, if we make a request to the app at URL, /, /Home, or /Home/Index, a data token is set with a Name of "default route". If we make a request to /fancy-other-route, then the same action method will be executed, but the data token will have the value "fancy route". To easily visualise these values, I created the HomeController as follows:

public class HomeController : Controller
{
    public string Index()
    {
        var origin = (RouteOrigin)RouteData.DataTokens["routeOrigin"];
        return $"This is the Home controller.\nThe route data is '{origin.Name}'";
    }
}

If we hit the app at the two different paths, you can easily see the different data token values:

Different data token values based on route

This works for our global convention-based routes, but what if you are using attribute-based routing? How do we use data tokens then?

How to use data tokens with RouteAttributes?

The short answer is, you can't! You can use constraints and defaults when you define your routes using RouteAttributes by including them inline as part of the route template. But you can't define data tokens inline, so you can't use them with attribute routing.

The good news is that it really shouldn't be a problem. Attribute routing is often used when you are designing an API for consumption by various clients. It's good practice to have a well defined URL space when designing your APIs; that's one of the reasons attribute routing is suggested over conventional routing in this case.

A "well defined" URL space could mean a lot of things, but one of those would probably be not having multiple different URLs all executing the same action. If there's only one route that can be used to execute an action, then data tokens use their value. For example, the following API defines a route attribute for invoking the Get action.

public class InstrumentController
{
    [HttpGet("/instruments")]
    public IList<string> Get()
    {
        return new List<string> { "Guitar", "Bass", "Drums" };
    }
}

Associating a data token with the route wouldn't give you any more information when this method is invoked. We know which route it came from, as there is only one possibility - the HttpGet Route attribute!

Note: If an action has a route attribute, it can not be routed using conventional routing. That's how we know it's not routed from anywhere else.

When should I use data tokens?

I confess, I'm struggling with this section. Data tokens create a direct coupling between the routes and the action methods being executed. It seems like if your action methods are written in such a way as to depend on this route, you have bigger problems.

Also, the coupling is pretty insidious, as the data tokens are a hidden dependency that you have to know how to access. A more explicit approach might be to just set the values of appropriate route parameters.

For example, we could achieve virtually the same behaviour using explicit route parameters instead of data tokens. We could rewrite the routes as the following:

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "otherRoute",
        template: "fancy-route-with-param",
        defaults: new
        {
            controller = "Home",
            action = "Other",
            routeOrigin = "fancy route"
        });

    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}",
        defaults: new { routeOrigin = "default" });
});

Here we are providing a route value for each route for the routeOrigin parameter. This will be explicitly bound to our action method if we define it like the following:

public class HomeController : Controller
{
    public string Other(string routeOrigin)
    {
        return $"This is the Other action.\nThe route param is '{routeOrigin}'";
    }
}

We now have an explicit dependency on the routeOrigin parameter which is automatically populated for us:

Using route parameters instead

Now, I know this behaviour is not the same as when we used dataTokens. In this case, the routeOrigin parameter is actually bound using the normal model binding mechanism, and you can only use values that can be converted to/from strings. But personally, as I say I don't really see a need for data tokens. Either using the route value approach seems preferable, or alternatively straight dependency injection, depending on your requirements.

Do let me know in the comments if there's a use case I've missed here, as currently I can't really see it!

Andrew Lock | .Net Escapades
Want an email when
there's new posts?