Composing Umbraco v8

Monday, February 4, 2019 5:11 PM , edited Tuesday, February 5, 2019 8:52 AM umbraco

Updated: rewrote the last section about enabling and disabling composers. It contained a weird "placeholder" composer that did nothing but disable and enable other composers. Has been replaced with a (hopefully) more elegant solution.

Umbraco is not a framework, nor a library. It is an application—but an application that can be extended and tailored to the needs of each particular website. Conceptually, the application is composed of elements such as content finders, controllers, property value converters, etc.

Composing the application is the responsibility of composers, i.e. classes implementing IComposer. The Umbraco runtime discovers them automatically (by scanning assemblies), instanciates them, and invokes their Compose method:

public interface IComposer
{
  void Compose(Composition composition);
}

Note: the runtime is the engine that boots and controls Umbraco. Explaining it all is out of the scope of this post.

The Composition object exposes a number of methods that give access to the various elements in Umbraco. Some are single elements, such as the IPublishedModelFactory implementation:

// replace the factory with ours
composition.SetPublishedModelFactory<MyFactory>();

Others are collections of elements, such as the content finders collection:

// insert our finder in first position
composition.ContentFinders().Insert<MyFinder>();

This composition model lets you tweak the structure of the application, without having to learn anything about "DI". The example below is all it takes to add a content finder. Simple things are simple.

public class MyFinderComposer : IComposer
{
  public void Compose(Composition composition)
  {
    composition.ContentFinders().Insert<MyFinder>();
  }
}

What about complex things?

Complex things are possible. Composition relies heavily on DI, and the Composition class implements the IRegister interface, which represents the minimalist DI abstraction used by Umbraco. It is therefore possible to register a service directly with:

// register an implementation if my service directly in the container
composition.Register<IMyService, MyService>(Lifetime.Singleton);

And, should you want to do very special things with the container, the Composition also exposes the inner container implementation, which you can then cast as you want. By default, Umbraco ships with a LightInject implementation of IRegister:

// gets the actual inner container
var container = composition.Concrete as LightInject.ServiceContainer;

And you now have access to the true LightInject (or whatever has been choosen) container and all its features.

Registering Uniques

Traditionally, DI supports various "life times" which can be Singleton etc. However, imagine that the following line exists, declaring that SomeService implements ISomeService, and is a singleton (i.e. only one instance of SomeService should be created):

composition.Register<ISomeService, SomeService>(Lifetime.Singleton);

How would one register another implementation of ISomeService? In pure DI, by replacing that line. But in our case, that line exists within Umbraco code and you cannot remove it. And, adding the following line is not going to work:

composition.Register<ISomeService, AnotherService>(Lifetime.Singleton);

What this does is... declare that ISomeService now has two implementations—and which one shall we use? To solve that situation, Umbraco uses the concepts of uniques. A unique is a service that can only have one implementation—and that implementation is a singleton. Unique services are registered in Umbraco with

composition.RegisterUnique<ISomeService, SomeService>();

And can be overriden by users with:

composition.RegisterUnique<ISomeService, AnotherService>();

And this second line overrides (removes) the first registration, thus ensuring that only one implementation exists for the service.

Ouch, this is complex

Yes. As explained, complex things are possible. But most simple things are simple: in most cases, you would not deal with uniques at all. As explained above, almost all uniques in Umbraco Core have a corresponding method defined on Composition, so all you need to know is:

// replace the factory with ours
composition.SetPublishedModelFactory<MyFactory>();

Composers Structure

Obviously, the order of execution of the various IComposer implementations is important. Therefore, Umbraco exposes various mechanisms to order and control them.

First, the IComposer interface is specialized into IRuntimeComposer, ICoreComposer and IUserComposer.

A finer-grain mechanism can then be used to refine the order of composition. Each composer can specify that it should compose before or after another composer, using the ComposeBefore and ComposeAfter attributes. For instance:

[ComposeBefore(typeof(ThatOtherComposer))]
public class ThisComposer : IUserComposer
{ ... }

The runtime orders the composers, based upon their interface and their attributes. It also detects impossible situations (cycles in dependencies, etc).

Enabling and Disabling Composers

Let's say Umbraco ships with two different ways of doing "something" (for instance, two front-end caches). Each way has its own composer, which registers all the relevant elements. Of course, if both composers are detected, there will be some sort of collision. Ideally, we want to disable one of them. That can be achieved with the Disable attribute:

[Disable]
public void Way2Composer : IComposer
{ ... }

Note that Umbraco also has a Enable attribute—but all composers are enabled by default.

When used without arguments, these attributes apply to the composer they are marking. But, and this is where it becomes interesting, they can be used with an argument to act on another component. Therefore, should a user want to replace our "something" with hers, she would write the following code:

[Disable(typeof(Way1Composer))]
public void MyComposer : IComposer
{
  public void Compose(Composition composition) 
  { ... }
}

But maybe she just wants to swap our two "something" implementations? In this case, assembly-level attributes can be used:

[assembly:DisableComposer(typeof(Way1Composer))]
[assembly:EnableComposer(typeof(Way2Composer))]

But—why use attributes, you may wonder? Why cannot we have a nice API for this? Well, because an API is code that executes whereas attributes are declarations. Assuming we have an API to enables and disables composers, how would the code invoking the API execute? Should we have composers' composers?

And, that is all about introducing composition. In next episodes, we'll talk about collection builders and components. Stay tuned!

comments powered by Disqus