RongQing Zhang
RongQing Zhang

Reputation: 13

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.

Upvotes: 1

Views: 83

Answers (1)

Ivan Petrov
Ivan Petrov

Reputation: 4802

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.

Upvotes: 3

Related Questions