飙血推荐
  • HTML教程
  • MySQL教程
  • JavaScript基础教程
  • php入门教程
  • JavaScript正则表达式运用
  • Excel函数教程
  • UEditor使用文档
  • AngularJS教程
  • ThinkPHP5.0教程

ASP.NET Core 6框架揭秘实例演示[02]:基于路由、MVC和gRPC的应用开发

时间:2022-02-15  作者:artech  
域名 Core可以视为一种底层框架,它为我们构建出了基于管道的请求处理模型,这个管道由一个服务器和多个中间件构成,而与路由相关的EndpointRoutingMiddleware和EndpointMiddleware是两个最为重要的中间件。MVC和gRPC开发框架就建立在路由基础上。本篇提供了四个实例用来演示如何利用路由、MVC和gRPC来开发API/APP。

域名 Core可以视为一种底层框架,它为我们构建出了基于管道的请求处理模型,这个管道由一个服务器和多个中间件构成,而与路由相关的EndpointRoutingMiddleware和EndpointMiddleware是两个最为重要的中间件。MVC和gRPC开发框架就建立在路由基础上。本篇提供了四个实例用来演示如何利用路由、MVC和gRPC来开发API/APP。

[113]路由的应用(源代码)
[114]开发MVC API(源代码)
[115]开发MVC APP(源代码)
[116]开发gRPC API(源代码)

[113]路由的应用

域名 Core的路由是由EndpointRoutingMiddleware和EndpointMiddleware这两个中间件实现的,在所有预定义的中间件类中,这应该算是最重要的两个中间件了,因为不仅仅是MVC和gRPC框架建立在路由系统之上,后面介绍的域名针对发布订阅和Actor编程模式也是如此。如下面的代码片段所示,我们在利用WebApplicationBuilder将代表承载应用的WebApplication对象构建出来之后,并没有注册任何的中间件,而是调用它的MapGet扩展方法注册了一个指向路径“/greet”的路由终结点(Endpoint)。该终结点的处理器是一个指向Greet方法的委托,意味着请求路径为“/greet”的GET请求会路由到这个终结点,并最终调用这个方法进行处理。

  1 using App;
  2 var builder = 域名teBuilder(args);
  3 域名ices
  4     .AddSingleton<IGreeter, Greeter>()
  5     .Configure<GreetingOptions>(域名ection("greeting"));
  6 var app = 域名d();
  7 域名et("/greet", Greet);
  8 域名();
  9 
 10 static string Greet(IGreeter greeter) => 域名t(域名);

域名 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就没有指定基类。

  1 public class GreetingController
  2 {
  3     [HttpGet("/greet")]
  4     public string Greet([FromServices] IGreeter greeter) => 域名t(域名);
  5 }

由于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的所示的输出结果。

  1 using App;
  2 var builder = 域名teBuilder(args);
  3 域名ices
  4     .AddSingleton<IGreeter, Greeter>()
  5     .Configure<GreetingOptions>(域名ection("greeting"))
  6     .AddControllers();
  7 var app = 域名d();
  8 域名ontrollers();
  9 域名();

[115]开发MVC APP

上面改造的MVC程序并没有涉及到视图,请求的响应内容是由Action方法直接提供的,现在我们利用视图来呈现最终响应的内容。由于上个例子调用IServiceCollection接口的AddControllers扩展方法只会注册Controller相关的服务,现在我们得将其换成AddControllersWithViews方法。顾名思义,新的扩展方法会将视图相关的服务添加进来。

  1 using App;
  2 var builder = 域名teBuilder(args);
  3 域名ices
  4     .AddSingleton<IGreeter, Greeter>()
  5     .Configure<GreetingOptions>(域名ection("greeting"))
  6     .AddControllersWithViews();
  7 var app = 域名d();
  8 域名ontrollers();
  9 域名();
 10 

我们对GreetinigController进行了改造。如下面的代码片段所示,我们让它继承Controller这个基类。Action方法Greet的返回类型改为IActionResult接口,具体返回的是通过View方法创建的代表默认视图(针对当前Action方法)的ViewResult对象。在Action方法返回之前,它还利用对ViewBag的设置将当前时间传递到呈现的视图中。

  1 public class GreetingController : Controller
  2 {
  3     [HttpGet("/greet")]
  4     public IActionResult Greet()
  5     {
  6         域名 = 域名;
  7         return View();
  8     }
  9 }
 10 

域名 Core MVC采用Razior视图引擎,视图被定义成一个后缀名为.cshtml的文件,这是一个按照Razor语法编写的静态HTML和动态C#代码动态交织的文本文件。由于上面为了呈现试图调用的View方法没有指定任何参数,所以视图引擎会根据当前Controller的名称(“Greeting”)和Action的名称(“Greet”)去定位定义目标视图的.cshtml文件。为了迎合默认的视图定位规则,我们需要采用Action的名称来命名创建的视图文件(域名ml),并将其添加到“Views/Greeting”目录下。

  1 @using App
  2 @inject IGreeter Greeter;
  3 <html>
  4     <head>
  5         <title>Greeting</title>
  6     </head>
  7     <body>
  8         <p>@域名t((DateTimeOffset)域名)</p>
  9     </body>
 10 </html>
 11 

上面这个代码片段就是添加的视图文件(Views/Greeting/域名ml)的内容。总体来说,这是一个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是一个空的域名 Core应用,gRPC服务的实现类型就放在这里,它同时也是承载gRPC服务的应用。Client是一个控制台程序,我们用它来模拟调用gRPC服务的客户端。gRPC是语言中立的远程调用框架,gRPC服务契约使用到的数据类型都采用标准的定义方式。具体来说,gRPC传输的数据采用Proto Buffers协议进行序列化,Proto Buffers采用高效紧凑的二进制编码。我们将用于定义数据类型和服务的Proto Buffers文件定义在Proto项目中,在这之前我们需要为这个空的类库项目添加针对“域名etCore”这个NuGet包的引用。

不再使用简单的“Hello World”,现在我们为演示的gPRC服务指定另一种稍微“复杂”一点的应用场景——用它来完成简单的加、减、乘、除运算。我们在Proto项目中添加一个名为域名o的文本文件,并在其中以如下的形式将Calculator这个rGPC服务定义出来。如代码片段所示,这个服务包含四个操作,它们的输入和输出都被定义成Proto Buffers消息。作为输入的InputMessage消息包含两个整型的数据成员(表示运算的两个操作数)。返回的OutpuMessage消息除了通过result表示计算结果外,还具有status和error两个成员,前者表示计算状态(成功还是失败),后者提供计算失败时的错误消息。

  1 syntax = "proto3";
  2 option csharp_namespace = "App";
  3 
  4 service Calculator {
  5   rpc Add (InputMessage) returns (OutpuMessage);
  6   rpc Substract (InputMessage) returns (OutpuMessage);
  7   rpc Multiply (InputMessage) returns (OutpuMessage);
  8   rpc Divide (InputMessage) returns (OutpuMessage);
  9 }
 10 
 11 message InputMessage {
 12   int32		x		= 1;
 13   int32		y		= 2;
 14 }
 15 
 16 message OutpuMessage {
 17   int32		status		= 1;
 18   int32		result		= 2;
 19   string	        error		= 3;
 20 } 

创建的域名o文件无法直接使用,我们需要利用内置的代码生成器将它转换成.cs代码。具体的作为很简单,我们只需要在Visual Studio的解决方案窗口中右键选择这个文件,打开如图4所示的属性对话框。我们在Build Action下拉列表中选择“Protobuf compiler”选项,同时在gRPC Stub Classes下拉列表中选择“Client and Server”。

image
图4 域名o文件属性对话框

做了这样的设置之后,在任何时对域名o文件所作的改变都将触发代码的自动生成,具体生成的.cs文件会自动保存在obj目录下。由于在gRPC Stub Classes下拉列表中选择了“Client and Server”选项,所以它不仅会生成服务端用来定义服务实现类型的Stub类,还会生成客户端用来调用服务的Stub类。上面以可视化形式所作的设置最终会体现在项目文件(域名oj)上,所以我们直接修改此文件也可以达到相同的目的,如下所示的就是这个文件的完整内容。

  1 <Project Sdk="域名">
  2   <PropertyGroup>
  3     <TargetFramework>net6.0</TargetFramework>
  4     <ImplicitUsings>enable</ImplicitUsings>
  5     <Nullable>enable</Nullable>
  6   </PropertyGroup>
  7   <ItemGroup>
  8     <None Remove="域名o" />
  9   </ItemGroup>
 10   <ItemGroup>
 11     <PackageReference Include="域名etCore" Version="域名.0" />
 12   </ItemGroup>
 13   <ItemGroup>
 14     <Protobuf Include="域名o" />
 15   </ItemGroup>
 16 </Project>
 17 

Proto项目中的域名o文件仅仅是按照标准的形式定义的“服务契约”,我们需要在Server项目中定义具体的实现类型。在添加了针对Proto项目的引用之后,我们定义了如下这个名为CalculatorService的gRPC服务实现类型。如代码片段所示,我们让CalculatorService类型继承自一个内嵌于Calculator中的CalculatorBase类型,这个Calculator类型就是根据域名o生成的一个类型。

  1 public class CalculatorService : 域名ulatorBase
  2 {
  3     private readonly ILogger _logger;
  4     public CalculatorService(ILogger<CalculatorService> logger) => _logger = logger;
  5 
  6     public override Task<OutpuMessage> Add(InputMessage request,  ServerCallContext context) => InvokeAsync((op1, op2) => op1 + op2, request);
  8     public override Task<OutpuMessage> Substract(InputMessage request,    ServerCallContext context) => InvokeAsync((op1, op2) => op1 - op2, request);
 10     public override Task<OutpuMessage> Multiply(InputMessage request,     ServerCallContext context) => InvokeAsync((op1, op2) => op1 * op2, request);
 12     public override Task<OutpuMessage> Divide(InputMessage request,     ServerCallContext context) => InvokeAsync((op1, op2) => op1 / op2, request);
 14 
 15     private Task<OutpuMessage> InvokeAsync(Func<int, int, int> calculate,     InputMessage input)
 16     {
 17         OutpuMessage output;
 18         try
 19         {
 20             output = new OutpuMessage { Status = 0, Result = calculate(input.X, input.Y) };
 22         }
 23         catch (Exception ex)
 24         {
 25             域名rror(ex, "Calculation error.");
 26             output = new OutpuMessage { Status = 1, Error = 域名ring() };
 27         }
 28         return 域名Result(output);
 29     }
 30 }
 31 

域名o文件为Calcultor服务定义的四个操作会转换成CalculatorBase类型中对应的虚方法,我们按照上面的方式重写了它们。在完成了针对gRPC服务实现类型的定义之后,我们需要对承载它的入口程序定义编写如下的代码。由于gRPC采用HTTP2传输协议,所以在利用WebApplicationBuilder的WebHost属性得到对应的IWebHostBuilder对象,我们调用其ConfigureKestrel扩展方法让默认注册的Kestrel服务器监听的终结点默认采用HTTP2协议。gRPC相关的服务通过调用IServiceCollection接口的AddGrpc扩展方法进行注册。由于gRPC也是建立在路由系统之上的,定义在服务中的每个操作最终也会转换成相应的路由终结点,这些终结点的生成和注册是通过调用WebApplication对象的MapGrpcService<TService>扩展方法完成的。

  1 using App;
  2 using 域名域名;
  3 var builder = 域名teBuilder(args);
  4 域名igureKestrel(kestrel => 域名igureEndpointDefaults( endpoint => 域名ocols =  域名2));
  5 域名rpc();
  6 var app = 域名d();
  7 域名rpcService<CalculatorService>();
  8 域名();

域名o文件生成的代码包含用来调用对应gRPC服务的Stub类,所以模拟客户端的Client项目也需要添加对Proto项目的引用。在此之后,我们可以编写如下的程序调用gRPC服务完成四种基本的数学运算。

  1 using App;
  2 using 域名;
  3 using 域名nt;
  4 
  5 using var channel = 域名ddress("http://localhost:5000");
  6 var client = new 域名ulatorClient(channel);
  7 var inputMessage = new InputMessage { X = 1, Y = 0 };
  8 
  9 await InvokeAsync(input => 域名sync(input), inputMessage, "+");
 10 await InvokeAsync(input => 域名tractAsync(input), inputMessage, "-");
 11 await InvokeAsync(input => 域名iplyAsync(input), inputMessage, "*");
 12 await InvokeAsync(input => 域名deAsync(input), inputMessage, "/");
 13 
 14 static async Task InvokeAsync(Func<InputMessage, AsyncUnaryCall<OutpuMessage>> invoker,  InputMessage input, string @operator)
 15 {
 16     var output = await invoker(input);
 17     if (域名us == 0)
 18     {
 19         域名eLine($"{input.X}{@operator}{input.Y}={域名lt}");
 20     }
 21     else
 22     {
 23         域名eLine(域名r);
 24     }
 25 }
 26 

如上面的代码片段所示,我们通过调用GrpcChannel类型的静态方法ForAddress针对gRPC服务的地址“http://localhost:5000”创建了一个GrpcChannel对象,该对象表示与服务进行通信的“信道(Channel)”。我们利用它创建了一个CalculatorClient对象作为调用gRPC服务的客户端或者代理,CalculatorClient类型同样是内嵌在生成的Calculator类型中。最终我们利用这个代理完成了针对四种基本运算的服务调用,具体的gRPC调用实现在InvokeAsync这个本地方法中。接下来我们以命令行的方式先后启动Server和Client应用,客户端和服务端控制台上会呈现出如图5所示的输出结果。由于我们传入的参数分别为1和0,所以除了除法运算,其它三此调用都会返回成功的结果,针对除法的调用则会将错误信息呈现出来。由于CalculatorService进行了异常处理,并且将异常信息以日志的形式记录了下来,所以错误信息也输出到了服务端的控制台上。

image

图5 gRPC应用的承载与调用

标签:编程
湘ICP备14001474号-3  投诉建议:234161800@qq.com   部分内容来源于网络,如有侵权,请联系删除。