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

3
我看到一些像这样的代码:
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>();
}

我知道依赖注入是如何工作的,但我的理解中间件是这样的,next(RequestDelegate)代表下一个中间件的Invoke方法,因此在CustomMiddleware中,即使第二个参数由DI解析,但RequestDelegate的定义是:

public delegate Task RequestDelegate(HttpContext context);

CustomMiddleware之前的中间件如何知道CustomMiddlewareInvoke方法由于多了一个参数而发生了变化?它事先无法知道,因此先前中间件的nextRequestDelegateCustomMiddlewareInvoke方法的签名不匹配?

2个回答

2
在框架代码内部,使用反射来确定中间件的构造函数和Invoke成员的参数,遵循以下约定:
中间件类必须包括: - 一个公共构造函数,其参数类型为RequestDelegate。 - 一个名为Invoke或InvokeAsync的公共方法。该方法必须: - 返回一个Task。 - 接受一个类型为HttpContext的第一个参数。
构造函数和Invoke/InvokeAsync的其他参数由依赖注入(DI)进行填充。
参考 编写自定义 ASP.NET Core 中间件
正如从 源代码 中演示的那样。
/// <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, InvokeAsyncMethodName, middleware));
        }

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

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

        var ctorArgs = new object[args.Length + 1];
        ctorArgs[0] = next;
        Array.Copy(args, 0, 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);
        };
    });
}

1
前置中间件如何知道 CustomMiddleware 的 Invoke 方法已经增加了一个额外的参数呢?
因为有惯例(和反射)。
Custom Middleware 没有继承任何接口或基类,所以运行时知道如何使用这个中间件的唯一方法是通过惯例:
1. 中间件类必须包括: - 一个公共构造函数,该构造函数具有类型为 RequestDelegate 的参数。 - 一个名为 Invoke 或 InvokeAsync 的方法。此方法必须: - 返回一个 Task。 - 接受类型为 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 方法的签名?”
RequestDelegate 的签名与 CustomMiddleware.Invoke 不匹配。但是,RequestDelegate 不需要与 CustomMiddleware.Invoke 的签名匹配。所有 RequestDelegate(即 next)关心的是将相同的 HttpContext 实例传递给中间件链,并且由于约定(如上所述),它总是会有这个实例(强调添加)。最后,next 并不直接调用 CustomMiddleware.Invoke。在中间件之间,DI 有机会注入下一个中间件所需的服务。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接