ASP.NET Core 依赖注入(DI)简介

时间:2024-02-17 18:24:24

本文为官方文档译文

ASP.NET Core是从根本上设计来支持和利用依赖注入。 ASP.NET Core应用程序可以通过将其注入到Startup类中的方法中来利用内置的框架服务,并且应用程序服务也可以配置为注入。 ASP.NET Core提供的默认服务容器提供了一个最小的功能集,而不是替换其他容器。

什么是依赖注入?

依赖注入,英文是Dependency Injection一般简称DI,是实现对象与其协作者或依赖关系之间松散耦合的技术。为了执行其操作,类所需的对象不是直接实例化协作者或使用静态引用,而是以某种方式提供给类。 大多数情况下,类将通过它们的构造函数来声明它们的依赖关系,允许它们遵循显式依赖原则。 这种方法被称为“构造方法注入”。

在设计时考虑到DI,它们更加松散耦合,因为他们没有直接的,硬编码的依赖于他们的合作者。 这遵循依赖性反转原则,其中指出“高级模块不应该依赖于低级模块;两者都应该取决于抽象”。 除了引用特定的实现之外,类请求构造类时提供给它们的抽象(通常是接口)。 将依赖关系提取到接口中并将这些接口的实现提供为参数也是策略设计模式的一个示例。

当系统被设计为使用DI时,有许多类通过它们的构造方法(或属性)请求它们的依赖关系,有一个专门用于创建这些类及其关联的依赖关系的类是有帮助的。 这些类被称为容器,或更具体地称为控制反转(IoC)容器或依赖注入(DI)容器。 容器本质上是一个工厂,负责提供从它请求的类型的实例。 如果给定类型已声明它具有依赖关系,并且容器已配置为提供依赖关系类型,那么它将创建依赖关系作为创建请求的实例的一部分。 以这种方式,可以将复杂的依赖关系图提供给类,而不需要任何硬编码的对象构造。 除了创建具有依赖关系的对象之外,容器通常会在应用程序中管理对象生命周期。

ASP.NET Core包括一个简单的内置容器(由IServiceProvider接口表示),默认情况下支持构造函数注入,ASP.NET通过DI可以提供某些服务。 ASP.NET的容器是指它作为服务管理的类型。 在本文的其余部分中,服务将引用由ASP.NET Core的IoC容器管理的类型。 您可以在应用程序的Startup类中的ConfigureServices方法中配置内置容器的服务。

本文介绍依赖注入,因为它适用于所有ASP.NET应用程序。 依赖注入和控制器涵盖MVC控制器内的依赖注入。

推荐Martin Fowler的文章:Inversion of Control Containers and the Dependency Injection Pattern

构造器注入

构造器注入要求所讨论的构造方法是公开的。 否则,你的应用程序会抛出InvalidOperationException

不能找到类型“xxx”的合适的构造函数。 确保类型是具体的,服务是为公共构造函数的所有参数注册的。

构造器注入需要只存在一个适用的构造函数。 支持构造函数重载,但只有一个重载可以存在,其参数都可以通过依赖注入来实现。 如果有多个存在,您的应用程序将抛出一个InvalidOperationException

接受所有给定参数类型的多个构造函数已在类型\'xxxx\'中找到。 应该只有一个适用的构造函数。

构造方法可以接受非依赖注入提供的参数,但这些参数必须支持默认值。 例如:

// throws InvalidOperationException: Unable to resolve service for type \'System.String\'...
public CharactersController(ICharacterRepository characterRepository, string title)
{
    _characterRepository = characterRepository;
    _title = title;
}

// runs without error
public CharactersController(ICharacterRepository characterRepository, string title = "Characters")
{
    _characterRepository = characterRepository;
    _title = title;
}

使用框架提供的服务

Startup类中的ConfigureServices方法负责定义应用程序将使用的服务,包括平台功能,如Entity Framework Core和ASP.NET Core MVC。 最初,提供给ConfigureServices的IServiceCollection具有定义的以下服务(取决于如何配置Host):

服务类型 生命周期
Microsoft.AspNetCore.Hosting.IHostingEnvironment Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.Logging.ILogger Singleton
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory Transient
Microsoft.AspNetCore.Http.IHttpContextFactory Transient
Microsoft.Extensions.Options.IOptions Singleton
System.Diagnostics.DiagnosticSource Singleton
System.Diagnostics.DiagnosticListener Singleton
Microsoft.AspNetCore.Hosting.IStartupFilter Transient
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton
Microsoft.Extensions.Options.IConfigureOptions Transient
Microsoft.AspNetCore.Hosting.Server.IServer Singleton
Microsoft.AspNetCore.Hosting.IStartup Singleton
Microsoft.AspNetCore.Hosting.IApplicationLifetime Singleton

以下是使用多个扩展方法(如AddDbContextAddIdentityAddMvc)向容器添加附加服务的示例。

// 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)遵循使用单个AddServiceName扩展方法注册该功能所需的所有服务的约定。

您可以通过参数列表在Startup方法中请求某些框架提供的服务 .

注册自己的服务

您可以注册自己的应用程序服务,如下所示。 第一个通用类型表示将从容器请求的类型(通常为接口)。 第二个通用类型表示将由容器实例化并用于实现这种请求的具体类型。

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

每个services.Add<ServiceName>扩展方法添加(并可能配置)服务。 例如,services.AddMvc()添加了MVC需要的服务。 建议您遵循此约定,将扩展方法放在Microsoft.Extensions.DependencyInjection命名空间中,以封装服务注册组。

AddTransient方法用于将抽象类型映射到为需要的每个对象单独实例化的具体服务。 这被称为服务的生命周期,其余的生命周期选项如下所述。 为您注册的每个服务选择适当的生命周期很重要。 应该向请求它的每个类提供一个新的服务实例? 在一个给定的Web请求中应该使用一个实例吗? 还是应该在应用程序的一生中使用单个实例?

在本文的示例中,有一个简单的控制器显示字符名称,名为CharactersController。 其Index方法显示当前存储在应用程序中的字符列表,如果不存在,则使用少数字符初始化集合。 请注意,虽然此应用程序使用Entity Framework CoreApplicationDbContext类作为其持久化,但在控制器中并不明显。 相反,具体的数据访问机制已经在遵循仓储模式的接口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。 依赖注入以这种链式方式使用是不寻常的,每个请求的依赖依次请求自己的依赖关系。 容器负责解析图中的所有依赖关系,并返回完全解析的服务。

创建请求的对象及其所需的所有对象以及所需的所有对象有时被称为对象图。 同样,必须解决的集合的依赖关系通常被称为依赖关系树或依赖图。

在这种情况下,ICharacterRepositoryApplicationDbContext都必须在启动中的ConfigureServices中的services容器中注册。 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

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.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>();

请注意,IOperationSingletonInstance服务正在使用具有Guid.Empty的已知ID的特定实例,因此在使用此类型时要清楚(其Guid将全为零)。 我们还注册了一个取决于每个其他操作类型的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\'显示所有控制器和服务的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();
        }
    }
}

下图分别为两次请求:

观察在请求中以及请求之间的哪个OperationId值有所不同。

  • Transient 对象总是不同的; 每个控制器和每个服务都提供了一个新的实例。

  • Scoped 对象在请求中是相同的,但在不同的请求中是不同的。

  • Singleton 对象对于每个对象和每个请求都是一样的(不管ConfigureServices中是否提供一个实例)

请求服务

来自HttpContext的ASP.NET请求中提供的服务通过RequestServices集合公开。

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

通常,您不应直接使用这些属性,而是倾向于通过类的构造构造方法请求类所需的类,并让框架注入这些依赖关系。 这产生了更容易测试的类(参见测试)并且更松散地耦合。

优先要求依赖关系作为访问RequestServices集合的构造方法参数。

自定义依赖注入服务

你应该设计你的服务以使用依赖注入来获取他们的协作者。 这意味着避免使用状态静态方法调用(这导致一个称为静态绑定的代码)以及服务中依赖类的直接实例化。 当选择是否实例化一个类型或通过依赖注入来请求它时,这可能有助于记住“New is Glue”这个短语。 通过遵循面向对象设计的SOLID原则,您的类自然会倾向于小型,考虑因素,易于测试。

如果你发现你的类倾向于有太多的依赖关系被注入呢? 这通常是您的类尝试做的太多的工作,可能违反SRP - 单一职责原则。 看看你是否可以通过将一些责任转移到一个类中来重构类。 请记住,您的Controller类应该专注于UI问题,因此业务规则和数据访问实现细节应该保存在适合这些单独问题的类中。

关于数据访问,您可以将DbContext注入到控制器中(假设您已将EF添加到ConfigureServices中的服务容器)。 一些开发人员更喜欢使用数据库的仓储接口,而不是直接注入DbContext。 使用接口将数据访问逻辑封装在一个位置可以最小化数据库更改时您将需要更改的位置。

释放服务

容器将为其创建的IDisposable类型调用Dispose。 但是,如果您将自己的实例添加到容器中,则不会被处理。

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

public void ConfigureServices(IServiceCollection services)
{
    // container will create the instance(s) of these types and will dispose them
    services.AddScoped<Service1>();
    services.AddSingleton<Service2>();

    // container did not create instance so it will NOT dispose it
    services.AddSingleton<Service3>(new Service3());
    services.AddSingleton(new Service3());
}

替换默认服务容器

内置的服务容器旨在满足框架和在其上生成的大多数使用者应用程序的基本需求。 但是,开发人员可以用其首选容器替换内置容器。 ConfigureServices方法通常返回void,但如果其签名更改为返回IServiceProvider,则可以配置和返回不同的容器。 有许多IOC容器可用于.NET。 在本示例中,使用Autofac程序包。

首先,安装相应的程序包:

  • Autofac
  • Autofac.Extensions.DependencyInjection

接下来,在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 new AutofacServiceProvider(container);
}

使用第三方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的更多信息

Thread safety

单例服务需要线程安全。 如果单例服务依赖于临时服务,则暂时性服务也可能需要线程安全,取决于单例使用的方式。

建议

在使用依赖注入时,请注意以下建议:

-DI用于具有复杂依赖关系的对象。 控制器,服务,适配器和仓储都是可能添加到DI的对象的示例。

  • 避免将数据和配置直接存储在DI中。 例如,用户的购物车通常不应该添加到服务容器中。 配置应使用选项模型。 同样,避免只存在的“数据持有者”对象,以允许访问其他对象。 如果可能,请通过DI请求实际的物品。

  • 避免静态访问服务。

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

  • 避免静态访问HttpContext

像所有的建议一样,你可能会遇到忽略一个需求的情况。 我们发现例外是罕见的 - 在框架本身中大多是非常特殊的情况。

记住,依赖注入是静态/全局对象访问模式的替代。 如果将其与静态对象访问混合,您将无法实现DI的优点。