.Net Core 基于CAP框架的事件总线
.Net Core 基于CAP框架的事件总线
CAP 是一个在分布式系统中(SOA,MicroService)实现事件总线及最终一致性(分布式事务)的一个开源的 C# 库,她具有轻量级,高性能,易使用等特点。
github:https://github.com/dotnetcore/CAP
CAP是一款优秀的框架,但是CAP在消息订阅的处理类必须使用[CapSubscribe]特性来绑定,本人觉得不是很科学.
好在2.5.1版本开放了接口IConsumerServiceSelector为public,提供了扩展的基础,以下将基于CAP,扩展成简洁的事件总线组件.
使用:
/// <summary> /// 修改用户名事件 /// </summary> public class UserNameUpdateEvent : IEvent { public int Id { get; set; } public string OldName { get; set; } public string NewName { get; set; } } /// <summary> /// 发布事件,你可以在任意地方注入IEventPublisher,进行事件发布 /// </summary> public class PublishEvent : Guc.Kernel.Dependency.ITransient { public PublishEvent(IEventPublisher eventPublisher) { EventPublisher = eventPublisher; } IEventPublisher EventPublisher { get; } public void Publish() { EventPublisher.Publish(new UserNameUpdateEvent { Id = 1, OldName = "老王1", NewName = "老王2" }); } } /// <summary> /// 事件处理,你可以注入任何需要的类型 /// </summary> public class UserNameUpdateEventHandler : IEventHandler<UserNameUpdateEvent> { public UserNameUpdateEventHandler(IUserStore userStore, ILogger<UserNameUpdateEventHandler> logger) { UserStore = userStore; Logger = logger; } IUserStore UserStore { get; } ILogger<UserNameUpdateEventHandler> Logger { get; } /// <summary> /// 执行事件处理 /// </summary> /// <param name="event">事件对象</param> /// <returns></returns> public async Task Execute(UserNameUpdateEvent @event) { Logger.LogInformation($"修改用户名:{@event.OldName}->{@event.NewName}"); await Task.CompletedTask; UserStore.Update(new UserModel { Id = @event.Id, Name = @event.NewName }); } }
约定事件类型的FullName为事件名称,如例中的:UserNameUpdateEvent的类型FullName:Guc.Sample.UserNameUpdateEvent;
约定事件处理类型的FullName为GroupName,如例中的:UserNameUpdateEventHandler 的类型FullName:Guc.Sample.UserNameUpdateEventHandler
如果使用的RabbitMQ作为消息的传输,则Guc.Sample.UserNameUpdateEvent为路由的Key,Guc.Sample.UserNameUpdateEventHandler 为队列名称.
使用:
services.AddGucKernel() .AddEventBus(capOptions => { // CAP相关的配置 });
扩展代码,github:https://github.com/280780363/guc/blob/master/src/Guc.EventBus/GucConsumerServiceSelector.cs:
using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using Microsoft.Extensions.DependencyInjection; using System.Collections.Concurrent; using DotNetCore.CAP; using Guc.Kernel.Utils; using Microsoft.Extensions.Options; namespace Guc.EventBus { class GucConsumerServiceSelector : IConsumerServiceSelector { private readonly CapOptions _capOptions; private readonly IServiceProvider _serviceProvider; /// <summary> /// since this class be designed as a Singleton service,the following two list must be thread safe!!! /// </summary> private readonly ConcurrentDictionary<string, List<RegexExecuteDescriptor<ConsumerExecutorDescriptor>>> _asteriskList; private readonly ConcurrentDictionary<string, List<RegexExecuteDescriptor<ConsumerExecutorDescriptor>>> _poundList; /// <summary> /// Creates a new <see cref="DefaultConsumerServiceSelector" />. /// </summary> public GucConsumerServiceSelector(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; _capOptions = serviceProvider.GetService<IOptions<CapOptions>>().Value; _asteriskList = new ConcurrentDictionary<string, List<RegexExecuteDescriptor<ConsumerExecutorDescriptor>>>(); _poundList = new ConcurrentDictionary<string, List<RegexExecuteDescriptor<ConsumerExecutorDescriptor>>>(); } public IReadOnlyList<ConsumerExecutorDescriptor> SelectCandidates() { var executorDescriptorList = new List<ConsumerExecutorDescriptor>(); executorDescriptorList.AddRange(FindConsumersFromInterfaceTypes(_serviceProvider)); return executorDescriptorList; } public ConsumerExecutorDescriptor SelectBestCandidate(string key, IReadOnlyList<ConsumerExecutorDescriptor> executeDescriptor) { var result = MatchUsingName(key, executeDescriptor); if (result != null) { return result; } //[*] match with regex, i.e. foo.*.abc result = MatchAsteriskUsingRegex(key, executeDescriptor); if (result != null) { return result; } //[#] match regex, i.e. foo.# result = MatchPoundUsingRegex(key, executeDescriptor); return result; } private IEnumerable<ConsumerExecutorDescriptor> FindConsumersFromInterfaceTypes( IServiceProvider provider) { var executorDescriptorList = new List<ConsumerExecutorDescriptor>(); using (var scoped = provider.CreateScope()) { var scopedProvider = scoped.ServiceProvider; var consumerServices = scopedProvider.GetServices<ICapSubscribe>(); foreach (var service in consumerServices) { var typeInfo = service.GetType().GetTypeInfo(); // 必须是非抽象类 if (!typeInfo.IsClass || typeInfo.IsAbstract) continue; // 继承自IEventHandler<> if (!typeInfo.IsChildTypeOfGenericType(typeof(IEventHandler<>))) continue; executorDescriptorList.AddRange(GetTopicAttributesDescription(typeInfo)); } return executorDescriptorList; } } private List<string> GetEventNamesFromTypeInfo(TypeInfo typeInfo) { List<string> names = new List<string>(); foreach (var item in typeInfo.ImplementedInterfaces) { var @interface = item.GetTypeInfo(); if (!@interface.IsGenericType) continue; if (@interface.GenericTypeArguments.Length != 1) continue; var eventType = @interface.GenericTypeArguments[0].GetTypeInfo(); if (!eventType.IsChildTypeOf<IEvent>()) continue; names.Add(eventType.FullName); } return names; } private IEnumerable<ConsumerExecutorDescriptor> GetTopicAttributesDescription(TypeInfo typeInfo) { var names = GetEventNamesFromTypeInfo(typeInfo); if (names.IsNullOrEmpty()) return new ConsumerExecutorDescriptor[] { }; List<ConsumerExecutorDescriptor> results = new List<ConsumerExecutorDescriptor>(); var methods = typeInfo.GetMethods(); foreach (var eventName in names) { var method = methods.FirstOrDefault(r => r.Name == "Execute" && r.GetParameters().Length == 1 && r.GetParameters()[0].ParameterType.FullName == eventName && r.GetParameters()[0].ParameterType.IsChildTypeOf<IEvent>()); if (method == null) continue; results.Add(new ConsumerExecutorDescriptor { Attribute = new CapSubscribeAttribute(eventName) { Group = typeInfo.FullName + "." + _capOptions.Version }, ImplTypeInfo = typeInfo, MethodInfo = method }); } return results; } private ConsumerExecutorDescriptor MatchUsingName(string key, IReadOnlyList<ConsumerExecutorDescriptor> executeDescriptor) { return executeDescriptor.FirstOrDefault(x => x.Attribute.Name == key); } private ConsumerExecutorDescriptor MatchAsteriskUsingRegex(string key, IReadOnlyList<ConsumerExecutorDescriptor> executeDescriptor) { var group = executeDescriptor.First().Attribute.Group; if (!_asteriskList.TryGetValue(group, out var tmpList)) { tmpList = executeDescriptor.Where(x => x.Attribute.Name.IndexOf('*') >= 0) .Select(x => new RegexExecuteDescriptor<ConsumerExecutorDescriptor> { Name = ("^" + x.Attribute.Name + "$").Replace("*", "[0-9_a-zA-Z]+").Replace(".", "\\."), Descriptor = x }).ToList(); _asteriskList.TryAdd(group, tmpList); } foreach (var red in tmpList) { if (Regex.IsMatch(key, red.Name, RegexOptions.Singleline)) { return red.Descriptor; } } return null; } private ConsumerExecutorDescriptor MatchPoundUsingRegex(string key, IReadOnlyList<ConsumerExecutorDescriptor> executeDescriptor) { var group = executeDescriptor.First().Attribute.Group; if (!_poundList.TryGetValue(group, out var tmpList)) { tmpList = executeDescriptor .Where(x => x.Attribute.Name.IndexOf('#') >= 0) .Select(x => new RegexExecuteDescriptor<ConsumerExecutorDescriptor> { Name = ("^" + x.Attribute.Name + "$").Replace("#", "[0-9_a-zA-Z\\.]+"), Descriptor = x }).ToList(); _poundList.TryAdd(group, tmpList); } foreach (var red in tmpList) { if (Regex.IsMatch(key, red.Name, RegexOptions.Singleline)) { return red.Descriptor; } } return null; } private class RegexExecuteDescriptor<T> { public string Name { get; set; } public T Descriptor { get; set; } } } }