如何在 ASP.NET Core DI 中注册具有多个接口的服务

文章讨论了在ASP.NETCore中使用Microsoft.Extensions.DependencyInjection注册实现多个接口的类时遇到的问题。当一个类如`MyTestClass`实现`ISomeInterface`和`ISomethingElse`,默认注册会导致为每个接口创建单独的实例。解决方法包括提供服务实例(适用于单例)和使用工厂方法来转发请求。文章强调了使用工厂方法的必要性,尽管它不是最高效的方式。
摘要由CSDN通过智能技术生成

在本文中,我描述了如何在 ASP.NET Core 中使用的 Microsoft.Extensions.DependencyInjection 容器中注册具有多个公共接口的具体类。 使用这种方法,您将能够使用具体类实现的任何接口来检索具体类。 例如,如果您有以下类:

public class MyTestClass: ISomeInterface, ISomethingElse { }

然后您将能够注入 ISomeInterfaceISomethingElse,您将收到相同的 MyTestClass 实例。

以特定方式注册 MyTestClass 以避免意外的生命周期问题很重要,例如有两个单例实例!

在这篇文章中,我简要概述了 ASP.NET Core 中的 DI 容器及其与第三方容器相比的一些限制。 然后,我将描述将对多个接口的请求“转发”到具体类型的概念,以及如何使用 ASP.NET Core DI 容器实现此目的。

ASP.NET Core DI 容器本身不支持将实现注册为多个服务(有时称为“转发”)。相反,您必须手动将服务的解析委托给工厂函数,例如
services.AddSingleton<IFoo>(x=> x.GetRequiredService<Foo>())

ASP.NET Core 中的依赖注入

ASP.NET Core 的关键特性之一是它使用依赖注入 (DI)。 该框架是围绕“符合容器”的抽象设计的,它允许框架本身使用一个简单的容器,同时还允许您插入功能更丰富的第三方容器。

“一致容器”的想法并非没有争议——我建议阅读 Mark Seemann 的这篇关于将一致容器作为反模式的文章,或者这篇来自 SimpleInjector 团队的关于 ASP.NET Core DI 容器的文章。

为了让第三方容器尽可能简单地实现符合标准的容器,它公开了非常有限的 API。 对于给定的服务(例如 IFoo),您可以定义实现它的具体类(例如 Foo),以及它应该具有的生命周期(例如 Singleton)。 在这方面有一些变体,您可以直接提供服务的实例,也可以提供工厂方法,但这是您所能得到的最复杂的方法。

相比之下,.NET 中的第三方 DI 容器通常会提供更高级的注册 API。 例如,许多 DI 容器公开了一个用于配置的“扫描”API,您可以在其中搜索程序集中的所有类型,并将它们添加到您的 DI 容器中。 以下是一个 Autofac 示例:

var dataAccess = Assembly.GetExecutingAssembly();

builder.RegisterAssemblyTypes(dataAccess) // find all types in the assembly
       .Where(t => t.Name.EndsWith("Repository")) // filter the types
       .AsImplementedInterfaces()  // register the service with all its public interfaces
       .SingleInstance(); // register the services as singletons

在此示例中,Autofac 将在程序集中查找名称以“Repository”结尾的所有具体类,并根据它们实现的任何公共接口将它们注册到容器中。 因此,例如,给定以下类和接口:

public interface IStubRepository {}
public interface ICachingRepository {}

public class StubRepository : IStubRepository {}
public class MyRepository : ICachingRepository {}

前面的Autofac代码相当于手动在Startup.ConfigureServices中的ASP.NET Core容器中注册这两个类及其各自的接口:

services.AddSingleton<IStubRepository, StubRepository>();
services.AddSingleton<ICachingRepository, MyRepository>();

但是如果一个类实现了多个接口会怎样呢?

将单个实现注册为多个服务

实现多个接口的类很常见,例如:

public interface IBar {}
public interface IFoo {}

public class Foo : IFoo, IBar {}

让我们编写一个快速测试,看看如果我们使用 ASP.NET Core DI 容器针对两个接口注册类会发生什么:

[Fact]
public void WhenRegisteredAsSeparateSingleton_InstancesAreNotTheSame()
{
    var services = new ServiceCollection();

    services.AddSingleton<IFoo, Foo>();
    services.AddSingleton<IBar, Foo>();

    var provider = services.BuildServiceProvider();

    var foo1 = provider.GetService<IFoo>(); // An instance of Foo
    var foo2 = provider.GetService<IBar>(); // An instance of Foo

    Assert.Same(foo1, foo2); // FAILS
}

我们将 Foo 注册为 IFooIBar 的单例,但结果可能不是您所期望的。 我们实际上有两个 FooSingleton”实例,各自用于它注册的每个服务。

转发服务请求

针对多个服务注册一个实现的一般模式是一种常见的模式。 大多数第三方 DI 容器都内置了这个概念。例如:

  • Autofac 默认使用此行为 - 之前的测试会通过
  • Windsor 具有“转发类型”的概念,允许您将多个服务“转发”到单个实现
  • StructureMap(现已停用)具有类似的“转发”类型概念。 据我所知,它的继任者 Lamar 还没有,但我在这一点上可能是错的。

鉴于此要求非常普遍,ASP.NET Core DI 容器不可能实现这一点似乎很奇怪。 这个问题是在 2 年前提出的(由 David Fowler 提出),但已关闭。 幸运的是,有几个概念上简单但有些不雅的解决方案。

1.提供服务实例(仅限单例)

最简单的方法是在注册服务时提供 Foo 的实例。 每个注册的服务都将返回您在请求时提供的确切实例,确保只有一个实例。

[Fact]
public void WhenRegisteredAsInstance_InstancesAreTheSame()
{
    var foo = new Foo(); // The singleton instance
    var services = new ServiceCollection();

    services.AddSingleton<IFoo>(foo);
    services.AddSingleton<IBar>(foo);

    var provider = services.BuildServiceProvider();

    var foo1 = provider.GetService<IFoo>();
    var foo2 = provider.GetService<IBar>();

    Assert.Same(foo1, foo); // PASSES;
    Assert.Same(foo2, foo); // PASSES;
}

对此有一个很大的警告——您必须能够在配置时实例化 Foo,并且您必须知道并提供它的所有依赖项。 这在某些情况下可能对您有用,但不是很灵活。

此外,您只能使用这种方法来注册单例。 如果您希望 Foo 成为每个请求范围(Scoped)的单个实例,那么您就不走运了。 相反,您需要使用以下技术。

2.使用工厂方法实现转发

如果我们分解我们的需求,就会出现一个替代解决方案:

  • 我们希望我们注册的服务(Foo)有特定的生命周期(例如 SingletonScoped
  • 当请求 IFoo 时,返回 Foo 的实例
  • 请求 IBar 时,也返回 Foo 的实例

根据这三个规则,我们可以编写另一个测试:

[Fact]
public void WhenRegisteredAsForwardedSingleton_InstancesAreTheSame()
{
    var services = new ServiceCollection();

    services.AddSingleton<Foo>(); // 我们必须显式注册 Foo
    services.AddSingleton<IFoo>(x => x.GetRequiredService<Foo>()); // 将请求转发给 Foo
    services.AddSingleton<IBar>(x => x.GetRequiredService<Foo>()); // 将请求转发给 Foo

    var provider = services.BuildServiceProvider();

    var foo1 = provider.GetService<Foo>(); // Foo 的一个实例
    var foo2 = provider.GetService<IFoo>(); // Foo 的一个实例
    var foo3 = provider.GetService<IBar>(); // Foo 的一个实例

    Assert.Same(foo1, foo2); // 通过
    Assert.Same(foo1, foo3); // 通过
}

为了将对接口的请求“转发”到具体类型,您必须做两件事:

  • 使用 services.AddSingleton<Foo>() 显式注册具体类型
  • 通过提供工厂函数将接口请求委托给具体类型:services.AddSingleton<IFoo>(x =>x.GetRequiredService<Foo>())

使用这种方法,无论您请求哪个实现的服务,您都将拥有一个真正的 Foo 单例实例。

这种提供“转发”类型的方法已在原始问题中指出,并有一个警告——它不是很有效。通常最好尽可能避免使用“服务定位器样式”GetService() 调用。 但是,我觉得在这种情况下这绝对是更可取的做法。

概括

在这篇文章中,我描述了如果将具体类型注册为 ASP.NET Core DI 服务的多个服务会发生什么。 特别是,我展示了如何最终得到 Singleton 对象的多个副本,这可能会导致细微的错误。 要解决这个问题,您可以在注册时提供服务实例,也可以使用工厂方法委托服务解析。 使用工厂方法不是很有效,但通常是最好的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值