Search code examples
c#.netgenericsdependency-injectionreflection

How to register a generic type using Microsoft.Extensions.Hosting by reflection?


I have generic classes:

public interface ICommon<T>
{
    public string Test();
}

public class Common<T>  : ICommon<T> 
{
    public string Test()
    {
        return "Common";
    }
}

I want to register it to IHost. If I write it like this (one way), it'll get registered:

services.TryAddScoped(typeof(ICommon<>), typeof(Common<>));

ServiceDescriptor like this

If I write it like this (two way), it'll get Exception:

var types = assembly.GetTypes().Where(t => !t.IsInterface && !t.IsAbstract);
foreach (var type in types)
{
    var name = type.Name;
    var interfaceType = type.GetInterfaces().ToList().Find(p => p.Name.Equals($"I{name}"));
    if (interfaceType is null)
    {
        continue;
    }
    services.TryAddSingleton(interfaceType, type);
}

ServiceDescriptor like this

Is there any way I can register generic classes through reflection?

I tried Microsoft.Extensions.Hosting.9.0.2 Microsoft.Extensions.Hosting.8.0.1.
It's the same thing.


Solution

  • Calling GetInterfaces() on an open-generic Common<> type does not return the open-generic interface type ICommon<> - what you need to register with the DI.

    Instead, it returns a closed-generic ICommon<T> where T is the generic parameter found in the open-generic Common<>'s placeholder type parameters (see below for examples). You need to extract the (open) generic type definition from this type instance.

    var types = assembly.GetTypes().Where(t => !t.IsInterface && !t.IsAbstract 
    && t.IsGenericTypeDefinition); 
    // added IsGenericTypeDefinition to make sure we are registering Common<>, not some compiler generated Common<T>
    
    foreach (var type in types) {
        var genericArguments = type.GetGenericArguments();
        var interfaceType = type.GetInterfaces()
                    .Where(i => i.IsGenericType)
                    .Where(i => i.Name == $"I{type.Name}")
                    // just for edge case where Common<T> might implement
                    // multiple ICommon
                    // ICommon<T>,ICommon<List<T>>
                    .Where(i => i.GetGenericArguments().SequenceEqual(genericArguments))
                    .FirstOrDefault();
        if (interfaceType is null) {
            continue;
        }
    
        // in reality we need to maybe always get 
        // the GenericTypeDefinition
        interfaceType = interfaceType.IsGenericTypeDefinition ?
            interfaceType : interfaceType.GetGenericTypeDefinition();
        services.TryAddSingleton(interfaceType, type);
    }
    

    This behavior for GetInterfaces() is a bit strange and I'd say not really documented. The docs only touch on constructed generic type:

    If the current Type represents a constructed generic type, this method returns the Type objects with the type parameters replaced by the appropriate type arguments.

    However, Common<> in our case is generic type definition, not a constructed generic type - we haven't specified what T is. From docs:

    A constructed generic type, or constructed type, is the result of specifying types for the generic type parameters of a generic type definition.

    A bit of code demonstrating what's happening:

    class A<T> : I<T> { }
    
    interface I<T> { }
    
    var openGenericType = typeof(A<>);
    openGenericType.IsGenericTypeDefinition.Dump(); // True
    openGenericType.ContainsGenericParameters.Dump(); // True
    
    // get generic parameter type
    var openGenericTypeParameter = openGenericType
        .GetGenericArguments().FirstOrDefault();
    openGenericTypeParameter.Dump(); // typeof(T)
    Console.WriteLine("########################");
    
    var openGenericInterfaceType = typeof(I<>);
    openGenericInterfaceType.IsGenericTypeDefinition.Dump(); //True
    openGenericInterfaceType.ContainsGenericParameters.Dump(); // True
    
    // get generic parameter type
    var openGenericInterfaceTypeParameter = openGenericInterfaceType
        .GetGenericArguments().FirstOrDefault();
    openGenericInterfaceTypeParameter.Dump(); // typeof(T)
    Console.WriteLine("########################");
    
    // however the above two ARE different "placeholder" parameter types
    (openGenericTypeParameter == openGenericInterfaceTypeParameter)
        .Dump(); // False
    Console.WriteLine("########################");
    
    
    var closedInterfaceType = openGenericType
        .GetInterfaces().FirstOrDefault();
    
    // GetInterfaces creates a closed generic
    closedInterfaceType.IsGenericTypeDefinition.Dump(); // False
    
    // based on the placeholder paramater type of
    // the openGeneric (not the interface)
    (closedInterfaceType.GetGenericArguments()[0] ==
    openGenericTypeParameter).Dump(); // True
    
    (closedInterfaceType.GetGenericArguments()[0] ==
        openGenericInterfaceTypeParameter).Dump(); // False
    
    // Mimick what GetInterfaces does
    var ourClosedGeneric = openGenericInterfaceType
        .MakeGenericType(openGenericTypeParameter);
    
    (closedInterfaceType == ourClosedGeneric).Dump(); // True
    

    Best explanation I could find as to why we have this comes from a github issue by jkotas:

    Also, note that one type can implement multiple generic interfaces instantiated over different arguments, e.g.

    using System;
    using System.Collections.Generic;
    
    foreach (var iface in typeof(G<>).GetInterfaces())
    {
       Console.WriteLine(iface);
       Console.WriteLine(iface == typeof(I<>));
       Console.WriteLine(iface.GetGenericTypeDefinition() == typeof(I<>));
    }
    
    interface I<T>
    {
    }
    
    class G<T> : I<T>, I<List<T>>
    {
    }
    

    In this example there needs to be a way to differentiate between I<T> and I<List<T> - GetInterfaces() cannot just return ONE open-generic definition I<>.

    Another point I can think of is that G<T> can inherit Base<string instead of Base<T> - so a decision has to been made to always "instantiate" the open-generic base/interface for a given class into a closed-generic to make matters more consistent across different cases.