Reputation: 13
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<>));
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);
}
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
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