How To Create Custom Middlewares in ASP.NET Core

antonmartyniuk

Anton Martyniuk

Posted on April 24, 2024

How To Create Custom Middlewares in ASP.NET Core

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 in WebApplication 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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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}");
    }
}
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This approach is called by convention, because middleware class must follow these rules:

  • middleware class should have a InvokeAsync method with a required HttpContext argument
  • middleware class should inject a next RequestDelegate in the constructor
  • middleware class call the next RequestDelegate delegate and pass it the HttpContext 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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode
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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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 in WebApplication 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!

💖 💪 🙅 🚩
antonmartyniuk
Anton Martyniuk

Posted on April 24, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related