Accessing route values in endpoint middleware in ASP.NET Core 3.0
Tags: aspnetcore, netcore30, routing
Published: 2020-04-19 17:50:42

---
title: Accessing route values in endpoint middleware in ASP.NET Core 3.0
published: true
date: 2020-03-31 10:00:00 UTC
tags: ASP.NET Core,.NET Core 3.0,Routing
canonical_url: https://andrewlock.net/accessing-route-values-in-endpoint-middleware-in-aspnetcore-3/
---

 ![Accessing route values in endpoint middleware in ASP.NET Core 3.0](https://andrewlock.net/content/images/2020/route_values_banner.png)

In [my recent series about upgrading to ASP.NET Core 3.0](https://andrewlock.net/series/upgrading-to-asp-net-core-3/), I described how [the new endpoint routing system](https://andrewlock.net/converting-a-terminal-middleware-to-endpoint-routing-in-aspnetcore-3/#the-evolution-of-routing) can be combined with terminal middleware (i.e. middleware that generates a response). In that post I showed how you can map a path, e.g. `/version`, to the terminal middleware and create an _endpoint_.

There are a number of benefits to this, such as removing the duplication of CORS and Authorization logic that is required in ASP.NET Core 2.x. Another benefit is that you now get proper "MVC-style" routing with placeholders and capture groups, instead of the simple "constant-prefix" `Map()` function that's available in ASP.NET Core 2.0.

In this post I show how you can access the route values exposed by the endpoint routing system in your middleware.

## Route values in endpoint routing

Endpoint routing separates the "identify which route was selected" step, from the "execute the endpoint at that route" step of an ASP.NET Core middleware pipeline (see [my previous post for a more in depth discussion](https://andrewlock.net/converting-a-terminal-middleware-to-endpoint-routing-in-aspnetcore-3/#the-evolution-of-routing)). By splitting these two steps, which previously were both handled internally by the MVC middleware, we can now use fully featured _routing_ (which was previously an MVC feature) with non-MVC components, such as middleware.

> There is a general push in this direction among the ASP.NET Core team currently. [Project Houdini](https://www.youtube.com/watch?v=7dJBmV_psW0) is attempting to turn more MVC features into "core" ASP.NET Core features.

Lets imagine you want to have a simple endpoint that generates random numbers (I know, it's a silly example). The caveat is that the request must also contain a max and min value for the range of numbers. For example, `/random/50/100` should return a random value between 50 and 100.

In ASP.NET Core 2.x, handling dynamic routes like this is a bit of a pain for middleware. In fact, generally speaking, it probably wouldn't be worth the hassle at all - you'd be better off just using the routing and model binding features built in to MVC instead. Nevertheless, for comparison purposes (and to show the benefits of 3.0) I show how you might do this below.

### The basic random number middleware

Whichever approach we're going to be using - either the manual 2.x approach or the 3.0 endpoint routing approach, we need our random number generating middleware. The basic outline of the middleware is shown below

```csharp
public class RandomNumberMiddleware
{
    private static readonly Random _random = new Random();
    public RandomNumberMiddleware(RequestDelegate next) { } // Required

    public async Task InvokeAsync(HttpContext context)
    {
        // Try and get the max and min values from the route/path
        var maybeValues = ParseMaxMin(context);

        if (!maybeValues.HasValue)
        {
            context.Response.StatusCode = 400; //couldn't parse route values
            return;
        }

        // deconstruct the tuple
        var (min, max) = maybeValues.Value; 

        // Get the random number using the extracted limits
        var value = GetRandomValue(min, max);

        // Write the response as plain text
        context.Response.ContentType = "text/plain";
        await context.Response.WriteAsync(value.ToString());
    }

    private static int GetRandomValue(int min, int max)
    {
        // Get a random number (swapping max and min if necessary)
        return min < max
            ? _random.Next(min, max)
            : _random.Next(max, min);
    }

    private static (int min, int max)? ParseMaxMin(HttpContext context)
    {
        /* Parse the values from the HttpContext, shown below*/
    }
}
```

The middleware shown above is pretty much the same both in the "legacy" version and in the endpoint routing version, it's only the `ParseMaxMin` function that will change. Follow through the `InvokeAsync` function to make sure you understand what's happening. First we try and extract the max and min values from the request (we'll come to that shortly), and if that fails, we return a `400 Bad Request` response. If the values were extracted successfully, we generate a random number and return it in a plain text response.

Hopefully that's all relatively easy to follow. Which brings us to the `ParseMaxMin` function. This function needs to grab the max and min values from the incoming request, i.e. the `10` and `50` from `/random/10/50`.

### Parsing the path in ASP.NET Core 2.0

Unfortunately, without endpoint routing we're stuck with plain ol' string manipulation, splitting segments on `/` and trying to parse out numbers:

```csharp
private static (int min, int max)? ParseMaxMin(HttpContext context)
{
    var path = context.Request.Path;
    if (!path.HasValue) return null; // e.g. /random, /random/

    var segments = path.Value.Split('/');

    if (segments.Length != 3) return null; // e.g. /random/12, /random/blah, /random/123/12/tada
    System.Diagnostics.Debug.Assert(string.IsNullOrEmpty(segments[0])); // first segment is always empty
    if (!int.TryParse(segments[1], out var min)) return null; // e.g. /random/blah/123
    if (!int.TryParse(segments[2], out var max)) return null; // e.g. /random/123/blah

    return (min, max);
}
```

This isn't a huge amount of code, but it's the gnarly sort of stuff I hate writing, just to grab a couple of values from the path. I added a bunch of the error checking to catch all the mal-formed URLs where the user doesn't provide two integers as well, and we'll return a 400 response for those. It's not awful, but you can easily see how the code for this could balloon with more complex requirements.

To use the middleware we create a branch using the `Map` extension method in `Startup.Configure()`.

```csharp
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();

        app.Map("/random", // branch the pipeline
            random => random.UseMiddleware<RandomNumberMiddleware>()); // run the middleware

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
        });
    }
}
```

Now if you hit a valid URL prefixed with `/random` you'll get a random number. If you hit the root home page at `/` you'll get the Hello World response, and if you hit an _invalid_ URL prefix with `/random` you'll get a `400 Bad Request` response.

![Image of the different responses possible when using manual parsing of route parameters](https://andrewlock.net/content/images/2020/route_values_01.png)

### Accessing route values from middleware

Now lets look at the alternative approach, using endpoint routing. I'm going to work backwords in this case. To start with, I'll create an extension method to make it easy to register the middleware as an endpoint. This code is [straight out of my previous post](https://andrewlock.net/converting-a-terminal-middleware-to-endpoint-routing-in-aspnetcore-3/#converting-the-middleware-to-endpoint-routing), so be sure to check that one if the code below doesn't make sense.

```csharp
public static class RandomNumberRouteBuilderExtensions
{
    public static IEndpointConventionBuilder MapRandomNumberEndpoint(
        this IEndpointRouteBuilder endpoints, string pattern)
    {
        var pipeline = endpoints.CreateApplicationBuilder()
            .UseMiddleware<RandomNumberMiddleware>()
            .Build();

        return endpoints.Map(pattern, pipeline).WithDisplayName("Random Number");
    }
}
```

With this extension method available, we can register our middleware with a route pattern. This is the same routing used by MVC so you can use all the same features - optional and default values, constraints, catch-all parameters. In this case, I've added constraints to the `min` and `max` route values to ensure that the values provided are convertible to `int`. More on that shortly.

```csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRandomNumberEndpoint("/random/{min:int}/{max:int}"); // <-- Add this line
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}
```

We're nearly there. All that remains is to implement `ParseMaxMin` for our middleware. Now, to be clear, we _could_ still manually parse the values out of `Request.Path` as we did previously, but we don't have to. Endpoint routing takes care of all that itself - all we need to do is to access the route values by name, and convert them to `int`s:

```csharp
private static (int min, int max)? ParseMaxMin(HttpContext context)
{
    // Retrieve the RouteData, and access the route values
    var routeValues = context.GetRouteData().Values;

    // Extract the values
    var minValue = routeValues["min"] as string;
    var maxValue = routeValues["max"] as string;

    // Alternatively, grab the values directly
    // var minValue = context.GetRouteValue("min") as string;
    // var maxValue = context.GetRouteValue("max") as string;

    // The route values are strings, but will always be parseable as int 
    var min = int.Parse(minValue);
    var max = int.Parse(maxValue);

    // The values must be valid, so no error cases
    return (min, max);
}
```

There's a couple of things to note with this approach:

- The route value constraint ensures we can only get valid `int` values for the `min` and `max` routes values, so there's no need for error checking in the `ParseMaxMin` function.
- The route values are stored as `string`s, not `int`s, so we still need to convert them. We have _routing_ but not _model binding_.
- `GetRouteData()` gives access to the whole `RouteData` object, so we can also access other data like [data tokens](https://andrewlock.net/using-routing-datatokens-in-asp-net-core/). Alternatively, you can use `GetRouteValue()` to access a single route value at a time.

The code here is much simpler than before, and isn't messing around with string manipulation. It's much more obvious what's going on! The behaviour is the same as before in the happy cases, but if you enter values for `max` and `min` that can't be parsed as integers, you'll get a `404 Not Found` response, rather than a `400 Bad Request`.

![Image of the different responses possible when using endpoint routing](https://andrewlock.net/content/images/2020/route_values_02.png)

The code in `ParseMaxMin` is definitely nicer than before, but there's a couple of problems with this approach:

- Getting a `404` when you have a typo in the `min` or `max` values is not a good user experience. It happens because we're using the route constraints for validation, which [is generally not a good idea](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-3.1#route-constraint-reference). A better approach would be to remove the constraints, and handle invalid values in the `ParseMaxMin` function instead, returning a `400` to the user instead.
- If you don't specify the route template correctly, such as a typo in `max`/`min` (e.g. `/random/{maximum}/{min}`), or you forgot to include one of them (e.g. `/random/{max}`), you will get an exception at runtime when the middleware executes! 

Clearly we need to be a _bit_ more careful, even when using endpoint routing.

### Playing it safe

First of all, lets remove the `int` constraint from the route path. We can also make the parameters optional, so that requests to `/random/123` etc are still handled by the middleware, ensuring we can generate a more meaningful `400 Bad Request` instead of a 404.

```csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRandomNumberEndpoint("/random/{min?}/{max?}"); // no route value constraints, and optional parameters
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}
```

Next, even though we can easily extract route values from the request, it's wise to be defensive in the middleware. Using `int.TryParse` is a simple way to add some safety, and ensures we return a `400 Bad Request` response when the user enters gibberish, or misses parameters entirely.

```csharp
private static (int min, int max)? ParseMaxMin(HttpContext context)
{
    var routeValues = context.GetRouteData().Values;
    var minValue = routeValues["min"] as string;
    var maxValue = routeValues["max"] as string;

    if (!int.TryParse(minValue, out var min)) return null; // e.g. /random/blah/123
    if (!int.TryParse(maxValue, out var max)) return null; // e.g. /random/123/blah

    return (min, max);
}
```

Running the application again, gives us the best of both worlds. A simple `ParseMaxMin` function, and the application behaviour we're after.

![Image of returning a 400 response instead of a 404 response when an invalid request is made](https://andrewlock.net/content/images/2020/route_values_01.png)

Endpoint routing certainly makes things easier for these sorts of cases, but I think things will really get interesting if Project Houdini ends up allowing things like model binding to be used to simplify some of this mapping code (without bloating the simple approach if that's all you need). Either way, it's good to know accessing routing information is just a `GetRouteData()` away if you need it!

## Summary

In this post I showed how you could access route values from terminal middleware when used with endpoint routing. I showed how endpoint routing removes a lot of the previous boilerplate that would be required when branching the middleware with `Map`. Instead, you can rely on endpoint routing to parse the request's path for you.

As a follow up, I described the behaviour if you go a bit too far with routing, and use route constraints for validation. While tempting (because it's so easy!) it's better to perform validation of route parameters in your middleware, so you can return an appropriate (`400`) response if necessary.