zoukankan      html  css  js  c++  java
  • ASP.NET Core 1.0基础之依赖注入

    来源https://docs.asp.net/en/latest/fundamentals/dependency-injection.html
    ASP.NET Core 1.0在设计上原生就支持和有效利用依赖注入。在Startup类中,应用可以通过将框架内嵌服务注入到方法中来使用他们;另一方面,你也可以配置服务来注入使用。默认的服务容器只提供了最小的特性集合,所以并不打算取代其他的IoC容器。

    什么是依赖注入DI##

    依赖注入是为了达到解耦对象和其依赖的一项技术。一个类为了完成自身某些操作所需的对象是通过某种方式提供的,而不是使用静态引用或者直接实例化。通常情况下,类通过构造器来声明其依赖,遵循显式依赖原则。这种方式称作构造器注入。

    当以DI思想来设计类时,这些类更加松耦合,因为他们不直接硬编码的依赖其合作者。这遵循了依赖倒置原则,即高层模块不应依赖底层模块,两者都应依赖抽象。类在构建时所需是抽象(如接口interface),而不是具体的实现。把依赖抽离成接口,把这些接口的实现作为参数也是策略设计模式的例子。

    当一个系统使用DI来设计时,很多类通过构造器或者属性来添加依赖,这样就很方便有一个专门的类来创建这些类以及他们相关的依赖。这样的类称之为“容器”或者“IoC容器”或“DI容器”。一个容器本质上是一个工厂,来给请求者提供类型实例。如果给定类型声明了自身依赖,容器也配置了来提供这些依赖类型,那么它会创建这些依赖作为请求实例的一部分。通过这种方式可以为 类提供复杂的依赖图,而不需要任何硬编码的对象依赖。除了创建依赖对象外,容器一般还管理应用内的对象生命周期。

    ASP.NET Core 1.0提供了一个简单的内置容器(以IServiceProvider为代表),默认支持构造器注入,这样ASP.NET可以通过DI使某些服务可用。ASP.NET把它所管理的类型称之为服务。本文的剩下部分,服务即指ASP.NET IoC容器所管理的类型。你可以在Startup类中的ConfigureServices 方法中配置内置的容器服务。
    Note: Martin Fowler写过一篇很详细的依赖反转的文章。微软对此也有很棒的描述连接

    使用框架提供的服务##

    ConfigureServices方法负责定义应用使用的服务,包括平台特性服务如EF和ASP.NET MVC。最初提供给ConfigureServices的IServiceCollection只有少数服务。默认web模板提供了怎么通过扩展方法来添加额外服务到容器的例子,如AddEntityFramework, AddIdentity, 和AddMVC。

    public void ConfigureServices(IServiceCollection services)
    {
        // Add framework services.
        services.AddEntityFramework()
            .AddSqlServer()
            .AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));
    
        services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();
    
        services.AddMvc();
    
        // Add application services.
        services.AddTransient<IEmailSender, AuthMessageSender>();
        services.AddTransient<ISmsSender, AuthMessageSender>();
    }
    

    ASP.NET提供的特性和中间件遵循使用一个AddService扩展方法的约定,来注册该特性使用的所需的所有服务。

    Note:你可以在Startup方法中请求某些framework-provided服务,详见应用启动Application Startup
    当然,除了配置框架提供的各种服务,你也可以配置自己定义的服务。

    注册自定义服务##

    在默认web模板中,有如下两个服务被添加到IServiceCollection中

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

    AddTransient方法将抽象类型映射为实体服务,对于每个请求这都单独实例化,这称作服务的生命周期。额外生命周期选项如下。对于每个注册的服务选择合适的生命周期是很重要的。是对每个请求类都提供一个新的实例化服务?还是在给定web请求内只实例化一次?还是在应用周期内只有单例?

    在本文的例子中,有个简单的CharacterController来显示Character姓名,在Index方法中显示已存储的Character(如果没有则创建)。虽然注册了EF服务,但本例持久化没有使用数据库。具体的数据获取服务抽象到了ICharacterRepository接口实现中,这遵从了仓储模式。在构造器中请求ICharacterRepository参数,并将其赋给私有变量,来根据需要获取Character。

    using System.Linq;
    using DependencyInjectionSample.Interfaces;
    using DependencyInjectionSample.Models;
    using Microsoft.AspNet.Mvc;
    
    namespace DependencyInjectionSample.Controllers
    {
        public class CharactersController : Controller
        {
            private readonly ICharacterRepository _characterRepository;
    
            public CharactersController(ICharacterRepository characterRepository)
            {
                _characterRepository = characterRepository;
            }
    
            // GET: /characters/
            public IActionResult Index()
            {
                var characters = _characterRepository.ListAll();
                if (!characters.Any())
                {
                    _characterRepository.Add(new Character("Darth Maul"));
                    _characterRepository.Add(new Character("Darth Vader"));
                    _characterRepository.Add(new Character("Yoda"));
                    _characterRepository.Add(new Character("Mace Windu"));
                    characters = _characterRepository.ListAll();
                }
    
                return View(characters);
            }
        }
    

    接口ICharacterRepository只简单定义了两个方法,Controller通过其来操作Charcter实例。

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

    接口有具体类型CharacterRepository来实现,在运行时被使用。
    Note: CharacterRepository类只是使用DI的普通例子,你可以对应用所有的服务使用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实例。这种链式的依赖注入是很常见的,被依赖本身又有自己的依赖。容器来负责以树形的方式来解析所有这些依赖,并返回解析完成的服务。

    Note: 创建请求对象,以及其依赖,其依赖的依赖,有时这被称之为对象图。同样的,需要解析的对象集合称之为依赖树或者依赖图

    在本例中,ICharacterRepository和ApplicationDbContext都必须在ConfigureServices中注册。ApplicationDbContext是通过扩展方法AddEntityFramework来配置,它包括添加DbContext (AddDbContext)的一个扩展。仓储的注入在在ConfigureServices方法的结尾。

    services.AddTransient<ISmsSender, AuthMessageSender>();
    services.AddScoped<ICharacterRepository, CharacterRepository>();
    
    // Show different lifetime options
    services.AddTransient<IOperationTransient, Operation>();
    

    EF contexts需要使用scoped生命周期来添加到服务容器。如果你使用了上面的helper方法,这是已经处理好的。使用EF的仓储服务应该使用同样的生命周期。

    警告:主要不安全的来源是通过单例来解析Scoped生命周期服务服务。这样做的后果,很有可能在处理后续请求时使用的服务的状态是错误的。

    服务生命周期和注册选项##

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

    • Transient: Transient服务在每次被请求时都会被创建。这种生命周期比较适用于轻量级的无状态服务。
    • Scoped: Scoped生命周期的服务是每次web请求被创建。
    • Singleton: Singleton生命能够周期服务在第一被请求时创建,在后续的每个请求都会使用同一个实例。如果你的应用需要单例服务,推荐的做法是交给服务容器来负责单例的创建和生命周期管理,而不是自己来走这些事情。
    • Instance: 你也可以选择直接添加实例到服务容器。如果这样做,该实例会被后续的所有请求所使用(这样就会创建一个scoped-Singleton实例)。Instance和Singleton的一个主要区别在于,Instance服务是由ConfigureServices创建,然而Singleton服务是lazy-loaded,在第一个被请求时才会被创建。

    服务可以通过若干种方式注册到容器。我们已经看到,对于给定类型通过指定具体类型来注册服务的实现。除此之外,也可以指定一个工厂,用来按需创建实例。第三种方法是直接指定要使用的类型实例,在这种方式下,容器自身不会尝试去创建实例。

    为了演示这四种不同的生命周期和注册选项,考虑一个简单接口,代表这一个或多个任务操作,并且含有一个唯一标识符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 IOperationInstance : IOperation
        {
        }
    }
    

    我们通过一个类来实现这些接口,接受一个Guid作为构造器参数,或者使用new Guid来提供(如果没有提供的话)。
    接下来在ConfigureServices中,根据类型的生命周期来添加到容器中

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

    注意到对于Instance生命周期的实例,我们是自己提供了已知的Guid.Empty标识符,这样我们能在该实例被使用时识别它。我们也注册了一个OperationService,它依赖其他Operation类型。这样我们就能弄清楚在一个请求内,对于每个类型我们是得到同样的实例还是一个新的实例。

    using DependencyInjectionSample.Interfaces;
    
    namespace DependencyInjectionSample.Services
    {
        public class OperationService
        {
            public IOperationTransient TransientOperation { get; private set; }
            public IOperationScoped ScopedOperation { get; private set; }
            public IOperationSingleton SingletonOperation { get; private set; }
            public IOperationInstance InstanceOperation { get; private set; }
    
            public OperationService(IOperationTransient transientOperation,
                IOperationScoped scopedOperation,
                IOperationSingleton singletonOperation,
                IOperationInstance instanceOperation)
            {
                TransientOperation = transientOperation;
                ScopedOperation = scopedOperation;
                SingletonOperation = singletonOperation;
                InstanceOperation = instanceOperation;
            }
        }
    }
    

    为了演示对应用的单个请求内和不同请求内的对象生命周期,样例包含一个OperationController依赖每种类型的Operation以及OperationService。Index方法显示所有的服务Id。

    using DependencyInjectionSample.Interfaces;
    using DependencyInjectionSample.Services;
    using Microsoft.AspNet.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 IOperationInstance _instanceOperation;
    
            public OperationsController(OperationService operationService,
                IOperationTransient transientOperation,
                IOperationScoped scopedOperation,
                IOperationSingleton singletonOperation,
                IOperationInstance instanceOperation)
            {
                _operationService = operationService;
                _transientOperation = transientOperation;
                _scopedOperation = scopedOperation;
                _singletonOperation = singletonOperation;
                _instanceOperation = instanceOperation;
            }
    
            public IActionResult Index()
            {
                ViewBag.Transient = _transientOperation;
                ViewBag.Scoped = _scopedOperation;
                ViewBag.Singleton = _singletonOperation;
                ViewBag.Instance = _instanceOperation;
                ViewBag.Service = _operationService;
                return View();
            }
        }
    }
    

    然后有两个请求到达controller action。
    index1
    index2

    观察请求内和请求间的OperationId哪个变化。

    • Transient 服务的对象总是不同的。每个controller和service都提供一个新的实例
    • Scoped的对象在一个request内是一样的,而不同的request间是不一样的。
    • Singleton对象是一直保持不表的。
    • Instance对象,对于每一个对象和request都是一样的,也即是在ConfigureServices中所指定的对象。

    请求服务和应用服务##

    HttpContext中的一个ASP.NET请求中的可用服务分为两个集合,ApplicationServicesRequestServices
    service1

    Request services作为应用的一部分是你可以配置和request的。而Application Services则是局限于在在应用启动(Startup)时可用的服务。Scoped的服务是作为Request Services的一部分而不是Applocation Services的一部分。当对象指定依赖时,是由RequestServices中的类型所提供,而不是ApplicationServices。

    一般来将你不应该直接使用这些属性,而是倾向于通过类的构造器来请求这些类型,让框架来注入这些依赖。这样产生的类更容易测试和更松耦合。

    Note: 需要重点记住的是,应用几乎总是会使用RequestServices,任何情况下你都不应该直接使用这些属性。而是通过构造器来请求所需服务。

    自定义依赖注入服务##

    你可以设计自己的服务并通过依赖注入到需求方。这样可以避免使用有状态的静态方法调用(会导致code smell,即static cling)和服务内对依赖类的直接实例化。当选择是否通过New来实例化一个类型或者通过依赖注入来氢气,记住“New is Glue”也许是点帮助的。通过遵循Solid面向对象设计原则,设计的类自然就会small, well-factored,和easily tested。

    如果你发现类有了太多需要注入的依赖怎么办?一般来说,这是类承担了太多职责的标志,很有可能违反了SRP(单一职责原则)。检查是否能把其中的某些职责转到新的类。记住,Controller类应该只关注UI,所以业务规则和数据获取实现应该放在合适的关注点分离的类中。

    至于数据获取,你可以注入EF DbContext类型到Controller中(假设你已经在Startup类中配置了EF)。然而,一般避免在UI项目中直接依赖DbContext类型,而是依赖抽象,如Repository接口,在接口的实现中限制EF的相关知识。这样会减少项目和数据访问策略的耦合,使得测试代码更加容易。

    取代默认服务容器##

    内建的服务容器只满足框架最基本的需求,大部分的consumer 应用基于此构建。然而,如果开发者希望取代内建的容器,使用自己偏好的容器,也是可以很容易做到的。ConfigureServices方法一般返回void,但是如果返回类型签名改为IServiceProvider,就可以配置和返回别的容器。有很多IoC .NET容器。当它们可用时,本文会添加这些容器的DNX实现。在本例中,使用Autofac包。

    首先在project.json中添加合适的容器包。

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

    然后在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>();
    }
    

    Note: 当使用第三方DI容器时,需要改变ConfigreServices的返回签名,改成IServiceProvider而不是void。

    最后在DefaultModule中配置Autofac。

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

    然后在运行时,Autofac将会解析类型和注入的依赖

    Package(Nuget) ProjectSite
    Autofac.Dnx http://autofac.org
    StructureMap.Dnx http://structuremap.github.io
    ##推荐规范## 当使用依赖注入时,记住如下推荐规范:
    • DI是针对于有复杂依赖的对象。Controllers, services, adapters和 repositories都是一些可以添加依赖的对象的例子。
    • 避免直接在DI中存储数据和配置。例如,用户购物车不应添加到服务容器中。配置应该使用Options Model中文链接。类似的,避免“data holder” objects that only exist to allow access to some other object。如果可能尽量通过DI来获取实际的item。
    • 避免静态获取服务
    • 在应用代码中避免service location
    • 避免静态获取HttpContext

    Note: 如上所有推荐规范,你可能遇到必须忽略某条的情形。但是这种情形很少,而且基本都是框架本身内部的情形。
    记住,依赖注入是static/global对象获取模式的一个替代方式。如果你把DI和静态对象接入混用,你可能不能体会到DI的优势。

    Download sample from GitHub

  • 相关阅读:
    LeetCode 1122. Relative Sort Array (数组的相对排序)
    LeetCode 46. Permutations (全排列)
    LeetCode 47. Permutations II (全排列 II)
    LeetCode 77. Combinations (组合)
    LeetCode 1005. Maximize Sum Of Array After K Negations (K 次取反后最大化的数组和)
    LeetCode 922. Sort Array By Parity II (按奇偶排序数组 II)
    LeetCode 1219. Path with Maximum Gold (黄金矿工)
    LeetCode 1029. Two City Scheduling (两地调度)
    LeetCode 392. Is Subsequence (判断子序列)
    写程序判断系统是大端序还是小端序
  • 原文地址:https://www.cnblogs.com/yanbinliu/p/5170737.html
Copyright © 2011-2022 走看看