這篇文章主要講解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被用來解析類型和注入依賴。
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-2.2