Conditional middleware based on request in ASP.NET Core
This post looks at how to configure ASP.NET Core middleware in a way that allows you to have different middleware for different types of request. There are many use-cases for this but one common example for this requirement is a project with both MVC and API actions where you want to error handling to be different for each.
A middleware primer
Middleware is simply the pipeline for handling web requests in ASP.NET Core. All ASP.NET Core applications need a minimum of one piece of middleware to respond to requests and your applications are effectively just a collection of middleware. Even the MVC pipeline itself is middleware.
Each middleware component has an Invoke method with an HttpContext argument. You can use this argument to handle the request and generate a response if applicable:
public async Task Invoke(HttpContext context)
{
if (context.Request.Path...)
{
await context.Response.WriteAsync("writing text...");
}
}
Optionally, you can add other arguments to your Invoke method and these dependencies will be resolved on each request using the baked in dependency injection support (these dependencies would need to be registered in ConfigureServices):
public async Task Invoke(HttpContext context, MyOtherDependency blah)
{
if (blah.DoSomething(context))
{
...
}
}
Middleware Execution
We'll learn how to configure the middleware pipeline in the next section but it is important to be aware that the order of registration determines the order of execution.
The top-most piece of middleware in your application will always get called for each request. This is done automatically by the framework. This middleware may send a response to the client (terminating the pipeline), or it may call the next piece of middleware. For the latter option, it of course needs access to the next middleware component.
This is why most middleware components are defined with a constructor that takes a RequestDelegate argument.
public class MyMiddleware
{
private readonly RequestDelegate _next;
public MyMiddleware(RequestDelegate next, OtherDependencies...)
{
_next = next;
}
}
The RequestDelegate is populated automatically and gives you a reference to the next piece of middleware in the pipeline. We typically store the RequestDelegate so we can call it from the Invoke method which is where the middleware does its work.
The Ins and Outs of Invoke
If we look at the typical structure of a middleware's Invoke method, we see something interesting:
public async Task Invoke(HttpContext context)
{
// code executed before the next middleware
...
await _next.Invoke(context); // call next middleware
// code executed after the next middleware
...
}
Each piece of middleware has two chances to do some work - once before the next middleware is called (the in phase) and once after (the out phase).
It is also important to realise that a middleware component does not have to call the next middleware component in all cases. As an example, you may have some middleware checking for an API key and if the key is not present then you may wish to write a response directly and short-circuit the rest of the pipeline. After all, there is probably not a need to execute your secure controller actions if the user is unauthenticated.
When writing middleware, you typically either call the next middleware component or you write a response. Rarely or ever should you do both.
You can find out more about middleware in the ASP.Net Core docs
Registering middleware with Use*
Middleware is registered in the Configure method of Startup.cs. The Configure method has an IApplicationBuilder argument that (via several extension methods) provides the methods necessary for all types of middleware registration.
The standard way to register middleware that applies to all requests is with UseMiddleware:
public void Configure(IApplicationBuilder app, ...)
{
app.UseMiddleware<MyCustomMiddleware>();
}
In reality, you rarely need to call UseMiddleware directly because the standard approach for middleware authors is to write extension methods specific to the middleware being registered:
public void Configure(IApplicationBuilder app, ...)
{
app.UseMyCustomMiddleware();
}
Behind the scenes, the extension method normally just registers the middleware with UseMiddleware
public static class MyCustomMiddlewareExtensions
{
public static IApplicationBuilder UseMyCustomMiddleware(this IApplicationBuilder app)
{
app.UseMiddleware<MyCustomMiddleware>();
return app;
}
}
In our example, using the custom extension method is only slightly more readable and easier to use than the original UseMiddleware approach but this is only because our middleware is so simple. Most real-world middleware allows some configuration and custom extensions methods can provide a much nicer programming model than the params object[] args approach used by UseMiddleware.
Use* extensions methods are the standard way to register middleware applicable to all requests. In the next section, we'll look at situations where you want to have a different middleware pipeline for certain requests.
Middleware branching with MapWhen
MapWhen allows you to split the middleware pipeline into two completely separate branches by specifying a predicate:
app.UseMiddlewareOne();
app.MapWhen(context => context.Request.Path.StartsWithSegments("/api"), appBuilder =>
{
appBuilder.UseMiddlewareTwo();
});
app.UseMiddlewareThree();
In this example, middleware One will always execute. Assuming no short-circuiting (see above), if the request path start with '/api' then middleware Two will execute. Otherwise, middleware Three will execute. With this configuration, there is no way for both middleware Two and middleware Three to execute for a single request.
I tend not to use MapWhen very often but as a usage example, we can restrict the StaticFiles middleware from running for all requests and instead specify a certain path:
app.MapWhen(context => context.Request.Path.Value.StartsWithSegments("/assets"),
appBuilder => appBuilder.UseStaticFiles());
This would result in the static files middleware only running for request paths starting with '/assets' (where we store our static files). Any request with a path not matching this criteria would not run the StaticFiles middleware and the next registered middleware would run directly instead (perhaps saving valuable nano-seconds!).
Conditional middleware with UseWhen
The final case I want to look at is when you want most of your middleware to run for all requests but you have some conditional pieces - specific middleware that should only run for certain requests.
This is easily achieved with UseWhen which also uses a predicate to determine if the middleware should run:
app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), appBuilder =>
{
appBuilder.UseStatusCodePagesWithReExecute("/apierror/{0}");
appBuilder.UseExceptionHandler("/apierror/500");
});
This code uses different error handling middleware when the request is for the API (determined using the URL). The predicate approach provides a lot of flexibility and you can conditionally apply middleware based on cookies, headers, current user and much more.
As with MapWhen, all middleware registered before the UseWhen call will apply to all requests.
The primary difference between UseWhen and MapWhen is how later (i.e. registered below) middleware is executed. Unlike MapWhen, UseWhen continues to execute later middleware regardless of whether the UseWhen predicate was true or false.
Let's change our example to use UseWhen:
app.UseMiddlewareOne();
app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), appBuilder =>
{
appBuilder.UseMiddlewareTwo();
});
app.UseMiddlewareThree();
This time, assuming no short-circuiting, middleware One and Three will be executed in all cases. Middleware Two will only be executed if the request path starts with '/api'.
UseWhen is very powerful and incredibly useful. Rather than the middleware itself deciding on if it should execute (perhaps via configuration), we can control middleware ourselves by choosing how we register it.
Here are a few examples:
- Restrict output caching to anonymous users.
- Add diagnostic headers for a certain IP subnet
- Handle errors differently for API and MVC actions
- Restrict certain requests from analytics
Conclusion
This post looked at the different options for configuring ASP.NET Core middleware in startup.cs. We showed the standard way to register middleware which results in the middleware executing for every request. We then described two other approaches which allows you to customise which pieces of middleware run for each request. Much of the time, you will use the standard approach of registering the middleware for all requests but it is very useful to know that alternatives exist for special situations.
Hi , Its a nice article.
Is it ideal to attach MetaData info to the response in the middleware?
Like what you explained in following article: https://www.devtrends.co.uk/blog/wrapping-asp.net-web-api-responses-for-consistency-and-to-provide-additional-information
Thanks
Ravi
Hi Paul,
thanks for a great article! This helped me a lot.
Thanks,
Branislav
If you add in the following dependencies with non-singleton lifetime such as datacontext or repositories, you are running on errors.
public MyMiddleware(RequestDelegate next, OtherDependencies...)
{
_next = next;
}
Instead add those dependencies in the Invoke
public async Task Invoke(HttpContext context, /*put non-singleton deps here*/)
{
// code executed before the next middleware
...
await _next.Invoke(context); // call next middleware
// code executed after the next middleware
...
}
Hi
how would you differentiate between mvc and web api calls? I tried the following, but all web api call end up in the mvc error handler.
app.UseWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")), appBuilder =>
{
appBuilder.UseStatusCodePagesWithReExecute("/apierror/{0}");
appBuilder.UseExceptionHandler("/apierror/500");
});
app.UseWhen(context => !context.Request.Path.StartsWithSegments(new PathString("/api")), appBuilder =>
{
app.UseStatusCodePagesWithReExecute("/error/{0}");
app.UseExceptionHandler("/error/500");
});
Any thoughts?
Thanks,
Serge
@Serge - in the second UseWhen, you are using app.UseStatusCodePagesWithReExecute and app.UseExceptionHandler instead of appBuilder.* so both pieces of middleware get applied in all cases. I have done this myself on more than one occasion!
Hi Paul
thanks so much. As soon as I had posted it here I saw it myself but my comment wasn't visible yet or else I would have corrected myself. That took me forever to figure out!
Thanks,
Serge
Thanks! This article answered a question I have been searching for the whole day.
Hi Paul,
Thanks for the great article. I was going through many examples and used to wonder about middle ware and how it works. This gave me a good understanding.
Thanks a lot!
Thanks, this was helpful in setting up my middleware to only run for certain requests. However, I'm struggling still with it because I want to apply authorization to certain requests but not to others. In the old world I'd just create custom attributes and apply them to certain requests and not others. It's very kludgy to set up rules for when middleware should run off of what the url... How would you handle this in a clean way?
Thanks!
Thank you! Please don't mind if I ask more questions.
Excellent! Thank you.
My little change (to use pre defined constants)
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes?view=aspnetcore-5.0
and optionally, I encapsulated a little bit: (I had a few paths to check)
app.UseWhen(context => this.CheckMoreAdvancedRules(context), appBuilder =>
{
appBuilder.UseMiddleware<ApiKeyMiddleware>();
});
private bool CheckMoreAdvancedRules(Microsoft.AspNetCore.Http.HttpContext ctx)
{
bool returnValue = false;
if (null != ctx && ctx.Request.Path.StartsWithSegments("/api"))
{
returnValue = true;
}
if (!returnValue)
{
/* do whatever you want here */
/* if the moon is blue at midnight (then) returnValue = true; */
}
return returnValue;
}
Also note that I used "UseWhen" instead of "MapWhen".
I was getting a:
"The request reached the end of the pipeline without executing the endpoint"
I am using the ASP.NET Core 6.new minimal hosting model and can't get this to work.
I tried to use UseWhen almost like to presented it. In my case, I only want an error page for page requests, not for api calls.
When I start the application with this code:
app.UseWhen(context => !context.Request.Path.StartsWithSegments(new PathString("/api")),
appBuilder =>
{
appBuilder.UseStatusCodePagesWithReExecute("/Home/Error/{0}");
});
the inner block with appBuilder.UseStatusCodePagesWithReExecute is called once when the application starts, but not the condition. The condition is evaluated at each request, but the inner block is never executed again, even if I am sure the condition evaluates to true. I even replaced the condition with true, to check.
I also noticed that the appBuilder parameter is an IApplicationBuilder, whereas app is a WebApplication, which leaves me wondering what a builder
If I just call appBuilder.UseStatusCodePagesWithReExecute("/Home/Error/{0}"); outside the UseWhen block, my error pages render just fine.
Is this an issue with UseWhen and the minimal hosting model?
@R.S. It is working fine for me in .NET 6 with minimal APIs and razor pages. It is normal for the conditional to be evaluated for every request but for the inner block to only be executed once.
The following uses the error page for web pages but not for the API:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
var app = builder.Build();
app.UseStaticFiles();
app.UseWhen(x => !x.Request.Path.StartsWithSegments("/api"),
appBuilder => appBuilder.UseStatusCodePagesWithReExecute("/Error"));
app.UseRouting();
app.MapGet("/api/foo", () => new { hello = "world" });
app.MapRazorPages();
app.Run();