顯示具有 ASPNET Core 標籤的文章。 顯示所有文章
顯示具有 ASPNET Core 標籤的文章。 顯示所有文章

2023年2月8日 星期三

動手練習 ASP.NET Core7 用強型別來讀取設定練習

動手練習 ASP.NET Core7 用強行別來讀取設定練習

最近在進行 ASP.NET Core 7 專案開發的時候,突然遇到一個需要,那就是在 [Program.cs] 這個檔案內,進行使用 [build.Services] 這個屬性要進行 DI / IoC 容器注入的時候,卻想要讀取 ASP.NET Core 的設定 Configuration 內提供的相關設定值的時候,卻又想要使用強型別的方式,將一個區段內的設定值,直接全部讀取出來,並且儲存到一個物件內,這樣就會免除了需要逐一透過 [Configuration] 物件來取得這些物件值的麻煩程式碼,也會造成神奇字串輸入錯誤所造成的額外副作用影響問題。

會有這樣的需求,是因為要進行 JWT 的需求設計,而在 [Program.cs] 內,需要使用 [builder.Services.AddAuthentication().AddJwtBearer] 來進行 JWT 服務的宣告,然而,在這個 [AddJwtBearer] 方法內,將會需要建立一個 [TokenValidationParameters] 物件,宣告這個 JWT 要進行那些驗證行為與這個 JWT 的設定值。

由於在進行設計使用者登入身分驗證的時候,若通過身分驗證後,需要產生一個適合的 JWT 物件出來,此時,也需要用剛剛提到的 JWT 相關設定值。因此,就會想要把這些設定值設定到 [appsettings.json] 檔案內,方便可以集中管理與變更,也不用把這些設定值使用 Hard Code 的方式,寫死在程式碼內,造成使用 Ctrl + C / Ctrl + V 操作模式下所造成的副作用影想。

在這裡將會要使用 ASP.NET Core 中的選項模式 所提供的能力來做到可以使用強型別的方式,來讀取 Configuration 設定的內容值,另外,在這裡也會使用較不方便的 Configuration 物件,說明如何使用弱型別的方式來讀取設定值的做法。

建立 ASP.NET Core 7 專案

首先先來建立一個 ASP.NET Core 空白專案,請依照底下說明來建立這個專案

  • 打開 Visual Studio 2022 IDE 應用程式
  • 從 [Visual Studio 2022] 對話窗中,點選右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗右半部
    • 切換 [所有語言 (L)] 下拉選單控制項為 [C#]
    • 切換 [所有專案類型 (T)] 下拉選單控制項為 [Web]
  • 在中間的專案範本清單中,找到並且點選 [空的 ASP.NET Core] 專案範本選項
  • 點選右下角的 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗
  • 找到 [專案名稱] 欄位,輸入 AN020 作為專案名稱
  • 點選右下角的 [下一步] 按鈕
  • 現在將會看到 [其他資訊] 對話窗
  • 請點選右下角的 [建立] 按鈕

修正設定檔案的來源內容

  • 在專案根節點內,找到並且打開 [appsettings.json] 檔案
  • 找到 "AllowedHosts": "*" 屬性宣告項目
  • 在該節點之後,加入底下宣告內容
"JWT": {
"ValidIssuer": "Backend",
"ValidAudience": "Backend",
"ExpireMinutes": 20,
"RefreshExpireDays": 7,
"IssuerSigningKey": "https://randomkeygen.com/"
}
  • 在這裡建立一個設定設定節點,該設定節點物件內將會包含五個屬性值,分別是 ValidIssuer 、 ValidAudience 、 ExpireMinutes 、 RefreshExpireDays 、 IssuerSigningKey
  • 這五個屬性值將會用於設定這個專案內的 JWT 元件運作行為與表現
  • 底下將會是完成後的 [appsettings.json] 檔案內所有內容
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "JWT": {
    "ValidIssuer": "Backend",
    "ValidAudience": "Backend",
    "ExpireMinutes": 20,
    "RefreshExpireDays": 7,
    "IssuerSigningKey": "https://randomkeygen.com/"
  }
}

使用弱型別方式,讀取 設定 中的 JWT 區段內容

  • 在專案根目錄內,找到並且打開 [Program.cs] 檔案
  • 找到 var builder = WebApplication.CreateBuilder(args); 敘述
  • 在此敘述下方加入底下程式碼
#region 使用 Configuration 物件來讀取設定內容
string ValidIssuer = builder.Configuration["JWT:ValidIssuer"];
IConfigurationSection configurationSection = builder
    .Configuration.GetSection("JWT");
string ValidAudience = configurationSection["ValidAudience"];
string IssuerSigningKey = builder.Configuration["JWT:IssuerSigningKey"];
string ExpireMinutesContent = builder.Configuration["JWT:ExpireMinutes"];
int ExpireMinutes = int.Parse(ExpireMinutesContent);
string RefreshExpireDaysContent = configurationSection["RefreshExpireDays"];
int RefreshExpireDays = int.Parse(RefreshExpireDaysContent);

Console.WriteLine($"Configuration ValidIssuer : {ValidIssuer}");
Console.WriteLine($"Configuration ValidAudience : {ValidAudience}");
Console.WriteLine($"Configuration ExpireMinutes : {ExpireMinutes}");
Console.WriteLine($"Configuration RefreshExpireDays : {RefreshExpireDays}");
Console.WriteLine($"Configuration IssuerSigningKey : {IssuerSigningKey}");
  • 首先,想要取得型別為 [ConfigurationManager] 的物件,可以透過 [builder.Configuration] 物件來得到
  • ConfigurationManager 提供 key 為字串,而 value 為字串的資料字典運算子,因此,可以使用設定路徑字串作為鍵值,取得當時設定屬性值是甚麼。
  • 不同屬性間,可以使用 冒號 : 來分隔不同屬性階層關係
  • 例如,這裡使用 string ValidIssuer = builder.Configuration["JWT:ValidIssuer"]; 敘述,宣告鍵值 Key 為 JWT:ValidIssuer 這個絕對路徑,取得 [ConfigurationManager] 物件內的相對應 Value 值到 [ValidIssuer] 區域變數內
  • 另外,可以透過 [Configuration.GetSection] 方法,設定內容內的某個子階層設定內容,得到的物件型別將會是 ConfigurationSection
  • [ConfigurationSection] 型別的物件,提供 key 為字串,而 value 為字串的資料字典運算子
  • 透過 [ConfigurationSection] 物件,可以使用相對路徑的描述方式,取得某個子階層內的設定屬性值是甚麼。
  • 例如,這個敘述 string ValidAudience = configurationSection["ValidAudience"]; ,將會取得指定子階層內的 [ValidAudience] 屬性值到 ValidAudience 區域變數內
  • 在這個 JWT 設定區段內,有兩個屬性的屬性值將會是整數,其他的都是字串
  • 因此,需要使用上述的方式,取得相關屬性在設定環境內的實際文字內容,接著使用 [int.Parse] 方法,強制轉型成為整數數值。
  • 最後,就可以將這五個 .NET 物件,顯示在 Console 螢幕上
  • 從這裡程式碼與使用方式可以觀察到,由於 Configuration 屬性本身是透過 資料字典 資料結構來取的指定鍵值所對應的數值,不過,所取得的 Value 都是一樣的型別,都是字串;因此,若在設定環境中,指定某個屬性的型別要為 bool, 整數, 浮點數, 列舉,此時,程式設計師需要自己進行這些型別的轉換。
  • 另外一個看到的問題,那就是這裡要取得五個設定屬性內容,就需要在 .NET 系統內宣告五個區域物件來分別儲存這些設定數值,這顯得相當的不方便與不好維護。

建立要讀取設定內容的類別

  • 使用滑鼠右擊專案根節點
  • 從彈出功能表中,點選 [加入] > [類別] 功能項目
  • 此時出現 [新增項目] 對話窗視窗
  • 在 [新增項目] 對話窗下方的名稱欄位內,輸入 JwtConfiguration
  • 點選右下方 [新增] 按鈕,完成新增這個類別檔案
  • 使用底下程式碼,替換掉剛剛產生出來的檔案內容
namespace AN020
{
    public class JwtConfiguration
    {
        public string ValidIssuer { get; set; }
        public string ValidAudience { get; set; }
        public int ExpireMinutes { get; set; }
        public int RefreshExpireDays { get; set; }
        public string IssuerSigningKey { get; set; }
    }
}
  • 在這裡建立一個新的類別,其名稱為 [JwtConfiguration]
  • 在這個類別內,建立了五個屬性,其屬性名稱將會與剛剛在 [appsettings.json] 檔案內新增內容的屬性名稱需要相同。

使用強型別方式來取得設定環境內的數值

  • 有了 [JwtConfiguration] 類別,就可以開始進行用強型別方式來取得設定內的綁定數值
  • 在專案根目錄內,找到並且打開 [Program.cs] 檔案
  • 找到 var app = builder.Build(); 敘述
  • 在此敘述上方加入底下程式碼
#region 加入 設定 強型別 注入宣告
builder.Services.Configure<JwtConfiguration>(builder.Configuration
    .GetSection("JWT"));
#endregion

#region 使用 ServiceProvider 進行強型別讀取 JWT 設定值
using ServiceProvider serviceProvider = builder.Services.BuildServiceProvider();
JwtConfiguration jwtConfiguration = serviceProvider
    .GetRequiredService<IOptions<JwtConfiguration>>().Value;
#endregion

#region 顯示 JWT 設定值
Console.WriteLine($"ValidIssuer: {jwtConfiguration.ValidIssuer}");
Console.WriteLine($"ValidAudience: {jwtConfiguration.ValidAudience}");
Console.WriteLine($"ExpireMinutes: {jwtConfiguration.ExpireMinutes}");
Console.WriteLine($"RefreshExpireDays: {jwtConfiguration.RefreshExpireDays}");
Console.WriteLine($"IssuerSigningKey: {jwtConfiguration.IssuerSigningKey}");
#endregion
  • 首先,需要宣告準備要使用強型別的方式來取得設定環境內的數值
  • 透過 [builer.Services] 來對 DI / IoC 容器進行需要用到的型別註冊
  • 在此將會透過 builder.Services.Configure<JwtConfiguration>(builder.Configuration.GetSection("JWT")); 敘述來做到,在這裡將會透過 builder.Configuration.GetSection("JWT") 方法取得子階層節點的設定內容,接著,註冊這個設定子節點內容將會對應到 [JwtConfiguration] 類別
  • 想要取得這個設定內容子階層的所有數值到 [JwtConfiguration] 的物件內,這個時候就需要透過 DI 相依性注入機制,使用 [IOptions] 這樣的型別來取得一個物件,接著使用這個物件的 Value 屬性,便可以取得一個型別為 [JwtConfiguration] 物件
  • 不過,因為在這裡的程式碼,還在進行 DI 容器的註冊與設定工作,所以,幾乎沒有辦法透過建構式等方式來注入相依物件
  • 因此,在此將會使用 服務定位器 Service Locator 機制來取的一個 DI 容器物件,使用該 DI Container 物件提供的方法,取得指定型別的相依型別物件
  • 這可以從 ServiceProvider serviceProvider = builder.Services.BuildServiceProvider() 敘述可以看出,透過 Services (型別為 [IServiceCollection] 的物件)提供的 [BuildServiceProvider] 方法,便可以取得這裡個 IoC 容器物件
  • 使用 JwtConfiguration jwtConfiguration = serviceProvider.GetRequiredService<IOptions<JwtConfiguration>>().Value 敘述,便可以取得位於設定環境中的 [JWT] 子階層的所有設定內容到 JwtConfiguration 物件內
  • 最後,將 [JwtConfiguration] 物件內的屬性值輸出到螢幕上

執行專案與查看輸出結果

  • 請執行此專案,並且查看 Console 輸出結果是否如下所示
Configuration ValidIssuer : Backend
Configuration ValidAudience : Backend
Configuration ExpireMinutes : 20
Configuration RefreshExpireDays : 7
Configuration IssuerSigningKey : https://randomkeygen.com/
ValidIssuer: Backend
ValidAudience: Backend
ExpireMinutes: 20
RefreshExpireDays: 7
IssuerSigningKey: https://randomkeygen.com/
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:7033
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5063
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\Vulcan\Projects\AN020\AN020 

 








2023年1月25日 星期三

動手練習 ASP.NET Core7 相依性注入 Dependency Injection 的使用方式

動手練習 ASP.NET Core7 相依性注入 Dependency Injection 的使用方式

在現今專案開發過程中,相依性注入 Dependency Injection 這個設計模式 Design Pattern 扮演著相當重要的角色,因為,一旦在專案中使用了相依性注入這個設計模式,將會透過 DI / IoC Container 容器來進行所需要使用到的服務的 註冊 Registration,而在各個相關類別內,可以透過經常使用的 建構式注入 Constructor Injection 的方式,宣告這個類別需要使用到其他的服務,這個過程稱之為 解析 Resolve;當在這個類別取得相依物件之後,便可以進行此相依物件的操作與使用,一旦此物件使用完畢之後,當初所注入的服務物件,便會根據當初所注入時期宣告的生命週期來進行管理,由 DI / IoC 容器決定何時要 釋放 Release 當初所注入的服務物件。

因此,整個相依性注入這個設計模式,將會圍繞著 RRR (Registration , Resolve , Release)這三種操作來進行,透過這個設計模式將會獲得到整個專案具有鬆散耦合的特性,這也代表了這個專案具有好維護的特性,因為,一旦當初所規劃與設計的實作服務物件需要進行變更或者更換的時候,此時,便可以設計另外一個新服務類別,無需使用原有的服務類別來進行修改,緊接著透過對相依性服務容器的註冊階段,宣告此專案將會注入此一新設計的服務物件,不再使用原有的服務物件。

對於第一次接觸這個相依性注入設計模式的程式設計師,絕大多數很難接受這樣的開發做法,猛一看,整個開發過程似乎變得相當複雜,而且產生出更多的程式碼,不過,這一切的辛苦與努力,只是為了要能夠達成一件事情,讓整體專案變得好維護、好擴充與延伸,因此,一旦學會與使用這個設計模式之後,所得到的效益與成果,是沒有使用這個設計模式之前很難想像到的。

那麼,相依性注入 Dependency Injection 這個設計模式 Design Pattern 究竟好不好學習與實做呢?接下來就來看看

在這個練習中,將會設計一個服務類別 [MyService] ,此類別內僅會有一個 [Hi] 方法,並且該方法會回傳一個字串。

首先先來建立一個 ASP.NET Core Web 應用程式 (Model-View-Controller) 專案,請依照底下說明來建立這個專案

  • 打開 Visual Studio 2022 IDE 應用程式
  • 從 [Visual Studio 2022] 對話窗中,點選右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗右半部
    • 切換 [所有語言 (L)] 下拉選單控制項為 [C#]
    • 切換 [所有專案類型 (T)] 下拉選單控制項為 [Web]
  • 在中間的專案範本清單中,找到並且點選 [ASP.NET Core Web 應用程式 (Model-View-Controller)] 專案範本選項
  • 點選右下角的 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗
  • 找到 [專案名稱] 欄位,輸入 AN005 作為專案名稱
  • 點選右下角的 [下一步] 按鈕
  • 現在將會看到 [其他資訊] 對話窗
  • 請點選右下角的 [建立] 按鈕

建立需要使用到的服務類別

在這裡將會按照一般常用的設計方式來進行操作

  • 使用滑鼠右擊該專案節點

  • 在彈出功能表上,選取 [加入] > [新增資料夾] 選項

  • 在新產生的資料節點上,輸入 [Services] 這個名稱作為資料夾的名稱

    日後,若有其他相關服務要進行新增,請在此資料夾下來進行新增新的類別來進行設計,如此的好處將會是可以把所有的服務都存在同一個地方,方便區隔與管理,當然,一旦該資料夾的服務隨著時間延續,需求不斷的增加,也會造成該資料夾下的類別檔案數量越來越多,此時有一種選擇方式,那就是在此 [Services] 資料夾下,建立出更多不同分類的資料夾,江河是的服務類別檔案,拖拉到合適的資料夾內,如此將會達到更加清爽的檢視與管理目的。

  • 使用滑鼠右擊 [Services] 資料夾節點

  • 在彈出功能表上,選取 [加入] > [類別] 選項

  • 此時,將會出現 [新增項目] 對話窗

  • 在此對話窗最下方的 [名稱] 欄位內,輸入 MyService 這個文字,作為此新建立類別的名稱與檔案名稱

  • 底下將會是剛剛建立的 [MyService] 類別的程式碼

namespace AN005
{
    public class MyService
    {
    }
}
  • 請依據底下程式碼重新設計此新的類別程式碼
public class MyService
{
    public string Hi(string name)
    {
        return $"Hi {name}";
    }
}

在此設計一個 [Hi] 方法,將會接收一個字串型別的參數,也就是要傳入一個名字,接著將會對這個名字文字說 Hi,因此透過 $"Hi {name}" 表示式將其組合起需要的新文字內容,最後此文字回傳回去。

為新建立的服務類別,建立新的介面

原則上來說,當要設計一個新的服務類別,應該會要設計該類別的 介面 Interface ,也就是該類別對外溝通的合約,接著,才會依據這個新建立的 介面 來實作出新的服務類別出來;不過,在這裡將會簡化這個過程,而是先設計一個服務類別,然後,透過 Visual Studio 2022 所提供的 重構 Refactor 工具,為這個類別自動產生對外溝通會用到的合約介面出來。

  • 將滑鼠移動到 [MyService] 這個文字任何地方

  • 使用滑鼠右擊此文字,將會出現如下圖的彈跳功能表清單出來

  • 請選取 [快速動作與重構] 這個選項

  • 接下來將會看到另外一個彈跳功能表清單

  • 在這個彈跳功能表清單中,看到需要使用到的重構功能,請選取 [擷取介面] 這個項目

    這個功能選項將可讓您使用類別、結構或介面的現有成員來建立一個新介面,如此,便會自動產生出一個對外會用到溝通合約介面了,想要使用這個類別的任何功能或者屬性,僅能夠透過此介面來存取,對於參考與使用這個類別所建立的物件而言,使用者並不需要知道是哪個類別來提供這些服務,使用者僅需要參考所用到的介面,而真正的服務類別也僅需要實作所指定的介面即可。

  • 緊接著將會看到新的對話窗出現,在這個 [擷取介面] 對話窗內,將可以調整需要產生介面的內容

    在此為了簡化練習過程,將會使用預設值來操作,更多關於這方面的資訊,可以參考 擷取介面重構

  • 在 [擷取介面] 對話窗內,點選右下角的 [確定] 按鈕,以便產生出這個新的介面

  • 底下將會是新產生出的來的介面檔案內容

    這是一個 C# Interface 介面宣告,這可以從 public interface ... 看的出來。

    這個介面內僅宣告一個成員,那就是一個名稱為 Hi 的方法,從該 方法簽章 Method Signatures 可以看出,這個介面需要一個方法,其需要傳入一個型別為字串的參數,而該方法將會回傳一個型別為字串的物件

    對於任何要實作 Implementation 這個介面的類別,在該類別內都需要也剛剛提到的函式簽章存在

namespace AN005.Services
{
    public interface IMyService
    {
        string Hi(string name);
    }
}
  • 原有的 [MyService] 類別,將會被重構為如下程式碼

    在這個類別名稱之後,自動加入了  : IMyService 這個介面宣告,表示這個類別有實作這個 [IMyService] 介面合約內容。

namespace AN005.Services
{
    public class MyService : IMyService
    {
        public string Hi(string name)
        {
            return $"Hi {name}";
        }
    }
}

開始用使用相依性注入容器來進行新服務註冊

一般來說,當要進行與使用相依性服務容器來進行註冊的時候,通常會在 Composition Root 組合根 地方來進行,也就是通常的程式進入點位置,在這裡 ASP.NET Core7 相關專案,程式進入點的位置就是在 [Program.cs] 這個檔案內來宣告,所以,接下來的工作就要進行這個服務的註冊程式設計。

  • 在專案根目錄下,找到並且打開 [Program.cs] 檔案

  • 找到 builder.Services.AddRazorPages(); 敘述

  • 在其下方加入底下程式碼

    型別為 WebApplicationBuilder 的 [builder] 物件,裡面有個 [Services] 屬性,他的型別為 IServiceCollection ,其目的將會是指定服務描述項集合的合約,講白話點,那就是要在這裡進行對相依性服務容器來進行 註冊 宣告

    對於 [AddTransient] 這個方法,將會是指定此注入行為所產生的物件,其物件生命週期為何,也就是該物件何時會產生、何時會消滅掉,這個 [AddTransient] 方法將會宣告當要注入此物件的時候,將會新建立與產生新的物件,而當參考使用的物件歸還記憶體之後,這個注入的物件,也會消滅並把所占有的記憶體歸還給作業系統。

    這個 [AddTransient] 方法提供泛型方法覆載,在這裡的範例中,先傳入一個介面型別,緊接著傳入一個有實作該介面的類別,一旦想要透過相依性注入容器來注入或者取得(正式的名稱為 解析 Resolve),僅會告知需要一個 [IMyServer] 型別的物件,而究竟是哪個有實作該介面的物件會產生出來,就會依據這裡的宣告來建立新的物件。

    之前有提到,若因為有新的需求需要變更這個 [MyServer] 類別,程式設計師可以依據新的需求,重新設計一個新的 [MyService2] 類別,接著在此修改宣告,任何未來想要使用 [IMyService] 這個介面的時候,都會注入 [MyService2] 物件,也就是說,相依性服務容器的註冊程式碼將會改成 builder.Services.AddTransient<IMyService, MyService2>(); ,最後,不用再修改任何程式碼,重新啟動這個專案,此時,該專案便可以滿足新變更的需求。

builder.Services.AddTransient<IMyService, MyService>();

注入與使用 MyService 物件

  • 在 [Program.cs] 檔案內找到 var app = builder.Build(); 敘述
  • 在該敘述之後加入底下程式碼

在型別為 [WebApplication] 的 [app] 物件內,將會有個型別為 IServiceProvider 的 [Services] 屬性,其目的為定義機制來擷取服務物件,也就是為其他物件提供自訂支援的物件,講白話來說,就是可以透過這個屬性來進行手動注入需求

因此,使用 [GetService] 這個泛型方法,提供一個 [IMyService] 型別,代表說要透過相依性注入容器,取得一個有實作 [IMyService] 介面的物件。

根據剛剛的程式碼,在相依性注入容器內對於 [IMyService] 介面的註冊內容,將會對應到 [MyService] 這個型別類別,因此,當呼叫了 app.Services.GetService<IMyService>() 這個方法呼叫,相依性注入容器將會產生一個新的 [MyService] 物件,並且回傳到 [myService] 區域變數內。

var myService = app.Services.GetService<IMyService>();
var logger = app.Services.GetService<ILogger<Program>>();
var hi = myService.Hi("Lee");
logger.LogInformation($"In WebApplication, Call Hi Method : {hi}");

最後整個專案將會如下結構