ASP.NET Core's middleware architecture offers a powerful way to build and configure the HTTP request pipeline in your applications.
In this post, you'll explore what middleware is and how to create a custom middlewares in ASP.NET Core.
What is Middleware in ASP.NET Core?
Middleware in ASP.NET Core is a software component that is a part of application pipeline that handles requests and responses.
In ASP.NET Core there are multiple middlewares that are combined in a chain with each other.
Each middleware component in the pipeline is responsible for invoking the next component in the sequence.
Any middleware can stop other middlewares from execution by short-circuiting the chain if necessary.
Middlewares in ASP.NET Core is a classic implementation of chain of responsibility design pattern.
ASP.NET Core has a lot of built-in middlewares and many provided by Nuget packages.
The order in which middlewares are added to the application pipeline is critical.
It defines how the incoming HTTP requests travel through the pipeline and in what sequence the responses are sent back.
Middlewares are executed in the order they are added to the pipeline in the WebApplication object.
If you want to learn more about common middlewares and their correct order - read my blog post.
How To Create Custom Middleware in ASP.NET Core
You can create a custom middleware in the following ways:
- provide a delegate for
Use
method inWebApplication
class - create a Middleware class by convention
- create a Middleware class by inheriting from
IMiddleware
interface
With a Use Method in WebApplication Class
You can call a Use
method on the WebApplication
class to create a middleware:
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddConsole();
var app = builder.Build();
app.Use(async (context, next) =>
{
Console.WriteLine("Request is starting...");
await next();
Console.WriteLine("Request has finished");
});
app.MapGet("/api/books", () =>
{
var books = SeedService.GetBooks(10);
return Results.Ok(books);
});
await app.RunAsync();
In this example when calling a /api/books
endpoint, the middleware declared in the Use
method is called first.
await next.Invoke()
calls the books endpoint itself, but before and after we have a message logged to console:
Request is starting...
Request has finished
Middlewares are executed in the order they are added to the pipeline in the WebApplication object.
Each middleware can perform operations before and after the next middleware:
Before: executing operations before calling the next middleware can include tasks like logging, authentication, validation, etc.
After: operations after calling the next middleware can include tasks like response modification or error handling.
The real power of middlewares is that you can chain them freely in any order you want.
To stop the request from executing and short-cut the middleware chain (stop other middlewares from executing) - write a response directly into HttpContext instead of calling the await next.Invoke()
method:
await context.Response.WriteAsync("Some response here");
With a Middleware Class by Convention
You can extract a middleware to a separate class that follows the specific convention:
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
public LoggingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
Console.WriteLine($"Request: {context.Request.Method} {context.Request.Path}");
await _next(context);
Console.WriteLine($"Response: {context.Response.StatusCode}");
}
}
To add this middleware to the pipeline, call the UseMiddleware
method on the WebApplication
class:
app.Use(async (context, next) =>
{
// Middleware from previous example
});
app.UseMiddleware<LoggingMiddleware>();
As a result of executing this middleware the following will be logged to the console when executing an /api/books
endpoint:
Request is starting...
Request: GET /api/books
Response: 200
Request has finished
This approach is called by convention, because middleware class must follow these rules:
- middleware class should have a
InvokeAsync
method with a requiredHttpContext
argument - middleware class should inject a next
RequestDelegate
in the constructor - middleware class call the next
RequestDelegate
delegate and pass it theHttpContext
argument
With a Middleware Class That Implements IMiddleware
Interface
The previous approach has its drawbacks: the developer needs to create a middleware class that follows all mentioned above rules, otherwise a middleware won't work.
But there is a safer way to create a middleware: implement the IMiddleware
interface:
public class ExecutionTimeMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var watch = Stopwatch.StartNew();
await next(context);
watch.Stop();
Console.WriteLine($"Request executed in {watch.ElapsedMilliseconds}ms");
}
}
This approach is much safer as the compiler tells how the middleware class should look like.
For this approach you need to manually register ExecutionTimeMiddleware
in the DI container:
builder.Services.AddScoped<ExecutionTimeMiddleware>();
To add this middleware to the pipeline, call the UseMiddleware
method on the WebApplication
class:
app.Use(async (context, next) =>
{
// Middleware from previous example
});
app.UseMiddleware<LoggingMiddleware>();
app.UseMiddleware<ExecutionTimeMiddleware>();
As a result of executing this middleware the following will be logged to the console when executing an /api/books
endpoint:
Request is starting...
Request: GET /api/books
Request executed in 68ms
Response: 200
Request has finished
Middlewares and Dependency Injection
Middlewares by convention have Singleton
lifetime by default and all dependencies injected in constructor must be singletons too.
As we already know, middlewares run per each request, and you can inject scoped dependencies in the InvokeAsync
method after HttpContext
.
Here we are injecting a ILoggingService
that is registered as scoped service in DI:
builder.Services.AddScoped<ILoggingService, ConsoleLoggingService>();
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
public LoggingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, ILoggingService loggingService)
{
loggingService.LogRequest(context.Request.Method, context.Request.Path);
await _next(context);
loggingService.LogResponse(context.Response.StatusCode);
}
}
This approach is suitable only for middleware classes created by convention.
To inject scoped services into middleware classes that implement IMiddleware
interface, simply use the constructor:
public class ExecutionTimeMiddleware : IMiddleware
{
private readonly ILoggingService _loggingService;
public ExecutionTimeMiddleware(ILoggingService loggingService)
{
_loggingService = loggingService;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// ...
}
}
NOTE: when creating a middleware class that implements IMiddleware
interface - you are responsible for selecting an appropriate DI lifetime for it.
You can create Singleton
, Scoped
or Transient
middleware, select what suits the best in each use case.
Summary
You can create a custom middleware in the following ways:
- provide a delegate for
Use
method inWebApplication
class - create a Middleware class by convention
- create a Middleware class by inheriting from
IMiddleware
interface
My preferred choice is to create a middleware by inheriting from the IMiddleware
interface.
This approach offers a safer, more convenient way to create middlewares and a straightforward dependency injection strategy through a constructor.
And it also gives a full control over the middleware lifetime.
Hope you find this blog post useful. Happy coding!
Originally published at https://antondevtips.com.
After reading the post consider the following:
- Subscribe to receive newsletters with the latest blog posts
- Download the source code for this post from my github (available for my sponsors on BuyMeACoffee and Patreon)
If you like my content — consider supporting me
Unlock exclusive access to the source code from the blog posts by joining my Patreon and Buy Me A Coffee communities!
Top comments (0)