Coding, Testing

When is the ExportProvider Interesting?

Recently, when writing about the CompositionTests API (here) I wrote that our test should focus on the interesting part of the composition and that most of the time the catalog is the only interesting item.  Later I wrote that there were other scenarios where the ExportProvider was also interesting and when I wrote that I was thinking of custom ExportProvider implementations.  Just Google “mef custom exportprovider” and you will find a wealth of articles written by people who created their own ExportProviders to adapt MEF for their specific needs.  I think I even built one back when I first started out with MEF.

But lets say you haven’t needed or wanted to roll your own ExportProvider.  Does trusty old CompositionContainer ever effect composition?  Can it ever be interesting enough to motivate you to use VerifyCompositionInfo?

A Simple Mailer

Here’s an example of when you might want to use VerifyCompositionInfo because the CompositionContainer is interesting.  Say you are working on an application that wants to send emails. You might start with a little mail sender like this:

[Export(typeof(ISendMail))]
public class BasicMailSender : ISendMail
{
    [ImportingConstructor]
    public BasicMailSender(
        SmtpHost host,
        Port port)
    {
        this.port = port;
        this.host = host;
    }

    public void SendMessage(MailMessage message)
    {
        // ...
    }
}

You have read the CompositionTests readme, and written a simple Program class that allows you to share your ComposablePartCatalog (in this case a TypeCatalog) with your test.

public class Program
{
    static Program()
    {
        Catalog = new TypeCatalog(typeof(BasicMailSender));
        Host = new CompositionContainer(Catalog);
    }

    public static TypeCatalog Catalog { get; private set; }

    public static CompositionContainer Host { get; private set; }

    public static void Main(string[] args)
    {
    }
}

And written a test.

using ApprovalTests.Reporters;
using CompositionTests;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MailApp.Tests
{
    [TestClass]
    [UseReporter(typeof(DiffReporter))]
    public class CompositionTest
    {
        [TestMethod]
        public void VerifyComposition()
        {
            MefComposition.VerifyTypeCatalog(Program.Catalog);
        }
    }
}

Given our current setup, we would expect an unhappy composition, shown below.

[Part] MailApp.BasicMailSender from: TypeCatalog (Types='MailApp.BasicMailSender').
  [Primary Rejection]
  [Export] MailApp.BasicMailSender (ContractName="MailApp.BasicMailSender")
  [Import] MailApp.BasicMailSender..ctor (Parameter="host", ContractName="MailApp.SmtpHost")
    [Exception] System.ComponentModel.Composition.ImportCardinalityMismatchException: No exports were found that match the constraint: 
    ContractName    MailApp.SmtpHost
    RequiredTypeIdentity    MailApp.SmtpHost
   at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition, AtomicComposition atomicComposition)
   at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition)
   at Microsoft.ComponentModel.Composition.Diagnostics.CompositionInfo.AnalyzeImportDefinition(ExportProvider host, IEnumerable`1 availableParts, ImportDefinition id)
  [Import] MailApp.BasicMailSender..ctor (Parameter="port", ContractName="MailApp.Port")
    [Exception] System.ComponentModel.Composition.ImportCardinalityMismatchException: No exports were found that match the constraint: 
    ContractName    MailApp.Port
    RequiredTypeIdentity    MailApp.Port
   at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition, AtomicComposition atomicComposition)
   at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition)
   at Microsoft.ComponentModel.Composition.Diagnostics.CompositionInfo.AnalyzeImportDefinition(ExportProvider host, IEnumerable`1 availableParts, ImportDefinition id)

So we need to provide SmtpHost and Port implementations. The decision on the Port implementation is easy. While we might want to use a non-default Port during testing, we feel pretty safe determining that we want to use the default SMTP port (25) in production. So we can whip up a DefaultPort implementation and export it.

[Export(typeof(Port))]
public class DefaultPort : Port
{
    public DefaultPort() :
        base(25)
    {
    }
}

Everything works as expected if we update our TypeCatalog with this new type.

static Program()
{
    Catalog = new TypeCatalog(typeof(BasicMailSender), typeof(DefaultPort));
    Host = new CompositionContainer(Catalog);
}

And now the test produces this result:

[Part] MailApp.BasicMailSender from: TypeCatalog (Types='MailApp.BasicMailSender, MailApp.DefaultPort').
  [Primary Rejection]
  [Export] MailApp.BasicMailSender (ContractName="MailApp.BasicMailSender")
  [Import] MailApp.BasicMailSender..ctor (Parameter="host", ContractName="MailApp.SmtpHost")
    [Exception] System.ComponentModel.Composition.ImportCardinalityMismatchException: No exports were found that match the constraint: 
    ContractName    MailApp.SmtpHost
    RequiredTypeIdentity    MailApp.SmtpHost
   at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition, AtomicComposition atomicComposition)
   at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition)
   at Microsoft.ComponentModel.Composition.Diagnostics.CompositionInfo.AnalyzeImportDefinition(ExportProvider host, IEnumerable`1 availableParts, ImportDefinition id)
  [Import] MailApp.BasicMailSender..ctor (Parameter="port", ContractName="MailApp.Port")
    [SatisfiedBy] MailApp.DefaultPort (ContractName="MailApp.Port") from: MailApp.DefaultPort from: TypeCatalog (Types='MailApp.BasicMailSender, MailApp.DefaultPort').

[Part] MailApp.DefaultPort from: TypeCatalog (Types='MailApp.BasicMailSender, MailApp.DefaultPort').
  [Export] MailApp.DefaultPort (ContractName="MailApp.Port")

The Port is now satisfied, but SmtpHost is trickier. While the default port for SMTP is defined by standard, there is no such thing as a default server. Now, I would note that one option is to simply approve this result. There is no rule that says you can only approve a CompositionTest when it has no exceptions. The fact that SmtpHost can’t be satisfied could just mean that this part must be provided by a third-party at deployment time. However, there are two reasons why I’ll try to satisfy all of the parts in this test.

  • Ignoring exceptions is like ignoring warnings. When there are only a few you can say to yourself, “Well, that one is there for a reason, and I understand why.” But when the number of warnings/exceptions grow, the signal to noise ratio rises and eventually obscures real errors.
  • If I don’t try to satisfy the part, I won’t be able to show you a scenario where the ExportProvider is interesting.

Lets say that we have a SmtpHost in our test project that we use for development, it points at the localhost. When making these kind of test collaborators I usually nest the Testing class inside the test it supports.

[TestClass]
[UseReporter(typeof(DiffReporter))]
public class CompositionTest
{
    [TestMethod]
    public void VerifyComposition()
    {
        MefComposition.VerifyTypeCatalog(Program.Catalog);
    }

    [Export(typeof(SmtpHost))]
    public class TestingSmtpHost : SmtpHost
    {
        public TestingSmtpHost()
            : base("localhost")
        {
        }
    }
}

It would be nice to use this part when verifying composition, but since it is in our test class, our production code can’t see it. One way around this is to add an instance to our CompositionContainer using a CompositionBatch. We’ll need to add a method, Program.AddInstance, to allow this.

public class Program
{
    static Program()
    {
        Catalog = new TypeCatalog(typeof(BasicMailSender), typeof(DefaultPort));
        Host = new CompositionContainer(Catalog);
    }

    public static TypeCatalog Catalog { get; private set; }

    public static CompositionContainer Host { get; private set; }

    public static void AddInstance(object part)
    {
        var batch = new CompositionBatch();
        batch.AddPart(part);
        Host.Compose(batch);
    }

    // ...
}

Now we can add a class constructor to our test, so that Program is only manipulated once during the course of the test.

[TestClass]
[UseReporter(typeof(DiffReporter))]
public class CompositionTest
{
    static CompositionTest()
    {
        Program.AddInstance(new TestingSmtpHost());
    }

Note that you can’t just throw any object into the method and expect it to compose, it has to be an attributed MEF part. Ok, now we might expect our test result to show a happy composition, but the BasicMailSender still fails to compose.

[Import] MailApp.BasicMailSender..ctor (Parameter="host", ContractName="MailApp.SmtpHost")
  [Exception] System.ComponentModel.Composition.ImportCardinalityMismatchException: No exports were found that match the constraint: 
    ContractName    MailApp.SmtpHost
    RequiredTypeIdentity    MailApp.SmtpHost

What’s up with that? Look closely at the AddInstance method. Nowhere in that method do we manipulate the Catalog.  When we use CompositionBatch, we are adding part directly to the CompositionContainer–outside the catalog. So, our ExportProvider (in this case just a plain old CompositionContainer) is suddenly interesting. Without it, there is no way for the CompositionInfo used by CompositionTests to know about the instance of TestingSmtpHost that we added.

Lets rewrite our test to use an overload of VerifyCompositionInfo so that we can pass in the ExportProvider.

[TestMethod]
public void VerifyComposition()
{
    MefComposition.VerifyCompositionInfo(Program.Catalog, Program.Host);
}

And now our composition is happy.

[Part] MailApp.BasicMailSender from: TypeCatalog (Types='MailApp.BasicMailSender, MailApp.DefaultPort').
  [Export] MailApp.BasicMailSender (ContractName="MailApp.BasicMailSender")
  [Import] MailApp.BasicMailSender..ctor (Parameter="host", ContractName="MailApp.SmtpHost")
    [SatisfiedBy] MailApp.Tests.CompositionTest+TestingSmtpHost (ContractName="MailApp.SmtpHost") from: MailApp.Tests.CompositionTest+TestingSmtpHost
  [Import] MailApp.BasicMailSender..ctor (Parameter="port", ContractName="MailApp.Port")
    [SatisfiedBy] MailApp.DefaultPort (ContractName="MailApp.Port") from: MailApp.DefaultPort from: TypeCatalog (Types='MailApp.BasicMailSender, MailApp.DefaultPort').

[Part] MailApp.DefaultPort from: TypeCatalog (Types='MailApp.BasicMailSender, MailApp.DefaultPort').
  [Export] MailApp.DefaultPort (ContractName="MailApp.Port")

Notice that while the TestingSmtpHost satisfies the host parameter on the BasicMailSender constructor, its “from” tag doesn’t indicate that it comes from a catalog (in contrast, you can see that DefaultPort came from the TypeCatalog). Likewise, TestingSmtpHost has no [Part] listing and a [Part] listing for DefaultPort does exist.

Most of the time, you can analyze your composition using just the catalog, but the container can have parts too. When your container has parts, that’s when you’ll need to use VerifyCompositionInfo directly to get the full analysis.

You can download and play with the MailApp code from the Sample directory in the CompositionTests github repository.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s