In my current project, I'm using quite a few Chain of Responsibility patterns.
However, I find it a bit awkward to configure the chain via dependency injection.
Given this model:
public interface IChainOfResponsibility
{
IChainOfResponsibility Next { get; }
void Handle(Foo foo);
}
public class HandlerOne : IChainOfResponsibility
{
private DbContext _dbContext;
public HandlerOne(IChainOfResponsibility next, DbContext dbContext)
{
Next = next;
_dbContext = dbContext;
}
public IChainOfResponsibility Next { get; }
public void Handle(Foo foo) { /*...*/}
}
public class HandlerTwo : IChainOfResponsibility
{
private DbContext _dbContext;
public HandlerTwo(IChainOfResponsibility next, DbContext dbContext)
{
Next = next;
_dbContext = dbContext;
}
public IChainOfResponsibility Next { get; }
public void Handle(Foo foo) { /*...*/}
}
My Startup becomes:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IChainOfResponsibility>(x =>
new HandlerOne(x.GetRequiredService<HandlerTwo>(), x.GetRequiredService<DbContext>())
);
services.AddTransient(x =>
new HandlerTwo(null, x.GetRequiredService<DbContext>())
);
}
How to configure my chain of responsibility more cleanly?
I've hacked a simple solution, as I couldn't find anything that did what I wanted. Seems to be working fine, as long as the GetRequiredService
can resolve all constructor dependencies of all the handlers of the chain.
My startup class becomes:
public void ConfigureServices(IServiceCollection services)
{
services.Chain<IChainOfResponsibility>()
.Add<HandlerOne>()
.Add<HandlerTwo>()
.Configure();
}
And the code that does the magic:
public static class ChainConfigurator
{
public static IChainConfigurator<T> Chain<T>(this IServiceCollection services) where T : class
{
return new ChainConfiguratorImpl<T>(services);
}
public interface IChainConfigurator<T>
{
IChainConfigurator<T> Add<TImplementation>() where TImplementation : T;
void Configure();
}
private class ChainConfiguratorImpl<T> : IChainConfigurator<T> where T : class
{
private readonly IServiceCollection _services;
private List<Type> _types;
private Type _interfaceType;
public ChainConfiguratorImpl(IServiceCollection services)
{
_services = services;
_types = new List<Type>();
_interfaceType = typeof(T);
}
public IChainConfigurator<T> Add<TImplementation>() where TImplementation : T
{
var type = typeof(TImplementation);
if (!_interfaceType.IsAssignableFrom(type))
throw new ArgumentException($"{type.Name} type is not an implementation of {_interfaceType.Name}", nameof(type));
_types.Add(type);
return this;
}
public void Configure()
{
if (_types.Count == 0)
throw new InvalidOperationException($"No implementation defined for {_interfaceType.Name}");
bool first = true;
foreach (var type in _types)
{
ConfigureType(type, first);
first = false;
}
}
private void ConfigureType(Type currentType, bool first)
{
var nextType = _types.SkipWhile(x => x != currentType).SkipWhile(x => x == currentType).FirstOrDefault();
var ctor = currentType.GetConstructors().OrderByDescending(x => x.GetParameters().Count()).First();
var parameter = Expression.Parameter(typeof(IServiceProvider), "x");
var ctorParameters = ctor.GetParameters().Select(x =>
{
if (_interfaceType.IsAssignableFrom(x.ParameterType))
{
if (nextType == null)
return Expression.Constant(null, _interfaceType);
else
return Expression.Call(typeof(ServiceProviderServiceExtensions), "GetRequiredService", new Type[] { nextType }, parameter);
}
return (Expression)Expression.Call(typeof(ServiceProviderServiceExtensions), "GetRequiredService", new Type[] { x.ParameterType }, parameter);
});
var body = Expression.New(ctor, ctorParameters.ToArray());
var resolveType= first ? _interfaceType : currentType;
var expressionType = Expression.GetFuncType(typeof(IServiceProvider), resolveType);
var expression = Expression.Lambda(expressionType, body, parameter);
var compiledExpr = (Func<IServiceProvider, object>)expression.Compile();
_services.AddTransient(resolveType, compiledExpr);
}
}
}
PS.: I'm answering my own question for future reference (myself and hopefully others), but I'd love some feedback on this.
Quick solution working for simplest cases of dependency chains.
public static IServiceCollection AddChained<TService>(this IServiceCollection services, params Type[] implementationTypes)
{
if (implementationTypes.Length == 0)
{
throw new ArgumentException("Pass at least one implementation type", nameof(implementationTypes));
}
foreach(Type type in implementationTypes)
{
services.AddScoped(type);
}
int order = 0;
services.AddTransient(typeof(TService), provider =>
{
//starts again
if (order > implementationTypes.Length - 1)
{
order = 0;
}
Type type = implementationTypes[order];
order++;
return provider.GetService(type);
});
return services;
}
and then
services.AddChained<IService>(typeof(SomeTypeWithIService), typeof(SomeType));