Surpising ImportMany behavior with RegistrationBuilder

Feb 22, 2012 at 3:48 PM
Edited Feb 22, 2012 at 3:51 PM

I've run into an issue with RegistrationBuilder that I found somewhat surprising, and I might be inclined to think it could be a bug.

Here is my situation: I have a service (call it SomeOtherService) that depends on another service (call it ISomeService).  SomeOtherService accepts an ISomeService instance in its constructor:
public SomeOtherService( ISomeService someService )
{
   this.someService = someService;
}

I set up a registration builder that registers these services with a couple lines of code.  Easy peasy. However when I attempt to resolve an instance of SomeOtherService, MEF throws an exception:

The importing constructor on type 'MefRegistrationIssue.SomeOtherService' is using ImportManyAttribute on parameter 'someService' with a non-assignable type. On constructor parameters the ImportManyAttribute only supports importing into types T[] or IEnumerable<T>.

I was confused by this for a bit, because clearly the SomeOtherService constructor accepts a single instance, not an enumerable.
The catch is that ISomeService itself inherits from IEnumerable<string>, which seems to trick MEF into registering the import as a ImportMany.  Looking at the MEF source code (it's awesome that you put it out there!), the offending line of code is in ImportBuilder.BuildAttributes:

bool asMany = (!this._asManySpecified) ? type != StringType && typeof(IEnumerable).IsAssignableFrom(type) : this._asMany;

The condition typeof(IEnumerable).IsAssignableFrom(type) is obviously the culprit here, since ISomeService is assignable to IEnumerable.  I wonder if the condition needs to be more clever here, for example by matching exactly on IEnumerable/IEnumerable<T> instead of checking for type compatibility.  Or is this working exactly as intended?

For what its worth, the actual services I am working with are third party code, and I don't have the freedom to alter the interfaces.

 

Heres the test case:

using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Registration;
using System.Reflection;

namespace MefRegistrationIssue
{
 public interface ISomeService : IEnumerable<string>
 {
  int GetANumber();
 }
 
 public class SomeService : ISomeService
 {
  public int GetANumber()
  {
   return 2;
  }

  public IEnumerator<string> GetEnumerator()
  {
   yield return "foo";
   yield return "bar";
  }

  IEnumerator IEnumerable.GetEnumerator()
  {
   return this.GetEnumerator();
  }
 }

 public class SomeOtherService
 {
  public SomeOtherService( ISomeService someService )
  {
   this.someService = someService;
  }

  public int DoWork()
  {
   return this.someService.GetANumber();
  }

  ISomeService someService;
 }


 class Program
 {
  static void Main( string[] args )
  {
     var builder = new RegistrationBuilder();
     builder.ForType<SomeService>().Export<ISomeService>();
     builder.ForType<SomeOtherService>().Export();

     var container = new CompositionContainer( 
        new AssemblyCatalog( Assembly.GetExecutingAssembly(), builder ) );
   
     var someOtherService =   container.GetExportedValue<SomeOtherService>();
     someOtherService.DoWork();
  }

 }
}

 

Feb 22, 2012 at 5:20 PM

Hi there – thanks for the report.

This was by design, though we can see that the experience here could use a bit of improvement.

There are two ways to switch this import back to a “single” import:

1. Place the [ImportingConstructor] attribute on the SomeOtherService constructor and apply an [Import] attribute to the someService parameter

2. In the registration rule for SomeOtherService, use .AsMany(false) to configure the someService parameter

Does either option work in your scenario?

Cheers,
Nick

From: jlewicki [email removed]
Sent: Wednesday, February 22, 2012 8:49 AM
To: Nicholas Blumhardt
Subject: Surpising ImportMany behavior with RegistrationBuilder [MEF:341470]

From: jlewicki

I've run into an issue with RegistrationBuilder that I found somewhat surprising, and I might be inclined to think it could be a bug.

The issue I ran into was that I have a service (call it SomeOtherService) that depends on another service (call it ISomeService). SomeOtherService accepts a ISomeService instance in its constructor:
public SomeOtherService( ISomeService someService )
{
this.someService = someService;
}

I set up a registration builder that registers these services with a couple lines of code. Easy peasy. However when I attempt to resolve an instance of SomeOtherService, MEF throws an exception:

The importing constructor on type 'MefRegistrationIssue.SomeOtherService' is using ImportManyAttribute on parameter 'someService' with a non-assignable type. On constructor parameters the ImportManyAttribute only supports importing into types T[] or IEnumerable<T>.

I was confused by this for a bit, because clearly the SomeOtherService constructor accepts a single instance, not an enumerable.
The catch is that ISomeService itself inherits from IEnumerable<string>, which seems to trick MEF into registering the import as a ImportMany. Looking at the MEF source code (it's awesome that you put it out there!), the offending line of code is in ImportBuilder.BuildAttributes:

bool asMany = (!this._asManySpecified) ? type != StringType && typeof(IEnumerable).IsAssignableFrom(type) : this._asMany;

The condition typeof(IEnumerable).IsAssignableFrom(type) is obviously the culprit here, since ISomeService is assignable to IEnumerable. I wonder if the condition needs to be more clever here, for example by matching exactly on IEnumerable/IEnumerable<T> instead of checking for type compatibility. Or is this working exactly as intended?

Heres the test case:

using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Registration;
using System.Reflection;
 
namespace MefRegistrationIssue
{
 public interface ISomeService : IEnumerable<string>
 {
  int GetANumber();
 }
 
 public class SomeService : ISomeService
 {
  public int GetANumber()
  {
   return 2;
  }
 
  public IEnumerator<string> GetEnumerator()
  {
   yield return "foo";
   yield return "bar";
  }
 
  IEnumerator IEnumerable.GetEnumerator()
  {
   return this.GetEnumerator();
  }
 }
 
 public class SomeOtherService
 {
  public SomeOtherService( ISomeService someService )
  {
   this.someService = someService;
  }
 
  public int DoWork()
  {
   return this.someService.GetANumber();
  }
 
  ISomeService someService;
 }
 
 
 class Program
 {
  static void Main( string[] args )
  {
     var builder = new RegistrationBuilder();
     builder.ForType<SomeService>().Export<ISomeService>();
     builder.ForType<SomeOtherService>().Export();
 
     var container = new CompositionContainer( 
        new AssemblyCatalog( Assembly.GetExecutingAssembly(), builder ) );
   
     var someOtherService =   container.GetExportedValue<SomeOtherService>();
     someOtherService.DoWork();
  }
 
 }
}
 
 
 
Feb 22, 2012 at 5:35 PM

Thanks for the prompt reply.

Per your suggestion, I was able to get around this by using the SelectConstructor overload that provides access to the ImportBuilder, which allowed me to say: ib.AsMany(false). 

Thanks! -John