Introduction
In this article, we will try to understand the concept of middleware in ASP.NET core. We will see how middleware plays an important part in request response pipeline and how we can write and plug-in our custom middleware.
Background
Before we could get into what middleware is and the value it brings, we need to understand how the request response worked in classic ASP.NET model. In earlier days, the request and response objects in ASP.NET were very big and had very tight coupling with IIS. This was a problem because some of the values in these objects are filled by the IIS request-response pipeline and unit testing such bloated objects was a very big challenge.
So the first problem that needed to be solved was to decouple the applications from web servers. This was very nicely defined by community owned standards called Open Web Interface for .NET (OWIN). Since the older ASP.NET applications were dependent on System.Web
DLL which internally had very tight coupling with IIS, it was very difficult to decouple the applications from web servers. To circumvent this problem, OWIN defines is to remove the dependency of web applications on System.web
assembly so that the coupling with web server (IIS) gets removed.
OWIN primarily defines the following actors in its specifications:
- Server — The HTTP server that directly communicates with the client and then uses OWIN semantics to process requests. Servers may require an adapter layer that converts to OWIN semantics.
- Web Framework — A self-contained component on top of OWIN exposing its own object model or API that applications may use to facilitate request processing. Web Frameworks may require an adapter layer that converts from OWIN semantics.
- Web Application — A specific application, possibly built on top of a Web Framework, which is run using OWIN compatible Servers.
- Middleware — Pass through components that form a pipeline between a server and application to inspect, route, or modify request and response messages for a specific purpose.
- Host — The process an application and server execute inside of, primarily responsible for application startup. Some Servers are also Hosts.
Since OWIN is just a standard, there are multiple implementations for this in the last few years starting from Katana to the present day implementation in ASP.NET core. We will now focus on how the middleware implementation looks like in ASP.NET core.
Before that, let's try to understand what a middleware is. For the developer coming from the ASP.NET world, the concept of HTTPModule
and HTTPHander
is fairly familiar. These are used to intercept the request-response pipeline and implement our custom logic by writing custom modules or handlers. In the OWIN world, the same thing is achieved by the middleware.
OWIN specifies that the request coming from web server to the web application has to pass through multiple components in a pipeline sort of fashion where each component can inspect, redirect, modify or provide a response for this incoming request. The response then will get passed back to the web server in the opposite order back to the web server which then can be served back to the user. The following image visualizes this concept:
If we look at the above diagram, we can see that the request passes through a chain of middleware and then some middleware decides to provide a response for the request and then response travels back to web server passing through all the same middleware it passed through while request. So a middleware typically can:
- Process the request and generate the response
- Monitor the request and let it pass thorough to next middleware in line
- Monitor the request, modify it and then let it pass through to next middleware in line
If we try to find the middleware with actual use cases defined above:
- Process the request and generate the response: MVC itself is a middleware that typically gets configured in the very end of middleware pipeline
- Monitor the request and let it pass through to the next middleware in line: Logging middleware which simply logs the request and response details
- Monitor the request, modify it and then let it pass through to the next middleware in line: Routing and Authentication module where we monitor the request decide which controller to call (routing) and perhaps update the identity and Principle for authorization (Auth-Auth).
Using the Code
In this article, we will create 2 owin middleware. First one will demonstrate the scenario where we are not altering the request. For this, we will simply log the request and response time in the log - TimingMiddleware
. Second one will check the incoming response, find a specific header value to determine which tenant is calling the code and then returning back if the tenant is not valid - MyTenantValidator
.
Note: Before we get started with the sample implementation, it's good to highlight the point that middleware is an implementation of pipes and filter pattern. Pipes and filter pattern says that if we need to performs a complex processing that involves a series of separate activity, it's better to separate out each activity as a separate task that can be reused. This gives us benefits in terms of reusability, performance and scalability.
Let's start by looking at how the middleware class definition should look like. There are two ways to define our custom middleware:
- Custom middleware class
- Inline custom middleware
Custom Middleware Class
First way is to have a custom class containing our middleware logic.
public class MyCustomMiddleware
{
private readonly RequestDelegate _next;
public MyCustomMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
await _next(context);
}
}
What this class does is that it gets called once the request reached this middleware. InvokeAsync
function will get called and the current HttpContext
will be passed to it. We can then execute our custom logic using this context and then call the next middleware in the pipeline. Once the request is processed by all middleware after this middleware, the response is generated and the response will follow the reverse chain and the function will reach after our _next
call where we can put the logic that we want to execute before the response goes back to the previous middleware.
For our middleware to get into the pipeline, we need to use the Configure
method in our Startup
class to hook our middleware.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMiddleware<MyCustomMiddleware>();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
The above code shows how we have hooked in our custom middleware as the first middleware in the pipeline. The middleware will be called in the same order as they are hooked in this method. So in the above code, our middleware will be called first and the MVC middleware will be the last one to get called.
Inline Custom Middleware
The inline custom middleware is directly defined in the Configure
method. The following code shows how to achieve this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.Use(async (context, next) =>
{
await next();
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
The end result will be the same for both approaches. So if our middleware is doing some trivial things that do not impact the readability of code if we put as inline, we could create the inline custom middleware. If the code that we want has significant code and logic in our middleware, we should use the custom middleware class to define our middleware.
Coming back to the middleware that we are going to implement, we will use the inline approach to define the TimingMiddleware
and the custom class approach to define the MyTenantValidator
.
Implementing the TimingMiddleware
The sole purpose of this middleware is to inspect the request and response and log the time that this current request took to process. Let's define it as inline middleware. The following code shows how this can be done.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.Use(async (context, next) =>
{
DateTime startTime = DateTime.Now;
await next();
DateTime endTime = DateTime.Now;
TimeSpan responseTime = endTime - startTime;
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
We have hooked this middleware just before MVC middleware so that we can measure the time our request processing is taking. It is defined after UseStaticFiles
middleware so that this middleware will not get invoked for all static files that are being served from our application.
Implementing the MyTenantValidator
Now let's implement a middleware that will take care of tenant verification. It will check for the incoming header and if the tenant is not valid, it will stop the request processing.
Note: For the sake of simplicity, I will be looking for a hard coded tenant id value. But in real world applications, this approach should never be used. This is being done only for demonstration purposes. Note that we will be using the tenant id value as 12345678
.
This middleware will be written in its separate class. The logic is simple, check for the headers in incoming request. If the header matches the hard coded tenant id, let the request proceed to the next middleware else terminate the request by sending response from this middleware itself. Let's look at the code of this middleware.
public class MyTenantValidator
{
private readonly RequestDelegate _next;
public MyTenantValidator(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
StringValues authorizationToken;
context.Request.Headers.TryGetValue("x-tenant-id", out authorizationToken);
if(authorizationToken.Count > 0 && authorizationToken[0] == "12345678")
{
await _next(context);
}
else
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
await context.Response.WriteAsync("Invalid calling tenant");
return;
}
}
}
Now let's register this middleware in our Startup
class.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMiddleware<MyTenantValidator>();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.Use(async (context, next) =>
{
DateTime startTime = DateTime.Now;
await next();
DateTime endTime = DateTime.Now;
TimeSpan responseTime = endTime - startTime;
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
With this code in place, if we try to run the application, we can see the response as error.
To circumvent this issue, we need to pass the tenant id in the header.
With this change, when we access the application again, we should be able to browse our application.
Note: Even though we were talking in context of ASP.NET core, the concept of middleware is the same in all MVC implementations that are adhering to OWIN standards.
Points of Interest
In this article, we talked about ASP.NET core middleware. We looked at what middleware is and how we can write our own custom middleware. This article has been written from a beginner's perspective. I hope this has been somewhat informative.
References
History
- 12th September, 2018: First version