依賴注入(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>();
});
}