五、集成外部组件和处理器

到目前为止,我们一直在开发 FlixOneStore。 在前一章中,我们添加了购物车和运输设施。 然而,一些组织可能不需要这样的设施,因为一些组织在家里什么都有。 例如,我们的 FlixOneStore 需要一个外部组件来帮助我们跟踪分配和支付管理系统。

在本章中,我们将通过代码示例来讨论外部组件。 我们将主要讨论以下议题:

  • 了解中间件
  • 在中间件中将日志记录添加到 API 中
  • 通过构建我们自己的中间件来拦截 HTTP 请求和响应
  • JSON-RPC 用于 RPC 通信

了解中间件

顾名思义,中间件是一种连接两个不同或相似位置的软件。 在软件工程的世界中,中间件是一个软件组件,并被组装在应用管道中以处理请求和响应。

这些组件还可以检查请求是否应该传递给下一个组件,或者请求应该在触发/调用下一个组件之前还是之后由组件处理。 这个请求管道是通过使用请求委托构建的。 这个请求委托与每个 HTTP 请求交互。

看看下面来自 ASP 文档的引用.NET Core(https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/):

"Middleware is software that's assembled into an application pipeline to handle requests and responses."

看看下图,它展示了一个简单的中间件组件的例子:

要求代表

请求由UseRunMapMapWhen扩展方法处理。 这些方法配置请求委托。

为了详细理解这一点,让我们使用ASP.NET Core创建一个虚拟项目。 完成以下步骤:

  1. 打开 Visual Studio。
  2. 转到文件|新建|项目,或按Ctrl+Shift+N。 参考以下截图:

Creating a new project using Visual Studio 2017

  1. 在新项目屏幕上,选择 ASP.NET Core Web 应用。

  2. 命名您的新项目(比如Chap05_01),选择一个位置,然后单击 OK,如下图所示:

Selecting new project template

  1. 从新的 ASP.NET Core Web Application 模板界面,选择 API 模板。 确保你选择了。net Core 和 ASP。 2.0 NET Core。

  2. 单击“确定”,如下图所示:

  1. 解决方案资源管理器打开。 你会看到文件/文件夹结构,如下截图所示:

Showing file/folder structure of Chap05_01 project

从我们刚刚创建的虚拟项目中,打开Startup.cs文件并查看Configure方法,它包含以下代码:

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
  if (env.IsDevelopment())
  {
    app.UseDeveloperExceptionPage();
  }
  app.UseMvc();
}

前面的代码是不言自明的:它告诉系统通过启动Microsoft.AspNetCore.Builder.IApplicationBuilderapp.UseMvc()扩展方法将Mvc添加到请求管道中。

You can get more information on IApplicationBuilder at https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.iapplicationbuilder?view=aspnetcore-2.0.

它还指示系统在开发环境时使用特定的异常页面。 通过上述方法配置应用。

在下一节中,我们将详细讨论四种重要的IApplicationBuilder方法。

使用

方法将委托添加到应用请求管道中。 请看下面的截图,看看这个方法的签名:

Signature of Use method

正如我们在前一节中讨论的,中间件方法可以短路请求管道或将请求传递给下一个委托。

短路一个请求只是结束一个请求。

请看以下关于Use方法的代码:

public void Configure(IApplicationBuilder app)
{
  async Task Middleware(HttpContext context, Func<Task> next)
  {
    //other stuff
    await next.Invoke();
    //other stuff
  }
  app.Use(Middleware);
}

在前面的代码中,我尝试在一个本地函数的帮助下解释Use方法的虚拟实现。 在这里,您可以看到Middleware正在调用或将请求传递给下一个委托,在await next.Invoke();之前或之后。 您可以编写/实现其他代码短语,但这些短语不应该向客户端发送响应,例如那些写入输出、生成 404 状态的代码短语等等。

局部函数是在方法中声明的方法,可以在方法本身的作用域内调用。 这些方法只能由其他方法使用。

运行

Run方法以与Use方法相同的方式向请求管道添加一个委托,但此方法终止请求管道。 请看下面的截图,看看这个方法的签名:

看看下面的代码:

public void Configure(IApplicationBuilder app, ILoggerFactory logger)
{
  logger.AddConsole();
  //add more stuff that does not responses client
  async Task RequestDelegate(HttpContext context)
  {
    await context.Response.WriteAsync("This ends the request or 
    short circuits request.");
  }
  app.Run(RequestDelegate);
}

在前面的代码中,我试图说明Run终止请求管道。 这里,我使用了一个本地函数RequestDelegate

您可以看到,在此之前我添加了一个控制台记录器,并且可以添加更多的代码短语,但不能添加那些将响应发送回客户机的代码短语。 这里,Run通过返回一个字符串结束。 运行 Visual Studio 或按F5-你会得到类似以下截图的输出:

地图

当您想要连接多个中间件实例时,Map方法会有所帮助。 为此,Map调用另一个请求委托。 请看下面的截图,看看这个方法的签名:

Signature of Map method

看看下面的代码:

public void Configure(IApplicationBuilder app)
{
  app.UseMvc();
  app.Map("/testroute", TestRouteHandler);
  async Task RequestDelegate(HttpContext context)
  {
    await context.Response.WriteAsync("This ends the request or 
    short circuit request.");
  }
  app.Run(RequestDelegate);
}

在这段代码中,我添加了一个仅映射<url>/testrouteMap。 接下来是我们前面讨论过的相同的Run方法。 TestRoutehandler是私有方法。 看看下面的代码:

private static void  TestRouteHandler(IApplicationBuilder app) 
{
  async Task Handler(HttpContext context)
  {
    await context.Response.WriteAsync("This is called from testroute.
    " + "This ends the request or short circuit request.");
  }
  app.Run(Handler);
}

app.Run(Handler);之前是一个正常的委托。 现在,运行代码并查看结果。 它们应该类似如下截图:

您可以看到,web 应用的根显示了在Run委托方法中提到的字符串。 你会得到如下截图所示的输出:

在中间件的 API 中添加日志记录

简而言之,日志记录不过是在一个地方获取日志文件的过程或操作,以获取通信期间 api 中发生的事件或其他操作。 在本节中,我们将为我们的产品 api 实现日志记录。

在开始查看如何记录 api 的事件之前,让我们先快速查看一下现有的产品 api。

Refer to the Request delegates section to refresh your memory as to how you can create a new ASP.NET Core project.

下面的截图显示了我们产品 api 的项目结构:

以下是我们的Product模型:

public class Product
{
  public Guid Id { get; set; }
  public string Name { get; set; }
  public string Description { get; set; }
  public string Image { get; set; }
  public decimal Price { get; set; }
  public Guid CategoryId { get; set; }
  public virtual Category Category { get; set; }
}

Product模型是一个类,它代表一个包含属性的产品。

下面是我们的存储库接口:

public interface IProductRepository
{
  void Add(Product product);
  IEnumerable<Product> GetAll();
  Product GetBy(Guid id);
  void Remove(Guid id);
  void Update(Product product);
}

IProductRepository接口具有我们的 api 从产品的操作开始所需要的方法。

让我们来看看我们的ProductRepository课:

public class ProductRepository : IProductRepository
{
  private readonly ProductContext _context;
  public ProductRepository(ProductContext context) => 
  _context = context;
  public IEnumerable<Product> GetAll() => _context.Products.
  Include(c => c.Category).ToList();
  public Product GetBy(Guid id) => _context.Products.Include
  (c => c.Category).FirstOrDefault(x => x.Id == id);
  public void Add(Product product)
  {
    _context.Products.Add(product);
    _context.SaveChanges();
  }
  public void Update(Product product)
  {
    _context.Update(product);
    _context.SaveChanges();
  }
  public void Remove(Guid id)
  {
    var product = GetBy(id);
    _context.Remove(product);
    _context.SaveChanges();
  }
}

类实现了IProductRepository接口。 前面的代码是不言自明的。

打开Startup.cs文件,添加以下代码:

services.AddScoped<IProductRepository, ProductRepository>();
services.AddDbContext<ProductContext>(
o => o.UseSqlServer(Configuration.GetConnectionString
("ProductConnection")));
services.AddSwaggerGen(swagger =>
{
  swagger.SwaggerDoc("v1", new Info { Title = "Product APIs", 
  Version = "v1" });
});

对于我们的产品 api 的 Swagger 支持,您需要添加Swashbuckle.ASPNETCoreNuGet 包。

现在,打开appsettings.json文件并添加以下代码:

"ConnectionStrings": 
{
  "ProductConnection": "Data Source=.;Initial 
  Catalog=ProductsDB;Integrated 
  Security=True;MultipleActiveResultSets=True"
}

让我们看看我们的ProductController包含什么:

[HttpGet]
[Route("productlist")]
public IActionResult GetList()
{
  return new 
  OkObjectResult(_productRepository.GetAll().
  Select(ToProductvm).ToList());
}

上述代码是我们产品 api 的GET资源。 它调用ProductRepositoryGetAll()方法,对响应进行转置,然后返回。 在前面的代码中,我们已经指示系统用ProductRepository类解析IProductRepository接口。 参考Startup类。

这里是转置响应的方法:

private ProductViewModel ToProductvm(Product productModel)
{
  return new ProductViewModel
  {
    CategoryId = productModel.CategoryId,
    CategoryDescription = productModel.Category.Description,
    CategoryName = productModel.Category.Name,
    ProductDescription = productModel.Description,
    ProductId = productModel.Id,
    ProductImage = productModel.Image,
    ProductName = productModel.Name,
    ProductPrice = productModel.Price
  };
}

前面的代码接受一个Product类型的参数,然后返回一个ProductViewModel类型的对象。

下面的代码展示了如何注入我们的控制器构造函数:

private readonly IProductRepository _productRepository;
public ProductController(IProductRepository productRepository)
{
  _productRepository = productRepository;
}

在前面的代码中,我们注入了我们的ProductRepository,并且它将在任何人调用产品 api 的任何资源时自动初始化。

现在,您可以开始使用应用了。 从菜单中运行应用或单击F5。 在 web 浏览器中,可以使用后缀/swagger作为地址的 URL。

For the complete source code, refer to the GitHub repository link at https://github.com/PacktPublishing/Building-RESTful-Web-services-with-DOTNET-Core.

它将显示 Swagger API 文档,如下面的截图所示:

单击GET /api/Product/productlist资源。 它将返回一个产品列表,如下图所示:

让我们为 API 实现日志记录。 请注意,为了使我们的演示简短,我没有添加复杂的场景来跟踪所有内容。 我正在添加简单的日志来展示日志功能。

要开始为我们的产品 api 实现日志记录,请在一个名为Logging的新文件夹中添加一个名为LogAction的新类。 下面是来自LogAction类的代码:

public class LogActions 
{
  public const int InsertProduct = 1000;
  public const int ListProducts = 1001;
  public const int GetProduct = 1002;
  public const int RemoveProduct = 1003;
}

前面的代码包含的常量就是应用的操作,也称为事件

更新我们的ProductController; 它现在看起来应该像以下代码:

private readonly IProductRepository _productRepository;
private readonly ILogger _logger;
public ProductController(IProductRepository productRepository, ILogger logger)
{
  _productRepository = productRepository;
  _logger = logger;
}

在前面的代码中,我们添加了一个来自依赖注入容器的ILogger接口(参见https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.0了解更多细节)。

让我们将日志功能添加到产品 API 的GET资源中:

[HttpGet]
[Route("productlist")]
public IActionResult GetList()
{
  _logger.LogInformation(LogActions.ListProducts, "Getting all
  products.");
  return new 
  OkObjectResult(_productRepository.GetAll().Select(ToProductvm).
  ToList()); 
}

前面的代码返回产品列表并记录信息。

为了测试这个场景,我们需要一个客户端或一个 API 工具,这样我们才能看到输出。 为此,我们将使用Postman扩展(参见https://www.getpostman.com/了解更多细节)。

首先,我们需要运行应用。 为此,打开 Visual Studio 命令提示符,移动到项目文件夹,然后传递命令dotnet run。 你会看到一个类似的消息如下面的截图所示:

现在,启动 Postman 并调用GET /api/product/productlist资源:

通过单击 Send 按钮,您将期望返回产品列表,但情况并非如此,如下截图所示:

前面的异常发生是因为我们在ProductController中使用了不可注入的非泛型类型。

因此,我们需要在我们的ProductController中做一些微小的改变。 看看下面的代码片段:

private readonly IProductRepository _productRepository;
private readonly ILogger<ProductController> _logger;
public ProductController(IProductRepository productRepository, ILogger<ProductController> logger)
{
  _productRepository = productRepository;
  _logger = logger;
}

在前面的代码中,我添加了一个泛型ILogger<ProductController>类型。 由于它是可注射的,它将自动得到解决。

Logging is slightly different in .NET Core 2.0 compared to its earlier versions. The implementation of the nongeneric ILogger is not available by default, but it is available for ILogger<T>. If you want to use nongeneric implementation, use ILoggerFactory instead of ILogger.

In this case, the constructor of our ProductController would look like the following:

private readonly IProductRepository _productRepository; private readonly ILogger _logger;

public ProductController(IProductRepository productRepository, ILoggerFactory logger) { _productRepository = productRepository; _logger = logger.CreateLogger("Product logger"); }

打开Program类并更新它。 它应该看起来像下面的代码片段:

public static void Main(string[] args)
{
  var webHost = new WebHostBuilder()
  .UseKestrel()
  .UseContentRoot(Directory.GetCurrentDirectory())
  .ConfigureAppConfiguration((hostingContext, config) =>
  {
    var env = hostingContext.HostingEnvironment;
    config.AddJsonFile("appsettings.json", optional: true,
    reloadOnChange: true)
    .AddJsonFile($"appsettings.{env.EnvironmentName}.json", 
    optional: true, reloadOnChange: true);
    config.AddEnvironmentVariables();
  })
  .ConfigureLogging((hostingContext, logging) =>
  {
    logging.AddConfiguration(hostingContext.Configuration.
    GetSection("Logging"));
    logging.AddConsole();
    logging.AddDebug();
  })
  .UseStartup<Startup>()
  .Build();
  webHost.Run();
}

您还需要更新appsettings.json文件,并为记录器编写更多代码,使您的文件看起来像以下片段:

{
  "ApplicationInsights": 
  {
    "InstrumentationKey": ""
  },
  "Logging": 
  {
    "IncludeScopes": false,
    "Console": 
    {
      "LogLevel": 
      {
        "Default": "Warning",
        "System": "Information",
        "Microsoft": "Information"
      }
    }
  },
  "ConnectionStrings": 
  {
    "ProductConnection": "Data Source=.;Initial
    Catalog=ProductsDB;Integrated   
    Security=True;MultipleActiveResultSets=True"
  }
}

现在,再次打开 Visual Studio 命令提示符并编写dotnet build命令。 它将构建项目,你将得到类似以下截图的消息:

【t】【t】

从这一点开始,如果你运行 Postman,它会给你结果,如下面的截图所示:

前面的代码添加了记录操作的功能。 你会收到类似的日志操作如下截图所示:

【t】【t】

这里,我们编写了一些使用默认ILogger的代码。 我们使用了默认方法来调用日志记录器; 然而,在某些情况下,我们需要一个定制的日志记录器。 在下一节中,我们将讨论如何为自定义日志记录器编写中间件。

通过构建我们自己的中间件来拦截 HTTP 请求和响应

在本节中,我们将为现有的应用创建我们自己的中间件。 在这个中间件中,我们将记录所有请求和响应。 让我们通过以下步骤:

  1. 打开 Visual Studio。

  2. 点击文件|打开|项目/解决方案(或按Ctrl+Shift+O),打开产品 api 的现有项目,如下截图所示:

  1. 找到解决方案文件夹,单击“打开”,如下图所示:

  1. 打开解决方案资源管理器,添加一个新文件夹,并通过右键单击项目名称将其命名为Middleware,如下图所示:

  1. 右键单击Middleware文件夹并选择 Add | New Item。
  2. 从 web 模板中,选择 Middleware Class 并将新文件命名为FlixOneStoreLoggerMiddleware。 然后单击“Add”,如下图所示:

你的文件夹层次结构应该像下面的截图所示:

Thanks to Justin Williams who provided a solution for POST resources; his solution is available at https://github.com/JustinJohnWilliams/RequestLogging.

请看下面我们的FlixOneStoreLoggerMiddleware类的代码片段:

private readonly RequestDelegate _next;
private readonly ILogger<FlixOneLoggerMiddleware> _logger;
public FlixOneLoggerMiddleware(RequestDelegate next, ILogger<FlixOneLoggerMiddleware> logger)
{
  _next = next;
  _logger = logger;
}

在前面的代码中,我们只是利用内置的 DI 使用RequestDelegate来创建我们的自定义中间件。

下面的代码向我们展示了如何为日志连接所有的请求和响应:

public async Task Invoke(HttpContext httpContext)
{
  _logger.LogInformation(await  
  GetFormatedRequest(httpContext.Request));
  var originalBodyStream = httpContext.Response.Body;
  using (var responseBody = new MemoryStream())
  {
    httpContext.Response.Body = responseBody;
    await _next(httpContext);
    _logger.LogInformation(await 
    GetFormatedResponse(httpContext.Response));
    await responseBody.CopyToAsync(originalBodyStream);
  }
}

请参考本章的请求委托部分,我们在这里讨论了中间件。 在前面的代码中,我们只是在ILogger泛型类型的帮助下记录请求和响应。 await _next(httpContext);线继续与请求管道连接。

打开Setup.cs文件,在Configure方法中添加以下代码:

loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
//custom middleware
app.UseFlixOneLoggerMiddleware();

在前面的代码中,我们利用了ILoggerFactory并添加ConsoleDebug来记录请求和响应。 UseFlixOneLoggerMiddleware方法实际上是一种可拓方法。 为此,将以下代码添加到FlixOneStoreLoggerExtension类:

public static class FlixOneStoreLoggerExtension
{
  public static IApplicationBuilder UseFlixOneLoggerMiddleware
  (this IApplicationBuilder applicationBuilder)
  {
    return applicationBuilder.UseMiddleware<FlixOneLoggerMiddleware>();
  }
}

现在,无论何时任何请求进入我们的产品 api,日志都应该出现,如下图所示:

在本节中,我们创建了一个自定义中间件,然后记录所有请求和响应。

JSON-RPC 用于 RPC 通信

JSON-RPC 是一种无状态的轻量级远程过程调用(RPC)协议。 该规范(即 JSON-RPC 2.0 规范(详见http://www.jsonrpc.org/specification)定义了各种数据结构及其处理规则。

下面几节中显示了规范中的主要对象。

请求对象

对象表示发送到服务器的任何调用/请求。 对象有以下成员:

  • jsonrpc:JSON-RPC 协议版本字符串。 它的必须是准确的(在本例中是 2.0 版)。
  • 方法:包含待修正方法名称的字符串。 以单词rpc开头并后面是句号字符(U+002E 或 ASCII 46)的方法名对于 rpc 内部方法和扩展是受限制的,并且不能用于其他任何事情。
  • 参数:支配参数值的结构化值。 在整个魔法过程中都要佩戴它。 该成员可以被删除。
  • id:客户端固定的标识符,必须包含字符串、数字或null值。

响应对象

根据规范,每当对服务器进行调用时,必须有来自服务器的响应。 Response被表示为一个 JSON 对象,包含以下成员:

  • jsonrpc:JSON-RPC 协议版本
  • result:如果请求成功,则为需要的成员
  • error:如果有错误,则是必需的成员
  • id:必需成员

在本节中,我们概述了 JSON-RPC 规范 2.0。

总结

在本章中,我们讨论了关于支付网关、订单跟踪、通知服务等的外部 api /组件的集成。 我们还使用实际代码实现了它们的功能。

测试是帮助我们消除代码错误的一个过程。 对于所有想要使他们的代码干净和可维护的开发人员来说,这也是一种实践。 在下一章中,我们将讨论日常开发活动中的测试范例。 我们将讨论一些与测试范例相关的重要术语。 我们还将介绍关于这些术语的理论,然后我们将介绍代码示例,查看存根和模拟,并学习集成、安全性和性能测试。