using Microsoft.Extensions.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Composing; /// /// Provides a base class for collection builders. /// /// The type of the builder. /// The type of the collection. /// The type of the items. public abstract class CollectionBuilderBase : ICollectionBuilder where TBuilder : CollectionBuilderBase where TCollection : class, IBuilderCollection { private readonly Lock _locker = new(); private readonly List _types = new(); private Type[]? _registeredTypes; /// /// Gets the collection lifetime. /// protected virtual ServiceLifetime CollectionLifetime => ServiceLifetime.Singleton; /// public virtual void RegisterWith(IServiceCollection services) { if (_registeredTypes != null) { throw new InvalidOperationException("This builder has already been registered."); } // register the collection services.Add(new ServiceDescriptor(typeof(TCollection), CreateCollection, CollectionLifetime)); // register the types RegisterTypes(services); } /// /// Creates a collection. /// /// A collection. /// Creates a new collection each time it is invoked. public virtual TCollection CreateCollection(IServiceProvider factory) => factory.CreateInstance(CreateItemsFactory(factory)); /// /// Gets the internal list of types as an IEnumerable (immutable). /// public IEnumerable GetTypes() => _types; /// /// Gets a value indicating whether the collection contains a type. /// /// The type to look for. /// A value indicating whether the collection contains the type. /// /// Some builder implementations may use this to expose a public Has{T}() method, /// when it makes sense. Probably does not make sense for lazy builders, for example. /// public virtual bool Has() where T : TItem => _types.Contains(typeof(T)); /// /// Gets a value indicating whether the collection contains a type. /// /// The type to look for. /// A value indicating whether the collection contains the type. /// /// Some builder implementations may use this to expose a public Has{T}() method, /// when it makes sense. Probably does not make sense for lazy builders, for example. /// public virtual bool Has(Type type) { EnsureType(type, "find"); return _types.Contains(type); } /// /// Configures the internal list of types. /// /// The action to execute. /// Throws if the types have already been registered. protected void Configure(Action> action) { lock (_locker) { if (_registeredTypes != null) { throw new InvalidOperationException( "Cannot configure a collection builder after it has been registered."); } action(_types); } } /// /// Gets the types. /// /// The internal list of types. /// The list of types to register. /// Used by implementations to add types to the internal list, sort the list, etc. protected virtual IEnumerable GetRegisteringTypes(IEnumerable types) => types; /// /// Creates the collection items. /// /// The collection items. protected virtual IEnumerable CreateItems(IServiceProvider factory) { if (_registeredTypes == null) { throw new InvalidOperationException( "Cannot create items before the collection builder has been registered."); } return _registeredTypes // respect order .Select(x => CreateItem(factory, x)) .ToArray(); // safe } /// /// Creates a collection item. /// protected virtual TItem CreateItem(IServiceProvider factory, Type itemType) => (TItem)factory.GetRequiredService(itemType); protected Type EnsureType(Type type, string action) { if (typeof(TItem).IsAssignableFrom(type) == false) { throw new InvalidOperationException( $"Cannot {action} type {type.FullName} as it does not inherit from/implement {typeof(TItem).FullName}."); } return type; } private void RegisterTypes(IServiceCollection services) { lock (_locker) { if (_registeredTypes != null) { return; } Type[] types = GetRegisteringTypes(_types).ToArray(); // ensure they are safe foreach (Type type in types) { EnsureType(type, "register"); } // register them - ensuring that each item is registered with the same lifetime as the collection. // NOTE: Previously each one was not registered with the same lifetime which would mean that if there // was a dependency on an individual item, it would resolve a brand new transient instance which isn't what // we would expect to happen. The same item should be resolved from the container as the collection. foreach (Type type in types) { services.Add(new ServiceDescriptor(type, type, CollectionLifetime)); } _registeredTypes = types; } } // used to resolve a Func> parameter private Func> CreateItemsFactory(IServiceProvider factory) => () => CreateItems(factory); }