ASP.NET Core中的依赖注入【下】

此为系列文章,对MSDN ASP.NET Core 的官方文档进行系统学习与翻译。其中或许会添加本人对 ASP.NET Core 的浅显理解

服务注册方法

服务注册扩展方法提供了适用于各个场景下的重载。如下表格所示:

MethodAutomatic

object

disposal

Multiple

implementations

Pass args
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>()

Example:

services.AddSingleton<IMyDep, MyDep>();

YesYesNo
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION})

Examples:

services.AddSingleton<IMyDep>(sp => new MyDep());

services.AddSingleton<IMyDep>(sp => new MyDep("A string!"));

YesYesYes
Add{LIFETIME}<{IMPLEMENTATION}>()

Example:

services.AddSingleton<MyDep>();

YesNoNo
AddSingleton<{SERVICE}>(new {IMPLEMENTATION})

Examples:

services.AddSingleton<IMyDep>(new MyDep());

services.AddSingleton<IMyDep>(new MyDep("A string!"));

NoYesYes
AddSingleton(new {IMPLEMENTATION})

Examples:

services.AddSingleton(new MyDep());

services.AddSingleton(new MyDep("A string!"));

N

关于类型清理的更多信息,请参考the Disposal of services这一章节,对于多个实现的一个通用场景便是模拟类型用来测试。

只有在还没有注册的实现的时候,TryAdd{LIFETIME}方法才会注册一个服务。

在如下的代码中,第一行为IMyDependency注册了MyDependency,而第二行没有任何效果,因为IMyDependency已经有了一个注册的实现。

services.AddSingleton<IMyDependency, MyDependency>();
// The following line has no effect:
services.TryAddSingleton<IMyDependency, DifferentDependency>();

更多信息,请参考:

TryAddEnumerable(ServiceDescriptor) 方法仅仅在还没有相同类型的实现时,才会注册一个服务。多个服务通过IEnumerable<{SERVICE}>来进行解析。当注册服务时,如果相同类型之中的一个还没有注册时,开发者仅仅只想添加一个实例。这个方法被库的作者用来避免在容器中注册了一个实例的两个拷贝。

在如下的示例中,第一行为IMyDep1注册了MyDep,第二行为IMyDep2注册了MyDep。而第三行不会有任何效果,因为IMyDep1已经有一个注册号的实现MyDep

public interface IMyDep1 {}
public interface IMyDep2 {}

public class MyDep : IMyDep1, IMyDep2 {}

services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyDep1, MyDep>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyDep2, MyDep>());
// Two registrations of MyDep for IMyDep1 is avoided by the following line:
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyDep1, MyDep>());

构造函数注入行为

可以通过两种方式来解析服务:

  • IServiceProvider
  • ActivatorUtilities,允许在不使用DI容器中的注册服务的情况下创建一个对象。ActivatorUtilities随着面向用户的抽象层一起被使用,比如Tag Helpers,MVC控制器,模型绑定器。

构造函数可以接受并非是DI提供的参数,但是必须为那些参数分配一个默认值。当服务通过IServiceProvider或者ActivatorUtilities进行解析时,构造函数注入则需要一个public的构造函数。

当服务通过ActivatorUtilities进行解析时,构造函数注入必须只能有一个可用的构造函数存在。ASP.NET Core DI支持构造函数重载,但是只能有一个合适的构造函数存在,其所有的参数可以被DI所满足。

Entity Framework上下文

通常我们使用scoped生命周期来将实体框架上下文添加进服务容器中,这是因为wep app数据库操作通常对于每一次的客户端请求是scoped。当注册数据库上下文时,如果我们通过调用AddDbContext<TContext>重载而不指定生命周期的话,默认的生命周期便是scoped。给定生命周期的服务不应该使用比其生命周期更短的数据库上下文服务。

生命周期以及注册选项

为了演示生命周期以及注册选项的不同,考虑如下的一个接口,其将任务体现为带有唯一标识符的一个操作,OperationId。对于下列服务来说,由于操作服务的生命周期的配置的不同,当被一个类请求时,容器要么提供一个相同的,要么提供一个不同的服务实例。

public interface IOperation
{
    Guid OperationId { get; }
}

public interface IOperationTransient : IOperation
{
}

public interface IOperationScoped : IOperation
{
}

public interface IOperationSingleton : IOperation
{
}

public interface IOperationSingletonInstance : IOperation
{
}

这些接口在Operation类中被实现。如果一个GUID都没有提供的话,Operation的构造函数会生成一个。

public class Operation : IOperationTransient, 
    IOperationScoped, 
    IOperationSingleton, 
    IOperationSingletonInstance
{
    public Operation() : this(Guid.NewGuid())
    {
    }

    public Operation(Guid id)
    {
        OperationId = id;
    }

    public Guid OperationId { get; private set; }
}

我们也注册了一个OperationService服务,其依赖于各个其他的Operation类型。当我们通过依赖注入来请求一个OperationService时,它要么接收各个服务的新实例,要么使用一个已经存在的实例,这要取决于它所依赖的各个服务的生命周期。

  • 当从容器中请求的transient服务被创建时,IOperationTransient服务的OperationId是与OperationService的OperationId不同的。OperationService需要一个IOperationTransient类的新实例,而新的实例会导致不同的OperationId
  • 当每一次客户端请求创建一个scoped服务时,IOperationScoped服务的OperationId都会是和OperationServiceOperationId相同的,因为它们都是在一个客户端请求之中。但在不同的客户端请求之间,scoped服务的OperationId值是不同的。
  • 当singleton和singleton-instance服务被创建之后,它们会被使用在整个客户端请求以及所有的服务中,因此在所有的服务请求中,OperationId都是不变的。
public class OperationService
{
    public OperationService(
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance instanceOperation)
    {
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = instanceOperation;
    }

    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance { get; }
}

Startup.ConfigureServices中,每一个类型都会根据它们的命名生命周期被添加进容器中:

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddScoped<IMyDependency, MyDependency>();
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();
    services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));

    // OperationService depends on each of the other Operation types.
    services.AddTransient<OperationService, OperationService>();
}

IOperationSingletonInstance这个服务使用了一个带有已知ID(Guid.Empty)的特定实例。当这个类型被使用时,我们很明白它的GUID是全部为0。

这个示例app演示了在一个请求之内以及多个请求之间对象的生命周期。示例app的IndexModel请求了各个类型的IOperation以及OperationService服务。然后页面通过页面赋值,显示了所有页面模型类以及服务的OperationId值。

public class IndexModel : PageModel
{
    private readonly IMyDependency _myDependency;

    public IndexModel(
        IMyDependency myDependency, 
        OperationService operationService,
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance singletonInstanceOperation)
    {
        _myDependency = myDependency;
        OperationService = operationService;
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = singletonInstanceOperation;
    }

    public OperationService OperationService { get; }
    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }

    public async Task OnGetAsync()
    {
        await _myDependency.WriteMessage(
            "IndexModel.OnGetAsync created this message.");
    }
}

以下输出显示了两个请求的结果:

第一次请求:

Controller operations:

Transient: d233e165-f417-469b-a866-1cf1935d2518
Scoped: 5d997e2d-55f5-4a64-8388-51c4e3a1ad19
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instance: 00000000-0000-0000-0000-000000000000

OperationService operations:

Transient: c6b049eb-1318-4e31-90f1-eb2dd849ff64
Scoped: 5d997e2d-55f5-4a64-8388-51c4e3a1ad19
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instance: 00000000-0000-0000-0000-0000000000

第二次请求:

Controller operations:

Transient: b63bd538-0a37-4ff1-90ba-081c5138dda0
Scoped: 31e820c5-4834-4d22-83fc-a60118acb9f4
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instance: 00000000-0000-0000-0000-000000000000

OperationService operations:

Transient: c4cbacb8-36a2-436d-81c8-8c1b78808aaf
Scoped: 31e820c5-4834-4d22-83fc-a60118acb9f4
Singleton: 01271bc1-9e31-48e7-8f7c-7261b040ded9
Instance: 00000000-0000-0000-0000-000000000000

观察在一个请求和两次请求之间,哪个OperationId值发生了变化:

  • Transient对象总是不同的,transient OperationId 值对于第一次和第二次请求,在OperationService中以及整个客户端请求中都是不同的。每次客户端请求以及服务请求都会提供一个新的实例。
  • Scoped 对象在一个客户端请求是相同的,而在不同的客户端请求之间是不同的。
  • Singleton 对象对于每一个对象以及每一次请求都是相同的,不管是否有一个Operation实例在Startup.ConfigureServices.已经被提供。

从main函数中调用服务

使用IServiceScopeFactory.CreateScope 创建一个IServiceScope 来在app的域中解析一个scoped服务。这种方法可用来在startup中访问一个scoped服务,用以执行初始化任务。以下示例演示了如何在Program.Main中为MyScopedService获取上下文:

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

public class Program
{
    public static async Task Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        using (var serviceScope = host.Services.CreateScope())
        {
            var services = serviceScope.ServiceProvider;

            try
            {
                var serviceContext = services.GetRequiredService<MyScopedService>();
                // Use the context here
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred.");
            }
        }

        await host.RunAsync();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

作用域验证

当app运行于开发环境并调用CreateDefaultBuilder来创建宿主时,默认的服务器会执行一些检查以校验:

  • scoped服务不会直接或者间接从根服务提供器来进行解析。
  • scoped服务不会直接或者间接注入到单例对象中。

BuildServiceProvider被调用时会创建根服务提供器。根服务提供器的生命周期对应于app的生命周期,它随着app的开始而开始,随着app的关闭而销毁。

scoped服务会被创建它们的服务所销毁。如果一个scoped服务在根容器中被创建,那么这个服务的生命周期便会提升到单例,这是因为只有当appg关闭时候它才会被根容器销毁。当BuildServiceProvider被调用时,对服务域的验证会捕获所有这些情况。

更多信息,请参考ASP.NET Core Web Host

请求服务

在ASP.NET Core请求中可用的服务是通过HttpContext.RequestServices这个集合暴漏出来的。

请求服务代表着那些作为app的一部分被配置和请求的服务。当对象指定了一些依赖,它们会用在RequestServices中发现的类型所满足,而不会使用ApplicationServices

通常来说,app不会直接使用这些属性。相反,会使用类的构造函数来请求这些类所需要的类型,并允许框架来注入这些依赖。这使得类更加容易进行测试。

注意:作为构造函数参数来请求依赖而不要直接访问RequestServices集合。

针对依赖注入设计服务

最佳实践是:

  • 将服务设计为使用依赖注入来获取它们的依赖。
  • 避免有状态的,静态的类以及成员。设计app来使用单例服务来代替,这会避免创建全局状态。
  • 避免在服务中直接实例化一个依赖类。将实例化代码放到一个特定的实现中。
  • 使app类小,结构简洁,易于测试。

如果一个类似乎有太多的注入的依赖,这大体上可以说明这个类承担了太多的责任并违反了Single Responsibility Principle (SRP)。可以通过将其 一部分责任移到一个新的类中来重构它。请务必记住Razor Pages页面模型类以及MVC控制器类应该集中于UI概念。业务规则以及数据访问实现细节应该保持在另外一些合适的类中,他们是相互独立的概念。

服务的销毁

对于容器创建的IDisposable类型,它会调用Dispose方法。如果一个实例通过用户代码被添加进容器,那么它不会被自动销毁。

// Services that implement IDisposable:
public class Service1 : IDisposable {}
public class Service2 : IDisposable {}
public class Service3 : IDisposable {}

public interface ISomeService {}
public class SomeServiceImplementation : ISomeService, IDisposable {}

public void ConfigureServices(IServiceCollection services)
{
    // The container creates the following instances and disposes them automatically:
    services.AddScoped<Service1>();
    services.AddSingleton<Service2>();
    services.AddSingleton<ISomeService>(sp => new SomeServiceImplementation());

    // The container doesn't create the following instances, so it doesn't dispose of
    // the instances automatically:
    services.AddSingleton<Service3>(new Service3());
    services.AddSingleton(new Service3());
}

默认服务容器的替换

内置的服务容器被设计为服务于框架及大部分消费的app。我们推荐使用内置的服务容器,除非你需要一个特定的特性,而恰好内置的容器不支持它,比如:

  • Property injection
  • Injection based on name
  • Child containers
  • Custom lifetime management
  • Func<T> support for lazy initialization
  • Convention-based registration

下列第三方容器可以和ASP.NET Core app一起使用:

线程安全

创建线程安全的单例服务。如果一个单例服务依赖于一个transient服务,那么这个transient服务或许也需要线程安全,这取决于单例服务将如何使用它。

一个单独服务的工厂方法,比如AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>)方法的第二个参数,不需要是线程安全的。像是一个静态类型的构造方法,它保证被一个单独的线程调用一次。

一些建议

  • 基于async/await以及task服务不会被支持,C#不支持异步构造函数。因此,推荐的模式是在同步解析服务对象后使用异步方法。
  • 避免将数据以及配置直接存储在服务容器中。举个例子,一个用户的购物车不应该被添加进服务容器中。配置应该使用选项模式。类似的,避免 数据持有对象的存在而访问其他对象。最好是通过DI来请求实际的条目。
  • 避免对服务的静态访问(例如,静态类型的IApplicationBuilder.ApplicationServices用于任何地方的使用)。
  • 避免使用服务定位模式。例如,当你可以通过DI来获取一个服务实例的时候,不要通过GetService 来获取它

错误:

public class MyClass()
{
    public void MyMethod()
    {
        var optionsMonitor = 
            _services.GetService<IOptionsMonitor<MyOptions>>();
        var option = optionsMonitor.CurrentValue.Option;

        ...
    }
}

正确:

public class MyClass
{
    private readonly IOptionsMonitor<MyOptions> _optionsMonitor;

    public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
    {
        _optionsMonitor = optionsMonitor;
    }

    public void MyMethod()
    {
        var option = _optionsMonitor.CurrentValue.Option;

        ...
    }
}
  • 另一个需要避免的服务定位变量是注入一个在运行时解析依赖的工厂,这两个实践混合了IoC策略。
  • 避免对HttpContext的静态访问。

像所有的推荐一样,你会遇到一些需要忽视一条推荐的情况,然而异常情况是比较少的--主要是框架本身内部的特殊情况。

DI是静态/全局访问模式的一种替换。如果你将它与静态访问对象混合起来使用,或许你会意识不到它所带来的益处。

其他资源