五、集成外部组件和处理器
到目前为止,我们一直在开发 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."
看看下图,它展示了一个简单的中间件组件的例子:
要求代表
请求由Use
、Run
、Map
和MapWhen
扩展方法处理。 这些方法配置请求委托。
为了详细理解这一点,让我们使用ASP.NET Core
创建一个虚拟项目。 完成以下步骤:
- 打开 Visual Studio。
- 转到文件|新建|项目,或按Ctrl+Shift+N。 参考以下截图:
Creating a new project using Visual Studio 2017
-
在新项目屏幕上,选择 ASP.NET Core Web 应用。
-
命名您的新项目(比如
Chap05_01
),选择一个位置,然后单击 OK,如下图所示:
Selecting new project template
-
从新的 ASP.NET Core Web Application 模板界面,选择 API 模板。 确保你选择了。net Core 和 ASP。 2.0 NET Core。
-
单击“确定”,如下图所示:
- 解决方案资源管理器打开。 你会看到文件/文件夹结构,如下截图所示:
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.IApplicationBuilder
的app.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>/testroute
的Map
。 接下来是我们前面讨论过的相同的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.ASPNETCore
NuGet 包。
现在,打开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
资源。 它调用ProductRepository
的GetAll()
方法,对响应进行转置,然后返回。 在前面的代码中,我们已经指示系统用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 请求和响应
在本节中,我们将为现有的应用创建我们自己的中间件。 在这个中间件中,我们将记录所有请求和响应。 让我们通过以下步骤:
-
打开 Visual Studio。
-
点击文件|打开|项目/解决方案(或按Ctrl+Shift+O),打开产品 api 的现有项目,如下截图所示:
- 找到解决方案文件夹,单击“打开”,如下图所示:
- 打开解决方案资源管理器,添加一个新文件夹,并通过右键单击项目名称将其命名为
Middleware
,如下图所示:
- 右键单击
Middleware
文件夹并选择 Add | New Item。 - 从 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
并添加Console
和Debug
来记录请求和响应。 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 /组件的集成。 我们还使用实际代码实现了它们的功能。
测试是帮助我们消除代码错误的一个过程。 对于所有想要使他们的代码干净和可维护的开发人员来说,这也是一种实践。 在下一章中,我们将讨论日常开发活动中的测试范例。 我们将讨论一些与测试范例相关的重要术语。 我们还将介绍关于这些术语的理论,然后我们将介绍代码示例,查看存根和模拟,并学习集成、安全性和性能测试。
版权属于:月萌API www.moonapi.com,转载请注明出处