ASP.NET Core 6框架揭祕例項演示[01]: 程式設計初體驗

語言: CN / TW / HK

作為《ASP.NET Core 3框架揭祕》的升級版,《ASP.NET Core 6框架揭祕》提供了很多新的章節,同時對現有的內容進行大量的修改。雖然本書旨在對ASP.NET Core框架的架構設計和實現原理進行剖析,但是其中提供的 258個例項 演示卻可以作為入門材料,這個系列會將這些演示例項單獨提取出來並進行彙總。對於想學習ASP.NET Core的同學,如果你覺得沒有必要“磚的這麼深”,倒是可以看看。本片提供的20個簡單的演示例項基本涵蓋了ASP.NET Core 6基本的程式設計模式,我們不僅會利用它們來演示針對控制檯、API、MVC、gRPC應用的構建與程式設計,還會演示Dapr在.NET 6中的應用。除此之外,這20個例項還涵蓋了針對依賴注入、配置選項、日誌記錄的應用。

[101]利用命令列建立.NET程式( 原始碼

[102]採用Minimal API構建ASP.NET Core程式( 原始碼

[103]一步建立WebApplication物件( 原始碼

[104]使用原始形態的中介軟體( 原始碼

[105]使用中介軟體委託變體(1)( 原始碼

[106]使用中介軟體委託變體(2)( 原始碼

[107]定義強型別中介軟體型別( 原始碼

[108]定義基於約定的中介軟體型別(建構函式注入)( 原始碼

[109]定義基於約定的中介軟體型別(方法注入)( 原始碼

[110]配置的應用( 原始碼

[111]Options的應用( 原始碼

[112]日誌的應用( 原始碼

[101]利用命令列建立.NET程式

我們按照圖1所示的方式執行“dotnet new”命令(dotnet new console -n App)建立一個名為“App”的控制檯程式。該命令執行之後會在當前工作目錄建立一個由指定應用名稱命名的子目錄,並將生成的檔案存放在裡面。

圖1 執行“dotnet new”命令建立一個控制檯程式

.csproj檔案最終是為MSBuild服務的,該檔案提供了相關的配置來控制MSBuild針對當前專案的編譯和釋出行為。如下所示的就是App.csproj檔案的全部內容,如果你曾經檢視過傳統.NET Framework下的.csproj檔案,你會驚歎於這個App.csproj檔案內容的簡潔。.NET 6下的專案檔案的簡潔源於對SDK的應用。不同的應用型別會採用不同的SDK,比如我們建立的這個控制檯應用採用的SDK為“Microsoft.NET.Sdk”,ASP.NET應用會採用另一個名為“Microsoft.NET.Sdk.Web”的SDK。SDK相等於為某種型別的專案制定了一份面向MSBuild的基準配置,如果在專案檔案的<Project>根節點設定了具體的SDK,意味著直接將這份基準配置繼承下來。

  1 <Project Sdk="Microsoft.NET.Sdk">
  2    <PropertyGroup>
  3      <OutputType>Exe</OutputType>
  4      <TargetFramework>net6.0</TargetFramework>
  5      <ImplicitUsings>enable</ImplicitUsings>
  6      <Nullable>enable</Nullable>
  7    </PropertyGroup>
  8  </Project>

如上面的程式碼片段所示,與專案相關的屬性可以分組定義在專案檔案的<PropertyGroup>節點下。這個App.csproj檔案定義了四個屬性,其中OutputType和TargetFramework屬性表示編譯輸出型別與採用的目標框架。由於我們建立的是一個針對 .NET 6的可執行控制檯應用,所以TargetFramework和OutputType分別設定為“net6.0”和“Exe”。 專案的ImplicitUsings屬性與C# 10提供的一個叫做“全域性名稱空間”新特性有關,另一個名為Nullable的屬性與C#與一個名為“空值(Null)驗證”的特性有關。

如下所示的就是專案目錄下的生成的Program.cs檔案的內容。可以看出整個檔案只有兩行文字,其中一行還是註釋。這唯一的一行程式碼呼叫了Console型別的靜態方法將字串“Hello, World!”輸出到控制檯上。這裡體現了C# 10另一個被稱為“頂級語句(Top-level Statements)”的新特性——入口程式的程式碼可以作為頂層語句獨立存在。

  1 // See https://aka.ms/new-console-template for more information
  2 Console.WriteLine("Hello, World!");

針對 .NET應用的編譯和運行同樣可以執行“dotnet.exe”命令列完成的。如圖2所示,在將專案根目錄作為工作目錄後,我們執行“dotnet build”命令對這個控制檯應用實施編譯。由於預設採用Debug編譯模式,所以編譯生成的程式集會儲存在“\bin\Debug\”目錄下。同一個應用可以採用多個目標框架,針對不同目標框架編譯生成的程式集是會放在不同的目錄下。由於我們建立的是針對 .NET 6.0的應用程式,所以最終生成的程式集被儲存在“\bin\Debug\net6.0\”目錄下。

圖2執行“dotnet build”命令編譯一個控制檯程式

如果檢視編譯的輸出目錄,可以發現兩個同名(App)的程式集檔案,一個是App.dll,另一個是App.exe,後者在尺寸上會大很多。App.exe是一個可以直接執行的可執行檔案,而App.dll僅僅是一個單純的動態連結庫,需要藉助命令列dotnet才能執行。

如圖3所示,當我們執行“dotnet run”命令後,編譯後的程式隨即被執行,“Hello, World!”字串被直接列印在控制檯上。執行“dotnet run”命令啟動程式之前其實無須顯式執行“dotnet build”命令對原始碼實施編譯,因為該命令會自動觸發編譯操作。在執行“dotnet”命令啟動應用程式集時,我們也可以直接指定啟動程式集的路徑(“dotnet bin\Debug\net6.0\App.dll”)。實際上dotnet run主要用在開發測試中,dotnet {AppName}.dll的方式才是部署環境(比如Docker容器)中採用的啟動方式。

圖3執行dotnet命令執行一個控制檯程式

[102]採用Minimal API構建ASP.NET Core程式

前面利用dotnet new命令建立了一個簡單的控制檯程式,接下來我們將其改造成一個ASP.NET Core應用。我們在前面已經說過,不同的應用型別會採用不同的SDK,所以我們直接修改App.csproj檔案將SDK設定為“Microsoft.NET.Sdk.Web”。由於不需要利用生成的.exe檔案來啟動ASP.NET Core應用,所以應該將XML元素<OutputType>Exe</OutputType>從<PropertyGroup>節點中刪除。

  1 <Project Sdk="Microsoft.NET.Sdk.Web">
  2   <PropertyGroup>
  3     <TargetFramework>net6.0</TargetFramework>
  4     <ImplicitUsings>enable</ImplicitUsings>
  5     <Nullable>enable</Nullable>
  6   </PropertyGroup>
  7 </Project> 

ASP.NET Core (Core)應用的承載(Hosting)經歷了三次較大的變遷,由於最新的承載方式提供的API最為簡潔且依賴最小,我們將它稱為 “Minimal API” 。本書除了在第16章 “應用承載(上)” 會涉及到其他兩種承載模式外,本書提供的所有演示例項均會使用Minimal API。如下所示的是我們採用這種程式設計模式編寫的第一個Hello World程式。

  1 RequestDelegate handler = context => context.Response.WriteAsync("Hello, World!");
  2 WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
  3 WebApplication app = builder.Build();
  4 app.Run(handler: handler);
  5 app.Run(); 

上面的程式碼片段涉及到三個重要的物件,其中WebApplication物件表示承載的應用,Minimal API採用“構建者(Builder)”模式來構建它,此構建者體現為一個WebApplicationBuilder物件。如程式碼片段所示,我們呼叫WebApplication型別的靜態工廠方法CreateBuilder建立了一個WebApplicationBuilder物件,該方法的引數args代表命令列引數陣列。在呼叫此該物件的Build方法將WebApplication物件構建出來後,我們呼叫了它的Run擴充套件方法並使用一個RequestDelegate物件作為其引數。RequestDelegate雖然是一個簡單的委託型別,但是它在ASP.NET Core框架體系中地位非凡,我們現在先來對它做一個簡單的介紹。

當一個ASP.NET Core啟動之後,它會使用註冊的伺服器繫結到指定的埠進行請求監聽。當接收抵達的請求之後,一個通過HttpContext物件表示的上下文物件會被創建出來。我們不僅可以從這個上下文中提取出所有與當前請求相關的資訊,還能直接使用該上下文完成對請求的響應。關於這一點完全可以從HttpContext這個抽象類如下兩個核心屬性Request和Response看出來。

  1 public abstract class HttpContext
  2 {
  3     public abstract HttpRequest 	Request { get }
  4 public abstract HttpResponse 	Response { get }
  5 ...
  6 } 

由於ASP.NET Core應用針對請求的處理總是在一個HttpContext上下文中進行,所以針對請求的處理器可以表示為一個Func<HttpContext, Task>型別的委託。由於這樣的委託會被廣泛地使用,所以ASP.NET Core直接定義了一個專門的委託型別,就是我們在程式中使用到的RequestDelegate。從如下所示的針對RequestDelegate型別的定義可以看出,它本質上就是一個Func<HttpContext, Task>委託。

  1 public delegate Task RequestDelegate(HttpContext context);

再次回到演示程式。我們首先建立了一個RequestDelegate委託,對應的目標方法會在響應輸出流中寫入字串 “Hello, World!” 。我們將此委託作為引數呼叫WebApplication物件的Run擴充套件方法,這個呼叫可以理解為將這個委託作為所有請求的處理器,接收到的所有請求都將通過這個委託來處理。演示程式最後呼叫WebApplication另一個無參Run擴充套件方法是為了啟動承載的應用。在Visual Studio下,我們可以直接按F5(或者Ctrl + F5)啟動該程式,當然針對命令列 “dotnet run” 命令的應用啟動方式依然有效,本書提供的演示例項大都會採用這種方式。如圖4所示,我們以命令列方式啟動程式後,控制檯上回出現ASP.NET Core框架輸出的日誌,通過日誌表明應用已經開始在預設的兩個終結點(http://localhost:5000和 https://localhost:5001 )監聽請求了。我們使用瀏覽器針對這兩個終結點發送了兩個請求,均得到一致的響應。從響應的內容可以看出應用正是利用我們指定的RequestDelegate委託處理請求的。

圖4啟動應用程式並利用瀏覽器進行訪問

[103]一步建立WebApplication物件

上面演示的程式先呼叫定義在WebApplication型別的靜態工廠方法CreateBuilder建立一個WebApplicationBuilder物件,再利用後者構建一個代表承載應用的WebApplication物件。WebApplicationBuilder提供了很多用來對構建WebApplication進行設定的API,但是我們的演示例項並未使用到它們,此時我們可以直接呼叫靜態工廠方法Create將WebApplication物件創建出來。在如下所示的改寫程式中,我們直接將請求處理器定義成一個本地靜態方法HandleAsync。

  1 var app = WebApplication.Create(args);
  2 app.Run(handler: HandleAsync);
  3 app.Run();
  4 
  5 static Task HandleAsync(HttpContext httpContext)   => httpContext.Response.WriteAsync("Hello, World!"); 

[104]使用原始形態的中介軟體

承載的ASP.NET Core應用最終體現為由註冊中介軟體構建的請求處理管道。在伺服器接收到請求並將成功構建出HttpContext上下文之後,會將請求交付給這個管道進行處理。待管道完成了處理任務之後,控制權再次回到伺服器的手中,它會將處理的結果轉換成響應傳送出去。從應用程式設計的角度來看,這個管道體現為上述的RequestDelegate委託,組成它的單箇中間件則體現為另一個型別為Func<RequestDelegate,RequestDelegate>的委託,該委託的輸入和輸出都是一個RequestDelegate物件,前者表示由後續中介軟體構建的管道,後者代表將當前中介軟體納入此管道後生成的新管道。在上面演示的例項中,我們將一個RequestDelegate委託作為引數呼叫了WebApplication的Run擴充套件方法,我們當時說這是為應用設定一個請求處理器。其實這種說法不夠準確,該方法僅僅是註冊一箇中間件而已。說得更加具體一點,這個方法用於註冊處於管道末端的中介軟體。為了讓讀者體驗到中介軟體和管道針對請求的處理,我們對上面演示應用進行了如下的改寫。

  1 var app = WebApplication.Create(args);
  2 IApplicationBuilder appBuilder = app;
  3 appBuilder
  4     .Use(middleware: HelloMiddleware)
  5     .Use(middleware: WorldMiddleware);
  6 app.Run();
  7 
  8 static RequestDelegate HelloMiddleware(RequestDelegate next)
  9     => async httpContext => {
 10     await httpContext.Response.WriteAsync("Hello, ");
 11     await next(httpContext);
 12 };
 13 
 14 static RequestDelegate WorldMiddleware(RequestDelegate next)
 15      => httpContext => httpContext.Response.WriteAsync("World!"); 

由於中介軟體體現為一個Func<RequestDelegate,RequestDelegate>委託,所以我們利用上面定義的兩個與該委託型別具有一致宣告的本地靜態方法HelloMiddleware和WorldMiddleware來表示對應的中介軟體。我們將完整的文字“Hello, World!”拆分為“Hello, ”和“World!”兩段,分別由上述兩個終結點寫入響應輸出流。在創建出代表承載應用的WebApplication物件之後,我們將它轉換成IApplicationBuilder介面型別,並呼叫其Use方法完成了對上述兩個中介軟體的註冊(由於WebApplication型別顯式實現了定義在IApplicationBuilder介面中的Use方法,我們不得不進行型別轉換)。如果利用瀏覽器採用相同的地址請求啟動後的應用,我們依然可以得到如圖4所示的響應內容。

[105]使用中介軟體委託變體(1)

雖然中介軟體最終總是體現為一個Func<RequestDelegate,RequestDelegate>委託,但是我們在開發過程中可以採用各種不同的形式來定義中介軟體,比如我們可以將中介軟體定義成如下兩種型別的委託。這兩個委託內容分別使用作為輸入引數的RequestDelegate和Func<Task>完整對後續管道的呼叫。

  • Func<HttpContext, RequestDelegate, Task>
  • Func<HttpContext, Func<Task>, Task>

我們現在來演示如何使用Func<HttpContext, RequestDelegate, Task>委託的形式來定義中介軟體。如下面的程式碼片段所示,我們將HelloMiddleware和WorldMiddleware替換成了與Func<HttpContext, RequestDelegate, Task>委託型別具有一致宣告的本地靜態方法。

  1 var app = WebApplication.Create(args);
  2 app
  3     .Use(middleware: HelloMiddleware)
  4     .Use(middleware: WorldMiddleware);
  5 app.Run();
  6 
  7 static async Task HelloMiddleware(HttpContext httpContext, RequestDelegate next)
  8 {
  9     await httpContext.Response.WriteAsync("Hello, ");
 10     await next(httpContext);
 11 };
 12 
 13 static Task WorldMiddleware(HttpContext httpContext, RequestDelegate next) => httpContext.Response.WriteAsync("World!"); 

[106]使用中介軟體委託變體(2)

下面的程式以類似的方式將這兩個中介軟體替換成與Func<HttpContext, Func<Task>, Task>委託型別具有一致宣告的本地方法。當我們呼叫WebApplication的Use方法將這兩種“變體”註冊為中介軟體的時候,該方法內部會將提供的委託轉換成Func<RequestDelegate,RequestDelegate>型別。

  1 var app = WebApplication.Create(args);
  2 app
  3     .Use(middleware: HelloMiddleware)
  4     .Use(middleware: WorldMiddleware);
  5 app.Run();
  6 
  7 static async Task HelloMiddleware(HttpContext httpContext, Func<Task> next)
  8 {
  9     await httpContext.Response.WriteAsync("Hello, ");
 10     await next();
 11 };
 12 
 13 static Task WorldMiddleware(HttpContext httpContext, Func<Task> next)    => httpContext.Response.WriteAsync("World!"); 

[107]定義強型別中介軟體型別

當我們試圖利用一個自定義中介軟體來完成某種請求處理功能時,其實很少會將中介軟體定義成上述的這三種委託形式,基本上都會將其定義成一個具體的型別。中介軟體型別有定義方式,一種是直接實現IMiddleware介面,本書將其稱為“強型別”的中介軟體定義方式。我們現在就採用這樣的方式定義一個簡單的中介軟體型別。不論在定義中介軟體型別,還是定義其他的服務型別,如果它們具有對其他服務的依賴,我們都會採用依賴注入(Dependency Injection)的方式將它們整合在一起。整個ASP.NET Core框架就建立在依賴注入框架之上,依賴注入已經成為ASP.NET Core最基本的程式設計方式 。我們接下來會演示依賴注入在自定義中介軟體型別中的應用。

在前面演示的例項中,我們利用中介軟體寫入以“硬編碼”方式指定的問候語“Hello, World!”,現在我們選擇由如下這個IGreeter介面表示的服務根據指定的時間來提供對應的問候語,Greeter型別是該介面的預設實現。這裡需要提前說明一下,本書提供的所有的演示例項都以“App”命名,獨立定義的型別預設會定義在約定的“App”名稱空間下。為了節省篇幅,接下來提供的型別定義程式碼片段將不再提供所在的名稱空間,當啟動應用程出現針對“App”名稱空間的匯入時不要感到奇怪。

  1 namespace App
  2 {
  3     public interface IGreeter
  4     {
  5         string Greet(DateTimeOffset time);
  6     }
  7 
  8     public class Greeter : IGreeter
  9     {
 10         public string Greet(DateTimeOffset time) => time.Hour switch
 11         {
 12             var h when h >= 5 && h < 12 	=> "Good morning!",
 13             var h when h >= 12 && h < 17 	=> "Good afternoon!",
 14             _ 				=> "Good evening!"
 15         };
 16     }
 17 } 

我們定義瞭如下這個名為GreetingMiddleware的中介軟體型別。如程式碼片段所示,該型別實現了IMiddleware介面,針對請求的處理實現在InvokeAsync方法中。我們在GreetingMiddleware型別的建構函式中注入了IGreeter物件,並利用它在實現的InvokeAsync方法中根據當前時間來提供對應的問候語,後者將作為請求的響應內容。

  1 public class GreetingMiddleware : IMiddleware
  2 {
  3     private readonly IGreeter _greeter;
  4     public GreetingMiddleware(IGreeter greeter)    => _greeter = greeter;
  5 
  6     public Task InvokeAsync(HttpContext context, RequestDelegate next) => context.Response.WriteAsync(_greeter.Greet(DateTimeOffset.Now));
  7 }
  8 

針對GreetingMiddleware中介軟體的應用體現在如下的程式中。如程式碼片段所示,我們呼叫了WebApplication物件的UseMiddleware<GreetingMiddleware>擴充套件方法註冊了這個中介軟體。由於強型別中介軟體例項是由依賴注入容器在需要的時候實時提供的,所以我們必須預先將它註冊為服務。註冊的註冊最終會新增到WebApplicationBuilder的Services屬性返回的IServiceCollection物件上,我們在得到這個物件後通過呼叫它的AddSingleton< GreetingMiddleware >方法將該中介軟體註冊為“單例服務”。由於中介軟體依賴IGreeter服務,所以我們呼叫AddSingleton<IGreeter, Greeter>擴充套件方法對該服務進行了註冊。

  1 using App;
  2 var builder = WebApplication.CreateBuilder(args);
  3 builder.Services
  4     .AddSingleton<IGreeter, Greeter>()
  5     .AddSingleton<GreetingMiddleware>();
  6 var app = builder.Build();
  7 app.UseMiddleware<GreetingMiddleware>();
  8 app.Run(); 

該程式啟動之後,針對它的請求會得到根據當前時間的生成問候語。如圖5所示,由於目前的時間為晚上七點,所以瀏覽器上顯示“Good evening!”。

圖5自定義中介軟體返回的問候語

[108]定義基於約定的中介軟體型別(建構函式注入)

中介軟體型別其實並不一定非得實現某個介面,或者繼承某個基類,按照既定的約定進行定義即可。按照ASP.NET Core的約定,中介軟體型別需要定義成一個公共例項型別(靜態型別無效),其建構函式可以注入任意的依賴服務,但必須包含一個RequestDelegate型別的引數,該引數表示由後續中介軟體構建的管道,當前中介軟體利用它將請求分發給後續管道作進一步處理。針對請求的處理實現在一個命名為InvokeAsync或者Invoke的方法中,該方法返回型別為Task, 第一個引數並繫結為當前的HttpContext上下文,所以GreetingMiddleware中介軟體型別可以改寫成如下的形式。

  1 public class GreetingMiddleware
  2 {
  3     private readonly IGreeter _greeter;
  4     public GreetingMiddleware(RequestDelegate next, IGreeter greeter)   => _greeter = greeter;
  5     public Task InvokeAsync(HttpContext context) => context.Response.WriteAsync(_greeter.Greet(DateTimeOffset.Now));
  6 } 

強型別的中介軟體例項是在對請求進行處理的時候由依賴注入容器實時提供的,按照約定定義的中介軟體例項則不同,當我們在註冊中介軟體的時候就已經利用依賴注入容器將它創建出來,所以前者可以採用不同的生命週期模式,後者總是一個單例物件。也正是因為這個原因,我們不需要將中介軟體註冊為服務。

  1 using App;
  2 var builder = WebApplication.CreateBuilder(args);
  3 builder.Services.AddSingleton<IGreeter, Greeter>();
  4 var app = builder.Build();
  5 app.UseMiddleware<GreetingMiddleware>();
  6 app.Run(); 

[109]定義基於約定的中介軟體型別(方法注入)

對於按照約定定義的中介軟體型別,依賴服務不一定非要注入到建構函式中,它們選擇直接注入到InvokeAsync或者Invoke方法中,所以上面這個GreetingMiddleware中介軟體也可以定義成如下的形式。對於按照約定定義的中介軟體型別,建構函式注入和方法注入並不是等效,兩者之間的差異會在第18章“應用承載(下)”中進行介紹。

  1 public class GreetingMiddleware
  2 {
  3     public GreetingMiddleware(RequestDelegate next){}
  4     public Task InvokeAsync(HttpContext context, IGreeter greeter) => context.Response.WriteAsync(greeter.Greet(DateTimeOffset.Now));
  5 }

[110]配置的應用

開發ASP.NET Core應用過程會廣泛使用到配置(Configuration),ASP.NET Core採用了一個非常靈活的配置框架,我們可以儲存在任何載體的資料作為配置源。我們還可以將結構化的配置轉換成對應的選項(Options)型別,以強型別的方式來使用它們。針對配置選項的系統介紹被放在第5章“配置選項(上)”和第6章“配置選項(下)”中,我們先在這裡“預熱”一下。在前面演示的例項中,Greeter型別針對指定時間提供的問候語依然是以“硬編碼”的方式提供的,現在我們選擇將它們放到配置檔案以方便進行調整中。為此我們在專案根目錄下新增一個名為“appsettings.json”的配置檔案,並將三條問候語以如下的形式定義在這個JSON檔案中。

  1 {
  2   "greeting": {
  3     "morning": "Good morning!",
  4     "afternoon": "Good afternoon!",
  5     "evening": "Good evening!"
  6   }
  7 }

ASP.NET Core應用中的配置通過IConfiguration物件表示,我們可以採用依賴注入的形式“自由”地使用它。對於演示的程式來說,我們只需要按照如下的方式將IConfiguration物件注入到Greeter型別的建構函式中,然後呼叫其GetSection方法得到定義了上述問候語的配置節(“greeting”)。在實現的Greet方法中,我們以索引的方式利用指定的Key(“morning”、“afternoon”和“evening”)提取對應的問候語。由於應用啟動的時候會自動載入這個按照約定命名的(“appsettings.json”)配置檔案,所以演示程式的其他地方不要作任何修改。

  1 public class Greeter : IGreeter
  2 {
  3     private readonly IConfiguration _configuration;
  4     public Greeter(IConfiguration configuration)    => _configuration = configuration.GetSection("greeting");
  5 
  6     public string Greet(DateTimeOffset time) => time.Hour switch
  7     {
  8         var h when h >= 5 && h < 12 	=> _configuration["morning"],
  9         var h when h >= 12 && h < 17 	=> _configuration["afternoon"],
 10         _ 				        => _configuration["evening"],
 11     };
 12 }

[111]Options的應用

正如前面所說,將結構化的配置轉換成對應型別的Options物件,以強型別的方式來使用它們是更加推薦的程式設計模式。為此我們為配置的三條問候語定義瞭如下這個GreetingOptions配置選項型別。

  1 public class GreetingOptions
  2 {
  3     public string Morning { get; set; } 	= default!;
  4     public string Afternoon { get; set; } 	= default!;
  5     public string Evening { get; set; } 	= default!;
  6 }

雖然Options物件不能直接以依賴服務的形式進行注入,但卻可以由注入的IOptions<TOptions>物件來提供。如下面的程式碼片段所示,我們在Greeter型別的建構函式中注入了IOptions<GreetingOptions>物件,並利用其Value屬性中得到了我們需要的GreetingOptions物件。在有了這個物件後,實現的Greet方法中只需要從對應的屬性中獲取相應的問候語就可以了。

  1 public class Greeter : IGreeter
  2 {
  3     private readonly GreetingOptions _options;
  4     public Greeter(IOptions<GreetingOptions> optionsAccessor)     => _options = optionsAccessor.Value;
  5 
  6     public string Greet(DateTimeOffset time) => time.Hour switch
  7     {
  8         var h when h >= 5 && h < 12 		=> _options.Morning,
  9         var h when h >= 12 && h < 17 		=> _options.Afternoon,
 10         _ 					=> _options.Evening
 11     };
 12 }

由於IOptions<GreetingOptions>物件提供的配置選項不能無中生有(實際上存在於配置中),我們需要將對應的配置節(“greeting”)繫結到GreetingOptions物件上。這項工作其實也屬於服務註冊的範疇,具體可以按照如下的形式呼叫IServiceCollection物件的Configure<TOptions>擴充套件方法來完成。如程式碼片段所示,代表應用整體配置的IConfiguration物件來源於WebApplicationBuilder的Configuration屬性。

  1 using App;
  2 var builder = WebApplication.CreateBuilder(args);
  3 builder.Services
  4     .AddSingleton<IGreeter, Greeter>()
  5     .Configure<GreetingOptions>(builder.Configuration.GetSection("greeting"));
  6 var app = builder.Build();
  7 app.UseMiddleware<GreetingMiddleware>();
  8 app.Run();

[112]日誌的應用

診斷日誌對於糾錯排錯必不可少。ASP.NET Core採用的診斷日誌框架強大、易用且靈活。在我們演示的程式中,Greeter型別會根據指定的時間返回對應的問候語,現在我們將時間和對應的問候語以日誌的方式記錄下來看看兩者是否匹配。我們在前面曾說過,依賴注入是ASP.NET Core應用最基本的程式設計模式。我們將涉及的功能(不論是業務相關的還是業務無關的)進行拆分,最終以具有不同粒度的服務將整個應用化整為零,服務之間的依賴關係直接以注入的方式來解決。我們在前面演示了針對配置選項的注入,接下來我們用來記錄日誌的ILogger物件依然看採用注入的方式獲得。如下面的程式碼片段所示,我們在Greeter型別的建構函式中注入了ILogger<Greeter>物件。在實現的Greet方法中,我們呼叫該物件的LogInformation擴充套件方法記錄了一條Information等級的日誌,日誌內容體現了時間與問候語文字之間的對映關係。

  1 public class Greeter : IGreeter
  2 {
  3     private readonly GreetingOptions 	_options;
  4     private readonly ILogger 		_logger;
  5 
  6     public Greeter(IOptions<GreetingOptions> optionsAccessor, ILogger<Greeter> logger)
  7     {
  8         _options 	= optionsAccessor.Value;
  9         _logger 	= logger;
 10     }
 11 
 12     public string Greet(DateTimeOffset time)
 13     {
 14         var message = time.Hour switch
 15         {
 16             var h when h >= 5 && h < 12 	=> _options.Morning,
 17             var h when h >= 12 && h < 17 	=> _options.Afternoon,
 18             _ 					=> _options.Evening
 19         };
 20         _logger.LogInformation(message:"{time} => {message}",time, message);
 21         return message;
 22     }
 23 }

採用Minimal API編寫的ASP.NET Core應用會預設將診斷日誌整合進來,所以整個演示程式的其它地方都不要修改。當修改後的應用啟動之後,針對每一個請求都會通過日誌留下“痕跡”。由於控制檯是預設開啟的日誌輸出渠道之一,日誌內容直接會輸出到控制檯上。圖5所示的是以命令列形式啟動應用的控制檯,上面顯示的都是以日誌形式輸出的內容。在眾多系統日誌中,我們發現有一條是由Greeter物件輸出的。

圖5輸出到控制檯上的日誌