Home > Back-end >  Generically Register All Concrete types for Lazy Loading in DI
Generically Register All Concrete types for Lazy Loading in DI

Time:05-07

I want to setup Lazy Loading of registered types in a generic way that does not require me to register each one individually.

I can create an extension method to simultaneously register lazily like so:

public static IServiceCollection AddLazyTransient<T>(this IServiceCollection services) 
    where T : class
{
    return services
        .AddTransient<T>()
        .AddTransient<Func<T>>(s => s.GetRequiredService<T>);
}

Which works fine, until unfortunately when my T now a Type, for example I am using Assembly style registration to get all classes in my WPF project which inherit from Window like so:

public static IServiceCollection RegisterWindows<T>(this IServiceCollection services)
{
    var assemblyName = typeof(T).Assembly.FullName ??
        throw new ApplicationException(
            $"No assembly name for type '{typeof(T).FullName}'");
    var assembly = AppDomain.CurrentDomain.Load(assemblyName);

    var windowTypes = assembly.GetTypes()
        .Where(t => t.IsClass && !t.IsAbstract && t.IsAssignableTo(typeof(Window)))
        .ToList();

    foreach (var windowType in windowTypes)
    {
        services.AddTransient(windowType);

        // But how do I register a Func<Type> here now?

    }

    return services;
}

I have tried the following:

var windowFuncType = Expression.GetFuncType(windowType);
services.AddTransient(windowFuncType, s => () => s.GetRequiredService(windowType));

But that gives the following error:

System.InvalidCastException: 'Unable to cast object of type System.Func'1[System.Object]' to type 'System.Func'1[MyApp.Wpf.MainWindow]'.'

Which makes sense. ServiceProvider.GetRequiredService(Type) returns an object, so I am registering a Func<object> when it is expecting a Func<MainWindow>

So I need to convert Func<object> to Func<Type> manually via reflection, in the middle of the registration, how do I do that though?

CodePudding user response:

There are probably many ways to do this, but the simplest that comes to my mind is to just call the generic AddLazyTransient<T> method using reflection from inside the non-generic AddLazyTransient method. For instance:

public static void AddLazyTransient(this IServiceCollection services, Type type)
{
    // Define an expression that contains a reference to the method to call
    Expression<Action> expression = () => AddLazyTransient<object>(null);

    // Extract the method from the expression
    var addLazyTransientOfObject = ((MethodCallExpression)expression.Body).Method;

    // Convert it to a generic type definition.
    var addLazyTransientOfT = addLazyTransientOfObject.GetGenericMethodDefinition();

    // Convert it to the exact closed method definition we require
    var addLazyTransientOfType = addLazyTransientOfT.MakeGenericMethod(type);

    // Call the method with its required parameter.
    addLazyTransientOfType.Invoke(null, new[] { services });
}

There are certainly other ways of tackling this problem, such as calling AddTransient by supplying a typeof(Func<>).MakeGenericType(type) and a delegate that is compiled from the original Func<object> with a cast, which can be done using Expression.Convert, Expression.Invoke, and Expression.Lambda.Compile(), but as I said, I think this is easier.

  • Related