﻿# 如何在 .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` 版，可以到[這裡](https://dot.net)下載。

```bash
dotnet new globaljson --sdk-version 2.2.101
```

其 `global.json` 檔案內容如下：

```json
{
  "sdk": {
    "version": "2.2.101"
  }
}
```

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

```bash
dotnet new console -n c1
cd c1
```

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

```bash
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 開啟當前資料夾：

```bash
code .
```

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

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/a290f2385edbc1e06556-image_3169.png)

### 建立 DI 容器

我們先用最簡單的方式，使用 .NET Core 內建的 [ServiceCollection](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicecollection) 來建立 DI 容器。設定前請記得引用 [Microsoft.Extensions.DependencyInjection](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection?view=aspnetcore-2.1) 與 [Microsoft.Extensions.Logging](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging) 命名空間：

```csharp
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
```

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

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

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

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

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

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

```csharp
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)`

假設我們這樣寫：

```csharp
logger.LogCritical("Something Happen");
```

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

完整的原始碼如下：

```csharp
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 訊息呢？是不是我哪裡沒設對？哪裡沒設好？

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/cc851e60505df74b86a4-image_3170.png)

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

有趣的地方是，這個問題在我的電腦大概會有 **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 的時候遇到鬼。

最後找到[這則討論串](https://github.com/aspnet/Logging/issues/631#issuecomment-306327773)才驚覺，原來 [Microsoft.Extensions.Logging.Console](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Console) 套件中提供的 Console Logging Provider 為了效能考量，會在背景執行緒輸出 Log 訊息，如果你的應用程式 **關閉** 的太快，他會來不及輸出 Log 訊息到 Console 上，所以才會什麼都看不到！

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

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

```csharp
services.Dispose();
```

如果改用 `using` 語法，完整的程式碼如下：

```csharp
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 容器進行設定，關注點分離的特性比較清楚，而且程式碼也比較乾淨。

```csharp
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;
        });
    }
  }
}
```

執行結果如下：

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/567608d96d5bb95e6840-image_3171.png)

### 相關連結

-   [Logging in ASP.NET Core | Microsoft Docs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/)
-   [ASP.NET Core 中的記錄 | Microsoft Docs](https://learn.microsoft.com/zh-tw/aspnet/core/fundamentals/logging/)
-   [Console logger from DI doesn't produce console output · Issue #631 · aspnet/Logging](https://github.com/aspnet/Logging/issues/631)
-   [The difference between GetService() and GetRequiredService() in ASP.NET Core](https://andrewlock.net/the-difference-between-getservice-and-getrquiredservice-in-asp-net-core/)
