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

什么是依赖注入?

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

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

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

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

使用框架提供的服务

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

// 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需要的服务。

AddTransient方法用于将抽象类型映射到为每一个需要它的对象单独实例化的具体服务。这就是称为服务的生命周期,并额外的生命周期选项在下面描述。为你注册的每一个服务选择合适的生命周期是很重要的。

是否应该向请求它的每个类提供一个新的服务实例?或者在一个给定的Web请求中使用一个实例?或者应该在应用程序生命周期中使用单个实例?

下面的代码中,具体的数据访问机制被抽象在遵循仓储模式(repository pattern)IUserRepository接口后面。IUserRepository的实例是通过构造函数请求并分配给一个私有字段,然后用来访问所需的用户。

public class UserController : Controller
{
    private readonly IUserRepository _userRepository;
    private readonly ILogger<UserController> _logger;

    public UserController(IUserRepository userRepository, ILogger<UserController> logger)
    {
        _userRepository = userRepository;
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<UserItem> GetAll()
    {
        _logger.LogInformation("LoggingEvents.LIST_ITEMS","Listing all items");
        return _userRepository.GetAll();
    }

IUserRepository定义了控制器需要使用User实例的几个方法。

public interface IUserRepository
{
    void Add(UserItem item);
    IEnumerable<UserItem> GetAll();
    UserItem Find(string key);
    UserItem Remove(string key);
    void Update(UserItem item);
}

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

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

public class UserRepository : IUserRepository
{
    private readonly UserContext _context;      //<== public class UserContext : DbContext

    public _UserRepository(UserContext context) //<==
    {
        _context = context;                     //<==
    }

    public async Task<IEnumerable<User>> GetAll()
    {
        return await _context.Users.ToListAsync();
    }

    public async Task<User> Get(int id)
    {
        return await _context.Users.SingleOrDefaultAsync(u => u.ID == id);
    }

    public async void Add(User user)
    {
        _context.Add(user);
        await _context.SaveChangesAsync();
    }

    public async void Update(User user)
    {
        _context.Update(user);
        await _context.SaveChangesAsync();
    }

    public async void Delete(int id)
    {
        var user =   _context.Users.SingleOrDefault(u => u.ID == id);
        _context.Users.Remove(user);
        await _context.SaveChangesAsync();
    }

    public bool UserExists(int id)
    {
        return _context.Users.Any(e => e.ID == id);
    }
}

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

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

UserRepositoryDbContext都必须在Statup类中的ConfigureServices方法的服务容器中注册。DbContext通过对扩展方法AddDbContext<T>的调用进行配置。以下代码显示了UserRepository类型的注册。

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    string connectionString = Configuration.GetConnectionString("MyConnection");

    services.AddDbContext<UserContext>(options =>
                options.UseMySQL(connectionString));

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

    // Register application services.
    // Add our repository type
    services.AddScoped<IUserRepository, UserRepository>();
}

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

服务生命周期和注册选项

ASP.NET服务可以配置成如下的生命周期:

  • Transient(瞬态)

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

  • Scoped(作用域)

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.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
services.AddTransient<OperationService, OperationService>();

注意IOperationSingletonInstance服务使用了一个具有已经Guid.EmptyID的指定实例,所以该类型在使用时是明确的。我们还注册了一个依赖于其他每个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类型以及OperationServiceIndex动作方法接着会显示所有的控制器和服务的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(请求服务)将你配置和请求的服务描述为应用程序的一部分。当你的对象指定依赖关系,这些满足要求的对象通过查找RequestServices中对应的类型得到,而不是ApplicationServices

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

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

替换默认的服务容器

内置的服务容器旨在提供框架的基本需求,并且大部分的客户应用程序构造在其之上。然而,开发人员可以用他们偏爱的容器替换内置的容器。ConfigureServices方法通常会返回一个void,但如果将它的签名改成返回IServiceProvider,就可以配置并返回一个不同的容器。有很多IoC容器可用于.NET。这里我们使用Autofac

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

"dependencies" : {
    "Autofac": "4.0.0",
    "Autofac.Extensions.DependencyInjection": "4.0.0"
},

接着,在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的原有返回类型void,让它返回一个IServiceProvider

最后,在DefaultModule中正常配置Autofac:

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

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

建议

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

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

记住,依赖注入是静态/全局对象访问模式的替代。如果你混合使用静态对象访问,你将无法实现DI的好处。

原文链接

Dependency Injection

个人博客

我的个人博客