.NET Core 的相依性注入的 服務定位器 Service Locator ,如何使用 ServiceProvider 教學
在 .NET Core 開發框架下,微軟 Microsoft 有提供一個 Microsoft.Extensions.DependencyInjection NuGet 套件,這提供了一個 DI / IoC Container 相依性注入/相依反轉 容器類別庫,雖然這個 DI 容器並沒有提供如同其他 DI 容器的多樣性功能,但是對於要使用相依性注入的開發專案上,已經相當充分與足夠了。
對於相依性注入容器的主要功能,是要能夠提供 3R ,也就是 Register 註冊、解析 Resolve、釋放 Release ,這三大功能。
當建立一個 ASP.NET Core 專案,可以從 Startup.cs 類別中,看到
public void ConfigureServices(IServiceCollection services)
這個方法內,提供了 IServiceCollection 具體實作的物件,開發人員可以透過 IServiceCollection 物件來針對 DI Container 容器來進行抽象型別與具體實作型別(或者只針對具體實作類別) 來進行註冊 Registration。
這篇文章的範例程式碼可以從 GitHub 取得
現在,使用 Visual Studio 2019 建立一個 ASP.NET Coer Web API 類型的專案,接著,打開 Startup.cs 這個檔案,在這裡建立一個新的介面 IMessage 與這個介面的具體實作類別 ConsoleMessage ,如下面的程式碼。
public interface IMessage
{
string Write(string message);
}
public class ConsoleMessage : IMessage
{
public string Write(string message)
{
string result = $"[Console 輸出] {message}";
Console.WriteLine(result);
return result;
}
}
接下來將需要使用 MS.DI 這個套件,做到服務定位器的功能,也就是說希望能夠透過服務定位器這個物件,進行解析出 IMessage 這個介面所需要用到的具體實作物件。現在需要在 Startup.ConfigureServices 方法內,透過 IServiceCollection services 參數,來進行這個介面與具體實作類別的註冊,也就是要執行
services.AddTransient<IMessage, ConsoleMessage>();
這個方法,這裡使用了底下的程式碼。public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddTransient<IMessage, ConsoleMessage>();
}
請在 Startup 類別內,找到 Startup.Configure 方法,
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
,透過注入的 IApplicationBuilder app 這個物件,裡面有個 ApplicationServices 屬性,這個屬性的型別為 IServiceProvider,在這裡請將這個屬性值指定到新設定的靜態變數 public static IServiceProvider serviceProvider;
內,使用這個敘述 serviceProvider = app.ApplicationServices;
public static IServiceProvider serviceProvider;
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
serviceProvider = app.ApplicationServices;
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc();
}
為了要能夠使用 DI 容器來進行要注入的型別解析需求,所以,需要取得 IServiceProvider 這個介面的實作物件;不過,要取得這個 IServiceProvider 物件,可以從不同的地方取得,那麼,從不同地方取得的 IServiceProvider 物件,表現上是有什麼不同呢?現在,可以打開 Controllers 資料夾上的 ValuesController.cs 這個檔案,填入底下的程式碼。
在這個 ValuesController 控制器中,將會從底下幾個地方,取得 IServiceProvider:
- 建構式函式來注入 IServiceProvider serviceProvider
- 在 Get 方法中,使用 FromServices 屬性,注入 IServiceProvider serviceProvider
- 在上面的說明內容,有實作出 IApplicationBuilder.ApplicationServices 這個屬性值,並且存放到 Startup.serviceProvider 這個靜態屬性上
- 最後,透過這個 Action 動作方法中,可以使用 HttpContext.RequestServices 取得 IServiceProvider 這個物件
現在,先來看看這個 Get 動作程式碼產生什麼樣的輸出結果;從這個輸出結果可以看出兩件事情。
- 對於 IApplicationBuilder.ApplicationServices 得到的 ServiceProvider 物件 (34921712),與另外三個管道所取得的 ServiceProvider 的物件 (39157888) 是不同的。
- 不管是哪個 ServiceProvider 物件,都可以使用 IServiceProvider.GetService
() 這個敘述,來取得 IMessage 這個介面所註冊的對應具體實作類別的物件。因為此次註冊的介面與類別,使用的是 AddTransient ,所以,可以看到這裡將會得到不同的 IMessage 實作物件,分別是 : 27973187 , 50833863 , 5822459 , 60684095
[
"從 IApplicationBuilder.ApplicationServices 取得的 ServiceProvider 34921712",
"從 IApplicationBuilder.ApplicationServices 解析出 IMessage 27973187",
"建構式注入取得的 ServiceProvider 39157888",
"從 建構式注入取得的 ServiceProvider 解析出 IMessage 50833863",
"使用 FromServices 屬性取得的 ServiceProvider 39157888",
"使用 FromServices 屬性取得的 ServiceProvider 解析出 IMessage 5822459",
"從 RequestServices 取得的 ServiceProvider 39157888",
"從 RequestServices 取得的 ServiceProvider 解析出 IMessage 60684095"
]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private readonly IServiceProvider serviceProvider;
public ValuesController(IServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider;
}
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get([FromServices] IServiceProvider actionServiceProvider)
{
List<string> result = new List<string>();
result.Add($"從 IApplicationBuilder.ApplicationServices 取得的 ServiceProvider {Startup.serviceProvider.GetHashCode().ToString()}");
result.Add($"從 IApplicationBuilder.ApplicationServices 解析出 IMessage {Startup.serviceProvider.GetService<IMessage>().GetHashCode().ToString()}");
result.Add($"建構式注入取得的 ServiceProvider {serviceProvider.GetHashCode().ToString()}");
result.Add($"從 建構式注入取得的 ServiceProvider 解析出 IMessage {serviceProvider.GetService<IMessage>().GetHashCode().ToString()}");
result.Add($"使用 FromServices 屬性取得的 ServiceProvider {actionServiceProvider.GetHashCode().ToString()}");
result.Add($"使用 FromServices 屬性取得的 ServiceProvider 解析出 IMessage {actionServiceProvider.GetService<IMessage>().GetHashCode().ToString()}");
result.Add($"從 RequestServices 取得的 ServiceProvider {HttpContext.RequestServices.GetHashCode().ToString()}");
result.Add($"從 RequestServices 取得的 ServiceProvider 解析出 IMessage {HttpContext.RequestServices.GetService<IMessage>().GetHashCode().ToString()}");
return result;
}
// GET api/values/5
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
return "value";
}
// POST api/values
[HttpPost]
public void Post([FromBody] string value)
{
}
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody] string value)
{
}
// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
接下來,在 Startup.ConfigureServices 方法內,將 AddTransient 改成 AddSingleton 這個方法,也就是說,當要進行 IMessage 介面的具體實作物件注入的時候,使用 Singleton 的注入方式。
現在,先來看看這個 Get 動作程式碼產生什麼樣的輸出結果;從這個輸出結果可以看出兩件事情。
- 對於 IApplicationBuilder.ApplicationServices 得到的 ServiceProvider 物件 (48948582),與另外三個管道所取得的 ServiceProvider 的物件 (28785966) 是不同的。
- 不管是哪個 ServiceProvider 物件,使用 IServiceProvider.GetService
() 這個敘述,來取得 IMessage 這個介面所註冊的對應具體實作類別的物件,都是同一個物件 (53046438)
[
"從 IApplicationBuilder.ApplicationServices 取得的 ServiceProvider 48948582",
"從 IApplicationBuilder.ApplicationServices 解析出 IMessage 53046438",
"建構式注入取得的 ServiceProvider 28785966",
"從 建構式注入取得的 ServiceProvider 解析出 IMessage 53046438",
"使用 FromServices 屬性取得的 ServiceProvider 28785966",
"使用 FromServices 屬性取得的 ServiceProvider 解析出 IMessage 53046438",
"從 RequestServices 取得的 ServiceProvider 28785966",
"從 RequestServices 取得的 ServiceProvider 解析出 IMessage 53046438"
]
最後,在 Startup.ConfigureServices 方法內,將 AddSingleton 改成 AddScoped 這個方法,也就是說,當要進行 IMessage 介面的具體實作物件注入的時候,若在同一個 Request 下,所注入的 IMessage 物件,就像是 Singleton 的用法,像是使用 Singleton 的注入方式。
現在,先來看看這個 Get 動作程式碼產生什麼樣的輸出結果;從這個輸出結果可以看出兩件事情。
現在,當執行到
result.Add($"從 IApplicationBuilder.ApplicationServices 解析出 IMessage {Startup.serviceProvider.GetService<IMessage>().GetHashCode().ToString()}");
敘述之後,竟會得到底下的例外異常訊息:System.InvalidOperationException: 'Cannot resolve scoped service 'CoreServiceLocator.IMessage' from root provider.'
所以,請把這個 Get 動作內的這兩行敘述先註解起來,然後重新執行一次。
// result.Add($"從 IApplicationBuilder.ApplicationServices 取得的 ServiceProvider {Startup.serviceProvider.GetHashCode().ToString()}");
// result.Add($"從 IApplicationBuilder.ApplicationServices 解析出 IMessage {Startup.serviceProvider.GetService<IMessage>().GetHashCode().ToString()}");
- 現在已經把 IApplicationBuilder.ApplicationServices 的執行程式碼移除了,而透過另外三個管道所取得的 ServiceProvider 的物件 (59274039) 還是都相同的。
- 不管是哪個 ServiceProvider 物件,使用 IServiceProvider.GetService
() 這個敘述,來取得 IMessage 這個介面所註冊的對應具體實作類別的物件,都是同一個物件 (1177678)
若在同一個 Request 下所做的 Scope 注入行為,就類似於 Singleton 的註冊行為。
[
"建構式注入取得的 ServiceProvider 59274039",
"從 建構式注入取得的 ServiceProvider 解析出 IMessage 1177678",
"使用 FromServices 屬性取得的 ServiceProvider 59274039",
"使用 FromServices 屬性取得的 ServiceProvider 解析出 IMessage 1177678",
"從 RequestServices 取得的 ServiceProvider 59274039",
"從 RequestServices 取得的 ServiceProvider 解析出 IMessage 1177678"
]