Here's my implementation of 3 extension methods:
bool CanMakeGenericTypeVia(this Type openConstructedType, Type closedConstructedType)
Type MakeGenericTypeVia(this Type openConstructedType, Type closedConstructedType)
MethodInfo MakeGenericMethodVia(this MethodInfo openConstructedMethod, params Type[] closedConstructedParameterTypes)
The first allows you to check if a closed-constructed type matches an open-constructed type definition. If so, the second can infer all the required type arguments to return a closed-constructed from a given closed-constructed type. Finally, the third method can resolve all this automatically for methods.
Note that these methods will not fail or return false if you pass another open-constructed type as the "closed-constructed" type argument, as long as this second type respects all the type constraints of the initial open-constructed type. They will instead resolve as much type information as possible from the given types. Therefore, if you want to make sure the resolution gave a fully closed-constructed type, you should check that the result's ContainsGenericParameters
returns false. This matches the behaviour of .NET's MakeGenericType
or MakeGenericMethod
.
Also note that I'm not very well informed on co- and contravariance, so these implementations might not be correct in that regard.
Example usage:
public static void GenericMethod<T0, T1>(T0 direct, IEnumerable<T1> generic)
where T0 : struct
where T1 : class, new(), IInterface
{ }
public interface IInterface { }
public class CandidateA : IInterface { private CandidateA(); }
public struct CandidateB : IInterface { }
public class CandidateC { public CandidateC(); }
public class CandidateD : IInterface { public CandidateD(); }
var method = GetMethod("GenericMethod");
var type0 = method.GetParameters()[0].ParameterType;
var type1 = method.GetParameters()[1].ParameterType;
// Results:
type0.CanMakeGenericTypeVia(typeof(int)) // true
type0.CanMakeGenericTypeVia(typeof(IList)) // false, fails struct
type1.CanMakeGenericTypeVia(typeof(IEnumerable<CandidateA>))
// false, fails new()
type1.CanMakeGenericTypeVia(typeof(IEnumerable<CandidateB>))
// false, fails class
type1.CanMakeGenericTypeVia(typeof(IEnumerable<CandidateC>))
// false, fails : IInterface
type1.CanMakeGenericTypeVia(typeof(IEnumerable<CandidateD>))
// true
type0.MakeGenericTypeVia(typeof(int))
// typeof(int)
type1.MakeGenericTypeVia(typeof(List<CandidateD>))
// IEnumerable<CandidateD>
method.MakeGenericMethodVia(123.GetType(), (new CandidateD[0]).GetType())
// GenericMethod(int, IEnumerable<CandidateD>)
method.MakeGenericMethodVia(123.GetType(), type1)
// GenericMethod<T1>(int, IEnumerable<T1>)
// (partial resolution)
Implementation:
public static bool CanMakeGenericTypeVia(this Type openConstructedType, Type closedConstructedType)
{
if (openConstructedType == null)
{
throw new ArgumentNullException("openConstructedType");
}
if (closedConstructedType == null)
{
throw new ArgumentNullException("closedConstructedType");
}
if (openConstructedType.IsGenericParameter) // e.g.: T
{
// The open-constructed type is a generic parameter.
// First, check if all special attribute constraints are respected.
var constraintAttributes = openConstructedType.GenericParameterAttributes;
if (constraintAttributes != GenericParameterAttributes.None)
{
// e.g.: where T : struct
if (constraintAttributes.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint) &&
!closedConstructedType.IsValueType)
{
return false;
}
// e.g.: where T : class
if (constraintAttributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint) &&
closedConstructedType.IsValueType)
{
return false;
}
// e.g.: where T : new()
if (constraintAttributes.HasFlag(GenericParameterAttributes.DefaultConstructorConstraint) &&
closedConstructedType.GetConstructor(Type.EmptyTypes) == null)
{
return false;
}
// TODO: Covariance and contravariance?
}
// Then, check if all type constraints are respected.
// e.g.: where T : BaseType, IInterface1, IInterface2
foreach (var constraint in openConstructedType.GetGenericParameterConstraints())
{
if (!constraint.IsAssignableFrom(closedConstructedType))
{
return false;
}
}
return true;
}
else if (openConstructedType.ContainsGenericParameters)
{
// The open-constructed type is not a generic parameter but contains generic parameters.
// It could be either a generic type or an array.
if (openConstructedType.IsGenericType) // e.g. Generic<T1, int, T2>
{
// The open-constructed type is a generic type.
var openConstructedGenericDefinition = openConstructedType.GetGenericTypeDefinition(); // e.g.: Generic<,,>
var openConstructedGenericArguments = openConstructedType.GetGenericArguments(); // e.g.: { T1, int, T2 }
// Check a list of possible candidate closed-constructed types:
// - the closed-constructed type itself
// - its base type, if any (i.e.: if the closed-constructed type is not object)
// - its implemented interfaces
var inheritedClosedConstructedTypes = new List<Type>();
inheritedClosedConstructedTypes.Add(closedConstructedType);
if (closedConstructedType.BaseType != null)
{
inheritedClosedConstructedTypes.Add(closedConstructedType.BaseType);
}
inheritedClosedConstructedTypes.AddRange(closedConstructedType.GetInterfaces());
foreach (var inheritedClosedConstructedType in inheritedClosedConstructedTypes)
{
if (inheritedClosedConstructedType.IsGenericType &&
inheritedClosedConstructedType.GetGenericTypeDefinition() == openConstructedGenericDefinition)
{
// The inherited closed-constructed type and the open-constructed type share the same generic definition.
var inheritedClosedConstructedGenericArguments = inheritedClosedConstructedType.GetGenericArguments(); // e.g.: { float, int, string }
// For each open-constructed generic argument, recursively check if it
// can be made into a closed-constructed type via the closed-constructed generic argument.
for (int i = 0; i < openConstructedGenericArguments.Length; i++)
{
if (!openConstructedGenericArguments[i].CanMakeGenericTypeVia(inheritedClosedConstructedGenericArguments[i])) // !T1.IsAssignableFromGeneric(float)
{
return false;
}
}
// The inherited closed-constructed type matches the generic definition of
// the open-constructed type and each of its type arguments are assignable to each equivalent type
// argument of the constraint.
return true;
}
}
// The open-constructed type contains generic parameters, but no
// inherited closed-constructed type has a matching generic definition.
return false;
}
else if (openConstructedType.IsArray) // e.g. T[]
{
// The open-constructed type is an array.
if (!closedConstructedType.IsArray ||
closedConstructedType.GetArrayRank() != openConstructedType.GetArrayRank())
{
// Fail if the closed-constructed type isn't an array of the same rank.
return false;
}
var openConstructedElementType = openConstructedType.GetElementType();
var closedConstructedElementType = closedConstructedType.GetElementType();
return openConstructedElementType.CanMakeGenericTypeVia(closedConstructedElementType);
}
else
{
// I don't believe this can ever happen.
throw new NotImplementedException("Open-constructed type contains generic parameters, but is neither an array nor a generic type.");
}
}
else
{
// The open-constructed type does not contain generic parameters,
// we can proceed to a regular closed-type check.
return openConstructedType.IsAssignableFrom(closedConstructedType);
}
}
public static Type MakeGenericTypeVia(this Type openConstructedType, Type closedConstructedType, Dictionary<Type, Type> resolvedGenericParameters, bool safe = true)
{
if (openConstructedType == null)
{
throw new ArgumentNullException("openConstructedType");
}
if (closedConstructedType == null)
{
throw new ArgumentNullException("closedConstructedType");
}
if (resolvedGenericParameters == null)
{
throw new ArgumentNullException("resolvedGenericParameters");
}
if (safe && !openConstructedType.CanMakeGenericTypeVia(closedConstructedType))
{
throw new InvalidOperationException("Open-constructed type is not assignable from closed-constructed type.");
}
if (openConstructedType.IsGenericParameter) // e.g.: T
{
// The open-constructed type is a generic parameter.
// We can directly map it to the closed-constructed type.
// Because this is the lowest possible level of type resolution,
// we will add this entry to our list of resolved generic parameters
// in case we need it later (e.g. for resolving generic methods).
// Note that we allow an open-constructed type to "make" another
// open-constructed type, as long as the former respects all of
// the latter's constraints. Therefore, we will only add the resolved
// parameter to our dictionary if it actually is resolved.
if (!closedConstructedType.ContainsGenericParameters)
{
if (resolvedGenericParameters.ContainsKey(openConstructedType))
{
if (resolvedGenericParameters[openConstructedType] != closedConstructedType)
{
throw new InvalidOperationException("Nested generic parameters resolve to different values.");
}
}
else
{
resolvedGenericParameters.Add(openConstructedType, closedConstructedType);
}
}
return closedConstructedType;
}
else if (openConstructedType.ContainsGenericParameters) // e.g.: Generic<T1, int, T2>
{
// The open-constructed type is not a generic parameter but contains generic parameters.
// It could be either a generic type or an array.
if (openConstructedType.IsGenericType) // e.g. Generic<T1, int, T2>
{
// The open-constructed type is a generic type.
var openConstructedGenericDefinition = openConstructedType.GetGenericTypeDefinition(); // e.g.: Generic<,,>
var openConstructedGenericArguments = openConstructedType.GetGenericArguments(); // e.g.: { T1, int, T2 }
// Check a list of possible candidate closed-constructed types:
// - the closed-constructed type itself
// - its base type, if any (i.e.: if the closed-constructed type is not object)
// - its implemented interfaces
var inheritedCloseConstructedTypes = new List<Type>();
inheritedCloseConstructedTypes.Add(closedConstructedType);
if (closedConstructedType.BaseType != null)
{
inheritedCloseConstructedTypes.Add(closedConstructedType.BaseType);
}
inheritedCloseConstructedTypes.AddRange(closedConstructedType.GetInterfaces());
foreach (var inheritedCloseConstructedType in inheritedCloseConstructedTypes)
{
if (inheritedCloseConstructedType.IsGenericType &&
inheritedCloseConstructedType.GetGenericTypeDefinition() == openConstructedGenericDefinition)
{
// The inherited closed-constructed type and the open-constructed type share the same generic definition.
var inheritedClosedConstructedGenericArguments = inheritedCloseConstructedType.GetGenericArguments(); // e.g.: { float, int, string }
// For each inherited open-constructed type generic argument, recursively resolve it
// via the equivalent closed-constructed type generic argument.
var closedConstructedGenericArguments = new Type[openConstructedGenericArguments.Length];
for (int j = 0; j < openConstructedGenericArguments.Length; j++)
{
closedConstructedGenericArguments[j] = MakeGenericTypeVia
(
openConstructedGenericArguments[j],
inheritedClosedConstructedGenericArguments[j],
resolvedGenericParameters,
safe: false // We recursively checked before, no need to do it again
);
// e.g.: Resolve(T1, float)
}
// Construct the final closed-constructed type from the resolved arguments
return openConstructedGenericDefinition.MakeGenericType(closedConstructedGenericArguments);
}
}
// The open-constructed type contains generic parameters, but no
// inherited closed-constructed type has a matching generic definition.
// This cannot happen in safe mode, but could in unsafe mode.
throw new InvalidOperationException("Open-constructed type is not assignable from closed-constructed type.");
}
else if (openConstructedType.IsArray) // e.g. T[]
{
var arrayRank = openConstructedType.GetArrayRank();
// The open-constructed type is an array.
if (!closedConstructedType.IsArray ||
closedConstructedType.GetArrayRank() != arrayRank)
{
// Fail if the closed-constructed type isn't an array of the same rank.
// This cannot happen in safe mode, but could in unsafe mode.
throw new InvalidOperationException("Open-constructed type is not assignable from closed-constructed type.");
}
var openConstructedElementType = openConstructedType.GetElementType();
var closedConstructedElementType = closedConstructedType.GetElementType();
return openConstructedElementType.MakeGenericTypeVia
(
closedConstructedElementType,
resolvedGenericParameters,
safe: false
).MakeArrayType(arrayRank);
}
else
{
// I don't believe this can ever happen.
throw new NotImplementedException("Open-constructed type contains generic parameters, but is neither an array nor a generic type.");
}
}
else
{
// The open-constructed type does not contain generic parameters,
// it is by definition already resolved.
return openConstructedType;
}
}
public static MethodInfo MakeGenericMethodVia(this MethodInfo openConstructedMethod, params Type[] closedConstructedParameterTypes)
{
if (openConstructedMethod == null)
{
throw new ArgumentNullException("openConstructedMethod");
}
if (closedConstructedParameterTypes == null)
{
throw new ArgumentNullException("closedConstructedParameterTypes");
}
if (!openConstructedMethod.ContainsGenericParameters)
{
// The method contains no generic parameters,
// it is by definition already resolved.
return openConstructedMethod;
}
var openConstructedParameterTypes = openConstructedMethod.GetParameters().Select(p => p.ParameterType).ToArray();
if (openConstructedParameterTypes.Length != closedConstructedParameterTypes.Length)
{
throw new ArgumentOutOfRangeException("closedConstructedParameterTypes");
}
var resolvedGenericParameters = new Dictionary<Type, Type>();
for (int i = 0; i < openConstructedParameterTypes.Length; i++)
{
// Resolve each open-constructed parameter type via the equivalent
// closed-constructed parameter type.
var openConstructedParameterType = openConstructedParameterTypes[i];
var closedConstructedParameterType = closedConstructedParameterTypes[i];
openConstructedParameterType.MakeGenericTypeVia(closedConstructedParameterType, resolvedGenericParameters);
}
// Construct the final closed-constructed method from the resolved arguments
var openConstructedGenericArguments = openConstructedMethod.GetGenericArguments();
var closedConstructedGenericArguments = openConstructedGenericArguments.Select(openConstructedGenericArgument =>
{
// If the generic argument has been successfully resolved, use it;
// otherwise, leave the open-constructe argument in place.
if (resolvedGenericParameters.ContainsKey(openConstructedGenericArgument))
{
return resolvedGenericParameters[openConstructedGenericArgument];
}
else
{
return openConstructedGenericArgument;
}
}).ToArray();
return openConstructedMethod.MakeGenericMethod(closedConstructedGenericArguments);
}