AddPart : provided instance not preserved?

Oct 19, 2009 at 5:14 PM
Edited Oct 19, 2009 at 5:41 PM

Hi all,
Here I come across a small issue with my code. It's a bit hard to explain so...

I'd like to instantiate a logging component before I Compose the Parts... And I'd like to keep the same instance within the Container afterward.

But with my code, the Container creates a new instance (look like this behavior is caused by the "ImportMany" of "IComponent" in my "Program" class).

I'd be really interested in your opinion.
Am I doing something wrong?

Here's the code :

//----------------------------------------------------------

class Program
    {
        public static int Random = 1;

        static void Main(string[] args)
        {
            new Program().Run();
            Console.ReadKey();
        }

        [ImportMany]
        IEnumerable<IComponent> _Components;

        void Run()
        {
            //ILogger logger = new LogComponent();
            LogComponent logger = new LogComponent();
            logger.Log("Run.");

            var catalog = new AggregateCatalog();
            catalog.Catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly()));

            var batch = new CompositionBatch();
            batch.AddPart(logger);
            batch.AddPart(this);

            CompositionContainer container = new CompositionContainer(catalog);
            container.Compose(batch);

            foreach (var comp in _Components)
            {
                comp.Touch();
            }
        }

    }

//----------------------------------------------------------

interface IComponent
    {
        int ID { get; }
        void Touch();
    }

//----------------------------------------------------------

interface ILogger
    {
        int ID { get; }
        void Log(string message);
    }

//----------------------------------------------------------

[PartCreationPolicy(CreationPolicy.Shared)]
[Export(typeof(IComponent))]
[Export(typeof(ILogger))]
class LogComponent : IComponent, ILogger
    {
        public int ID { get; set; }

        public LogComponent()
        {
            ID = Program.Random++;
            Console.WriteLine(String.Format("LogComponent {0} : New.", ID));
        }

        public void Touch()    
        {
            Console.WriteLine(String.Format("LogComponent {0} : Touch", ID));
        }

        public void Log(string message)
        {
            Console.WriteLine(message);
        }
    }

//----------------------------------------------------------

[PartCreationPolicy(CreationPolicy.Shared)]
[Export(typeof(IComponent))]
class FooComponent : IComponent
    {
        public int ID { get; set; }

        [Import]
        ILogger _Logger;  // <-- HERE I GET MY FIRST LOGGER (the one provided) <--

        public FooComponent()
        {
            ID = Program.Random++;
        }

        public void Touch()    
        {
            Console.WriteLine(String.Format("FooComponent : logger is {0}", _Logger.ID));
        }
    }

//----------------------------------------------------------

Coordinator
Oct 19, 2009 at 8:26 PM

It looks like LogComponent is in the same assembly as everything else.  That means that it will be included in the AssemblyCatalog that you created.  So you have one of them that has been explicitly added to the container via the CompositionBatch, and the catalog offers another.  Under the hood there is a different export provider for each of these, and through the design of the AggregateExportProvider, when an import of a single item is requested, you will just get the one that you added explicitly.  If you request any number, however, you will get all of them.

The easiest way to fix this is probably to prevent the item from showing up in the catalog at all by adding a PartNotDiscoverableAttribute to the LogComponent.

Thanks,
Daniel 

Oct 20, 2009 at 7:57 AM
Edited Oct 20, 2009 at 8:18 AM

Ok... So that's what's going on here. Thanks for the light you put on it.
Yet, I feel uncomfortable with adding a PartNotDiscoverableAttribute, because this would mean the component is aware of how specifically he will be used.
Now what if I want to have my LogComponent in a separate assembly?

Next question : does that make sense?
Is that a good default behaviour that the batch considers the provided instance as a new part?
Would it not make sense to detect the duplicate type and exports?

Thanks,
Frédéric

Developer
Oct 20, 2009 at 4:48 PM

I agree that if you can avoid using the PartNotDiscoverableAttribute you should and putting the component in its own assembly would help eliminate the issue but would still require you to know too much information about that assembly and the components in it.

Ideally what you should be doing here is not actually manually creating the instance of the LogComponent nor adding it to the container. In general it is preferable that people avoid adding instances directly to the container (perhaps with the exception of the bootstrapping/application object), this is because it makes static analyzability difficult.

If you need to use the LogComponent in your Program class then simply Import it like any other component would then after you call Compose(batch) you can do your logging.

Oct 20, 2009 at 7:24 PM

That would obsiously help avoiding the problem. But this look like a workaround, not a solution.

As I need to log before I Compose, I plan using the first instance until I Compose. Then, dispose it.
After composing I'll use the imported instance.

Is there any plan for an API for managing instances within the container?

Oct 21, 2009 at 11:55 PM

What kind of API would help you with this scenario?

Oct 22, 2009 at 11:46 AM

What I would basically need in this scenario is providing an instance to the container. I mean: as instance, not as part.

Would that match the paradigm?

Developer
Oct 23, 2009 at 5:30 PM

So you can actually add instances to the container as an "Exported Value", as opposed to a part. So you could do the following:

batch.AddExportedValue<ILogger>(logger);

Which will add the logger instance as a exported value of type ILogger, instead of a part. In doing so though you wouldn't get any imports on the logger satisfied (doesn't seem to effect your scenario because your logger doesn't have any imports) and you would also need to remove the Export attributes from the LogComponent so it didn't show up in the catalog. If you wanted to expose the IComponent exported value as well then you would also need to call batch.AddExportedValue<IComponent>(logger).

Hopefully that gets you closer to what you are looking for.

Oct 26, 2009 at 10:39 AM
Edited Oct 26, 2009 at 11:26 AM

Thanks for your attention.

That actually gets me closer, yes.
This still makes me uncomfortable, but I can live with that for now.

 

Same issue, more questions :
Can I set the part CreationPolicy for the provided LogComponent?

Here is a short scenario. What do you think of it?
Is it planned making that possible with MEF?

1/ Declare Exports in the LogComponent class.
2/ Instanciate a LogComponent instance.
3/ Let MEF discover all Parts.
4/ Add the LogComponent instance to the container :
   a/ The container detects the provided instance is of same type as one of the discovered parts.
   b/ The container registers the provided instance as ExportValue for all Exported contracts of the LogComponent class (depending on the defined CreationPolicy, let's consider "Shared").
   c/ Imports within the provided instance are satisfied.
5/ Composition :
   a/ The container does not instantiate LogComponent bacause he already has one and CreationPolicy is set to "Shared").
   b/ The container uses the registered LogComponent instance to satisfy Imports (Program's IComponents and FooComponent's ILogger).