前面基礎管理的功能基本開發完了,接下來我們來優化一下開發功能,來添加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
歡迎進羣催更。