asp.net core 系列之Dependency injection(依賴注入)
這篇文章主要講解asp.net core 依賴注入的一些內容。
ASP.NET Core支援依賴注入。這是一種在類和其依賴之間實現控制反轉的一種技術(IOC).
一.依賴注入概述
1.原始的程式碼
依賴就是一個物件的建立需要另一個物件。下面的MyDependency是應用中其他類需要的依賴:
public class MyDependency { public MyDependency() { } public Task WriteMessage(string message) { Console.WriteLine( $"MyDependency.WriteMessage called. Message: {message}"); return Task.FromResult(0); } }
一個MyDependency類被建立使WriteMessage方法對另一個類可用。MyDependency類是IndexModel類的依賴(即IndexModel類的建立需要用到MyDependency類):
public class IndexModel : PageModel { MyDependency _dependency = new MyDependency(); public async Task OnGetAsync() { await _dependency.WriteMessage( "IndexModel.OnGetAsync created this message."); } }
2.原始程式碼分析
IndexModel類建立了MyDependency類,並且直接依賴MyDependency例項。上面的程式碼依賴是有問題的,並且應該被避免(避免直接建立依賴的例項物件),
原因如下:
- 需要用一個不同的實現來替換MyDependency,這個類必須被修改
- 如果MyDependency有依賴,他們必須被這個類配置。在一個有很多類依賴MyDependency的大的專案中,配置程式碼在應用中會很分散。
- 這種實現對於單元測試是困難的。對於MyDependency,應用應該使用mock或者stub,用這種方式是不可能的。
依賴注入解決那些問題:
- 介面的使用抽象了依賴的實現
- 在service container註冊依賴。ASP.NET Core提供了一個內建的service container, IServiceProvider. Services是在應用的Startup.ConfigureServices中被註冊。
- 一個類是在建構函式中注入service。框架執行著建立一個帶依賴的例項的責任,並且當不需要時,釋放。
3.下面是改良後的程式碼
這示例應用中,IMyDependency介面定義了一個方法:
public interface IMyDependency { Task WriteMessage(string message); }
介面被一個具體的型別,MyDependency實現:
public class MyDependency : IMyDependency { private readonly ILogger<MyDependency> _logger; public MyDependency(ILogger<MyDependency> logger) { _logger = logger; } public Task WriteMessage(string message) { _logger.LogInformation( "MyDependency.WriteMessage called. Message: {MESSAGE}", message); return Task.FromResult(0); } }
在示例中,IMydependency例項被請求和用於呼叫服務的WriteMessage方法:
public class IndexModel : PageModel { private readonly IMyDependency _myDependency; public IndexModel( IMyDependency myDependency, OperationService operationService, IOperationTransient transientOperation, IOperationScoped scopedOperation, IOperationSingleton singletonOperation, IOperationSingletonInstance singletonInstanceOperation) { _myDependency = myDependency; OperationService = operationService; TransientOperation = transientOperation; ScopedOperation = scopedOperation; SingletonOperation = singletonOperation; SingletonInstanceOperation = singletonInstanceOperation; } public OperationService OperationService { get; } public IOperationTransient TransientOperation { get; } public IOperationScoped ScopedOperation { get; } public IOperationSingleton SingletonOperation { get; } public IOperationSingletonInstance SingletonInstanceOperation { get; } public async Task OnGetAsync() { await _myDependency.WriteMessage( "IndexModel.OnGetAsync created this message."); } }
4.改良程式碼分析及擴充套件講解(使用DI)
MyDependency在建構函式中,要求有一個ILogger<TCategoryName>。用一種鏈式的方法使用依賴注入是很常見的。每個依賴依次再請求它自己需要的依賴。(即:MyDependency是一個依賴,同時,建立MyDependency又需要其他依賴:ILogger<TCategoryName>。)
IMyDependency和ILogger<TCategoryName>必須在service container中註冊。IMyDependency是在Startup.ConfigureServices中註冊。ILogger<TCategoryName>是被logging abstractions infrastructure註冊,所以它是一種預設已經註冊的框架提供的服務。(即框架自帶的已經註冊的服務,不需要再另外註冊)
容器解析ILogger<TCategoryName>,通過利用泛型. 消除註冊每一種具體的構造型別的需要。(因為在上面的例子中,ILogger中的泛型型別為MyDependency,但是如果在其他類中使用ILogger<>, 型別則是其他型別,這裡使用泛型比較方便)
services.AddSingleton(typeof(ILogger<T>), typeof(Logger<T>));
這是它的註冊的語句(框架實現的),其中的用到泛型,而不是一種具體的型別。
在示例應用中,IMyDependency service是用具體的型別MyDependency來註冊的。這個註冊包括服務的生命週期(service lifetime)。Service lifetimes隨後會講。
如果服務的建構函式要求一個內建型別,像string,這個型別可以被使用configuration 或者options pattern來注入:
public class MyDependency : IMyDependency { public MyDependency(IConfiguration config) { var myStringValue = config["MyStringKey"]; // Use myStringValue } ... }
或者 options pattern(注意:不止這些,這裡簡單舉例)
二.框架提供的服務(Framework-provided services)
Startup.ConfigureServices方法有責任定義應用使用的服務,包括平臺功能,例如Entity Framework Core和ASP.NET Core MVC。最初,IServiceColletion提供給ConfigureServices下面已經定義的服務(依賴於怎樣配置host):
當一個service colletion 擴充套件方法可以用來註冊一個服務,習慣是用一個單獨的Add{SERVICE_NAME} 擴充套件方法來註冊服務所需要的所有服務。下面的程式碼是一個怎麼使用擴充套件方法AddDbContext, AddIdentity,和AddMvc, 新增額外的服務到container:
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); services.AddMvc(); }
更多的資訊:ServiceCollection Class
三. 服務生命週期(Service lifetimes)
為每個註冊的服務選擇一個合適的生命週期。ASP.NET Core服務可以用下面的宣告週期配置:
Transient、Scoped、Singleton
Transient(臨時的)
臨時的生命週期服務是在每次從服務容器中被請求時被建立。這個生命週期對於lightweight(輕量的),stateless(無狀態的)服務比較合適。
Scoped(範圍)
範圍生命週期被建立,一旦每個客戶端請求時(connection)
警告:當在中介軟體中使用範圍服務時,注入服務到Invoke或者InvokeAsync方法。不要通過建構函式注入,因為那回強制服務表現的像是singleton(單例)。
Singleton(單獨)
單獨生命週期在第一次請求時被建立(或者說當ConfigureService執行並且被service registration指定時)。之後每一個請求都使用同一個例項。如果應用要求一個單獨行為(singleton behavior),允許service container來管理服務生命週期是被推薦的。不要實現一個單例設計模式並且在類中提供使用者程式碼來管理這個物件的生命週期。
警告:從一個singleton來解析一個範圍服務(scoped service)是危險的。它可能會造成服務有不正確的狀態,當處理隨後的請求時。
建構函式注入行為
服務可以被通過兩種機制解析:
- IServiceProvider
- ActivatorUtilities : 允許物件建立,可以不通過在依賴注入容器中注入的方式。ActivatorUtilities是使用user-facing abstractions,例如Tag Helpers , MVC controllers 和 model binders.
建構函式可以接受引數,不通過依賴注入提供,但是這些引數必須指定預設值。
當服務被通過IServiceProvider或者ActivatorUtilities解析時,建構函式注入要求一個公共的建構函式。
當服務被ActivatorUtilities解析時,建構函式注入要求一個合適的建構函式存在。建構函式的過載是被支援的,但是隻有一個過載可以存在,它的引數可以被依賴注入執行(即:可以被依賴注入執行的,只有一個建構函式的過載)。
四. Entity Framework contexts
Entity Framework contexts 通常使用scoped lifetime ,新增到服務容器中(service container).因為web 應用資料庫操作的範圍適用於client request(客戶端請求)。預設的生命週期是scoped,如果一個生命週期沒有被AddDbContext<TContext>過載指定,當註冊database context時。給出生命週期的服務不應該使用一個生命週期比服務的生命週期短的database context.
五.Lifetime and registration options
為了說明lifetime和registration options之間的不同,考慮下面的介面:這些介面表示的任務都是帶有唯一標識的操作。取決於這些介面的操作服務的生命週期怎麼配置,container提供了要麼是同一個要麼是不同的服務當被一個類請求時:
public interface IOperation { Guid OperationId { get; } } public interface IOperationTransient : IOperation { } public interface IOperationScoped : IOperation { } public interface IOperationSingleton : IOperation { } public interface IOperationSingletonInstance : IOperation { }
這些介面在一個Operation類中被實現。Operation 建構函式生成了一個GUID,如果GUID沒被提供:
public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton, IOperationSingletonInstance { public Operation() : this(Guid.NewGuid()) { } public Operation(Guid id) { OperationId = id; } public Guid OperationId { get; private set; } }
OperationService依賴於其他的Operation 型別被註冊。當OperationService被通過依賴注入請求,它要麼接收每個服務的一個新例項要麼接收一個已經存在的例項(在依賴服務的生命週期的基礎上)。
- 當臨時服務(transient services)被建立時,當被從容器中請求時,IOperationTransient服務的OperationId是不同的。OperationService接收到一個IOperationTransient類的例項。這個新例項產生一個不同的OperationId.
- 每個client請求時,scoped services被建立,IOperationScoped service的OperationId是一樣的,在一個client request內。跨越client requests,兩個service享用一個不同的OperationId的值。
- 當singleton和singleton-instance服務一旦被建立,並且被使用跨越所有的client requests和所有的服務,則OperationId跨越所有的service requests是一致的。
public class OperationService { public OperationService( IOperationTransient transientOperation, IOperationScoped scopedOperation, IOperationSingleton singletonOperation, IOperationSingletonInstance instanceOperation) { TransientOperation = transientOperation; ScopedOperation = scopedOperation; SingletonOperation = singletonOperation; SingletonInstanceOperation = instanceOperation; } public IOperationTransient TransientOperation { get; } public IOperationScoped ScopedOperation { get; } public IOperationSingleton SingletonOperation { get; } public IOperationSingletonInstance SingletonInstanceOperation { get; } }
在Startup.ConfigureServices中,每個型別根據命名的生命週期被新增到容器中:
public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); services.AddScoped<IMyDependency, MyDependency>(); services.AddTransient<IOperationTransient, Operation>(); services.AddScoped<IOperationScoped, Operation>(); services.AddSingleton<IOperationSingleton, Operation>(); services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty)); // OperationService depends on each of the other Operation types. services.AddTransient<OperationService, OperationService>(); }
IOperationSingletonInstance服務是一個特殊的例項,它的ID是Guid.Empty. 它是清楚的,當這個型別被使用(它的GUID都是0組成的)
示例應用說明了requests內的物件生命週期和兩個requests之間的物件生命週期。示例應用的IndexModel請求IOperation的每個型別和OperationService。這個頁面展示了所有的這個page model類的和服務的OperationId值,通過屬性指定。
public class IndexModel : PageModel { private readonly IMyDependency _myDependency; public IndexModel( IMyDependency myDependency, OperationService operationService, IOperationTransient transientOperation, IOperationScoped scopedOperation, IOperationSingleton singletonOperation, IOperationSingletonInstance singletonInstanceOperation) { _myDependency = myDependency; OperationService = operationService; TransientOperation = transientOperation; ScopedOperation = scopedOperation; SingletonOperation = singletonOperation; SingletonInstanceOperation = singletonInstanceOperation; } public OperationService OperationService { get; } public IOperationTransient TransientOperation { get; } public IOperationScoped ScopedOperation { get; } public IOperationSingleton SingletonOperation { get; } public IOperationSingletonInstance SingletonInstanceOperation { get; } public async Task OnGetAsync() { await _myDependency.WriteMessage( "IndexModel.OnGetAsync created this message."); } }
下面的輸出展示了兩個請求的結果:
從結果看出:
- Transient物件總是不同的。Transient OperationId的值對於第一個和第二個客戶端請求是在OperationService中不同的,並且跨越client requests. 一個新的例項被提供給每個service request和client request.
- Scoped物件對於一個client request內部是一樣的,跨越client request是不同的。
- Singleton物件對於每個物件和每個請求都是一樣的,不管Operation例項是否在ConfigureServices中被提供了。
可以看出,Transient一直在變;Scoped 同一個client request請求內不變;Singleton一直不變;
六. Call Services from main(在main中呼叫services)
用IServiceScopeFactory.CreateScope建立一個IServiceScope 來解析一個scoped service在應用的範圍內。這個方式是有用的對於在Startup中得到一個scoped service 來執行初始化任務。下面的例子展示了MyScopedServcie怎樣包含一個context,在Program.Main中:
public static void Main(string[] args) { var host = CreateWebHostBuilder(args).Build(); using (var serviceScope = host.Services.CreateScope()) { var services = serviceScope.ServiceProvider; try { var serviceContext = services.GetRequiredService<MyScopedService>(); // Use the context here } catch (Exception ex) { var logger = services.GetRequiredService<ILogger<Program>>(); logger.LogError(ex, "An error occurred."); } } host.Run(); }
七.Scope validation(範圍驗證)
當應用在開發環境執行時,預設的service provider 執行檢查來驗證:
- Scoped services不是直接或間接的被從root service provider中解析
- Scoped services 不是直接或間接的被注入為singletons
root service provider 是當BuildServiceProvider被呼叫時被建立的。Root service provider的生命週期對應於應用/伺服器 的生命週期,當provider隨著應用啟動並且當應用關閉時會被釋放。
Scoped服務被建立它們的容器釋放。如果scoped service在root container中被建立,服務的生命週期實際上是被提升為singleton,因為它只有當應用或者伺服器關閉時才會被root container釋放。驗證servcie scopes 注意這些場景,當BuildServiceProvider被呼叫時。
八.Request Services
來自HttpContext的ASP.NET Core request中的可用的services通過HttpContext.RequestServices集合來暴露。
Request Services代表應用中被配置的services和被請求的部分。當物件指定依賴,會被RequestService中的型別滿足,而不是ApplicationServices中的。
通常,應用不應該直接使用那些屬性。相反的,請求滿足那個型別的的這些類,可以通過建構函式並且允許框架注入這些依賴。這使類更容易測試。
注意:請求依賴,通過建構函式引數來得到RequestServices集合更受歡迎。
九. Design services for dependency injection
最佳實踐:
- 設計services使用依賴注入來包含它們的依賴
- 避免stateful,靜態的方法呼叫
- 避免在services內直接初始化依賴類。直接初始化是程式碼關聯一個特定的實現
- 使應用的類small, well-factored,和easily tested.
如果一個類似乎有很多注入的依賴,這通常是它有太多職責的訊號,並且違反了Single Responsibility Principle(SRP)單一職責原則。嘗試通過移動一些職責到一個新類來重構這個類。記住,Razor Pages page model classes和MVC controller classes應該專注於UI層面。Business rules和data access implementation細節應該在那些合適的分開的關係的類中。
Disposal of services
容器為它建立的類呼叫IDisposable的Dispose。如果一個例項被使用者程式碼新增到容器中,它不會自動釋放。
// Services that implement IDisposable: public class Service1 : IDisposable {} public class Service2 : IDisposable {} public class Service3 : IDisposable {} public interface ISomeService {} public class SomeServiceImplementation : ISomeService, IDisposable {} public void ConfigureServices(IServiceCollection services) { // The container creates the following instances and disposes them automatically: services.AddScoped<Service1>(); services.AddSingleton<Service2>(); services.AddSingleton<ISomeService>(sp => new SomeServiceImplementation()); // The container doesn't create the following instances, so it doesn't dispose of // the instances automatically: services.AddSingleton<Service3>(new Service3()); services.AddSingleton(new Service3()); }
即,如果,類是被使用者程式碼新增容器中的,不會自動釋放。像下面這種直接new類的。
十.Default service container replacement
內建的service container意味著提供服務來滿足框架和大多消費應用的需求。我們建議使用功能內建容器,除非你需要一個特殊的功能,內建容器不支援。有些功能在第三方容器支援,但是內建容器不支援:
- Property injection
- Injection based on name
- Child containers
- Custom lifetime management
- Fun<T> support for lazy initializtion
下面的示例,使用Autofac替代內建容器:
- 安裝合適的容器包:
-
- Autofac
-
- Autofac.Extensions.DependencyInjection
- 在Startup.ConfigureServices中配置容器,並且返回IServiceProvider:
public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddMvc(); // Add other framework services // Add Autofac var containerBuilder = new ContainerBuilder(); containerBuilder.RegisterModule<DefaultModule>(); containerBuilder.Populate(services); var container = containerBuilder.Build(); return new AutofacServiceProvider(container); }
要使用第三方容器,Startup.ConfigureServices必須返回IServiceProvider.
- 在DefaultModule中配置Autofac
public class DefaultModule : Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType<CharacterRepository>().As<ICharacterRepository>(); } }
在執行時,Autofac被用來解析型別和注入依賴。
更多: Autofac documentation.
Thread safety
建立執行緒安全的單例服務。如果一個單例服務對一個臨時的服務有依賴,這個臨時的服務可能需要要求執行緒安全根據它怎樣被單例服務使用。
單例服務的工廠方法,例如AddSingleton<TService>(IServiceColletion, Func<IServiceProvider, TService>)的第二個引數,不需要執行緒安全。像一個型別的建構函式,它一次只能被一個執行緒呼叫。
十一.Recommendations
- Async/await 和 Task 依據service resolution(服務解決)是不支援的。C# 不支援非同步的建構函式;因此,推薦的模式是在同步解析服務之後使用非同步方法。
- 避免直接在service container中儲存資料和配置。例如,使用者的購物車不應該被新增到service container. 配置應該使用option pattern. 相似的,避免data holder物件可接近其他物件。最好是請求實際的item通過DI.
- 避免靜態得到services(例如,靜態型別IApplicationBuilder.ApplicationServices的在別處的使用)
- 避免使用service locator pattern. 例如,當你可以用DI時,不要用GetService來獲取一個服務。
錯誤的:
public void MyMethod() { var options = _services.GetService<IOptionsMonitor<MyOptions>>(); var option = options.CurrentValue.Option; ... }
正確的:
private readonly MyOptions _options; public MyClass(IOptionsMonitor<MyOptions> options) { _options = options.CurrentValue; } public void MyMethod() { var option = _options.Option; ... }
- 另一個service locator 變數要避免,是注入一個在執行時解析依賴的工廠。那些實踐的兩者都混合了Inversion of Control策略(即避免依賴注入和其他方式混合使用)。
- 避免靜態得到HttpContext(例如,IHttpContextAccessor.HttpContext)
有時候的場景,可能需要忽略其中的建議。
DI是static/global object access patterns的可替代方式。如果你把它和static object access 方式混合使用,可能不能認識到DI的好處。
參考網址:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore