Middleware in ASP.NET Core: How to Extend and Optimize Your API

Middleware in ASP.NET Core powers tasks like logging and authentication, optimizing API functionality. Learn its basics and three ways to create it in .NET for scalable, maintainable APIs.

3 days ago   •   8 min read

By Pavle Davitkovic
Table of contents

APIs have become the backbone of many applications. Why?

Because it provides a standardized way to exchange data and functionality between disparate systems.

As your API grows, it becomes increasingly important to ensure that your codebase remains organized, maintainable, and extensible.

One of the powerful tools available in NET for achieving this goal is middleware. 

We'll explore the role of middleware in and how you can use it to extend the functionality of your API. 

Firstly, I will grasp the basic concept of middleware. Then, we'll dive into how to create middlewares in .NET in 3 ways to handle common API-related tasks such as, for example, API key validation.

So, let's get started!

What is middleware in ASP.NET Core?

Middleware in ASP.NET Core is a powerful concept that allows you to create modular, reusable components that can augment the request/response pipeline of your web application. Is responsible for processing the request, potentially modifying it, and then passing it along to the next piece of middleware in the pipeline.

By leveraging middleware, you can add cross-cutting concerns, such as logging, authentication, and error handling, to your API without cluttering your actions or business logic.

Source: Microsoft documentation

The work of the middleware can be divided in 3 steps:

  • Process the incoming HTTP request: they can inspect, modify, or act upon the incoming HTTP request before passing it down the pipeline to the next middleware.
  • Call the next middleware: typically one middleware calls the next middleware in the pipeline to ensure the request continues through the chain.
  • Process the outgoing HTTP response: After the request completes processing by the subsequent middleware (or the endpoint), middleware can inspect, modify, or act upon the outgoing response as it bubbles back up through the pipeline.

And there is no limitation on how much middleware you can chain together, but there is a strict rule about the order they are chained.

The importance of middleware order

The order in which middleware is added to the pipeline plays a critical role in how requests and responses are processed. It’s essentially a sequential chain, where each middleware performs its task and decides whether to pass control to the next component. A slight misconfiguring the order can lead to unexpected behavior.

Source: Microsoft documentation

They are executed sequentially which means that they are executed in the same order they are registered. This determines how the incoming HTTP request flows through the pipeline.

Also, some middleware depends on others to run first. For example:

  • Authentication middleware should precede any middleware that requires a user's identity to function properly
  • Error-handling middleware should appear at the top to catch exceptions from all downstream components.

But they can also terminate the pipeline early by not calling the next middleware. Authorization middleware may block a request if the user lacks required permissions. This flow is called short-circuiting.

There is a two types of middlewares in .NET:

  • Built-in
  • Custom

Let’s explore them in detail.

Built-in middlewares

It refers to the predefined components that are provided by the framework to handle common concerns like authentication, routing, and more. These middleware are part of the NET request processing pipeline and are designed to handle specific tasks during the processing of HTTP requests and responses.

Some of built in middlewares at your disposal:

  1. Routing - responsible for routing incoming HTTP requests to the appropriate controller, action, or endpoint
  2. Authentication - handles the authentication of users based on the request's credentials
  3. Authorization - determining whether an authenticated user has the necessary permissions or roles to access a resources
  4. Static files - serves static files (such as images, JavaScript, CSS files) directly from the server
  5. Cross-origin resource sharing - allows or restricts which domains can access your API from the browser
  6. Logging handling - logs information about HTTP requests and responses
  7. Session - provides functionality to store and retrieve session data between requests

Custom Middleware: When and How to Use It

On the other hand, custom middleware refers to middleware components that developers create themselves to handle specific tasks that are not addressed by the built-in middleware.

They can be added to the request pipeline to implement application-specific logic, such as logging requests in a specific format, modifying request headers or handling particular business logic.

Two characteristic that are worth mentioning are:

  • Pipeline integration: They can be inserted at any point in the pipeline, depending on when you need it to execute relative to other middleware. And they should follow the order of middlewares.
  • Reusable logic: Once created, they can be reused across different projects

In .NET there are three ways of creating custom middlewares:

  • With request delegate
  • Convention based
  • Factory based

Let’s explore them in detail now.

Create middleware with request delegate

Simplest way is to use request delegates to create middleware. Request delegate is a function that processes HTTP requests. You can define middleware inline using request delegates with methods two methods:

  • Run 
  • Use

They work differently, but both need to be called on a WebApplication instance.

Let’s first see how Run works.

The method is used to terminate the middleware pipeline and execute a terminal middleware component.

When you call the Run method, it sets the RequestDelegate that will be executed when the middleware pipeline reaches that point. 

The RequestDelegate passed to is the last middleware component in the pipeline.

As you can see, the flexibility here is limited. And this is not a good approach if you want to have more than one middleware in the pipeline. That is where the Use method comes into place.

Use method registers middleware that can process incoming requests, call the next middleware, and act on outgoing responses. It has access to the HttpContext and the next delegate for controlling the flow of the request pipeline.

Awaiting the next delegate allows the request to continue through the pipeline. To short-circuit the pipeline, simply avoid invoking the next delegate.

This approach is straightforward and simple, but it can make code bloated as the application grows.

Which leads to the second approach.

Create convention based middleware

Convention-based middleware in .NET simplifies the process of defining and configuring middleware by leveraging established conventions instead of explicit configurations. 

The established convention is:

  • Middleware class - represents created middleware
  • Class constructor: it accepts a RequestDelegate parameter that allows the middleware to invoke the next component in the pipeline.
  • Invoke Method: Implement an Invoke or InvokeAsync method:
    • This method processes HTTP requests and responses.
    • The method should have a HttpContext parameter to access request and response data.
    • Call the next middleware in the pipeline using the RequestDelegate.
  • Integration: Use the middleware in the pipeline by adding it with the UseMiddleware<TMiddleware>() extension method or a custom extension method.

This is how it look in the code:

And registration into pipeline:

Or you can use simpler approach, in my opinion to achieve same result:

This approach is a common way to create middleware. But there is one more way which is the most flexible and my personal favourite.

Create factory based middleware

Third, and my personal favourite approach is a factory-based approach.

This involves creating middleware instances dynamically for each client request, enabling fine-grained control over middleware lifecycle and dependency injection. 

For scoped or transient dependencies(per-request data or services) this can be the first choice in my opinion.

Here are the two key interfaces involved:

  • IMiddleware: Represents middleware that is instantiated and executed for each HTTP request.
  • IMiddlewareFactory: Manages the creation of IMiddleware instances.

When middleware is registered in the DI container and implements the first interface, the framework runtime leverages the registered IMiddlewareFactory to create and resolve instances, rather than relying on the default convention-based middleware activation process.

The implementation is can be divided in three steps:

  • Class implementation:
  • Di registration:
  • Add middleware to the pipeline:

Pretty simple and clean, right?

I also want to demystify what is this my preferred approach:

  • Strong typing 
  • Flexible middleware registration
  • Per request middleware instances

And just to be clear, this is not the “right way” of creating them. Just my personal preference. At the end, you should use the approach that fits your use case best.

Use cases

I have shown you how to create middleware in all possible ways in .NET. But what are use cases for all three approaches? Let’s see:

  • Inline middleware: Great for experimenting or building small proof-of-concept features without creating separate middleware classes that are unlikely to be reused
  • Convention-based middleware: Ideal for lightweight middleware with minimal dependencies, such as logging, setting headers, or request redirection

A bit beyond middlewares

Without the right tools, debugging and optimizing middleware can be a challenge.

This is where Treblle steps in.

One of Treblle's features is observability. It’s a real-time monitoring that captures request and response data as it flows through middleware, providing developers with immediate visibility into the API's behavior.

Additionally, Treblle’s error tracking feature helps pinpoint specific problems that may occur in the middleware pipeline by offering detailed logs and stack traces, making it easier to debug complex issues. 

But it doesn't stop there—Treblle also provides valuable performance metrics that measure how long the middleware takes to process requests. This enables identifying potential bottlenecks, optimizing performance, and enhancing the overall efficiency of the application. 

And the best part? A few lines of code is all it takes to integrate into an application.

Conclusion

Middlewares represents a powerful and flexible mechanism for extending and customizing API functionality. Whether you are using the built-in middleware or creating custom middleware to address the pipeline plays a crucial role in shaping the request-response lifecycle. 

As APIs become increasingly complex, mastering middleware becomes crucial for building robust, scalable, and maintainable web applications that can adapt to evolving business requirements.

There are three ways to create and use middleware in .NET and the best option largely depends on your application's complexity and requirements.

But what is 100% true that middlewares bring is:

  • Performance optimization
  • Security 
  • Maintainability

Middleware is key to creating APIs that are secure, efficient, and easy to maintain. With the right approach, you can ensure your application is ready to meet evolving challenges and scale effortlessly.

💡
Want to take your middleware and API performance to the next level? Try Treblle for real-time insights, error tracking, and optimization—all in one platform.

Spread the word

Keep reading