.NET Core 应用程序的简单交易:微服务示例架构
.NET Core 应用程序的微服务示例架构
.NET Core 2.2 示例,包含 C#.NET、EF 和 SQL Server
- 引言
- 应用程序架构
- 微服务设计
- 安全:基于 JWT Token 的身份验证
- 开发环境
- 技术
- 使用的开源工具
- 云平台服务
- 数据库设计
- WebApi 端点
- 解决方案结构
- 异常处理
- 数据库并发处理
- Azure AppInsights:日志记录和监控
- Swagger:API 文档
- Postman 集合
- 如何运行应用程序
- 控制台应用程序 - 网关客户端
引言
这是一个 .NET Core 示例应用程序,演示了如何使用 ASP.NET Core Web API、C#.NET、Entity Framework 和 SQL Server 构建和实现一个简单的自动银行功能(如余额查询、存款、取款)的微服务后端系统。
应用程序架构
该示例应用程序基于微服务架构构建。使用微服务架构构建应用程序有几个优点,例如服务可以独立开发、部署和扩展。下图显示了后端架构的高层设计。
- 身份微服务 - 根据用户名、密码进行用户身份验证,并颁发一个 JWT Bearer Token,其中包含基于声明的身份信息。
- 交易微服务 - 处理账户交易,如查询余额、存款、取款。
- API 网关 - 作为后端应用程序的入口点,提供数据聚合和微服务通信路径。
微服务设计
此图显示了交易微服务的内部设计。与交易服务相关的业务逻辑和数据逻辑写在一个单独的交易处理框架中。该框架通过 Web API 接收输入,并根据一些简单的规则处理这些请求。交易数据存储在 SQL 数据库中。
安全:基于 JWT Token 的身份验证
已实现基于 JWT Token 的身份验证来保护 WebApi 服务。身份微服务充当认证服务器,在验证用户凭据后颁发有效令牌。API 网关将令牌发送给客户端。客户端应用程序在后续请求中使用该令牌。
开发环境
技术
- C#.NET
- ASP.NET WEB API Core
- SQL Server
使用的开源工具
- Automapper(用于对象到对象的映射)
- Entity Framework Core(用于数据访问)
- Swashbuckle(用于 API 文档)
- XUnit(用于单元测试用例)
- Ocelot(用于 API 网关聚合)
云平台服务
- Azure App Insights(用于日志记录和监控)
- Azure SQL 数据库(用于数据存储)
数据库设计
WebApi 端点
应用程序在 API 网关中配置了四个 API 端点,以演示启用基于令牌的安全选项的功能。这些路由暴露给客户端应用程序以使用后端服务。
通过 API 网关配置并可访问的端点
- 路由:"/user/authenticate" [
HttpPost
] - 用于验证用户并颁发令牌 - 路由:"/account/balance" [
HttpGet
] - 用于检索账户余额 - 路由:"/account/deposit" [
HttpPost
] - 用于向账户存款 - 路由:"/account/withdraw" [
HttpPost
] - 用于从账户取款
在微服务层面实现的端点
- 路由:"/api/user/authenticate" [
HttpPost
] - 用于验证用户并颁发令牌 - 路由:"/api/account/balance" [
HttpGet
] - 用于检索账户余额 - 路由:"/api/account/deposit" [
HttpPost
] - 用于向账户存款 - 路由:"/api/account/withdraw" [
HttpPost
] - 用于从账户取款
解决方案结构
Identity.WebApi
- 使用用户名、密码作为输入参数处理身份验证部分,并颁发带有
Claims-Identity
信息的 JWT Bearer Token。
- 使用用户名、密码作为输入参数处理身份验证部分,并颁发带有
Transaction.WebApi
- 支持
Balance
、Deposit
和Withdraw
三种 HTTP 方法。接收这些方法的 HTTP 请求。 - 通过中间件处理异常
- 读取 Authorization Header 中的身份信息,该 Header 包含 Bearer Token
- 调用
Transaction
框架中的相应函数 - 将交易响应结果返回给客户端
- 支持
Transaction.Framework
- 定义存储库(数据)层和服务(业务)层的接口
- 定义领域模型(业务对象)和实体模型(数据模型)
- 定义业务异常和领域模型验证
- 定义框架所需的 `Struct`、`Enum`、`Constants` 等数据类型
- 实现执行所需账户交易的业务逻辑
- 实现从 SQL 数据库读取和写入数据的逻辑
- 执行领域模型与实体模型之间的映射任务,反之亦然
- 处理数据库更新并发冲突
- 通过依赖注入将接口及其实现注册到 Service Collection 中
Gateway.WebApi
- 通过检查请求中的 JWT Token 来验证传入的 HTTP 请求。
- 将 HTTP 请求路由到下游服务。
SimpleBanking.ConsoleApp
- 一个控制台客户端应用程序,连接到 API 网关,可用于使用用户名、密码登录,并针对账户执行
Balance
、Deposit
和Withdraw
等交易。
- 一个控制台客户端应用程序,连接到 API 网关,可用于使用用户名、密码登录,并针对账户执行
异常处理
编写了一个中间件来处理异常,并在启动时注册该中间件作为 HTTP 请求的一部分运行。每个 HTTP 请求都经过此异常处理中间件,然后执行 Web API 控制器操作方法。
- 如果操作方法成功,则将成功响应发送回客户端。
- 如果操作方法抛出任何异常,则异常将被中间件捕获和处理,并将适当的响应发送回客户端。
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (Exception ex)
{
var message = CreateMessage(context, ex);
_logger.LogError(message, ex);
await HandleExceptionAsync(context, ex);
}
}
数据库并发处理
数据库并发是指当多个事务同时尝试更新数据库中的相同数据时发生的冲突。在下面的图表中,如果交易 1 和交易 2 是针对同一个账户,一个试图向账户存款,另一个系统试图从账户取款,这时就会发生并发。该框架包含两个逻辑层,一个处理业务逻辑,另一个处理数据逻辑。
当从数据库读取数据,并在该数据上应用业务逻辑时,在此上下文中,与同一记录相关的值将有三种不同的状态。
- 数据库值是当前存储在数据库中的值。
- 原始值是从数据库最初检索到的值。
- 当前值是应用程序尝试写入数据库的新值。
当系统尝试保存更改并使用并发令牌识别正在更新到数据库的值不是从数据库读取的原始值时,每个事务中值的状态都会产生冲突,并抛出 DbUpdateConcurrencyException
。
处理并发冲突的一般方法是:
- 在
SaveChanges
期间捕获DbUpdateConcurrencyException
。 - 使用
DbUpdateConcurrencyException.Entries
为受影响的实体准备一组新的更改。 - 刷新并发令牌的原始值,以反映数据库中的当前值。
- 重试过程,直到没有发生冲突。
while (!isSaved)
{
try
{
await _dbContext.SaveChangesAsync();
isSaved = true;
}
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.Entries)
{
if (entry.Entity is AccountSummaryEntity)
{
var databaseValues = entry.GetDatabaseValues();
if (databaseValues != null)
{
entry.OriginalValues.SetValues(databaseValues);
CalculateNewBalance();
void CalculateNewBalance()
{
var balance = (decimal)entry.OriginalValues["Balance"];
var amount = accountTransactionEntity.Amount;
if (accountTransactionEntity.TransactionType ==
TransactionType.Deposit.ToString())
{
accountSummaryEntity.Balance =
balance += amount;
}
else if (accountTransactionEntity.TransactionType ==
TransactionType.Withdrawal.ToString())
{
if(amount > balance)
throw new InsufficientBalanceException();
accountSummaryEntity.Balance =
balance -= amount;
}
}
}
else
{
throw new NotSupportedException();
}
}
}
}
}
Azure AppInsights:日志记录和监控
Azure AppInsights 已集成到“交易微服务”中,用于收集应用程序遥测数据。
public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationInsightsTelemetry(Configuration);
}
适用于 ASP.NET Core 的 AppInsights SDK 在 ILoggerFactory
上提供了一个名为 AddApplicationInsights
的扩展方法,用于配置日志记录。所有与 Deposit
和 Withdraw
相关的交易都通过 ILogger
记录到 AppInsights 日志中。
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory log)
{
log.AddApplicationInsights(app.ApplicationServices, LogLevel.Information);
}
要使用 AppInsights,您需要有一个 Azure 帐户,并在 Azure Portal 中为您的应用程序创建一个 AppInsights 实例,这将为您提供一个仪表板密钥,应在 appsettings.json 中进行配置。
"ApplicationInsights": {
"InstrumentationKey": "<Your Instrumentation Key>"
},
Swagger:API 文档
Swagger Nuget 包已添加到“交易微服务”中,并在 startup.cs 中配置了 Swagger 中间件用于 API 文档。运行 WebApi 服务时,可以通过 Swagger 端点“/swagger”访问 swagger UI。
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(c => {
c.SwaggerDoc("v1", new Info { Title = "Simple Transaction Processing",
Version = "v1" });
});
}
public void Configure
(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory log)
{
app.UseSwagger();
app.UseSwaggerUI(c => {
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Simple Transaction Processing v1");
});
}
Postman 集合
从 这里 下载 postman 集合,通过网关运行 API 端点。
如何运行应用程序
- 从 这里 下载 SQL 脚本。
- 在 SQL Server 上运行脚本以创建必要的表和示例数据。
- 在 Visual Studio 2017 或更高版本中打开解决方案(.sln)。
- 在
Transaction.WebApi
-> Appsettings.json 文件中配置 SQL 连接字符串。 - 在 Transaction.WebApi -> Appsettings.json 文件中配置 AppInsights 仪表板密钥。如果您没有密钥或不需要日志,则在 Startup.cs 文件中注释掉与 AppInsight 相关的代码。
- 查看
Identity.WebApi
-> UserService.cs 文件中的身份信息。在 Identity 服务中,用户的详细信息已为少数账户硬编码,可用于运行应用程序。相同的详细信息如下表所示。 - 运行解决方案中的以下项目
Identity.WebApi
Transaction.WebApi
Gateway.WebApi
SimpleBanking.ConsoleApp
- 在
ConsoleApp
中正确配置网关主机和端口。 - 在网关 -> configuration.json 中正确配置身份和交易服务的 Host 和端口。
- 用于测试的示例数据
账户号码 | 货币 | 用户名 | 密码 |
3628101 | 欧元 | speter | test@123 |
3637897 | 欧元 | gwoodhouse | pass@123 |
3648755 | 欧元 | jsmith | admin@123 |