The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

如何在 .NET Core 主控台專案中使用 DI (相依注入) 並取得 ILogger 服務

速度快錯了嗎!疑?這篇文章的起頭好怪。因為今天在寫 .NET Core 程式的時候,一份很簡單的程式碼,卻怎樣都無法正確執行,嘗試了各種寫法還是鬼打牆,寫到有點懷疑人生。今天我就順便把 .NET Core 中設定 DI 與使用 ILogger 物件的過程詳細交代一遍,請看官務必看到最後喔!

以下過程我會用 .NET Core CLI 與 Visual Studio Code 進行示範。

建立 Console 主控台專案

由於我這邊因為已經安裝過 Visual Studio 2019 Preview 版本,而 .NET Core SDK 預設已經選用 2.2.200-preview-009648 版本,為了要示範操作,我打算先在當前目錄建立一個 global.json 全域檔案,限制當前目錄使用的 .NET Core SDK 版本。目前最新穩定版為 2.2.101 版,可以到這裡下載。

dotnet new globaljson --sdk-version 2.2.101

global.json 檔案內容如下:

{
  "sdk": {
    "version": "2.2.101"
  }
}

接著我們建立一個新的 Console 專案:

dotnet new console -n c1
cd c1

安裝 Logging 與 相依注入 相關套件

dotnet add package Microsoft.Extensions.Logging
dotnet add package Microsoft.Extensions.Logging.Console
dotnet add package Microsoft.Extensions.DependencyInjection

開啟 visual Studio Code 並自動設定開發環境

用 Visual Studio Code 開啟當前資料夾:

code .

記得讓 VSCode 的 C# 延伸模組自動設定你的專案:

建立 DI 容器

我們先用最簡單的方式,使用 .NET Core 內建的 ServiceCollection 來建立 DI 容器。設定前請記得引用 Microsoft.Extensions.DependencyInjectionMicrosoft.Extensions.Logging 命名空間:

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;

我們將會使用 Console 紀錄提供者 (logging provider) 來當範例,所以會透過 config.AddConsole() 來設定預設 Logger 會以 Console 來輸出紀錄:

var services = new ServiceCollection()
    .AddLogging(config => config.AddConsole())
    .BuildServiceProvider();

取得註冊在 DI 容器中的服務物件

由於我們已經把 Logging 服務註冊到 DI 容器中 (透過 .AddLogging() 方法),從 DI 容器中取出物件的方式如下:

var logger = services.GetRequiredService<ILogger<Program>>();

你也可以用 GetService() 來取得服務物件:

var logger = services.GetService<ILogger<Program>>();

上述兩種取得服務物件的方法,唯一的差異在於:

  • 使用 GetRequiredService() 如果找不到物件時,直接引發例外狀況。
  • 使用 GetService() 如果找不到物件時,會直接回傳 null 空值。

呼叫 ILogger 提供的 APIs

取得到 ILogger 服務物件後,我們就可以透過下列 API 進行追蹤紀錄:

  • logger.LogTrace(string)
  • logger.LogDebug(string)
  • logger.LogInformation(string)
  • logger.LogWarning(string)
  • logger.LogError(string)
  • logger.LogCritical(string)

假設我們這樣寫:

logger.LogCritical("Something Happen");

大功告成!就是這麼簡單!

完整的原始碼如下:

using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace c1
{
  class Program
  {
    static void Main(string[] args)
    {
      var services = new ServiceCollection()
        .AddLogging(config => config.AddConsole())
        .BuildServiceProvider();

      var logger = services.GetRequiredService<ILogger<Program>>();

      logger.LogCritical("Something Happen");

      Console.WriteLine("Hello World!");
    }
  }
}

進行夢靨般的測試

接下來,直接在 Visual Studio Code 中按下 F5 就可以立刻啟動程式!

這時你可以從「偵錯主控台」發現,疑?! 說好的 Log 訊息呢?是不是我哪裡沒設對?哪裡沒設好?

(... 接下來開始偵錯兩小時 ... 詭異 ... 鬱悶 ... 懷疑人生 ... 剁手 ...)

有趣的地方是,這個問題在我的電腦大概會有 1% 的機率會輸出 Something Happen,真是見鬼啦!

大家應該都有這種經驗,就是當程式怎樣都寫不出來的時候,就會開始亂寫、亂 try,那怕這一切是那麼的沒有邏輯!!

我教大家幾招:

  1. Main() 最後面加上 System.Threading.Thread.Sleep(0);
  2. Main() 最後面加上 System.Threading.Tasks.Task.Delay(1);
  3. Main() 最後面加上 Console.ReadLine();Console.ReadKey();

以上這些解法,當然是從網路上搜到的,但江湖傳說這麼多,你 (COPY) 對了嗎?!

問題發生的原因與解決方案

這個問題我花了一些時間搜尋解答,大部分找到的答案都不太滿意,解決方法都怪怪的。因為之前寫 ASP.NET Core 的時候從來沒遇到過,反而是在寫 Console 的時候遇到鬼。

最後找到這則討論串才驚覺,原來 Microsoft.Extensions.Logging.Console 套件中提供的 Console Logging Provider 為了效能考量,會在背景執行緒輸出 Log 訊息,如果你的應用程式 關閉 的太快,他會來不及輸出 Log 訊息到 Console 上,所以才會什麼都看不到!

微軟 .NET Core 技術團隊給出的建議是,建議你自己 Dispose (釋放) ServiceProvider 物件,當 ServiceProvider 物件被釋放的時候,所有在背景累積的訊息佇列(Message Queue)就會一口氣釋出到 Console 畫面上。當然,其他 Logging Provider 也會有相同的狀況,所以大家必須注意這枚地雷。

因此,正確的寫法,應該是在 Main() 最後面加上以下程式碼:

services.Dispose();

如果改用 using 語法,完整的程式碼如下:

using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace c1
{
  class Program
  {
    static void Main(string[] args)
    {
      using(var services = new ServiceCollection()
            .AddLogging(config => config.AddConsole())
            .BuildServiceProvider())
      {
        var logger = services.GetRequiredService<ILogger<Program>>();

        logger.LogCritical("Something Happen");

        Console.WriteLine("Hello World!");
      }
    }
  }
}

重構程式碼

我個人是喜愛 ASP.NET Core 所用的 Builder Pattern (建造者模式) 來對 DI 容器進行設定,關注點分離的特性比較清楚,而且程式碼也比較乾淨。

using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace c1
{
  class Program
  {
    static void Main(string[] args)
    {
      using(var svcs = ConfigureServices(new ServiceCollection()).BuildServiceProvider())
      {
        var logger = svcs.GetRequiredService<ILogger<Program>>();

        logger.LogCritical("Something Happen");

        Console.WriteLine("Hello World!");
      }
    }

    private static IServiceCollection ConfigureServices(IServiceCollection services)
    {
      return services
        .AddLogging(builder =>
        {
          builder.AddConsole();
        })
        .Configure<LoggerFilterOptions>(options =>
        {
          options.MinLevel = LogLevel.Information;
        });
    }
  }
}

執行結果如下:

 

相關連結

留言評論