ASP.NET Core依赖注入

原文:Dependency Injection
作者:Steve Smith
翻译:刘浩杨
校对:许登洋(Seay)高嵩

ASP.NET Core 的底层设计支持和使用依赖注入。ASP.NET Core 应用程序可以利用内置的框架服务将它们注入到启动类的方法中,并且应用程序服务能够配置注入。由 ASP.NET Core 提供的默认服务容器提供了最小功能集并且不是要取代其他容器。

查看或下载示例代码

什么是依赖注入

依赖注入(Dependency injection,DI)是一种实现对象及其合作者或依赖项之间松散耦合的技术。将类用来执行其操作(Action)的这些对象以某种方式提供给该类,而不是直接实例化合作者或使用静态引用。通常,类会通过它们的构造函数声明其依赖关系,允许它们遵循 显示依赖原则 (Explicit Dependencies Principle) 。这种方法被称为 “构造函数注入(constructor injection)”。

当类的设计使用 DI 思想,它们耦合更加松散,因为它们没有对它们的合作者直接硬编码的依赖。这遵循 依赖倒置原则(Dependency Inversion Principle),其中指出 “高层模块不应该依赖于低层模块;两者都应该依赖于抽象。” 类要求在它们构造时向其提供抽象(通常是 interfaces ),而不是引用特定的实现。提取接口的依赖关系和提供这些接口的实现作为参数也是 策略设计模式(Strategy design pattern) 的一个示例。

当系统被设计使用 DI ,很多类通过它们的构造函数(或属性)请求其依赖关系,有一个类被用来创建这些类及其相关的依赖关系是很有帮助的。这些类被称为 容器(containers) ,或者更具体地,控制反转(Inversion of Control,IoC) 容器或者依赖注入(Dependency injection,DI)容器。容器本质上是一个工厂,负责提供向它请求的类型实例。如果一个给定类型声明它具有依赖关系,并且容器已经被配置为提供依赖类型,它将把创建依赖关系作为创建请求实例的一部分。通过这种方式,可以向类型提供复杂的依赖关系而不需要任何硬编码的类型构造。除了创建对象的依赖关系,容器通常还会管理应用程序中对象的生命周期。

ASP.NET Core 包含了一个默认支持构造函数注入的简单内置容器(由 IServiceProvider 接口表示),并且 ASP.NET 使某些服务可以通过 DI 获取。ASP.NET 的容器指的是它管理的类型为 services。在这篇文章的其余部分, services 是指由 ASP.NET Core 的 IoC 容器管理的类型。你在应用程序 Startup 类的 ConfigureServices 方法中配置内置容器的服务。

注解
Martin Fowler 写过一篇全面的文章发表在 Inversion of Control Containers and the Dependency Injection Pattern. Microsoft 模式与实践小组(Microsoft Patterns and Practices)也有丰富的关于 Dependency Injection 的描述。

注解
本文介绍了依赖注入,因为它适用于所有的 ASP.NET 应用程序。 MVC 控制器中的依赖注入包含在 Dependency Injection and Controllers

使用框架提供的服务

Startup 类的 ConfigureServices 方法负责定义应用程序将使用的服务,包括平台功能,比如 Entity Framework Core 和 ASP.NET Core MVC 。最初, IServiceCollection 只向 ConfigureServices 提供了几个服务定义。下面是一个如何使用一些扩展方法(如 AddDbContextAddIdentity 和 AddMvc )向容器中添加额外服务的例子。

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.AddMvc();

    // Add application services.
    services.AddTransient<IEmailSender, AuthMessageSender>();
    services.AddTransient<ISmsSender, AuthMessageSender>();
}

ASP.NET 提供的功能和中间件,例如 MVC,遵循约定——使用一个单一的 AddService扩展方法来注册所有该功能所需的服务。

小技巧
你可以在 Startup 的方法中通过它们的参数列表请求一些框架提供的服务 - 查看 Application Startup 获取更多信息。

当然,除了使用各种框架功能配置应用程序,你也能够使用 ConfigureServices 配置你自己的应用程序服务。

注册你自己的服务

你可以按照如下方式注册你自己的应用程序服务。第一个泛型类型表示将要从容器中请求的类型(通常是一个接口)。第二个泛型类型表示将由容器实例化并且用于完成这些请求的具体类型。

services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();

注解
每个 services.Add<service> 调用添加(和可能配置)服务。 例如: services.AddMvc() 添加 MVC 需要的服务。

AddTransient 方法用于将抽象类型映射到为每一个需要它的对象分别实例化的具体服务。这被称作为服务的 生命周期(lifetime),另外的生命周期选项在下面描述。为你注册的每一个服务选择合适的生命周期是重要的。应该为每个请求的类提供一个新的服务实例?应该在一个给定的网络请求中使用一个实例?或者应该在应用程序生命周期中使用单例?

在这篇文章的示例中,有一个名称为 CharactersController 的简单控制器。它的 Index 方法显示已经存储在应用程序中的当前字符列表,并且,如果它不存在的话,初始化具有少量字符的集合。值得注意的是,虽然应用程序使用 Entity Framework Core 和 ApplicationDbContext 类作为持久化,这在控制器中都不是显而易见的。相反,具体的数据访问机制被抽象在遵循 仓储模式(repository pattern) 的 ICharacterRepository 接口后面。 ICharacterRepository 的实例是通过构造函数请求并分配给一个私有字段,然后用来访问所需的字符。

public class CharactersController : Controller
{
    private readonly ICharacterRepository _characterRepository;

    public CharactersController(ICharacterRepository characterRepository)
    {
        _characterRepository = characterRepository;
    }

    // GET: /characters/
    public IActionResult Index()
    {
        PopulateCharactersIfNoneExist();
        var characters = _characterRepository.ListAll();

        return View(characters);
    }
    
    private void PopulateCharactersIfNoneExist()
    {
        if (!_characterRepository.ListAll().Any())
        {
            _characterRepository.Add(new Character("Darth Maul"));
            _characterRepository.Add(new Character("Darth Vader"));
            _characterRepository.Add(new Character("Yoda"));
            _characterRepository.Add(new Character("Mace Windu"));
        }
    }
}

ICharacterRepository 只定义了控制器需要使用 Character 实例的两个方法。

using System.Collections.Generic;
using DependencyInjectionSample.Models;

namespace DependencyInjectionSample.Interfaces
{
    public interface ICharacterRepository
    {
        IEnumerable<Character> ListAll();
        void Add(Character character);
    }
}

这个接口在运行时使用一个具体的 CharacterRepository 类型来实现。

注解
在 CharacterRepository 类中使用 DI 的方式是一个你可以在你的应用程序服务遵循的通用模型,不只是在“仓储”或者数据访问类中。

using System.Collections.Generic;
using System.Linq;
using DependencyInjectionSample.Interfaces;

namespace DependencyInjectionSample.Models
{
    public class CharacterRepository : ICharacterRepository
    {
        private readonly ApplicationDbContext _dbContext;

        public CharacterRepository(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public IEnumerable<Character> ListAll()
        {
            return _dbContext.Characters.AsEnumerable();
        }

        public void Add(Character character)
        {
            _dbContext.Characters.Add(character);
            _dbContext.SaveChanges();
        }
    }
}

注意的是 CharacterRepository 需要一个 ApplicationDbContext 在它的构造函数中。依赖注入用于像这样的链式方法并不少见,每个请求依次请求它的依赖关系。容器负责解析所有的依赖关系,并返回完全解析后的服务。

注解
创建请求对象,和它需要的所有对象,以及那些需要的所有对象,有时称为一个 对象图(object graph)。同样的,必须解析依赖关系的集合通常称为 依赖树(dependency tree) 或者 依赖图(dependency graph)

在这种情况下, ICharacterRepository 和 ApplicationDbContext 都必须在 Startup 类ConfigureServices 方法的服务容器中注册。ApplicationDbContext 的配置调用 AddDbContext<T> 扩展方法。下面的代码展示 CharacterRepository 类型的注册。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseInMemoryDatabase()
    );

    // Add framework services.
    services.AddMvc();

    // Register application services.
    services.AddScoped<ICharacterRepository, CharacterRepository>();
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();
    services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
    services.AddTransient<OperationService, OperationService>();
}

Entity Framework 上下文应当使用 Scoped 生命周期添加到服务容器中。如果你使用上图所示的帮助方法则这是自动处理的。仓储将使 Entity Framework 使用相同的生命周期。

警告
最主要的危险是要小心从单例解析一个 Scoped 服务。在这种情况下很可能处理后续请求的时候服务会出现不正确的状态。

服务生命周期和注册选项

ASP.NET 服务可以被配置为以下生命周期:

瞬时
瞬时(Transient)生命周期服务在它们每次请求时被创建。这一生命周期适合轻量级的,无状态的服务。

作用域
作用域(Scoped)生命周期服务在每次请求被创建一次。

单例
单例(Singleton)生命周期服务在它们第一次被请求时创建(或者如果你在 ConfigureServices
运行时指定一个实例)并且每个后续请求将使用相同的实例。如果你的应用程序需要单例行为,建议让服务容器管理服务的生命周期而不是在自己的类中实现单例模式和管理对象的生命周期。

服务可以用多种方式在容器中注册。我们已经看到了如何通过指定具体类型用来注册一个给定类型的服务实现。除此之外,可以指定一个工厂,它将被用来创建需要的实例。第三种方式是直接指定要使用的类型的实例,在这种情况下容器将永远不会尝试创建一个实例。

为了说明这些生命周期和注册选项之间的差异,考虑一个简单的接口将一个或多个任务表示为有一个唯一标识符 OperationId 的 操作 。依据我们如何配置这个服务的生命周期,容器将为请求的类提供相同或不同的服务实例。要弄清楚哪一个生命周期被请求,我们将创建每一个生命周期选项的类型:

using System;

namespace DependencyInjectionSample.Interfaces
{
    public interface IOperation
    {
        Guid OperationId { get; }
    }

    public interface IOperationTransient : IOperation
    {
    }
    public interface IOperationScoped : IOperation
    {
    }
    public interface IOperationSingleton : IOperation
    {
    }
    public interface IOperationSingletonInstance : IOperation
    {
    }
}

我们使用 Operation 类实现这些接口。它的构造函数接收一个 Guid,若未提供则生成一个新的 Guid

接下来,在 ConfigureServices 中,每一个类型根据它们命名的生命周期被添加到容器中:

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

请注意, IOperationSingletonInstance 服务使用一个具有已知 Guid.Empty ID 的具体实例,所以该类型在使用时是明确的。我们还注册了一个依赖于其他每个 Operation 类型的 OperationService,因此在一个请求中对于每个操作类型,该服务获取相同的实例或创建一个新的实例作为控制器将是明确的。所有服务通过属性暴露依赖关系,因此它们可以显示在视图中。

using DependencyInjectionSample.Interfaces;

namespace DependencyInjectionSample.Services
{
    public class OperationService
    {
        public IOperationTransient TransientOperation { get; }
        public IOperationScoped ScopedOperation { get; }
        public IOperationSingleton SingletonOperation { get; }
        public IOperationSingletonInstance SingletonInstanceOperation { get; }

        public OperationService(IOperationTransient transientOperation,
            IOperationScoped scopedOperation,
            IOperationSingleton singletonOperation,
            IOperationSingletonInstance instanceOperation)
        {
            TransientOperation = transientOperation;
            ScopedOperation = scopedOperation;
            SingletonOperation = singletonOperation;
            SingletonInstanceOperation = instanceOperation;
        }
    }
}

为了证明对象的生命周期在应用程序的每个单独的请求内,还是请求之间,此示例包含OperationsController
请求每一个 IOperation 类型和 OperationService。 Index action 接下来显示所有控制器和服务的 OperationId 值。

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
using Microsoft.AspNetCore.Mvc;

namespace DependencyInjectionSample.Controllers
{
    public class OperationsController : Controller
    {
        private readonly OperationService _operationService;
        private readonly IOperationTransient _transientOperation;
        private readonly IOperationScoped _scopedOperation;
        private readonly IOperationSingleton _singletonOperation;
        private readonly IOperationSingletonInstance _singletonInstanceOperation;

        public OperationsController(OperationService operationService,
            IOperationTransient transientOperation,
            IOperationScoped scopedOperation,
            IOperationSingleton singletonOperation,
            IOperationSingletonInstance singletonInstanceOperation)
        {
            _operationService = operationService;
            _transientOperation = transientOperation;
            _scopedOperation = scopedOperation;
            _singletonOperation = singletonOperation;
            _singletonInstanceOperation = singletonInstanceOperation;
        }

        public IActionResult Index()
        {
            // viewbag contains controller-requested services
            ViewBag.Transient = _transientOperation;
            ViewBag.Scoped = _scopedOperation;
            ViewBag.Singleton = _singletonOperation;
            ViewBag.SingletonInstance = _singletonInstanceOperation;
            
            // operation service has its own requested services
            ViewBag.Service = _operationService;
            return View();
        }
    }
}

现在两个独立的请求到这个 controller action:
lifetimes_request1.png
lifetimes_request2.png

观察 OperationId 值在请求和请求之间的变化。

  • 瞬时(Transient) 对象总是不同的;向每一个控制器和每一个服务提供了一个新的实例

  • 作用域(Scoped) 对象在一次请求中是相同的,但在不同请求中是不同的

  • 单例(Singleton) 对象对每个对象和每个请求是相同的(无论是否在 ConfigureServices 中提供实例)

请求服务

来自 HttpContext 的一次 ASP.NET 请求中可用的服务通过 RequestServices 集合公开的。
request-services.png

请求服务将你配置的服务和请求描述为应用程序的一部分。当你的对象指定依赖关系,这些满足要求的对象通过查找 RequestServices 中对应的类型得到,而不是 ApplicationServices

通常,你不应该直接使用这些属性,而更倾向于通过类的构造函数请求需要的类的类型,并且让框架来注入依赖关系。这将会生成更易于测试的 (查看 Testing) 和更松散耦合的类。

注解
更倾向于请求依赖关系作为构造函数的参数来访问 RequestServices 集合。

设计你的依赖注入服务

你应该设计你的依赖注入服务来获取它们的合作者。这意味着在你的服务中避免使用有状态的静态方法调用(代码被称为 static cling)和直接实例化依赖的类型。当选择实例化一个类型还是通过依赖注入请求它时,它可以帮助记住这句话, New is Glue。通过遵循 面向对象设计的 SOLID 原则,你的类将倾向于小、易于分解及易于测试。

如果你发现你的类往往会有太多的依赖关系被注入时该怎么办?这通常表明你的类试图做太多,并且可能违反了单一职责原则(SRP) - 单一职责原则。看看你是否可以通过转移一些职责到一个新的类来重构类。请记住,你的 Controller 类应该重点关注用户界面(User Interface,UI),因此业务规则和数据访问实现细节应该保存在这些适合单独关注的类中。

关于数据访问,如果你已经在 Startup 类中配置了 EF,那么你能够方便的注入 Entity Framework 的 DbContext 类型到你的控制器中。然而,最好不要在你的 UI 项目直接依赖 DbContext。相反,依赖于一个抽象(比如一个仓储接口),并且限定使用 EF (或其他任何数据访问技术)来实现这个接口。这将减少应用程序和特定的数据访问策略之间的耦合,并且使你的应用程序代码更容易测试。

替换默认的服务容器

内置的服务容器的意图在于提供框架的基本需求并且大多数客户应用程序建立在它之上。然而,开发人员可以很容易地使用他们的首选容器替换默认容器。ConfigureServices 方法通常返回 void,但是如果改变它的签名返回 IServiceProvider,可以配置并返回一个不同的容器。有很多 IOC 容器可用于 .NET。在这个例子中, Autofac 包被使用。

首先,在 project.json 的 dependencies 属性中添加适当的容器包:

"dependencies" : {
  "Autofac": "4.0.0-rc2-237",
  "Autofac.Extensions.DependencyInjection": "4.0.0-rc2-200"
},

接着,在 ConfigureServices 中配置容器并返回 IServiceProvider

public IServiceProvider ConfigureServices(IServiceCollection services)
{
  services.AddMvc();
  // add other framework services

  // Add Autofac
  var containerBuilder = new ContainerBuilder();
  containerBuilder.RegisterModule<DefaultModule>();
  containerBuilder.Populate(services);
  var container = containerBuilder.Build();
  return container.Resolve<IServiceProvider>();
}

注解
当使用第三方 DI 容器时,你必须更改 ConfigureServices 让它返回 IServiceProvider 而不是 void

最后,在 DefaultModule 中配置 Autofac:

public class DefaultModule : Module
{
  protected override void Load(ContainerBuilder builder)
  {
    builder.RegisterType<CharacterRepository>().As<ICharacterRepository>();
  }
}

在运行时,Autofac 将被用来解析类型和注入依赖关系。 了解更多有关使用 Autofac 和 ASP.NET Core

建议

当使用依赖注入时,请记住以下建议:

  • DI 针对具有复杂依赖关系的对象。控制器,服务,适配器和仓储都是可能被添加到 DI 的对象的例子。
    避免直接在 DI 中存储数据和配置。例如,用户的购物车通常不应该被添加到服务容器中。配置应该使用 Options Model。 同样, 避免 “数据持有者” 对象只是为了允许访问其他对象而存在。如果可能的话,最好是通过 DI 获取实际的项。

  • 避免静态访问服务。

  • 避免在应用程序代码中服务定位。

  • 避免静态访问 HttpContext

注解
像所有的建议,你可能遇到必须忽视其中一个的情况。我们发现了少见的例外 – 非常特别的情况是框架本身。

记住,依赖注入是静态/全局对象访问模式的 另一选择。如果你把它和静态对象访问混合的话,你将无法了解 DI 的有用之处。