歡迎光臨
每天分享高質量文章

ASP.NET Core中實現單體程式的事件發佈/訂閱 – LamondLu – 博客園

標題: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(); } public List 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 where T : EventBase { void Run(T obj); Task RunAsync(T obj); }

這個泛型接口類的是泛型型別必須繼承自EventBase類。接口提供了2個方法RunRunAsync。 它們定義了該接口的實現類必須實現同一個處理邏輯的同步和異步方法。

為購物車提交事件編寫事件處理器

有了事件處理器接口,接下來我們就可以開始為購物車提交事件添加事件處理器了。這裡我們為了實現前面定義的邏輯,我們需要創建2個處理器CreateOrderHandlerConfirmEmailSentHandler

CreateOrderHandler.cs

 public class CreateOrderHandler : IEventHandler { 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 { 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> _mappings = new Dictionary>(); public EventHandlerContainer(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public static void Subscribe() where T : EventBase where THandler : IEventHandler { var name = typeof(T).Name; if (!_mappings.ContainsKey(name)) { _mappings.Add(name, new List { }); } _mappings[name].Add(typeof(THandler)); } public static void Unsubscribe() where T : EventBase where THandler : IEventHandler { var name = typeof(T).Name; _mappings[name].Remove(typeof(THandler)); if (_mappings[name].Count == 0) { _mappings.Remove(name); } } public void Publish(T o) where T : EventBase { var name = typeof(T).Name; if (_mappings.ContainsKey(name)) { foreach (var handler in _mappings[name]) { var service = (IEventHandler)_serviceProvider.GetService(handler); service.Run(o); } } } public async Task PublishAsync(T o) where T : EventBase { var name = typeof(T).Name; if (_mappings.ContainsKey(name)) { foreach (var handler in _mappings[name]) { var service = (IEventHandler)_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(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); EventHandlerContainer.Subscribe(); EventHandlerContainer.Subscribe(); }

註意:這裡保證一個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

然後我們使用[PUT] /api/shoppingCarts/ShoppingCart_636872897140555966來模擬提交購物車,程式傳回操作成功

最後我們查看一下控制台的輸出日誌

2個事件處理器都被正確觸發了。

總結

至此我們的代碼重構完成。 最終的代碼中,SubmitShoppingCart方法,僅負責修改購物車狀態併發布一個購物車提交的事件。生成訂單和發送郵件的功能代碼都被移動到了獨立的處理類中。

這樣的方式的好處不僅僅是完成了代碼的解耦,針對後續的擴展也非常有利,想想一下,如果在未來當前專案需求追加這樣一個功能,當提交購物車的時候,除了要發送確認郵件,還要發送手機短信。這時候你根本不需要去修改ShoppingCartManager類,你只需要針對ShoppingCartSubmittedEvent在再添加一個新的事件處理器即可,這也滿足的SOLID的開閉原則。

 

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

赞(0)

分享創造快樂