ASP.NET Core – Dependency Injection
前言
很久很久以前就写过了 Asp.net core 学习笔记 ( DI 依赖注入 ), 这篇只是整理一下而已.
参考
Using dependency injection in a .Net Core console application
介绍
Dependency Injection 是一种管理依赖的方式。
class 之间经常会有依赖关系,比如
public class ServiceA { public void DoSomething() => Console.WriteLine("do something..."); } public class ServiceB(ServiceA serviceA) { public void DoSomething() => serviceA.DoSomething(); }
ServiceB 的某个方法需要使用到 ServiceA 的某个方法,这就表示 ServiceB 依赖了 ServerA。
依赖关系会让实例化变得复杂和繁琐。
var serviceA = new ServiceA(); var serviceB = new ServiceB(serviceA); serviceB.DoSomething();
若想实例化 ServiceB 首先需要实例化 ServiceA,试想想如果依赖链很长的话,代码将变得非常长和混乱。
而依赖注入就是为了解决这个问题而诞生的。使用依赖注入后,我们在实例化时就不需要去理会复杂的依赖关系了。
许多地方都有 Dependency Injection 这个概念。比如 Java、Angular 也有。但是它们的实现都有一些些区别,也可以理解为变种吧,但中心思想是一样的。
How It Work ?
首先, 把整个项目的 Service (也就是 class) 都交给一个 ServiceProvider 来管理. 不管你要什么 Service 都必须跟 ServiceProvider 拿.
每个 Service 通过 Construtor 来表达依赖关系.每当有人需要 Service 的时候, ServiceProvider 就会查找 Service 的依赖, 一层层的去创建出所有需要的对象.
整个过程大致上就是这样.
Get Started
用 Console 测试就可以了
dotnet new console -o DependencyInjection
dotnet add package Microsoft.Extensions.DependencyInjection
Console 没有很多 build-in 的 dll, 如果想方便可以一次添加.
dotnet add package Microsoft.AspNetCore.App
ServiceCollection & ServiceProvider
ServiceA 依赖 ServiceB
public class ServiceA { private readonly ServiceB _serviceB; public ServiceA( ServiceB serviceB ) { _serviceB = serviceB; } public string GetValue() { return _serviceB.GetValue(); } } public class ServiceB { public string GetValue() { return "Service B value"; } }
program.cs
public class Program { public static void Main(string[] args) { var serviceCollection = new ServiceCollection(); // 一个 Services 大合集 serviceCollection.AddScoped<ServiceA, ServiceA>(); // 添加 ServiceA 和 ServiceB 进去大合集 serviceCollection.AddScoped<ServiceB, ServiceB>(); var serviceProvider = serviceCollection.BuildServiceProvider(); // 把所有 Service 丢进去后就可以创建出 Provider var serviceA = serviceProvider.GetRequiredService<ServiceA>(); // 通过 Provider 获取 Service, Provider 在创建 ServiceA 前, 会先创建 ServiceB 并提供给 ServiceA Console.WriteLine(serviceA.GetValue()); } }
创建大合集 > 把所有 service 放入合集 > 创建 Provider > 通过 Provider 获取 Service.
Provider 的职责就是管理好依赖, 依据每个 class 的依赖去创建对象.
添加 Service 的细节 Singleton, Scoped, Transient
serviceCollection.AddSingleton<ServiceB, ServiceB>(); serviceCollection.AddScoped<ServiceB, ServiceB>(); serviceCollection.AddTransient<ServiceB, ServiceB>();
Singleton 就是单列, ServiceProvider 第一次创建对象后会把对象收起来, 往后都直接返回这个对象.
Scoped 也是单列, 但是它有一个范围. Singleton 的范围是整个 Application 结束都是单列 (用在 Web Application, 一个 request 就表示一个 Scope)
public static void Main(string[] args) { var serviceCollection = new ServiceCollection(); serviceCollection.AddScoped<ServiceA, ServiceA>(); serviceCollection.AddScoped<ServiceB, ServiceB>(); var serviceProvider = serviceCollection.BuildServiceProvider(); ServiceA serviceA; using (var scope = serviceProvider.CreateScope()) // 开启 scope { serviceA = scope.ServiceProvider.GetRequiredService<ServiceA>(); var serviceAA = scope.ServiceProvider.GetRequiredService<ServiceA>(); Console.WriteLine((serviceA == serviceAA).ToString()); // 在 scope 内是同一个对象 } var serviceAAA = serviceProvider.GetRequiredService<ServiceA>(); Console.WriteLine((serviceA == serviceAAA).ToString()); // scope 之外和 scope 内是不同的对象 }
Transient 表示每一次 ServiceProvider 都会创建新的实例.
什么时候应该使用什么呢? 参考: Stack Overflow – AddTransient, AddScoped and AddSingleton Services Differences
Interface Best Practice
上面的例子中, 直接提供了类型, 这个是不理想的, 我们应该要面向接口编程 (方便扩展, 维护)
serviceCollection.AddScoped<ServiceA, ServiceA>();
设计一个接口
然后使用接口声明
serviceCollection.AddScoped<IGetValue, ServiceA>();
serviceProvider.GetRequiredService<IGetValue>();
这样的好处就是, 以后可以替换具体实现. 只要实现了接口, 任何一个 class 都可以替换上去.
Multiple Provide
当同一个接口提供多个 Service 时, 可以通过 GetServices 获取到所有 services, identity 的 UserValidator 就用了这个方式
serviceCollection.AddScoped<IGetValue, ServiceC>(); serviceCollection.AddScoped<IGetValue, ServiceD>(); Console.WriteLine(string.Join(',', services.Select(s => s.GetType().Name))); // ServiceC,ServiceD
Create Instance with ServiceProvider
如果我们想通过反射实例化一个带有依赖的 class, 可以这么写
var instance = ActivatorUtilities.CreateInstance<AbcService>(serviceProvider);
把 ServiceProvider 丢进去就可以了. 内部会调用 GetRequiredService
Web Application 中使用 Dependency Injection
program.cs
var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); builder.Services.AddScoped<ServiceA, ServiceA>();
Razor Page or Controller
public class IndexModel : PageModel { public IndexModel( ServiceA serviceA // 可以通过 Constructor ) { } public void OnGet(ServiceA serviceA) // 可以通过 Parameter //public void OnGet([FromServices] ServiceA serviceA) // 在 .NET 7 之前需要加上 [FromServices] { } }
View
@page @model IndexModel @inject ServiceA ServiceA // 使用 @inject