标题:ASP.NET Core中实现单体程序的事件发布/订阅
作者:Lamond Lu
地址:https://www.cnblogs.com/lwqlun/p/10468058.html
项目源代码:https://github.com/lamondlu/EventHandlerInSingleApplication
背景
事件发布/订阅是一种非常强大的模式,它可以帮助业务组件间实现完全解耦,不同的业务组件只依赖事件,只关注哪些事件是需要自己处理的,而不用关注谁来处理自己发布事件,事件追溯(Event Sourcing)也是基于事件发布/订阅的。在微服务架构中,事件发布/订阅有非常多的应用场景。今天我给大家分享一个基于ASP.NET Core的单体程序使用事件发布/订阅的例子,针对分布式项目的事件发布/订阅比较复杂,难点是事务处理,后续我会另写一篇博文来演示。
案例说明
当前我们有一个基于ASP.NET Core的电子商务系统,在项目的初期,业务非常简单,只有一个购物车模块和一个订单模块,所有的代码都放在一个项目中。
整个项目使用了一个简单的三层架构。
这里当用户提交购物车的时候,程序会在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
需要负责的,所以我们需要它们从这个方法中移出去,使用的方法就是事件订阅/发布。
新的架构图
以下是使用事件发布/订阅之后的系统架构图。
- 这里我们会创建一个购物车提交事件
ShoppingCartSubmittedEvent
。 - 当站点启动的时候,我们会在一个名为
EventHandlerContainer
的类中注册订阅ShoppingCartSubmittedEvent
事件的2个处理类CreateOrderHandler
和ConfirmEmailSentHandler
。 - 在
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个方法Run
和RunAsync
。 它们定义了该接口的实现类必须实现同一个处理逻辑的同步和异步方法。
为购物车提交事件编写事件处理器
有了事件处理器接口,接下来我们就可以开始为购物车提交事件添加事件处理器了。这里我们为了实现前面定义的逻辑,我们需要创建2个处理器CreateOrderHandler
和ConfirmEmailSentHandler
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.cs
的ConfigureServices
方法,这里我们需要进行服务注册,并完成事件订阅。
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
最后我们来修改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
然后我们使用[PUT] /api/shoppingCarts/ShoppingCart_636872897140555966来模拟提交购物车,程序返回操作成功
最后我们查看一下控制台的输出日志
2个事件处理器都被正确触发了。
总结
至此我们的代码重构完成。 最终的代码中,SubmitShoppingCart
方法,仅负责修改购物车状态并发布一个购物车提交的事件。生成订单和发送邮件的功能代码都被移动到了独立的处理类中。
这样的方式的好处不仅仅是完成了代码的解耦,针对后续的扩展也非常有利,想想一下,如果在未来当前项目需求追加这样一个功能,当提交购物车的时候,除了要发送确认邮件,还要发送手机短信。这时候你根本不需要去修改ShoppingCartManager
类,你只需要针对ShoppingCartSubmittedEvent
在再添加一个新的事件处理器即可,这也满足的SOLID的开闭原则。
ASP.NET Core中实现单体程序的事件发布/订阅的更多相关文章
-
Asp.Net Core 中获取应用程序物理路径(Getting the Web Root Path and the Content Root Path in ASP.NET Core)
如果要得到传统的ASP.Net应用程序中的相对路径或虚拟路径对应的服务器物理路径,只需要使用使用Server.MapPath()方法来取得Asp.Net根目录的物理路径,如下所示: // Classi ...
-
ASP.NET Core 中的应用程序启动 Startup
ASP.NET Core 应用使用Startup类来作为启动类. Startup类中包含了ConfigureServices方法,Configure方法,IConfiguration,IHos ...
-
ASP.NET Core 中文文档 第三章 原理(1)应用程序启动
原文:Application Startup 作者:Steve Smith 翻译:刘怡(AlexLEWIS) 校对:谢炀(kiler398).许登洋(Seay) ASP.NET Core 为你的应用程 ...
-
在docker中运行ASP.NET Core Web API应用程序
本文是一篇指导快速演练的文章,将介绍在docker中运行一个ASP.NET Core Web API应用程序的基本步骤,在介绍的过程中,也会对docker的使用进行一些简单的描述.对于.NET Cor ...
-
在Visual Studio 2017中使用Asp.Net Core构建Angular4应用程序
前言 Visual Studio 2017已经发布了很久了.做为集成了Asp.Net Core 1.1的地表最强IDE工具,越来越受.NET系的开发人员追捧. 随着Google Angular4的发布 ...
-
【Asp.Net Core】在Visual Studio 2017中使用Asp.Net Core构建Angular4应用程序
前言 Visual Studio 2017已经发布了很久了.做为集成了Asp.Net Core 1.1的地表最强IDE工具,越来越受.NET系的开发人员追捧. 随着Google Angular4的发布 ...
-
ASP.NET Core Web 应用程序系列(五)- 在ASP.NET Core中使用AutoMapper进行实体映射
本章主要简单介绍下在ASP.NET Core中如何使用AutoMapper进行实体映射.在正式进入主题之前我们来看下几个概念: 1.数据库持久化对象PO(Persistent Object):顾名思义 ...
-
ASP.NET Core Web 应用程序系列(三)- 在ASP.NET Core中使用Autofac替换自带DI进行构造函数和属性的批量依赖注入(MVC当中应用)
在上一章中主要和大家分享了在ASP.NET Core中如何使用Autofac替换自带DI进行构造函数的批量依赖注入,本章将和大家继续分享如何使之能够同时支持属性的批量依赖注入. 约定: 1.仓储层接口 ...
-
ASP.NET Core Web 应用程序系列(二)- 在ASP.NET Core中使用Autofac替换自带DI进行批量依赖注入(MVC当中应用)
在上一章中主要和大家分享在MVC当中如何使用ASP.NET Core内置的DI进行批量依赖注入,本章将继续和大家分享在ASP.NET Core中如何使用Autofac替换自带DI进行批量依赖注入. P ...
随机推荐
-
怎么样修改PHPStorm中文件修改后标签和文件名的颜色与背景色
自从最近在PHPstrom里引入Git,并且使用MONOKAI_SUBLIME主题之后 ,当文件在PHPstrom中进行编辑,文档内容变化时,左侧项目文件列表中的文件名颜色以及右侧编辑区域标签卡的文件 ...
-
BZOJ1093 最大半连通子图
Description 一个有向图G=(V,E)称为半连通的(Semi-Connected),如果满足:?u,v∈V,满足u→v或v→u,即对于图中任意 两点u,v,存在一条u到v的有向路径或者从v到 ...
-
Java substring() 方法
Java String类 substring() 方法返回字符串的子字符串. 语法 public String substring(int beginIndex) 或 public String su ...
-
WP-PostViews Plus停止计数
最近老是发现WP-PostViews Plus停止计数,网上查了资料才发现,WP Super Cache和WP-PostViews Plus存在冲突.根据自己网站目前的情况看,暂时用不到WP Supe ...
-
linux_samba服务安装
什么是samba服务? 用于Windows和linux系统之间实现共享文件的目的服务 如何配置其服务? Linux端: 搭建服务 1. 安装samba yum install -y samba 2. ...
-
涉及模式之 装饰器模式详解(与IO不解的情缘)
作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可. LZ到目前已经写了九个设计模 ...
-
python类:类方法和静态方法
http://blog.csdn.net/pipisorry/article/details/49516185 面相对象程序设计中,类方法和静态方法是经常用到的两个术语.逻辑上讲:类方法是只能由类名调 ...
-
Android探究之View的绘制流程
Android中Activity是作为应用程序的载体存在,代表着一个完整的用户界面,提供了一个窗口来绘制各种视图,当Activity启动时,我们会通过setContentView方法来设置一个内容视图 ...
-
Netty学习(八)-Netty的心跳机制
版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/a953713428/article/details/69378412我们知道在TCP长连接或者Web ...
-
FAST特征点检测算法
一 原始方法 简介 在局部特征点检测快速发展的时候,人们对于特征的认识也越来越深入,近几年来许多学者提出了许许多多的特征检测算法及其改进算法,在众多的特征提取算法中,不乏涌现出佼佼者. 从最早期的Mo ...