An enterprise developer moving to ASP.NET Core must feel a bit like a character in Asimov’s “The Gods Themselves”. In the book, humans contact aliens who live in an alternate universe with different physical laws. The landscape of ASP.NET Core is familiar. You can still find controllers, views, models, DbContext classes, script files, and CSS. But, the infrastructure and the laws are different.
For example, the hierarchy of XML configuration files in this new world is gone. The twin backbones of HTTP processing, HTTP Modules and HTTP Handlers, are also gone. In this post, we’ll talk about the replacement for modules and handlers, which is middleware.
Previous versions of ASP.NET gave us a customizable but rather inflexible HTTP processing pipeline. This pipeline allowed us to install HTTP modules and execute logic for cross cutting concerns like logging, authentication, and session management. Each module had the ability to subscribe to preset events raised by ASP.NET. When implementing a logger, for example, you might subscribe to the BeginRequest and EndRequest events and calculate the amount of time spent in between. One of the tricks in implementing a module was knowing the order of events in the pipeline so you could subscribe to an event and inspect an HTTP message at the right time. Catch a too-early event, and you might not know the user’s identity. Catch a too-late event and a handler might have already changed a record in the database.
Although the old model of HTTP processing served us well for over a decade, ASP.NET Core brings us a new pipeline based on middleware. The new pipeline is completely ours to configure and customize. During the startup of our application, we’ll use code to tell ASP.NET which pieces of middleware we want in the application, and the order in which the middleware should execute.
Once an HTTP request arrives at the ASP.NET server, the server will pass the request to the first piece of middleware in our application. Each piece of middleware has the option of creating a response, or calling into the next piece of middleware. One way to visualize the middleware is to think of a stack of components in your application. The stack builds a bi-directional pipeline. The first component will see every incoming request. If the first component passes a request to the next component in the stack, the first component will eventually see the response coming out of a component further up the stack.
A piece of middleware that comes late in the stack may never see a request if the previous piece of middleware does not pass the request along. This might happen, for example, because a piece of middleware you use for authorization checks finds out that the current user doesn’t have access to the application.
It’s important to know that some pieces of middleware will never create a response and only exist to implement cross cutting concerns. For example, there is a middleware component to transform an authentication token into a user identity, and another middleware component to add CORS headers into an outgoing response. Microsoft and other third parties provide us with hundreds of middleware components.
Other pieces of middleware will sometimes jump in to create or override an HTTP response at the appropriate time. For example, Microsoft provides a piece of middleware that will catch unhandled exceptions in the pipeline and create a “developer friendly” HTML response with a stack trace. A different piece of middleware will map the exception to a “user friendly” error page. You can configure different middleware pipelines for different environments, such as development versus production.
Another way to visualize the middleware pipeline is to think of a chain of responsibility.
Each piece of middleware has a specific focus. A piece of middleware to log every request would appear early in the chain to ensure the logging middleware sees every request. A later piece of middleware might even route a request outside of the middleware and into another framework or another set of components, like forwarding a request to the MVC framework for processing.
This article doesn’t provide extensive technical coverage of middleware. However, to give you a taste of what the code looks like, let’s see what it looks like to configure existing middleware and create a new middleware component.
Adding middleware to an application happens in the Configure method of the startup class for an application. The Configure method is injectable, meaning you can ask for any other services you need, but the one service you’ll always need is the IApplicationBuilder service. The application builder allows us to configure middleware. Most middleware will live in a NuGet package. Each NuGet package will include extension methods for IApplicationBuilder to add a middleware component using a simple method call. For example:
public void Configure(IApplicationBuilder app) { app.UseDeveloperExceptionPage(); app.UseFileServer(); app.UseCookieAuthentication(AppCookieAuthentication.Options); app.UseMvc(); }
Notice the extension methods all start with the word Use. The above code would create a pipeline with 4 pieces of middleware. All the above middleware is provided by Microsoft. The first piece of middleware displays a “developer friendly” error page if there is an uncaught exception later in the pipeline. The second piece of middleware will serve up files from the file system when a request matches the file name and path. The third piece transforms an ASP.NET authentication cookie into a user identity. The final piece of middleware will send the request to the MVC framework where MVC will try to match the request to an MVC controller.
You can implement a middleware component as a class with a constructor and an Invoke method. ASP.NET will pass a reference to the next piece of middleware as as a RequestDelegate constructor parameter. Each HTTP transaction will pass through the Invoke method of the middleware.
The following piece of middleware will write a greeting like “Hello!” into the response if the request path starts with /hello. Otherwise, the middleware will call into the next component to produce a response, but add an HTTP header with the current greeting text.
public class SayHelloMiddleware { public SayHelloMiddleware(RequestDelegate next, SayHelloOptions options) { _options = options; _next = next; } public async Task Invoke(HttpContext context) { if (context.Request.Path.StartsWithSegments("/hello")) { await context.Response.WriteAsync(_options.GreetingText); } else { await _next(context); context.Response.Headers.Add("X-GREETING", _options.GreetingText); } } readonly RequestDelegate _next; readonly SayHelloOptions _options; }
Although this middleware is trivial, the example should give you an idea of what middleware can do. First, Invoke will receive an HttpContext object with access to the request and the response. You can inspect incoming headers and create outgoing headers. You can read the request body or write into the response. The logic inside Invoke can decide if you want to call the next piece of middleware or handle the response entirely in the current middleware. Note the name Invoke is the method ASP.NET will automatically look for (no interface implementation required), and is injectable.
I’ve personally found middleware to be liberating. The ability to explicitly configure every component of the HTTP processing pipeline makes it easy to know what is happening inside an application. The application is also as lean as possible because we can install only the features an application requires.
Middleware is one reason why ASP.NET Core can perform better and use less memory than its predecessors.
There are a few downsides to middleware.
First, I know many enterprises with custom HTTP modules. The services provided by these modules range from custom authentication and authorization logic, to session state management, to custom instrumentation. To use these services in ASP.NET Core the logic will need to move into middleware. I don’t see the port from modules to middleware as challenging, but the port is time consuming for such critical pieces of infrastructure. You’ll want to identify any custom modules (and handlers) an enterprise application relies on so the port happens early.
Secondly, I’ve seen developers struggle with the philosophy of middleware. Middleware components are highly asynchronous and often follow functional programming idioms. Also, there are no interfaces to guide middleware development as most of the contracts rely on convention instead of the compiler. All of these changes make some developers uncomfortable.
Thirdly, when developers use middleware they often struggle finding the right middleware to use. Microsoft distributes ASP.NET Core middleware in granular NuGet packages, meaning you have to know the middleware exists, then find and install the package, and then find the extension method to install the middleware. As ASP.NET Core has moved from release candidate to the current 1.1 release, there has been churn in the package names themselves, which has led to frustration in finding the right package name.
Expect to see middleware play an increasingly important role in the future. Not only will Microsoft and others create more middleware, but also expect the sophistication of middleware to increase. Future middleware will not only continue to replace IIS features like URL re-writing, but also change our application architecture by enabling additional frameworks and the ability to compose new behavior into an application.
Don’t underestimate the importance of porting existing logic into middleware and the impact of a middleware on an application’s behavior.
ASP.NET Core and the Enterprise Part 3: Middleware (this one)