Customizing ASP.NET Core Part 6: Middleware
ASP.NET Core comes with some great built-in middleware that allows for customization of your app. Code along with an ASP.NET Core expert and learn to wield this power.
Join the DZone community and get the full member experience.
Join For FreeWow, it is already the sixth part of this series. In this post, I'm going to write about middleware and how you can use them to customize your app a little more. I quickly go through the basics about middleware and then I'll write about some more specials things you can do with middleware.
The Series Topics
- Customizing ASP.NET Core Part 1: Logging
- Customizing ASP.NET Core Part 2: Configuration
- Customizing ASP.NET Core Part 3: Dependency Injection
- Customizing ASP.NET Core Part 4: HTTPS
- Customizing ASP.NET Core Part 5: HostedServices
- Customizing ASP.NET Core Part 6: MiddleWares - This article
- Customizing ASP.NET Core Part 7: OutputFormatter
- Customizing ASP.NET Core Part 8: ModelBinder
- Customizing ASP.NET Core Part 9: ActionFilter
- Customizing ASP.NET Core Part 10: TagHelpers
About MiddleWares
Most of you already know what middlewares are, but some of you maybe don't. Even if you've used ASP.NET Core for a while, you don't really need to know any real details about middlewares, because they are mostly hidden behind nicely named extension methods like UseMvc()
, UseAuthentication()
, UseDeveloperExceptionPage()
, and so on. Every time you call a Use
-method in the Startup.cs
in the Configure
method, you'll implicitly use at least one or maybe more middlewares.
A middleware is a piece of code that handles the request pipeline. Imagine the request pipeline as a huge tube where you can call something in and where an echo comes back. The middlewares are responsible for creating this echo or for manipulating the sound, to enrich the information or to handle the source sound or to handle the echo.
Middlewares are executed in the order they are configured. The first configured middleware is the first that gets executed.
In an ASP.NET Core web, if the client requests an image or any other static file, the StaticFileMiddleWare
searches for that resource and returns that resource if it finds one. If not, this middleware does nothing except to call the next one. If there is no last middleware that handles the request pipeline, the request returns nothing. The MvcMiddleWare
also checks the requested resource, tries to map it to a configured route, executes the controller, creates a view, and returns an HTML or Web API result. If MvcMiddleWare
doesn't find a matching controller, it will return a result anyway, in this case, a 404 Status result. It returns an echo in any case. This is why MvcMiddleWare
is the last configured middleware.
(Image source: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.1)
An exception handling middleware usually is one of the first middlewares to get configured, but it is not because it gets executed not first but last. The first configured middleware is also the last one if the echo comes back down the tube. An exception handling middleware validates the result and displays a possible exception in a browser and client-friendly way. This is where a runtime error gets a 500 Status.
You are able to see how the pipeline is executed if you create an empty ASP.NET Core application. I usually use the console and the .NET CLI tools:
dotnet new web -n MiddleWaresSample -o MiddleWaresSample
cd MiddleWaresSample
Open the Startup.cs file with your favorite editor. It should be pretty empty compared to a regular ASP.NET Core application:
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}
}
There is the DeveloperExceptionPageMiddleware
used and a special lambda middleware that only writes "Hello World!" to the response stream. The response stream is the echo I wrote about previously. This special middleware stops the pipeline and returns something as an echo. So it is the last one.
Leave this middleware and add the following lines right before app.Run()
:
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("===");
await next();
await context.Response.WriteAsync("===");
});
app.Use(async (context, next) =>
{
await context.Response.WriteAsync(">>>>>> ");
await next();
await context.Response.WriteAsync(" <<<<<<");
});
These two calls of app.Use()
also create two lambda middlewares, but this time the middlewares are calling the next ones. Each middleware knows the next one and calls it. Both middlewares are writing to the response stream before and after the next middleware is called. This should demonstrate how the pipeline works. Before the next middleware is called, the actual request is handled and after the next middleware is called, the response (echo) is handled.
If you now run the application (using dotnet run
) and open the displayed URL in the browser, you should see a plain text result like this:
===>>>>>> Hello World! <<<<<<===
Does this make sense to you? If yes, let's see how to use this concept to add some additional functionality to the request pipeline.
Writing a Custom Middleware
ASP.NET Core is based on middlewares. All the logic that gets executed during a request is somehow based on middleware. So we are able to use this to add custom functionality to the web. We want to know the execution time of every request that goes through the request pipeline. I do this by creating and starting a Stopwatch
before the next middleware is called and by stopping the measuring of the execution time after the next middleware is called:
app.Use(async (context, next) =>
{
var s = new Stopwatch();
s.Start();
// execute the rest of the pipeline
await next();
s.Stop(); //stop measuring
var result = s.ElapsedMilliseconds;
// write out the milliseconds needed
await context.Response.WriteAsync($"Time needed: {result }");
});
After that, I write out the elapsed milliseconds to the response stream.
If you write some more middlewares like the Configure
method in the Startup.cs
file, it get's pretty messy. This is why most middlewares are written as separate classes. This could look like this:
public class StopwatchMiddleWare
{
private readonly RequestDelegate _next;
public StopwatchMiddleWare(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
var s = new Stopwatch();
s.Start();
// execute the rest of the pipeline
await next();
s.Stop(); //stop measuring
var result = s.ElapsedMilliseconds;
// write out the milliseconds needed
await context.Response.WriteAsync($"Time needed: {result }");
}
}
This way we get the next middleware via the constructor and the current context in the Invoke()
method.
Note: The middleware is initialized when the application starts and exists once during the application's lifetime. The constructor gets called once. On the other hand, the
Invoke()
method is called once per request.
To use this middleware, there is a generic UseMiddleware()
method available that you can use in the configure method:
app.UseMiddleware<StopwatchMiddleWare>();
The more elegant way is to create an extensions method that encapsulates this call:
public static class StopwatchMiddleWareExtension
{
public static IApplicationBuilder UseStopwatch(this IApplicationBuilder app)
{
app.UseMiddleware<StopwatchMiddleWare>();
return app;
}
}
Now you can simply call it like this:
app.useStopwatch();
This is the way you can provide additional functionality to an ASP.NET Core web app through the request pipeline. You are able to manipulate the request or even the response using middlewares.
TheAuthenticationMiddleWare
, for example, tries to request user information from the request. If it doesn't find some it asks the client about it by sending a specific response back to the client. If it finds some, it adds the information to the request context and makes it available to the entire application this way.
What Else Can We Do Using Middleware?
Did you know that you can divert the request pipeline into two or more branches?
The next snippet shows how to create branches based on specific paths:
app.Map("/map1", app1 =>
{
// some more middlewares
app1.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
});
app.Map("/map2", app2 =>
{
// some more middlewares
app2.Run(async context =>
{
await context.Response.WriteAsync("Map Test 2");
});
});
// some more middlewares
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hello World!");
});
The path "/map1" is a specific branch that continues the request pipeline inside. The same with "/map2". Both maps have their own middleware configurations inside. All other un-specified paths will follow the main branch.
There's also a MapWhen()
method to branch the pipeline based on a condition instead of branch based on a path:
public void Configure(IApplicationBuilder app)
{
app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
app1 =>
{
// some more middlewares
app1.Run(async context =>
{
await context.Response.WriteAsync("MapBranch Test");
});
});
// some more middlewares
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
});
}
You can create conditions based on configuration values or, as shown here, based on properties of the request context. In this case, a query string property is used. You can use HTTP headers, form properties, or any other property of the request context.
You are also able to nest the maps to create child and grandchild branches if needed.
Map()
or MapWhen()
is used to provide a special API or a resource-based specific path or specific condition. The ASP.NET Core HealthCheck API is used like this. It first uses MapWhen()
to specify the port to use and then the Map()
to set the path for the HealthCheck API, or it uses Map()
only if no port is specified. At the end, the HealthCheckMiddleware is used:
private static void UseHealthChecksCore(IApplicationBuilder app, PathString path, int? port, object[] args)
{
if (port == null)
{
app.Map(path, b => b.UseMiddleware<HealthCheckMiddleware>(args));
}
else
{
app.MapWhen(
c => c.Connection.LocalPort == port,
b0 => b0.Map(path, b1 => b1.UseMiddleware<HealthCheckMiddleware>(args)));
}
}
(See here on GitHub)
Conclusion
Most of the ASP.NET Core features are based on middlewares and we are able to extend ASP.NET Core by creating our own middlewares.
In the next two articles, I will have a look into different data types and how to handle them. I will create API outputs with any format and data types I want and except data of any type and format.
Published at DZone with permission of Juergen Gutsch, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments