Detailed explanation of ASP.NET Core Middleware

Keywords: Programming Session REST

What is Middleware?

Middleware is software that is assembled into application pipelines to process requests and responses. Each component:

  • Choose whether to pass the request to the next component in the pipeline.
  • You can perform work before and after calling the next component in the pipeline.

Request delegates are used to build the request pipeline and process each HTTP request.

Request delegation is configured using Run, Map and Use extension methods. A separate request delegate can be specified as an inline anonymous method (called inline Middleware), or it can be defined in a reusable class. These reusable classes and inline anonymous methods are middleware or middleware components. Each middleware component in the request process is responsible for invoking the next component in the pipeline and, if appropriate, for link shorting.

The migration of HTTP module to middleware explains the difference between the request pipeline in ASP.NET Core and previous versions (ASP. Net), and provides more middleware examples.

Using iaapplicationbuilder to create middleware pipeline

The ASP.NET Core request process consists of a series of request delegates, as shown in the following figure (the execution process follows the black arrow):

Each delegate can perform operations before and after the next delegate. Delegates can also decide not to pass requests to the next delegate, which is called a short circuit in the request pipeline. Short circuit is usually desirable as it avoids unnecessary work. For example, the static file middleware can return a request for a static file and short the rest of the pipeline. Exception handling delegates need to be called early in the pipeline, so they can catch exceptions from later pipelines.

The simplest is probably an ASP.NET Core application that establishes a request delegate to handle all requests. This case does not contain the actual request pipeline. Instead, an anonymous method is called for each HTTP request.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello, World!");
        });
    }
}

The first app.Run delegate terminates the pipeline.

There are the following codes:

Through browser access, it is found that the pipeline was terminated in the first app.Run.

You can connect multiple request delegates with app.Use. The next parameter represents the next delegate in the pipeline. (remember that you can end the pipeline without calling the next parameter.) You can usually perform operations before and after the next delegation, as shown in the following example:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
	        {
				await context.Response.WriteAsync("Before the first delegate executes the next delegate\r\n");
				//Call the next delegate in the pipeline
		        await next.Invoke();
		        await context.Response.WriteAsync("After the first delegate finishes executing the next delegate\r\n");
			});
	        app.Run(async context =>
	        {
		        await context.Response.WriteAsync("Enter the second delegation\r\n");
				await context.Response.WriteAsync("Hello from 2nd delegate.\r\n");
		        await context.Response.WriteAsync("End second delegation\r\n");
			});
    }
}

The browser access results are as follows:

You can see that the execution order of the request delegation follows the above flowchart.

be careful:
Do not call next.Invoke after the response has been sent to the client. After the response starts, changes to HttpResponse throw an exception. For example, setting response headers, status codes, and other changes will throw exceptions. Write the response body after calling next.

  • May result in a breach of agreement. For example, write the content length that exceeds the content length.

  • The response content format may be broken. For example, write an HTML footer to a CSS file.

HttpResponse.HasStarted is a useful hint to indicate whether a response header and / or body has been sent and written.

order

At Startup. The order in which middleware components are added to the Configure method defines the order in which they are called on requests, and the reverse order in which they respond. This ordering is critical for security, performance, and functionality.

The Startup.Configure method (shown below) adds the following middleware components:

  1. Exception / error handling
  2. Static file service
  3. identity authentication
  4. MVC
public void Configure(IApplicationBuilder app)
{
    app.UseExceptionHandler("/Home/Error"); // Call first to catch exceptions
                                            								// thrown in the following middleware.

    app.UseStaticFiles();                   // Return static files and end pipeline.

    app.UseAuthentication();               // Authenticate before you access
                                           					// secure resources.

    app.UseMvcWithDefaultRoute();          // Add MVC to the request pipeline.
}

In the above code, UseExceptionHandler is the first middleware component added to the pipeline, so it captures any exceptions that occur in subsequent calls.

The static file middleware is invoked ahead of time in the pipeline, so it can handle requests and short circuits without passing through the remaining components. Static file middleware does not provide authorization checking. Any files provided by it, including those under wwwroot, are public.

If the request is not processed by the static file middleware, it will be passed to the Identity middleware (app.UseAuthentication) that performs the authentication. Authentication does not short-circuit unauthenticated requests. Although authentication requests are made, authorization (and denial) occurs only after MVC selects a specific Razor page or controller and action.

Authorization (and denial) occurs only after MVC selects a specific Razor page or Controller and Action.

The following example demonstrates the middleware sequence in which requests for static files are processed by the static file middleware before responding to the compression middleware. Static files are not compressed in the order of middleware. MVC responses from UseMvcWithDefaultRoute can be compressed.

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();         // Static files not compressed
    app.UseResponseCompression();
    app.UseMvcWithDefaultRoute();
}

Use, Run, and Map

You can Use use Use, Run and Map to configure the HTTP pipeline. The Use method shortens the pipeline (that is, the next request delegate may not be called). The Run method is a convention, and some middleware components may be exposed to the Run [Middleware] method running at the end of the pipeline. The Map * extension is used as a contract for branch pipes. The Map branches the request pipeline according to the matching of the given request path. If the request path starts with the given path, the branch is executed.

public class Startup
{
    private static void HandleMapTest1(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 1");
        });
    }

    private static void HandleMapTest2(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 2");
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.Map("/map1", HandleMapTest1);

        app.Map("/map2", HandleMapTest2);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

The following table shows the http://localhost:19219 Requests and responses for:

request response
localhost:1234 Hello from non-Map delegate.
localhost:1234/map1 Map Test 1
localhost:1234/map2 Map Test 2
localhost:1234/map3 Hello from non-Map delegate.

When using Map, the matching path segments are removed from HttpRequest.Path and appended to httprequest.pathbase for each request.

MapWhen branches the request pipeline based on the result of a given predicate. Any predicate of type func < httpcontext, bool > can be used to map requests to new branches of the pipeline. In the following example, a predicate is used to detect the presence of a branch of a query string variable:

public class Startup
{
    private static void HandleBranch(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            var branchVer = context.Request.Query["branch"];
            await context.Response.WriteAsync($"Branch used = {branchVer}");
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
                               HandleBranch);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

The following table shows using the above code http://localhost:19219 Requests and responses for:

request response
localhost:1234 Hello from non-Map delegate.
localhost:1234/?branch=1 Branch used = master

Map supports nesting, for example:

app.Map("/level1", level1App => {
       level1App.Map("/level2a", level2AApp => {
           // "/level1/level2a"
           //...
       });
       level1App.Map("/level2b", level2BApp => {
           // "/level1/level2b"
           //...
       });
   });

Map can also match multiple clips at once, for example:

app.Map("/level1/level2", HandleMultiSeg);

Built in middleware

ASP.NET Core comes with the following middleware components:

middleware describe
Authentication Provide authentication support
CORS Configure cross domain resource sharing
Response Caching Provide cache response support
Response Compression Provide response compression support
Routing Define and constrain request routes
Session Provide user session management
Static Files Service support for static file and directory browsing
URL Rewriting Middleware Support for overriding URLs and redirecting requests

Write Middleware

Middleware is usually encapsulated in a class and exposed using extension methods. View the following middleware, which sets the Culture of the current request from the query string:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use((context, next) =>
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                var culture = new CultureInfo(cultureQuery);

                CultureInfo.CurrentCulture = culture;
                CultureInfo.CurrentUICulture = culture;
            }

            // Call the next delegate/middleware in the pipeline
            return next();
        });

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });

    }
}

You can test middleware by passing Culture, such as http://localhost:19219/?culture=zh-CN

The following code moves the middleware delegation to a class:

using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;

namespace Culture
{
    public class RequestCultureMiddleware
    {
        private readonly RequestDelegate _next;

        public RequestCultureMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public Task Invoke(HttpContext context)
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                var culture = new CultureInfo(cultureQuery);

                CultureInfo.CurrentCulture = culture;
                CultureInfo.CurrentUICulture = culture;

            }

            // Call the next delegate/middleware in the pipeline
            return this._next(context);
        }
    }
}

The middleware is exposed through the extension method of iaapplicationbuilder as follows:

using Microsoft.AspNetCore.Builder;

namespace Culture
{
    public static class RequestCultureMiddlewareExtensions
    {
        public static IApplicationBuilder UseRequestCulture(
            this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<RequestCultureMiddleware>();
        }
    }
}

The following code calls the middleware from Configure:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseRequestCulture();

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });

    }
}

Middleware should follow the principle of explicit dependency by exposing its dependency in its constructor. Middleware is built once in the application lifecycle. If you need to share services with middleware in the request, see the following request dependencies.

Middleware components can resolve the dependency of dependency injection by constructing method parameters. UseMiddleware can also accept other parameters directly.

Dependencies per request

Because middleware is built at application startup, not every request, the scope life cycle services used by the middleware constructor are not shared with other dependency injection types during each request. If you must share scope services between middleware and other types, add these services to the signature of the Invoke method. The Invoke method can accept other parameters populated by dependency injection. For example:

public class MyMiddleware
{
    private readonly RequestDelegate _next;

    public MyMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext, IMyScopedService svc)
    {
        svc.MyProperty = 1000;
        await _next(httpContext);
    }
}

Posted by Headwaters on Mon, 18 May 2020 02:40:41 -0700