Composer Pattern (Visitor Pattern With A Composer) [& IOC Container Bonus] In .NET & C#

In this post I'm going to introduce a development pattern that I have called the Composer pattern.

I have implemented this pattern with huge success to fill an IOC container with registrations (the container "bootstrapper"). It provides excellent separation of concerns (SOC) and upholds all SOLID principles. While I have primarily employed the Composer pattern in the IOC container, it is easily applied for many other uses — and is not tied to the container in any way.

Here is a simplified diagram:


The pattern is not complicated, is interface driven, and has two basic actors. At first, this pattern does resemble the Visitor pattern; and as you'll see, I feel that this may be viewed as something like a refined or derived Visitor pattern overall: it is like the Participants are visitors, but here, the Composer is central.

The primary actor is the "Composer", and that holds the object of interest; which is termed the "Target". Therefore, the Target is the subject of the Composer's composition, and all actors are involved to "Compose" this Target.

The purpose of the pattern is for the Composer to bring up the Target, and then simply notify other actors to act on it.

The other actors are termed "Participants". Technically there is more than one type, but the primary Participant is termed a "Part Provider". The Composer holds all of the Participants, and simply notifies them when the Target is to be Composed — the Target is then Composed through the Parts provided by each Participant (or any other actions that the Participant could take upon the given Target).

Since the Composer is holding the Target upon which the Participants must act, it also comes to hold the Participants themselves. Participants for a given Target are "Added" to the Composer; and it can then invoke them for the Composition. That's what makes the Composer the centralized object: it creates the Target, collects the Participants, and then Composes.

The Composer goes through a single "Compose" operation, which fundamentally just invokes each Participant with the Target. The Participants act on the Target, and the Composition is considered complete: the Target has been Composed.

So the overall process needed is like this:

  1. Select the Target and instantiate a Composer for it.
  2. Add Participants to the Composer for this Target.
  3. Invoke the Composition.

IOC Container Example Use Case

In the IOC container example, the Target is the IOC Container, and the Participants make registrations into the container.

Imagine an application holding a root container. The container is filled with registrations for components that are implemented through interfaces. (I.E. the application defines its' components through interfaces; components implement the interfaces; and those implementations must then be registered into the container.)

In my implementation, I use MEF to bring together the application components. Each component Exports a Composer Participant; and that participant will register its' implementations into the container.

So we have an IOC container bootstrapper that holds the Composer. The Composer's Target is our container. The bootstrapper locates the component assemblies, and discovers the exported Composer Participants. It then adds them to the Composer. When Composed, each Participant makes its' registrations into the container.

Some more details follow, and remember that the central topic is our Composer.

About The Example Container Composer

In the following diagram, you can see that the application components are aware of an IContainer abstraction: This is our Target interface; and is a simple interface for an IOC container (with a registration method like RegisterType<TService, TImplementation>()).

So each application component contains its' implementations, and also provides an Exported IComposerParticipant<IContainer> that will register that component's implementations.


So again, in my application architecture, it is the job of each component to provide its' implementation, and it then also provides an Export — a Composer Participant — and that will make this component's registrations into the container.

This frees the container bootstrapper from requiring knowledge of any component interfaces; and it only needs to determine for itself how to locate the component Assemblies. And so I use MEF: the components Export a Composer Participant, and these are simply discovered. [So all the work is handed to each Participant to register into the container. This will happen only once when the container is composed, and the components do not hold onto the container (it is provided only for the Composer Participants to make the registrations).]

This is accomplished using a Composer in our container bootstrapper; and each component exports a Participant for the container. The bootstrapper locates and adds the Participants, and the Composition then easily bootstraps the Target container.

The Complete Composer Interface

As mentioned, the pattern includes more than one type of Participant, and in fact the Composer provides for these through a three-milestone Compose Sequence: ProvideParts, Bootstrap, Compose. The actual complete composition sequence proceeds like this (including some concrete implementation methods in italics):
  1. [Begin]
    • BeforeCompose
    • GetTarget : TTarget
    • WithNewTarget
  2. ProvideParts
    • [ProvideParts Event Callbacks]
    • WithAllParts
  3. Bootstrap
    • BeforeHandleComposed
  4. HandleComposed
    • AfterComposed
    • RaiseComposed (Event)
The three numbered methods outline the public Composer interface. The italic methods are protected concrete methods for the Composer implementation, and allow instrumentation of the process, and notification. These may be implemented by delegates, and otherwise may have default or empty implementations. In particular, the GetTarget method is responsible for producing the Composer's Target for each Composition: the default implementation simply returns a Target that was provided to the Composer when constructed.

The numbered methods invoke the pattern's defined one-method Participants, which are these: the Parts Provider, the Bootstrapper, and the Composition Handler:
  1. IProvideParts<in TTarget> : IComposerParticipant<TTarget>
    • void ProvideParts<T>(ProvidePartsEventArgs<T> eventArgs)
          where T : TTarget;
  2. IBootstrap<in TTarget> : IComposerParticipant<TTarget>
    • void HandleBootstrap<T>(ComposerEventArgs<T> eventArgs)
          where T : TTarget;
  3. IHandleComposed<in TTarget> : IComposerParticipant<TTarget>
    • void HandleComposed<T>(ComposerEventArgs<T> eventArgs)
          where T : TTarget;
The IComposerParticipant is an empty root interface for all participants:
  • IComposerParticipant<in TTarget> { }
The root interface simplifies scenarios like the above mentioned MEF discovery: the Composer Participants can export this root interface; and then it is up to the Composer implementation to actually reflect the Participant for it's defined interfaces. This is not a mandatory part of the pattern, but it enables the single unified interface to be used to collect and type all Participants.

The IComposer interface is defined this way:

IComposer<out TTarget>
  • IComposer<TTarget> Participate<T>(
            IComposerParticipant<T> partcipant,
            bool oneTimeOnly = false)
        where T : TTarget;
  • void Remove<T>(
            IComposerParticipant<T> partcipant)
        where T : TTarget;
  • TTarget Compose();
  • AggregateComposerException LastComposeErrors { get; }
It simply defines the Participate and Remove methods for participants, and the Compose method; and it also provides the AggregateComposerException to hold any composition errors.

One Last Participant

There is in fact also one last defined Participant: the IRequestComposition participant; which is provided for objects that are able to request that the Target is Composed, or re-composed. This interface defines its' own event:

IRequestComposition<TTarget> : IComposerParticipant<TTarget>
  • event EventHandler<RequestCompositionEventArgs<TTarget>> CompositionRequested;
If an IRequestComposition Participant raises its' CompositionRequested event, then the Composer performs a composition immediately. The given RequestCompositionEventArgs also defines properties for the invoker to request actions on the Composer before the composition begins.

Summary

That all basically sums up the pattern:

Prerequisite:
  1. Select the Target and instantiate a Composer for it.
  2. Add Participants to the Composer for this Target.
  3. Invoke the Composition.
Composition:
  1. Provide Parts [A Participant]
  2. Bootstrap [A Participant]
  3. Handle Composed [A Participant]
In our IOC container example:
  1. Target is an empty container
  2. Participants are provided by the components (and will make container registrations)
  3. Composition populates and fully readies the container
  1. Parts participants make registrations
  2. Bootstrappers can start services
  3. When Composed, the container is ready

Some Details

You may notice that the Participant interfaces define their methods with the covariant T : TTarget parameter. This was implemented to allow Participants that define a contravariant type to participate in a composition on a covariant type.

Note also that compositions can be re-invoked if your Composer allows it: your target may be updated, reset, or refreshed by further compositions (yet that is under your own control). That is why the IComposer Participate method defines the boolean oneTimeOnly parameter: a single participant can be restricted to run only once even if the composition is re-invoked.

Although the Bootstrap phase follows the Parts Providers, it is named "Bootstrap" because it runs before the IHandleComposed Participants, which are intended to handle the fully-composed Target. Bootstrappers can run first, to perform some type of final preparations or finalizations.

All Participant interfaces are invoked with an event argument, which always provides the current Target. This allows for future expansion by augmenting the event argument. Currently, the ProvidePartsEventArgs holds a method that allows attaching a callback to that event. In the composition sequence outlined above, these callbacks are invoked at "[ProvideParts Event Callbacks]", which is invoked before the IBootstrap Participants. This allows a Parts Provider to re-access the Target after all parts have been provided, but before the Bootstrappers run.

Also, the implementation does do some simple reflection to handle the Participants, and that is not cached (it uses System.Collections.IEnumerable.OfType<T>() each time), and therefore overall it was not implemented with maximum speed as a priority; yet it's fast and reasonably terse. It for sure has release-worthy stability; and has been around for well over a year.

Lastly, my implementation is single-threaded: the Compose sequence runs sequentially. It would not be hard to augment that with an option to invoke the Participants in parallel, but it will require hacking into the project to add that (perhaps with some policy to define an option). The implementation is not very complicated overall, and there is one primary method to handle the sequence.

Code And Examples

The complete C# code for my implementation of this pattern is in fact part of a large repository of my own, which was not set up for sharing: i.e. it contains many internal projects all together. I have simply copied out the required projects and included a Visual Studio solution. So, although there are no external dependencies, there are dependencies on my own abstractions and utilities for the implementation. All of the required code is in this repository. The repository is on GitHub, here: https://github.com/steevcoco/Sc.Composer

Hey: Why net45 and not Standard and Core

Oops I almost forgot. This code is all in net45 projects. That is because my business was driven by a primary project, and it was targeting net45. The fact is there will be little to no change to retarget all of the code: all libraries can target Standard; and in fact it was so smooth I once had a simple build switch to switch the target — I.E. it was important.

You will find dependency projects grouped as "Dependencies". The Composer implementation is in the Sc.Composer project. Examples are grouped under "Examples".

I have also included my Sc.Composer.Mef project, which should be noted is NOT even used as the actual Container Composer in the IOC Container example, but is used only to gather the Exported Participants there, via some static utility methods that it also provides. (That project provides a Composer implementation that can perform a configuration on a MEF ContainerConfiguration; and in fact, under the hood, the static helpers create a MEF Composer to perform the import.)

I have included two examples: a simple project that includes all of its own classes, called SimpleExample, that just runs through a short Main method. And I have also included an example of the IOC Container scenario, that utilizes MEF discovery as described: it runs from the primary ContainerExample project, and imports Composer Participants implemented by the Component1 and Component2 projects.

Please send feedback!

Comments