浅谈ASP.NET Core中的DI

DI的一些事

传送门马丁大叔的文章

什么是依赖注入(DI: Dependency Injection)?

依赖注入(DI)是一种面向对象的软件设计模式,主要是帮助开发人员开发出松耦合的应用程序。同时呢,让应用更容易进行单元测试和维护。
DI其实就是用一个注入器类为一个对象提供其依赖的一个过程!如何更好的理解呢?下面就举个列子解释下!
比如 class Client,它要使用服务class Service的提供的功能,这个时候就说Service是Client的依赖,程序实现如下:

 var s = new Service();
 var c = new Client(s);

很明显我们还要承担创建Service的对象的职责,程序出现了强耦合问题,后面如果需求变化,我们要替换掉Service,那我们就要修改这边的代码,这样的程序很面明,扩展性,灵活性比较差了!

引入DI之后呢,我们应该还有一个注入器类,假设是 class Injector 。为了更好的解释DI的好处,上面的代码我们重新设定为 class Client 依赖 接口IService , class Service 实现了IService ,这个时候我们的程序主流程不需要关注如何创建的Service,可以把这部分的职责委托给Injector,我们只要告诉Injector,我需要IService,请提供给我,程序实现如下:

var s = Injector.Get(typeof(IService));
var c = new Client(s);

这样的好处就很明显了,我们只关注自己的核心业务职责,对应依赖如何创建的,具体是什么类实现的,都不用自己管了,权力交给注入器就可以了!

<u> 划重点</u>:其实上面这个过程大家应该发现了我们把本来自己的一部分控制权,转交给了注入器去做,这个就是我们经常说的IOC(Inversion of Control,控制反转)。DI其实就是IOC计原则的一种实现。还有我们平常说的观察者默认,其实也是IOC的一种实现,核心就是把部分职责(非核心职责)转交出去,从而去构建出一种松耦合的应用!

什么是依赖注入容器(DI Container)?

DI Container ,也可以叫 IOC Container,其实是一个框架(Framework),它提供了一整套的DI解决方案,它负责创建依赖,然后自动把依赖注入到需要它们的其他对象里面,同时还负责管理依赖的生命周期!一些强大的第三方容器还提供各种各样的功能,使我们更加愉快的撸代码!

常用的第三方DI Container:

  • Spring.NET

  • Autofac

  • Unity

  • Ninject

ASP .NET Core中的DI

在ASP.NET Core中,把依赖统一称作服务(services),所以DI Container就也被称为Service Container,Asp.NET Core提供了一个简单的内置容器 IServiceProvider ,它默认支持构造器注入 constructor injection,满足我们大多数的功能需求!

服务主要分为两类:

  1. Framework Services:框架服务,由ASP.NET Core框架提供,例如 IApplicationBuilderIHostingEnvironment ... ,详情见https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1#framework-provided-services

  2. Appliction Services:应用服务,由我们根据实际业务创建。

如果要通过DI容器自动实现注入我们的服务,我们必须要先在容器中登记服务(只需要注册应用服务,框架服务已经被ASP.NET Core框架注入了)。

注册服务(Registering Services)

假设我们有一个ILog接口和它的一个实现类,我们要把它注入到DI容器里面,然后在应用中使用它。

public interface ILog
{
    void Info(string msg);
    void Error(string err);
}

public class ConsoleLogger:ILog
{
    public void Info(string msg)
    {
        Console.WriteLine(msg);
    }
    public void Error(string err)
    {
        Console.WriteLine(err);
    }
}

然后在Startup类的ConfigureServices()方法中注册上面的服务,ConfigureServices()有一个IServiceCollection参数,就是用它来注册应用服务。

public class Startup
{    
    public void ConfigureServices(IServiceCollection services)    
    {        
        services.Add(new ServiceDescriptor(typeof(ILog), typeof(ConsoleLogger), ServiceLifetime.Singleton));            
    } 
}

ServiceDescriptor用来描述服务的类型、服务的具体实现已经服务的一个生命周期(Service Lifetime),上面我们指定了服务ILog的实现是ConsoleLogger,且是一个单例(Singleton)。

通常情况我们都是使用IServiceCollection扩展方法来注册服务

services.AddSingleton<ILog, ConsoleLogger>();//注册为单例

services.TryAddSingleton<ILog, ConsoleLogger>();

构造函数注入(Contractor Injection)

一旦我们注册了函数,当应用类的构造函数包含了需要依赖的服务,DI容器就自动帮我们注入依赖。

public class HomeController : Controller
{
    ILog _log;

    public HomeController(ILog log)
    {
        _log = log;
    }
    public IActionResult Index()
    {
        _log.Info("Executing /home/index");


        return View();
    }
}    

控制器需要ILog服务,只需要在构造函数的参数中包含ILog类型即可,我们不需做其他任何事,DI容器自动给我们创建ILog的实例,并根据注入时指定的生命周期,在切当是时机销毁(Dispose)这个实例。

Action方法注入(Method Injection)

有时候,我们只需要在某一个方法中需要这个服务,这时,我们可以给方案的参数标记上[FromServices] 这个特性,容器就能自动为我们注入这个依赖服务的实例了

public IActionResult Index([FromServices] ILog log)
{

    log.Info("Index method executing");


    return View();

}

属性注入(Property Injection)

ASP.NET Core自带这个容器不支持,需要使用第三方容器,例如Autofac 。

服务的生命周期(Service Lifetime)

DI容器负责管理已注册服务的生命周期,它根据指定的生命周期自动销毁服务实例。ASP.NET Core 的服务可以配置以下三种生命周期形式:

  • Transient(瞬态)

    每次从DI容器中解析(获取)服务时,DI容器都是返回一个新的服务实例。

    //原始方法
    services.Add(new ServiceDescriptor(typeof(ILog), typeof(ConsoleLogger), ServiceLifetime.Transient));
    //扩展方法
    services.AddTransient<ILog, ConsoleLogger>();
    
  • Scoped(作用域)

    每一个作用域范围内(例如每一个HTTP 请求)从DI容器中解析出来的实例都是同一个

    //原始方法
    services.Add(new ServiceDescriptor(typeof(ILog), typeof(ConsoleLogger), ServiceLifetime.Scoped));
    //扩展方法
    services.AddScoped<ILog, ConsoleLogger>();
    
  • Singleton(单例)

    只在第一次请求是创建,之后所有请求都共享同一个服务实例,直到应用程序的生命周结束。所以在ASP.NET Core应用中,没有必须手动去创建一个单例,通过注册一个单例服务到容器中即可。

    //原始方法
    services.Add(new ServiceDescriptor(typeof(ILog), new ConsoleLogger()));
    //扩展方法 
    services.AddSingleton<ILog, ConsoleLogger>();
    

DI使用经验的一些总结

泛型如何注册?

通过开放式泛型(Open Generics)注册服务。假设上面的类型修改成 ILog<T>

ConsoleLogger<T> ,那么我们按照下面的方式注册即可

services.AddScoped(typeof(ILog<>), typeof(ConsoleLogger<>));

相同的接口类型注入两个实现类型会怎样?

如果我们在注册服务时,相同类型注册多次并不会报错,但在解析时,返回的是最后一次注册的类型的实例。

    public interface ILog

    {
        void Info(string msg);
    }

    public class Logger1 : ILog
    {
        public void Info(string msg)
        {
        }
    }

    public class Logger2 : ILog
    {
        public void Info(string msg)
        {
        }
    }

public class Startup
{
     public void ConfigureServices(IServiceCollection services)
     {
         services.AddTransient<ILog, Logger1>();
         services.AddTransient<ILog, Logger2>();
     }
}
public class HomeController : Controller
{
     ILog _log;
     public HomeController(ILog log)
     {

         _log = log;//_log is Logger2

     }
}

为了避免我们多次注册,导致具体实现被覆盖的问题,所以我们一般都是使用 TryAddTransient,这个方法注册时,检测到相同类型已经被注册过,就不会在进行注册

public class Startup
{
     public void ConfigureServices(IServiceCollection services)
     {
         services.TryAddTransient<ILog, Logger1>();
         services.TryAddTransient<ILog, Logger2>();
     }
}
public class HomeController : Controller
{
     ILog _log;
     public HomeController(ILog log)
     {

         _log = log;//_log is Logger1

     }
}

自己想要手动从容器中获取服务对象,怎么做?

ASP.NET Core中的DI Container是IServiceProvider,只要获取这个对象,然后调用 GetService 这个方法即可。

  • 如果在Controller中

    HttpContext的属性RequestServices就是IServiceProvider类型,所以我们可以按照下面的方法:

          public IActionResult Index()
          {
              var services = this.HttpContext.RequestServices;
              var log = (ILog)services.GetService(typeof(ILog));
    
              log.Info("Index method executing");
    
              return View();
          }
    
  • 如果在应用中

    在应用中时,我们可以通过构造函数注入IServiceProvider

    public class MyAppService
    {
        private IServiceProvider _services;
        public MyAppService(IServiceProvider services)
        {
            _services=services;
        }
        public void Test()
        {
            //原始方法
            var log = (ILog)_services.GetService(typeof(ILog));
            //通过扩展方法,需要nuget添加Microsoft.Extensions.DependencyInjection.Abstractions 这个引用
            var log = _services.GetService<ILog>();
        }
    }
    

结语

ASP.NET Core已经很强大了,提供了很多实用功能,希望.Net的战友们能在基本知识储备的前提下,多多发掘总结出ASP.NET Core开发的最佳实践,为推动.NET Core生态建设贡献一份自己的力量!:muscle:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容