Before going head first into code, it's good to understand what middleware is. Within the domain of web applications, middleware operates as an intermediary layer, managing common tasks between distinct endpoints. With that concept in mind, let's explore three reasons why using middleware in your application can be advantageous:
Middleware enables breaking down complex tasks into reusable components, promoting a modular architecture that leads to code reuse, separation of concerns, and easy adaptability to changes.
Middleware manages the flow of operations in a consistent manner, ensuring that tasks like security, authentication, and error handling are uniformly applied to requests.
Middleware's flexibility allows for easy addition, modification, or removal of functionalities without disrupting the core application logic. It also facilitates third-party integrations, expanding application capabilities.
I'll start with the ASP.NET Core API template: Let's create a simple project, with OpenAPI for quick manual testing. If you did everything right, you will have a simple WeatherAPI project, and by running it you will see something like this: By executing this endpoint, you will receive random weather data—a nifty feature for initial exploration. In the next section, we'll transform this endpoint to accept user input. Not only that, but we'll also intentionally introduce some bugs along the way. These issues will give us a better perspective on how middleware comes into play.
This is our new endpoint. I'll explain some of the changes later:
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get(string city)
{
var cityId = Cities[city];
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
CityId = cityId,
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
I've added the query parameter city
. Using the city parameter, we get cityId
from the internal dictionary Cities
.
Added the CityId
property on WeatherForecast
Filled the property with the dictionary value Cities; is a simple dictionary with the city name and an id.
And don't worry, this is not my top 5 of the best cities in the world.
csharp
private static readonly Dictionary<string, int> Cities = new()
{
{ "Rio de Janeiro", 1 },
{ "Gothenburg", 2 },
{ "New York", 3 },
{ "Tokyo", 4 },
{ "Venice", 5 }
};
Now, let's quickly test this to see what happens...
If I execute the code and make a request with the city parameter equal to Tokyo
, I get a response similar to this:
[
{
"date": "2023-08-24",
"temperatureC": 32,
"temperatureF": 89,
"summary": "Cool",
"cityId": 3
},
{
"date": "2023-08-25",
"temperatureC": -7,
"temperatureF": 20,
"summary": "Hot",
"cityId": 3
}
.
.
.
]
Let's ignore this drastic temperature change from one day to another and test it again, but now with a city that does not exist, or even with tokyo
all lowercase.
If we do that, we will encounter an exception and receive an Internal Server Error response, complete with the entire stack trace in the body. Something similar to this:
Exposing a stack trace can be a bad user experience, and it might also present security risks. Stack traces can inform attackers about your application's inner workings and potential vulnerabilities. To fix that, we can implement our ErrorHandlingMiddleware
, which will catch unhandled exceptions and return a more secure and user-friendly message.
To solve this issue, we can wrap the endpoint invocation in a try-catch
block and provide a user-friendly response if any exception occurs. The following ErrorHandlingMiddleware
does that. You can put this class anywhere in your project. I usually create a "Middlewares" folder.
namespace CrazyClouds.Middlewares
{
public class ErrorHandlingMiddleware
{
private readonly ILogger<ErrorHandlingMiddleware> _logger;
private readonly RequestDelegate _next;
public ErrorHandlingMiddleware(
ILogger<ErrorHandlingMiddleware> logger,
RequestDelegate next)
{
_logger = logger;
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
// Call the next middleware in the pipeline
await _next(context);
}
catch (Exception ex)
{
// Log the exception
_logger.LogError("unexpected error", ex);
// Prepare user-friendly response
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "text";
await context.Response.WriteAsync("An unexpected error occurred.");
}
}
}
}
Now that we have the ErrorHandlingMiddleware
class defined, we can inject it into our application by adding the following line in Program.cs
:
app.UseMiddleware<ErrorHandlingMiddleware>(); // Our new Middleware
app.UseAuthorization();
app.MapControllers();
app.Run();
If you take a closer look, we have been using middlewares since the start of the project. Both methods, UseAuthorization()
and MapControllers()
are pre-defined middlewares that execute specific tasks such as authorization/authentication and routing, respectively.
Important! In .NET Core, the order in which you add middleware components to the pipeline determines the sequence in which they are executed when processing an incoming request. This concept is often called "first added, first executed." So if we had the following middlewares in our application:
app.UseMiddleware<Middleware1>();
app.UseMiddleware<Middleware2>();
app.UseMiddleware<Middleware3>();
Middleware1 will be executed first, followed by Middleware2, and then Middleware3. So it's important to add the ErrorHandlingMiddleware
at the beginning of the pipeline so that it also catches any error that might occur in one of the other middlewares.
Now if we use our endpoint with an invalid input, we get the following response:
Status: 500 - Internal Server Error An unexpected error occurred.
It's much better than our previous response. Of course, in a real application, we should not return an Unexpected Error when a user provides an invalid input. We could change this middleware to catch a BadHttpRequestException
and then return a Bad Request response, for example. But I will leave this improvement for you to implement =)
I hope this blog post was helpful to introduce the key benefits of middlewares to you. By delegating common and contained tasks to middlewares you can design clean, modular and easily maintainable software.
Happy Coding!"