The code below does not work when RegistrationBuilder
is used. When the RegistrationBuilder
is not added to the AssemblyCatalog constructor, type constrained generics work.
[TestClass]
public class TypeConstraints
{
[TestMethod]
public void TypeConstraintTest()
{
var rb = new RegistrationBuilder();
var a = new AssemblyCatalog(Assembly.GetExecutingAssembly(), rb);
//var a = new AssemblyCatalog(Assembly.GetExecutingAssembly()); //Works!
var aggr = new AggregateCatalog(a);
var c = new CompositionContainer(aggr);
var item = c.GetExportedValue<IConstrained<Item>>();
Assert.IsNotNull(item);
}
}
public interface IConstrained<T> where T : IItem
{}
[Export(typeof (IConstrained<>))]
public class Constrained<T> : IConstrained<T> where T : IItem
{}
public class Item : IItem
{}
public interface IItem
{}
First of all let’s describe what exactly causes this behavior.
The RegistrationBuilder wraps the actual types of the assembly in a proxy type called CustomType. This proxy more or less only exists to give the RegistrationBuilder the opportunity to inject the Export and Import attributes on the fly.
Sadly this Proxy also return wrapped types when you call GetGenericParameterConstraints. So it is not a RuntimType IItem you get it’s a CustomType IItem. When you try to get an export for IConstrained the AssemblyCatalog checks a lot of things to tell if your export matches you import. One of these checks is if the generic type constraint is satisfied. It’s more or less a check like this. (Simplified)
exportToCheck.GenericTypeConstraints[0].IsAssignableFrom(typeof(Item))
The IsAssignableForm method of the CustomType is implemented like this.
public override bool IsAssignableFrom(Type c)
{
ProjectingType projectingType = c as ProjectingType;
return !(projectingType == null) && this.Projector == projectingType.Projector &&
base.UnderlyingType.IsAssignableFrom(projectingType.UnderlyingType);
}
It only works if you pass another proxy type.
I really think that’s a major bug of the RegistrationBuilder and you should report it to Microsoft Connect.
To work around this issue you have to unproject the GenericTypeContraints saved with your ComposablePartDefinition.
Bad news is that all the relevant classes are internal so you cannot just override the GetGenericParameterConstraints Method.
I solved this issue by inheriting the AssemblyCatalog and unprojecting the constraint types manually.
public class MyAssemblyCatalog : AssemblyCatalog { private Func unprojectDelegate;
private bool projectionsChecked = false;
public MyAssemblyCatalog(Assembly assembly, CustomReflectionContext reflectionContext)
: base(assembly, reflectionContext)
{
this.ReflectionContext = reflectionContext;
}
public CustomReflectionContext ReflectionContext { get; private set; }
public Type Unproject(Type type)
{
if (this.unprojectDelegate == null) {
var param = Expression.Parameter(typeof(CustomReflectionContext));
var param2 = Expression.Parameter(typeof(Type));
var prop = Expression.Property(param, param.Type.GetProperty("Projector", BindingFlags.Instance | BindingFlags.NonPublic));
var method = prop.Type.GetMethod("Unproject", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(Type) }, null);
var body = Expression.Call(prop, method, param2);
this.unprojectDelegate = Expression.Lambda<Func<CustomReflectionContext, Type, Type>>(body, param, param2).Compile();
}
return unprojectDelegate(this.ReflectionContext, type);
}
private void EnsureUnprojectedGenericTypeConstraints()
{
if (!this.projectionsChecked) {
foreach (var item in this) {
object value1;
if (item.Metadata.TryGetValue("System.ComponentModel.Composition.GenericParameterConstraints", out value1)) {
var items = (object[])value1;
foreach (var entry in items) {
var types = entry as Type[];
if (types != null) {
for (int i = 0; i < types.Length; i++) {
types[i] = Unproject(types[i]);
}
}
}
}
}
projectionsChecked = true;
}
}
public override System.Collections.Generic.IEnumerable<Tuple<ComposablePartDefinition, ExportDefinition>> GetExports(ImportDefinition definition)
{
EnsureUnprojectedGenericTypeConstraints();
return base.GetExports(definition);
}
}
Now the Test works.
[TestMethod]
public void TypeConstraintTest()
{
var rb = new RegistrationBuilder();
var a = new MyAssemblyCatalog(Assembly.GetExecutingAssembly(), rb);
var aggr = new AggregateCatalog(a);
var c = new CompositionContainer(aggr);
var item = c.GetExportedValue<IConstrained<Item>>();
Assert.IsNotNull(item);
}