依赖注入如何与中间件一起工作?

问题描述

我看到了一些类似这样的代码

public class CustomMiddleware {
    private RequestDelegate next;

    public CustomMiddleware (RequestDelegate nextDelegate) {
       next = nextDelegate;
    }
    
    public async Task Invoke(HttpContext context,IResponseFormatter formatter) {
       ...
       await next(context);
    }
}

并且 IResponseFormatter 服务注册为:

public void ConfigureServices(IServiceCollection services) {
    services.AddTransient<IResponseFormatter,GuidService>();
}

我知道 DI 是如何工作的,但我对中间件如何工作的理解是,next(RequestDelegate) 表示下一个中间件的 Invoke 方法,所以在 CustomMiddleware 中,即使是第二个参数也被解析通过 DI,但 RequestDelegate 的定义是

public delegate Task RequestDelegate(HttpContext context);

CustomMiddleware 之前的中间件如何知道 CustomMiddlewareInvoke 方法通过额外的参数发生了变化?它无法提前知道,因此之前的中间件的 next RequestDelegateCustomMiddlewareInvoke 方法的签名不匹配?

解决方法

在内部,框架代码通过以下约定使用反射来确定中间件的构造函数和 Invoke 成员的参数

中间件类必须包括:

  • 带有 RequestDelegate 类型参数的公共构造函数。
  • 一个名为 Invoke 或 InvokeAsync 的公共方法。该方法必须:
    • 返回一个任务。
    • 接受类型为 HttpContext 的第一个参数。

构造函数和 Invoke/InvokeAsync 的附加参数是 由依赖注入 (DI) 填充。

参考Write custom ASP.NET Core middleware

Source code

所示
/// <summary>
/// Adds a middleware type to the application's request pipeline.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> instance.</param>
/// <param name="middleware">The middleware type.</param>
/// <param name="args">The arguments to pass to the middleware type instance's constructor.</param>
/// <returns>The <see cref="IApplicationBuilder"/> instance.</returns>
public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app,[DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware,params object?[] args)
{
    if (typeof(IMiddleware).IsAssignableFrom(middleware))
    {
        // IMiddleware doesn't support passing args directly since it's
        // activated from the container
        if (args.Length > 0)
        {
            throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));
        }

        return UseMiddlewareInterface(app,middleware);
    }

    var applicationServices = app.ApplicationServices;
    return app.Use(next =>
    {
        var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);
        var invokeMethods = methods.Where(m =>
            string.Equals(m.Name,InvokeMethodName,StringComparison.Ordinal)
            || string.Equals(m.Name,InvokeAsyncMethodName,StringComparison.Ordinal)
            ).ToArray();

        if (invokeMethods.Length > 1)
        {
            throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName,InvokeAsyncMethodName));
        }

        if (invokeMethods.Length == 0)
        {
            throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName,middleware));
        }

        var methodInfo = invokeMethods[0];
        if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
        {
            throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName,nameof(Task)));
        }

        var parameters = methodInfo.GetParameters();
        if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
        {
            throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName,nameof(HttpContext)));
        }

        var ctorArgs = new object[args.Length + 1];
        ctorArgs[0] = next;
        Array.Copy(args,ctorArgs,1,args.Length);
        var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices,middleware,ctorArgs);
        if (parameters.Length == 1)
        {
            return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate),instance);
        }

        var factory = Compile<object>(methodInfo,parameters);

        return context =>
        {
            var serviceProvider = context.RequestServices ?? applicationServices;
            if (serviceProvider == null)
            {
                throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
            }

            return factory(instance,context,serviceProvider);
        };
    });
}
,

CustomMiddleware 之前的中间件怎么知道 CustomMiddleware 的 Invoke 方法因为多了一个参数而改变了?

因为惯例(和反思)。

自定义中间件不继承任何接口或基类,因此运行时知道如何使用中间件的唯一方法是通过convention

中间件类必须包括:

  • 带有 RequestDelegate 类型参数的公共构造函数。
  • 名为 Invoke 或 InvokeAsync 的方法。该方法必须:
    • 返回一个任务。
    • 接受类型为 HttpContext 的第一个参数。

有了这些知识,可以安全地运行中间件:您可以使用反射来获取 Invoke 的依赖项,并在知道返回类型是 Task 的情况下执行它。例如:

MethodInfo method = middleware.GetType().GetMethod("Invoke");
ParameterInfo[] parameters = method.GetParameters();
// ... instatiate the dependencies using something like ServiceProvider
// and bundle them up into an object[].
method.Invoke(middleware/**,injected dependencies go here as an object[] **/);

无法提前知道,因此前一个中间件的下一个RequestDelegate与CustomMiddleware的Invoke方法的签名不匹配?

RequestDelegateCustomMiddleware.Invoke 的签名不匹配。
RequestDelegate 不需要匹配CustomMiddleware.Invoke的签名。

所有 RequestDelegate (next) 所关心的是将相同的 HttpContext 实例向下传递到中间件链,并且由于(之前提到的)约定(强调补充):

  • 名为 Invoke 或 InvokeAsync 的方法。该方法必须:
    • 返回一个任务。
    • 接受 HttpContext 类型的第一个参数。

最后,next 不是直接调用 CustomMiddleware.Invoke。在中间件之间,DI 有机会注入下一个中间件所需的服务。