We’re going to talk about what middleware is, what it does, why we use it, and demo several implementations of various kinds of middleware. We’ll also talk about the pipeline that middleware exists in, how to create it, and why the order of operations in that pipeline matters. Finally, we’ll even show two ways to conditionally execute middleware in the pipeline to give you a finer-grain control of what your app does.
Middleware Basics
At its most fundamental, any given interaction using HTTP is comprised of a request (usually from a browser or API) and a response. Browsers, APIs, or other requestors submit a request and wait for the target (a web server, another API, something else) to return a response.
Middleware sits between the requestor and the target, and can directly modify the response, log things, or generally modify the behavior of the code that generates the response, optionally using the data within the request to do so.
ASP.NET 6 implements a pipeline consisting of a series of middleware classes. A request filters down the pipeline until it reaches a point where a middleware class (or something invoked by that middleware) creates a response. The response is then filtered back up through the middleware in reverse order until it reaches the requestor.
Each middleware component consists of a request delegate, a specific kind of object in .NET that can pass execution control onto the next object. Each request delegate chooses whether or not to pass the request on to the next delegate in the pipeline. Depending on the results of its calculations, a middleware may choose not to give execution control to the next item.
What Is Middleware For?
Before we continue, we should probably talk about why we might want to use middleware in an ASP.NET 6 application. To do that, let’s talk about common scenarios.
One common scenario that middleware serves well (and one that we’ll dive into in a later post) is logging. middleware easily enables logging of requests, including URLs and paths, to a logging system for reporting on later.
Middleware is also a great place to do authorization and authentication, diagnostics, and error logging and handling.
In short, middleware is used for operations that are not domain-specific logic and need to happen on every request, or a majority of requests.
Let’s take a look at a default Program.cs file generated by Visual Studio when you create a new .NET 6 web application:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/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.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
This file creates the pipeline that the ASP.NET 6 web application uses to process requests. It also adds a set of “default” middleware to the pipeline using special methods provided by .NET 6, such as UseStaticFiles()
which allows the app to return static files such as .js and .css, and UseRouting()
which adds .NET Routing to handle the routing of URLs to server endpoints. By default, if you are using an ASP.NET 6 app, you are already using middleware.
Further, ASP.NET 6 apps can use quite a bit of this “built-in” middleware provided by .NET 6. A full list can be found on the Microsoft docs site.
Let’s create a super-simple middleware that does exactly one thing: it returns “Hello world” as its response.
In Program.cs, we add a new middleware using the Run()
method, like so:
When we run the app, we will see this very simple output:
We’ve implemented our own custom middleware in ASP.NET 6! However, there’s a problem, as we will see shortly.
Run(), Use(), and Map()
When reading the Program.cs file, you can generally identify which parts of the application are considered middleware by looking at the method used to add them to the pipeline. Most commonly this will be done by the methods Run()
, Use()
, and Map()
.
Run()
The Run()
method invokes a middleware at that point in the pipeline. However, that middleware will always be terminal, e.g. the last middleware executed before the response is returned. Recall this block of code from the previous section:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Code after this block will not be executed
app.Run(async context =>
{
await context.Response.WriteAsync("Hello world");
});
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/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.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
Because of the invocation of Run()
, nothing written after that invocation will be executed.
Use()
The Use()
method places a middleware in the pipeline and allows that middleware to pass control to the next item in the pipeline.
app.Use(async (context, next) =>
{
//Do work that does not write to the Response
await next.Invoke();
//Do logging or other work that does not write to the Response.
});
Note the next
parameter. That parameter is the request delegate mentioned earlier. It represents the next middleware piece in the pipeline, no matter what that is. By awaiting on next.Invoke()
, we are allowing the request to proceed through to the next middleware.
Also, note that it is generally bad practice to modify the response in this kind of middleware unless the pipeline will stop processing here. Modifying a response that has already been generated could cause the response to become corrupted.
Map()
The Map()
method is a special case, one that we will talk about more in a later post. It allows us to “branch” the pipeline; we can use it to conditionally invoke middleware based upon the request path.
app.Map("/newpath", HandleBranchOne);
//Rest of Program.cs file
app.Run();
static void HandleNewPath(IApplicationBuilder app)
{
app.Run(async context =>
{
await context.Response.WriteAsync("Code for new path executed");
});
}
Futher Reading:
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0
Pingback: Custom Middleware Classes in ASP.Net 6 - Pradeep Barli