ASP.NET Core中實現單體程序的事件發佈/訂閱

標題:ASP.NET Core中實現單體程序的事件發佈/訂閱
做者:Lamond Lu
地址:http://www.javashuo.com/article/p-phbjtsuc-ds.html
項目源代碼:https://github.com/lamondlu/EventHandlerInSingleApplicationhtml

背景

事件發佈/訂閱是一種很是強大的模式,它能夠幫助業務組件間實現徹底解耦,不一樣的業務組件只依賴事件,只關注哪些事件是須要本身處理的,而不用關注誰來處理本身發佈事件,事件追溯(Event Sourcing)也是基於事件發佈/訂閱的。在微服務架構中,事件發佈/訂閱有很是多的應用場景。今天我給你們分享一個基於ASP.NET Core的單體程序使用事件發佈/訂閱的例子,針對分佈式項目的事件發佈/訂閱比較複雜,難點是事務處理,後續我會另寫一篇博文來演示。git

案例說明

當前咱們有一個基於ASP.NET Core的電子商務系統,在項目的初期,業務很是簡單,只有一個購物車模塊和一個訂單模塊,全部的代碼都放在一個項目中。github

整個項目使用了一個簡單的三層架構。數據庫

這裏當用戶提交購物車的時候,程序會在ShoppingCartManager類的SubmitShoppingCart方法中執行3個操做c#

  • 修改當前購物車的狀態爲完成
  • 根據購物車中的物品建立一個新訂單
  • 給用戶發郵件

代碼以下:api

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事件處理器會負責發送郵件。

好的,下面讓咱們來一步一步實現以上描述的代碼。app

添加事件基類

這裏咱們首先定義一個事件基類,其中暫時只添加了一個屬性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的開閉原則。

相關文章
相關標籤/搜索