Custom middleware in an ASP.NET Core application

Posted by: Suprotim Agarwal , on 6/18/2023, in Category ASP.NET Core
Views: 48792
Abstract: This article focuses on the implementation of custom middleware in ASP.NET Core, exploring examples of how custom middleware can be created for logging, exception handling, and caching. It delves into how custom middleware can be combined with built-in middleware to create a flexible processing pipeline tailored to meet the specific requirements of an application. By the end of the article, you will have a deeper understanding of the benefits and implementation of custom middleware in ASP.NET Core.

In a previous article on Middleware in ASP.NET Core, we saw that Middleware is a software component that handles requests and responses in an application pipeline. It serves as a link between a server and a client application. The middleware is responsible for tasks like authentication, authorization, logging, error handling, routing, and so on.

A custom middleware is a software component designed to extend the functionality of a pipeline in an application. It is a piece of software that is inserted into the pipeline of the application to perform a specific task, such as authentication, logging, error handling, or routing, and it can be used to add custom functionality to the application.

Examples of a Custom Middleware implementation

Custom middleware in ASP.NET Core allows developers to execute code before or after the request-response cycle. It provides a flexible and extensible way for developers to add custom functionality to their applications. With custom middleware, developers can create a pipeline of processing for each request and response that can be tailored to meet the specific needs of their applications.

In Visual Studio, you can right click the project > Add New Item and choose a ‘Middleware class’ to add a middleware to your application.

Here is an example of a custom middleware implementation for authentication:

public class CustomAuthMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task InvokeAsync(HttpContext context)
    {
        string authHeader = context.Request.Headers["Authorization"];
        if (authHeader != null && authHeader.StartsWith("Bearer"))
        {
            string token = authHeader.Substring("Bearer ".Length).Trim();

            try
            {
                // Validate the token
                var claimsPrincipal = ValidateToken(token);
                context.User = claimsPrincipal;
            }
            catch
            {
                // Return unauthorized status code if the token is invalid
                context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                return;
            }
        }

        await _next(context);
    }

    private ClaimsPrincipal ValidateToken(string token)
    {
        // Implement validation logic here
           
    }
}

The code begins by declaring a class that is injected with the RequestDelegate delegate, which is used to handle incoming HTTP requests.  The RequestDelegate is a delegate that represents the next middleware component in the pipeline.  In other words, it is a reference to the next middleware component in the pipeline that executes after the current middleware component completes processing the request.

 

A middleware component is typically defined as a class that either implements the IMiddleware interface or utilizes a function that takes a HttpContext object and returns a Task. The HttpContext object carries important details about the current HTTP request being processed, such as request method, URL, headers, and body.

Middleware components utilize the HttpContext object to examine and adjust the incoming request or outgoing response as needed. Examples include adding a custom header to the response or performing authentication logic on the incoming request.

In the constructor of the middleware component, the _next field is initialized with the provided RequestDelegate. This permits the middleware component to invoke the next middleware component in the pipeline after completing request processing. The middleware component also has the option to bypass the pipeline by not calling the next middleware component, and instead respond to the request on its own.

The asynchronous InvokeAsync method is then declared. This is used to handle incoming requests. The first step in this method is to check the Authorization header of the request for a Bearer token. If the token is found, it is validated using the ValidateToken method. If the token is valid, the claims of the token are used to set the context.user to a ClaimsPrincipal. If the token is not valid, the status code of the response is set to Unauthorized. Finally, the _next RequestDelegate is invoked, which is responsible for processing the request.

The ValidateToken method is responsible for validating the token. This method must be implemented according to the authentication mechanism used.

The custom middleware can be added to the HTTP pipeline as follows:

// Extension method used to add the middleware to the HTTP request pipeline.
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<CustomAuthMiddleware>();
    }
}

Custom Middleware to add Logging

Another example of custom middleware could be to add logging to track request and response details, such as request time, URL, status code, etc. Here is an example:

public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task InvokeAsync(HttpContext context)
    {
        var startTime = DateTime.Now;

        await _next(context);

        var endTime = DateTime.Now;
        var elapsedTime = endTime - startTime;

        var logMessage = $"{context.Request.Method} {context.Request.Path} {context.Response.StatusCode} {elapsedTime.TotalMilliseconds}ms";
        Console.WriteLine(logMessage);
    }
}

The above code is a middleware class that logs requests. The middleware class takes in a RequestDelegate object as its argument and sets it as the next delegate to be invoked in the pipeline.

When the middleware is invoked, the start time of the request is logged and then the request is passed to the next delegate. Once the response is received from the next delegate, the end time is logged and the total time elapsed is calculated. Finally, a log message containing the request method, path, status code and total time elapsed is written to the console.

Combining Custom Middleware with other built-in Middleware

Additionally, custom middleware can be combined with other built-in middleware and third-party middleware to create a pipeline of processing for each request and response.

For example, you can use custom middleware for authentication, followed by built-in middleware for static file serving, followed by custom middleware for logging.

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

app.UseMiddleware<CustomAuthMiddleware>();
app.UseStaticFiles();
app.UseMiddleware<RequestLoggingMiddleware>();

In this example, each middleware component will be executed in the order they are added to the pipeline. The CustomAuthMiddleware will be executed first, followed by the built-in UseStaticFiles middleware, and finally the RequestLoggingMiddleware. The result of each middleware’s execution will be passed on to the next middleware in the pipeline until the response is returned to the client.

Custom Middleware for Exception Handling and Caching

So far, we have seen how custom middleware in ASP.NET Core can be used to add custom functionality to the request-response pipeline. One of the most common uses of custom middleware is exception handling and Caching.

Here’s an example of how to add custom exception handling middleware:

Create a new class for your custom middleware, for example:

public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            // Log the exception
            Console.WriteLine("An exception occurred: " + ex.Message);

            // Set the response status code to 500 Internal Server Error
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        }
    }
}

Use the UseMiddleware method to add the custom middleware to the request-response pipeline:

app.UseMiddleware<ExceptionHandlingMiddleware>();

With this custom exception handling middleware in place, any exceptions that occur in your application will be caught and logged, and the response status code will be set to 500 Internal Server Error. This can be useful for handling unexpected errors and providing a consistent response to the client.

Another common use case for custom middleware is caching. Here’s an example of how to add custom caching middleware:

Create a new class for your custom middleware, for example:

public class CachingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IMemoryCache _memoryCache;

    public CachingMiddleware(RequestDelegate next, IMemoryCache memoryCache)
    {
        _next = next;
        _memoryCache = memoryCache;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var cacheKey = context.Request.Path;

        if (_memoryCache.TryGetValue(cacheKey, out byte[] cachedData))
        {
            context.Response.Body = new MemoryStream(cachedData);
            return;
        }

        var originalBodyStream = context.Response.Body;
        using (var memoryStream = new MemoryStream())
        {
            context.Response.Body = memoryStream;

            await _next(context);

            var responseData = memoryStream.ToArray();
            _memoryCache.Set(cacheKey, responseData, TimeSpan.FromMinutes(10));
            memoryStream.Position = 0;
            await memoryStream.CopyToAsync(originalBodyStream);
        }
    }
}

The above code is a middleware class that allows for caching of HTTP responses in an application. It uses a memory cache, that stores data in the RAM of the computer, rather than on a hard drive.

When invoked, the middleware first checks if a response has already been cached for the current request. If so, it sets the response body to the cached data and exits. If not, it continues by creating a MemoryStream object to store the response data. It then sets the response body to the MemoryStream object and calls the next piece of middleware in the pipeline.

After the response is generated, Responses will be cached in memory using the `IMemoryCache` service for 10 minutes. Finally, the response data is copied back to the original response body stream, allowing it to be sent back to the client. Subsequent requests for the same URL within the cache time window will receive the cached response, instead of making a new request to the server.

Conclusion

In conclusion, custom middleware in ASP.NET Core is a powerful tool for adding custom functionality to your application, such as exception handling, caching, and more. With custom middleware, you can create a flexible and extensible pipeline of processing for each request and response, tailored to meet the specific needs of your application.

This article was technically reviewed by Mahesh Sabnis

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Suprotim Agarwal, MCSD, MCAD, MCDBA, MCSE, is the founder of DotNetCurry, DNC Magazine for Developers, SQLServerCurry and DevCurry. He has also authored a couple of books 51 Recipes using jQuery with ASP.NET Controls and The Absolutely Awesome jQuery CookBook.

Suprotim has received the prestigious Microsoft MVP award for Sixteen consecutive years. In a professional capacity, he is the CEO of A2Z Knowledge Visuals Pvt Ltd, a digital group that offers Digital Marketing and Branding services to businesses, both in a start-up and enterprise environment.

Get in touch with him on Twitter @suprotimagarwal or at LinkedIn



Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!