Composing Umbraco v8 Collections

Posted on February 5, 2019 in umbraco

(continuing from Composing Umbraco v8)

In the previous post, we briefly mentioned "collections of elements", such as the content finders collection, and presented the following example:

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

Collections are another concept that Umbraco uses to make things simpler, on top of DI. In pure DI, when one wants to use several implementations of a service, one would do:

container.Register<ISomeService, SomeService>();
container.Register<ISomeService, AnotherService>();

And the DI container would support injecting, for instance, an IEnumerable<ISomeService>. That does not work really well for us. In pure DI, one would remove an implementation by removing one of the two lines. But, with Umbraco, these two lines would be in Umbraco's code, and not removable by users. Also, the order of the instances can be of some importance (think about content finders), but DI containers either don't support ordering, or require some specialized way of dealing with it.

To handle this, Umbraco introduces ICollectionBuilder: a collection builder builds a collection, allowing users to add and remove types before anything is actually registered into DI. In our example, we would define a very simple collection:

public class SomeServiceCollection : BuilderCollectionBase<ISomeService>
{
  public SomeServiceCollection(IEnumerable<ISomeService> items)
    : base(items)
  { }
}

And an equally simple collection builder:

public class SomeServiceCollectionBuilder
 : OrderedCollectionBuilderBase<SomeServiceCollectionBuilder, SomeServiceCollection, ISomeService>
{
  protected override SomeServiceCollectionBuilder This => this;
}

Composition the Collection

With both the collection and the builder in place, we can compose the collection in the following way:

composition.WithCollectionBuilder<SomeServiceCollectionBuilder>()
  .Append<AnotherService>()
  .Remove<SomeService();

And this code can be repeated in different composers, to further refine the collection.

It is a good idea to define an extension method for the Composition class, such as the following one (we have done it for most built-in collections):

public static SomeServiceCollectionBuilder SomeServices(this Composition composition)
  => composition.WithCollectionBuilder<SomeServiceCollectionBuilder>();

Enabling you to write, in a simpler way:

composition.SomeServices()
  .Append<AnotherService>()
  .Remove<SomeService();

Using the Collection

Collection builders automatically register the collections they build. Whenever we need to use our services, we can inject the collection:

public class UsingTheServices
{
  public UsingTheServices(SomeServiceCollection services)
  { }
}

The collection is an IEnumerable<ISomeService> and can be used as such (it is possible to run a foreach loop on it, for instance). It is guaranteed to contain exactly instances of the types that were configured, in the order that was specified.

Fine Tuning Collections

By default, collections are registered as singletons. That is, it is created once, you can inject it and enumerate it and it is cheap. But the CollectionBuilderBase abstract class has a property that can be used to override this, and specify a different lifetime, in your own builder:

protected override Lifetime CollectionLifetime => Lifetime.Transient;

The CollectionBuilderBase abstract class also has various virtual methods used to create all items, create one specific item, create the whole collection, etc. that can be overriden in your builders to control how the collection is created: refer to Umbraco Core source code for examples.

Finally, the various collection builder base classes are:

And... that's all about collections, for today. In a next episode, we will talk about components. Stay tuned!

There used to be Disqus-powered comments here. They got very little engagement, and I am not a big fan of Disqus. So, comments are gone. If you want to discuss this article, your best bet is to ping me on Mastodon.