ASP.NET Core 6框架揭秘實例演示[01]: 編程初體驗

作為《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-3
圖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 //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\」目錄下。
  1-4
圖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容器)中採用的啟動方式。

1-5
圖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框架輸出的日誌,通過日誌表明應用已經開始在默認的兩個終結點(//localhost:5000和//localhost:5001)監聽請求了。我們使用瀏覽器針對這兩個終結點發送了兩個請求,均得到一致的響應。從響應的內容可以看出應用正是利用我們指定的RequestDelegate委託處理請求的。

clip_image002

圖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!」。

clip_image002[6]
圖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對象輸出的。

clip_image002[8]
圖5 輸出到控制台上的日誌