How to create and use the custom Middleware in .Net Core

1. Introduction

Middleware in ASP.NET Core refers to software components that are assembled into an application pipeline to handle requests and responses. They are configured as part of the request processing pipeline in the Startup.cs file. Middleware can help separate cross-cutting concerns from app logic for modular and maintainable code, so it is good for logging, authentication, caching, compression, security, and more.

ASP.NET Core includes many built-in middleware like Static File Middleware, Routing Middleware, Endpoint Routing Middleware, etc. But in this article, we will create our middleware

2. Create and use the Middleware

For creating a middleware, there are two things that need to be done:

Do you want to be a good trading in cTrader?   >> TRY IT! <<

1) Create a constructor to inject the RequestDelegate for the next middleware
2) Implement the InvokeAsync method, pass the business object in it, and then you can handle everything that you want!

For example, we create a request log middleware to log each request, then we need to create a constructor as below

private readonly RequestDelegate _next;
public RequestLogMiddleware(RequestDelegate next)
{
    _next = next;
}

because we need to log the message, so it needs to pass the logger object in InvokeAsync, and define the logger in global

private readonly ILogger<RequestLogMiddleware> _logger;

public async Task InvokeAsync(HttpContext context, ILogger<RequestLogMiddleware> logger)
{
    logger.LogDebug("Request received: {Method} {Path}", context.Request.Method, context.Request.Path);
    await _next(context);
    logger.LogDebug("Response sent: {StatusCode}", context.Response.StatusCode);
}

So, the complete code is below

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

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

    public async Task InvokeAsync(HttpContext context, ILogger<RequestLogMiddleware> logger)
    {
        logger.LogDebug("Request received: {Method} {Path}", context.Request.Method, context.Request.Path);

        await _next(context);

        logger.LogDebug("Response sent: {StatusCode}", context.Response.StatusCode);
    }
}

after that, we can create an extension method for easy use it

public static class RequestLogMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestLogMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestLogMiddleware>();
    }
}

in the end, we can use the middleware in program.cs

var app = builder.Build();
app.UseRequestLogMiddleware();

3. Other Useful Middlewares

The middleware is easy to create and use, so I think the main point is what middleware (or ideas) can we create. I will show you some of them below 🙂

3.1. Error Handling Middleware

Handle the common errors

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            // Log error
            if (context.Response.StatusCode == 404)
            {
                // Redirect to 404 page
                context.Response.Redirect("/error/404");
            }
            else
            {
                // Handle other errors
                context.Response.StatusCode = 500;
                context.Response.ContentType = "text/plain";
                await context.Response.WriteAsync("An unexpected error occurred!");
            }
        }
    }
}

public static class ErrorHandlingMiddlewareExtensions
{
    public static IApplicationBuilder UseErrorHandlingMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ErrorHandlingMiddleware>();
    }
}

This middleware wraps all subsequent handlers in a try-catch block. If any unhandled exception occurs in later middleware, it will be caught here.

3.2. Response Compression Middleware

Compress responses to reduce bandwidth usage.

public class ResponseCompressionMiddleware
{
  private readonly RequestDelegate _next;

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

  public async Task Invoke(HttpContext context)
  {
    // Check if client supports compression
    if(context.Request.Headers["Accept-Encoding"].Contains("gzip"))
    {
      // Stream to hold compressed response
      MemoryStream compressedStream = new MemoryStream(); 

      // Compress stream
      using (GZipStream gzipStream = new GZipStream(compressedStream, CompressionMode.Compress))
      {
        await _next(context);
        context.Response.Body.CopyTo(gzipStream); 
      }

      // Replace uncompressed response with compressed one
      context.Response.Body = compressedStream;

      // Header to indicate compressed response
      context.Response.Headers.Add("Content-Encoding", "gzip"); 
    }
    else 
    {
      await _next(context);
    }
  }
}

public static class ResponseCompressionMiddlewareExtensions
{
    public static IApplicationBuilder UseResponseCompressionMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ResponseCompressionMiddleware>();
    }
}

This middleware checks the request headers, compresses the response using GZipStream if the client accepts gzip, and replaces the uncompressed response with the compressed one. This reduces bandwidth usage.

3.3. Async Middleware

Implementing asynchronous logic and database calls in a middleware using async/await.

public class AsyncMiddleware
{
  private readonly RequestDelegate _next;

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

  public async Task Invoke(HttpContext context)
  {
    // Call database async
    var data = await FetchFromDatabase();

    // Network call
    var response = await CallExternalService();

    // Set context info
    context.Items["data"] = data;

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

  private async Task<Data> FetchFromDatabase() 
  {
    // Fetch data from database
  }

  private async Task<Response> CallExternalService()
  {
   // Call API   
  }
}

public static class AsyncMiddlewareExtensions
{
    public static IApplicationBuilder UseAsyncMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<AsyncMiddleware>();
    }
}

This allows the middleware to perform asynchronous operations like database queries, network calls, long-running CPU tasks, etc. without blocking the request thread. The next middleware is invoked once the async work is completed.

3.4. Security Middleware

Verify user rights and role whether is correct

public class SecurityMiddleware 
{
  public async Task Invoke(HttpContext context)
  {
    // Check authentication
    if(!context.User.Identity.IsAuthenticated)
    {
      context.Response.Redirect("/account/login");
      return;
    }

    // Check authorization for admin page
    if(context.Request.Path.Value.StartsWith("/admin") && !context.User.HasRequiredRole("Admin")) 
    {
      context.Response.StatusCode = 403;
      return;
    }

    // Validate business rules
    if(!IsValidRequest(context.Request))
    {
       context.Response.StatusCode = 400;  
       return;
    }

    await _next(context);
  }

  private bool IsValidRequest(HttpRequest request)
  {
    // Check headers, params, business rules etc.
    return true;
  }
}

public static class SecurityMiddlewareExtensions
{
    public static IApplicationBuilder UseSecurityMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<SecurityMiddleware>();
    }
}

This middleware encapsulates all security and validation logic into a single middleware that is applied globally. The app code only focuses on business logic.

3.5. Localization Middleware

Set culture, localization, or time zones based on request attributes.

public class LocalizationMiddleware
{
  public async Task Invoke(HttpContext context)
  {
    var userLanguage = context.Request.Headers["Accept-Language"].FirstOrDefault();

    // Set culture from header
    CultureInfo culture;
    if(!string.IsNullOrEmpty(userLanguage))
    {
       culture = new CultureInfo(userLanguage);
    }
    else 
    {
       culture = new CultureInfo("en-US"); 
    }

    CultureInfo.CurrentCulture = culture;
    CultureInfo.CurrentUICulture = culture;

    // Set timezone
    var timezone = context.Request.Headers["TimeZone"].FirstOrDefault();

    if(!string.IsNullOrEmpty(timezone))
    {
       TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(timezone);
       TimeZoneInfo.Local = timeZone; 
    }

    await _next(context);
  }
}

public static class LocalizationMiddlewareExtensions
{
    public static IApplicationBuilder UseLocalizationMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<LocalizationMiddleware>();
    }
}

This middleware checks request headers for the user’s preferred language and timezone. It sets CurrentCulture and CurrentUICulture based on Accept-Language header. It also sets the Local TimeZone based on the TimeZone header. This can be used to dynamically localize responses for each user by looking up their headers. The app will format dates, numbers, and currencies appropriately for the user. The middleware must be configured early in program.cs before localizable content is generated.

3.6. Session Middleware

Manipulate session state and manage cookies

public class SessionMiddleware 
{
  public async Task Invoke(HttpContext context)
  {
    // Get session object
    var session = context.Session;

    // Set value in session
    session.SetString("Key", "Value");

    // Get value from session
    var value = session.GetString("Key");

    // Create cookie
    context.Response.Cookies.Append("CookieName", "CookieValue");

    // Delete cookie
    context.Response.Cookies.Delete("CookieName");

    // Set cookie expiration
    context.Response.Cookies.Append("CookieName", "Value", new CookieOptions
    {
      Expires = DateTime.Now.AddDays(1)
    });

    await _next(context);
  }
}

public static class SessionMiddlewareExtensions
{
    public static IApplicationBuilder UseSessionMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<SessionMiddleware>();
    }
}

Session state and cookies are often used for user data, preferences, shopping carts, tokens etc. that need to persist across multiple requests. This middleware provides a centralized place to manage both sessions and cookies in your application.

3.7. RateLimit Middleware

A middleware that limits the number of requests from a client in a time period to prevent abuse/DDoS. It would keep track of requests and respond with 429 Too Many Requests if the limit exceeds.

 public class RateLimitMiddleware
{
  private const int PerMinuteLimit = 10;
  private static Dictionary<string, int> _requests = new Dictionary<string, int>();

  public async Task Invoke(HttpContext context)
  {
    var clientIp = context.Connection.RemoteIpAddress.ToString();

    // Increment counter for client
    if(_requests.ContainsKey(clientIp))
    {
      _requests[clientIp]++;  
    }
    else
    {
      _requests.Add(clientIp, 1);
    }

    // Check if over limit
    if(_requests[clientIp] > PerMinuteLimit)
    {
      context.Response.StatusCode = 429; // Too many requests
      return;
    }

    await _next.Invoke(context);

    // Remove counter after request is complete
    _requests.Remove(clientIp); 
  }
}

public static class RateLimitMiddlewareExtensions
{
    public static IApplicationBuilder UseRateLimitMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RateLimitMiddleware>();
    }
}

This allows limiting total requests per minute from a client IP. The limit and time window can be configured as per your needs.

3.8. URL Rewrite Middleware

Handle URL rewriting

public class RewriteMiddleware
{
  public async Task Invoke(HttpContext context)
  {
    var requestPath = context.Request.Path.Value;

    if(requestPath.StartsWith("/old"))
    {
      var newPath = requestPath.Replace("/old", "/new");

      context.Response.Redirect(newPath);
      return;
    }

    await _next(context);
  }
}

public static class RewriteMiddlewareExtensions
{
    public static IApplicationBuilder UseRewriteMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RewriteMiddleware>();
    }
}

This middleware checks the request path – if it starts with /old, it replaces that part with /new and redirects to the new URL.

For example, a request to /old/page will be redirected to /new/page.

The old to new URL mapping can be extended as:

var rewrites = new Dictionary<string, string>
{
  {"/old/page1", "/new/page1"},
  {"/old/page2", "/new/page2"}
};

var newPath = rewrites[requestPath];

This allows you to centrally handle URL rewrites for your entire application in a single place. Useful when reorganizing URLs.

The middleware can be placed early in the pipeline to redirect old URLs before the request goes further.

3.9. Https Redirect Middleware

Redirect all HTTP requests to https to enforce site-wide SSL.

public class HttpsRedirectMiddleware 
{
  public async Task Invoke(HttpContext context)
  {
    if (!context.Request.IsHttps)
    {
      var host = context.Request.Host.ToString();
      var redirectUrl = "https://" + host + context.Request.Path;

      context.Response.Redirect(redirectUrl); 
      return;
    }

    await _next(context);
  }
}

public static class HttpsRedirectMiddlewareExtensions
{
    public static IApplicationBuilder UseHttpsRedirectMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<HttpsRedirectMiddleware>();
    }
}

This middleware checks if the request scheme is HTTPS. If not, it reconstructs the URL with an HTTPS scheme and redirects to it.

For example, a request to http://example.com/page will be redirected to https://example.com/page

This can be used to enforce HTTPS and SSL for your entire application by adding this middleware early in the pipeline.

3.10. Header injection Middleware

A middleware that injects useful headers like X-Request-Id for tracing requests across microservices.

public class HeaderInjectionMiddleware
{
  public async Task Invoke(HttpContext context)
  {
    // Generate unique request ID
    var requestId = Guid.NewGuid().ToString();

    // Inject X-Request-ID header
    context.Response.Headers.Add("X-Request-ID", requestId);

    await _next(context);
  }
}

public static class HeaderInjectionMiddlewareExtensions
{
    public static IApplicationBuilder UseHeaderInjectionMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<HeaderInjectionMiddleware>();
    }
}

This middleware generates a unique GUID on each request and inserts it as a custom header X-Request-ID in the response.

This can be useful to:

1) Track and correlate requests across microservices
2) Debug issues by tracing logs with request ID
3) Analyze metrics for requests

This middleware can be configured early in the pipeline before headers are sent. Multiple headers can be injected in a similar way.

4. Conclusion

The middleware is useful and very easy to create, I shared some ideas on how to create and use it in this article, hope can help you solve the problems 🙂

Loading

Views: 31
Total Views: 474 ,

Leave a Reply

Your email address will not be published. Required fields are marked *