Skip to content
Elisenda Gascon By Elisenda Gascon Apprentice Engineer II
Understanding Middleware in ASP.NET Core

TLDR; Middleware is responsible for processing HTTP messages in ASP.NET Core applications. In this post, we define the middleware pipeline, explore a real example, and look at how to create custom middleware components.

In this post, we explore the Configure method in the Startup class in a ASP.NET Core web application. Inside of the configure method we find the middleware pipeline, responsible for processing HTTP messages.

The Middleware Pipeline

Middleware is responsible for processing HTTP messages. Let’s illustrate this with a very simple middleware pipeline.

Showing diagram of the middleware pipeline.

Imagine a middleware pipeline consisting of three elements. Once an HTTP request is received, it will be inspected by the first piece of middleware. If it doesn’t have any response to return or any exception to throw, it passes the request on to the next component in the pipeline. Again, after inspecting the request, this component will either return a response, throw an exception, or pass the request along to the following piece of middleware. Suppose it does pass it along. Imagine this third and last component of this pipeline is responsible for finding something inside of the application that can process the request. If this was a POST request for a certain URL, it would go and look for a Razor Page in the folder with the same name as the URL in the request. If it finds the file, it will pass the request on to that Razor Page, which will generate a response in the form of HTML.

At this point, it is important to note that the middleware pipeline is bi-directional. When the request enters the pipeline, it will flow through the different components of the pipeline until one of them can produce a response, or an exception is thrown. Once this happens, the response will go through the pipeline again in the opposite direction, being inspected by the same components that the request went through on the way in.

So, in our example, once this third piece of middleware has emitted a response, this will be passed back onto the second component, and to the first one after. Most components won’t act after inspecting the response, but some might be in charge of recording information about the process or rendering an exception thrown by one of the components in the pipeline, if that was the case. We will later see some concrete examples of middleware components and what they do.

It is important to note that the order in which the middleware is placed inside of the Configure method in the Startup class will determine the order in which the request will invoke the different components of the middleware pipeline. The order of the components of the pipeline will affect the performance of the app, as we will see later.

A Concrete Example

Now that we have described how the middleware pipeline works, let’s look at a real example of middleware in an ASP.NET Core web application. When building a new web application, Visual Studio adds a middleware pipeline by default.

Below is the Configure method in the Startup class in a new ASP.NET Core web app.

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

Let’s look at what each of these middleware components do.

  • DeveloperExceptionPage:

In order to understand what these components do, let’s remind ourselves again that the middleware pipeline is bi-directional. This means that the first component of the pipeline will be both the first to inspect the incoming HTTP request and the last to inspect the response. This will condition what we decide to place at the very beginning of the pipeline (or at the very end). In this case, DeveloperExceptionPage has been placed at the top because it cares about being the last to inspect the response. Its task is to handle exceptions being thrown by any other component in the pipeline.

When DeveloperExceptionPage receives an HTTP request, it will simply pass it on to the next component in the pipeline. If any subsequent component in the pipeline throws an exception, it will be passed back to the previous components of the pipeline and will finally reach DeveloperExceptionPage. DeveloperExceptionPage is in charge of rendering HTML containing detailed information on the exception, which will be useful for developers when debugging the problem.

  • ExceptionHandler:

Note that DeveloperExceptionPage is only used in development. When running in production, the level of detail rendered would be not only unnecessary, but could also expose sensitive information to users. When in production, ExceptionHandler will be used instead. ExceptionHandler operates in the same way as DeveloperExceptionPage, but the information displayed when an exception has been thrown is less detailed and more user friendly.

  • Hsts:

Hsts is also only used in production. This will check that the browser sending the request is operating over a secure connection.

  • HttpsRedirection:

This will redirect any requests using plain HTTP to an HTTPS URL.

  • StaticFiles:

The StaticFiles middleware will attempt to serve a request by responding to a file that’s in the wwwroot folder of the application.

  • Routing and Endpoints:

When UseRouting and UseEndpoints are used in the middleware pipeline, this means that ASP.NET Core is performing EndPoint routing. This is where routing is implemented splitting the routing middleware into two separate middleware components: Routing and Endpoints. An endpoint is the object that contains the information needed to execute a request. So here, the Routing middleware matches the HTTP request to an endpoint first and then the Endpoints middleware executes the matched endpoint.

  • Authorisation:

In between the Routing and Endpoints middleware, Authorisation will check whether the browser is authorised to access the information it is requesting.

If any of these pieces of middleware can’t fulfil their purpose, they will throw an exception that will be handled by either DeveloperExceptionPage or ExceptionHandler, depending on whether we’re running in development or production.

Building Custom Middleware

In some situations, you might want to build your own custom piece of middleware. The easiest way to do this is by invoking a method on IApplicationBuilder named Use.

Showing the pop-up information on Use in Visual Studio.

We see that Use() takes a function of RequestDelegate and RequestDelegate. So we pass into Use a parameter of type RequestDelegate, and a RequestDelegate will be returned.

A RequestDelegate is a method that takes an HttpContext as a parameter and returns a Task.

Showing the RequestDelegate method in Visual Studio.

So, as you would expect knowing the definition of middleware, a RequestDelegate is a method that will handle an HTTP request. All the information about that HTTP request will be inside of the HttpContext passed as a parameter inside of RequestDelegate. Based on this HttpContext, the middleware will be able to inspect the request and produce a response.

Let’s call our middleware component SayHello, which we pass inside of Use() .

App.Use(SayHello);

Now, create a method SayHello, taking a RequestDelegate as a parameter and returning a RequestDelegate.

private RequestDelegate SayHello(RequestDelegate arg)
{
    return async ctx =>
    {
        await ctx.Response.WriteAsync("Hello, World");
    };
}

This piece of middleware, when invoked, will display the message “Hello, World!”.

It is important to remember at this point that the order of the middleware inside of the Configure method matters. Let’s see this in practice.

Let’s first place SayHello at the beginning of the middleware pipeline:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.Use(SayHello);

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    
    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

What will happen when this web application is run? We said that, when in development, DeveloperExceptionPage is the first component in the middleware pipeline, which will always pass the HTTP request onto the next component in the pipeline. The next component is our SayHello. From its definition, we know this component will always return a response – the message “Hello, World!”. Once SayHello produces a response, it will be passed back to the previous component in the pipeline, DeveloperExceptionPage. This won’t have to do anything, since no exceptions have been thrown by any components. So, no matter what the HTTP request is, when our middleware pipeline is set up in this order, the response will always be the “Hello, World!” message.

Showing the resulting web app with three different URLs. All three display the message "Hello, World!".

What happens if we move SayHello to the very end of the pipeline, after the Endpoints component? When the HTTP request is valid, meaning that the Endpoints middleware does have something to return, then our web app will work exactly like before we added the new piece of middleware. This is because the HTTP request never reaches the SayHello. If, however, the HTTP request is an invalid URL, we will then hit the end of the pipeline, as none of the earlier components will be able to return anything.

Showing the web app with the default URL, displaying the welcome page.

Showing the web app with the URL /foo. The message "Hello, World!" is displayed.

This would of course not be done in a real web application, but it is useful to illustrate the concept. If we move SayHello back to the top of the pipeline, we can choose to only invoke it in certain situations, such as when the HTTP request matches a certain URL. Using an IF statement inside of our method, we can activate SayHello only when the URL in the request is “/hello”. Otherwise, we can use the next parameter to pass the request to the next middleware component in the pipeline.

private RequestDelegate SayHello(RequestDelegate next)
{
    return async ctx =>
    {
        if (ctx.Request.Path.StartsWithSegments("/hello"))
        {
            await ctx.Response.WriteAsync("Hello, World!");
        }
        else
        {
            await next(ctx);
        }
    };
}

Now, requesting a valid URL will let us access the different pages in our web app. If we pass an invalid URL, an error is thrown. Finally, if we request the URL “/hello”, the message “Hello, World” is returned.

Showing the web app with the default URL, displaying the welcome page.

Showing the web app with the URL /foo. An error page is displayed.

Showing the web app with the URL /hello. The message "Hello, World!" is displayed.

Conclusion

We have first seen how a middleware pipeline works - the HTTP request invokes each middleware component one by one until one produces a response, which makes its way out going through the same components in the opposite direction. We noted that each piece of middleware has a unique purpose, and the order in which they are called affects the performance of the application. Finally, we gave an example of building a custom middleware component, and illustrated the different behaviours the application could have, simply by changing the order of the components.

Elisenda Gascon

Apprentice Engineer II

Elisenda Gascon

Elisenda was an Apprentice Engineer from 2021 to 2023 at endjin after graduating with a mathematics degree from UCL. Her passion for problem solving drove her to pursue a career in software engineering.

During her time at endjin, she helped clients by delivering data analytics solutions, using tools such as Azure Synapse, Databricks notebooks, and Power BI. Through this, she also gained experience as a consultant by delivering presentations, running customer workshops, and managing stakeholders.

Through her contributions to internal projects, she gaines experience in web development in ASP.NET, contributed to the documentation of the Z3.Linq library, and formed part of a team to develop a report to explore global trends in wealth & health.

During her training as a software engineer, Elisenda wrote a number of blog posts on a wide range of topics, such as DAX, debugging NuGet packages, and dependency injection. She has also become a Microsoft certified Power BI analyst and obtained the Green Software for Practitioners certification from the Linux Foundation.