ASP.NET Core 6框架揭秘實例演示[02]:基於路由、MVC和gRPC的應用開發


ASP.NET Core可以視為一種底層框架,它為我們構建出了基於管道的請求處理模型,這個管道由一個服務器和多個中間件構成,而與路由相關的EndpointRoutingMiddleware和EndpointMiddleware是兩個最為重要的中間件。MVC和gRPC開發框架就建立在路由基礎上。本篇提供了四個實例用來演示如何利用路由、MVC和gRPC來開發API/APP。(本篇提供的實例已經匯總到《ASP.NET Core 6框架揭秘-實例演示版》)

[113]路由的應用(源代碼
[114]開發MVC API(源代碼
[115]開發MVC APP(源代碼
[116]開發gRPC API(源代碼

[113]路由的應用

ASP.NET Core的路由是由EndpointRoutingMiddleware和EndpointMiddleware這兩個中間件實現的,在所有預定義的中間件類中,這應該算是最重要的兩個中間件了,因為不僅僅是MVC和gRPC框架建立在路由系統之上,后面介紹的Dapr.NET針對發布訂閱和Actor編程模式也是如此。如下面的代碼片段所示,我們在利用WebApplicationBuilder將代表承載應用的WebApplication對象構建出來之后,並沒有注冊任何的中間件,而是調用它的MapGet擴展方法注冊了一個指向路徑“/greet”的路由終結點(Endpoint)。該終結點的處理器是一個指向Greet方法的委托,意味着請求路徑為“/greet”的GET請求會路由到這個終結點,並最終調用這個方法進行處理。

using App;
var builder = WebApplication.CreateBuilder(args);
builder.Services
    .AddSingleton<IGreeter, Greeter>()
    .Configure<GreetingOptions>(builder.Configuration.GetSection("greeting"));
var app = builder.Build();
app.MapGet("/greet", Greet);
app.Run();

static string Greet(IGreeter greeter) => greeter.Greet(DateTimeOffset.Now);

ASP.NET Core的路由系統的強大之處在於,我們可以使用任何類型的委托作為注冊終結點的處理器,路由系統在調用處理器方法之前會“智能地”提取相應的數據初始化每一個參數。當方法執行之后,它還會針對我們具體返回的對象來對請求實施響應。對於我們提供的Greet方法來說,路由系統在調用它之前會利用依賴注入容器提供作為參數的IGreeter對象。由於返回的是一個字符串,文本經過編碼后會直接作為響應的主體內容, 響應的內容類型(Content-Type)最終會被設置為“text/plain”。程序啟動之后,如果我們利用瀏覽器請求“/greet”這個路徑,針對當前時間解析出來的問候語會以圖1的形式呈現出來。

clip_image002
圖1 采用路由返回的問候

[114]開發MVC API

我們直接將上面演示的程序改寫成MVC應用。MVC應用以Controller為核心,所有的請求總是指向定義在某個Controller類型中的某個Action方法。當應用接收到請求之后,會激活對應的Controller對象,並通過執行對應的Action方法來處理該請求。按照約定,合法的Controller類型必須是以“Controller”作為后綴命名的公共實例類型。我們一般會讓定義的Controller類型派生自Controller基類以“借用”一些有用的API,但這不是必須的,比如下面定義的GreetingController就沒有指定基類。

public class GreetingController
{
    [HttpGet("/greet")]
    public string Greet([FromServices] IGreeter greeter) => greeter.Greet(DateTimeOffset.Now);
}

由於MVC框架是建立在路由系統之上的,定義在Controller類型中的Action方法最終會轉換成一個或者多個注冊到指定路徑模板的終結點。對於定義在GreetingController類型中的Action方法Greet來說,我們通過標注的HttpGetAttrbute特性不僅為對應的路由終結點定義了針對HTTP方法的約束(該終結點僅限於處理GET請求),還同時指定了綁定的請求路徑(“/greet”)。

依賴的服務可以直接注入到Controller類型中。具體來說,它支持兩種注入形式,一種是注入到構造函數中,另一種則是直接注入到Action方法中。對於方法注入,對應參數上必須標注一個FromServiceAttribute特性。我們IGreeter對象就是采用這種方式注入注入到Greet方法中的。和路由系統針對返回對象的處理方式一樣,MVC框架針對Action方法的返回值也會根據其類型進行針對性的處理。Greet方法直接返回的字符串會直接作為響應的主體內容,響應的內容類型(Content-Type)會被設置為“text/plain”。

在完成了針對GreetingController類型的定義之后,我們需要對入口程序進行如下的修改。如代碼片段所示,在完成了針對IGreeter服務的注冊和針對GreetingOptions配置選項的設置之后,我們調用同一個IServiceCollection對象的AddControllers擴展方法注冊了與Controller相關服務的注冊。在WebApplication對象被構建出來后,我們調用了它的MapControllers擴展方法將定義在所有Controller類型中的Action方法映射為對應的終結點。程序啟動之后,如果我們利用瀏覽器請求“/greet”這個路徑,我們依然會得到如圖1的所示的輸出結果。

using App;
var builder = WebApplication.CreateBuilder(args);
builder.Services
    .AddSingleton<IGreeter, Greeter>()
    .Configure<GreetingOptions>(builder.Configuration.GetSection("greeting"))
    .AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();

[115]開發MVC APP

上面改造的MVC程序並沒有涉及到視圖,請求的響應內容是由Action方法直接提供的,現在我們利用視圖來呈現最終響應的內容。由於上個例子調用IServiceCollection接口的AddControllers擴展方法只會注冊Controller相關的服務,現在我們得將其換成AddControllersWithViews方法。顧名思義,新的擴展方法會將視圖相關的服務添加進來。

using App;
var builder = WebApplication.CreateBuilder(args);
builder.Services
    .AddSingleton<IGreeter, Greeter>()
    .Configure<GreetingOptions>(builder.Configuration.GetSection("greeting"))
    .AddControllersWithViews();
var app = builder.Build();
app.MapControllers();
app.Run();

我們對GreetinigController進行了改造。如下面的代碼片段所示,我們讓它繼承Controller這個基類。Action方法Greet的返回類型改為IActionResult接口,具體返回的是通過View方法創建的代表默認視圖(針對當前Action方法)的ViewResult對象。在Action方法返回之前,它還利用對ViewBag的設置將當前時間傳遞到呈現的視圖中。

public class GreetingController : Controller
{
    [HttpGet("/greet")]
    public IActionResult Greet()
    {
        ViewBag.Time = DateTimeOffset.Now;
        return View();
    }
}

ASP.NET Core MVC采用Razior視圖引擎,視圖被定義成一個后綴名為.cshtml的文件,這是一個按照Razor語法編寫的靜態HTML和動態C#代碼動態交織的文本文件。由於上面為了呈現試圖調用的View方法沒有指定任何參數,所以視圖引擎會根據當前Controller的名稱(“Greeting”)和Action的名稱(“Greet”)去定位定義目標視圖的.cshtml文件。為了迎合默認的視圖定位規則,我們需要采用Action的名稱來命名創建的視圖文件(Greet.cshtml),並將其添加到“Views/Greeting”目錄下。

@using App
@inject IGreeter Greeter;
<html>
    <head>
        <title>Greeting</title>
    </head>
    <body>
        <p>@Greeter.Greet((DateTimeOffset)ViewBag.Time)</p>
    </body>
</html>

上面這個代碼片段就是添加的視圖文件(Views/Greeting/Greet.cshtml)的內容。總體來說,這是一個HTML文檔,除了在主體部分呈現的問候語文本(前置的@字符定義動態執行的C#表達式)是根據指定時間動態解析出來的,其他內容則均為靜態的HTML。我們借助@inject指令將依賴的IGreeter對象以屬性的形式注入進來,並且將屬性名稱設置為Greeter,所以我們可以在視圖中直接調用它的Greet方法得到呈現的問候語。調用Greet方法指定的時間是GreetingController利用ViewBag傳遞過來的,所以我們可以直接利用它將其提取出來。程序啟動之后,如果我們利用瀏覽器請求“/greet”這個路徑,雖然瀏覽器也會呈現出相同的文本(如圖2所示),但是響應的內容是完全不同的。之前響應的僅僅是內容類型為“text/plain”的單純文本,現在響應則是一份完整的HTML文檔,內容類型為“text/html”。

clip_image002[5]
圖2 以試圖形式返回的問候

[116]開發gRPC API

雖然Vistual Studio提供了創建gRPC的項目模板,該模板提供的腳手架會自動為我們創建一系列的初始文件,同時也會對項目做一些初始設置,但這反而是筆者不想要的,至少是不希望在這里使用這個模板。和前面一樣,我們希望演示的實例只包含最本質和必要的元素,所以我們選擇在一個空的解決方案上構建gRPC應用。

image
圖3 gRPC解決方案

如圖3所示,我們在一個空的解決方案上添加了三個項目。Proto是一個空的類庫項目,我們將會使用它來存放標准的Proto Buffers消息和gRPC服務的定義;Server是一個空的ASP.NET Core應用,gRPC服務的實現類型就放在這里,它同時也是承載gRPC服務的應用。Client是一個控制台程序,我們用它來模擬調用gRPC服務的客戶端。gRPC是語言中立的遠程調用框架,gRPC服務契約使用到的數據類型都采用標准的定義方式。具體來說,gRPC傳輸的數據采用Proto Buffers協議進行序列化,Proto Buffers采用高效緊湊的二進制編碼。我們將用於定義數據類型和服務的Proto Buffers文件定義在Proto項目中,在這之前我們需要為這個空的類庫項目添加針對“Grpc.AspNetCore”這個NuGet包的引用。

不再使用簡單的“Hello World”,現在我們為演示的gPRC服務指定另一種稍微“復雜”一點的應用場景——用它來完成簡單的加、減、乘、除運算。我們在Proto項目中添加一個名為Calculator.proto的文本文件,並在其中以如下的形式將Calculator這個rGPC服務定義出來。如代碼片段所示,這個服務包含四個操作,它們的輸入和輸出都被定義成Proto Buffers消息。作為輸入的InputMessage消息包含兩個整型的數據成員(表示運算的兩個操作數)。返回的OutpuMessage消息除了通過result表示計算結果外,還具有status和error兩個成員,前者表示計算狀態(成功還是失敗),后者提供計算失敗時的錯誤消息。

syntax = "proto3";
option csharp_namespace = "App";

 service Calculator {
   rpc Add (InputMessage) returns (OutpuMessage);
   rpc Substract (InputMessage) returns (OutpuMessage);
   rpc Multiply (InputMessage) returns (OutpuMessage);
   rpc Divide (InputMessage) returns (OutpuMessage);
 }

 message InputMessage {
   int32		x		= 1;
   int32		y		= 2;
 }

 message OutpuMessage {
   int32		status		= 1;
   int32		result		= 2;
   string	               error		= 3;
 }

創建的Calculator.proto文件無法直接使用,我們需要利用內置的代碼生成器將它轉換成.cs代碼。具體的作為很簡單,我們只需要在Visual Studio的解決方案窗口中右鍵選擇這個文件,打開如圖4所示的屬性對話框。我們在Build Action下拉列表中選擇“Protobuf compiler”選項,同時在gRPC Stub Classes下拉列表中選擇“Client and Server”。

image
圖4 Calculator.proto文件屬性對話框

做了這樣的設置之后,在任何時對Calculator.proto文件所作的改變都將觸發代碼的自動生成,具體生成的.cs文件會自動保存在obj目錄下。由於在gRPC Stub Classes下拉列表中選擇了“Client and Server”選項,所以它不僅會生成服務端用來定義服務實現類型的Stub類,還會生成客戶端用來調用服務的Stub類。上面以可視化形式所作的設置最終會體現在項目文件(Proto.csproj)上,所以我們直接修改此文件也可以達到相同的目的,如下所示的就是這個文件的完整內容。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <None Remove="Calculator.proto" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.40.0" />
  </ItemGroup>
  <ItemGroup>
    <Protobuf Include="Calculator.proto" />
  </ItemGroup>
</Project>

Proto項目中的Calculator.proto文件僅僅是按照標准的形式定義的“服務契約”,我們需要在Server項目中定義具體的實現類型。在添加了針對Proto項目的引用之后,我們定義了如下這個名為CalculatorService的gRPC服務實現類型。如代碼片段所示,我們讓CalculatorService類型繼承自一個內嵌於Calculator中的CalculatorBase類型,這個Calculator類型就是根據Calculator.proto生成的一個類型。

public class CalculatorService : Calculator.CalculatorBase
{
    private readonly ILogger _logger;
    public CalculatorService(ILogger<CalculatorService> logger) => _logger = logger;

    public override Task<OutpuMessage> Add(InputMessage request,  ServerCallContext context) => InvokeAsync((op1, op2) => op1 + op2, request);
    public override Task<OutpuMessage> Substract(InputMessage request,    ServerCallContext context) => InvokeAsync((op1, op2) => op1 - op2, request);
    public override Task<OutpuMessage> Multiply(InputMessage request,     ServerCallContext context) => InvokeAsync((op1, op2) => op1 * op2, request);
    public override Task<OutpuMessage> Divide(InputMessage request,     ServerCallContext context) => InvokeAsync((op1, op2) => op1 / op2, request);

    private Task<OutpuMessage> InvokeAsync(Func<int, int, int> calculate,     InputMessage input)
    {
        OutpuMessage output;
        try
        {
            output = new OutpuMessage { Status = 0, Result = calculate(input.X, input.Y) };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Calculation error.");
            output = new OutpuMessage { Status = 1, Error = ex.ToString() };
        }
        return Task.FromResult(output);
    }
}

Calculator.proto文件為Calcultor服務定義的四個操作會轉換成CalculatorBase類型中對應的虛方法,我們按照上面的方式重寫了它們。在完成了針對gRPC服務實現類型的定義之后,我們需要對承載它的入口程序定義編寫如下的代碼。由於gRPC采用HTTP2傳輸協議,所以在利用WebApplicationBuilder的WebHost屬性得到對應的IWebHostBuilder對象,我們調用其ConfigureKestrel擴展方法讓默認注冊的Kestrel服務器監聽的終結點默認采用HTTP2協議。gRPC相關的服務通過調用IServiceCollection接口的AddGrpc擴展方法進行注冊。由於gRPC也是建立在路由系統之上的,定義在服務中的每個操作最終也會轉換成相應的路由終結點,這些終結點的生成和注冊是通過調用WebApplication對象的MapGrpcService<TService>擴展方法完成的。

using App;
using Microsoft.AspNetCore.Server.Kestrel.Core;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(kestrel => kestrel.ConfigureEndpointDefaults( endpoint => endpoint.Protocols =  HttpProtocols.Http2));
builder.Services.AddGrpc();
var app = builder.Build();
app.MapGrpcService<CalculatorService>();
app.Run();

Calculator.proto文件生成的代碼包含用來調用對應gRPC服務的Stub類,所以模擬客戶端的Client項目也需要添加對Proto項目的引用。在此之后,我們可以編寫如下的程序調用gRPC服務完成四種基本的數學運算。

 using App;
 using Grpc.Core;
 using Grpc.Net.Client;

 using var channel = GrpcChannel.ForAddress("http://localhost:5000");
 var client = new Calculator.CalculatorClient(channel);
 var inputMessage = new InputMessage { X = 1, Y = 0 };

 await InvokeAsync(input => client.AddAsync(input), inputMessage, "+");
 await InvokeAsync(input => client.SubstractAsync(input), inputMessage, "-");
 await InvokeAsync(input => client.MultiplyAsync(input), inputMessage, "*");
 await InvokeAsync(input => client.DivideAsync(input), inputMessage, "/");

 static async Task InvokeAsync(Func<InputMessage, AsyncUnaryCall<OutpuMessage>> invoker,  InputMessage input, string @operator)
 {
     var output = await invoker(input);
     if (output.Status == 0)
     {
         Console.WriteLine($"{input.X}{@operator}{input.Y}={output.Result}");
     }
     else
     {
         Console.WriteLine(output.Error);
     }
 }

如上面的代碼片段所示,我們通過調用GrpcChannel類型的靜態方法ForAddress針對gRPC服務的地址“http://localhost:5000”創建了一個GrpcChannel對象,該對象表示與服務進行通信的“信道(Channel)”。我們利用它創建了一個CalculatorClient對象作為調用gRPC服務的客戶端或者代理,CalculatorClient類型同樣是內嵌在生成的Calculator類型中。最終我們利用這個代理完成了針對四種基本運算的服務調用,具體的gRPC調用實現在InvokeAsync這個本地方法中。接下來我們以命令行的方式先后啟動Server和Client應用,客戶端和服務端控制台上會呈現出如圖5所示的輸出結果。由於我們傳入的參數分別為1和0,所以除了除法運算,其它三此調用都會返回成功的結果,針對除法的調用則會將錯誤信息呈現出來。由於CalculatorService進行了異常處理,並且將異常信息以日志的形式記錄了下來,所以錯誤信息也輸出到了服務端的控制台上。

image

圖5 gRPC應用的承載與調用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM