Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Creating an endpoint from multiple middleware in ASP.NET Core 3.x

$
0
0
Creating an endpoint from multiple middleware in ASP.NET Core 3.x

In a recent post I discussed the changes to routing that come in ASP.NET Core 3.0, and how you can convert a "terminal" middleware to the new "endpoint" design. One question I've received is whether that removes the need for "branching" the pipeline, and if not, how can you achieve the same thing with endpoints?

This short post assumes that you've already read my post on converting a terminal middleware to endpoint routing, so if you haven't already, take a look at that one first! I give a quick recap below, but I won't go into details.

Converting terminal middleware to endpoints

In my previous post on terminal middleware I used a VersionMiddleware class as an example. This middleware always returns a response, which is the FileVersion of the app's assembly:

public class VersionMiddleware
{
    readonly RequestDelegate _next;
    static readonly Assembly _entryAssembly = System.Reflection.Assembly.GetEntryAssembly();
    static readonly string _version = FileVersionInfo.GetVersionInfo(_entryAssembly.Location).FileVersion;

    public VersionMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        context.Response.StatusCode = 200;
        await context.Response.WriteAsync(_version);
    }
}

In ASP.NET Core 2.x, you could include the middleware as a conditional branch in your middleware pipeline, so that it only executes if the app receives a request starting with "/version":

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();

    app.UseCors();

    app.Map("/version", versionApp => 
        versionApp.UseMiddleware<VersionMiddleware>()); 

    app.UseMvcWithDefaultRoute();
}

This approach creates a middleware pipeline that looks something like the following, with the Map() call branching the pipeline based on the incoming request.

Image of the app pipeline branching at Map

In ASP.NET Core 3.x using the Map() function is generally not idiomatic. Instead, it's more natural to use Endpoint Routing. In my previous post on terminal middleware, I showed how you could take middleware like the VersionMiddleware above, and turn it into an "endpoint" that can be used like this:

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();

    app.UseRouting();

    app.UseCors();

    // Execute the endpoint selected by the routing middleware
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapVersion("/version");
        endpoints.MapDefaultControllerRoute();
    });
}

This approach is very similar to the original 2.x version, but with a few benefits:

  • You can declaratively add authorization or CORS requirements to the version endpoint.
  • You benefit from full route-template matching, rather than being a simple "starts-with" check
  • Middleware placed between UseRouting() and UseEndpoints() knows which endpoint is going to be run before it runs.

The end result is a bit like the middleware pipeline still branching, but only branching right at the end when it splits into the various endpoints:

Image of the middleware pipeline splitting at the end of the request

Creating larger middleware branches

One of the nice things about using Map in 2.x (which is the origin of the question that spawned this post) was that you are given a whole new IApplicationBuilder that you can add middleware to.

For example, lets say you had a middleware that resizes an image provided in the body of the request, the ResizeImageMiddleware. You don't have the source code for this middleware - maybe you got it from a NuGet package - but you want to add some logging/caching/metrics around the requests. That's easy to do in 2.x, as you can add those extra features as middleware in the Map branch:

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();

    app.UseCors();

    app.Map("/resizeImage", resizeAppBuilder => 
        resizeAppBuilder
            .UseMiddleware<LoggingMiddleware>() // <- Add extra middleware in the branch.
            .UseMiddleware<CachingMiddleware>() // <- Only runs when ResizeImageMiddleware will be hit
            .UseMiddleware<ResizeImageMiddleware>()); 

    // <- The LoggingMiddleware and CachingMiddleware don't run if a request makes it to the MVC branch
    app.UseMvcWithDefaultRoute();
}

Because these middleware are added to the branch, they'll only execute for requests that ultimately hit the ResizeImageMiddleware, not requests that hit the MVC branch

Image of the app pipeline with extra middleware

On the face of it, you might feel a bit stuck when you try to do the same thing in ASP.NET Core 3.0. The middleware pipeline is effectively linear if you don't use Map(). You can't do the following, as that would execute the LoggingMiddleware and CachingMiddleware for every request:

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();

    app.UseRouting();

    app.UseCors();

    // These will now run for every request - not what we want
    app.UseMiddleware<LoggingMiddleware>();
    app.UseMiddleware<CachingMiddleware>(); 

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapResizeImageEndpoint("/resizeImage"); // <- We only want to run them here 
        endpoints.MapDefaultControllerRoute();
    });
}

Technically you could do this if you customize the LoggingMiddleware and CachingMiddleware to execute based on the endpoint selected inside UseRouting, but there's an easier way.

I showed how you can retrieve the selected endpoint name by calling HttpContext.GetEndpoint() in a recent post on Serilog.

I cheated a little bit in the code above, as I didn't show the implementation of the MapResizeImageEndpoint(). If we look at the implementation shown below, then the better option should be more apparent:

public static class ResizeImageEndpointRouteBuilderExtensions
{
    public static IEndpointConventionBuilder MapResizeImageEndpoint(
        this IEndpointRouteBuilder endpoints, string pattern)
    {
        var pipeline = endpoints.CreateApplicationBuilder()
            .UseMiddleware<ResizeImageMiddleware>()
            .Build();

        return endpoints.Map(pattern, pipeline).WithDisplayName("Resize image");
    }
}

This is the same format of extension method as I showed in my previous post for converting the VersionMiddleware to an endpoint. This code shows that an "endpoint" has its own IApplicationBuilder, which means you're not limited to adding a single piece of middleware, you can add a whole pipeline!

public static class ResizeImageEndpointRouteBuilderExtensions
{
    public static IEndpointConventionBuilder MapResizeImageEndpoint(
        this IEndpointRouteBuilder endpoints, string pattern)
    {
        var pipeline = endpoints.CreateApplicationBuilder()
            .UseMiddleware<LoggingMiddleware>() // <- Add the extra middleware 
            .UseMiddleware<CachingMiddleware>() // <- They will only be executed when the endpoint runs
            .UseMiddleware<ResizeImageMiddleware>()
            .Build();

        return endpoints.Map(pattern, pipeline).WithDisplayName("Resize image");
    }
}

With the updated extension method, our middleware pipeline is again apparently simple:

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();

    app.UseRouting();

    app.UseCors();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapResizeImageEndpoint("/resizeImage"); // <- runs all our middleware
        endpoints.MapDefaultControllerRoute();
    });
}

We get all the benefits of endpoint routing here - we could add Authorization or CORS policies to the image endpoint for example - but we've also kept all the middleware unchanged from ASP.NET Core 2.x. In effect we've cut the resize image "branch" off at the original Map(), and moved it to the end instead:

Image of the resize image middleware branch in ASP.NET Core 3.x

The solution shown above doesn't work in all situations - as far as I know there's no easy way to add extra middleware to the MVC endpoints added by MapDefaultControllerRoute() (or the other MVC endpoint methods). If that's something you need, you could look at using MVC filters as a way to hook into the pipeline instead.

Summary

In this post I showed how you can create "composite" endpoints in ASP.NET Core 3.x, which consist of multiple middleware. This was commonly achieved in ASP.NET Core 2.x by calling Map() to branch the middleware pipeline, and can be used in ASP.NET Core 3.x in a similar way


Viewing all articles
Browse latest Browse all 743

Trending Articles