An Absolute Beginner's Tutorial on Middleware in ASP.NET Core/MVC
Let's see how middleware plays an important part in the request-response pipeline and how we can write and plug in our custom middleware.
Join the DZone community and get the full member experience.
Join For Free
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 the request-response pipeline and how we can write and plug-in our custom middleware.
background
before we can get into the what middleware is and the value it brings, we need to understand how the request-response works in a classic asp.net model. in earlier days, the request and response objects in asp.net were very big and had a 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 a community-owned standard called open web interface for .net (owin) since the older asp.net applications were dependent on system. web dll, which internally had a very tight coupling with iis, it was very difficult to decouple the applications from web servers. to circumvent this problem, owin defines to remove the dependency of web applications on system.web assembly so that the coupling with a web server (iis) gets removed.
owin primarily defines the following actors in its specifications:
- server — the http server 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 have been multiple implementations for this in 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 will then get passed back to the web server in the opposite order back to the web server, which can then 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 the response travels back to the 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 through 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 the middleware pipeline
- monitor the request and let it pass through to 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 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 to 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
. the second one to 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, its good to highlight the point that middleware is an implementation of pipes and filter patterns. pipes and filter patterns say that if we need to perform a complex processing that involves a series of separate activities, 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. there are two ways to define our custom middleware:
- custom middleware class
- inline custom middleware
custom middleware class
the 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)
{
// todo: our logic that we need to put in when the request is coming in
// call the next delegate/middleware in the pipeline
await _next(context);
// todo: our logic that we need to put in when the response is going back
}
}
what this class does is gets called once the request reached to this middleware. the 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)
{
// our custom middleware
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 that 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)
{
// our custom middleware
app.use(async (context, next) =>
{
// todo: our logic that we need to put in when the request is coming in
// call the next delegate/middleware in the pipeline
await next();
// todo: our logic that we need to put in when the response is going back
});
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 to 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;
// call the next delegate/middleware in the pipeline
await next();
datetime endtime = datetime.now;
timespan responsetime = endtime - starttime;
// log the response time here using your favorite logging or telemetry module
});
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. for not, 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 the 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 a 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")
{
// call the next delegate/middleware in the pipeline
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;
// call the next delegate/middleware in the pipeline
await next();
datetime endtime = datetime.now;
timespan responsetime = endtime - starttime;
// log the response time here using your favorite logging or telemetry module
});
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 an 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 the context of asp.net core, the concept of middleware is same in all mvc implementations that are adhering to owin standards.
point 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
download the sample code for the article here: owintest
Published at DZone with permission of Rahul Rajat Singh, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments