eShopOnContainers 知多少[7]:Basket microservice

購物車界面

引言

Basket microservice(購物車微服務)主要用於處理購物車的業務邏輯,包括:redis

  1. 購物車商品的CRUD
  2. 訂閱商品價格更新事件,進行購物車商品同步處理
  3. 購物車結算事件發佈
  4. 訂閱訂單成功建立事件,進行購物車的清空操做

架構模式

數據驅動/CRUD 微服務設計

如上圖所示,本微服務採用數據驅動的CRUD微服務架構,來執行購物車商品的維護操做。並使用Redis數據庫進行持久化。
這種類型的服務在單個 ASP.NET Core Web API 項目中便可實現全部功能,該項目包括數據模型類、業務邏輯類及其數據訪問類。其項目結構以下:
數據庫

核心技術選型:json

  1. ASP.NET Core Web API
  2. Entity Framework Core
  3. Redis
  4. Swashbuckle(可選)
  5. Autofac
  6. Eventbus
  7. Newtonsoft.Jsonapi

    實體建模和持久化

    該微服務的核心領域實體是購物車,其類圖以下:安全

其中CustomerBasketBasketItem爲一對多關係,使用倉儲模式進行持久化。架構

  1. 經過對CustomerBasket對象進行json格式的序列化和反序列化來完成在redis中的持久化和讀取。
  2. 以單例模式注入redis鏈接ConnectionMultiplexer,該對象最終經過構造函數注入到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);
});

事件的註冊和消費

在本服務中主要須要處理如下事件的發佈和消費:app

  1. 事件發佈:當用戶點擊購物車結算時,發佈用戶結算事件。
  2. 事件消費:訂單建立成功後,進行購物車的清空
  3. 事件消費:商品價格更新後,進行購物車相關商品的價格同步
private void ConfigureEventBus(IApplicationBuilder app)
{
    var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();

    eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>();
    eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>();
}

以上都是基於事件總線來達成。async

認證和受權

購物車管理界面是須要認證和受權。那天然須要與上游的Identity Microservice進行銜接。在啓動類進行認證中間件的配置。ide

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();
}

手動啓用斷路器

在該微服務中,定義了一個中斷中間件FailingMiddleware,經過訪問http://localhost:5103/failing獲取該中間件的啓用狀態,經過請求參數指定:即經過http://localhost:5103/failing?enablehttp://localhost:5103/failing?disable來手動中斷和恢復服務,來模擬斷路,以便用於測試斷路器模式。
開啓斷路後,當訪問購物車頁面時,Polly在重試指定次數依然沒法訪問服務時,就會拋出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服務時指定了兩個過濾器:全局異常過濾器和模型驗證過濾器。

// Add framework services.
services.AddMvc(options =>
{
    options.Filters.Add(typeof(HttpGlobalExceptionFilter));
    options.Filters.Add(typeof(ValidateModelStateFilter));

}).AddControllersAsServices();
  1. 全局異常過濾器是經過定義BasketDomainException異常和HttpGlobalExceptionFilter過濾器來實現的。
  2. 模型驗證過濾器是經過繼承ActionFilterAttribute特性實現的ValidateModelStateFilter來獲取模型狀態中的錯誤。
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認證受權集成

由於默認啓用了安全認證,因此爲了方便在SwaggerUI界面進行測試,那麼咱們就必須爲其集成認證受權。代碼以下:

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>();
});

其中有主要作了三件事:

  1. 配置受權Url
  2. 配置TokenUrl
  3. 指定受權範圍
  4. 注入受權檢查過濾器AuthorizeCheckOperationFilter用於攔截須要受權的請求
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" } }
            });
        }
    }
}

最後

本服務較以前講的Catalog microservice 而言,主要是多了一個認證和redis存儲。

相關文章
相關標籤/搜索