ASP.NET依赖注入中MediatR管道行为注册异常排查
在使用MediatR管道时,需要确保服务按正确顺序注册,注册代码如下:
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CommandLoggingBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(PermissionBehavior<,>)); services.AddTransient<IPipelineBehavior<RegistrationCommand, Result<AuthorizationResponse>>, ValidationBehavior<RegistrationCommand, Result<AuthorizationResponse>>>(); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(TransactionPipelineBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(DistributedLockPipelineBehavior<,>));
除ValidationBehavior外,其余实现均使用泛型约束,需为所有泛型参数注册;ValidationBehavior仅为拥有验证器的类型注册,因此使用精确泛型参数。
现象:
- 构建
WebApplication前,通过BuildServiceProvider获取IPipelineBehavior<RegistrationCommand, Result<AuthorizationResponse>>服务时,5个管道均按正确顺序和实现存在; - 运行时执行相同获取操作,最后一个
DistributedLockPipelineBehavior被ValidationBehavior副本替换。
若新增一次DistributedLockPipelineBehavior注册:
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CommandLoggingBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(PermissionBehavior<,>)); services.AddTransient<IPipelineBehavior<RegistrationCommand, Result<AuthorizationResponse>>, ValidationBehavior<RegistrationCommand, Result<AuthorizationResponse>>>(); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(TransactionPipelineBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(DistributedLockPipelineBehavior<,>)); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(DistributedLockPipelineBehavior<,>));
仅最后一个注册的DistributedLockPipelineBehavior会被替换。
已新建项目复现该问题,Program.cs代码如下:
var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CommandLoggingBehavior<,>)); builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(PermissionBehavior<,>)); builder.Services.AddTransient(typeof(IPipelineBehavior<MockRequest, MockResponse>), typeof(ValidationBehavior<MockRequest, MockResponse>)); builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>)); builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(DistributedLockBehavior<,>)); var app = builder.Build(); var pipes = app.Services.GetServices<IPipelineBehavior<MockRequest, MockResponse>>(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); public class MockRequest { } public class MockResponse { } public interface IPipelineBehavior<TRequest, TResponse> { } public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { } public class CommandLoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { } public class PermissionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { } public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { } public class DistributedLockBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { }
多次添加ValidationBehavior后,也会出现类似异常情况。请问是什么原因导致服务注册出现该问题?
问题出在ASP.NET Core依赖注入(DI)系统对开放泛型与封闭泛型服务的匹配优先级处理上,具体逻辑如下:
服务注册的存储差异:
- 注册开放泛型服务(比如
typeof(IPipelineBehavior<,>))时,DI系统会把它存成一个通用模板,等需要时再实例化为具体的封闭泛型类型。 - 注册封闭泛型服务(比如
IPipelineBehavior<MockRequest, MockResponse>)时,DI系统直接存储这个具体的服务类型。
- 注册开放泛型服务(比如
服务解析的匹配BUG:
调用GetServices<T>()时,DI系统会先收集所有直接注册的T类型服务,再把兼容的开放泛型模板实例化成T类型加入集合。但这里有个逻辑问题:当DI系统处理后面的开放泛型实例化时,如果已经存在同类型的封闭泛型注册,会错误地把最后一个开放泛型实例化的服务替换成那个封闭泛型服务(也就是你的ValidationBehavior)。对应你的场景:
- 注册顺序是:开放泛型→封闭泛型→开放泛型
- 解析
IPipelineBehavior<MockRequest, MockResponse>时,DI先把前两个开放泛型实例化为对应服务,接着加入封闭泛型的ValidationBehavior,然后处理后面的TransactionBehavior和DistributedLockBehavior开放泛型实例化。 - 最后一个开放泛型(
DistributedLockBehavior)实例化时,DI系统错误地将其替换为已存在的ValidationBehavior副本,就出现了你看到的现象。 - 重复注册
DistributedLockBehavior时,只有最后一个实例化的会被替换,因为前面的实例化已经完成,只有最后一个触发了这个替换逻辑。
构建前后差异的原因:
构建WebApplication前调用BuildServiceProvider,DI系统还没经过ASP.NET Core内部的服务优化(比如类型合并、验证),解析逻辑更直接,不会触发这个BUG;而WebApplication.Build()会触发这些优化,导致问题显现。
内容的提问来源于stack exchange,提问作者WingiM

