一、事件总线设计方案

1.1、事件总线的概念

  • 事件总线是一个事件管理器,负责统一处理系统中所有事件的发布和订阅。
  • 事件总线模式通过提供一种松耦合的方式来促进系统内部的业务模块之间的通信,从而增强系统的灵活性和可维护性。

1.2、实现的功能目标

  • 注入事件总线服务到DI容器,自动注入整个程序集的事件;
  • 每个事件处理程序能够自动依赖注入;
  • 通过特性标注事件消息模型、事件处理器;
  • 事件总线服务提供一个发布事件的方法,根据消息模型,自动找到并触发对应的事件处理程序,并传递事件参数。

二、使用案例

2.1、事件消息模型

  • 需要继承 EventArgs
public class UserTestEventArgs : EventArgs
{
public string UserId { get; set; }
public string UserName { get; set; }
}

2.2、事件处理程序

  • 该事件模型,触发的事件处理程序,会自动依赖注入
[LocalEventHandler(typeof(UserTestEventArgs), 1)]
public class UserTest1EventHandler(SingletonTestService singletonService, ScopeTestService scopeService, TransientTestService transientService) : ILocalEventHandler<UserTestEventArgs>
{
public Task OnEventHandlerAsync(object sender, UserTestEventArgs e)
{
Console.WriteLine($"事件1被'{sender.GetType().Name}'触发,参数:" + JsonUtils.ToJson(e));
try
{
singletonService.Test();
scopeService.Test();
transientService.Test();
}
catch (Exception ex)
{
}
return Task.CompletedTask;
}
}

2.3、注入事件总线服务到DI

  • 在Startup.cs 或 Program.cs 中,注入服务
builder.Services.AddLocalEventBus(typeof(UserTest1EventHandler).Assembly); // 注入事件总线服务,自动注册这个程序集内的所有事件处理器。

2.4、使用事件总线服务,触发事件

  • 通过构造函数依赖注入,拿到事件总线服务 ILocalEventBus
  • 调用事件总线服务,发布事件消息,触发事件处理程序 eventBus.PublishAsync(this, args);
// 事件总线测试控制器
public class EventTestController(
ILocalEventBus eventBus, // 主构造函数,依赖注入事件总线服务
IUserService userService // 测试服务
) : ControllerBase
{
[HttpPost]
public Task Test(UserTestEventArgs args) // UserTestEventArgs 事件消息模型
{
var users = userService.GetUsers();
return eventBus.PublishAsync(this, args); // 发布事件消息,触发事件处理程序。 this:触发事件的对象 args:事件消息
}
}

三、事件总线功能开发

3.1、本地事件总线 服务接口

  • 事件的发布方法设计,基于 .Net 标准事件模式 思想。
  • 这里需要泛型参数:事件消息模型类型,以便在触发事件时可以找到注册的该消息模型对应的事件处理器。
/// <summary>
/// 本地事件总线
/// </summary>
public interface ILocalEventBus
{
/// <summary>
/// 触发对应 事件消息模型 对应的 事件处理程序
/// </summary>
/// <typeparam name="TEventArgs">事件消息模型类型</typeparam>
/// <param name="sender">触发事件的对象</param>
/// <param name="args">事件消息模型</param>
Task PublishAsync<TEventArgs>(object sender, TEventArgs args) where TEventArgs : EventArgs;
}

3.2、事件处理器 泛型接口

/// <summary>
/// 本地事件 事件处理程序接口
/// </summary>
/// <typeparam name="TEventArgs">事件消息模型</typeparam>
public interface ILocalEventHandler<TEventArgs> where TEventArgs : EventArgs
{
/// <summary>
/// 事件处理程序方法
/// </summary>
/// <param name="sender">事件触发者</param>
/// <param name="e">事件消息</param>
Task OnEventHandlerAsync(object sender, TEventArgs e);
}

3.3、本地事件处理程序 特性

  • 本地事件处理程序 特性 :用于在事件处理器上标注。
  • 【MessageType】 指定该消息处理器,接受的消息模型类型。 在事件触发时,通过消息类型,找到该消息处理器,并调用。
  • 【Sort】如果多个消息处理器,声明接受同一个类型的消息模型。那么当这个类型的消息发布时,会触发这些多个事件处理程序,会通过指定的该触发顺序挨个触发。其中一个事件处理器执行报错,不会影响其他的。
/// <summary>
/// 本地事件处理程序 特性
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class LocalEventHandlerAttribute : Attribute
{
/// <summary>
/// 事件消息模型类,需要继承EventArgs
/// </summary>
public Type MessageType { get; set; }
/// <summary>
/// 触发顺序 正序
/// </summary>
public int Sort { get; set; } /// <summary>
/// 构造函数
/// </summary>
/// <param name="messageType">事件消息类型</param>
/// <param name="sort">触发顺序 正序</param>
public LocalEventHandlerAttribute(Type messageType, int sort = 0)
{
if(!messageType.IsSubclassOf(typeof(EventArgs)))
{
throw new Exception($"【LocalEventBus】The MessageType '{messageType.Name}' can not assignable from '{nameof(EventArgs)}'");
}
MessageType = messageType;
Sort = sort;
}
}

3.4、事件处理程序信息

/// <summary>
/// 事件处理程序模型
/// </summary>
public class LocalEventHandlerModel
{
/// <summary>
/// 触发顺序
/// </summary>
public int Sort { get; set; }
/// <summary>
/// 事件处理程序类型
/// </summary>
public Type HandlerType { get; set; }
}

3.5、事件总线服务

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Concurrent; namespace Singer.Framework.LocalEventBus; /// <summary>
/// 本地事件总线
/// </summary>
public sealed class LocalEventBus(IHttpContextAccessor httpContextAccessor) : ILocalEventBus
{
/// <summary>
/// 事件消息类型 - 对应的事件处理程序的类型集合
/// </summary>
private ConcurrentDictionary<Type, ConcurrentBag<LocalEventHandlerModel>> Events = new ConcurrentDictionary<Type, ConcurrentBag<LocalEventHandlerModel>>(); /// <summary>
/// 触发对应 事件消息模型 对应的 事件处理程序
/// </summary>
/// <typeparam name="TEventArgs">事件消息模型类型</typeparam>
/// <param name="sender">触发事件的对象</param>
/// <param name="args">事件消息模型</param>
public async Task PublishAsync<TEventArgs>(object sender, TEventArgs args)
where TEventArgs : EventArgs
{
var argType = typeof(TEventArgs);
if (!Events.TryGetValue(argType, out ConcurrentBag<LocalEventHandlerModel>? handlers))
return;
if (handlers == null || handlers.Count == 0)
return;
foreach (var handlerModel in handlers.OrderBy(x => x.Sort))
{
try
{
// 在此时 通过 DI 和事件处理程序类型 获取到 事件处理程序实例
var handlerInstance = httpContextAccessor?.HttpContext?.RequestServices?.GetService(handlerModel.HandlerType);
if (handlerInstance != null)
{
var method = handlerModel.HandlerType.GetMethod("OnEventHandlerAsync");
Task? task = (Task?)method?.Invoke(handlerInstance, [sender, args]);
if (task != null)
await task;
}
}
catch (Exception)
{
}
}
} /// <summary>
/// 向事件总线中添加多个事件处理程序
/// </summary>
/// <param name="handlerDic">事件消息类型 - 对应的事件处理程序模型列表 字典</param>
public void AddHandlers(Dictionary<Type, List<LocalEventHandlerModel>> handlerDic)
{
if (handlerDic == null || handlerDic.Count == 0)
return;
foreach (var item in handlerDic)
{
if (Events.TryGetValue(item.Key, out ConcurrentBag<LocalEventHandlerModel> handlerModels))
{
foreach (var value in item.Value)
{
handlerModels.Add(value);
}
}
else
{
Events.TryAdd(item.Key, new ConcurrentBag<LocalEventHandlerModel>(item.Value));
}
}
} }

3.6、事件总线服务 依赖注入处理

/// <summary>
/// 本地事件总线 服务拓展
/// </summary>
public static class LocalEventBusServiceExtensions
{
/// <summary>
/// 注册事件总线
/// 将整个程序集中带有EventHandler特性的事件处理程序类都注册到事件总线中
/// <param name="handlerAssembly">事件处理程序所在的程序集</param>
/// </summary>
public static void AddLocalEventBus(this IServiceCollection services, Assembly handlerAssembly)
{
var handlerTypes = handlerAssembly.GetTypes().Where(x => x.GetCustomAttribute<LocalEventHandlerAttribute>() != null);
Dictionary<Type, List<LocalEventHandlerModel>> handlerDic = new();
foreach (Type handlerType in handlerTypes)
{
services.Replace(new ServiceDescriptor(handlerType, handlerType, ServiceLifetime.Scoped)); // 将事件处理程序注入到容器中,这样事件处理程序也可以像普通服务一样使用依赖注入
var attribute = handlerType.GetCustomAttribute<LocalEventHandlerAttribute>();
if (attribute == null)
continue; var handlerModel = new LocalEventHandlerModel() { Sort = attribute.Sort, HandlerType = handlerType };
if (handlerDic.ContainsKey(attribute.MessageType))
{
handlerDic[attribute.MessageType].Add(handlerModel);
}
else
{
handlerDic.Add(attribute.MessageType, new List<LocalEventHandlerModel>() { handlerModel });
}
}
LocalEventBus? eventBus = services.BuildServiceProvider().GetService<ILocalEventBus>() as LocalEventBus; // 此方法可能多次调用,单例处理 services.AddHttpContextAccessor(); // 依赖Http上下文,通过HttpContext拿到每次事件触发时的 服务作用域
services.AddSingleton<ILocalEventBus, LocalEventBus>(sp => // 将事件总线 注册为 单例服务
{
IHttpContextAccessor httpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>();
eventBus = eventBus ?? new LocalEventBus(httpContextAccessor);
eventBus.AddHandlers(handlerDic); // 将解析出来的事件处理程序添加到事件总线中
return eventBus;
});
}
}

.Net Web项目中,实现轻量级本地事件总线 框架的更多相关文章

  1. [Abp vNext 源码分析] - 13. 本地事件总线与分布式事件总线 (Rabbit MQ)

    一.简要介绍 ABP vNext 封装了两种事件总线结构,第一种是 ABP vNext 自己实现的本地事件总线,这种事件总线无法跨项目发布和订阅.第二种则是分布式事件总线,ABP vNext 自己封装 ...

  2. 转 web项目中的web.xml元素解析

    转 web项目中的web.xml元素解析 发表于1年前(2014-11-26 15:45)   阅读(497) | 评论(0) 16人收藏此文章, 我要收藏 赞0 上海源创会5月15日与你相约[玫瑰里 ...

  3. web项目中获取spring的bean对象

    Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架,如何在程序中不通过注解的形式(@Resource.@Autowired)获取Spring配置的bean呢? Bean工厂(c ...

  4. 在基于MVC的Web项目中使用Web API和直接连接两种方式混合式接入

    在我之前介绍的混合式开发框架中,其界面是基于Winform的实现方式,后台使用Web API.WCF服务以及直接连接数据库的几种方式混合式接入,在Web项目中我们也可以采用这种方式实现混合式的接入方式 ...

  5. (转)关于java和web项目中的相对路径问题

    原文:http://blog.csdn.net/yethyeth/article/details/1623283 关于java和web项目中的相对路径问题 分类: java 2007-05-23 22 ...

  6. linux 下用renameTo方法修改java web项目中文件夹名称问题

    经测试,在Linux环境中安装tomcat,然后启动其中的项目,在项目中使用java.io.File.renameTo(File dest)方法可行. 之前在本地运行代码可以修改,然后传到Linux服 ...

  7. 在java web项目中实现随项目启动的额外操作

    前言 在web项目中经常会遇到在项目启动初始,会要求做一些逻辑的实现,比如实现一个消息推送服务,实现不同类型数据同步的回调操作初始化,或则通知其他客户服务器本项目即将启动,等等.对于这种要求,目前个人 ...

  8. asp.net Web项目中使用Log4Net进行错误日志记录

      使用log4net可以很方便地为应用添加日志功能.应用Log4net,开发者可以很精确地控制日志信息的输出,减少了多余信息,提高了日志记录性能.同时,通过外部配置文件,用户可以不用重新编译程序就能 ...

  9. Java Web项目中使用Freemarker生成Word文档

    Web项目中生成Word文档的操作屡见不鲜.基于Java的解决方式也是非常多的,包含使用Jacob.Apache POI.Java2Word.iText等各种方式,事实上在从Office 2003開始 ...

  10. web项目中nicedit富文本编辑器的使用

    web项目中nicedit富文本编辑器的使用 一.为什么要用富文本编辑器? 先说什么是富文本编辑器吧,普通的html中input或textarea标签只能进行简单的输入,而做不到其他的文本调整功能,甚 ...

随机推荐

  1. 写写stream流的终结操作

    终结操作和中间操作的区别:中间操作返回的一直都是stream,所以可以一直使用,但是终结操作返回的不是stream,后面不能继续操作 foreach:对流中的所有元素进行遍历操作 count:获取当前 ...

  2. NameCheap域名怎么样,如何注册购买域名?如何解析域名?

    Namecheap介绍 Namecheap是一家国外域名注册商和网站托管公司,成立于2000年,提供域名注册.虚拟主机.电子邮件托管.SSL证书.免费的WHOIS保护.CDN.VPS主机和独立服务器. ...

  3. 浅谈Git架构和如何避免代码覆盖的事故

    浅谈Git架构和如何避免代码覆盖的事故 Git 不同于 SVN 的地方在于, Git 是分布式的版本管理系统, 所有的客户端和服务器都保存了一份代码, 涉及到仓库仓之间的同步, 所以处理不当极易造成冲 ...

  4. .NET 结果与错误处理利器 FluentResults

    前言 在项目开发中,方法返回的结果(成功或失败)对我们开发来说很重要.传统方法,如通过异常来指示错误或使用特定的返回类型(如布尔值加输出参数),虽然有效,但可能缺乏直观性和灵活性. FluentRes ...

  5. Parallel and Sequential Data Structures and Algorithms

    并串行 从零开始考前突击并串行数据结构与算法 强烈建议和原教材参照着看 Introduction 本书的要点 定义问题 不同的算法解决 设计抽象数据类型和相应的数据结构实现 分析比较算法和数据类型的代 ...

  6. 在vscode中通过修改launch.json文件为项目添加启动参数——在launch.json文件中修改args变量

    以前一直在使用pycharm,不管怎么说毕竟国内外的Python编程者大部分都更支持pycharm,并且认为pycharm是Python语言编程中最好用的编辑器,但是随着国内编程人员一茬一茬的兴起很多 ...

  7. 读论文《Reinforced Attention for Few-Shot Learning and Beyond》

    2022年4月22日,实验室开组会,我讲了论文<Reinforced Attention for Few-Shot Learning and Beyond>,最近整理资料又再读了一遍,这里 ...

  8. 解密prompt系列35. 标准化Prompt进行时! DSPy论文串烧和代码示例

    一晃24年已经过了一半,我们来重新看下大模型应用中最脆弱的一环Prompt Engineering有了哪些新的解决方案.这一章我们先看看大火的DSPy框架,会先梳理DSPy相关的几篇核心论文了解下框架 ...

  9. 小程序报错 .wxss 无法找到

    转子:http://blog.csdn.net/u012329294/article/details/78610593

  10. 首次尝试SeaTunnel同步Doris至Hive?这些坑你不能不避

    笔者使用SeaTunnel 2.3.2版本将Doris数据同步到Hive(cdh-6.3.2)首次运行时有如下报错,并附上报错的解决方案: java.lang.NoClassDefFoundError ...