eShopOnContainers 知多少[7]:Basket microservice

购物车界面

引言

Basket microservice(购物车微服务)主要用于处理购物车的业务逻辑,包括:

  1. 购物车商品的CRUD
  2. 订阅商品价格更新事件,进行购物车商品同步处理
  3. 购物车结算事件发布
  4. 订阅订单成功创建事件,进行购物车的清空操作

架构模式

数据驱动/CRUD 微服务设计

如上图所示,本微服务采用数据驱动的CRUD微服务架构,并使用Redis数据库进行持久化。
这种类型的服务在单个 ASP.NET Core Web API 项目中即可实现所有功能,该项目包括数据模型类、业务逻辑类及其数据访问类。其项目结构如下:


核心技术选型:

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

实体建模和持久化

该微服务的核心领域实体是购物车,其类图如下:

其中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);
});

事件的注册和消费

在本服务中主要需要处理以下事件的发布和消费:

  1. 事件发布:当用户点击购物车结算时,发布用户结算事件。
  2. 事件消费:订单创建成功后,进行购物车的清空
  3. 事件消费:商品价格更新后,进行购物车相关商品的价格同步
private void ConfigureEventBus(IApplicationBuilder app)
{
    var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();

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

以上都是基于事件总线来达成。

认证和授权

购物车管理界面是需要认证和授权。那自然需要与上游的Identity Microservice进行衔接。在启动类进行认证中间件的配置。

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存储。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,193评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,306评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,130评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,110评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,118评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,085评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,007评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,844评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,283评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,508评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,395评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,985评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,630评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,797评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,653评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,553评论 2 352

推荐阅读更多精彩内容

  • 有一个画面,令我至今难忘。 一个黄昏,一位女子开车与一男子开的车相擦,不等女子下车,男子气冲冲地对她一通责难。女子...
    带露珠的翅膀阅读 253评论 0 4
  • 九月银川,风总是很大,夜总是很冷,在一场开学季的洗礼中,踹怀着梦想,渐进。 几天前的一个下午,小区门前的几棵枣树被...
    浅予南风阅读 1,036评论 0 3
  • 高中时,我有一位闺密,和她的相遇让我想起了一首歌――《一个像夏天一个像秋天》。准确的说,我们初中是校友,那个时候散...
    落日船客阅读 434评论 1 3