DEV Community

Cover image for ASP.NET Core Middleware
Rasul
Rasul

Posted on

ASP.NET Core Middleware

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 haven’t fully understood 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 own 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, 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 also touch on topics that intersect with middleware, like self-hosting, OWIN, and built-in features.

Let’s Start

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 either before or after the next middleware (Chain of Responsibility — COR).

ASP.NET Core is actually an abstract web framework. It provides us with an OWIN-based or application backbone that allows us to create a pipeline by attaching our own 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 own custom middleware to process requests or responses and get results.

Image description
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.

Image description
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)

public class StrongyTypedMiddleware : IMiddleware
    {
        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            await context.Response.WriteAsync("Hello !");
        }
    }
Enter fullscreen mode Exit fullscreen mode

Using a convention-based approach

public class ConventionBasedMiddleware
    {
        private RequestDelegate _next;
        public ConventionBasedMiddleware(RequestDelegate next) { _next = next; }
        public async Task InvokeAsync(HttpContext context)
        {
            await context.Response.WriteAsync("Hello !");
        }
    }
Enter fullscreen mode Exit fullscreen mode

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, it needs the next middleware object. 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 4 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)

Image description

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.

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);
        }
    }
Enter fullscreen mode Exit fullscreen mode

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 the 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.

// 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;
        }
    }
Enter fullscreen mode Exit fullscreen mode

Request-editing middleware: is designed to change the structure of the incoming request before it reaches the next components (middlewares) in the chain. This is useful when there is a change in the platform’s working principles or to help the next middlewares in the chain process the request more easily. It’s like transforming the request into a different schema.

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);
        }
    }
Enter fullscreen mode Exit fullscreen mode

Response-editing middleware: 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.

// 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 site@admin.com!");
                return;
            } 
        }

        // Simulate.
        private bool ExistsError(HttpContext contex)
        {
            return true;
        }
    }
Enter fullscreen mode Exit fullscreen mode

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 one). 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 3 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.         
      });        
}
Enter fullscreen mode Exit fullscreen mode

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!");     
     });       
}
Enter fullscreen mode Exit fullscreen mode

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!");
     });       
}
Enter fullscreen mode Exit fullscreen mode

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:

public static class AppBuilderErrorMiddlewareExtensions
    {
        public static IApplicationBuilder UseHttpErrorHandler(this IApplicationBuilder app)
        {
            if (app == null)
                throw new ArgumentNullException(nameof(IApplicationBuilder));

            return app.UseMiddleware<ResponseEditingMiddleware>();
        }
    }
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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 in 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
}
Enter fullscreen mode Exit fullscreen mode

I hope this has been helpful and that you have grasped the concept in a deeper way.

Stay tuned!

Top comments (0)