Basic usage of ASP.NET Core middleware

Basic usage of ASP.NET Core middleware

ASP.NET Core middleware

The processing flow of ASP.NET Core is a pipeline, and the middleware is a component that is assembled into the pipeline to process requests and responses. The middleware is executed in the order of assembly and decides whether to enter the next component. The processing flow of the middleware pipeline is as follows (the picture comes from the official website):

The pipeline processing method makes it more convenient for us to extend the program.

Use middleware

The ASP.NET Core middleware model is that we can quickly develop our own middleware and complete the extension of the application. Let's first understand the development of middleware from a simple example.

Run

1. we create an ASP.NET Core application with the following code in Startup.cs:

app.Run(async (context) =>
{
    await context.Response.WriteAsync("Hello World!");
});

In this code, use the Run method to run a delegate. This is the simplest middleware. It intercepts all requests and returns a piece of text as a response. The Run delegate terminates the operation of the pipeline, so it is also called terminal middleware .

Use

Let's look at another example:

app.Use(async (context, next) =>
{
   //Do something here
    
   //Invoke next middleware
    await next.Invoke();
    
   //Do something here
    
});

In this code, use the Use method to run a delegate, we can execute custom code before and after the Next call, which can facilitate logging and other tasks. In this code, use the next.Invoke() method to call the next middleware, thus connecting the middleware pipeline; if the next.Invoke() method is not called, the pipeline will be short-circuited .

Map and MapWhen

To deal with the above two methods, ASP.NET Core can also use Map to create branch based on path matching and MapWhen to create branch based on condition. code show as below:

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

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, IHostingEnvironment env)
{
    app.Map("/map", HandleMap);
    
    app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
               HandleBranch);
    
    app.Run(async context =>
    {
        await context.Response.WriteAsync("Hello World!");
    });
}

The above code demonstrates how to use Map and MapWhen to create branches based on paths and conditions. In addition, the Map method also supports hierarchical branching, we refer to the following code:

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

It should be noted that when using Map, the matching Path will be deleted from HttpRequest.Path, and the line segment will be appended to HttpRequest.PathBase for each request. For example, for a path /level1/level2a, when processing in level1App, its request path is truncated to /level2a, when processing in level2AApp, its path becomes /, and the corresponding PathBase will become /level1/level2a.

Development middleware

Seeing this, we already know the basic usage of middleware, it's time to write a real middleware.

Contract-based middleware development

A simple example is provided on the ASP.NET Core official website. The area information of the application is set through middleware. The code is as follows:

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}");
    });
}

Through this code, we can set the area information of the application by way of QueryString. But how can such code be reused? Note that middleware must be reusable and convenient for reuse. Let's transform this code:

public class RequestCultureMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task InvokeAsync(HttpContext context)
    {
       //...

       //Call the next delegate/middleware in the pipeline
        await _next(context);
    }
}

Define a delegate here to execute specific business logic, and then call this delegate in Configure:

app.UseMiddleware<RequestCultureMiddleware>();

This is still inconvenient, not as convenient as we use app.UseMvc(), so let's add an extension method to achieve more convenient reuse:

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

Then we can use middleware like this:

app.UseRequestCulture();

By entrusting the construction of the middleware, the application creates this middleware at runtime and adds it to the pipeline. It should be noted here that the creation of middleware is singleton, and each middleware has only one instance in the life cycle of the application. So the question is, what should we do if our business logic requires multiple instances? please watch the following part.

Request-based dependency injection

Through the above code, we already know how to write a middleware and how to reuse this middleware conveniently. In the middleware creation process, the container will create a middleware instance for us, and only one instance of this middleware will be created in the entire application life cycle. Usually our program does not allow such injection logic.

In fact, we can understand middleware as the entrance of business logic. The real business logic is implemented through the Application Service layer. We only need to inject the application service into the Invoke method.

ASP.NET Core provides us with this mechanism, allowing us to perform dependency injection according to the request, that is, to create a service for each request. code show as below:

public class CustomMiddleware
{
    private readonly RequestDelegate _next;

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

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

In this code, the instance of CustomMiddleware is still a singleton, but IMyScopedService is injected according to the request, and an instance of IMyScopedService is created for each request. The life cycle of the svc object is PerRequest.

Agreement-based middleware template

Here is a complete example, which can be understood as a middleware development template for easy reference in future use. The whole process is divided into the following steps:

  • Encapsulate business logic into ApplicationService
  • Create a middleware proxy class
  • Create a middleware extension class
  • Use middleware

code show as below:

namespace MiddlewareDemo
{
    using Microsoft.AspNetCore.Http;
    using System.Threading.Tasks;
    
   //1. Define and implement business logic
    public interface IMyScopedService
    {
        int MyProperty {get; set;}
    }

    public class MyScopedService: IMyScopedService
    {
        public int MyProperty {get; set;}
    }

   //2. Create a middleware proxy class
    public class CustomMiddleware
    {
        private readonly RequestDelegate _next;

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

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

//3.1 Add dependent service registration
namespace Microsoft.Extensions.DependencyInjection
{
    using MiddlewareDemo;
    public static partial class CustomMiddlewareExtensions
    {
       ///<summary>
       ///Add service dependency registration
       ///</summary>
        public static IServiceCollection AddCustom(this IServiceCollection services)
        {
            return services.AddScoped<IMyScopedService, MyScopedService>();
        }

    }
}

//3.2 Create a middleware extension class
namespace Microsoft.AspNetCore.Builder
{
    using MiddlewareDemo;

    public static partial class CustomMiddlewareExtensions
    {
       ///<summary>
       ///Use middleware
       ///</summary>
        public static IApplicationBuilder UseCustom(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<CustomMiddleware>();
        }
    }
}

//4. Use middleware
public void ConfigureServices(IServiceCollection services)
{
    services.AddCustom();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseCustom();
}

Factory activated middleware

The middleware development we introduced earlier is based on conventions, allowing us to quickly get started with development. At the same time, ASP.NET Core also provides a middleware development method based on factory activation. We can develop middleware by implementing IMiddlewareFactory and IMiddleware interfaces.

public class FactoryActivatedMiddleware: IMiddleware
{
    private readonly AppDbContext _db;

    public FactoryActivatedMiddleware(AppDbContext db)
    {
        _db = db;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var keyValue = context.Request.Query["key"];

        if (!string.IsNullOrWhiteSpace(keyValue))
        {
            _db.Add(new Request()
                {
                    DT = DateTime.UtcNow, 
                    MiddlewareActivation = "FactoryActivatedMiddleware", 
                    Value = keyValue
                });

            await _db.SaveChangesAsync();
        }

        await next(context);
    }
}

The above code demonstrates how to use factory-activated middleware. There are two points to note during use: 1. Service registration is required in ConfigureServices; 2. Passing parameters is not supported in the UseMiddleware() method.

Reference documents

Reference: https://cloud.tencent.com/developer/article/1511638 ASP.NET Core Middleware Basic Usage-Cloud + Community-Tencent Cloud