千家信息网

关于ASP.NET Core依赖注入的详细介绍

发表于:2025-01-16 作者:千家信息网编辑
千家信息网最后更新 2025年01月16日,这篇文章主要讲解了"关于ASP.NET Core依赖注入的详细介绍",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"关于ASP.NET Core依赖注入的
千家信息网最后更新 2025年01月16日关于ASP.NET Core依赖注入的详细介绍

这篇文章主要讲解了"关于ASP.NET Core依赖注入的详细介绍",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"关于ASP.NET Core依赖注入的详细介绍"吧!

目录
  • 依赖注入

    • 什么是依赖注入

    • 依赖注入有什么好处

  • ASP.NET Core内置的依赖注入

    • 服务生存周期

    • 服务释放

    • TryAdd{Lifetime}扩展方法

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

    • Replace && Remove 扩展方法

  • Autofac

    • 服务解析和注入

      • 构造函数注入

      • 方法注入

      • 属性注入

    • 一些注意事项

      • 框架默认提供的服务

        依赖注入

        什么是依赖注入

        简单说,就是将对象的创建和销毁工作交给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 - MyService1services.TryAddEnumerable(ServiceDescriptor.Singleton());// 注册了 IMyService - MyService2services.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 的实现替换为 MyService1services.Replace(ServiceDescriptor.Singleton());// 移除 IMyService 注册的实现 MyServiceservices.Remove(ServiceDescriptor.Singleton());// 移除 IMyService 的所有注册services.RemoveAll();// 清除所有服务注册services.Clear();

        Autofac

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

        1.安装nuget包:

        Install-Package AutofacInstall-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.IApplicationBuilderFactoryTransient
        IHostApplicationLifetimeSingleton
        IHostLifetimeSingleton
        IWebHostEnvironmentSingleton
        IHostEnvironmentSingleton
        Microsoft.AspNetCore.Hosting.IStartupSingleton
        Microsoft.AspNetCore.Hosting.IStartupFilterTransient
        Microsoft.AspNetCore.Hosting.Server.IServerSingleton
        Microsoft.AspNetCore.Http.IHttpContextFactoryTransient
        Microsoft.Extensions.Logging.ILoggerSingleton
        Microsoft.Extensions.Logging.ILoggerFactorySingleton
        Microsoft.Extensions.ObjectPool.ObjectPoolProviderSingleton
        Microsoft.Extensions.Options.IConfigureOptionsTransient
        Microsoft.Extensions.Options.IOptionsSingleton
        System.Diagnostics.DiagnosticSourceSingleton
        System.Diagnostics.DiagnosticListenerSingleton

        感谢各位的阅读,以上就是"关于ASP.NET Core依赖注入的详细介绍"的内容了,经过本文的学习后,相信大家对关于ASP.NET Core依赖注入的详细介绍这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是,小编将为大家推送更多相关知识点的文章,欢迎关注!

        0