Endpoint Routing in ASP.NET Core Simplified

In this article, let's discuss the drawbacks with the conventional MVC routing and what Endpoint Routing brings to the table in ASP.NET Core 3.1 onwards.

Introduction

Endpoint routing is a new middleware introduced starting from ASP.NET Core 3.x, which is said to replace the traditional MVC routing which has been the default routing mechanism from the good old times till the recent ASP.NET Core 2.2. Endpoint Routing is said to bring in performance improvements and overall a new Routing mechanism into the ASP.NET Core.

But when you look at how it actually works, there is lot more than just a new routing middleware – the Endpoint Routing brings in some interesting and important changes in how Routing happens in an ASP.NET Core application. In this article, let’s go deep dive into what is Endpoint Routing middleware and how it actually works.

What is Routing?

Let’s start by revisiting one of the basics of a Web application – Routing. In simple words, Routing is the process of selecting a handler based on the input request. The web application contains many handlers (called Controllers) which can process a request and generate an appropriate response.

Since there are many Controllers in an application, which is responsible for a unique processing logic or use case, we need to figure out which is the most appropriate Controller which can handle the incoming request based on some “criteria”. This mechanism which decides to which Controller the request must be transferred to – is called as Routing.

There are three important phases in how Routing works:

  1. Route Resolution – processing the incoming request and constructing the Route object based on the Route Mappings
  2. Route Dispatching – determining which Controller / handler “best” matches this route and then invoking that controller with this object
  3. Optional Route Mapping – overriding the default route patterns available in the routing table with a user-defined route patterns

Until ASP.NET Core 2.2, this routing mechanism is taken up by the MVC middleware which is added at the end of the request pipeline. It looks like below:

public void Configure(IApplicationBuilder app) 
{
    app.UseMvc(config => {
        config.MapRoute(
          name: "default", 
          template: "{controller=Home}/{action=Index}/{id?}");
    });
}

As you can see, the middleware optionally takes a RegExp kind of a string called “Routing Pattern” which is applied onto the incoming request Path to decide which Controller matches this request the “best”, and the MVC middleware invokes that particular Controller along with this request object.

Issues with the MVC Routing

There are a few important problems with the existing MVC routing approach:

  1. The UseMvc() middleware needs to be the last executing middleware in the pipeline, which is the only place where the request is truly translated into its actual detail.

    This means that unless the UseMvc() middleware is invoked, you never know which is the controller or handler which the incoming request is going to be translated into.

    You won’t get any detail about the routing based information. This is a disadvantage for route based access controlling middlewares such as Authorization / Authentication, CORS and so on.
  2. The UseMvc() middleware does a dual job of both Route Resolution and Route Dispatch, which is like having more than one responsibility for itself.
  3. Apart from the MVC routing, there are other components as well which require their own routing mechanism such as RazorPages, SignalR and gRPC.

    Having to setup different routing setups for different components within the same block makes the code even more redundant and confusing.

The Endpoint Routing Approach

The Endpoint Routing middleware addresses all these problems by splitting the responsibilities into more than one middleware. In Endpoint Routing, you need to include two middlewares in place of one:

public void Configure(IApplicationBuilder app) 
{
    // routing middleware
    app.UseRouting();

    app.UseCors();

    app.UseAuthentication();

    app.UseAuthorization();

    // endpoint mapping
    app.UseEndpoints((config) => {
        config.MapControllerRoute(
            name: "default", 
            pattern: "{Controller=Home}/{Action=Index}/{id?}");
        config.MapHub<MyHub>("/myhub");
        config.MapRazorPages();
    });
}

You can see from the above snippet, that in the place of UseMvc() we now add two new middlewares – UseRouting() that is purely responsible for Route Resolution, and UseEndpoints() which is responsible for Route Dispatch.

It doesn’t make much sense right? Let’s try to understand what happens here.

How does an Endpoint Works?

You can draw a vague analogy of a train changing tracks to how Endpoint Routing works.

Imagine a train approaching a train station with more than one platforms to arrive on. Before the train changes tracks to the actual platform it is designated to arrive on, it receives a signal with detail on which track it is going to switch onto, and how it needs to move there.

This happens a little bit away from the actual traction change, where the main line splits into various lines for the platforms. The train, once passing through the signal, is now completely aware of where it is heading to and how it needs to change (in its speed or any other internals).

We know that in the case of MVC middleware routing, we don’t know how our incoming request has been resolved into what route and other information. This is because it happens at the UseMvc() middleware and that one sits at the end of the pipeline.

With Endpoint routing, the UseRouting() middleware takes up that job and this middleware now sits at the beginning of the pipeline. The middleware reads the incoming request and then creates an “Endpoint” object which contains all the route-related information of this request, such as what it is parsed into and so on. You can actually find this by the below call:

public void Configure(IApplicationBuilder builder) 
{
    app.UseRouting();

    /* Endpoint data is available at this point */

    app.Use((context, next) => {
        // fetches the Endpoint object set into
        // the HttpContext by the Routing middleware
        var endpoint = context.GetEndpoint();

        // the endpoint object now contains all the detail 
        // about the current request route

        next.Invoke();
    });
}

Whats more? this Endpoint object is placed into the HttpContext object which is available all over the pipeline to all the middlewares that are executed after the UseRouting() middleware.

That makes the route-dependent middlewares such as Authentication, Authorization, CORS and other empowered with route information, making their lives happy. This also solves the first two problems mentioned above.

This is evident from a warning that occurs when you try to place Authorization or CORS middleware not in between the Routing and Endpoints middleware.

Coming to the UseEndpoints() middleware, it takes the responsibility of the Route Dispatching, where the created Endpoints are processed and the “best” matching “Handler” is invoked. What makes it different from the MVC middleware? MVC middleware can dispatch only routes related to a Controller.

In UseEndpoints, all the components which rely on Routing are unified into a single dispatcher which maintains the routing mappings for API / MVC Controllers, SignalR hubs, RazorPages, Blazor Pages, gRPC calls and so on.

This creates an intuitive experience for the developers who now no need to add different routing middlewares for different components and instead just add mappings related to all required components inside the Endpoints middleware. The Endpoints middleware examines the incoming request and decides which Handler of which component needs to be invoked and does the job for us.

public void Configure(IApplicationBuilder builder) 
{
    // endpoint mapping
    app.UseEndpoints((config) => {
        
        // API or MVC Routes
        config.MapControllerRoute(
            name: "default", 
            pattern: "{Controller=Home}/{Action=Index}/{id?}");
        
        // SignalR hubs
        config.MapHub<MyHub>("/myhub");
        
        // Razor Pages
        config.MapRazorPages();
    });
}

What happens after Endpoints routing?

That’s an interesting scenario. For an incoming request that has been matched to some route, the Endpoints middleware is the “last middleware” to be executed in the pipeline.

This is because, if there was a matching route for this Endpoint the middleware invokes the particular handler with this endpoint object and there the response is generated and it flows through the pipeline starting from the UseEndpoints() middleware again in opposite direction.

If we place any middleware after the Endpoints middleware, it is not invoked for this request. On the other hand, Let’s say we have an incoming request which is not matched to any of the route available in the Endpoints middleware. Then the request goes beyond the UseEndpoints() middleware and hits the middleware that is after the UseEndpoints() middleware.

public void Configure(IApplicationBuilder builder) 
{
    app.Use((context, next) => {
        // Fetch the Endpoint object
        var endpoint = context.GetEndpoint();

        // endpoint is NULL
        // since the Routing middleware 
        // is not yet executed
        
        next();
    });

    app.UseRouting();

    app.Use((context, next) => {
        // Fetch the Endpoint object
        var endpoint = context.GetEndpoint();

        // CASE 1: Matching Route Exists
        // endpoint is NOT NULL
        // since the Routing middleware 
        // is executed

        // CASE 2: Matching Route Doesn't Exist
        // for a request which doesn't
        // match any Endpoint Route
        // endpoint is NULL

        next();
    });

    // only a "/" endpoint is mapped
    app.UseEndpoints((config) => {
        config.MapGet("/", context => {
            context.Response.WriteAsync("This is a Default GET");
            Task.FromResult(0);
        });
    });

    app.Use((context, next) => {
        // CASE 1: Matching Route Exists
        // This middleware isn't even invoked

        // CASE 2: Matching Route Doesn't Exist
        // This middleware is invoked
        Console.WriteLine("This is the middleware that follows the Endpoint");
        
        next();
    });
}

Conclusion

While Endpoint Routing replaces the MVC routing from ASP.NET 3.x onwards, it brings together a simple, yet powerful routing mechanism which complements every other middleware which relies on the routing information.

It helps in finding out the routing information ahead of time, and together with the Endpoint middleware which is a single source for all kinds of route mappings the overall code readability and efficiency is improved.

Buy Me A Coffee

Found this article helpful? Please consider supporting!


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *