ASP.NET Core中实现单体程序的事件发布/订阅 - LamondLu - 博客园

标题:ASP.NET Core中实现单体程序的事件发布/订阅作者:Lamond Lu

地址:https://www.cnblogs.com/lwqlun/p/10468058.html


项目源代码:https://github.com/lamondlu/EventHandlerInSingleApplication


640?wx_fmt=png

背景

事件发布/订阅是一种非常强大的模式,它可以帮助业务组件间实现完全解耦,不同的业务组件只依赖事件,只关注哪些事件是需要自己处理的,而不用关注谁来处理自己发布事件,事件追溯(Event Sourcing)也是基于事件发布/订阅的。在微服务架构中,事件发布/订阅有非常多的应用场景。今天我给大家分享一个基于ASP.NET Core的单体程序使用事件发布/订阅的例子,针对分布式项目的事件发布/订阅比较复杂,难点是事务处理,后续我会另写一篇博文来演示。

案例说明

当前我们有一个基于ASP.NET Core的电子商务系统,在项目的初期,业务非常简单,只有一个购物车模块和一个订单模块,所有的代码都放在一个项目中。

整个项目使用了一个简单的三层架构。

640?wx_fmt=png

这里当用户提交购物车的时候,程序会在ShoppingCartManager类的SubmitShoppingCart方法中执行3个操作

  • 修改当前购物车的状态为完成

  • 根据购物车中的物品创建一个新订单

  • 给用户发邮件

代码如下:

 public void SubmitShoppingCart(string shoppingCartId) { var shoppingCart = _unitOfWork.ShoppingCartRepository .GetShoppingCart(shoppingCartId); _unitOfWork.ShoppingCartRepository .SubmitShoppingCart(shoppingCartId); _unitOfWork.OrderRepository .CreatOrder(new CreateOrderDTO { Items = shoppingCart.Items .Select(p => new NewOrderItemDTO { ItemId = p.ItemId, Name = p.Name, Price = p.Price }).ToList() }); //这里为了简化代码,我用命令行表示发送邮件的逻辑 Console.WriteLine("Confirm Email Sent."); _unitOfWork.Save(); }

根据SOLID设计原则中的单一责任原则,如果一个类承担的职责过多,就等于把这些职责耦合在一起了。这里生成订单和发送邮件都不应该是当前SubmitShoppingCart需要负责的,所以我们需要它们从这个方法中移出去,使用的方法就是事件订阅/发布。

新的架构图

以下是使用事件发布/订阅之后的系统架构图。

640?wx_fmt=png

  • 这里我们会创建一个购物车提交事件ShoppingCartSubmittedEvent

  • 当站点启动的时候,我们会在一个名为EventHandlerContainer的类中注册订阅ShoppingCartSubmittedEvent事件的2个处理类CreateOrderHandlerConfirmEmailSentHandler

  • SubmitShoppingCart方法中,我们会做2件事情:

    • 更改当前购物车的状态。

    • 发布ShoppingCartSubmittedEvent事件。

  • CreateOrderHandler事件处理器会调用OrderManager类中的创建订单方法。

  • ConfirmEmailSentHandler事件处理器会负责发送邮件。

好的,下面让我们来一步一步实现以上描述的代码。

添加事件基类

这里我们首先定义一个事件基类,其中暂时只添加了一个属性OccuredOn,它表示了当前事件的触发时间。

 public class EventBase { public EventBase() { OccuredOn = DateTime.Now; } protected DateTime OccuredOn { get; set; } }

定义购物车提交事件

接下来我们就需要创建购物车提交事件类ShoppingCartSubmittedEvent, 它继承自EventBase, 并提供了一个购物项集合

 public class ShoppingCartSubmittedEvent : EventBase { public ShoppingCartSubmittedEvent() { Items = new List<ShoppingCartSubmittedItem>(); } public List<ShoppingCartSubmittedItem> Items { get; set; } } public class ShoppingCartSubmittedItem { public string ItemId { get; set; } public string Name { get; set; } public decimal Price { get; set; } }

定义事件处理器接口

为了添加事件处理器,我们首先需要定义一个泛型接口类IEventHandler

 public interface IEventHandler<T> where T : EventBase { void Run(T obj); Task RunAsync(T obj); }

这个泛型接口类的是泛型类型必须继承自EventBase类。接口提供了2个方法RunRunAsync。 它们定义了该接口的实现类必须实现同一个处理逻辑的同步和异步方法。

为购物车提交事件编写事件处理器

有了事件处理器接口,接下来我们就可以开始为购物车提交事件添加事件处理器了。这里我们为了实现前面定义的逻辑,我们需要创建2个处理器CreateOrderHandlerConfirmEmailSentHandler

CreateOrderHandler.cs

 public class CreateOrderHandler : IEventHandler<ShoppingCartSubmittedEvent> { private IOrderManager _orderManager = null; public CreateOrderHandler(IOrderManager orderManager) { _orderManager = orderManager; } public void Run(ShoppingCartSubmittedEvent obj) { _orderManager.CreateNewOrder(new Models.DTOs.CreateOrderDTO { Items = obj.Items.Select(p => new Models.DTOs.NewOrderItemDTO { ItemId = p.ItemId, Name = p.Name, Price = p.Price }).ToList() }); } public Task RunAsync(ShoppingCartSubmittedEvent obj) { return Task.Run(() => { Run(obj); }); } }

代码解释:

  • CreateOrderHandler的构造函数中,我们注入了IOrderManager接口对象,CreateNewOrder负责最终创建订单的工作

  • 这里为了简化代码,我直接使用了Task.Run,并在其中调用了同步方法实现

ConfirmEmailSentHandler.cs

 public class ConfirmEmailSentHandler : IEventHandler<ShoppingCartSubmittedEvent> { public void Run(ShoppingCartSubmittedEvent obj) { Console.WriteLine("Confirm Email Sent."); } public Task RunAsync(ShoppingCartSubmittedEvent obj) { return Task.Run(() => { Console.WriteLine("Confirm Email Sent."); }); } }

代码解释:

  • 这个处理类非常简单,为了简化代码,我仅输出了一行文本来表示实际需要运行的代码。

OrderManager类添加创建订单方法

IOrderManager.cs

 public interface IOrderManager { string CreateNewOrder(CreateOrderDTO dto); }

OrderManager.cs

 public class OrderManager : IOrderManager { private IOrderRepository _orderRepository; public OrderManager(IOrderRepository orderRepository) { _orderRepository = orderRepository; } public string CreateNewOrder(CreateOrderDTO dto) { var orderId = _orderRepository.CreatOrder(dto); Console.WriteLine($"One order created: {JsonConvert.SerializeObject(dto)}"); return orderId; } }

创建EventHandlerContainer

下面我们来编写最核心的事件处理器容器。在这里我们的事件处理器容器完成了3个功能

  • 订阅事件(Subscribe Event)

  • 取消订阅事件(Unsubscribe Event)

  • 发布事件(Publish Event)

 public class EventHandlerContainer { private IServiceProvider _serviceProvider = null; private static Dictionary<string, List<Type>> _mappings = new Dictionary<string, List<Type>>(); public EventHandlerContainer(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public static void Subscribe<T, THandler>() where T : EventBase where THandler : IEventHandler<T> { var name = typeof(T).Name; if (!_mappings.ContainsKey(name)) { _mappings.Add(name, new List<Type> { }); } _mappings[name].Add(typeof(THandler)); } public static void Unsubscribe<T, THandler>() where T : EventBase where THandler : IEventHandler<T> { var name = typeof(T).Name; _mappings[name].Remove(typeof(THandler)); if (_mappings[name].Count == 0) { _mappings.Remove(name); } } public void Publish<T>(T o) where T : EventBase { var name = typeof(T).Name; if (_mappings.ContainsKey(name)) { foreach (var handler in _mappings[name]) { var service = (IEventHandler<T>)_serviceProvider.GetService(handler); service.Run(o); } } } public async Task PublishAsync<T>(T o) where T : EventBase { var name = typeof(T).Name; if (_mappings.ContainsKey(name)) { foreach (var handler in _mappings[name]) { var service = (IEventHandler<T>)_serviceProvider.GetService(handler); await service.RunAsync(o); } } } }

代码解释:

  • 这里我没有直接订阅事件处理器的实例,而且订阅了事件处理器的类型

  • 多个事件处理器可以订阅同一个事件

  • EventHandlerContainer的构造函数中,我们注入了一个IServiceProvider,我们可以使用它来获得对应事件处理器的实例。

在程序启动时,注册事件订阅

现在我们来Startup.csConfigureServices方法,这里我们需要进行服务注册,并完成事件订阅。

 public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); services.AddScoped<IOrderManager, OrderManager>(); services.AddScoped<IShoppingCartManager, ShoppingCartManager>(); services.AddScoped<IShoppingCartRepository, ShoppingCartRepository>(); services.AddScoped<IOrderRepository, OrderRepository>(); services.AddScoped<IUnitOfWork, UnitOfWork>(); services.AddScoped<CreateOrderHandler>(); services.AddScoped<ConfirmEmailSentHandler>(); services.AddScoped<EventHandlerContainer>(); EventHandlerContainer.Subscribe<ShoppingCartSubmittedEvent, CreateOrderHandler>(); EventHandlerContainer.Subscribe<ShoppingCartSubmittedEvent, ConfirmEmailSentHandler>(); }

注意:这里保证一个Api请求中的所有数据库操作在一个事务里,这里我们使用Scoped作用域。这样我们就可以在调用工作单元IUnitOfWork接口的Save代码中启用事务。


最后我们来修改ShoppingCartManager, 改用发布事件的方式来完成后续创建订单和发送邮件的功能。

 public void SubmitShoppingCart(string shoppingCartId) { var shoppingCart = _unitOfWork.ShoppingCartRepository .GetShoppingCart(shoppingCartId); _unitOfWork.ShoppingCartRepository .SubmitShoppingCart(shoppingCartId); _container.Publish(new ShoppingCartSubmittedEvent() { Items = shoppingCart .Items .Select(p => new ShoppingCartSubmittedItem { ItemId = p.ItemId, Name = p.Name, Price = p.Price }) .ToList() }); _unitOfWork.Save(); }

这样ShoppingCartManager就只需要关注购物车状态的变更,而不需要关注发送确认邮件和创建订单了。

最终效果

现在让我们启动项目,

首先我们使用[POST] /api/shoppingCarts来添加一个新的购物车, 这个API会返回当前购物车的Id

640?wx_fmt=png

然后我们使用[PUT] /api/shoppingCarts/ShoppingCart_636872897140555966来模拟提交购物车,程序返回操作成功

640?wx_fmt=png

最后我们查看一下控制台的输出日志

640?wx_fmt=png

2个事件处理器都被正确触发了。

总结

至此我们的代码重构完成。 最终的代码中,SubmitShoppingCart方法,仅负责修改购物车状态并发布一个购物车提交的事件。生成订单和发送邮件的功能代码都被移动到了独立的处理类中。

这样的方式的好处不仅仅是完成了代码的解耦,针对后续的扩展也非常有利,想想一下,如果在未来当前项目需求追加这样一个功能,当提交购物车的时候,除了要发送确认邮件,还要发送手机短信。这时候你根本不需要去修改ShoppingCartManager类,你只需要针对ShoppingCartSubmittedEvent在再添加一个新的事件处理器即可,这也满足的SOLID的开闭原则。


原文地址:https://www.cnblogs.com/lwqlun/p/10468058.html

.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com
640?wx_fmt=jpeg


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/316911.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CF1592E Bored Bakry(二进制+前缀异或和)

CF1592E Bored Bakrydescriptionsolutioncodedescription 题目链接 solution and\text{and}and如果第iii位为111&#xff0c;意味着区间内每个数的第iii位都是111 xor\text{xor}xor如果第iii位为111&#xff0c;意味着区间内有奇数个第iii位为111 这种涉及二进制操作的一般都…

Joy of Handcraft Gym - 102822J(线段树或差分)

Joy of Handcraft Gym - 102822J 题意&#xff1a; 每个灯有亮的周期和亮度&#xff0c;问1~m这段时间灯光最亮是多少 题解&#xff1a; 线段树维护区间最大值 根据灯的周期向这段区间加亮度k&#xff0c;然后利用线段树维护区间最大值 但是这样会超时&#xff0c;加个小优…

.NET Core 使用 HttpClient SSL 请求出错的解决办法

问题使用 HTTP Client 请求 HTTPS 的 API 时出现 The certificate cannot be verified up to a trusted certification authority 异常&#xff0c;并且证书已经传入。下面就是问题代码&#xff1a;public class Program{public static void Main(string[] args){var url &quo…

CF1580C Train Maintenance(分块)

CF1580C Train Maintenancedescriptionsolutioncodedescription 题目链接 solution 这是一种利用根号平衡时间复杂度的套路 分α\alphaα【操作参数】与n\sqrt{n}n​的关系&#xff0c;一半采取暴力&#xff0c;一半利用工具特殊处理 对于本题&#xff0c;假设第iii辆车的加…

P3746 [六省联考 2017] 组合数问题(倍增、dp)

解析 再次被“组合数问题”吊打qwq 和上一次不一样的是&#xff0c;这次更加被恶心到了。 一方面受上一个组合数问题影响&#xff0c;另外出题人也十分阴间&#xff0c;一开始还给了个组合数的公式&#xff0c;更加使我坚定的认为这是一道数学推柿子题。 然后就开始各种打表玩…

CF1473E Minimum Path(拆点+最短路)

CF1473E Minimum Pathdescriptionsolutioncodedescription 题目链接 solution 看到 ∑i1kwei\sum_{i1}^kw_{e_i}∑i1k​wei​​ 的式子&#xff0c;就应该联想到最短路 先考虑题目的弱化版&#xff0c;去掉 max,min\text{max},\text{min}max,min 的限制&#xff0c;变成一条…

2020CCPC绵阳

2020CCPC绵阳 题号题目名难度知识点AA Colorful GridBBuilding BlocksCCode a TrieDDefuse the Bombs签到二分EEscape from the IslandFFracture RayGGame of Cards银牌博弈论&#xff0c;SG函数HHide and SeekIInvaluable AssetsJJoy of Handcraft快铜线段树KKnowledge is Po…

P3747 [六省联考 2017] 相逢是问候(欧拉定理、线段树、光速幂)

解析 洛谷你恶事做尽&#xff01; 第三个tag在LOJ、bzoj等都是不需要的… 但在洛谷三只log根本过不去… 我谔谔。 如果做过 上帝与集合的正确用法 &#xff0c;那么本题就并不难了。 打个表就可以发现&#xff0c;不断取欧拉函数的上限只有log级别&#xff0c;这使得我们暴力…

Lottery Gym - 102822L

Lottery Gym - 102822L 题意&#xff1a; 有n个盒子&#xff0c;每个盒子有x个球&#xff0c;每个球的数值为2a,问最多能组成多少数&#xff1f;答案mod 1e97 题解&#xff1a; 二进制思维题&#xff0c;浓浓的cf风格 参考题解 我们将数按照幂次进行排序&#xff08;从小到…

[CCO 2019] Sirtet(差分约束+最短路)

[CCO 2019] Sirtetdescriptionsolutioncodedescription 题目链接 solution 很巧妙地将差分约束隐藏起来 问题的关键在于求出每一个sand停止运动的时间&#xff0c;这样很容易填涂出最后的答案&#xff08;向下平移即可&#xff09; 不妨设 ti,jt_{i,j}ti,j​ 表示 (i,j)(i…

P3748 [六省联考 2017] 摧毁“树状图”(树形dp)

Foreword\text{Foreword}Foreword 《小清新》树形 dp。 其实本题没有那么&#xff08;重读&#xff09;恶心&#xff0c;但我一开始写完 x0x0x0 后眼瞎没有看到 x1,2x1,2x1,2 时也必然是最优方案&#xff08;这种东西不黑体吗…&#xff09;可以直接无视&#xff0c;还在苦苦的…

微软4年后重登市值第一,纳德拉如何做到的?

他用4年多时间将微软的市值提高了5000亿美元&#xff0c;超越苹果再次成为全球市值最高的上市公司。译 | 达达萨提亚纳德拉&#xff08;Satya Nadella&#xff09;2014年刚刚执掌微软时&#xff0c;微软当时是一个日渐没落的帝国。但在他领导的4年多时间里&#xff0c;微软百花…

[AtCoder Regular Contest 124E] Pass to Next(dp+数学)

ARC 124 E Pass to Nextproblemsolutioncodeproblem 题目链接 solution 令 ci:c_i:ci​: 第 iii 个人传给下一个人球的个数 当 min⁡{ci}≠0\min\{c_i\}\neq 0min{ci​}​0 时&#xff0c;将每一个 cic_ici​ 都减小 111&#xff0c;显然答案序列并不会改变 所以&#x…

P2015 二叉苹果树

P2015 二叉苹果树 题意&#xff1a; 一个完全二叉树&#xff0c;n个点&#xff0c;n-1个边&#xff0c;每个边都有边权&#xff0c;问保留q个边&#xff0c;所能保留的最大边权是多少 题解&#xff1a; 树形dp dp[u][i]表示u的子树上保留i条边&#xff0c;至多保留的苹果数…

P4384 [八省联考 2018] 制胡窜(SAM)

Foreword\text{Foreword}Foreword 人都道正难则反&#xff0c;我偏说正也不难。 这里介绍一种正面直接统计的做法。 和补集做法相比&#xff0c;没有那么多的分类讨论&#xff0c;更多的是对问题的正向分析和逐层化简、转化&#xff0c;也并不麻烦。 由于需要写很多线段树的操作…

【.NET Core项目实战-统一认证平台】第十五章 网关篇-使用二级缓存提升性能

首先说声抱歉&#xff0c;可能是因为假期综合症&#xff08;其实就是因为懒哈&#xff09;的原因&#xff0c;已经很长时间没更新博客了&#xff0c;现在也调整的差不多了&#xff0c;准备还是以每周1-2篇的进度来更新博客&#xff0c;并完成本项目所有功能。言归正传&#xff…

树上子链(树形dp求树的直径)

树上子链 题意&#xff1a; 给定一棵树 T &#xff0c;树 T 上每个点都有一个权值。 定义一颗树的子链的大小为&#xff1a;这个子链上所有结点的权值和 。 请在树 T 中找出一条最大的子链并输出。 题解&#xff1a; 求树的直径&#xff0c;题目中存在负权值&#xff0c;树…

【无码专区1】简单路径的第二大边权(启发式合并+最小生成树)

只有std&#xff0c;没有自我实现&#xff0c;所以叫做无码专区 description 给一张无向图&#xff0c;多次询问&#xff0c;每次询问两个点之间所有简单路径&#xff08;不重复经过点&#xff09;中边权第二大&#xff08;不是严格第二大&#xff09;的权值的最小值。 数据…

# CF1572B Xor of 3(构造)

解析 你CF还是你CF 省选刷到2017再往前不是很想做了&#xff0c;就来CF玩一玩。 再次感受到被CF浅颜色构造虐的快感。 本题靠着各种乱搞特判在WA了无数次之后艹过去了。 根本没有什么正确性的玄学做法&#xff0c;但是看CF数据似乎把 nnn 较小的所有情况全都pia到数据里了&…

NET Core微服务之路:简单谈谈对ELK,Splunk,Exceptionless统一日志收集中心的心得体会...

前言日志&#xff0c;一直以来都是开发人员和运维人员最关心的问题。开发人员可通过日志记录来协助问题定位&#xff0c;运维人员可通过日志发现系统隐患&#xff0c;故障等定位问题。如果你的系统中没有日志&#xff0c;就像一个断了线的风筝&#xff0c;你永远不知道它会的落…