PAYATHON 2026

如何在 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,需要确保它有明确来源。这些注册示例都假定业务类型本身能够编译,且构造函数依赖已完成注册。

备注:内容仅供参考。