Understanding Middlewares in ASP.NET

Understanding Middlewares in ASP.NET

Middleware is a fundamental concept in ASP.NET that enables developers to process requests and responses in a modular pipeline. With the release of .NET 9, middleware continues to play a crucial role in shaping modern web applications. In this blog, we will explore middlewares in ASP.NET, beginning with a comparison of middleware in .NET Framework, .NET Core, and .NET, followed by an understanding of delegates and their relevance to RequestDelegate, and finally, a step-by-step guide to building custom middleware.

Middleware Approaches: .NET Framework vs .NET Core vs .NET

Feature.NET Framework.NET Core (.NET 1-5).NET 6+ (.NET 9)
Middleware ConceptHTTP Modules/PipelineMiddleware PipelineMinimal APIs & Middleware
ConfigurationGlobal.asax, HttpModules, HttpHandlersStartup.cs (Configure method)Program.cs (Minimal Hosting Model)
PerformanceSlower due to tightly coupled request processingLightweight and modularFurther optimised with native AOT
FlexibilityLimited due to rigid architectureHigh flexibility with DI supportImproved API routing and performance

Understanding Delegates and the Action Keyword

A delegate in C# is a type that represents references to methods with a specific signature. It allows methods to be passed as parameters, enabling flexible and extensible designs. A delegate is essentially a function pointer that is type-safe.

Example of a Delegate

public delegate void MyDelegate(string message);

public class Program
{
    public static void PrintMessage(string message)
    {
        Console.WriteLine(message);
    }

    public static void Main()
    {
        MyDelegate del = PrintMessage;
        del("Hello from delegate!");
    }
}

While delegates are custom types, C# provides built-in generic delegate types like Action<>, Func<>, and Predicate<>.

  • Action<T>: Represents a method that takes a parameter and returns void.

  • Func<T, TResult>: Represents a method that takes parameters and returns a value.

    example:

      Func<int, int, int> add = (int a, int b) => a + b;
      add(5, 7) // returns 12
    
  • Predicate<T>: Represents a method that takes a parameter and returns a boolean.

Example of an Action

Action<string> printMessage = Console.WriteLine;
printMessage("Hello using Action!");

The key difference between a delegate and Action is that a delegate is explicitly defined with a signature, whereas Action is a predefined delegate that simplifies function references without requiring a custom delegate definition.

How does this relate to ASP.NET middleware? In ASP.NET, middleware is built using a RequestDelegate, which is a delegate that processes HTTP requests.

public delegate Task RequestDelegate(HttpContext context);

It is the foundation of middleware in ASP.NET. Middlewares are executed sequentially, where each middleware component receives an HttpContext, performs processing, and then optionally passes control to the next middleware in the pipeline.

Example of Using RequestDelegate

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) => {
    Console.WriteLine("Middleware 1: Before Request");
    await next.Invoke();
    Console.WriteLine("Middleware 1: After Request");
});

app.Run(async context => {
    await context.Response.WriteAsync("Hello from .NET Middleware!");
});

app.Run();

In this example, the first middleware logs messages before and after the request processing, and the Run middleware handles the final response.

Building a Custom Middleware in .NET 9

To create a custom middleware, follow these steps:

Step 1: Create a Middleware Class

public class ExecutionTimeMiddleware {
    private readonly RequestDelegate _next;

    public ExecutionTimeMiddleware(RequestDelegate next) {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context) {
        var stopwatch = Stopwatch.StartNew();
        await _next(context);
        stopwatch.Stop();
        Console.WriteLine($"Request processed in {stopwatch.ElapsedMilliseconds} ms");
    }
}

Step 2: Register Middleware in Program.cs

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseMiddleware<ExecutionTimeMiddleware>();

app.Run(async context => {
    await context.Response.WriteAsync("Response from the App");
});

app.Run();

The IApplicationBuilder interface is the standard way to register middleware in a structured and reusable manner. Instead of directly adding middleware in Program.cs, we can create an extension method.

Step 3: Create an Extension Method

public static class ExecutionTimeMiddlewareExtensions {
    public static IApplicationBuilder UseExecutionTimeLogger(this IApplicationBuilder builder) {
        return builder.UseMiddleware<ExecutionTimeMiddleware>();
    }
}

Step 4: Register Middleware Using the Extension

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseExecutionTimeLogger();

app.Run(async context => {
    await context.Response.WriteAsync("Response from the App");
});

app.Run();

By using IApplicationBuilder, we ensure a standardised approach to middleware registration, improving readability and maintainability.

Terminating Middleware

A terminating middleware is a middleware component in ASP.NET that does not call the next delegate in the request pipeline. Instead, it processes the request and generates a response immediately, preventing any further middleware from executing.

Characteristics of Terminating Middleware:

  • It does not invoke await next.Invoke();

  • It short-circuits the request pipeline by sending a response directly.

  • Typically implemented using app.Run() instead of app.Use().

Example of Terminating Middleware:

app.Run(async context =>
{
    await context.Response.WriteAsync("This is a terminating middleware. No further processing will occur.");
});

In this case:

  • Since there is no next.Invoke() call, no middleware registered after this will run.

  • The request is handled entirely by this middleware.

Non-Terminating Middleware Example:

app.Use(async (context, next) =>
{
    Console.WriteLine("Middleware 1: Before Request");
    await next.Invoke();
    Console.WriteLine("Middleware 1: After Request");
});

Here, await next.Invoke(); ensures the next middleware in the pipeline executes, making it non-terminating.

Conclusion

Middleware is a powerful feature in ASP.NET, enabling modular and efficient request processing. Understanding delegates and RequestDelegate provides a foundation for grasping how middleware functions. With .NET 9, middleware has evolved to offer greater performance and flexibility. By following the steps outlined, developers can create custom middleware to suit their application’s needs.