如何在 ServiceCollection 中注册泛型参数结构不同的 Open Generics?
.NET 内置的 DI 容器无法用一条 open generic 注册表达下面的泛型参数转换:
IAsyncEvent<CreateAction<TModel>> → CreateEventAsync<TModel>
容器会按位置将请求中的泛型实参传给实现类型,不会从 CreateAction<TModel> 中提取 TModel。
如果启动时已知模型类型,可以逐个或批量注册 closed generic,之后就能直接注入 IAsyncEvent<CreateAction<Order>>。如果模型类型只能在运行时确定,可以编写显式接收 Type 的解析工厂。普通 AddScoped 工厂不会收到当前请求解析的 closed generic 类型。
为什么原来的注册方式不可行
下面的注册可以正常工作:
services.AddScoped(
typeof(IAsyncRepository<>),
typeof(AsyncRepository<>));
解析 IAsyncRepository<Order> 时,容器取出泛型实参 Order,构造 AsyncRepository<Order>。
如果这样注册:
services.AddScoped(
typeof(IAsyncEvent<>),
typeof(CreateEventAsync<>));
解析 IAsyncEvent<CreateAction<Order>> 时,容器会用完整的 CreateAction<Order> 闭合实现类型,得到:
CreateEventAsync<CreateAction<Order>>
这个类型实现的接口是:
IAsyncEvent<CreateAction<CreateAction<Order>>>
因此,它无法作为请求的 IAsyncEvent<CreateAction<Order>> 使用。
下面的反射写法也无法表达这种匹配关系:
typeof(IAsyncEvent<>).MakeGenericType(typeof(CreateAction<>))
它生成的类型仍包含未绑定的泛型参数,而且自身不是泛型类型定义。内置容器不会将它视为匹配任意 CreateAction<TModel> 的注册模板。
下面这种写法则无法编译:
typeof(IAsyncEvent<CreateAction<>>)
C# 不支持用这种嵌套的未绑定泛型写法表达类型模式。
方案一:注册具体的 closed generic
假设项目中已有 Order 模型,可以直接注册对应的服务类型和实现类型:
using Microsoft.Extensions.DependencyInjection;
services.AddScoped(
typeof(IAsyncRepository<>),
typeof(AsyncRepository<>));
services.AddScoped<
IAsyncEvent<CreateAction<Order>>,
CreateEventAsync<Order>>();
注册后,可以通过构造函数注入:
public sealed class OrderService
{
private readonly IAsyncEvent<CreateAction<Order>> _handler;
public OrderService(
IAsyncEvent<CreateAction<Order>> handler)
{
_handler = handler;
}
}
也可以从现有作用域中解析:
var handler = scope.ServiceProvider
.GetRequiredService<IAsyncEvent<CreateAction<Order>>>();
服务类型和实现类型都已闭合,容器只需构造 CreateEventAsync<Order> 并注入其依赖。
模型较多时,批量生成注册
如果启动时能拿到所有需要支持的模型类型,可以通过反射批量生成 closed generic 注册:
Type[] modelTypes =
{
typeof(Order),
typeof(Customer)
};
foreach (Type modelType in modelTypes)
{
Type actionType = typeof(CreateAction<>)
.MakeGenericType(modelType);
Type serviceType = typeof(IAsyncEvent<>)
.MakeGenericType(actionType);
Type implementationType = typeof(CreateEventAsync<>)
.MakeGenericType(modelType);
services.AddScoped(serviceType, implementationType);
}
Order 和 Customer 代表项目中的模型类型。传入的类型必须满足相关泛型约束,且不能包含未绑定的泛型参数。
这仍然是在逐个注册具体类型,只是省去了重复手写的代码。所有注册都应在构建服务提供程序之前完成。
方案二:运行时通过显式工厂解析
普通的非 keyed AddScoped 工厂接收以下委托:
Func<IServiceProvider, object>
委托没有“当前请求服务类型”参数,IServiceProvider 也没有查询当前解析目标的接口。内置容器还要求 open generic 注册使用开放的实现类型,因此无法通过为 IAsyncEvent<> 注册普通工厂来完成这层转换。
需要在运行时处理类型时,可以将目标类型显式传给自定义工厂方法。
先将实现类型注册为自身:
services.AddScoped(
typeof(IAsyncRepository<>),
typeof(AsyncRepository<>));
services.AddScoped(typeof(CreateEventAsync<>));
services.AddScoped<CreateEventResolver>();
工厂检查请求类型,并从中提取模型类型:
using System;
using Microsoft.Extensions.DependencyInjection;
public sealed class CreateEventResolver
{
private readonly IServiceProvider _services;
public CreateEventResolver(IServiceProvider services)
{
_services = services;
}
public object Resolve(Type serviceType)
{
if (serviceType is null)
{
throw new ArgumentNullException(nameof(serviceType));
}
if (!serviceType.IsGenericType ||
serviceType.ContainsGenericParameters ||
serviceType.GetGenericTypeDefinition() != typeof(IAsyncEvent<>))
{
throw new ArgumentException(
"需要传入已闭合的 IAsyncEvent<TEvent> 类型。",
nameof(serviceType));
}
Type eventType = serviceType.GetGenericArguments()[0];
if (!eventType.IsGenericType ||
eventType.GetGenericTypeDefinition() != typeof(CreateAction<>))
{
throw new ArgumentException(
"仅支持 IAsyncEvent<CreateAction<TModel>>。",
nameof(serviceType));
}
Type modelType = eventType.GetGenericArguments()[0];
Type implementationType = typeof(CreateEventAsync<>)
.MakeGenericType(modelType);
return _services.GetRequiredService(implementationType);
}
}
在当前作用域内调用:
var resolver = scope.ServiceProvider
.GetRequiredService<CreateEventResolver>();
var handler = (IAsyncEvent<CreateAction<Order>>)resolver.Resolve(
typeof(IAsyncEvent<CreateAction<Order>>));
调用方显式传入 closed generic 类型。工厂从 CreateAction<Order> 中提取 Order,再由容器解析 CreateEventAsync<Order>。
这种方式需要调用 CreateEventResolver,下面的接口解析调用仍然无法自动生效:
scope.ServiceProvider
.GetRequiredService<IAsyncEvent<CreateAction<Order>>>();
要直接解析该接口,仍需添加对应的 closed generic 注册。
工厂能否返回需要构造的类型
AddScoped(serviceType, implementationType) 的第二个参数可以是 Type,这里的实现类型在注册时就已确定。
使用工厂重载时,委托必须返回服务实例。如果返回一个 Type 对象,容器会直接将它作为工厂结果,不会继续将它解释为需要构造的实现类型。
动态确定实现类型后,应在工厂内部完成解析:
return serviceProvider.GetRequiredService(implementationType);
这要求实现类型已经注册。前面的 CreateEventResolver 通过将开放的实现类型注册为自身,满足了这一要求。
生命周期与示例代码的注意事项
将 Scoped 改为 Transient 不会改变泛型匹配规则。生命周期应根据处理器及其依赖的使用方式选择。
在工厂方案中,CreateEventAsync<> 的注册决定了处理器实例的生命周期。使用 AddScoped 时,同一作用域内重复解析同一个闭合实现类型会复用实例;使用 AddTransient 时,每次解析都会创建实例。工厂需要从当前作用域解析,尤其是在仓储本身也是 scoped 服务时。
题目中的定义还缺少两处与 DI 无关的代码:CreateAction<TModel> 声明了 ICloneable,需要实现 Clone();HandleAsync 使用了 userContext,需要确保它有明确来源。这些注册示例都假定业务类型本身能够编译,且构造函数依赖已完成注册。
备注:内容仅供参考。
