先前 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,其支持命名服務的功能。

Post a comment