.NET Middleware with Practical Applications and Full Code Samples

Title image

Middleware is an essential component of the ASP.NET request pipeline. It allows developers to run code during both the request and response lifecycle. In this article, we will explore what middleware is, and how it works, and provide real-world practical use cases with complete code examples.

What is Middleware in .NET?

Middleware in .NET is a software component that processes HTTP requests and responses. It is executed in a pipeline, where each middleware component can perform an action and decide whether to pass the request further or return a response.

Key Characteristics of Middleware

  • Executes sequentially in the request pipeline.
  • Can modify request and response objects.
  • Can terminate request processing if needed.
  • Supports dependency injection.
  • Used for authentication, logging, rate limiting, and more.

How Middleware Works in .NET?

Middleware components are registered in the Program.cs file using the UseMiddleware<T>() method or inline with app.Use(). The middleware pipeline is executed in the order they are added.

  • Receives an HttpContext
  • Performs operations (e.g., logging, authentication, etc.)
  • Calls the next middleware in the pipeline (or short-circuits the request)

Real-world middleware Use Cases with Code Examples


1. Request Logging Middleware

Use Case: Logging request and response details for debugging and monitoring.

Implementation

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

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

    public async Task Invoke(HttpContext context)
    {
        _logger.LogInformation($"Request: {context.Request.Method} {context.Request.Path}");
        await _next(context);
    }
}

Register the middleware as shown below.

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

app.UseMiddleware<LoggingMiddleware>(); //register here!!

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

app.Run();

2. Custom Authentication Middleware

Use Case: Checking if a request contains a valid API key before processing.

Implementation

public class ApiKeyMiddleware
{
    private readonly RequestDelegate _next;
    private const string API_KEY = "123456"; // Hardcoded for demonstration

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

    public async Task Invoke(HttpContext context)
    {
        if (!context.Request.Headers.TryGetValue("X-API-KEY", out var extractedApiKey) || extractedApiKey != API_KEY)
        {
            context.Response.StatusCode = 401; // Unauthorized
            await context.Response.WriteAsync("Invalid API Key.");
            return;
        }

        await _next(context);
    }
}

Register the middleware

app.UseMiddleware<ApiKeyMiddleware>();

3. Exception Handling Middleware

Use Case: Global error handling to return meaningful error responses.

Implementation

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

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

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unhandled exception occurred.");
            context.Response.StatusCode = 500;
            await context.Response.WriteAsync("An unexpected error occurred.");
        }
    }
}

Register the middleware.

app.UseMiddleware<ExceptionHandlingMiddleware>();

4. Rate Limiting Middleware

Use Case: Preventing abuse by limiting the number of requests from a client in a given time period.

Implementation

public class RateLimitingMiddleware
{
    private static readonly Dictionary<string, (int Count, DateTime LastRequest)> _clients = new();
    private readonly RequestDelegate _next;
    private const int Limit = 5;
    private static readonly TimeSpan TimeWindow = TimeSpan.FromMinutes(1);

    public RateLimitingMiddleware(RequestDelegate next) => _next = next;

    public async Task Invoke(HttpContext context)
    {
        var clientIP = context.Connection.RemoteIpAddress?.ToString();
        if (clientIP == null || !_clients.ContainsKey(clientIP) || DateTime.UtcNow - _clients[clientIP].LastRequest > TimeWindow)
            _clients[clientIP] = (1, DateTime.UtcNow);
        else if (_clients[clientIP].Count >= Limit)
        {
            context.Response.StatusCode = 429;
            await context.Response.WriteAsync("Rate limit exceeded. Try again later.");
            return;
        }
        else
            _clients[clientIP] = (_clients[clientIP].Count + 1, DateTime.UtcNow);

        await _next(context);
    }
}

Register the middleware

app.UseMiddleware<RateLimitingMiddleware>();

5. Content Compression Middleware

Use Case: Compressing responses to improve performance.

We can use built-in Response Compression or custom Content Compression Middleware.

Implementation. built-in

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true; // Enable compression for HTTPS requests
    options.Providers.Add<BrotliCompressionProvider>(); // Brotli compression
    options.Providers.Add<GzipCompressionProvider>();  // Gzip compression
});

builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
    options.Level = System.IO.Compression.CompressionLevel.Fastest; // Adjust compression level
});

var app = builder.Build();

app.UseResponseCompression(); // Enable middleware

app.MapGet("/", async context =>
{
    var responseText = new string('A', 1000); // Large response for compression testing
    await context.Response.WriteAsync(responseText);
});

app.Run();

6. Custom Content Compression Middleware

If you need more control over compression, you can write a custom middleware.

Implementation

public class CustomCompressionMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task Invoke(HttpContext context)
    {
        var originalBodyStream = context.Response.Body;
        using var compressedStream = new MemoryStream();
        using var gzipStream = new GZipStream(compressedStream, CompressionMode.Compress, true);

        context.Response.Body = gzipStream;
        context.Response.Headers.Add("Content-Encoding", "gzip"); // Indicate compression

        await _next(context);

        gzipStream.Close();
        compressedStream.Seek(0, SeekOrigin.Begin);
        await compressedStream.CopyToAsync(originalBodyStream);
    }
}

6. Response Caching Middleware

Use Case: Improving performance by caching responses.

Implementation

public class ResponseCachingMiddleware
{
    private readonly RequestDelegate _next;
    private static readonly Dictionary<string, (string Content, DateTime Expiry)> Cache = new();
    private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(30);

    public ResponseCachingMiddleware(RequestDelegate next) => _next = next;

    public async Task Invoke(HttpContext context)
    {
        if (Cache.TryGetValue(context.Request.Path, out var cacheEntry) && DateTime.UtcNow < cacheEntry.Expiry)
        {
            await context.Response.WriteAsync(cacheEntry.Content);
            return;
        }

        var originalBody = context.Response.Body;
        using var memoryStream = new MemoryStream();
        context.Response.Body = memoryStream;
        await _next(context);
        context.Response.Body = originalBody;
        var responseContent = Encoding.UTF8.GetString(memoryStream.ToArray());
        Cache[context.Request.Path] = (responseContent, DateTime.UtcNow + CacheDuration);
        await context.Response.WriteAsync(responseContent);
    }
}

7. Request Timing Middleware

Use Case: Measuring the time taken to process a request.

Implementation

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 Invoke(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();
        await _next(context);
        stopwatch.Stop();

        _logger.LogInformation($"Request took {stopwatch.ElapsedMilliseconds} ms");
    }
}

Register the middleware

app.UseMiddleware<RequestTimingMiddleware>();

8. Maintenance Mode Middleware

Use Case: Temporarily disabling access to an application during updates.

Implementation

public class MaintenanceMiddleware
{
    private readonly RequestDelegate _next;
    private readonly bool _isUnderMaintenance = true; // Change to false to disable maintenance mode

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

    public async Task Invoke(HttpContext context)
    {
        if (_isUnderMaintenance)
        {
            context.Response.StatusCode = 503;
            await context.Response.WriteAsync("The site is under maintenance. Please try again later.");
            return;
        }

        await _next(context);
    }
}

Register the middleware

app.UseMiddleware<MaintenanceMiddleware>();

9. Geo-Blocking Middleware

Use Case: Restricting access to specific countries based on IP address.

Implementation

public class GeoBlockingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly List<string> _blockedCountries = new() { "CN", "RU", "IR" }; // Example blocked countries

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

    public async Task Invoke(HttpContext context)
    {
        string userCountry = GetUserCountry(context); // Implement this based on an IP lookup service

        if (_blockedCountries.Contains(userCountry))
        {
            context.Response.StatusCode = 403;
            await context.Response.WriteAsync("Access denied for your region.");
            return;
        }

        await _next(context);
    }

    private string GetUserCountry(HttpContext context)
    {
        // Placeholder logic, use a third-party API or database lookup to determine the country from IP
        return "US"; // Assume US for this example
    }
}

Register middleware

app.UseMiddleware<GeoBlockingMiddleware>();

10. Request IP Logger Middleware

Use Case: Logging the IP address of every client making a request.

Implementation

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

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

    public async Task Invoke(HttpContext context)
    {
        var ip = context.Connection.RemoteIpAddress?.ToString();
        _logger.LogInformation($"Request received from IP: {ip}");

        await _next(context);
    }
}

Register middleware

app.UseMiddleware<RequestIpLoggingMiddleware>();

11. User Activity Tracking Middleware

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

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

    public async Task Invoke(HttpContext context)
    {
        var user = context.User.Identity?.Name ?? "Anonymous";
        var path = context.Request.Path;
        _logger.LogInformation($"User: {user} accessed {path} at {DateTime.UtcNow}");

        await _next(context);
    }
}

Register middleware

app.UseMiddleware<UserActivityMiddleware>();

12. Dark Mode Cookie Middleware

Use Case: Storing user preferences (like dark mode) in cookies.

public class DarkModeMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task Invoke(HttpContext context)
    {
        if (!context.Request.Cookies.ContainsKey("DarkMode"))
        {
            context.Response.Cookies.Append("DarkMode", "false");
        }

        await _next(context);
    }
}

Likewise, we can use middleware in .NET for several other use cases.

Conclusion

Middleware is a powerful feature in .NET applications that allows developers to enhance security, optimize performance, and improve user experience. The examples above demonstrate how middleware can be used for logging, authentication, exception handling, rate limiting, rate timing response compression, and caching.

By leveraging middleware effectively, developers can build scalable, maintainable, and high-performance applications. Whether you’re securing APIs, optimizing request handling, or improving efficiency, middleware provides a flexible and extensible solution for modern .NET applications.

Reference

Up Next
    Ebook Download
    View all
    Learn
    View all