造輪子之EventBus

飯勺oO發表於2023-10-12

前面基礎管理的功能基本開發完了,接下來我們來最佳化一下開發功能,來新增EventBus功能。
EventBus也是我們使用場景非常廣的東西。這裡我會實現一個本地的EventBus以及分散式的EventBus。
分別使用MediatR和Cap來實現。

現在簡單介紹一下這兩者:
MediatR是一個輕量級的中介者庫,用於實現應用程式內部的訊息傳遞和處理。它提供了一種簡單而強大的方式來解耦應用程式的不同部分,並促進了程式碼的可維護性和可測試性。使用MediatR,您可以定義請求和處理程式,然後透過傳送請求來觸發相應的處理程式。這種模式使得應用程式的不同元件可以透過訊息進行通訊,而不需要直接引用彼此的程式碼。MediatR還提供了管道處理功能,可以在請求到達處理程式之前或之後執行一些邏輯,例如驗證、日誌記錄或快取。
Cap是一個基於.NET的分散式事務訊息佇列框架,用於處理高併發、高可靠性的訊息傳遞。它支援多種訊息佇列中介軟體,如RabbitMQ、Kafka和Redis。Cap提供了一種可靠的方式來處理分散式事務,確保訊息的可靠傳遞和處理。它還支援事件釋出/訂閱模式,使得不同的服務可以透過釋出和訂閱事件來進行解耦和通訊。Cap還提供了一些高階功能,如訊息重試、訊息順序處理和訊息回溯,以應對各種複雜的場景。
總結來說,MediatR適用於應用程式內部的訊息傳遞和處理,它強調解耦和可測試性。而Cap則更適合處理分散式系統中的訊息傳遞和事務,它提供了高可靠性和高併發的支援,並且適用於處理複雜的分散式場景。

定義介面

新增一個ILocalEventBus介面,裡面包含一個PublishAsync事件釋出方法。

namespace Wheel.EventBus.Local
{
    public interface ILocalEventBus
    {
        Task PublishAsync<TEventData>(TEventData eventData, CancellationToken cancellationToken = default);
    }
}

新增一個IDistributedEventBus介面,裡面包含一個PublishAsync事件釋出方法。

namespace Wheel.EventBus.Distributed
{
    public interface IDistributedEventBus
    {
        Task PublishAsync<TEventData>(TEventData eventData, CancellationToken cancellationToken = default);
    }
}

新增一個IEventHandler的空介面,作為事件處理的基礎介面

namespace Wheel.EventBus
{
    public interface IEventHandler
    {
    }
}

LocalEventBus

這裡我們用MediatR的Notification來實現我們的本地事件匯流排。
首先安裝MediatR的Nuget包。

MediatREventBus

然後實現MediatREventBus,這裡其實就是包裝以下IMediator.Publish方法。

using MediatR;
using Wheel.DependencyInjection;

namespace Wheel.EventBus.Local.MediatR
{
    public class MediatREventBus : ILocalEventBus, ITransientDependency
    {
        private readonly IMediator _mediator;

        public MediatREventBus(IMediator mediator)
        {
            _mediator = mediator;
        }

        public Task PublishAsync<TEventData>(TEventData eventData, CancellationToken cancellationToken)
        {
            return _mediator.Publish(eventData, cancellationToken);
        }
    }
}

新增一個ILocalEventHandler介面,用於處理LocalEventBus發出的內容。這裡由於MediatR的強關聯,必須繼承INotification介面。

using MediatR;

namespace Wheel.EventBus.Local
{
    public interface ILocalEventHandler<in TEventData> : IEventHandler, INotificationHandler<TEventData> where TEventData : INotification
    {
        Task Handle(TEventData eventData, CancellationToken cancellationToken = default);
    }
}

然後我們來實現一個MediatR的INotificationPublisher介面,由於預設的兩種實現方式都是會同步阻塞請求,所以我們單獨實現一個不會阻塞請求的。

using MediatR;

namespace Wheel.EventBus.Local.MediatR
{
    public class WheelPublisher : INotificationPublisher
    {
        public Task Publish(IEnumerable<NotificationHandlerExecutor> handlerExecutors, INotification notification, CancellationToken cancellationToken)
        {
            return Task.Factory.StartNew(async () =>
            {
                foreach (var handler in handlerExecutors)
                {
                    await handler.HandlerCallback(notification, cancellationToken).ConfigureAwait(false);
                }
            }, cancellationToken);
        }
    }
}

接下來新增一個擴充套件方法,用於註冊MediatR。

namespace Wheel.EventBus
{
    public static class EventBusExtensions
    {
        public static IServiceCollection AddLocalEventBus(this IServiceCollection services)
        {
            services.AddMediatR(cfg =>
            {
                cfg.RegisterServicesFromAssemblies(Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll")
                                    .Where(x => !x.Contains("Microsoft.") && !x.Contains("System."))
                                    .Select(x => Assembly.Load(AssemblyName.GetAssemblyName(x))).ToArray());
                cfg.NotificationPublisher = new WheelPublisher();
                cfg.NotificationPublisherType = typeof(WheelPublisher);
            });
            return services;
        }
    }
}

這裡透過程式集註冊,會自動註冊所有整合MediatR介面的Handler。
然後指定NotificationPublisher和NotificationPublisherType是我們自定義的Publisher。

就這樣我們完成了LocalEventBus的實現,我們只需要定義我們的EventData,同時實現一個ILocalEventHandler,即可完成一個本地事件匯流排的處理。

DistributedEventBus

這裡我們透過CAP來實現我們的分散式事件匯流排。
首先需要安裝DotNetCore.CAP的相關NUGET包。如訊息佇列使用RabbitMQ則安裝DotNetCore.CAP.RabbitMQ,使用Redis則DotNetCore.CAP.RedisStreams,資料庫儲存用Sqlite則使用DotNetCore.CAP.Sqlite。

CapDistributedEventBus

這裡CapDistributedEventBus的實現其實就是包裝以下Cap的ICapPublisher.PublishAsync方法。

using DotNetCore.CAP;
using System.Reflection;
using Wheel.DependencyInjection;

namespace Wheel.EventBus.Distributed.Cap
{
    public class CapDistributedEventBus : IDistributedEventBus, ITransientDependency
    {
        private readonly ICapPublisher _capBus;

        public CapDistributedEventBus(ICapPublisher capBus)
        {
            _capBus = capBus;
        }

        public Task PublishAsync<TEventData>(TEventData eventData, CancellationToken cancellationToken = default)
        {
            var sub = typeof(TEventData).GetCustomAttribute<EventNameAttribute>()?.Name;
            return _capBus.PublishAsync(sub ?? nameof(eventData), eventData, cancellationToken: cancellationToken);
        }
    }
}

這裡使用了一個EventNameAttribute,這個用於自定義釋出的事件名稱。

using System.Diagnostics.CodeAnalysis;

namespace Wheel.EventBus
{
    [AttributeUsage(AttributeTargets.Class)]
    public class EventNameAttribute : Attribute
    {
        public string Name { get; set; }

        public EventNameAttribute([NotNull] string name)
        {
            Name = name;
        }

        public static string? GetNameOrDefault<TEvent>()
        {
            return GetNameOrDefault(typeof(TEvent));
        }

        public static string? GetNameOrDefault([NotNull] Type eventType)
        {
            return eventType
                       .GetCustomAttributes(true)
                       .OfType<EventNameAttribute>()
                       .FirstOrDefault()
                       ?.GetName(eventType)
                   ?? eventType.FullName;
        }

        public string? GetName(Type eventType)
        {
            return Name;
        }
    }
}

新增一個IDistributedEventHandler介面,用於處理DistributedEventBus發出的內容。

namespace Wheel.EventBus.Distributed
{
    public interface IDistributedEventBus
    {
        Task PublishAsync<TEventData>(TEventData eventData, CancellationToken cancellationToken = default);
    }
}

這裡由於對CAP做了2次封裝,所以需要重寫一下ConsumerServiceSelector。

using DotNetCore.CAP;
using DotNetCore.CAP.Internal;
using System.Reflection;
using TopicAttribute = DotNetCore.CAP.Internal.TopicAttribute;

namespace Wheel.EventBus.Distributed.Cap
{
    public class WheelConsumerServiceSelector : ConsumerServiceSelector
    {
        protected IServiceProvider ServiceProvider { get; }

        /// <summary>
        /// Creates a new <see cref="T:DotNetCore.CAP.Internal.ConsumerServiceSelector" />.
        /// </summary>
        public WheelConsumerServiceSelector(IServiceProvider serviceProvider) : base(serviceProvider)
        {
            ServiceProvider = serviceProvider;
        }

        protected override IEnumerable<ConsumerExecutorDescriptor> FindConsumersFromInterfaceTypes(IServiceProvider provider)
        {
            var executorDescriptorList = base.FindConsumersFromInterfaceTypes(provider).ToList();

            using var scope = provider.CreateScope();
            var scopeProvider = scope.ServiceProvider;
            //handlers
            var handlers = scopeProvider.GetServices<IEventHandler>()
                     .Select(o => o.GetType()).ToList();

            foreach (var handler in handlers)
            {
                var interfaces = handler.GetInterfaces();
                foreach (var @interface in interfaces)
                {
                    if (!typeof(IEventHandler).GetTypeInfo().IsAssignableFrom(@interface))
                    {
                        continue;
                    }
                    var genericArgs = @interface.GetGenericArguments();

                    if (genericArgs.Length != 1)
                    {
                        continue;
                    }
                    if (!(@interface.GetGenericTypeDefinition() == typeof(IDistributedEventHandler<>)))
                    {
                        continue;
                    }

                    var descriptors = GetHandlerDescription(genericArgs[0], handler);

                    foreach (var descriptor in descriptors)
                    {
                        var count = executorDescriptorList.Count(x =>
                            x.Attribute.Name == descriptor.Attribute.Name);

                        descriptor.Attribute.Group = descriptor.Attribute.Group.Insert(
                            descriptor.Attribute.Group.LastIndexOf(".", StringComparison.Ordinal), $".{count}");

                        executorDescriptorList.Add(descriptor);
                    }
                }
            }
            return executorDescriptorList;
        }

        protected virtual IEnumerable<ConsumerExecutorDescriptor> GetHandlerDescription(Type eventType, Type typeInfo)
        {
            var serviceTypeInfo = typeof(IDistributedEventHandler<>)
                .MakeGenericType(eventType);
            var method = typeInfo
                .GetMethod(
                    nameof(IDistributedEventHandler<object>.Handle)
                );
            var eventName = EventNameAttribute.GetNameOrDefault(eventType);
            var topicAttr = method.GetCustomAttributes<TopicAttribute>(true);
            var topicAttributes = topicAttr.ToList();

            if (topicAttributes.Count == 0)
            {
                topicAttributes.Add(new CapSubscribeAttribute(eventName));
            }

            foreach (var attr in topicAttributes)
            {
                SetSubscribeAttribute(attr);

                var parameters = method.GetParameters()
                    .Select(parameter => new ParameterDescriptor
                    {
                        Name = parameter.Name,
                        ParameterType = parameter.ParameterType,
                        IsFromCap = parameter.GetCustomAttributes(typeof(FromCapAttribute)).Any()
                                    || typeof(CancellationToken).IsAssignableFrom(parameter.ParameterType)
                    }).ToList();

                yield return InitDescriptor(attr, method, typeInfo.GetTypeInfo(), serviceTypeInfo.GetTypeInfo(), parameters);
            }
        }

        private static ConsumerExecutorDescriptor InitDescriptor(
            TopicAttribute attr,
            MethodInfo methodInfo,
            TypeInfo implType,
            TypeInfo serviceTypeInfo,
            IList<ParameterDescriptor> parameters)
        {
            var descriptor = new ConsumerExecutorDescriptor
            {
                Attribute = attr,
                MethodInfo = methodInfo,
                ImplTypeInfo = implType,
                ServiceTypeInfo = serviceTypeInfo,
                Parameters = parameters
            };

            return descriptor;
        }
    }
}

WheelConsumerServiceSelector的主要作用是動態的給我們的IDistributedEventHandler打上CapSubscribeAttribute特性,使其可以正確訂閱處理CAP的訊息佇列。

接下來新增一個擴充套件方法,用於註冊CAP。

using DotNetCore.CAP.Internal;
using System.Reflection;
using Wheel.EntityFrameworkCore;
using Wheel.EventBus.Distributed.Cap;
using Wheel.EventBus.Local.MediatR;

namespace Wheel.EventBus
{
    public static class EventBusExtensions
    {
        public static IServiceCollection AddDistributedEventBus(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddSingleton<IConsumerServiceSelector, WheelConsumerServiceSelector>();
            services.AddCap(x =>
            {
                x.UseEntityFramework<WheelDbContext>();

                x.UseSqlite(configuration.GetConnectionString("Default"));

                //x.UseRabbitMQ(configuration["RabbitMQ:ConnectionString"]);
                x.UseRedis(configuration["Cache:Redis"]);
            });
            return services;
        }
    }
}

就這樣我們完成了DistributedEventBus的實現,我們只需要定義我們的EventData,同時實現一個IDistributedEventHandler,即可完成一個分散式事件匯流排的處理。

啟用EventBus

在Program中新增兩行程式碼,這樣即可完成我們本地事件匯流排和分散式事件匯流排的整合了。

builder.Services.AddLocalEventBus();
builder.Services.AddDistributedEventBus(builder.Configuration);

測試效果

新增一個TestEventData,這裡為了省事,我就公用一個EventData類

using MediatR;
using Wheel.EventBus;

namespace Wheel.TestEventBus
{
    [EventName("Test")]
    public class TestEventData : INotification
    {
        public string TestStr { get; set; }
    }
}

一個TestEventDataLocalEventHandler,這裡注意的是,實現ILocalEventHandler不需要額外繼承ITransientDependency,因為MediatR會自動註冊所有繼承INotification介面的實現。否則會出現重複執行兩次的情況。

using Wheel.DependencyInjection;
using Wheel.EventBus.Local;

namespace Wheel.TestEventBus
{
    public class TestEventDataLocalEventHandler : ILocalEventHandler<TestEventData>
    {
        private readonly ILogger<TestEventDataLocalEventHandler> _logger;

        public TestEventDataLocalEventHandler(ILogger<TestEventDataLocalEventHandler> logger)
        {
            _logger = logger;
        }

        public Task Handle(TestEventData eventData, CancellationToken cancellationToken = default)
        {
            _logger.LogWarning($"TestEventDataLocalEventHandler: {eventData.TestStr}");
            return Task.CompletedTask;
        }
    }
}

一個TestEventDataDistributedEventHandler

using Wheel.DependencyInjection;
using Wheel.EventBus.Distributed;

namespace Wheel.TestEventBus
{
    public class TestEventDataDistributedEventHandler : IDistributedEventHandler<TestEventData>, ITransientDependency
    {
        private readonly ILogger<TestEventDataDistributedEventHandler> _logger;

        public TestEventDataDistributedEventHandler(ILogger<TestEventDataDistributedEventHandler> logger)
        {
            _logger = logger;
        }

        public Task Handle(TestEventData eventData, CancellationToken cancellationToken = default)
        {
            _logger.LogWarning($"TestEventDataDistributedEventHandler: {eventData.TestStr}");
            return Task.CompletedTask;
        }
    }
}

EventHandler透過日誌列印資料。
新增一個API控制器用於測試呼叫

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Wheel.TestEventBus;

namespace Wheel.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    [AllowAnonymous]
    public class TestEventBusController : WheelControllerBase
    {
        [HttpGet("Local")]
        public async Task<IActionResult> Local()
        {
            await LocalEventBus.PublishAsync(new TestEventData { TestStr = GuidGenerator.Create().ToString() });
            return Ok();
        }

        [HttpGet("Distributed")]
        public async Task<IActionResult> Distributed()
        {
            await DistributedEventBus.PublishAsync(new TestEventData { TestStr = GuidGenerator.Create().ToString() });
            return Ok();
        }
    }
}

啟用程式,呼叫API,可以看到,都成功執行了。
image.png
image.pngimage.png
CAP的本地訊息表也可以看到正常的傳送接收。

到這我們就完成了我們EventBus的整合了。

輪子倉庫地址https://github.com/Wheel-Framework/Wheel
歡迎進群催更。

image.png

相關文章