关于aspnetcore中间件的一些思考

关于aspnetcore中间件的一些思考

最近很久没有写博客了,还是自己懒惰了,前面一段时间重温了中间件的源码(24年一月份左右),主要是项目采用了和中间件类似的设计,实际上就是一个简化版的管道中间件模型,但是在使用过程中出现了一些问题,想着如何去解决,以及为什么出现这样的问题,于是有了这一篇记录。

什么是中间件

按照官方定义 :

中间件是一种装配到应用管道以处理请求和响应的软件。 每个组件:

  • 选择是否将请求传递到管道中的下一个组件
  • 可在管道中的下一个组件前后执行工作

请求委托用于生成请求管道。 请求委托处理每个 HTTP 请求。

说的已经很明确了,我们的请求实际上就是中间件去处理的,每个中间件处理自己职责部分的内容,最后将处理的结果返回给用户。

如何使用

约定俗称

中间件有约定俗称的使用方式 ,官方说明:

必须包括中间件类:

  • 具有类型为 RequestDelegate 的参数的公共构造函数。
  • 名为 Invoke/InvokeAsync 的公共方法。 此方法必须:
    • 返回 Task
    • 接受类型 HttpContext 的第一个参数。

构造函数和 Invoke/InvokeAsync 的其他参数由依赖关系注入 (DI) 填充。

using System.Globalization;

namespace Middleware.Example;

public class RequestCultureMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task InvokeAsync(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.
        await _next(context);
    }
}
生命周期:

中间件在应用启动时构造,因此具有应用程序生存期。 在每个请求过程中,中间件构造函数使用的范围内生存期服务不与其他依赖关系注入类型共享。 若要在中间件和其他类型之间共享范围内服务,请将这些服务添加到 InvokeAsync 方法的签名。 InvokeAsync 方法可接受由 DI 填充的其他参数:

解释一下:即中间件只是在启动时构造一个实例,不是每个请求过来就构造一个实例,所以构造函数中依赖的服务,只能是单例或者瞬时的,无法接受范围生命周期的参数。只能在 InvokeAsync 函数里面添加我们需要的范围生命周期参数。

如果还是不太明白,下面有源码解析。

实现接口

官方替我们定义了IMiddleware接口,我们实现接口即可。主要是实现InvokeAsync(HttpContext context, RequestDelegate next)函数,看官方示例

public class SimpleInjectorActivatedMiddleware : IMiddleware
{
    private readonly AppDbContext _db;

    public SimpleInjectorActivatedMiddleware(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 = "SimpleInjectorActivatedMiddleware", 
                    Value = keyValue
                });

            await _db.SaveChangesAsync();
        }

        await next(context);
    }
}

实现IMiddleware接口,进而实现继承的方法即可。但是和约定俗称的实现方式对比一下,实现接口方式的构造参数根据官方的说法,可以依赖范围服务,但是我们的InvokeAsync只能有这两个参数。我们需要注意的一点是:需要额外注册这个服务,生命周期为范围或者瞬时,如下,这更加说明了此种中间件是范围或者顺时生命周期的

builder.Services.AddTransient<FactoryActivatedMiddleware>();

以下是官方文档

IMiddlewareFactory/IMiddleware中间件激活的扩展点,具有以下优势:

  • 按客户端请求(作用域服务的注入)激活
  • 让中间件强类型化

UseMiddleware 扩展方法检查中间件的已注册类型是否实现 IMiddleware。 如果是,则使用在容器中注册的 IMiddlewareFactory 实例来解析 IMiddleware 实现,而不使用基于约定的中间件激活逻辑。 中间件在应用的服务容器中注册为作用域或瞬态服务。

IMiddleware 按客户端请求(连接)激活,因此作用域服务可以注入到中间件的构造函数中。

实现原理

上面知道了如何使用,同时也稍微了解了两者的差异,我们尝试从源码解析两者这样原因

首先我们知道要如何去使用,我们使用UseMiddleware<>来使用我们的中间件,在我们启动的时候,就会调用我们注册中间件,所以从这方面来找。下面是UseMiddleware源码

 public static IApplicationBuilder UseMiddleware(
        this IApplicationBuilder app,
        [DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware,
        params object?[] args)
    {
    	//如果middleware实现了 IMiddleware接口,
        if (typeof(IMiddleware).IsAssignableFrom(middleware))
        {   // IMiddleware不支持传递参数
            // IMiddleware doesn't support passing args directly since it's
            // activated from the container
            //省略一些代码
            var interfaceBinder = new InterfaceMiddlewareBinder(middleware);
            //直接返回
            return app.Use(interfaceBinder.CreateMiddleware);
        }
		 //如果middleware   不 实现 IMiddleware接口
        var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);
        MethodInfo? invokeMethod = null;
        foreach (var method in methods)
        {
            if (string.Equals(method.Name, InvokeMethodName, StringComparison.Ordinal) || string.Equals(method.Name, InvokeAsyncMethodName, StringComparison.Ordinal))
            {
                //省略判断,目标是寻找InvokeAsync方法
                invokeMethod = method;
            }
        } 
       //省略一些代码 
        var parameters = invokeMethod.GetParameters();
     //如果InvokeAsync方法的参数为空,或者第一个参数不是HttpContext类型,报错
        if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
        {
            throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));
        }
        var reflectionBinder = new ReflectionMiddlewareBinder(app, middleware, args, invokeMethod, parameters);
        return app.Use(reflectionBinder.CreateMiddleware);
    }

如果是实现Imiddleware接口:

代码中可以看到,我们的中间件如果是实现了IMiddleware,会把中间件转为为一个委托注入进去,具体关键代码是 interfaceBinder.CreateMiddleware

public RequestDelegate CreateMiddleware(RequestDelegate next)
        {
            return async context =>
            {    //di中获取middlewareFactory
                var middlewareFactory = (IMiddlewareFactory?)context.RequestServices.GetService(typeof(IMiddlewareFactory));
                //省略一些判断,利用middlewareFactory构造我们的中间件,实际上就是从di中获取,
                //只不过包裹了一层
                var middleware = middlewareFactory.Create(_middlewareType);  
 			    //省略一些判断
                try
                {  
                    await middleware.InvokeAsync(context, next);
                }
                finally
                {
                    middlewareFactory.Release(middleware);
                }
            };
        }

可以看到返回是 RequestDelegate ,内部逻辑是,对于每个context,从context.RequestServices获取我们注册的IMiddlewareFactory,然后再去创建我们的中间件,这就说明了,实际上我们这里注册的中间件是范围的,是针对于每个请求创建的。既然是范围的,那我们就可以从构造函数中注入生命周期为范围的服务。

如果是实现基于约定的:

如果没有实现IMiddleware,那么就检查,中间件是否实现了invokeasync函数,且第一个参数是不是context,返回的是不是task什么的,然后关键函数是:reflectionBinder.CreateMiddleware,实际上也是把中间件转化为RequestDelegate,但是逻辑不同,不是针对于每个context就构造一个,而是只是在应用启动的时候构造一次:

 public RequestDelegate CreateMiddleware(RequestDelegate next)
        {
            var ctorArgs = new object[_args.Length + 1];
            ctorArgs[0] = next;
            Array.Copy(_args, 0, ctorArgs, 1, _args.Length);
            // 直接利用反射构造,但是参数从从ApplicationServices中获取,但是这个是根容器,只能提供单例以及瞬时的服务,不能提供范围的服务
            var instance = ActivatorUtilities.CreateInstance(_app.ApplicationServices, _middleware, ctorArgs);
            if (_parameters.Length == 1)
            {
                return (RequestDelegate)_invokeMethod.CreateDelegate(typeof(RequestDelegate), instance);
            }
 
            // Performance optimization: Use compiled expressions to invoke middleware with services injected in Invoke.
            // If IsDynamicCodeCompiled is false then use standard reflection to avoid overhead of interpreting expressions.
            var factory = RuntimeFeature.IsDynamicCodeCompiled
                ? CompileExpression<object>(_invokeMethod, _parameters)
                : ReflectionFallback<object>(_invokeMethod, _parameters);
 			//到这里才返回委托
            return context =>
            {
                var serviceProvider = context.RequestServices ?? _app.ApplicationServices;
                if (serviceProvider == null)
                {
                    throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
                }
 			 //调用实例的_invokeMethod方法,实际上也是serviceProvider提供所需要的额外的参数,但是只是invokeasync方法而不是整个实例
                return factory(instance, context, serviceProvider);
            };
        }

从源码中可以看到,就是我们的中间件会在应用启动的时候就被创建出来了,但只是创建这一次,可以认为它是单例的,由我们的根容器创建的,所以构造函数参数只能是单例或者瞬时,但是他的invokeasync函数则是针对于context每次都会调用一次,提供参数的容器则是context.RequestServices 它不是根容器,所以可以提供范围服务。然后调用invokeasync函数。

总结

了解了中间件实现的两种方式,一种约定俗称,另一种实现接口,以及两者构造的差异以及依赖服务的差异。了解这些可以更加方便我们去编写合适的中间件去完成我们的业务。

参考文章

微软官方文章:https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0

写的非常棒的博主文章:https://www.cnblogs.com/xiaoxiaotank/p/15203811.html

posted @ 2024-05-27 00:29  果小天  阅读(12)  评论(0编辑  收藏  举报