ASP.NET Core中使用Scrutor扩展原生DI

1 ASP.NET Core DI容器

ASP.NET Core框架中默认自带了一个依赖注入组件,这个组件包括了一个简单的DI容器,可以为ASP.NET Core应用提供基础的容器能力。我们可以在这个Nuget包中找到这个容器:Microsoft.Extensions.DependencyInjection NuGet package..

其实已经有大量的第三方.NET DI库可以提供更多的容器管理能力,我们可以选择的已经很多了,例如:

  • Autofac

  • Windsor

  • StructureMap/Lamar

  • Simple Injector

  • Ninject

  • Dryloc

这些容器都提供了不同的能力和专长:基于attribute的配置、属性注入、性能优化 等等。

一个常见的功能是自动注入,它一般是基于对程序集的扫描,找到那些匹配的约定,然后帮我们注入容器,这可以极大地降低我们在Startup.ConfigureServices方法中的人工手动注入的工作量。

例如,如果你的注入代码一般是这样的:

services.AddScoped<IFoo, Foo>();
services.AddScoped<IBar, Bar>();
services.AddScoped<IBaz, Baz>();

然后,你可以通过扫描程序集来简化你的DI注入代码,就像这样,是不是简便许多,以后再配置注入,都不需要写代码了。

services.Scan(scan => 
    scan.FromCallingAssembly()                    
        .AddClasses()
        .AsMatchingInterface());

2  Scrutor vs 第三方DI容器

事实上,Scrutor并不是一个新的DI容器,它在底层仍然还是使用的ASP.NET Core的默认DI容器。因此,这里总结一下它的优势和劣势,供开发人员参考:

优势:

  • 可以十分简单快速地加入已有的ASP.NET Core应用。你只需要简单添加一个Scrutor的包,不需要引入其他额外的组件,特别是你不需要改变默认的DI容器。

  • 你也将Scrutor和其他的DI容器搭配。因为Scrutor使用的是内置的DI容器,并且大多数第三方的DI容器都提供了和ASP.NET Core的适配,因此它也可以和其他DI容器兼容工作。

  • Scrutor具有较强的稳定性,即使内置的DI容器发生了改变。希望这不会成为一个担忧,因为ASP.NET Core团队对DI容器做了许多重大的更改。

劣势:

  • 降低了功能性。因为它使用了内置DI容器,因此Scrutor经常会被其限制。内置DI容器的目的是保持简单,因此无法获取更多额外的重要功能。

我相信还有很多其他的劣势或缺点,因为降低功能性这个缺点就是一个较大的缺点。如果你已经使用了一款更为强大的DI容器,你可以继续坚定不移地使用它。另一方面,如果你目前仍然在用内置的DI容器,Scrutor是一个值得你看看并能简化你工作量的组件。

你可以通过在.NET CLI 或 Nuget管理 运行以下命令来安装Scrutor:

dotnet add package Scrutor

下一小节,我会为你演示如何使用Scrutor来进行程序集扫描帮你进行自动注入。

3 使用Scrutor进行程序集扫描

Scrutor API包含了针对IServiceCollection的两个扩展方法:Scan() 和 Decorate()。本文会主要介绍Scan()方法,以及它提供的一些选项。

Scan()方法提供了一个参数:一个需要定义4个点的配置项委托:

  • 一个选择器(Selector) - 哪些实现类需要注册?

  • 一个注册策略(Registration Strategy) - 如何处理重复的services 或 实现类

  • Services - 哪些服务的具体实现需要注册为services

  • 声明周期 - 即Singleton、Scoped 和 Transient 三种生命周期

 
例如,在一个Scan方法中,我们可以像如下代码示例一样添加所有具体类作为Transient生命周期的services:
services.Scan(scan => scan     
  .FromCallingAssembly() // 1. Find the concrete classes
    .AddClasses()        //    to register
      .UsingRegistrationStrategy(RegistrationStrategy.Skip) // 2. Define how to handle duplicates
      .AsSelf()    // 2. Specify which services they are registered as
      .WithTransientLifetime()); // 3. Set the lifetime for the services

让我们添加一些接口和实现来继续看看,假设我们在某个程序集中有如下所示的接口和实现:

public interface IService { }
public class Service1 : IService { }
public class Service2 : IService { }
public class Service : IService { }
public interface IFoo {}
public interface IBar {}
public class Foo: IFoo, IBar {}

那么,在之前的Scan()方法就会帮我们注入Service1, Service2, Service3 以及 Foo 类,它等同于下面直接使用内置DI容器的代码:

services.AddTransient<Service1>();
services.AddTransient<Service2>();
services.AddTransient<Service>();
services.AddTransient<Foo>();

在下一小节,我们看看第一个点:选择器(Selector) - 选择哪些实现类注入。

3.1 选择和过滤需要注册的类

Scrutor提供了大量不同的方式来搜索类型 及 过滤,在这一小节我就来介绍一下可供使用的不同选项。

如果你已经使用了其他DI容器如StructureMap或Autofac提供的程序集扫描能力,那你应该对此很熟悉了。
(1)明确指定类型

最简单的类型指定如下所示,我们注册了两个service:Service1和Service2,都是瞬时生命周期:

services.Scan(scan => scan
  .AddTypes<Service1, Service2>()
    .AsSelf()
    .WithTransientLifetime());

它等同于下面的代码:

services.AddTransient<Service1>();
services.AddTransient<Service2>();

通过AddTypes<>方法,可以添加多个泛型参数。但是,在实际场景中,你可能更需要使用程序集扫描的方式来自动注入。

(2)扫描某个程序集指定类型

实际上,Scrutor最大的特点就在于提供了程序集扫描来自动注入的能力。例如,下面的示例中,Scrutor会从所有的程序集 或 当前执行的程序集 中寻找包含IService的具体类:

services.Scan(scan => scan
  .FromAssemblyOf<IService>()
    .AddClasses()
      .AsSelf()
      .WithTransientLifetime());

基于目前3.0.0版本的Scrutor(这个版本是原文作者写这篇文章的时候的最新版本,目前最新版本是4.1.0了),它提供了以下的一些程序集扫描的方法供你选择:

  • FromAssemblyOf<>, FromAssemblyOf - 从所有程序集中找寻包含类型 或 多个类型 的实现

  • FromCallingAssembly, FromExecutingAssembly, FromEntryAssembly - 扫描正被调用的、执行的 或 程序入口 程序集。如果你分不清楚这几个程序集的区别,推荐阅读:https://docs.microsoft.com/zh-cn/dotnet/api/system.reflection.assembly?view=netstandard-2.0

  • FromAssemblyDependencies - 扫描某个程序集依赖的所有程序集

  • FromApplicationDependencies, FromDependencyContext - 扫描运行时库。我不是太了解这个DependencyContext,因此你可以自己尝试一下。

(3)过滤找到的类

无论你选择哪一种程序集扫描的方式,你都需要随后调用 AddClasses() 方法来选择需要加入容器的具体类。这个方法有一些重载,你可以使用它们来过滤出你真正想要注入的类型:

  • AddClasses() - 添加所有public 和 非抽象 的类

  • AddClasses(publicOnly) - 添加所有非抽象的类, 你也可以通过传递 publicOnly = false 来添加internal/private的内部类。

  • AddClasses(predicate) - 运行一个任意的Action来过滤,这是非常实用的一个参数,下面会展示示例。

  • AddClasses(predicate, publicOnly) - 一种结合了上面两种方式的混合使用。

通过predicate委托来过滤具体类是一个非常实用的功能,你可以在不同的方式下使用此predicate委托。例如,假设你只想注入某些实现了某个具体接口的类:

services.Scan(scan => scan
  .FromAssemblyOf<IService>()
    .AddClasses(classes => classes.AssignableTo<IService>())
        .AsImplementedInterfaces()
        .WithTransientLifetime());

或者你也可以添加过滤条件:只存在于某个命名空间下

services.Scan(scan => scan
  .FromAssemblyOf<IService>()
    .AddClasses(classes => classes.InNamespaces("MyApp"))
        .AsImplementedInterfaces()
        .WithTransientLifetime());

或者,你也可以基于Type本身使用一些过滤条件:

services.Scan(scan => scan
  .FromAssemblyOf<IService>()
    .AddClasses(classes => classes.Where(type => type.Name.EndsWith("Repository"))
        .AsImplementedInterfaces()
        .WithTransientLifetime());

一旦你定义了你需要注入的具体类的选择器,你可以选择定义你的替换策略了。

3.2 通过替换策略处理重复的services

Scrutor允许你通过替换策略(ReplacementStrategy)控制如何处理一些已经被DI容器注入的services。目前,Scrutor提供了五种可供使用的替换策略:

  • Append - 不用担心重复的注入,这是默认的选项。如果,你不显示声明一个注册策略。

  • Skip - 如果某个service已经被注入到容器,不会再添加一个新的注入。

  • Replace(ReplacementBehavior.ServiceType) -  如果某个service已经被注入到容器,在创建新注入之前,移除所有之前已注入的。

  • Replace(ReplacementBehavior.ImplementationType) - 如果某个实现已经被注入到容器。

要选择一个替换策略,你可以在定义类型选择器之后使用 UsingRegistrationStrategy() 方法:

services.Scan(scan => scan
  .FromAssemblyOf<IService>()
    .AddClasses()
      .UsingRegistrationStrategy(RegistrationStrategy.Skip)
      .AsSelf()
      .WithTransientLifetime());

也许你对这些替换策略的区别还是有点懵逼,别急,我们一个一个来看。假设我们的DI容器中已经包含了以下注入:

services.AddTransient<ITransientService, TransientService>();
services.AddScoped<IScopedService, ScopedService>();

随后,在扫描过程中,Scrutor会找到下列类并将其注入为瞬时生命周期的键值对:

public class TransientService : IFooService {}
public class AnotherService : IScopedService {}

那么,对于各种不同的替换策略,最后的结果如何呢?

先来看看Append策略,答案很明显,你可以获取到所有的类:

services.AddTransient<ITransientService, TransientService>(); // From previous registrations
services.AddScoped<IScopedService, ScopedService>(); // From previous registrations
services.AddTransient<IFooSerice, TransientService>();
services.AddScoped<IScopedService, AnotherService>();

再看看Skip策略,重复的IScopedService将会被忽略,但是最后结果是增加了一个 TransientService/IFooService 的键值对:

services.AddTransient<ITransientService, TransientService>(); // From previous registrations
services.AddScoped<IScopedService, ScopedService>(); // From previous registrations
services.AddTransient<IFooSerice, TransientService>();

再看看Replace(ReplacementBehavior.ServiceType)策略,在这个案例下,原先指定的IScopedService的实现类ScopedService会被新的实现类AnotherService替换进行注入,如下所示:

services.AddTransient<ITransientService, TransientService>(); // From previous registrations
services.AddTransient<IFooSerice, TransientService>();
services.AddScoped<IScopedService, AnotherService>(); // Replaces ScopedService

随后看看Replace(ReplacementBehavior.ImplementationType),在这个案例下,原先指定的ITransientService的实现类TransientService由于并没有实现ITransientService而是实现了IFooService,因此它的注入会被更改为IFooService,同时多余的IScopedService仍然会被追加,如下所示:

services.AddScoped<IScopedService, ScopedService>(); // From previous registrations
services.AddTransient<IFooSerice, TransientService>(); // Changed from ITransientService to IFooService
services.AddScoped<IScopedService, AnotherService>();

最后,如果我们使用Replace(ReplacementBehavior.All)策略,之前的注入都会被移除,并替换为以下代码:

services.AddTransient<IFooSerice, TransientService>();
services.AddScoped<IScopedService, AnotherService>();

替换策略可能是你最难理解的一个缓解,相信经过上面的讲解,你会对Scrutor的自动注入是否会对已有手动注入的任何类有影响有一个较为清晰的了解。

3.3 将实现类注册为一个service

我们已经探讨了怎样添加实现类 和 实现策略。但是,我们仍然需要选择这些类如何被注入到容器中。

Scrutor提供了许多的选项,我会一一带你了解,并展示对应的“手动”注入是怎样的。这些选项如下所示:

  • AsSelf()

  • AsMatchingInterface()

  • AsImplementedInterfaces()

  • AsSelfWithInterfaces()

  • As<>()

如果你正在用默认的注入策略,你可以在 AddClasses() 方法之后调用上述任何一个方法。或者你用了某个策略,在 UsingRegistrationStrategy() 方法之后调用即可。

services.Scan(scan => scan
  .FromAssemblyOf<IService>()
    .AddClasses()
      .AsSelf() // Specify how the to register the implementations/services
      .WithSingletonLifetime());

对于这个案例,我假设Scrutor已经通过程序集扫描发现了以下类:

public class TestService: ITestService, IService {}
(1)通过AsSelf直接注入

对于一些并没有实现某个接口的类来说,或者你就想直接注入的类,你可以直接使用AsSelf()方法进行注入。它等同于下面的代码:

services.AddSingleton<TestService>();
(2)通过标准命名匹配注入

我们常见的一个模式就是 一个class实现了一个对应的interface。因此,你可以通过使用 AsMatchingInterface() 方法将其注入。这种方式等同于:

services.AddSingleton<ITestService, TestService>();
(3)通过一个实现类对应多个接口注入

如果一个类实现了多个接口,那么有时候你就想要将其匹配多个接口进行注入。那么,你可以通过使用 AsImplementedInterfaces() 方法注入。这种方式等同于:

services.AddSingleton<ITestService, TestService>();
services.AddSingleton<IService, TestService>();

(4)通过使用转发服务注入某个实现类

ASP.NET Core DI容器并不支持“转发”服务类型,所以你基本上只能通过手动使用对象工厂来实现。例如:

services.AddSingleton<TestService>();
services.AddSingleton<ITestService>(x => x.GetRequiredService<TestService>());
services.AddSingleton<IService>(x => x.GetRequiredService<TestService>());

使用Scrutor,你可以轻松地使用 AsSelfWithInterfaces() 方法来实现这个模式。

(5)将一个实现类注入为任意service

最后一个注入选项是定义一个service,例如,假设我们对 IMyService 使用 As() 方法,即 As(),最终注入的结果如下所示:

services.AddSingleton<IMyService, TestService>();
需要注意的是如果你试着注入了一个应用中并没有实现的service,你会在运行时得到一个 InvalidOperationException 的异常。

现在,你应该已经对不同的注入选项有了一定的了解了。最后一个需要考虑的就是声明周期了,幸运的是,Scrutor使用了内置的DI容器,它和你熟悉的ASP.NET Core DI容器中的设置几乎没什么差别。

4 为注入的类指定声明周期

Scrutor也提供了三种和ASP.NET Core中对应的生命周期:
(1)WithTransientLifetime() - 默认选项,如果不声明的话。
(2)WithScopedLifetime() - 与请求的生命周期对应。
(3)WithSingletonLifetime() - 单例模式,全局唯一。

5 多个选择器链式声明

实际场景中,你会为不同的子集设置不同的生命周期。Scrutor的API允许你通过链式在一个地方定义多个子集的生命周期。这种体验非常自然,如下所示:

services.Scan(scan => scan
  .FromAssemblyOf<CombinedService>()
    .AddClasses(classes => classes.AssignableTo<ICombinedService>()) // Filter classes
      .AsSelfWithInterfaces()
      .WithSingletonLifetime()

    .AddClasses(x=> x.AssignableTo(typeof(IOpenGeneric<>))) // Can close generic types
      .AsMatchingInterface()

    .AddClasses(x=> x.InNamespaceOf<MyClass>())
      .UsingRegistrationStrategy(RegistrationStrategy.Replace()) // Defaults to ReplacementBehavior.ServiceType
      .AsMatchingInterface()
      .WithScopedLifetime()

  .FromAssemblyOf<DatabaseContext>()   // Can load from multiple assemblies within one Scan()
    .AddClasses() 
      .AsImplementedInterfaces()
);

总之,Scrutor可以支持你实现任何你需要手动在ASP.NET Core DI容器中的注入配置,而且以一种更为简洁的方式。如果你不想引入第三方的DI容器,就想使用内置的DI容器的话,我强烈建议你了解Scrutor。

6 总结

Scrutor 为 ASP.NET Core内置的DI容器 添加了一个非常实用的功能:程序集扫描自动注入,它不是一个第三方的DI容器,但是却增加了一些扩展,让我们能更加简单地注入我们的services。

要注入你的services,我们只需要在 Startup.ConfigureServices 方法中调用 Scan() 方法即可。你只需要定义四件事情:

  • 一个选择器(Selector) - 哪些实现类需要注册?

  • 一个注册策略(Registration Strategy) - 如何处理重复的services 或 实现类

  • Services - 哪些服务的具体实现需要注册为services

  • 声明周期 - 即Singleton、Scoped 和 Transient 三种生命周期

你可以将多个Scan整合成一个调用链,来为你的类应用不同的规则子集。如果你对Scrutor感兴趣,你可以访问github:https://github.com/khellang/Scrutor 了解更多!

Ref 原文信息

作者:Andrew Lock

链接:https://andrewlock.net/using-scrutor-to-automatically-register-your-services-with-the-asp-net-core-di-container