c# - 如何在责任链中注入下一个处理程序的依赖关系?
问题描述
在我当前的项目中,我使用了很多责任链模式。
但是,我发现通过依赖注入配置链有点尴尬。
鉴于此模型:
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) { /*...*/}
}
我的启动变成:
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>())
);
}
如何更干净地配置我的责任链?
解决方案
我破解了一个简单的解决方案,因为我找不到任何我想要的东西。它工作正常,因为它用于IServiceProvider.GetRequiredService
解决链中所有处理程序的所有构造函数依赖项。
我的启动类变成:
public void ConfigureServices(IServiceCollection services)
{
services.Chain<IChainOfResponsibility>()
.Add<HandlerOne>()
.Add<HandlerTwo>()
.Configure();
}
我正在做的是使用表达式动态生成问题中的 lambda。然后将其编译并注册到IServiceCollection.AddTransient
.
因为它生成编译代码,所以在运行时它应该和问题注册一样快。
这是执行魔术的代码:
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);
_types.Add(type);
return this;
}
public void Configure()
{
if (_types.Count == 0)
throw new InvalidOperationException($"No implementation defined for {_interfaceType.Name}");
foreach (var type in _types)
{
ConfigureType(type);
}
}
private void ConfigureType(Type currentType)
{
// gets the next type, as that will be injected in the current type
var nextType = _types.SkipWhile(x => x != currentType).SkipWhile(x => x == currentType).FirstOrDefault();
// Makes a parameter expression, that is the IServiceProvider x
var parameter = Expression.Parameter(typeof(IServiceProvider), "x");
// get constructor with highest number of parameters. Ideally, there should be only 1 constructor, but better be safe.
var ctor = currentType.GetConstructors().OrderByDescending(x => x.GetParameters().Count()).First();
// for each parameter in the constructor
var ctorParameters = ctor.GetParameters().Select(p =>
{
// check if it implements the interface. That's how we find which parameter to inject the next handler.
if (_interfaceType.IsAssignableFrom(p.ParameterType))
{
if (nextType is null)
{
// if there's no next type, current type is the last in the chain, so it just receives null
return Expression.Constant(null, _interfaceType);
}
else
{
// if there is, then we call IServiceProvider.GetRequiredService to resolve next type for us
return Expression.Call(typeof(ServiceProviderServiceExtensions), "GetRequiredService", new Type[] { nextType }, parameter);
}
}
// this is a parameter we don't care about, so we just ask GetRequiredService to resolve it for us
return (Expression)Expression.Call(typeof(ServiceProviderServiceExtensions), "GetRequiredService", new Type[] { p.ParameterType }, parameter);
});
// cool, we have all of our constructors parameters set, so we build a "new" expression to invoke it.
var body = Expression.New(ctor, ctorParameters.ToArray());
// if current type is the first in our list, then we register it by the interface, otherwise by the concrete type
var first = _types[0] == currentType;
var resolveType = first ? _interfaceType : currentType;
var expressionType = Expression.GetFuncType(typeof(IServiceProvider), resolveType);
// finally, we can build our expression
var expression = Expression.Lambda(expressionType, body, parameter);
// compile it
var compiledExpression = (Func<IServiceProvider, object>)expression.Compile();
// and register it in the services collection as transient
_services.AddTransient(resolveType, compiledExpression );
}
}
}
PS.:我正在回答我自己的问题以供将来参考(我自己和希望其他人),但我希望对此有一些反馈。
推荐阅读
- swift - 包加载:“忽略重复产品”(SwiftPM)
- r - 字符匹配和年份后的语句子串
- java - 如何将 StartActivityForResult 事件注册到 ViewModel?[MVVM]
- c# - 当我从我的 API 使用 Post 插入数据库时出现异常
- python - 使用 Anaconda 将 Django 应用程序部署到 Heroku 时出现“没有这样的选项:--allow-all-external”
- python - 重塑 NumPy 数组:IndexError:元组索引超出范围
- python - 更改字符串中某些字符大小写的函数
- rust - rust中Option的实际用途是什么?
- go - 无法在全新的 Go 项目中加载包
- pandas - 通过定时数据和交叉验证避免数据泄漏