前面基础管理的功能基本开发完了,接下来我们来优化一下开发功能,来添加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

欢迎进群催更。

造轮子之EventBus的更多相关文章

  1. 动手造轮子:实现一个简单的 EventBus

    动手造轮子:实现一个简单的 EventBus Intro EventBus 是一种事件发布订阅模式,通过 EventBus 我们可以很方便的实现解耦,将事件的发起和事件的处理的很好的分隔开来,很好的实 ...

  2. 动手造轮子:基于 Redis 实现 EventBus

    动手造轮子:基于 Redis 实现 EventBus Intro 上次我们造了一个简单的基于内存的 EventBus,但是如果要跨系统的话就不合适了,所以有了这篇基于 Redis 的 EventBus ...

  3. 动手造轮子:实现简单的 EventQueue

    动手造轮子:实现简单的 EventQueue Intro 最近项目里有遇到一些并发的问题,想实现一个队列来将并发的请求一个一个串行处理,可以理解为使用消息队列处理并发问题,之前实现过一个简单的 Eve ...

  4. 动手造轮子:实现一个简单的 AOP 框架

    动手造轮子:实现一个简单的 AOP 框架 Intro 最近实现了一个 AOP 框架 -- FluentAspects,API 基本稳定了,写篇文章分享一下这个 AOP 框架的设计. 整体设计 概览 I ...

  5. 避免重复造轮子的UI自动化测试框架开发

    一懒起来就好久没更新文章了,其实懒也还是因为忙,今年上半年的加班赶上了去年一年的加班,加班不息啊,好了吐槽完就写写一直打算继续的自动化开发 目前各种UI测试框架层出不穷,但是万变不离其宗,驱动PC浏览 ...

  6. 【疯狂造轮子-iOS】JSON转Model系列之二

    [疯狂造轮子-iOS]JSON转Model系列之二 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇<[疯狂造轮子-iOS]JSON转Model系列之一> ...

  7. 【疯狂造轮子-iOS】JSON转Model系列之一

    [疯狂造轮子-iOS]JSON转Model系列之一 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 之前一直看别人的源码,虽然对自己提升比较大,但毕竟不是自己写的,很容易遗 ...

  8. h5engine造轮子

    基于学习的造轮子,这是一个最简单,最基础的一个canvas渲染引擎,通过这个引擎架构,可以很快的学习canvas渲染模式! 地址:https://github.com/RichLiu1023/h5en ...

  9. 我为什么还要造轮子?欠踹?Monk.UI表单美化插件诞生记!

    背景 目前市场上有很多表单美化的UI,做的都挺不错,但是他们都有一个共同点,那就是90%以上都是前端工程师开发的,导致我们引入这些UI的时候,很难和程序绑定.所以作为程序员的我,下了一个决定!我要自己 ...

  10. 「iOS造轮子」之UIButton 用Block响应事件

    俗语说 一个不懒的程序员不是好程序员 造轮子,也只是为了以后更好的coding. coding,简易明了的代码更是所有程序员都希望看到的 无论是看自己的代码,还是接手别人的代码 都希望一看都知道这代码 ...

随机推荐

  1. 基于OLAP技术的企业级大数据分析平台的国际化发展与合作

    目录 标题:<基于OLAP技术的企业级大数据分析平台的国际化发展与合作> 背景介绍 随着全球化的不断推进,企业对大数据分析的需求日益增长.企业通过数据分析来发现隐藏在业务数据中的机会,从而 ...

  2. spingmvc配置AOP 之 非注解方式

    spingmvc配置AOP有两种方式,一种是利用注解的方式配置,另一种是XML配置实现. 应用注解的方式配置: 先在maven中引入AOP用到的依赖 <dependency> <gr ...

  3. 多线程知识:三个线程如何交替打印ABC循环100次

    本文博主给大家讲解一道网上非常经典的多线程面试题目.关于三个线程如何交替打印ABC循环100次的问题. 下文实现代码都基于Java代码在单个JVM内实现. 问题描述 给定三个线程,分别命名为A.B.C ...

  4. 字符串加密DES

    提前关于加密的方式,我目前知道的有MD5,DES等等.今天写一下使用DES的代码,方便下次使用. package mocha.framework.hrbase.rest.utils; import j ...

  5. 使用 Dockerfile 构建生产环境镜像

    传统部署的坑: 1202 年了,如果你连 Docker 都不知道是什么,我建议买一本书看看--或者谷歌一下,博客已经写烂了. 为什么有这篇文章,是因为我在真正做容器化改造的时候,发现公司生产环境存在大 ...

  6. Avalonia 列表拖拽替换

    实现目标,在一个ListBox中选择一个子项进行拖拽到另一个ListBox中,拖拽到某一子项区域进行替换 下面是axaml代码 1 <ListBox 2 Name="consumabl ...

  7. Hi3798MV200 恩兔N2 NS-1 (三): 制作 Ubuntu rootfs

    目录 Hi3798MV200 恩兔N2 NS-1 (一): 设备介绍和刷机说明 Hi3798MV200 恩兔N2 NS-1 (二): HiNAS海纳思使用和修改 Hi3798MV200 恩兔N2 NS ...

  8. 【Python】@property用法简述

    参考自:Python的@property是干嘛的?作者:Python测试开发 如果我们设置类的属性私有化,那么可以使用@property 使属性可以被外部访问并修改. 在使用 @property 之前 ...

  9. 从redis未授权访问到获取服务器权限

    从redis未授权访问到获取服务器权限 好久没写博客了,博客园快荒芜了.赶紧再写一篇,算是一个关于自己学习的简要的记录把. 这里是关于redis未授权访问漏洞的一篇漏洞利用: 首先是redis,靶场搭 ...

  10. 【译】基于XAML的跨平台框架对比分析

    多年来,基于XAML的UI框架已经有了很大的发展.下面的图表是最好的说明.这些框架主要包含:支持跨平台应用的Avalonia UI, Uno Platform和 .NET MAUI.事实上,除了Ava ...