How to Create Middleware in ASP.NET Core – Request Delegate, Convention-Based, and Factory-Based Approaches

Middleware is the backbone of any ASP.NET Core application.

Every request that hits your API passes through a series of middleware components. Each one gets a chance to look at the request, do something with it, and either pass it on or stop it in its tracks.

If you’ve ever used things like authentication, logging, CORS, or exception handling — yep, you’ve been using middleware all along.

But what if you want to write your own?

In this article, we’ll walk through three ways to create middleware in ASP.NET Core:

  • Using a Request Delegate
  • Convention-Based Middleware
  • Factory-Based Middleware

Each example will help you understand how the middleware pipeline works and how to apply these approaches in your own application.

What is Middleware?

Imagine an HTTP request like a traveler going through airport security. It passes through multiple checkpoints: ID check, baggage scan, customs. Each station checks something and decides whether to let it through, flag it, or stop it.

In ASP.NET Core, middleware works the same way.

Each middleware component sits in the request pipeline. It can:

  • Inspect or modify the incoming request
  • Call the next middleware in the pipeline
  • Inspect or modify the outgoing response
  • Or even terminate the pipeline early

It’s a powerful pattern because it lets you separate concerns like logging, authentication, exception handling, and custom behaviors, without cluttering your business logic.

And here’s something important to remember, Middleware runs in the order you register it. So yes, order matters — a lot.

Using a Request Delegate

When we say using a request delegate in ASP.NET Core middleware, we’re talking about creating middleware inline—without defining a separate class.

Think of it like writing a quick bit of logic directly inside your Program.cs using the app.Use(…) method.

A request delegate is just a function that:

  • Takes an HttpContext
  • Has access to the next middleware in the pipeline
  • Can perform some action before and after the next middleware runs

Here’s how that looks in code:

C#
app.Use(async (context, next) =>
{
    Console.WriteLine("Before the request");

    await next(context); // This moves to the next middleware

    Console.WriteLine("After the request");
});

In a middleware delegate, the context object provides access to everything about the current HTTP request — like the path, headers, and method. The next() function is what moves the request forward through the rest of the middleware pipeline. Any code written before await next() runs before the next middleware executes, while anything written after it runs after the response is generated. This allows you to wrap logic around the entire request/response lifecycle, making it useful for tasks like logging, measuring execution time, or modifying responses.

This allows you to do things like:

  • Measure request time
  • Log request and response paths
  • Add custom headers
  • Block specific paths
When Should You Use It?

Use a request delegate when:

  • You just need a quick bit of logic
  • You don’t plan to reuse the middleware elsewhere
  • The logic is short and self-contained

It’s perfect for quick experiments, debugging, or adding one-off features to the pipeline.

When to Avoid

Avoid this approach if:

  • The logic starts growing
  • You need to inject services (like a database or logger)
  • You want to reuse the middleware in other projects

For those cases, go with convention-based or factory-based middleware.

Convention-Based Middleware

The convention-based approach is the most common and structured way to build middleware in ASP.NET Core. Instead of writing logic directly in Program.cs, you define a separate class that represents your middleware. This class follows a specific pattern: it must have a constructor that accepts a RequestDelegate, which represents the next middleware in the pipeline and an InvokeAsync method that takes an HttpContext. This setup allows the middleware to plug smoothly into the request pipeline.

Inside the InvokeAsync method, you write your custom logic—like adding headers, logging, or validating requests. After your logic runs, you call _next(context) to ensure the request continues to the next component in the pipeline.

This pattern is ideal when your middleware becomes more complex, needs to be reused, or you simply want cleaner separation between components. It also supports dependency injection for services that are safe to inject through the constructor, like singletons or stateless helpers.

Let’s break it down with a simple example:

C#
public class CustomHeaderMiddleware(RequestDelegate next, ILogger<CustomHeaderMiddleware> logger)
{
    public async Task InvokeAsync(HttpContext context)
    {
        logger.LogInformation("CustomHeaderMiddleware executing for request: {Path}", context.Request.Path);

        context.Response.Headers.Add("X-App-Version", "1.0.0");

        await next(context);

        logger.LogInformation("CustomHeaderMiddleware finished processing request: {Path}", context.Request.Path);
    }
}

To register this middleware in your pipeline, you’d add:

C#
app.UseMiddleware<CustomHeaderMiddleware>();

This approach is great when your logic becomes too big for inline middleware or needs to be reused. It also gives you better organization and testability. You can inject dependencies (like ILogger) into the constructor, as long as they’re singleton or safe to share across requests.

Use this when you want to keep your middleware logic clean, reusable, and separated from your startup file. It’s perfect for larger applications or middleware that might be shared between projects.

Avoid this if the middleware is very simple or just used in one place — in those cases, an inline delegate might be faster and easier.

Factory-Based Middleware

The factory-based approach is designed for scenarios where your middleware needs to depend on scoped or transient services, like DbContext, UserManager, or any per-request dependency. In the conventional class-based approach, injecting those into the constructor isn’t ideal because the middleware class itself is typically treated as a singleton, meaning it could end up holding onto shared service instances across requests, which can lead to bugs or thread-safety issues.

To solve that, ASP.NET Core offers support for factory-based middleware by letting your middleware class implement the IMiddleware interface. When you implement IMiddleware, ASP.NET Core creates a new instance of your middleware per request, using dependency injection. This allows you to safely inject scoped services through the constructor.

Let’s see what this looks like in action:

C#
public sealed class LoggingMiddleware(ILogger<LoggingMiddleware> logger) : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        logger.LogInformation("Request path: {Path}", context.Request.Path);

        await next(context);

        logger.LogInformation("Response sent.");
    }
}

Since we’re using IMiddleware, we need to register the middleware class as a service in the DI container:

C#
builder.Services.AddScoped<LoggingMiddleware>();

Then you register it in the pipeline like this:

C#
app.UseMiddleware<LoggingMiddleware>();

ASP.NET Core now creates a new instance of LoggingMiddleware for each request, resolving all constructor-injected services correctly, including scoped services.

Use this approach when your middleware needs scoped services, and you want to follow clean DI principles. It’s also a great choice when you want full separation of concerns, as the framework handles middleware instantiation for each request.

Avoid this if you don’t need scoped dependencies, the conventional class-based method will work fine and may be simpler.

Don’t Forget Middleware Order

ASP.NET Core executes middleware in the exact order you register them. So if you do something like this:

C#
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<CustomHeaderMiddleware>()

It means:

  1. First: Authenticate the user
  2. Then: Check their authorization
  3. Then: Add custom headers if needed

Getting the order wrong can result in things not working at all, so always be mindful of where you place each middleware.

Summary

In ASP.NET Core, you have three ways to create custom middleware:

ApproachDescriptionBest for
Request DelegateWrite logic inline using app.Use()Simple, one-off logic or quick checks
Convention-BasedCreate a class with InvokeAsync and inject dependencies via constructorClean, reusable, and testable logic
Factory-BasedImplement IMiddleware; safely inject scoped or transient services per requestMiddleware that depends on per-request services

Each method serves a purpose. The more complex or reusable your middleware needs to be, the more structure and flexibility you’ll want.

And don’t forget — the order in which you register middleware matters. A misplaced authentication or error handler can break your entire pipeline. Always be intentional with how you stack them.

Takeaway

Middleware is one of the most powerful parts of ASP.NET Core, and now you’ve seen how flexible it really is. Whether you’re adding a simple header, logging each request, or injecting scoped services safely, you have a clean path forward.

Start small with request delegates. As your app grows, move to structured class-based or factory-based middleware. Keep things readable, testable, and safe.