This post is part 2 of a four-part series. You might want to read Part 1 first.
In the last post, we talked about what Middleware is, what it's for, and simple ways of including it in our ASP.NET 6 app's pipeline.
In this post, we're going to expand on these fundamentals to build a few custom Middleware classes.
The Sample Project
Don't forget to check out the sample project over on GitHub:
The Standard Middleware Architecture
Unlike what we did in Part 1, most of the time we want to have our Middleware be separate classes, and not just additional lines in our Program.cs file.
Remember this middleware from Part 1? The one that returned a response consisting of "Hello Dear Readers!"?
//...Rest of Program.cs
app.Run(async context =>
{
await context.Response.WriteAsync("Hello Dear Readers!");
});
//...Rest of Program.cs
Let's create a custom middleware class that does the same thing.
Basics of a Middleware Class
Here's a basic empty class we'll use for this middleware:
namespace MiddlewareNET6Demo.Middleware
{
public class SimpleResponseMiddleware
{
}
}
A middleware class consists of three parts. First, any middleware class in ASP.NET 6 must include a private instance of RequestDelegate
which is populated by the class's constructor. Remember that RequestDelegate
represents the next piece of middleware in the pipeline:
namespace MiddlewareNET6Demo.Middleware
{
private readonly RequestDelegate _next;
public SimpleResponseMiddleware(RequestDelegate next)
{
_next = next;
}
}
Second, the class must have an async method InvokeAsync()
which takes an instance of HttpContext
as its first parameter.
public class SimpleResponseMiddleware
{
private readonly RequestDelegate _next;
public SimpleResponseMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
//...Implementation
}
}
Third, the middleware must have its own unique implementation. For this middleware, all we want to do is return a custom response:
public class SimpleResponseMiddleware
{
private readonly RequestDelegate _next;
public SimpleResponseMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
await context.Response.WriteAsync("Hello Dear Readers!");
}
}
NOTE: within the InvokeAsync()
method, most middleware will have a call to await next(context);
any middleware that does not do this will be a terminal middleware, because the pipeline beyond that point will not be run. Since our SimpleResponseMiddleware
does, in fact, want to stop the pipeline processing, it does not call await next(context)
.
Adding Middleware to the Pipeline
At this point, it is possible for us to wire up this middleware class to our app in the Program.cs file using the UseMiddleware<T>()
method:
//...Rest of Program.cs
app.UseMiddleware<LayoutMiddleware>();
//...Rest of Program.cs
For very simple middleware classes, this is sufficient. However, often we use extension methods instead of UseMiddleware<T>()
because they provide us with another layer of abstraction:
namespace MiddlewareNET6Demo.Extensions
{
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseSimpleResponseMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<SimpleResponseMiddleware>();
}
}
}
We would then use that extension method like so:
//...Rest of Program.cs
app.UseSimpleResponseMiddleware();
//...Rest of Program.cs
Either way of doing this is correct. I personally prefer the clarity and readability of the extension method route, but you can go with whatever you like better.
Building a Logging Middleware
One of the most common scenarios for middleware is logging, specifically logging of things like the request path or headers, culture, or response body.
The LoggingService Class
We're going to build a logging middleware class that does two things:
- Log the request path.
- Log the distinct response headers.
First, we must create an interfaceILoggingService
and class LoggingService
.
namespace MiddlewareNET6Demo.Logging
{
public class LoggingService : ILoggingService
{
public void Log(LogLevel logLevel, string message)
{
//...Implementation for logging
}
}
public interface ILoggingService
{
public void Log(LogLevel level, string message);
}
}
We then need to add LoggingService
to the Services collection of the app in Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddTransient<ILoggingService, LoggingService>();
//...Rest of Program.cs
Middleware classes can have services injected into them, just like normal classes.
The LoggingMiddleware Class
Now we need a LoggingMiddleware
class which actually does the logging. First, let's create a skeleton for the class LoggingMiddleware
, which accepts an instance of ILoggingService
as a parameter in the constructor:
namespace MiddlewareNET6Demo.Middleware
{
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILoggingService _logger;
public LoggingMiddleware(RequestDelegate next, ILoggingService logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
//...Implementation
}
}
}
Remember that we are recording the request's path, and the response's unique headers. That means we will have code on either side of the call to await next(context)
.
namespace MiddlewareNET6Demo.Middleware
{
public class LoggingMiddleware
{
//...Rest of implementation
public async Task InvokeAsync(HttpContext context)
{
//Log the incoming request path
_logger.Log(LogLevel.Information, context.Request.Path);
//Invoke the next middleware in the pipeline
await _next(context);
//Get distinct response headers
var uniqueResponseHeaders
= context.Response.Headers
.Select(x => x.Key)
.Distinct();
//Log these headers
_logger.Log(LogLevel.Information, string.Join(", ", uniqueResponseHeaders));
}
}
}
Adding LoggingMiddleware to the Pipeline
Since I prefer to use extension methods to add middleware to the pipeline, let's create a new one:
namespace MiddlewareNET6Demo.Extensions
{
public static class MiddlewareExtensions
{
//...Rest of implementation
public static IApplicationBuilder UseLoggingMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<LoggingMiddleware>();
}
}
}
Finally, we need to call our new extension method in Program.cs to add the LoggingMiddleware
class to the pipeline:
//...Rest of Program.cs
app.UseLoggingMiddleware();
//...Rest of Program.cs
When we run the app, we can see (by setting breakpoints) that the code will correctly log both the request path and the response headers.
Coming Up Next!
In the next post in this series, we discuss the order of operations for Middleware, show a new Middleware that logs execution time, and discuss a few of the common pitfalls that might happen when creating your Middleware pipeline. Stick around!
Happy Coding!