标题: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个处理类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

最后我们来修改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中实现单体程序的事件发布/订阅的更多相关文章

  1. Asp.Net Core 中获取应用程序物理路径(Getting the Web Root Path and the Content Root Path in ASP.NET Core)

    如果要得到传统的ASP.Net应用程序中的相对路径或虚拟路径对应的服务器物理路径,只需要使用使用Server.MapPath()方法来取得Asp.Net根目录的物理路径,如下所示: // Classi ...

  2. ASP.NET Core 中的应用程序启动 Startup

      ASP.NET Core 应用使用Startup类来作为启动类.   Startup类中包含了ConfigureServices方法,Configure方法,IConfiguration,IHos ...

  3. ASP.NET Core 中文文档 第三章 原理(1)应用程序启动

    原文:Application Startup 作者:Steve Smith 翻译:刘怡(AlexLEWIS) 校对:谢炀(kiler398).许登洋(Seay) ASP.NET Core 为你的应用程 ...

  4. 在docker中运行ASP.NET Core Web API应用程序

    本文是一篇指导快速演练的文章,将介绍在docker中运行一个ASP.NET Core Web API应用程序的基本步骤,在介绍的过程中,也会对docker的使用进行一些简单的描述.对于.NET Cor ...

  5. 在Visual Studio 2017中使用Asp.Net Core构建Angular4应用程序

    前言 Visual Studio 2017已经发布了很久了.做为集成了Asp.Net Core 1.1的地表最强IDE工具,越来越受.NET系的开发人员追捧. 随着Google Angular4的发布 ...

  6. 【Asp.Net Core】在Visual Studio 2017中使用Asp.Net Core构建Angular4应用程序

    前言 Visual Studio 2017已经发布了很久了.做为集成了Asp.Net Core 1.1的地表最强IDE工具,越来越受.NET系的开发人员追捧. 随着Google Angular4的发布 ...

  7. ASP.NET Core Web 应用程序系列(五)- 在ASP.NET Core中使用AutoMapper进行实体映射

    本章主要简单介绍下在ASP.NET Core中如何使用AutoMapper进行实体映射.在正式进入主题之前我们来看下几个概念: 1.数据库持久化对象PO(Persistent Object):顾名思义 ...

  8. ASP.NET Core Web 应用程序系列(三)- 在ASP.NET Core中使用Autofac替换自带DI进行构造函数和属性的批量依赖注入(MVC当中应用)

    在上一章中主要和大家分享了在ASP.NET Core中如何使用Autofac替换自带DI进行构造函数的批量依赖注入,本章将和大家继续分享如何使之能够同时支持属性的批量依赖注入. 约定: 1.仓储层接口 ...

  9. ASP.NET Core Web 应用程序系列(二)- 在ASP.NET Core中使用Autofac替换自带DI进行批量依赖注入(MVC当中应用)

    在上一章中主要和大家分享在MVC当中如何使用ASP.NET Core内置的DI进行批量依赖注入,本章将继续和大家分享在ASP.NET Core中如何使用Autofac替换自带DI进行批量依赖注入. P ...

随机推荐

  1. DDGScreenShot — 复杂屏幕截屏(如view ScrollView webView wkwebView)

    写在前面 最近有这么一个需求,分享页面,分享的是web订单截图,既然是web 就会有超出屏幕的部分, 生成的图片还要加上我们的二维码,这就涉及到图片的合成了. 有了这样的需求,就是各种google.也 ...

  2. java线程之线程通信

    前面提到多线程操作会有一个资源共享问题. 日常生活中,对于一个超市,有供货商往超市运货,有消费者从超市取货,供货商和消费者都与超市 建立了某种连接,超市就相当于公共资源,而他们的这种行为放到线程上说就 ...

  3. Python学习 Part7:类

    Python学习 Part7:类 1. 作用域和命名空间 命名空间(namespace)就是一个从名称到对象的映射. 命名空间的一些实例:内置名称集(函数,像abs(),和内置异常名称),一个模块中的 ...

  4. wordpress上下篇

    分页一: <!--上下篇开始--> <div class="shangxia"> <?php $current_category=get_the_ca ...

  5. sh 脚本执行sql文件传参数

    一.前言 今天做数据删除,用的命令行输入参数,并且调用执行的sql文件,我采用了sed命令,进行替换. sh脚本如下 #! /bin/sh echo "Please enter the ba ...

  6. 怎么确定Oracle客户端安装成功

    可通过能否登录sqlplus来判断是否安装成功. 操作系统:windows10 oracle版本:oracle 11g 步骤: 1.电脑win键+R键,输入cmd,进入命令提示符. 2.命令行中输入: ...

  7. vue项目如何刷新当前页面

    1.场景 在处理列表时,常常有删除一条数据或者新增数据之后需要重新刷新当前页面的需求. 2.遇到的问题 1. 用vue-router重新路由到当前页面,页面是不进行刷新的 2.采用window.rel ...

  8. 解决VMware虚拟机报错“无法连接MKS:套接字连接尝试次数太多,正在放弃”

    1.错误描述 在VMware中打开虚拟机时报错: "无法连接MKS:套接字连接尝试次数太多,正在放弃" 物理机操作系统: Windows 7 虚拟机操作系统: Kali Linux ...

  9. mybatis整合spring获取配置文件信息出错

    描述:mybatis整合spring加载jdbc.properties文件,然后使用里面配置的值来 配置数据源,后来发现用户变成了admin- jdbc.properties的配置: 加载配置: 报错 ...

  10. TestNG详解-深度好文

    转自: https://blog.csdn.net/lykangjia/article/details/56485295 TestNG详解-深度好文 2017年02月22日 14:51:52 阅读数: ...