ASP.NET Core 練習 - 依賴注入 DI
4 |
先前 ViewComponent 範例程式為求單純避免失焦,有幾段程式寫法不符合 ASP.NET Core 規範留下小尾巴,現在再來收拾它。
幾個明顯問題包含:
- 在 ASP.NET Core 中使用服務(如:SimpleWeatherService)應採用依賴注入 (Dependency Injection, DI) 機制,不建議直接 new SimpleWeatherService
(延伸閱讀:不可不知的 ASP.NET Core 依賴注入) - OpenData API URL 寫死在程式,移至 appSetting 才是正確姿勢
- 方法呼叫應採非同步方式(例如:Invoke() 應改成 InvokeAsync()。像 HttpClient 本質都是走非同步,配合 Invoke() 要寫成
httpClient.GetAsync(openDataApiUrl).Result.Content.ReadAsStringAsync().Result;
轉得蠻硬。
(延伸閱讀:ASP.NET async 基本心法)
讓我們來動手改造它。
首先看如何將 SimpleWeatherService 改為依賴注入,原本在 WeatherBlockViewComponent.Invoke() 是用 new SimpleWeatherService() 建立物件並呼叫 GetWeatherFromOpenDataApi() 方法。為了除去對 SimpleWetherService 的依賴,我們另外定義一個提供 GetWeatherData(string zoneName) 的介面 - IWeatherService,在 WeatherBlockViewComponent 改使用 IWeatherService 而非 SimpleWeatherService,未來如果要從 OpenData Web API 換成其他資料來源,WeatherBlockViewComponent 可以不需要修改。WeatherData 原本是 SimpleWeatherService 的子類別,配合抽出介面要拉出來成為為獨立類別。(註:抽取介面請善用 Visual Studio 的擷取介面重構功能,不要傻傻自己寫)
public class WeatherData
{
public string ZoneName { get; set; }
public string Status { get; set; }
public string MaxTemp { get; set; }
public string MinTemp { get; set; }
}
public interface IWeatherService
{
WeatherData GetWeatherData(string zoneName);
}
SimpleWeatherService 繼承 IWeatherService,實作 GetWeatherData(string zoneName),底層仍呼叫 GetWeatherFromOpenDataApi(zoneName) 取得資料:(由於 WeatherData 從 SimpleWeatherService 的子類別拉高到與 SimpleWeatherService 平行,Views/Shared/Components/WeatherBlock/Default.html 的 Model 型別要記得修改)
public class SimpleWeatherService : IWeatherService
{
public WeatherData GetWeatherData(string zoneName)
{
return GetWeatherFromOpenDataApi(zoneName);
}
//...其餘部分不變...
WeatherBlockViewComponent 則改用 IWeatherService 並透過 DI 容器取得物件,ASP.NET Core 的標準做法是改由建構式接收 IWeatherService 型別參數保存,將 IWeatherService 實體物件要用哪一種、如何產生則全權交由 ASP.NET Core 的 DI 機制管理:
public class WeatherBlockViewComponent : ViewComponent
{
readonly IWeatherService _weatherService;
public WeatherBlockViewComponent(IWeatherService weatherService)
{
_weatherService = weatherService;
}
public IViewComponentResult Invoke(string zoneName)
{
var data = _weatherService.GetWeatherData(zoneName);
return View(data);
}
}
改完後程式會出錯:
這是因為我們還沒向 ASP.NET Core 註冊 IWeatherService 服務,DI 機制不知道如何變出 IWeatherService 給 WeatherBlockViewComponent 用。
ASP.NET Core 的服務註冊要寫在 Startup.cs 的 ConfigureServices() 方法,我們加入一行 services.AddTransient<SimpleWeatherService>()
: (依據物件生命周期不同,註冊時有 Singleton、Scoped、Transient 三種選擇,詳情請參考不可不知的 ASP.NET Core 依賴注入)
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IWeatherService, SimpleWeatherService>();
services.AddControllersWithViews();
}
註冊後,程式顯示結果相同,顯示台北、南投、高雄三地的氣象,但底層已經改用 DI 方式決定天氣資訊來源。接著來看看改成 DI 有什麼優點? 假設我們想測試特定文字、數值的呈現效果,則可以做一顆模擬資料的 IWeatherService 類別 - FakeWeatherService:
public class FakeWeatherService : IWeatherService
{
public WeatherData GetWeatherData(string zoneName)
{
return new WeatherData
{
ZoneName = zoneName,
Status = "冰雹龍捲風",
MinTemp = "-18",
MaxTemp = "38"
};
}
}
下一步,我們將 Startup.ConfigureServices() 的註冊部分改成 services.AddTransient<IWeatherService, FakeWeatherService>();
,其餘程式不用修改,網頁變成:
提取介面並使用 DI 容器,我們可以在幾乎不修改程式的前題下輕鬆抽換底層實作,寫出結構分明容易維護的好程式。ASP.NET Core 的範例程式幾乎都已採行這種做法,大家在寫 ASP.NET Core 網站時就從善如流吧!
Example of using DI to inject service in ASP.NET Core.
Comments
# by Steve
大大你好,我是.net core 使用新手,想請教一個問題, 如果同一個interface想同時在Startup.ConfigureServices()註冊多個實例, 要怎麼在DI同時注入同一個interface的不同的實例
# by Jeffrey
to Steve,不太明白。DI 的情境多半是讓呼叫端只需指定 Interface 即可取得你事先註冊的實例,如果註冊多個,要用什麼方式決定使用哪一個?
# by azurestars
假設同時註冊三個實例 public void ConfigureServices(IServiceCollection services) { services.AddScoped<IDItestService, DItestServiceA>(); services.AddScoped<IDItestService, DItestServiceB>(); services.AddScoped<IDItestService, DItestServiceC>(); } 介面中有個方法 public string GetValue() 都只簡單吐回 "DItestServiceA" 、 "DItestServiceB" 、 "DItestServiceC" ====================================================== 一般注入時 private readonly IDItestService _dItestService; public TestController(IDItestService dItestService) { _dItestService = dItestService; } 此時 _dItestService.GetValue() 會是最後一個註冊的 DItestServiceC ====================================================== 要使用多個實例時、可以改成 private readonly IEnumerable<IDItestService> _dItestService; public TestController(IEnumerable<IDItestService> dItestService) { _dItestService = dItestService; } 此時 可以取取得各種實作 foreach (var item in _dItestService) { Console.Write(item.GetValue()); } 或者指定使用B實作 var itemB =_dItestService.FirstOrDefault(x => x.GetType().Name == "DItestServiceB"); Console.Write(itemB.GetValue()); ------------ 以上是我所知,或許還有其他方法~
# by RH999
to Steve,可以更換依賴注入容器,例如AutoFac,其支持命名服務的功能。