本篇提供的20個簡單的演示實例基本涵蓋了ASP.NET Core 6基本的編程模式,我們不僅會利用它們來演示針對控制臺、API、MVC、gRPC應用的構建與編程,還會演示Dapr在.NET 6中的應用。除此之外,這20個實例還涵蓋了針對依賴注入、配置選項、日志記錄的應用。[本篇節選自《ASP.NET Core 6框架揭秘》第一章]
[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,意味著直接將這份基準配置繼承下來。
<Project?Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net6.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable>
</PropertyGroup></Project>t>
如上面的代碼片段所示,與項目相關的屬性可以分組定義在項目文件的<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)”的新特性——入口程序的代碼可以作為頂層語句獨立存在。
//?See?https://aka.ms/new-console-template?for?more?informationConsole.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>節點中刪除。
<Project?Sdk="Microsoft.NET.Sdk.Web"><PropertyGroup><TargetFramework>net6.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable>
</PropertyGroup></Project>
ASP.NET Core (Core)應用的承載(Hosting)經歷了三次較大的變遷,由于最新的承載方式提供的API最為簡潔且依賴最小,我們將它稱為 “Minimal API” 。本書除了在第16章 “應用承載(上)” 會涉及到其他兩種承載模式外,本書提供的所有演示實例均會使用Minimal API。如下所示的是我們采用這種編程模式編寫的第一個Hello World程序。
RequestDelegate?handler?=?context?=>?context.Response.WriteAsync("Hello,?World!");
WebApplicationBuilder?builder?=?WebApplication.CreateBuilder(args);
WebApplication?app?=?builder.Build();
app.Run(handler:?handler);
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看出來。
public?abstract?class?HttpContext{?????public?abstract?HttpRequest?Request?{?get?}?????public?abstract?HttpResponse?Response?{?get?}...}
由于ASP.NET Core應用針對請求的處理總是在一個HttpContext上下文中進行,所以針對請求的處理器可以表示為一個Func<HttpContext, Task>類型的委托。由于這樣的委托會被廣泛地使用,所以ASP.NET Core直接定義了一個專門的委托類型,就是我們在程序中使用到的RequestDelegate。從如下所示的針對RequestDelegate類型的定義可以看出,它本質上就是一個Func<HttpContext, Task>委托。
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。
var?app?=?WebApplication.Create(args);
app.Run(handler:?HandleAsync);
app.Run();
static?Task?HandleAsync(HttpContext?httpContext)???
=>?httpContext.Response.WriteAsync("Hello,?World!");
[104]使用原始形態的中間件
承載的ASP.NET Core應用最終體現為由注冊中間件構建的請求處理管道。在服務器接收到請求并將成功構建出HttpContext上下文之后,會將請求交付給這個管道進行處理。待管道完成了處理任務之后,控制權再次回到服務器的手中,它會將處理的結果轉換成響應發送出去。從應用編程的角度來看,這個管道體現為上述的RequestDelegate委托,組成它的單個中間件則體現為另一個類型為Func<RequestDelegate,RequestDelegate>的委托,該委托的輸入和輸出都是一個RequestDelegate對象,前者表示由后續中間件構建的管道,后者代表將當前中間件納入此管道后生成的新管道。
在上面演示的實例中,我們將一個RequestDelegate委托作為參數調用了WebApplication的Run擴展方法,我們當時說這是為應用設置一個請求處理器。其實這種說法不夠準確,該方法僅僅是注冊一個中間件而已。說得更加具體一點,這個方法用于注冊處于管道末端的中間件。為了讓讀者體驗到中間件和管道針對請求的處理,我們對上面演示應用進行了如下的改寫。
var?app?=?WebApplication.Create(args);
IApplicationBuilder?appBuilder?=?app;
appBuilder.Use(middleware:?HelloMiddleware).Use(middleware:?WorldMiddleware);
app.Run();
static?RequestDelegate?HelloMiddleware(RequestDelegate?next)=>?async?httpContext?=>?{await?httpContext.Response.WriteAsync("Hello,?");await?next(httpContext);
};static?RequestDelegate?WorldMiddleware(RequestDelegate?next)
=>?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>委托類型具有一致聲明的本地靜態方法。
var?app?=?WebApplication.Create(args);
app.Use(middleware:?HelloMiddleware).Use(middleware:?WorldMiddleware);
app.Run();
static?async?Task?HelloMiddleware(HttpContext?httpContext,?RequestDelegate?next)
{await?httpContext.Response.WriteAsync("Hello,?");await?next(httpContext);
};
static?Task?WorldMiddleware(HttpContext?httpContext,?RequestDelegate?next)?=>?httpContext.Response.WriteAsync("World!");
[106]使用中間件委托變體(2)
下面的程序以類似的方式將這兩個中間件替換成與Func<HttpContext, Func<Task>, Task>委托類型具有一致聲明的本地方法。當我們調用WebApplication的Use方法將這兩種“變體”注冊為中間件的時候,該方法內部會將提供的委托轉換成Func<RequestDelegate,RequestDelegate>類型。
var?app?=?WebApplication.Create(args);
app.Use(middleware:?HelloMiddleware).Use(middleware:?WorldMiddleware);
app.Run();
static?async?Task?HelloMiddleware(HttpContext?httpContext,?Func<Task>?next)
{await?httpContext.Response.WriteAsync("Hello,?");await?next();
};
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”命名空間的導入時不要感到奇怪。
namespace?App
{????public?interface?IGreeter{????????string?Greet(DateTimeOffset?time);}????public?class?Greeter?:?IGreeter{????????public?string?Greet(DateTimeOffset?time)?=>?time.Hour?switch{var?h?when?h?>=?5?&&?h?<?12?=>?"Good?morning!",var?h?when?h?>=?12?&&?h?<?17?=>?"Good?afternoon!",_?=>?"Good?evening!"};}
}
我們定義了如下這個名為GreetingMiddleware的中間件類型。如代碼片段所示,該類型實現了IMiddleware接口,針對請求的處理實現在InvokeAsync方法中。我們在GreetingMiddleware類型的構造函數中注入了IGreeter對象,并利用它在實現的InvokeAsync方法中根據當前時間來提供對應的問候語,后者將作為請求的響應內容。
public?class?GreetingMiddleware?:?IMiddleware
{????private?readonly?IGreeter?_greeter;????public?GreetingMiddleware(IGreeter?greeter)????=>?_greeter?=?greeter;????public?Task?InvokeAsync(HttpContext?context,?RequestDelegate?next)=>?context.Response.WriteAsync(_greeter.Greet(DateTimeOffset.Now));
}
針對GreetingMiddleware中間件的應用體現在如下的程序中。如代碼片段所示,我們調用了WebApplication對象的UseMiddleware<GreetingMiddleware>擴展方法注冊了這個中間件。由于強類型中間件實例是由依賴注入容器在需要的時候實時提供的,所以我們必須預先將它注冊為服務。注冊的注冊最終會添加到WebApplicationBuilder的Services屬性返回的IServiceCollection對象上,我們在得到這個對象后通過調用它的AddSingleton< GreetingMiddleware >方法將該中間件注冊為“單例服務”。由于中間件依賴IGreeter服務,所以我們調用AddSingleton<IGreeter, Greeter>擴展方法對該服務進行了注冊。
using?App;
var?builder?=?WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IGreeter,?Greeter>().AddSingleton<GreetingMiddleware>();
var?app?=?builder.Build();
app.UseMiddleware<GreetingMiddleware>();
app.Run();
該程序啟動之后,針對它的請求會得到根據當前時間的生成問候語。如圖5所示,由于目前的時間為晚上七點,所以瀏覽器上顯示“Good evening!”。
圖5 自定義中間件返回的問候語
[108]定義基于約定的中間件類型(構造函數注入)
中間件類型其實并不一定非得實現某個接口,或者繼承某個基類,按照既定的約定進行定義即可。按照ASP.NET Core的約定,中間件類型需要定義成一個公共實例類型(靜態類型無效),其構造函數可以注入任意的依賴服務,但必須包含一個RequestDelegate類型的參數,該參數表示由后續中間件構建的管道,當前中間件利用它將請求分發給后續管道作進一步處理。針對請求的處理實現在一個命名為InvokeAsync或者Invoke的方法中,該方法返回類型為Task, 第一個參數并綁定為當前的HttpContext上下文,所以GreetingMiddleware中間件類型可以改寫成如下的形式。
public?class?GreetingMiddleware
{????private?readonly?IGreeter?_greeter;????public?GreetingMiddleware(RequestDelegate?next,?IGreeter?greeter)?=>?_greeter?=?greeter;????public?Task?InvokeAsync(HttpContext?context)?=>?context.Response.WriteAsync(_greeter.Greet(DateTimeOffset.Now));
}
強類型的中間件實例是在對請求進行處理的時候由依賴注入容器實時提供的,按照約定定義的中間件實例則不同,當我們在注冊中間件的時候就已經利用依賴注入容器將它創建出來,所以前者可以采用不同的生命周期模式,后者總是一個單例對象。也正是因為這個原因,我們不需要將中間件注冊為服務。
using?App;
var?builder?=?WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IGreeter,?Greeter>();
var?app?=?builder.Build();
app.UseMiddleware<GreetingMiddleware>();
app.Run();
[109]定義基于約定的中間件類型(方法注入)
對于按照約定定義的中間件類型,依賴服務不一定非要注入到構造函數中,它們選擇直接注入到InvokeAsync或者Invoke方法中,所以上面這個GreetingMiddleware中間件也可以定義成如下的形式。對于按照約定定義的中間件類型,構造函數注入和方法注入并不是等效,兩者之間的差異會在第18章“應用承載(下)”中進行介紹。
public?class?GreetingMiddleware
{????public?GreetingMiddleware(RequestDelegate?next){}????public?Task?InvokeAsync(HttpContext?context,?IGreeter?greeter)?=>?context.Response.WriteAsync(greeter.Greet(DateTimeOffset.Now));
}
[110]配置的應用
開發ASP.NET Core應用過程會廣泛使用到配置(Configuration),ASP.NET Core采用了一個非常靈活的配置框架,我們可以存儲在任何載體的數據作為配置源。我們還可以將結構化的配置轉換成對應的選項(Options)類型,以強類型的方式來使用它們。針對配置選項的系統介紹被放在第5章“配置選項(上)”和第6章“配置選項(下)”中,我們先在這里“預熱”一下。在前面演示的實例中,Greeter類型針對指定時間提供的問候語依然是以“硬編碼”的方式提供的,現在我們選擇將它們放到配置文件以方便進行調整中。為此我們在項目根目錄下添加一個名為“appsettings.json”的配置文件,并將三條問候語以如下的形式定義在這個JSON文件中。
{"greeting":?{"morning":?"Good?morning!","afternoon":?"Good?afternoon!","evening":?"Good?evening!"}
}
ASP.NET Core應用中的配置通過IConfiguration對象表示,我們可以采用依賴注入的形式“自由”地使用它。對于演示的程序來說,我們只需要按照如下的方式將IConfiguration對象注入到Greeter類型的構造函數中,然后調用其GetSection方法得到定義了上述問候語的配置節(“greeting”)。在實現的Greet方法中,我們以索引的方式利用指定的Key(“morning”、“afternoon”和“evening”)提取對應的問候語。由于應用啟動的時候會自動加載這個按照約定命名的(“appsettings.json”)配置文件,所以演示程序的其他地方不要作任何修改。
public?class?Greeter?:?IGreeter
{????private?readonly?IConfiguration?_configuration;????public?Greeter(IConfiguration?configuration)????=>?_configuration?=?configuration.GetSection("greeting");????public?string?Greet(DateTimeOffset?time)?=>?time.Hour?switch?{var?h?when?h?>=?5?&&?h?<?12?=>?_configuration["morning"],var?h?when?h?>=?12?&&?h?<?17=>?_configuration["afternoon"],_?=>?_configuration["evening"],};
}
[111]Options的應用
正如前面所說,將結構化的配置轉換成對應類型的Options對象,以強類型的方式來使用它們是更加推薦的編程模式。為此我們為配置的三條問候語定義了如下這個GreetingOptions配置選項類型。
public?class?GreetingOptions
{????public?string?Morning?{?get;?set;?}???public?string?Afternoon?{?get;?set;?}????public?string?Evening?{?get;?set;?}?
}
雖然Options對象不能直接以依賴服務的形式進行注入,但卻可以由注入的IOptions<TOptions>對象來提供。如下面的代碼片段所示,我們在Greeter類型的構造函數中注入了IOptions<GreetingOptions>對象,并利用其Value屬性中得到了我們需要的GreetingOptions對象。在有了這個對象后,實現的Greet方法中只需要從對應的屬性中獲取相應的問候語就可以了。
public?class?Greeter?:?IGreeter
{????private?readonly?GreetingOptions?_options;????public?Greeter(IOptions<GreetingOptions>?optionsAccessor)?????=>?_options?=?optionsAccessor.Value;????public?string?Greet(DateTimeOffset?time)?=>?time.Hour?switch{var?h?when?h?>=?5?&&?h?<?12?=>?_options.Morning,var?h?when?h?>=?12?&&?h?<?17?=>?_options.Afternoon,_?=>?_options.Evening};
}
由于IOptions<GreetingOptions>對象提供的配置選項不能無中生有(實際上存在于配置中),我們需要將對應的配置節(“greeting”)綁定到GreetingOptions對象上。這項工作其實也屬于服務注冊的范疇,具體可以按照如下的形式調用IServiceCollection對象的Configure<TOptions>擴展方法來完成。如代碼片段所示,代表應用整體配置的IConfiguration對象來源于WebApplicationBuilder的Configuration屬性。
using?App;
var?builder?=?WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IGreeter,?Greeter>().Configure<GreetingOptions>(builder.Configuration.GetSection("greeting"));
var?app?=?builder.Build();
app.UseMiddleware<GreetingMiddleware>();
app.Run();
[112]日志的應用
診斷日志對于糾錯排錯必不可少。ASP.NET Core采用的診斷日志框架強大、易用且靈活。在我們演示的程序中,Greeter類型會根據指定的時間返回對應的問候語,現在我們將時間和對應的問候語以日志的方式記錄下來看看兩者是否匹配。我們在前面曾說過,依賴注入是ASP.NET Core應用最基本的編程模式。我們將涉及的功能(不論是業務相關的還是業務無關的)進行拆分,最終以具有不同粒度的服務將整個應用化整為零,服務之間的依賴關系直接以注入的方式來解決。我們在前面演示了針對配置選項的注入,接下來我們用來記錄日志的ILogger對象依然采用注入的方式獲得。如下面的代碼片段所示,我們在Greeter類型的構造函數中注入了ILogger<Greeter>對象。在實現的Greet方法中,我們調用該對象的LogInformation擴展方法記錄了一條Information等級的日志,日志內容體現了時間與問候語文本之間的映射關系。
public?class?Greeter?:?IGreeter
{????private?readonly?GreetingOptions?_options;????private?readonly?ILogger?_logger;????public?Greeter(IOptions<GreetingOptions>?optionsAccessor,?ILogger<Greeter>?logger){_options?=?optionsAccessor.Value;_logger?=?logger;}????public?string?Greet(DateTimeOffset?time){var?message?=?time.Hour?switch{var?h?when?h?>=?5?&&?h?<?12?=>?_options.Morning,var?h?when?h?>=?12?&&?h?<?17?=>?_options.Afternoon,_?=>?_options.Evening};_logger.LogInformation("{time}?=>?{message}",time,?message);????????return?message;}
}
采用Minimal API編寫的ASP.NET Core應用會默認將診斷日志整合進來,所以整個演示程序的其它地方都不要修改。當修改后的應用啟動之后,針對每一個請求都會通過日志留下“痕跡”。由于控制臺是默認開啟的日志輸出渠道之一,日志內容直接會輸出到控制臺上。圖5所示的是以命令行形式啟動應用的控制臺,上面顯示的都是以日志形式輸出的內容。在眾多系統日志中,我們發現有一條是由Greeter對象輸出的。
圖5 輸出到控制臺上的日志