如何注册一个实现了通用接口且该接口还实现了另一个通用接口的服务

3

我有一个如下的场景,但无法使我的SpecificApi服务注册。

    public interface IDetail
    {
        string Name { get; set;}
    }

    public class SpecificDetail : IDetail
    {
        public string Name { get; set; }
    }

    public interface IBaseApi<TDetail> where TDetail: IDetail
    {
        TDetail Method1();
    }

    public interface ISpecificApi<TDetail> : IBaseApi<TDetail> where TDetail : IDetail
    {

    }

    public class SpecificApi : ISpecificApi<SpecificDetail>
    {
        public SpecificDetail Method1()
        {
            return new SpecificDetail();
        }
    }

    public class Consumer
    {
        public  Consumer(ISpecificApi<IDetail> api) // Generic must be of IDetail, not SpecificDetail
        {

        }
    }

我尝试了以下方法注册服务,但没有成功。
// Fails at runtime with System.ArgumentException: 'Open generic service type 'DiGenericsTest.ISpecificApi`1[TDetail]' requires registering an open generic implementation type. (Parameter 'descriptors')'
builder.Services.AddSingleton(typeof(ISpecificApi<>), typeof(SpecificApi));


// Fails at build time with "there is no implicit reference conversion"
builder.Services.AddSingleton<ISpecificApi<IDetail>, SpecificApi>();

// This runs, but then I have to inject ISpecificApi<SpecificDetail> into Consumer instead of ISpecificApi<IDetail>.
builder.Services.AddSingleton<ISpecificApi<SpecificDetail>, SpecificApi>();

builder.Services.AddSingleton<Consumer>();
2个回答

3

只需将你的接口修改为协变即可:

public interface IBaseApi<out TDetail> where TDetail : IDetail
{
    TDetail Method1();
}

public interface ISpecificApi<out TDetail> : IBaseApi<TDetail> where TDetail : IDetail
{

}

在此之后,builder.Services.AddSingleton<ISpecificApi<IDetail>, SpecificApi>() 将不会失败。

可以查看 dotnetfiddle 上的示例。

了解更多有关 协变和逆变 的信息。


1

问题并不在于注册,而是类型推断无法正常工作,因此这种情况在许多不同的上下文中出现。

尽管 SpecificDetail 实现了 IDetail 接口,但 SpecificApi 并没有实现 ISpecificApi<IDetail> 接口 特别地,它只实现了非常具体的接口:ISpecificApi<SpecificDetail>

  • 您的观察已经证实了这种行为

这是因为您使用了默认的Invariant接口规范。

  • 在泛型类型声明中,Invariant(默认行为,没有 inout 修饰符)声明要求实现完全匹配。任何实现细节都是内部的,不能保证。

传统的解决方法是通过从规范中删除对 SpecificDetail 的依赖来简化您的 API,但这会带来一个不太理想的副作用,即需要将 Method1 的返回类型设置为与 ISpecificApi<IDetail> 定义特别地匹配,返回 IDetail

public class SpecificApi : ISpecificApi<IDetail>
{
    public IDetail Method1()
    {
        return new SpecificDetail();
    }
}

这使得对于SpecificApi的使用变得模糊,虽然它可以工作,但是为了访问SpecificDetail的任何非IDetail成员,我们被迫先将其强制转换为SpecificDetail或冒着运行时异常的风险。
尽管我们现在知道SpecificApi.Method1返回SpecificDetail,但编译器和未来的开发人员可能会决定返回IDetail的不同实现,而我们的消费代码直到运行时才会知道实现已更改。
泛型中的协变性确实允许我们解决这个问题:定义变量泛型接口和委托,并且重要的是允许我们在已定义的接口上定义我们想要支持的实现类型的约束条件。
  • 使用in修饰符声明的逆变参数IComparer<in T>表示T可以是特定的T或者是一个比T派生更少的子孙类型。

  • 使用out修饰符的协变参数允许我们在泛型类型T的位置上使用一个更多派生的类型。这正是OP所需要的,以允许从IDetail继承的类型实现IBaseApi<out T>

想象一下在一个基础库中的以下声明:

public interface IBaseApi<out TDetail> where TDetail : IDetail
{
    TDetail Method1();
}

public interface ISpecificApi<out TDetail> : IBaseApi<TDetail> where TDetail : IDetail
{

}

您可以使用此中间件实现:

然后,您可以使用此中间件实现:

public class SpecificApi : ISpecificApi<SpecificDetail>
{
    public SpecificDetail Method1()
    {
        return new SpecificDetail();
    }
}

您可以注册此特定实现:

builder.Services.AddSingleton<ISpecificApi<IDetail>, SpecificApi>();

现在可以注入实现:

public class Consumer
{
    public Consumer(ISpecificApi<IDetail> api)
    {
        this.Api = api;
    }

    ISpecificApi<IDetail> Api { get; }
    IDetail Detail { get => Api.Method1(); }
}

正如您所见,当我们使用注入时仍然存在一些不确定性,尽管api的解析实例将是SpecificApi,但它仍无法隐式地被类型为SpecificApiMethod()的响应仍然在此级别受到IDetail的限制,并且不能隐式类型化为SpecificDetail,但这并不重要。
因此,从IoC的角度来看,在这种情况下,使用协变与使用不变接口的固定实现相比,对于消费逻辑来说几乎没有区别。
  • 这也是为什么许多开发人员不熟悉泛型中的 variance ,甚至可能不知道他们今天使用的支持 variance 的泛型。 对于消费者,价值较小,但对于控制和类库作者而言,可以成为一个非常有价值的机制。
正是在实现逻辑中, variance 可以为我们提供额外的编译器和智能提示支持,这将有助于保持代码更加清洁,并使您的意图更加明确。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接