Search code examples
c#dependency-injection

Registering as a Delegate then resolving as a Func in Microsoft DI Factory Method pattern


Let's assume that we have the following bunch of similar objects

public class ViewModel1
{
    public delegate ViewModel1 Factory(List<string> data);

    public ViewModel1(List<string> data, IOtherService service)
    {
    }
}

public class ViewModel2
{
    public delegate ViewModel2 Factory(int data);

    public ViewModel2(int data, IOtherService service)
    {
    }
}
// and so on

When creating such an object, we want to resolve IOtherService from the container and pass the data argument from the business logic (the type of the data can be any and different for different objects).

Typically this is done using the factory methods

//register
services.AddScoped<ViewModel1.Factory>(sp =>
{
    var factory = ActivatorUtilities.CreateFactory(typeof(ViewModel1), new[] { typeof(List<string>) });
    return data => (ViewModel1)factory.Invoke(sp, new object[] { data });
});

//use
var factory = sp.GetRequiredService<ViewModel1.Factory>();
var vm = factory(new List<string> { "test", "data" });

But I need to generalise such an approach to the something like this:

public static TModel GetVM<TModel, TData>(this ServiceProvider sp, TData data)
{
    //the problem is here: how to get the type of the factory, corresponding to the `TModel`?
    var factory = sp.GetRequiredService<Func<TData, TModel>>();
    var vm = factory(data);
    return vm;
}

//usage
var vm = sp.GetVM<ViewModel2, int>(data: 42); 

So, here we want to resolve the generic type TModel and we can't just resolve the specific factory delegate type like ViewModel2.Factory. For the Autofac we can use Func<TData, TModel> and DI container will find compatible registered delegate and will resolve it successfuly (e.g. Func<int, ViewModel2> -> ViewModel2.Factory) - see Delegate Factories. But MS DI does not do such a things out of the box.

So, the question is: how can I resolve Func<TIn, TRes> into the compatible registered delegate in MS DI?


Solution

  • If anyone will needs this functionality, the solution for the initial answer is below. However, I really suggest to take a look at the @Steven's comments and try to reconsider the design.

    The following code (based on the https://stackoverflow.com/a/72317274/2011071) will register the given delegate as delegate or as a compatible Func depending of the registerAsFunc value. It can be also modified to register both versions (delegate and Func) in the same time.

        public static IServiceCollection AddFactoryDelegate<TDelegate>(this IServiceCollection serviceCollection, bool registerAsFunc = false)
            where TDelegate : Delegate
        {
            var delegateType = typeof(TDelegate);
    
            // The invoke method is what will be called when we try to use the factory delegate
            // Delegate.Invoke is 100% here as we have constraint so TDelegate is a subclass of Delegate
            var invokeMethod = delegateType.GetMethod("Invoke")!;
    
            if (invokeMethod.ReturnType == typeof(void))
            {
                throw new ArgumentException(@"The delegate must have a return type.", nameof(TDelegate));
            }
    
            // Create the factory based on the type we want to create and the parameters of the delegate
            var factory = ActivatorUtilities.CreateFactory(invokeMethod.ReturnType, invokeMethod.GetParameters().Select(p => p.ParameterType).ToArray());
            var factoryExpression = Expression.Constant(factory);
            var factoryMethod = typeof(ObjectFactory).GetMethod("Invoke")!;
    
            // The factory delegate takes an IServiceProvider and a parameters array (object[]), 
            // so we'll need to cast our parameters to object
            var parameterExpressions = invokeMethod.GetParameters().Select(p => Expression.Parameter(p.ParameterType)).ToArray();
            var objectParameterExpressions = parameterExpressions.Select(p => Expression.TypeAs(p, typeof(object)));
    
            // Build our object[] array expression
            var arrayExpression = Expression.NewArrayInit(typeof(object), objectParameterExpressions);
    
            // Create the factory method call, passing the service provider and parameters array
            var serviceProviderParameterExpression = Expression.Parameter(typeof(IServiceProvider));
            var factoryCallExpression = Expression.Call(factoryExpression, factoryMethod, serviceProviderParameterExpression, arrayExpression);
    
            // The factory method returns object, so we need to cast that to the return type
            var resultConversionExpression = Expression.Convert(factoryCallExpression, invokeMethod.ReturnType);
    
            // Now we can construct our delegate that takes the parameters and returns the instantiated type
            var delegateLambdaExpression = registerAsFunc
                ? Expression.Lambda(resultConversionExpression, parameterExpressions)
                : Expression.Lambda<TDelegate>(resultConversionExpression, parameterExpressions);
    
            // Finally we need to wrap that up in a lambda method
            // that takes an IServiceProvider and returns the delegate as an object:
            var delegateConstructor = Expression.Lambda<Func<IServiceProvider, object>>(delegateLambdaExpression, serviceProviderParameterExpression);
    
            // Compile the delegate method factory lambda
            var compiledDelegateFactory = delegateConstructor.Compile();
    
            // Register the factory against the container
            serviceCollection.Add(new ServiceDescriptor(GetTypeForRegister(), compiledDelegateFactory, ServiceLifetime.Singleton));
    
            return serviceCollection;
    
            Type GetTypeForRegister()
            {
                if (!registerAsFunc)
                {
                    return typeof(TDelegate);
                }
                else
                {
                    // we need to build the type to register => typeof(Func<arg0, arg1, ..., res>);
                    var funcType = Type.GetType($"System.Func`{parameterExpressions.Length + 1}")?.MakeGenericType(parameterExpressions.Select(x => x.Type).Append(invokeMethod.ReturnType).ToArray());
                    return funcType ?? throw new Exception("Can't create Func type for the given delegate arguments list and return value");
                }
            }
        }
    

    Usage:

    services.AddFactoryDelegate<ViewModel2.Factory>(true);