热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

理解ASP.NETCore依赖注入(DependencyInjection)

把有依赖关系的类放到容器中,解析出这些类的实例,就是依赖注入。目的是实现类的解耦。本文主要介绍了ASP.NETCore依赖注入(DependencyInjection),需要了解具体内容的可以仔细阅读这篇文章,希望对你有所帮助

依赖注入

什么是依赖注入

简单说,就是将对象的创建和销毁工作交给DI容器来进行,调用方只需要接收注入的对象实例即可。

  • 微软官方文档-DI

依赖注入有什么好处

依赖注入在.NET中,可谓是“一等公民”,处处都离不开它,那么它有什么好处呢?

假设有一个日志类 FileLogger,用于将日志记录到本地文件。

public class FileLogger
{
    public void LogInfo(string message)
    {

    }
}

日志很常用,几乎所有服务都需要记录日志。如果不使用依赖注入,那么我们就必须在每个服务中手动 new FileLogger 来创建一个 FileLogger 实例。

public class MyService
{
    private readonly FileLogger _logger = new FileLogger();

    public void Get()
    {
        _logger.LogInfo("MyService.Get");
    }
}

如果某一天,想要替换掉 FileLogger,而是使用 ElkLogger,通过ELK来处理日志,那么我们就需要将所有服务中的代码都要改成 new ElkLogger。

public class MyService
{
    private readonly ElkLogger _logger = new ElkLogger();

    public void Get()
    {
        _logger.LogInfo("MyService.Get");
    }
}
  • 在一个大型项目中,这样的代码分散在项目各处,涉及到的服务均需要进行修改,显然一个一个去修改不现实,且违反了“开闭原则”。
  • 如果Logger中还需要其他一些依赖项,那么用到Logger的服务也要为其提供依赖,如果依赖项修改了,其他服务也必须要进行更改,更加增大了维护难度。
  • 很难进行单元测试,因为它无法进行 mock

正因如此,所以依赖注入解决了这些棘手的问题:

  • 通过接口或基类(包含抽象方法或虚方法等)将依赖关系进行抽象化
  • 将依赖关系存放到服务容器中
  • 由框架负责创建和释放依赖关系的实例,并将实例注入到构造函数、属性或方法中

ASP.NET Core内置的依赖注入

服务生存周期

Transient
瞬时,即每次获取,都是一个全新的服务实例

Scoped
范围(或称为作用域),即在某个范围(或作用域内)内,获取的始终是同一个服务实例,而不同范围(或作用域)间获取的是不同的服务实例。对于Web应用,每个请求为一个范围(或作用域)。

Singleton
单例,即在单个应用中,获取的始终是同一个服务实例。另外,为了保证程序正常运行,要求单例服务必须是线程安全的。

服务释放

若服务实现了IDisposable接口,并且该服务是由DI容器创建的,那么你不应该去Dispose,DI容器会对服务自动进行释放。

如,有Service1、Service2、Service3、Service4四个服务,并且都实现了IDisposable接口,如:

public class Service1 : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Service1.Dispose");
    }
}

public class Service2 : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Service2.Dispose");
    }
}

public class Service3 : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Service3.Dispose");
    }
}

public class Service4 : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Service4.Dispose");
    }
}

并注册为:

public void ConfigureServices(IServiceCollection services)
{
    // 每次使用完(请求结束时)即释放
    services.AddTransient();
    // 超出范围(请求结束时)则释放
    services.AddScoped();
    // 程序停止时释放
    services.AddSingleton();
    // 程序停止时释放
    services.AddSingleton(sp => new Service4());
}

构造函数注入一下

public ValuesController(
    Service1 service1, 
    Service2 service2, 
    Service3 service3, 
    Service4 service4)
{ }

请求一下,获取输出:

Service2.Dispose
Service1.Dispose

这些服务实例都是由DI容器创建的,所以DI容器也会负责服务实例的释放和销毁。注意,单例此时还没到释放的时候。

但如果注册为:

public void ConfigureServices(IServiceCollection services)
{
    // 注意与上面的区别,这个是直接 new 的,而上面是通过 sp => new 的
    services.AddSingleton(new Service1());
    services.AddSingleton(new Service2());
    services.AddSingleton(new Service3());
    services.AddSingleton(new Service4());
}

此时,实例都是咱们自己创建的,DI容器就不会负责去释放和销毁了,这些工作都需要我们开发人员自己去做。

更多注册方式,请参考官方文档-Service registration methods

TryAdd{Lifetime}扩展方法

当你将同样的服务注册了多次时,如:

services.AddSingleton();
services.AddSingleton();

那么当使用IEnumerable<{Service}>(下面会讲到)解析服务时,就会产生多个MyService实例的副本。

为此,框架提供了TryAdd{Lifetime}扩展方法,位于命名空间Microsoft.Extensions.DependencyInjection.Extensions下。当DI容器中已存在指定类型的服务时,则不进行任何操作;反之,则将该服务注入到DI容器中。

services.AddTransient();
// 由于上面已经注册了服务类型 IMyService,所以下面的代码不不会执行任何操作(与生命周期无关)
services.TryAddTransient();
services.TryAddTransient();
  • TryAdd:通过参数ServiceDescriptor将服务类型、实现类型、生命周期等信息传入进去
  • TryAddTransient:对应AddTransient
  • TryAddScoped:对应AddScoped
  • TryAddSingleton:对应AddSingleton
  • TryAddEnumerable:这个和TryAdd的区别是,TryAdd仅根据服务类型来判断是否要进行注册,而TryAddEnumerable则是根据服务类型和实现类型一同进行判断是否要进行注册,常常用于注册同一服务类型的多个不同实现。举个例子吧:
// 注册了 IMyService - MyService1
services.TryAddEnumerable(ServiceDescriptor.Singleton());
// 注册了 IMyService - MyService2
services.TryAddEnumerable(ServiceDescriptor.Singleton());
// 未进行任何操作,因为 IMyService - MyService1 在上面已经注册了
services.TryAddEnumerable(ServiceDescriptor.Singleton());

解析同一服务的多个不同实现

默认情况下,如果注入了同一个服务的多个不同实现,那么当进行服务解析时,会以最后一个注入的为准。

如果想要解析出同一服务类型的所有服务实例,那么可以通过IEnumerable<{Service}>来解析(顺序同注册顺序一致):

public interface IAnimalService { }
public class DogService : IAnimalService { }
public class PigService : IAnimalService { }
public class CatService : IAnimalService { }

public void ConfigureServices(IServiceCollection services)
{
    // 生命周期没有限制
    services.AddTransient();
    services.AddScoped();
    services.AddSingleton();
}

public ValuesController(
    // CatService
    IAnimalService animalService,   
    // DogService、PigService、CatService
    IEnumerable animalServices)
{
}

Replace && Remove 扩展方法

上面我们所提到的,都是注册新的服务到DI容器中,但是有时我们想要替换或是移除某些服务,这时就需要使用ReplaceRemove

// 将 IMyService 的实现替换为 MyService1
services.Replace(ServiceDescriptor.Singleton());
// 移除 IMyService 注册的实现 MyService
services.Remove(ServiceDescriptor.Singleton());
// 移除 IMyService 的所有注册
services.RemoveAll();
// 清除所有服务注册
services.Clear();

Autofac

Autofac 是一个老牌DI组件了,接下来我们使用Autofac替换ASP.NET Core自带的DI容器。

1.安装nuget包:

Install-Package Autofac
Install-Package Autofac.Extensions.DependencyInjection

2.替换服务提供器工厂

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup();
        })
        // 通过此处将默认服务提供器工厂替换为 autofac
        .UseServiceProviderFactory(new AutofacServiceProviderFactory());

3.在 Startup 类中添加 ConfigureContainer 方法

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        COnfiguration= configuration;
    }

    public IConfiguration Configuration { get; }

    public ILifetimeScope AutofacContainer { get; private set; }

    public void ConfigureServices(IServiceCollection services)
    {
        // 1. 不要 build 或返回任何 IServiceProvider,否则会导致 ConfigureContainer 方法不被调用。
        // 2. 不要创建 ContainerBuilder,也不要调用 builder.Populate(),AutofacServiceProviderFactory 已经做了这些工作了
        // 3. 你仍然可以在此处通过微软默认的方式进行服务注册
        
        services.AddOptions();
        services.AddControllers();
        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication.Ex", Version = "v1" });
        });
    }

    // 1. ConfigureContainer 用于使用 Autofac 进行服务注册
    // 2. 该方法在 ConfigureServices 之后运行,所以这里的注册会覆盖之前的注册
    // 3. 不要 build 容器,不要调用 builder.Populate(),AutofacServiceProviderFactory 已经做了这些工作了
    public void ConfigureContainer(ContainerBuilder builder)
    {
        // 将服务注册划分为模块,进行注册
        builder.RegisterModule(new AutofacModule());
    }
    
    public class AutofacModule : Autofac.Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            // 在此处进行服务注册
            builder.RegisterType().As();
        }
    }

    public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
    {
        // 通过此方法获取 autofac 的 DI容器
        AutofacCOntainer= app.ApplicationServices.GetAutofacRoot();
    }
}

服务解析和注入

上面我们主要讲了服务的注入方式,接下来看看服务的解析方式。解析方式有两种:

1.IServiceProvider

2.ActivatorUtilities

  • 用于创建未在DI容器中注册的服务实例
  • 用于某些框架级别的功能

构造函数注入

上面我们举得很多例子都是使用了构造函数注入——通过构造函数接收参数。构造函数注入是非常常见的服务注入方式,也是首选方式,这要求:

  • 构造函数可以接收非依赖注入的参数,但必须提供默认值
  • 当服务通过IServiceProvider解析时,要求构造函数必须是public
  • 当服务通过ActivatorUtilities解析时,要求构造函数必须是public,虽然支持构造函数重载,但必须只能有一个是有效的,即参数能够全部通过依赖注入得到值

方法注入

顾名思义,方法注入就是通过方法参数来接收服务实例。

[HttpGet]
public string Get([FromServices]IMyService myService)
{
    return "Ok";
}

属性注入

ASP.NET Core内置的依赖注入是不支持属性注入的。但是Autofac支持,用法如下:

老规矩,先定义服务和实现

public interface IUserService 
{
    string Get();
}

public class UserService : IUserService
{
    public string Get()
    {
        return "User";
    }
}

然后注册服务

  • 默认情况下,控制器的构造函数参数由DI容器来管理吗,而控制器实例本身却是由ASP.NET Core框架来管理,所以这样“属性注入”是无法生效的
  • 通过AddControllersAsServices方法,将控制器交给 autofac 容器来处理,这样就可以使“属性注入”生效了
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddControllersAsServices();
}

public void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterModule();
}

public class AutofacModule : Autofac.Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType().As();

        var cOntrollerTypes= Assembly.GetExecutingAssembly().GetExportedTypes()
            .Where(type => typeof(ControllerBase).IsAssignableFrom(type))
            .ToArray();

        // 配置所有控制器均支持属性注入
        builder.RegisterTypes(controllerTypes).PropertiesAutowired();
    }
}

最后,我们在控制器中通过属性来接收服务实例

public class ValuesController : ControllerBase
{
    public IUserService UserService { get; set; }

    [HttpGet]
    public string Get()
    {
        return UserService.Get();
    }
}

通过调用Get接口,我们就可以得到IUserService的实例,从而得到响应

User

一些注意事项

  • 避免使用服务定位模式。尽量避免使用GetService来获取服务实例,而应该使用DI。
using Microsoft.Extensions.DependencyInjection;

public class ValuesController : ControllerBase
{
    private readonly IServiceProvider _serviceProvider;

    // 应通过依赖注入的方式获取服务实例
    public ValuesController(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    [HttpGet]
    public string Get()
    {
        // 尽量避免通过 GetService 方法获取服务实例
        var myService = _serviceProvider.GetService();

        return "Ok";
    }
}
  • 避免在ConfigureServices中调用BuildServiceProvider。因为这会导致创建第二个DI容器的副本,从而导致注册的单例服务出现多个副本。
public void ConfigureServices(IServiceCollection services)
{
    // 不要在该方法中调用该方法
    var serviceProvider = services.BuildServiceProvider();
}
  • 一定要注意服务解析范围,不要在 Singleton 中解析 Transient 或 Scoped 服务,这可能导致服务状态错误(如导致服务实例生命周期提升为单例)。允许的方式有:

1.在 Scoped 或 Transient 服务中解析 Singleton 服务

2.在 Scoped 或 Transient 服务中解析 Scoped 服务(不能和前面的Scoped服务相同)

  • 当在Development环境中运行、并通过 CreateDefaultBuilder生成主机时,默认的服务提供程序会进行如下检查:

1.不能在根服务提供程序解析 Scoped 服务,这会导致 Scoped 服务的生命周期提升为 Singleton,因为根容器在应用关闭时才会释放。

2.不能将 Scoped 服务注入到 Singleton 服务中

  • 随着业务增长,需要依赖注入的服务也越来越多,建议使用扩展方法,封装服务注入,命名为Add{Group_Name},如将所有 AppService 的服务注册封装起来
namespace Microsoft.Extensions.DependencyInjection
{
    public static class ApplicationServiceCollectionExtensions
    {
        public static IServiceCollection AddApplicationService(this IServiceCollection services)
        {
            services.AddTransient();
            services.AddScoped();
            services.AddSingleton();
            services.AddSingleton(sp => new Service4());

            return services;
        }
    }
}

然后在ConfigureServices中调用即可

public void ConfigureServices(IServiceCollection services)
{
    services.AddApplicationService();
}

框架默认提供的服务

以下列出一些常用的框架已经默认注册的服务:

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

到此这篇关于理解ASP.NET Core 依赖注入(Dependency Injection)的文章就介绍到这了,更多相关ASP.NET Core 依赖注入内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!


推荐阅读
  • Imtryingtofigureoutawaytogeneratetorrentfilesfromabucket,usingtheAWSSDKforGo.我正 ... [详细]
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • Linux服务器密码过期策略、登录次数限制、私钥登录等配置方法
    本文介绍了在Linux服务器上进行密码过期策略、登录次数限制、私钥登录等配置的方法。通过修改配置文件中的参数,可以设置密码的有效期、最小间隔时间、最小长度,并在密码过期前进行提示。同时还介绍了如何进行公钥登录和修改默认账户用户名的操作。详细步骤和注意事项可参考本文内容。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 本文详细介绍了MysqlDump和mysqldump进行全库备份的相关知识,包括备份命令的使用方法、my.cnf配置文件的设置、binlog日志的位置指定、增量恢复的方式以及适用于innodb引擎和myisam引擎的备份方法。对于需要进行数据库备份的用户来说,本文提供了一些有价值的参考内容。 ... [详细]
  • 本文介绍了高校天文共享平台的开发过程中的思考和规划。该平台旨在为高校学生提供天象预报、科普知识、观测活动、图片分享等功能。文章分析了项目的技术栈选择、网站前端布局、业务流程、数据库结构等方面,并总结了项目存在的问题,如前后端未分离、代码混乱等。作者表示希望通过记录和规划,能够理清思路,进一步完善该平台。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • Linux如何安装Mongodb的详细步骤和注意事项
    本文介绍了Linux如何安装Mongodb的详细步骤和注意事项,同时介绍了Mongodb的特点和优势。Mongodb是一个开源的数据库,适用于各种规模的企业和各类应用程序。它具有灵活的数据模式和高性能的数据读写操作,能够提高企业的敏捷性和可扩展性。文章还提供了Mongodb的下载安装包地址。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • 如何查询zone下的表的信息
    本文介绍了如何通过TcaplusDB知识库查询zone下的表的信息。包括请求地址、GET请求参数说明、返回参数说明等内容。通过curl方法发起请求,并提供了请求示例。 ... [详细]
  • 本文记录了在vue cli 3.x中移除console的一些采坑经验,通过使用uglifyjs-webpack-plugin插件,在vue.config.js中进行相关配置,包括设置minimizer、UglifyJsPlugin和compress等参数,最终成功移除了console。同时,还包括了一些可能出现的报错情况和解决方法。 ... [详细]
  • 本文介绍了自动化测试专家Elfriede Dustin在2008年的文章中讨论了自动化测试项目失败的原因。同时,引用了IDT在2007年进行的一次软件自动化测试的研究调查结果,调查显示很多公司认为自动化测试很有用,但很少有公司成功实施。调查结果表明,缺乏资源是导致自动化测试失败的主要原因,其中37%的人认为缺乏时间。 ... [详细]
  • Android系统源码分析Zygote和SystemServer启动过程详解
    本文详细解析了Android系统源码中Zygote和SystemServer的启动过程。首先介绍了系统framework层启动的内容,帮助理解四大组件的启动和管理过程。接着介绍了AMS、PMS等系统服务的作用和调用方式。然后详细分析了Zygote的启动过程,解释了Zygote在Android启动过程中的决定作用。最后通过时序图展示了整个过程。 ... [详细]
  • 本文介绍了一种轻巧方便的工具——集算器,通过使用集算器可以将文本日志变成结构化数据,然后可以使用SQL式查询。集算器利用集算语言的优点,将日志内容结构化为数据表结构,SPL支持直接对结构化的文件进行SQL查询,不再需要安装配置第三方数据库软件。本文还详细介绍了具体的实施过程。 ... [详细]
author-avatar
MYJIE2502897603
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有