前面基礎管理的功能基本開發完了,接下來我們來最佳化一下開發功能,來新增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,可以看到,都成功執行了。
CAP的本地訊息表也可以看到正常的傳送接收。
到這我們就完成了我們EventBus的整合了。
輪子倉庫地址https://github.com/Wheel-Framework/Wheel
歡迎進群催更。