I developed your idea by introducing the notion of ChainLink(current, next).
public class ItemDecoratorChainLink : IItemDecorator
{
private readonly IItemDecorator[] _decorators;
public ItemDecoratorChainLink(
IItemDecorator current,
IItemDecorator next)
{
if (current == null)
{
throw new ArgumentNullException(nameof(current));
}
_decorators = next != null
? new[] { current, next }
: new[] { current };
}
public bool CanHandle(Item item) =>
_decorators.Any(d => d.CanHandle(item));
public void Decorate(Item item)
{
var decorators = _decorators.Where(d => d.CanHandle(item)).ToArray();
foreach (var decorator in decorators)
{
decorator.Decorate(item);
}
}
}
Thus you don't need to keep a reference to "next" link inside links but burden the chainLink with that. Your links, wherein, become cleaner, relieved from duplication, and can care single responsibility.
Below is a code for the chain builder:
public class ComponentChainBuilder<TInterface> : IChainBuilder<TInterface>
where TInterface : class
{
private static readonly Type InterfaceType = typeof(TInterface);
private readonly List<Type> _chain = new List<Type>();
private readonly IServiceCollection _container;
private readonly ConstructorInfo _chainLinkCtor;
private readonly string _currentImplementationArgName;
private readonly string _nextImplementationArgName;
public ComponentChainBuilder(
IServiceCollection container,
Type chainLinkType,
string currentImplementationArgName,
string nextImplementationArgName)
{
_container = container;//.GuardNotNull(nameof(container));
_chainLinkCtor = chainLinkType.GetConstructors().First();//.GuardNotNull(nameof(chainLinkType));
_currentImplementationArgName = currentImplementationArgName;//.GuardNeitherNullNorWhitespace(nameof(currentImplementationArgName));
_nextImplementationArgName = nextImplementationArgName;//.GuardNeitherNullNorWhitespace(nameof(nextImplementationArgName));
}
/// <inheritdoc />
public IChainBuilder<TInterface> Link(Type implementationType)
{
_chain.Add(implementationType);
return this;
}
/// <inheritdoc />
public IChainBuilder<TInterface> Link<TImplementationType>()
where TImplementationType : class, TInterface
=> Link(typeof(TImplementationType));
public IServiceCollection Build(ServiceLifetime serviceLifetime = ServiceLifetime.Transient)
{
if (_chain.Count == 0)
{
throw new InvalidOperationException("At least one link must be registered.");
}
var serviceProviderParameter = Expression.Parameter(typeof(IServiceProvider), "x");
Expression chainLink = null;
for (var i = _chain.Count - 1; i > 0; i--)
{
var currentLink = CreateLinkExpression(_chain[i - 1], serviceProviderParameter);
var nextLink = chainLink ?? CreateLinkExpression(_chain[i], serviceProviderParameter);
chainLink = CreateChainLinkExpression(currentLink, nextLink, serviceProviderParameter);
}
if (chainLink == null)
{
// only one type is defined so we use it to register dependency
_container.Add(new ServiceDescriptor(InterfaceType, _chain[0], serviceLifetime));
}
else
{
// chain is built so we use it to register dependency
var expressionType = Expression.GetFuncType(typeof(IServiceProvider), InterfaceType);
var createChainLinkLambda = Expression.Lambda(expressionType, chainLink, serviceProviderParameter);
var createChainLinkFunction = (Func<IServiceProvider, object>)createChainLinkLambda.Compile();
_container.Add(new ServiceDescriptor(InterfaceType, createChainLinkFunction, serviceLifetime));
}
return _container;
}
private NewExpression CreateLinkExpression(Type linkType, ParameterExpression serviceProviderParameter)
{
var linkCtor = linkType.GetConstructors().First();
var linkCtorParameters = linkCtor.GetParameters()
.Select(p => GetServiceProviderDependenciesExpression(p, serviceProviderParameter))
.ToArray();
return Expression.New(linkCtor, linkCtorParameters);
}
private Expression CreateChainLinkExpression(
Expression currentLink,
Expression nextLink,
ParameterExpression serviceProviderParameter)
{
var chainLinkCtorParameters = _chainLinkCtor.GetParameters().Select(p =>
{
if (p.Name == _currentImplementationArgName)
{
return currentLink;
}
if (p.Name == _nextImplementationArgName)
{
return nextLink;
}
return GetServiceProviderDependenciesExpression(p, serviceProviderParameter);
}).ToArray();
return Expression.New(_chainLinkCtor, chainLinkCtorParameters);
}
private static Expression GetServiceProviderDependenciesExpression(ParameterInfo parameter, ParameterExpression serviceProviderParameter)
{
// this is a parameter we don't care about, so we just ask GetRequiredService to resolve it for us
return Expression.Call(
typeof(ServiceProviderServiceExtensions),
nameof(ServiceProviderServiceExtensions.GetRequiredService),
new[] { parameter.ParameterType },
serviceProviderParameter);
}
}
And its extension:
public static IChainBuilder<TInterface> Chain<TInterface, TChainLink>(
this IServiceCollection container,
string currentImplementationArgumentName = "current",
string nextImplementationArgumentName = "next")
where TInterface : class
where TChainLink : TInterface
=> new ComponentChainBuilder<TInterface>(
container,
typeof(TChainLink),
currentImplementationArgumentName,
nextImplementationArgumentName);
The code for building chains looks like this:
serviceProvider.Chain<IItemDecorator, ItemDecoratorChainLink>()
.Link<ChannelItemDecorator>()
.Link<CompetitionItemDecorator>()
.Link<ProgramItemDecorator>()
.Build(ServiceLifetime.Singleton);
And the full example of this approach can be found on my GitHub:
https://github.com/alex-valchuk/dot-net-expressions/blob/master/NetExpressions/ConsoleApp1/ConsoleApp1/ChainBuilder/ComponentChainBuilder.cs