引言
Basket microservice(購物車微服務(wù))主要用于處理購物車的業(yè)務(wù)邏輯嗅骄,包括:
- 購物車商品的CRUD
- 訂閱商品價格更新事件,進(jìn)行購物車商品同步處理
- 購物車結(jié)算事件發(fā)布
- 訂閱訂單成功創(chuàng)建事件客情,進(jìn)行購物車的清空操作
架構(gòu)模式
如上圖所示,本微服務(wù)采用數(shù)據(jù)驅(qū)動的CRUD微服務(wù)架構(gòu)癞己,并使用Redis數(shù)據(jù)庫進(jìn)行持久化膀斋。
這種類型的服務(wù)在單個 ASP.NET Core Web API 項目中即可實現(xiàn)所有功能,該項目包括數(shù)據(jù)模型類痹雅、業(yè)務(wù)邏輯類及其數(shù)據(jù)訪問類仰担。其項目結(jié)構(gòu)如下:
核心技術(shù)選型:
- ASP.NET Core Web API
- Entity Framework Core
- Redis
- Swashbuckle(可選)
- Autofac
- Eventbus
- Newtonsoft.Json
實體建模和持久化
該微服務(wù)的核心領(lǐng)域?qū)嶓w是購物車,其類圖如下:其中CustomerBasket
與BasketItem
為一對多關(guān)系绩社,使用倉儲模式進(jìn)行持久化摔蓝。
- 通過對
CustomerBasket
對象進(jìn)行json格式的序列化和反序列化來完成在redis中的持久化和讀取赂苗。 - 以單例模式注入redis連接
ConnectionMultiplexer
,該對象最終通過構(gòu)造函數(shù)注入到RedisBasketRepository
中贮尉。
services.AddSingleton<ConnectionMultiplexer>(sp =>
{
var settings = sp.GetRequiredService<IOptions<BasketSettings>>().Value;
var configuration = ConfigurationOptions.Parse(settings.ConnectionString, true);
configuration.ResolveDns = true;
return ConnectionMultiplexer.Connect(configuration);
});
事件的注冊和消費(fèi)
在本服務(wù)中主要需要處理以下事件的發(fā)布和消費(fèi):
- 事件發(fā)布:當(dāng)用戶點(diǎn)擊購物車結(jié)算時拌滋,發(fā)布用戶結(jié)算事件。
- 事件消費(fèi):訂單創(chuàng)建成功后猜谚,進(jìn)行購物車的清空
- 事件消費(fèi):商品價格更新后败砂,進(jìn)行購物車相關(guān)商品的價格同步
private void ConfigureEventBus(IApplicationBuilder app)
{
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>();
eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>();
}
以上都是基于事件總線來達(dá)成。
認(rèn)證和授權(quán)
購物車管理界面是需要認(rèn)證和授權(quán)魏铅。那自然需要與上游的Identity Microservice
進(jìn)行銜接吠卷。在啟動類進(jìn)行認(rèn)證中間件的配置。
private void ConfigureAuthService(IServiceCollection services)
{
// prevent from mapping "sub" claim to nameidentifier.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
var identityUrl = Configuration.GetValue<string>("IdentityUrl");
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = identityUrl;
options.RequireHttpsMetadata = false;
options.Audience = "basket";
});
}
protected virtual void ConfigureAuth(IApplicationBuilder app)
{
if (Configuration.GetValue<bool>("UseLoadTest"))
{
app.UseMiddleware<ByPassAuthMiddleware>();
}
app.UseAuthentication();
}
手動啟用斷路器
在該微服務(wù)中吉嫩,定義了一個中斷中間件:FailingMiddleware
云挟,通過訪問http://localhost:5103/failing
獲取該中間件的啟用狀態(tài),通過請求參數(shù)指定:即通過http://localhost:5103/failing?enable
和http://localhost:5103/failing?disable
來手動中斷和恢復(fù)服務(wù),來模擬斷路杀迹,以便用于測試斷路器模式送朱。
開啟斷路后魁袜,當(dāng)訪問購物車頁面時敦第,Polly在重試指定次數(shù)依然無法訪問服務(wù)時鞠呈,就會拋出BrokenCircuitException
異常舀射,通過捕捉該異常告知用戶稍后再試山林。
public class CartController : Controller
{
//…
public async Task<IActionResult> Index()
{
try
{
var user = _appUserParser.Parse(HttpContext.User);
//Http requests using the Typed Client (Service Agent)
var vm = await _basketSvc.GetBasket(user);
return View(vm);
}
catch (BrokenCircuitException)
{
// Catches error when Basket.api is in circuit-opened mode
HandleBrokenCircuitException();
}
return View();
}
private void HandleBrokenCircuitException()
{
TempData["BasketInoperativeMsg"] = "Basket Service is inoperative, please try later on. (Business message due to Circuit-Breaker)";
}
}
注入過濾器
在配置MVC服務(wù)時指定了兩個過濾器:全局異常過濾器和模型驗證過濾器洼怔。
// Add framework services.
services.AddMvc(options =>
{
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
options.Filters.Add(typeof(ValidateModelStateFilter));
}).AddControllersAsServices();
- 全局異常過濾器是通過定義
BasketDomainException
異常和HttpGlobalExceptionFilter
過濾器來實現(xiàn)的诡右。 - 模型驗證過濾器是通過繼承
ActionFilterAttribute
特性實現(xiàn)的ValidateModelStateFilter
來獲取模型狀態(tài)中的錯誤域那。
public class ValidateModelStateFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.ModelState.IsValid)
{
return;
}
var validationErrors = context.ModelState
.Keys
.SelectMany(k => context.ModelState[k].Errors)
.Select(e => e.ErrorMessage)
.ToArray();
var json = new JsonErrorResponse
{
Messages = validationErrors
};
context.Result = new BadRequestObjectResult(json);
}
}
SwaggerUI認(rèn)證授權(quán)集成
因為默認(rèn)啟用了安全認(rèn)證淑蔚,所以為了方便在SwaggerUI界面進(jìn)行測試,那么我們就必須為其集成認(rèn)證授權(quán)。代碼如下:
services.AddSwaggerGen(options =>
{
options.DescribeAllEnumsAsStrings();
options.SwaggerDoc("v1", new Info
{
Title = "Basket HTTP API",
Version = "v1",
Description = "The Basket Service HTTP API",
TermsOfService = "Terms Of Service"
});
options.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
Type = "oauth2",
Flow = "implicit",
AuthorizationUrl = $"{Configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize",
TokenUrl = $"{Configuration.GetValue<string>("IdentityUrlExternal")}/connect/token",
Scopes = new Dictionary<string, string>()
{
{ "basket", "Basket API" }
}
});
options.OperationFilter<AuthorizeCheckOperationFilter>();
});
其中有主要做了三件事:
- 配置授權(quán)Url
- 配置TokenUrl
- 指定授權(quán)范圍
- 注入授權(quán)檢查過濾器
AuthorizeCheckOperationFilter
用于攔截需要授權(quán)的請求
public class AuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
// Check for authorize attribute
var hasAuthorize = context.ApiDescription.ControllerAttributes().OfType<AuthorizeAttribute>().Any() ||
context.ApiDescription.ActionAttributes().OfType<AuthorizeAttribute>().Any();
if (hasAuthorize)
{
operation.Responses.Add("401", new Response { Description = "Unauthorized" });
operation.Responses.Add("403", new Response { Description = "Forbidden" });
operation.Security = new List<IDictionary<string, IEnumerable<string>>>();
operation.Security.Add(new Dictionary<string, IEnumerable<string>>
{
{ "oauth2", new [] { "basketapi" } }
});
}
}
}
最后
本服務(wù)較之前講的Catalog microservice 而言,主要是多了一個認(rèn)證和redis存儲。