In this episode we take a look at ASP.NET Core's middlewares, how to create them and use them in the request handling pipeline.
For the walk-through you can check the next video, but if you prefer a quick read, skip to the written synthesis.
The playlist for the whole series is here.
Intro
We had a brief introduction to middlewares in the fourth episode, when we talked about the Program
and Startup
classes. In this post we’ll be revisiting the middleware topic, and going through the options we have to build middlewares.
As we’ve seen, the basic idea of middlewares is that they work like in a chain - a middleware is invoked, it can do something, then pass the request along to the next middleware and finally do something else when the next middleware is done. There can be cases where the next middleware isn't invoked, for instance in a authorization middleware, if the user isn't authenticated, it can immediately stop the request, returning a response by itself.
The following is a good image to illustrate the behavior, from the official docs.
Middlewares can range from simple things just to enrich the request handling pipeline - adding some headers, filter unauthorized access, collect some metrics, etc - to implementing a complete request handling logic - an endpoint for health checks, serving static files, MVC 🙂, etc.
To create middlewares, we have a couple of options to get started:
- Implement the logic in an inline anonymous function (like the example we saw in episode 4)
- Implement the logic in a class dedicated to it
In this post, we'll take a look at some simple examples of implementing middlewares with both of these options.
app.Use
Given we already saw a simple middleware implemented and registered in the pipeline with app.Use
, won't be going into much more detail than adding the code here for reference 🙂
Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//...
app.Use(async (context, next) =>
{
context.Response.OnStarting(() =>
{
context.Response.Headers.Add("X-Powered-By", "ASP.NET Core: From 0 to overkill");
return Task.CompletedTask;
});
await next.Invoke();
});
//...
}
As we can see, this registers an anonymous function that receives the HttpContext
and a Func
to invoke the next middleware in the chain. Then the implementation, well, we do whatever we want, in this case it's just adding a response header, while letting the rest of the pipeline work as normal, by invoking the next middleware.
Note that the order in which we register the middleware (during the Configure
method), defines the order in which they'll be run when handling a request, so order is not irrelevant.
app.UseMiddleware
With app.Use
, we register a lambda based middleware. To register a class based middleware we can use app.UseMiddleware<T>
extension method.
When creating class based middlewares, we have a couple of options:
- Convention-based activated - where we follow a convention to implement the class, and all will work kinda automagically
- Factory-based activated - the middleware class implements
IMiddleware
, so we get a more "normal" experience, like when we implement a controller inheriting from theController
class (which, by the way, is also not mandatory, we can also implement controllers by following some conventions)
Convention-based activated middleware
For a convention-based activated middleware, we should follow a couple of rules and we're good to go. A basic implementation would be something like the following.
public class SampleConventionActivatedMiddleware
{
private readonly RequestDelegate _next;
public SampleConventionActivatedMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// do stuff before the next middleware executes
await _next(context);
// do stuff after the next middleware executes
}
}
The constructor receives a delegate to invoke the next middleware in the chain, and we're storing it to use it when handling the requests.
Then we have an InvokeAsync
method that gets the HttpContext
as an argument. This might already tells us something, that's probably not completely clear at first look - the instance of the middleware used in the pipeline is basically a singleton, as it's constructed at application startup.
Let's take a look at a middleware with a little more going on.
public class RequestTimingAdHocMiddleware
{
private readonly RequestDelegate _next;
private int _requestCounter;
public RequestTimingAdHocMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, ILogger<RequestTimingAdHocMiddleware> logger)
{
var watch = Stopwatch.StartNew();
await _next(context);
watch.Stop();
Interlocked.Increment(ref _requestCounter);
logger.LogWarning("Request {requestNumber} took {requestTime}ms", _requestCounter, watch.ElapsedMilliseconds);
}
}
As we saw in the previous example, the middleware instance is a singleton, so if we have some dependencies, instead of getting them in the constructor as usual, we declare what we need in the InvokeAsync
method, so they can be resolved per request.
With all this in mind, what happens in this case is the _requestCounter
field will have a count of all the requests that went through the middleware during the application lifetime, because it's a singleton. Additionally, we get a logger as dependency injected on InvokeAsync
.
Sample log output:
11:38:09 CodingMilitia.PlayBall.GroupManagement.Web.Demo.Middlewares.RequestTimingAdHocMiddleware Warn Request 1 took 1ms
11:38:15 CodingMilitia.PlayBall.GroupManagement.Web.Demo.Middlewares.RequestTimingAdHocMiddleware Warn Request 2 took 0ms
11:38:19 CodingMilitia.PlayBall.GroupManagement.Web.Demo.Middlewares.RequestTimingAdHocMiddleware Warn Request 3 took 0ms
For all of this to work, of course we need to register the middleware in the request handling pipeline:
Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//...
app.UseMiddleware<RequestTimingAdHocMiddleware>();
//...
}
Factory-based activated middleware
Another option for class based middlewares is to implement the IMiddleware
interface. Continuing with the example we used for the convention-based approach, we get:
public class RequestTimingFactoryMiddleware : IMiddleware
{
private readonly ILogger<RequestTimingFactoryMiddleware> _logger;
private int _requestCounter;
public RequestTimingFactoryMiddleware(ILogger<RequestTimingFactoryMiddleware> logger)
{
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var watch = Stopwatch.StartNew();
await next(context);
watch.Stop();
Interlocked.Increment(ref _requestCounter);
_logger.LogWarning("Request {requestNumber} took {requestTime}ms", _requestCounter, watch.ElapsedMilliseconds);
}
}
Right off the bat, we can see a more traditional class layout, with a dependency injected in the constructor and the InvokeAsync
method to implement the interface.
In this type of middleware, the lifetime is up to us, as we need to register it in DI, and not only in the request handling pipeline like we did for the convention-based middleware.
Startup.cs
public class Startup
{
//...
public void ConfigureServices(IServiceCollection services)
{
//...
services.AddTransient<RequestTimingFactoryMiddleware>();
//...
}
//...
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//...
app.UseMiddleware<RequestTimingFactoryMiddleware>();
//...
}
}
In this case, because the class is being registered as transient, the request counter will not persist across requests, so we get the following log output:
11:52:06 CodingMilitia.PlayBall.GroupManagement.Web.Demo.Middlewares.RequestTimingFactoryMiddleware Warn Request 1 took 0ms
11:52:07 CodingMilitia.PlayBall.GroupManagement.Web.Demo.Middlewares.RequestTimingFactoryMiddleware Warn Request 1 took 0ms
11:52:08 CodingMilitia.PlayBall.GroupManagement.Web.Demo.Middlewares.RequestTimingFactoryMiddleware Warn Request 1 took 0ms
app.Run
Back to using anonymous functions as middlewares, besides the app.Use
extension method, we have app.Run
. It is basically the same, but to register a function as a terminal middleware, which means it won't flow the request through to any middleware that comes after it in the chain. It's basically the same as app.Use
without ever calling the next
argument 😛
Just for examples sake, in the request handling pipeline configuration, we can add something at the end:
Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//...
app.Run(async (context) =>
{
await context.Response.WriteAsync("No middlewares could handle the request");
});
}
In this case, if no middleware is capable of completely handle the request, particularly the MVC middleware, we send out a (pretty barebones) response informing of this fact.
app.Map and app.MapWhen
Another interesting couple of extension methods we have available are Map
and MapWhen
. These methods allow us to create branches in the request handling pipeline given some conditions.
Let's take a look at some examples.
app.Map
Map
allows us to branch the pipeline for a given path.
Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//...
app.Map("/ping", builder =>
{
builder.UseMiddleware<RequestTimingFactoryMiddleware>();
builder.Run(async (context) => { await context.Response.WriteAsync("pong from path"); });
});
//...
}
Taking the example above, for any request to /ping
, instead of following the usual pipeline, the middlewares configured in the lambda argument will be used, do we get the previously developed RequestTimingFactoryMiddleware
executing, plus a terminal middleware responding with a simple message.
app.MapWhen
MapWhen
works in a very similar fashion to Map
, but instead of using forcibly a path, we can define other conditions. To define the condition, we provide a function that gets an HttpContext
argument and returns a boolean, indicating if the branch should be used.
Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//...
app.MapWhen(
context => context.Request.Headers.ContainsKey("ping"),
builder =>
{
builder.UseMiddleware<RequestTimingAdHocMiddleware>();
builder.Run(async (context) => { await context.Response.WriteAsync("pong from header"); });
});
//...
}
This example is pretty similar to the previous one, but using as condition a check to see if there's a request header named ping
. If the header exists, than the branch is used and, in this case, the previously implemented RequestTimingAdHocMiddleware
is used, plus another terminal middleware to output a simple response message.
Outro
That does it for a quick look at implementing middlewares. It's simple enough, and I think there isn't much more to it. To build more complex middlewares, we just need to add more logic upon these initial steps we followed to set them up.
Like I said in other posts in this series, the official docs will most likely have much more information when compared to these rather quick walk-throughs I'm putting out 😉.
The source code for this post is here.
Please send any feedback so I can improve and adjust the next episodes.
Thanks for stopping by, cyaz!
Top comments (0)