ASP.NET Core uses the notion of a pipeline of middlewares. Each time a request is handled by ASP.NET, it goes through each middleware registered in the pipeline in turn. Each middleware will have an opportunity to inspect the request and do some work, decide if the pipeline execution should continue, and if so do some more work after the rest of the pipeline has executed.
You can think of this pipeline as a sequence of middlewares where they are first executed in the order they are defined, and then in the reverse order. For example if you have middleware A and middleware B, A will have a chance to inspect the request, then call B which will do the same, and after B finishes, A can do some extra work.
The middleware pipeline is configured in the Startup.cs
‘s appropriately named Configure
method. Here’s an example of a simple pipeline that uses MVC:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IdentityDbContext dbContext)
{
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
The DeveloperExceptionPage
, StaticFiles
and Mvc
are all middlewares and they run in the order they are defined. A good example of this back and forth nature of the pipeline is how the DeveloperExceptionPage
middleware handles exceptions raised in the middlewares below it. If an exception is raised in Mvc
the DeveloperExceptionPage
will have a chance to write a response with information about that exception.
There’s an extension method in IApplicationBuilder
that lets us write a middleware right in the Configure
method:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.Use(async (HttpContext context, Func<Task> next) =>
{
//do work before the invoking the rest of the pipeline
await next.Invoke(); //let the rest of the pipeline run
//do work after the rest of the pipeline has run
});
//...
}
You can certainly use this method to create your own middleware. However, this is not what this blog post is about. This blog post is a walkthrough on how you can create your own app.UseMyMiddleware()
. Specifically we’ll be creating a custom middleware that writes information about the requests that the ASP.NET application receives to a file on disk. We’ll call this middleware MyFileLoggerMiddleware
.
The options class
In case you need you middleware to be configurable you should create an options class. This is just a plain class with properties that you want to make available to your middleware when it’s executing.
For our simple example we’ll create an options class that will contain only one property. That property will store the path to the file that we want we want to log information to.
Again, it’s just a class, there’s no requirement to implement an interface or inherit from a specific class:
public class MyFileLoggerOptions
{
public string FileName {get; set;}
}
The middlweare
The middleware itself is also not required to implement any interface or inherit from any class. However, it does need to have a specific constructor and a method with a specific signature.
A middleware requires a constructor with at least one parameter of type RequestDelegate
. A RequestDelegate is just the definition of a function signature:
public delegate Task RequestDelegate(HttpContext context);
It’s function that receives an HttpContext
and returns a Task
. What it really represents in your middleware is the next middleware in the pipeline. By executing it you are effectively executing the rest of the pipeline, since all the middlewares are chained this way.
I mentioned that a middleware required at least one parameter. That’s because if you want to pass options to your middleware these will be made available in the middleware’s constructor as a second parameter of type IOptions<YourOptionsType>
. In this case because we want to pass options to our middleware, and our options class is MyFileLoggerOptions
, the second parameter type is IOptions<MyFileLoggerOptions>
.
You’ll want to grab a reference to these two parameters, here’s how that looks like in this example:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
public class MyFileLoggerMiddleware
{
private readonly RequestDelegate _next;
private readonly MyFileLoggerOptions _options;
public MyFileLoggerMiddleware(RequestDelegate next, IOptions<MyFileLoggerOptions> options)
{
_next = next;
_options = options.Value;
}
//...
Next comes the the method that gets invoked when the middleware is executed. That method is appropriately named Invoke
:
public async Task Invoke(HttpContext context)
{
var request = context.Request;
var requestLogMessage = $"REQUEST:\n{request.Method} - {request.Path.Value}{request.QueryString}";
requestLogMessage+= $"\nContentType: {request.ContentType ?? "Not specified"}";
requestLogMessage+= $"\nHost: {request.Host}";
File.AppendAllText(_options.FileName, $"{DateTime.Now.ToString("s")}\n{requestLogMessage}");
await _next(context);
var response = context.Response;
var responseLogMessage = $"\nRESPONSE:\nStatus Code: {response.StatusCode}";
File.AppendAllText(_options.FileName, $"{responseLogMessage}\n\n");
}
Notice that this method’s signature matches the signature defined in RequestDelegate
, which should not be a surprise since when you call await _next(context)
you are actually executing the next middleware’s Invoke
function.
The rest of the code just writes some information about the Request and corresponding Response to a file on disk. If you add this middleware as the first in the pipeline you’ll get the status code of the response that was set by the other middlewares later in the pipeline.
Defining a custom .UseMyMiddleware
extension method
If you want to stop here you can already use the middleware, just go to your Configure method in Startup.cs
and add the middleware to the pipeline by doing this:
app.UseMiddleware<MyFileLoggerMiddleware>(Options.Create(new MyFileLoggerOptions{
FileName = Path.Combine(env.ContentRootPath, "logFile.txt")
}));
The only thing to be aware here is that you have to use Options.Create
and pass in an instance of the options class.
However to be able to just do app.UseMyFileLogger
we need to create an extension method targeting IApplicationBuilder
, here’s how we can do that:
public static class MyFileLoggerMiddlewareExtensions
{
public static IApplicationBuilder UseMyFileLogger(this IApplicationBuilder app, MyFileLoggerOptions options)
{
return app.UseMiddleware<MyFileLoggerMiddleware>(Options.Create(options));
}
}
To use it in the Configure method in Startup.cs you can now simply do:
app.UseMyFileLogger(new MyFileLoggerOptions {
FileName = Path.Combine(env.ContentRootPath, "log.txt")
});
This was just an example of what you can do with a middleware, albeit probably not a very useful one since there are proper logging frameworks you should be using instead, for example serilog. However, it illustrates nicely how the middlewares interact with each other and how you can create your own.