基於事件匯流排EventBus實現郵件推送功能

灬丶發表於2024-08-26

有時候,有人給我的網站留了言,但是我必須要開啟我的網站(https://www.xiandanplay.com/)才知道,所以我便決定給網站增加一個郵件推送的功能,好讓我第一時間知道。於是乎,按照我自己的思路,同時為了去學習瞭解rabbitmq以及EventBus概念,我便設計了一套郵件推送的功能,這裡分享出來,可能方案不是很好,大家不喜勿噴。

什麼是事件匯流排

事件匯流排是對釋出-訂閱模式的一種實現。它是一種集中式事件處理機制,允許不同的元件之間進行彼此通訊而又不需要相互依賴,達到一種解耦的目的。

關於這個概念,網上有很多講解的,這裡我推薦一個講的比較好的(事件匯流排知多少)

什麼是RabbitMQ

RabbitMQ這個就不用說了,想必到家都知道。

粗糙流程圖

簡單來解釋就是:

1、定義一個事件抽象類

public abstract class EventData
    {
        /// <summary>
        /// 唯一標識
        /// </summary>
        public string Unique { get; set; }
        /// <summary>
        /// 是否成功
        /// </summary>
        public bool Success { get; set; }
        /// <summary>
        /// 結果
        /// </summary>
        public string Result { get; set; }
    }

 2、定義一個事件處理抽象類,以及對應的一個佇列訊息執行的一個記錄

public abstract class EventHandler<T> where T : EventData
    {
        public async Task Handler(T eventData)
        {
            await BeginHandler(eventData.Unique);
            eventData = await ProcessingHandler(eventData);
            if (eventData.Success)
                await FinishHandler(eventData);
        }
        /// <summary>
        ///  開始處理
        /// </summary>
        /// <param name="unique"></param>
        /// <returns></returns>
        protected abstract Task BeginHandler(string unique);
        /// <summary>
        /// 處理中
        /// </summary>
        /// <param name="eventData"></param>
        /// <returns></returns>
        protected abstract Task<T> ProcessingHandler(T eventData);
        /// <summary>
        /// 處理完成
        /// </summary>
        /// <param name="eventData"></param>
        /// <returns></returns>
        protected abstract Task FinishHandler(T eventData);
    }
   
   [Table("Sys_TaskRecord")]
    public class TaskRecord : Entity<long>
    {
        /// <summary>
        /// 任務型別
        /// </summary>
        public TaskRecordType TaskType { get; set; }
        /// <summary>
        /// 任務狀態
        /// </summary>
        public int TaskStatu { get; set; }
        /// <summary>
        /// 任務值
        /// </summary>
        public string TaskValue { get; set; }
        /// <summary>
        /// 任務結果
        /// </summary>
        public string TaskResult { get; set; }
        /// <summary>
        /// 任務開始時間
        /// </summary>
        public DateTime TaskStartTime { get; set; }
        /// <summary>
        /// 任務完成時間
        /// </summary>
        public DateTime? TaskFinishTime { get; set; }
        /// <summary>
        /// 任務最後更新時間
        /// </summary>
        public DateTime? LastUpdateTime { get; set; }
        /// <summary>
        /// 任務名稱
        /// </summary>
        public string TaskName { get; set; }
        /// <summary>
        /// 附加資料
        /// </summary>
        public string AdditionalData { get; set; }
    }

   3、定義一個郵件事件訊息類,繼承自EventData,以及一個郵件處理的Hanler繼承自EventHandler

 public class EmailEventData:EventData
    {
        /// <summary>
        /// 郵件內容
        /// </summary>
        public string Body { get; set; }
        /// <summary>
        /// 接收者
        /// </summary>
        public string Reciver { get; set; }
    }

 public class CreateEmailHandler<T> : Core.EventBus.EventHandler<T> where T : EventData
    {
        private IEmailService emailService;
        private IUnitOfWork unitOfWork;
        private ITaskRecordService taskRecordService;
        public CreateEmailHandler(IEmailService emailService, IUnitOfWork unitOfWork, ITaskRecordService taskRecordService)
        {
            this.emailService = emailService;
            this.unitOfWork = unitOfWork;
            this.taskRecordService = taskRecordService;
        }
        protected override async Task BeginHandler(string unique)
        {
            await taskRecordService.UpdateRecordStatu(Convert.ToInt64(unique), (int)MqMessageStatu.Processing);
            await unitOfWork.CommitAsync();
        }

        protected override async Task<T> ProcessingHandler(T eventData)
        {
            try
            {
                EmailEventData emailEventData = eventData as EmailEventData;
                await emailService.SendEmail(emailEventData.Reciver, emailEventData.Reciver, emailEventData.Body, "[閒蛋]收到一條留言");
                eventData.Success = true;
            }
            catch (Exception ex)
            {
                await taskRecordService.UpdateRecordFailStatu(Convert.ToInt64(eventData.Unique), (int)MqMessageStatu.Fail,ex.Message);
                await unitOfWork.CommitAsync();
                eventData.Success = false;
            }
            return eventData;

        }

        protected override async Task FinishHandler(T eventData)
        {
            await taskRecordService.UpdateRecordSuccessStatu(Convert.ToInt64(eventData.Unique), (int)MqMessageStatu.Finish,"");
            await unitOfWork.CommitAsync();
        }

   4、接著就是如何把事件訊息和事件Hanler關聯起來,那麼我這裡思路就是把EmailEventData的型別和CreateEmailHandler的型別先註冊到字典裡面,這樣我就可以根據EmailEventData找到對應的處理程式了,找型別還不夠,如何建立例項呢,這裡就還需要把CreateEmailHandler註冊到DI容器裡面,這樣就可以根據容器獲取物件了,如下

  public void AddSub<T, TH>()
             where T : EventData
             where TH : EventHandler<T>
        {
            Type eventDataType = typeof(T);
            Type handlerType = typeof(TH);
            if (!eventhandlers.ContainsKey(typeof(T)))
                eventhandlers.TryAdd(eventDataType, handlerType);
            _serviceDescriptors.AddScoped(handlerType);
        }
-------------------------------------------------------------------------------------------------------------------
 public Type FindEventType(string eventName)
        {
            if (!eventTypes.ContainsKey(eventName))
                throw new ArgumentException(string.Format("eventTypes不存在類名{0}的key", eventName));
            return eventTypes[eventName];
        }
------------------------------------------------------------------------------------------------------------------------------------------------------------
  public object FindHandlerType(Type eventDataType)
        {
            if (!eventhandlers.ContainsKey(eventDataType))
                throw new ArgumentException(string.Format("eventhandlers不存在型別{0}的key", eventDataType.FullName));
            var obj = _buildServiceProvider(_serviceDescriptors).GetService(eventhandlers[eventDataType]);
            return obj;
        }
----------------------------------------------------------------------------------------------------------------------------------
 private static IServiceCollection AddEventBusService(this IServiceCollection services)
        {
            string exchangeName = ConfigureProvider.configuration.GetSection("EventBusOption:ExchangeName").Value;
            services.AddEventBus(Assembly.Load("XianDan.Application").GetTypes())
                .AddSubscribe<EmailEventData, CreateEmailHandler<EmailEventData>>(exchangeName, ExchangeType.Direct, BizKey.EmailQueueName);
            return services;
        }

   5、傳送訊息,這裡程式碼簡單,就是簡單的傳送訊息,這裡用eventData.GetType().Name作為訊息的RoutingKey,這樣消費這就可以根據這個key呼叫FindEventType,然後找到對應的處理程式了

 using (IModel channel = connection.CreateModel())
{
     string routeKey = eventData.GetType().Name;
     string message = JsonConvert.SerializeObject(eventData);
     byte[] body = Encoding.UTF8.GetBytes(message);
     channel.ExchangeDeclare(exchangeName, exchangeType, true, false, null);
     channel.QueueDeclare(queueName, true, false, false, null);
     channel.BasicPublish(exchangeName, routeKey, null, body);
}

  

6、訂閱訊息,核心的是這一段

Type eventType = _eventBusManager.FindEventType(eventName);
var eventData = (T)JsonConvert.DeserializeObject(body, eventType);
EventHandler<T> eventHandler = _eventBusManager.FindHandlerType(eventType) as EventHandler<T>;

 public void Subscribe<T, TH>(string exchangeName, string exchangeType, string queueName)
            where T : EventData
            where TH : EventHandler<T>
        {
            try
            {
                _eventBusManager.AddSub<T, TH>();
                IModel channel = connection.CreateModel();
                channel.QueueDeclare(queueName, true, false, false, null);
                channel.ExchangeDeclare(exchangeName, exchangeType, true, false, null);
                channel.QueueBind(queueName, exchangeName, typeof(T).Name, null);
                var consumer = new EventingBasicConsumer(channel);
                consumer.Received += async (model, ea) =>
                {
                    string eventName = ea.RoutingKey;
                    byte[] resp = ea.Body.ToArray();
                    string body = Encoding.UTF8.GetString(resp);
                    try
                    {
                        Type eventType = _eventBusManager.FindEventType(eventName);
                        var eventData = (T)JsonConvert.DeserializeObject(body, eventType);
                        EventHandler<T> eventHandler = _eventBusManager.FindHandlerType(eventType) as EventHandler<T>;
                        await eventHandler.Handler(eventData);
                    }
                    catch (Exception ex)
                    {
                        LogUtils.LogError(ex, "EventBusRabbitMQ", ex.Message);
                    }
                    finally
                    {
                        channel.BasicAck(ea.DeliveryTag, false);
                    }
                  

                };
                channel.BasicConsume(queueName, autoAck: false, consumer: consumer);
            }
            catch (Exception ex)
            {
                LogUtils.LogError(ex, "EventBusRabbitMQ.Subscribe", ex.Message);
            }

        }

   注意,這裡我使用的時候有個小坑,就是最開始是用using包裹這個IModel channel = connection.CreateModel();導致最後程式啟動後無法收到訊息,然後去rabbitmq的管理介面發現沒有channel連線,佇列也沒有消費者,最後發現可能是using執行完後就釋放掉了,把using去掉就好了。

好了,到此,我的思路大概講完了,現在我的網站留言也可以收到郵件了,那麼多測試郵件,哈哈哈哈哈

大家感興趣的話可以去我的網站(https://www.xiandanplay.com/)踩一踩,互加友鏈也可以的,謝謝大家,不喜勿噴嘍!

相關文章