ASP.NET Core middleware (sometimes called middleware, and other times referred to as intermediate software or modules), and it’s also the first post of our series. The reason I chose this topic is to explain the ease and possibilities new technologies provide and also to dive a bit deeper into the subject.
I want to point out that during interviews, many people didn't fully understand this topic. Unfortunately, this leads to architectural mistakes, especially in the Presentation layer related to SRP (Single Responsibility Principle).
When ASP.NET Core was built, it was designed with a modular structure. You’ll understand the modular part as you read further. ASP.NET Core uses a pipeline architecture style (incoming requests pass through specific middleware, and each middleware performs its task. It’s not a design pattern or architectural pattern — it uses an architectural style, which I will discuss in another article). Thanks to the pipeline structure, we can create middleware and add it to the pipeline, and thus form the backbone of the application.
As a result, we can apply AOP (Aspect-Oriented Programming) features and add small modules, which helps us avoid code repetition and provides a flexible and expandable framework structure.
Of course, I know I have summarized this very briefly, but the goal is to explain the middleware structure. In another series, I will touch on topics that intersect with middleware, like self-hosting, OWIN, and built-in features.
Let’s explain
Before diving into the details, middleware has two main functions.
- It can check whether to call the next middleware in the pipeline.
- It can perform actions before or after the next middleware (Chain of Responsibility — COR).
ASP.NET Core is an abstract web framework. It provides an OWIN-based or application backbone that allows us to create a pipeline by attaching our application as middleware.
When more than one middleware is added, each middleware in the pipeline is connected like a chain, and they can call each other. Structurally, it uses the Chain of Responsibility pattern, often shortened to COR or chain (I’ll sometimes use the word chain).
ASP.NET Core creates a response to an incoming request and sends it back to users (clients). Middleware is responsible for handling these incoming requests and responses as part of their tasks.
Frameworks are abstract by design. If we don’t add the MVC (web app) component to the pipeline, the application won’t give any result because there’s no middleware in the pipeline to process or handle it.
To work with MVC, we need to integrate the MVC middleware into the ASP.NET Core pipeline. This allows us to get results from our Controller/Action requests (the Controller is executed, returns a specific ActionResult, and this ActionResult is executed, writing the result to HttpResponse. The user then sees this). Alternatively, we can add our custom middleware to process requests or responses and get results.
Actually, we don’t have an MVC middleware here. What we have is a Route middleware, which handles the incoming request by mapping the URI segments to RouteData. This later allows the execution of our Action using reflection.
What can we do with middleware?
In applications, certain modules are usually used, such as,
- Logging
- Authentication / Authorization
- Routing
- Static File handling
- Response Caching
- URL Rewriting
These are examples of middleware structures.
Before moving to examples, I want to note that we use the Chain of Responsibility (COR) pattern to create middleware!
Since middleware calls each other in a chain (I’ll use the word “chain” to make it easier for everyone to understand), they take an object of the RequestDelegate type. This object is the instance of the next or previous middleware in the chain (because of the chain structure), and it’s in your control whether to call the next one. In the following parts of my article, I’ll explain the types of middleware in more detail and how to use them in the chain.
There are two ways to create custom middleware in our applications.
Using the IMiddleware interface (strongly-typed middleware).
namespace MiddlewareExample
{
public class StrongTypedMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
await context.Response.WriteAsync("Hello !");
}
}
}
Using a convention-based approach.
namespace MiddlewareExample
{
public class ConventionBasedMiddleware
{
private RequestDelegate _next;
public ConventionBasedMiddleware(RequestDelegate next) { _next = next; }
public async Task InvokeAsync(HttpContext context)
{
await context.Response.WriteAsync("Hello !");
}
}
}
The InvokeAsync method must always be used, and as an input parameter, it must take an object of the HttpContext type. The return type of this method should be a Task.
For the middleware to work in a Chain of Responsibility (COR) style, the next middleware object needs to be created. This is optional - if you don’t want to call the next middleware, you don’t have to define it. This shows how flexible the middleware architecture can be.
Now, we will continue our article with examples of convention-based middleware.
What are the types of middleware?
In general, there are four types of middleware. These types are just abstract definitions, meaning that when creating middleware, certain functional abstract types are formed. The way these types are used is entirely based on the Chain of Responsibility (COR) pattern (the chain structure is demonstrated here).
The functional middleware types used in ASP.NET Core applications are,
- Response-editing middleware (edits the response)
- Request-editing middleware (edits the request)
- Short-circuiting middleware (stops the chain and doesn’t pass the request to the next middleware)
- Content-generating middleware (generates content or output)
We won’t follow the order listed above, but pay attention because the order is important based on the functional types!
- Content-generating middleware: This is one of the most important types of middleware. MVC is built on this category. Its purpose is to process the incoming request and produce a result for the end user.
namespace MiddlewareExample
{
public class ContentMiddleware
{
private RequestDelegate _next;
public ContentMiddleware(RequestDelegate next) { _next = next; }
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Path.ToString().ToLower().Contains("Home/Index"))
await context.Response.WriteAsync("Simple content generator middleware!");
else
await _next(context);
}
}
}
- Short-circuiting middleware: As an example of short-circuiting middleware, let’s use MVC again. If the view returned by your controller is already cached, there’s no need to continue through the middleware pipeline all the way to the MVC middleware. To optimize and structure the work correctly, we first go to the short-circuiting middleware, where we check if the data is already in the cache. If it is, we take the cached data and send it as a response. But if the data is not in the cache, we continue through the middleware pipeline and run the middleware that will generate the result.
namespace MiddlewareExample
{
// MOCK Cache sample. This class simulate caching , please use original caching middleware!
public class ShortCircuitMiddleware
{
private RequestDelegate _next;
public ShortCircuitMiddleware(RequestDelegate next) { _next = next; }
public async Task InvokeAsync(HttpContext context, IMemoryCache cache)
{
string key = GetCacheKey(context);
if(cache.TryGetValue<CacheItem>(key,out CacheItem item))
{
if(IsValid(context, item))
{
await context.Response.WriteAsync(item.ResponseStr);
return;
}
}
await _next(context);
}
// Simulate
private string GetCacheKey(HttpContext context)
{
return null;
}
// Simulate
private bool IsValid(HttpContext context,CacheItem item)
{
return true;
}
}
}
- Request-editing middleware: This is designed to change the structure of the incoming request before it reaches the next components (middleware) in the chain. This is useful when there is a change in the platform’s working principles or to help the next middleware in the chain process the request more easily. It’s like transforming the request into a different schema.
namespace MiddlewareExample
{
public class RequestEditingMiddleware
{
private RequestDelegate _next;
public RequestEditingMiddleware(RequestDelegate next) { _next = next; }
public async Task InvokeAsync(HttpContext context, IMemoryCache cache)
{
if(context.Request.Path.Value.Equals("/Home"))
{
context.Request.Path.Add(new PathString("Index"));
}
await _next(context);
}
}
}
- Response-editing middleware: This is a type of middleware that affects or manipulates the result produced by other middlewares in the chain that process the request and generate the response. For example, you can use this type of middleware to handle errors over HTTP. If a 404 error is encountered in the generated response, this functional middleware can be written to handle it, such as adding a custom 404 page or performing similar tasks.
namespace MiddlewareExample
{
// HttpErrorMiddleware Example
public class ResponseEditingMiddleware
{
private RequestDelegate _next;
private IHostingEnvironment _hosting;
public ResponseEditingMiddleware(RequestDelegate next, IHostingEnvironment envrionment) { _next = next; _hosting = envrionment; }
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
if (!_hosting.IsDevelopment())
{
if(ExistsError(context))
{
switch (context.Response.StatusCode)
{
case 404:
case 403:
case 400:
await context.Response.WriteAsync($"Occur error status code {context.Response.StatusCode}!");
break;
}
}
}
}
catch {
// Sample
await context.Response.WriteAsync($"Occur unknow error. Please report the [email protected]!");
return;
}
}
// Simulate.
private bool ExistsError(HttpContext contex)
{
return true;
}
}
}
Here, the other middleware in the chain is called first because the called middleware generates a response. Afterward, we intervene with the response by adding our necessary code, which allows us to manipulate the output. This shows us the flexibility of our chain structure.
Creating and Registering Middleware In the Pipeline
We define our middleware in the Configure method inside the Startup class (there are a few ways to do this, but I will show the classic and common ones). This is how our middleware runs.
In fact, in the Configure method, we create our pipeline by registering our middleware, and for this, we use the IApplicationBuilder interface.
The order of registration in the Configure method is important. Each middleware added is automatically connected to the next one in the chain (using builder and wrapper patterns behind the scenes). Based on the types of middleware we described earlier, they need to be ordered in a specific way. Let’s first look at how to register them, and then I will return to this topic later in the article.
There are three ways to register middleware.
- With the Use() method
- With the Run() method
- With the UseMiddleware() method
- Use() Method: The purpose of this method is to quickly register a delegate in the pipeline, and all of our abstract functional types can use this. It provides the method signature for “HttpContext” and “RequestDelegate,” meaning it takes one request container and passes the next middleware in the chain (using a Func delegate), for example.
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.Use(async (context, next) => {
// Do work that doesn't write to the response.
await next.Invoke();
// Do logging or other work that doesn't write to the response.
});
}
- Run() Method: The purpose of this method is to quickly register a delegate in the pipeline, but it can only logically take on the role of content-generating middleware among the abstract functional types mentioned above. It provides the method signature for “HttpContext,” meaning it passes one request container object (with the chain delegate being RequestDelegate), for example.
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.Run(async (context) =>
{
await context.Response.WriteAsync("Hi, generated for clients!");
});
}
- Map() Method: The purpose of this method is not to register middleware in the pipeline like Use or Run. Instead, it adds a new pipeline, allowing us to create an inner or nested pipeline (it enables creating a pipeline within a pipeline).
It allows us to create branches or sub-branches within a branch. However, there is an important difference: when we register, it requires a URI segment. This means that the inner pipeline will only work if the incoming request matches this segment, so it operates under a segment condition. We use this method when the application consists of multiple modules or in specific situations. It provides the method signature for an IApplicationBuilder object (using an Action delegate), for example.
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.Map("/index", (appBuilder) => {
appBuilder.Use(async (context, next) =>
{
await next.Invoke();
});
appBuilder.Use(async (context, next) =>
{
await context.Response.WriteAsync("Hi generated response for clients! (MAP) route /index");
});
});
app.Run(async context => {
await context.Response.WriteAsync("Hi generated response for clients!");
});
}
Best Practice
We use the UseMiddleware<>() and extension method to register middleware in a reusable, flexible, and configurable way. This method allows us to register middleware easily with its generic structure, enabling us to register it as a class.
Let’s create an example using one of the examples from above.
namespace MiddlewareExample
{
public static class AppBuilderErrorMiddlewareExtensions
{
public static IApplicationBuilder UseHttpErrorHandler(this IApplicationBuilder app)
{
if (app == null)
throw new ArgumentNullException(nameof(IApplicationBuilder));
return app.UseMiddleware<ResponseEditingMiddleware>();
}
}
}
In the Pipeline - COR and Ordering
We will create a sample pipeline with the example middleware we built.
As I mentioned, the order of the abstract functional types in the chain is very important. Why? Because the work done by the abstract function affects the chain structure. Any wrong order in the chain can be critical for security, performance, and proper functioning (if the chain structure is incorrect, it can cause the application not to work correctly and even lead to errors). Let’s explain the order one by one:
- Response-editing middleware comes first. This is because it concerns the output (response) of the processed request, not the incoming request. For example, let’s use our own example, the “HttpErrorMiddleware.” When we receive a request, this middleware will be the first to run. It will call the next middleware in the chain without processing it first. After the output (response) is generated, when it returns (upward in the pipeline), the middleware will manipulate the last produced output (response) to handle it finally. After the chain runs to MVC, if MVC returns an HTTP error, the output will be sent back through the chain. Our “HttpErrorMiddleware” runs last to provide a user-friendly output (it manipulates the output). You’ll notice that exception-handling middleware is always defined first. This is because if you call the method stack first, you can handle it last, which shows the flexibility of the COR.
- Request-editing middleware comes second because it may be necessary to transform the incoming request into a more flexible format for the next middleware to handle. For example, suppose we have a large shopping site that used PHP and has now transitioned to ASP.NET Core. Some URLs had to change. To prevent broken links and losing old users, we used URL rewriting. For instance, when a user used the URL www.shop.com/Home?/PAGE=1, we transformed it to www.shop.com/Home/Index/1 in MVC. How does this work? The incoming request, www.shop.com/Home?/PAGE=1, will come in, and our second middleware will analyze this URL and convert it into a format that MVC can understand.
- Short-circuiting middleware comes third because it produces a result quickly, stopping the chain before the middleware that generates the output. For optimization, it sends the result (response) back without going to the next middleware. An example is caching; if the previous result is cached, there is no need to perform the operation again. The result is produced and sent back without reaching the content-generating middleware. It can also be used to prevent bot attacks.
- Content-generating middleware is the last functional type of middleware. Its job is straightforward: to generate content, output, or results based on the incoming request. Finally, this middleware produces the necessary output. For example, MVC falls into this category. In a brief explanation of how MVC works, the UseMvc() middleware is built on our routing middleware. The incoming request is analyzed and parsed according to its route path, creating RouteData. Then, the specified Controller and Action are executed, and when the process is finished, the generated output is added to the HttpResponse class within the HttpContext and sent back to the user.
Let’s conclude our article by registering all the middleware we have created using the ‘Configure’ method. All the added middleware is convention-based.
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseHttpErrorHandler(); // 1. Response Editing middleware
app.UseRequestModifier(); // 2. Request Editing middleware
app.UseCacheResponse(); // 3. Short Circuit middleware
app.UseContentGenerator(); // 4. Content/Response generator middleware
}
I hope this has been helpful and that you have grasped the concept in a deeper way.
Stay tuned!