Skip to main content

C# Middleware

Introduction to Middleware​

Middleware forms the backbone of ASP.NET Core applications. At its core, middleware is software that's assembled into an application pipeline to handle requests and responses. Each component in the pipeline:

  1. Chooses whether to pass the request to the next component in the pipeline
  2. Can perform work before and after the next component in the pipeline

This pipeline-based approach allows you to modularize your request processing logic, making your application more maintainable and flexible.

Middleware Pipeline

How Middleware Works​

In ASP.NET Core, middleware executes in the order it's added to the pipeline. The request flows through each middleware component, and each component can:

  • Process the incoming request
  • Short-circuit the pipeline (not call the next middleware)
  • Perform additional processing after the next middleware completes

This creates a "Russian doll" model where each middleware wraps around the next one.

Let's examine the basic structure of middleware:

csharp
public class CustomMiddleware
{
private readonly RequestDelegate _next;

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

public async Task InvokeAsync(HttpContext context)
{
// Logic executed before the next middleware

await _next(context); // Call the next middleware

// Logic executed after the next middleware
}
}

Built-in Middleware in ASP.NET Core​

ASP.NET Core includes several built-in middleware components that you can use in your applications:

MiddlewareDescription
AuthenticationProvides authentication support
CORSConfigures Cross-Origin Resource Sharing
Exception HandlerCatches exceptions
RoutingDefines and constrains request routes
SessionProvides session management
Static FilesServes static files

Adding Middleware to Your Application​

Middleware is added to the application's request processing pipeline in the Configure method of the Startup class (or Program.cs in minimal APIs):

csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapRazorPages();
});
}

For minimal APIs in .NET 6+:

csharp
var builder = WebApplication.CreateBuilder(args);

// Add services

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();

app.MapControllers();
app.MapRazorPages();

app.Run();

Creating Custom Middleware​

There are three ways to create custom middleware:

1. Convention-based Middleware​

This is the traditional approach, where you create a class with a specific structure:

csharp
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;

public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}

public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();

try
{
await _next(context);
}
finally
{
stopwatch.Stop();
_logger.LogInformation($"Request {context.Request.Path} took {stopwatch.ElapsedMilliseconds}ms");
}
}
}

// Extension method to make it easier to add this middleware
public static class RequestTimingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestTimingMiddleware>();
}
}

Using this middleware in your application:

csharp
// In the Configure method or in Program.cs
app.UseRequestTiming();

2. Inline Middleware​

For simple middleware, you can define it directly in the Configure method:

csharp
app.Use(async (context, next) =>
{
// Do work before the next middleware
Console.WriteLine($"Request received for: {context.Request.Path}");

await next();

// Do work after the next middleware
Console.WriteLine($"Response status code: {context.Response.StatusCode}");
});

3. Using Map and MapWhen​

ASP.NET Core provides methods to create branches in the pipeline:

csharp
// Only executes for paths that start with /api
app.Map("/api", apiApp =>
{
apiApp.Run(async context =>
{
await context.Response.WriteAsync("API endpoint");
});
});

// Only executes when the condition is true
app.MapWhen(context => context.Request.Query.ContainsKey("admin"),
adminApp =>
{
adminApp.Run(async context =>
{
await context.Response.WriteAsync("Admin panel");
});
}
);

Real-World Example: Request Logging Middleware​

Let's create a middleware that logs all requests to a file:

csharp
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly string _logFilePath;

public RequestLoggingMiddleware(RequestDelegate next, string logFilePath)
{
_next = next;
_logFilePath = logFilePath;
}

public async Task InvokeAsync(HttpContext context)
{
// Log request information
string logMessage = $"[{DateTime.Now}] {context.Request.Method} {context.Request.Path}{context.Request.QueryString} - {context.Connection.RemoteIpAddress}";

// Continue processing
await _next(context);

// Log response status code
logMessage += $" - Status: {context.Response.StatusCode}";

// Write to file asynchronously
using (StreamWriter writer = File.AppendText(_logFilePath))
{
await writer.WriteLineAsync(logMessage);
}
}
}

// Extension method
public static class RequestLoggingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(
this IApplicationBuilder builder, string logFilePath)
{
return builder.UseMiddleware<RequestLoggingMiddleware>(logFilePath);
}
}

How to use this middleware in your application:

csharp
// In the Configure method or in Program.cs
app.UseRequestLogging(Path.Combine(env.ContentRootPath, "logs.txt"));

Middleware Order Matters​

The order in which you add middleware components is crucial. For example:

csharp
// Wrong order - authentication should come before endpoints
app.UseEndpoints(endpoints => { /* ... */ });
app.UseAuthentication(); // Too late!

// Correct order
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { /* ... */ });

A typical middleware pipeline might look like:

  1. Exception handling
  2. HTTPS redirection
  3. Static files
  4. Routing
  5. CORS
  6. Authentication
  7. Authorization
  8. Custom middleware
  9. Endpoints

Middleware Activation​

ASP.NET Core supports two middleware activation systems:

  1. Constructor injection: Services can be injected through the middleware constructor.
  2. Per-request activation: Some services (scoped services) should be injected in the InvokeAsync method.
csharp
public class MyMiddleware
{
private readonly RequestDelegate _next;
private readonly ISingletonService _singletonService;

// Constructor injection for singleton or transient services
public MyMiddleware(RequestDelegate next, ISingletonService singletonService)
{
_next = next;
_singletonService = singletonService;
}

// Per-request activation for scoped services
public async Task InvokeAsync(HttpContext context, IScopedService scopedService)
{
// Use scopedService here
await _next(context);
}
}

Summary​

Middleware is a powerful concept in ASP.NET Core that allows you to:

  • Process requests and responses in a modular way
  • Create a pipeline of components that handle requests in order
  • Short-circuit the pipeline when needed
  • Perform pre-processing and post-processing around other middleware

By understanding how middleware works and how to create your own middleware components, you can build more maintainable and flexible web applications in C#.

Exercises​

  1. Create a simple middleware that adds a response header with the current server time.
  2. Implement a middleware that logs the execution time of each request to the console.
  3. Build a middleware that checks if a specific query parameter exists and redirects to a different page if it doesn't.
  4. Create a middleware that catches exceptions and returns a custom error page.

Additional Resources​



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)