依赖注入(Dependency Injection)简称DI,DI实现了控制反转(Inversion of Control,Ioc),遵循了依赖倒置原则,
DI实现解耦、不需要手动去获取或创建依赖的对象
控制反转:由容器帮我们控制对象的创建和依赖对象的注入
正转:直接获取依赖对象并手动创建对象
案例:
一些接口和类
public interface IStorage
{
}
public class FileStorage : IStorage
{
public string Read(string path)
{
return File.ReadAllText(path);
}
}
public interface IBookService
{
string[] GetBooks();
}
public class BookService : IBookService
{
public IStorage Storage { get; }
public BookService(IStorage storage)
{
Storage = storage;
}
public string[] GetBooks()
{
// ...
return new string[] { };
}
}
不使用依赖注入:
class Program
{
static void Main(string[] args)
{
// 需要创建或获取依赖
IStorage fileStorage = new FileStorage();
// 需要手动new服务并传入依赖
IBookService bookService = new BookService(fileStorage);
bookService.GetBooks();
}
}
使用依赖注入:
class Program
{
static void Main(string[] args)
{
// 创建依赖容器
IServiceCollection serviceCollection = new ServiceCollection();
// 注册服务
serviceCollection.AddSingleton<IStorage, FileStorage>();
// 注册服务
serviceCollection.AddSingleton<IBookService, BookService>();
// 构建服务提供者
IServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();
// 获取服务,IBookService的实现BookService的依赖将自动注入
IBookService bookService = serviceProvider.GetService<IBookService>();
bookService.GetBooks();
}
}
服务注册
IServiceCollection
是一个ServiceDescriptor
服务描述器集合,ServiceDescriptor
描述了一个服务
public interface IServiceCollection :
IList<ServiceDescriptor>,
ICollection<ServiceDescriptor>,
IEnumerable<ServiceDescriptor>,
IEnumerable
{
}
注册服务就是向ServiceCollection
这个集合中添加ServiceDescriptor
IServiceCollection serviceCollection = new ServiceCollection();
var serviceDescriptor = new ServiceDescriptor(
typeof(IStorage), // 服务类型
typeof(FileStorage), // 实现类型
ServiceLifetime.Transient // 生命周期
);
// 清除服务
serviceCollection.Clear();
// 是否包含服务
if (serviceCollection.Contains(serviceDescriptor))
{
serviceCollection.Remove(serviceDescriptor);
}
// 注册服务
serviceCollection.Add(serviceDescriptor);
// 只有容器中不存在此服务时才注册服务
serviceCollection.TryAdd(serviceDescriptor);
AddSingleton
、AddScoped
、AddTransient
是构建ServiceDescriptor
的简便扩展方法
IServiceCollection serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IStorage, FileStorage>();
serviceCollection.AddScoped<IStorage, FileStorage>();
serviceCollection.AddTransient<IStorage, FileStorage>();
serviceCollection.AddTransient<FileStorage>(); // 等同于 serviceCollection.AddTransient<FileStorage, FileStorage>()
在向容器注册服务时,可以填写 实现类型、工厂或者实例
IServiceCollection serviceCollection = new ServiceCollection();
serviceCollection.Add(new ServiceDescriptor(typeof(IStorage),typeof(FileStorage),ServiceLifetime.Transient));
FileStorage fs = new FileStorage();
serviceCollection.Add(new ServiceDescriptor(typeof(IStorage), fs));
serviceCollection.Add(new ServiceDescriptor(typeof(IStorage), serviceProvider => new FileStorage(), ServiceLifetime.Singleton));
方法 | 对象自动 dispose | 多种实现 | 转递参数 |
---|---|---|---|
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>() 例子: services.AddSingleton<IMyDep, MyDep>()
|
是 | 是 | 否 |
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION}) 例子: services.AddSingleton<IMyDep>(sp => new MyDep()) services.AddSingleton<IMyDep>(sp => new MyDep(99));
|
是 | 是 | 是 |
Add{LIFETIME}<{IMPLEMENTATION}>() 例子: services.AddSingleton<MyDep>()
|
是 | 否 | 否 |
AddSingleton<{SERVICE}>(new {IMPLEMENTATION}) 例子: services.AddSingleton<IMyDep>(new MyDep()) services.AddSingleton<IMyDep>(new MyDep(99))
|
否 | 是 | 是 |
AddSingleton(new {IMPLEMENTATION}) 例子: services.AddSingleton(new MyDep()) services.AddSingleton(new MyDep(99))
|
否 | 否 | 是 |
不由服务容器创建的服务
考虑下列代码:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(new Service1());
services.AddSingleton(new Service2());
}
在上述代码中:
服务实例不是由服务容器创建的,框架不会自动释放服务,开发人员负责释放服务。
服务获取
GetRequiredService
与GetService
区别
如果容器中不存在要获取的服务,GetRequiredService
将抛出异常,GetService
将返回null
使用IServiceProvider
延迟获取服务
案例:
class MyService6
{
}
class MyService5
{
public IServiceProvider ServiceProvider { get; }
public MyService5(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
public void GetService6()
{
ServiceProvider.GetService<MyService6>();
}
}
获取IEnumerable<>
服务数组
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<Animal, Dog>();
serviceCollection.AddSingleton<Animal, Cat>();
serviceCollection.AddSingleton<Animal, Pig>();
var serviceProvider = serviceCollection.BuildServiceProvider(true);
var animals = serviceProvider.GetService<IEnumerable<Animal>>();
Console.WriteLine(animals.Count()); // 3
生命周期
有如下3种声明周期
- Transient:临时,每次都将创建一个实例
- Scoped:范围,作用域,对于 Web 应用程序,每次Http请求创建一个实例,也可以通过
CreateScope
创建一个作用域,在此作用域内只会创建一个实例 - Singleton:单例,只会创建一个实例
有作用域的服务由创建它们的容器释放
Transient
声明周期案例
class MyService : IDisposable
{
public MyService()
{
Console.WriteLine("MyService Construct"); // 创建一个新的实例将输出`MyService Construct`
}
public void Hello()
{
Console.WriteLine("MyService Hello");
}
public void Dispose()
{
Console.WriteLine("MyService Dispose");
}
}
C#2
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<MyService>();
var serviceProvider = serviceCollection.BuildServiceProvider();
serviceProvider.GetService<MyService>(); // 输出:MyService Construct
serviceProvider.GetService<MyService>(); // 输出:MyService Construct
serviceProvider.GetService<MyService>(); // 输出:MyService Construct
Scoped
声明周期案例
var serviceCollection = new ServiceCollection();
serviceCollection.AddScoped<MyService>();
var serviceProvider = serviceCollection.BuildServiceProvider();
serviceProvider.GetService<MyService>(); // 输出:MyService Construct
serviceProvider.GetService<MyService>(); // 无输出
serviceProvider.GetService<MyService>(); // 无输出
using (var serviceScope = serviceProvider.CreateScope())
{
serviceScope.ServiceProvider.GetService<MyService>(); // 输出:MyService Construct
serviceScope.ServiceProvider.GetService<MyService>(); // 无输出
serviceScope.ServiceProvider.GetService<MyService>(); // 无输出
}
// 上面作用域结束后,将自动释放服务,输出 MyService Dispose
Single
声明周期案例
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<MyService>();
var serviceProvider = serviceCollection.BuildServiceProvider();
serviceProvider.GetService<MyService>(); // 输出:MyService Construct
serviceProvider.GetService<MyService>(); // 无输出
serviceProvider.GetService<MyService>(); // 无输出
using (var serviceScope = serviceProvider.CreateScope())
{
serviceScope.ServiceProvider.GetService<MyService>(); // 无输出
serviceScope.ServiceProvider.GetService<MyService>(); // 无输出
serviceScope.ServiceProvider.GetService<MyService>(); // 无输出
}
作用域验证
在调用BuildServiceProvider
时可以传入参数来配置是否启用作用域验证
对于Web应用程序,如果应用环境为“Development”(开发环境),默认作用域验证将启用(CreateDefaultBuilder
会将 ServiceProviderOptions.ValidateScopes
设为 true
),若要始终验证作用域(包括在生存环境中验证),请使用HostBuilder
上的 UseDefaultServiceProvider
配置 ServiceProviderOptions
启用作用域验证后,将验证以下内容:
- 确保没有从根服务提供程序直接或间接解析到有作用域的服务
- 未将有作用域的服务直接或间接注入到单一实例。
案例
class MyService2
{
}
class MyService3
{
public MyService3(MyService2 myService2)
{
}
}
C#2
var serviceCollection = new ServiceCollection();
serviceCollection.AddScoped<MyService2>();
var serviceProvider = serviceCollection.BuildServiceProvider(true); // 传入true,开启作用域验证
using (var serviceScope = serviceProvider.CreateScope())
{
serviceScope.ServiceProvider.GetService<MyService2>(); // 正确用法
}
serviceProvider.GetService<MyService2>(); // 将抛出异常,因为不能从根服务提供程序直接或间接解析到有作用域的服务
C#3
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<MyService3>();
serviceCollection.AddScoped<MyService2>();
var serviceProvider = serviceCollection.BuildServiceProvider(true);
serviceProvider.GetService<MyService3>(); // 将抛出异常,不能将有作用域的服务直接或间接注入到单一实例
调用 BuildServiceProvider
时,会创建根服务提供程序。 在启动提供程序和应用时,根服务提供程序的生存期对应于应用/服务的生存期,并在关闭应用时释放。
有作用域的服务由创建它们的容器释放
如果作用域创建于根容器,则该服务的生存会有效地提升至单一实例,因为根容器只会在应用/服务关闭时将其释放
构造函数注入行为
服务能被获取通过:
- IServiceProvider
- ActivatorUtilities:创建没有在容器中注入的服务
构造函数可以使用没有在容器中注入的服务,但是参数必须分配默认值。
通过IServiceProvider
或ActivatorUtilities
解析服务时,构造函数注入需要公共构造函数
通过ActivatorUtilities
解析服务时,构造函数注入要求仅存在一个适用的构造函数。 ActivatorUtilities
支持构造函数重载,其所有参数都可以通过依赖项注入来实现。
案例
class MyService4
{
public MyService4()
{
Console.WriteLine("0 Parameter Constructor");
}
public MyService4(string a)
{
Console.WriteLine("1 Parameter Constructor");
}
public MyService4(string a, string b)
{
Console.WriteLine("2 Parameter Constructor");
}
}
C#2
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<MyService4>();
var serviceProvider = serviceCollection.BuildServiceProvider(true);
ActivatorUtilities.CreateInstance<MyService4>(serviceProvider); // 0 Parameter Constructor
ActivatorUtilities.CreateInstance<MyService4>(serviceProvider, "Param 1"); // 1 Parameter Constructor
ActivatorUtilities.CreateInstance<MyService4>(serviceProvider, "Param 1", "Param 2"); // 2 Parameter Constructor
ActivatorUtilities.CreateInstance<MyService4>(serviceProvider, "Param 1", 12);// 抛出异常,没有找到合适的构造函数
ActivatorUtilities.CreateInstance<MyService4>(serviceProvider, "Param 1", "Param 2", "Param 3");// 抛出异常,没有找到合适的构造函数
Asp.Net Core,注入 Startup 的服务
服务可以注入 Startup
构造函数和 Startup.Configure
方法
使用泛型主机 (IHostBuilder
) 时,只能将以下服务注入 Startup
构造函数:
- IWebHostEnvironment
- IHostEnvironment
- IConfiguration
任何向 DI 容器注册的服务都可以注入 Startup.Configure
方法:
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
}
使用扩展方法注册服务组
ASP.NET Core
框架使用一种约定来注册一组相关服务。 约定使用单个 Add{GROUP_NAME}
扩展方法来注册该框架功能所需的所有服务。 例如,AddControllers
扩展方法会注册 MVC 控制器所需的服务
从 main 调用服务
使用 IServiceScopeFactory.CreateScope
创建 IServiceScope
以解析应用范围内的作用域服务。 此方法可以用于在启动时访问有作用域的服务以便运行初始化任务。
以下示例演示如何访问范围内 IMyDependency
服务并在 Program.Main
中调用其 WriteMessage
方法:
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var serviceScope = host.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
try
{
var myDependency = services.GetRequiredService<IMyDependency>();
myDependency.WriteMessage("Call services from main");
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred.");
}
}
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}