Sitemap
Furkan Uzunöz

Furkan Uzunöz

The Heart of .NET Core: Understanding and Harnessing Middleware

6 min readNov 1, 2023

--

We want our code to be less repetitive and more modular in solving cross-cutting concerns and different problems. When I mention cross-cutting concerns, the first thing that comes to mind for all of us is aspect-oriented programming (AOP), but sometimes we may encounter situations where we cannot use AOP. To give examples of these situations, if the web API you are using is a minimal API, you don’t have much chance to use AOP.While middleware and AOP are entirely different concepts, they can sometimes be used to address similar problems. Using middleware, you can intercept a request before it reaches the endpoint and perform desired operations on it. You can even perform model binding in middleware. From this perspective, although middleware and AOP are quite different, middleware provides more flexibility in accomplishing various tasks. Now, let’s gather some information about middleware and create a few examples.

Press enter or click to view image in full size

Modern web applications involve numerous tasks to handle user requests and responses. Processing requests, performing authentication, logging, caching, error handling, and many other tasks are fundamental functions of a web application. To handle and manage these tasks in an organized manner, Middleware is used in .NET Core. In this article, we will delve into what Middleware is, how it works, and how it is used in .NET Core in detail.

What is Middleware?

Middleware represents intermediary layers in a web application’s processing pipeline. It intercepts requests at different points and facilitates various tasks as the request flows through. Each Middleware performs a specific function and is part of a chain that controls the sequence of processing within an application. This enables clean separation and reusability of different concerns within a web application

Middleware Chain

Middleware forms a chain that spans the entire lifecycle of a request. Each request enters this chain, and each Middleware in the chain processes the request and passes it on to the next Middleware. Here is an example of how an HTTP request is processed through a Middleware chain:

  1. When an HTTP request is received, it enters the Middleware chain.
  2. The first Middleware processes the request and, if necessary, passes it to the next Middleware.
  3. Each Middleware processes the request in sequence and forwards it to the next Middleware.
  4. The last Middleware completes its processing, and a response is generated
  5. The response is propagated backward through the Middleware chain, and each Middleware performs any necessary actions.,

This allows web applications to handle complex processes in a modular and organized way lets create a few examples.

Creating Custom Middleware

To create custom Middleware, you need to create a class and implement the Invoke or InvokeAsync method. This method is where the Middleware's functionality is defined. For example:

public class CustomMiddleware
{
private readonly RequestDelegate _next;

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

public async Task InvokeAsync(HttpContext context)
{
// Perform Middleware functionality here.

// To pass the chain to the next Middleware:
await _next(context);
}
}

This custom Middleware processes the request and passes the chain to the next Middleware. Middleware allows you to perform various operations between Middleware components.

Get Furkan Uzunöz’s stories in your inbox

Join Medium for free to get updates from this writer.

Now, let’s try to use middleware for authentication and global error handling tasks.

Authentication Middleware

One of the advantages of performing authentication as middleware is that if the user doesn’t have permission for that endpoint, you can terminate the pipeline right there and return a response to the user. When we do this with aspects, the incoming request must first pass through all these pipelines before reaching the endpoint. Now, let’s create an example of an authentication middleware.

using Microsoft.AspNetCore.Http;
using System.Net;
using System.Threading.Tasks;

public class AccessControlMiddleware
{
private readonly RequestDelegate _nextHandler;

public AccessControlMiddleware(RequestDelegate nextHandler)
{
_nextHandler = nextHandler;
}

public async Task InvokeAsync(HttpContext httpContext)
{
// Enable request buffering
httpContext.Request.EnableBuffering();

// Verify user permissions
if (HasAccessPermission(httpContext))
{
// If access is allowed, proceed to the next middleware
await _nextHandler(httpContext);
}
else
{
// If access is denied, perform authorization
var authService = RetrieveAuthService();
var authorizationResult = await authService.ValidateAccess();

if (authorizationResult.HttpStatusCode != (int)HttpStatusCode.OK)
{
// Respond with error code and message if authorization fails
httpContext.Response.StatusCode = authorizationResult.HttpStatusCode;
await httpContext.Response.WriteAsJsonAsync(authorizationResult);
}
else
{
// Cache the user upon successful authorization
CacheHelper.AddUserToCache(httpContext.User.Identity.Name);
}

// Continue with the request pipeline
await _nextHandler(httpContext);
}
}

private bool HasAccessPermission(HttpContext httpContext)
{
// Custom logic to verify access permission
// Example: Check using a cached list of users
var userName = httpContext.User.Identity.Name;

return CacheHelper.IsUserAuthorized(userName);
}

private AuthService RetrieveAuthService()
{
// Logic to obtain authentication service instance
return new AuthService();
}
}

In the middleware we’ve created, we first check the cache to see if the user sending the request has the necessary permissions. This way, if it’s an authorized person, they can pass through this middleware directly to the next one. If they don’t have the necessary permissions, we check their authorization. If the result is not equal to a successful 200 response, we stop the middleware here and add the error message and status code to the response. If it is successful, we add the person to the cache and allow them to continue to the other middlewares.

One of the most valuable pieces of information about middleware might be enabling buffering. Under normal circumstances, a request body can only be read once in a request. However, in middleware, there may be situations where we need to read the body multiple times in different custom middlewares. In such cases, we need to enable buffering.lets create global exception handling middleware

Global Exception Handling Middleware

In this middleware, when you encounter an error, you can log that error to a database or other places. However, in the example, I will only perform the operation of writing this error to the response when an error is encountered.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using System;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using YourNamespaceHere; // Gerekli namespace'leri ekleyin

public class ErrorHandlingMiddleware
{
private readonly RequestDelegate _nextHandler;

public ErrorHandlingMiddleware(RequestDelegate nextHandler)
{
_nextHandler = nextHandler;
}

public async Task InvokeAsync(HttpContext httpContext)
{
try
{
if (!httpContext.Response.HasStarted)
{
await _nextHandler(httpContext);
}
}
catch (Exception exception)
{
// Handle exception logic
await ProcessExceptionAsync(httpContext, exception);
}
}

private static Task ProcessExceptionAsync(HttpContext httpContext, Exception exception)
{
if (httpContext.Response.HasStarted)
{
return Task.CompletedTask;
}

var httpStatusCode = HttpStatusCode.InternalServerError;

if (exception is BadHttpRequestException)
{
httpStatusCode = HttpStatusCode.BadRequest;
}
else if (exception is UnauthorizedAccessException)
{
httpStatusCode = HttpStatusCode.Unauthorized;
}
else if (exception is ValidationException)
{
httpStatusCode = HttpStatusCode.BadRequest;
}
// Add additional conditions for other types of errors as needed

httpContext.Response.ContentType = "application/json";
httpContext.Response.StatusCode = (int)httpStatusCode;

var errorResponse = new
{
message = exception.Message
};

var jsonResponse = JsonSerializer.Serialize(errorResponse);
return httpContext.Response.WriteAsync(jsonResponse);
}
}

As you can see, using if-else blocks for each error type here disrupts the readability and organization of the code. In fact, what I would like you to do is to handle this in your own code according to each error type. In short, to discuss a better practice, the first thing you should do is to create a Dictionary<Exception, HttpStatusCode> in Some Extension where you perform the necessary mapping and replace the if-else blocks with a single line of code to complete the operation. I expect you to do this yourself :)

Using Middleware in Web Api

Where you should add these depends on the API and version you are using. For example, in a regular .NET Core project, you should add them to the startup.cs, while in a .NET 6 minimal API, you should use the program.cs file. However, in terms of the end result, we are adding them to the same place.

var app = builder.Build();
app.UseMiddleware<AuthorizationMiddleware>();
app.UseMiddleware<ExceptionMiddleware>();

In a .NET 6 minimal API, the usage mentioned above should be sufficient.

It should not be forgotten that middlewares will run in the order they are added, as mentioned earlier. One of the most crucial points is that middlewares are often perceived as layers that only work when the endpoint is reached, but this is a significant misconception. After your endpoint runs and the response is generated, these middlewares are processed sequentially once again. Therefore, it should be remembered that all the code following the await _next(context); line will also execute. Using an incorrect if-else block could lead to an infinite loop in the code.

--

--