user9623401
user9623401

Reputation:

how does dependency injection work with Middleware?

I saw some code lilke this:

public class CustomMiddleware {
    private RequestDelegate next;

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

and IResponseFormatter service is registered as:

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

I know how DI works, but my understanding how middleware works is, next(RequestDelegate) represents the next middleware's Invoke method, so in CustomMiddleware, even the second argument is resolved by DI, but the definition of RequestDelegate is

public delegate Task RequestDelegate(HttpContext context);

how does the previous middleware before CustomMiddleware knows that CustomMiddleware's Invoke method has changed by having an extra argument? It cannot know in advance, therefore the previous middleware's next RequestDelegate does't match the signature of CustomMiddleware's Invoke method?

Upvotes: 4

Views: 5652

Answers (2)

Connor Low
Connor Low

Reputation: 7226

How does the previous middleware before CustomMiddleware know that CustomMiddleware's Invoke method has changed by having an extra argument?

Because of convention (and reflection).

Custom Middleware doesn't inherit any interfaces or base classes, so the only way for the runtime to know how to use the middleware is by convention:

The middleware class must include:

  • A public constructor with a parameter of type RequestDelegate.
  • A method named Invoke or InvokeAsync. This method must:
    • Return a Task.
    • Accept a first parameter of type HttpContext.

With this knowledge, safely run the middleware: you can use reflection to get the dependencies of Invoke and execute it knowing the return type is Task. For example:

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[] **/);

It cannot know in advance, therefore the previous middleware's next RequestDelegate does't match the signature of CustomMiddleware's Invoke method?

RequestDelegate does not match the signature of CustomMiddleware.Invoke.
RequestDelegate does not need to match the signature of CustomMiddleware.Invoke.

All the RequestDelegate (next) cares about is passing the same HttpContext instance down the middleware chain, and it will always have that because of the (previously mentioned) convention (emphasis added):

  • A method named Invoke or InvokeAsync. This method must:
    • Return a Task.
    • Accept a first parameter of type HttpContext.

Finally, next is not directly calling CustomMiddleware.Invoke. In between middlewares, DI has the opportunity to inject services needed for the next middleware.

Upvotes: 2

Nkosi
Nkosi

Reputation: 247471

Internally, framework code uses reflection to determine arguments for both constructor and Invoke member of the middleware by the following convention

The middleware class must include:

  • A public constructor with a parameter of type RequestDelegate.
  • A public method named Invoke or InvokeAsync. This method must:
    • Return a Task.
    • Accept a first parameter of type HttpContext.

Additional parameters for the constructor and Invoke/InvokeAsync are populated by dependency injection (DI).

Reference Write custom ASP.NET Core middleware

As demonstrated from the 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, 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);
        };
    });
}

Upvotes: 3

Related Questions