实用技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > ASP.NET > 实用技巧 > ASP.NET Core 依赖注入

理解ASP.NET Core 依赖注入(Dependency Injection)

作者:xiaoxiaotank

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

依赖注入

什么是依赖注入

简单说,就是将对象的创建和销毁工作交给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");
    }
}

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

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<Service1>();
    // 超出范围(请求结束时)则释放
    services.AddScoped<Service2>();
    // 程序停止时释放
    services.AddSingleton<Service3>();
    // 程序停止时释放
    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<IMyService, MyService>();
services.AddSingleton<IMyService, MyService>();

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

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

services.AddTransient<IMyService, MyService1>();
// 由于上面已经注册了服务类型 IMyService,所以下面的代码不不会执行任何操作(与生命周期无关)
services.TryAddTransient<IMyService, MyService1>();
services.TryAddTransient<IMyService, MyService2>();
// 注册了 IMyService - MyService1
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService1>());
// 注册了 IMyService - MyService2
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService2>());
// 未进行任何操作,因为 IMyService - MyService1 在上面已经注册了
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyService, MyService1>());

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

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

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

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

public void ConfigureServices(IServiceCollection services)
{
    // 生命周期没有限制
    services.AddTransient<IAnimalService, DogService>();
    services.AddScoped<IAnimalService, PigService>();
    services.AddSingleton<IAnimalService, CatService>();
}

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

Replace && Remove 扩展方法

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

// 将 IMyService 的实现替换为 MyService1
services.Replace(ServiceDescriptor.Singleton<IMyService, MyService>());
// 移除 IMyService 注册的实现 MyService
services.Remove(ServiceDescriptor.Singleton<IMyService, MyService>());
// 移除 IMyService 的所有注册
services.RemoveAll<IMyService>();
// 清除所有服务注册
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<Startup>();
        })
        // 通过此处将默认服务提供器工厂替换为 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<UserService>().As<IUserService>();
        }
    }

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

服务解析和注入

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

1.IServiceProvider

2.ActivatorUtilities

构造函数注入

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

方法注入

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

[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";
    }
}

然后注册服务

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddControllersAsServices();
}

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

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

        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

一些注意事项

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<IMyService>();

        return "Ok";
    }
}
public void ConfigureServices(IServiceCollection services)
{
    // 不要在该方法中调用该方法
    var serviceProvider = services.BuildServiceProvider();
}

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

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

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

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

namespace Microsoft.Extensions.DependencyInjection
{
    public static class ApplicationServiceCollectionExtensions
    {
        public static IServiceCollection AddApplicationService(this IServiceCollection services)
        {
            services.AddTransient<Service1>();
            services.AddScoped<Service2>();
            services.AddSingleton<Service3>();
            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 依赖注入内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文