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

·

8 min read

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

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, ensuring that your codebase remains organized, maintainable, and extensible becomes increasingly important.

Middleware is one of the most powerful tools available in NET for achieving this goal.

We'll explore the role of middleware 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 creating middleware in .NET in 3 ways to handle common API-related tasks such as 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 your web application's request/response pipeline. It 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 into 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 is processed by the subsequent middleware (or the endpoint), the middleware can inspect, modify, or act upon the outgoing response as it bubbles back up through the pipeline.

There is no limit to how much middleware you can chain together, but there is a strict rule about the order in which they are chained.

The importance of middleware order

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

Source: Microsoft documentation

They are executed sequentially, meaning they are executed in the same order in which 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.

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

There is two types of middleware in .NET:

  • Built-in

  • Custom

Let’s explore them in detail.

Built-in middlewares

It refers to the predefined components provided by the framework to handle common concerns like authentication, routing, and more. These middleware components 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 resources

  4. Static files - serves static files (such as images, JavaScript, and 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 characteristics that are worth mentioning are:

  • Pipeline integration: They can be inserted at any point in the pipeline, depending on when you need them to execute relative to other middleware. They should follow the order of middleware.

  • Reusable logic: Once created, they can be reused across different projects

In .NET, there are three ways of creating custom middleware:

  • With request delegate

  • Convention based

  • Factory based

Let’s explore them in detail now.

Create middleware with request delegate

The 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 must be called on a web application instance.

Let’s first see how Run works.

The method terminates the middleware pipeline and executes 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. This is not a good approach if you want to have more than one middleware in the pipeline. That is where the method of use comes into play.

The method registers middleware to process incoming requests, calls the next middleware, and acts 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 can make code bloated as the application grows.

Which leads to the second approach.

Create convention-based middleware

Convention-based middleware in .NET simplifies 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 looks in the code:

Registration into pipeline:

Or you can use a simpler approach, in my opinion, to achieve the 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 favorite.

Create factory-based middleware

The third, and my personal favorite approach, is a factory-based approach.

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

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

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 can be divided into three steps:

  • Class implementation:

  • Di registration:

  • Add middleware to the pipeline:

Pretty simple and clean, right?

I also want to demystify what is 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—it's just my personal preference. In the end, you should use the approach that best fits your use case.

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 middleware

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 application's overall efficiency.

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

Conclusion

Middleware 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 depends largely 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 secure, efficient, and easy-to-maintain APIs. 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.

Talk to our API Expert