Asp.net Core 系列之--1.事件驱动初探:简单事件总线实现(SimpleEventBus)
ChuanGoing 2019-08-06
前言
开篇之前,简单说明下随笔原因。在园子里游荡了好久,期间也起过要写一些关于.NET的随笔,因各种原因未能付诸实现。
前段时间拜读daxnet的系列文章,感受颇深,犹豫好久,终于决定开始记录本人的学习点滴。
系列说明
本系列目的是构建一套基于领域驱动设计(DDD)的基础架构,渐进式实现CQRS/消息事件驱动型业务基础框架,中间会夹杂着其他的中间件的学习介绍,仅供学习交流用(.NET CORE/Standard 2.0)。
因为接触领域驱动设计时间不长,现实上述目标可能会比较曲折,有不规范的地方望读者指正。
构建开始前,简单介绍下本篇的学习曲线:
1.引入Ioc/DI
2.简单型事件驱动总线(EventBus)实现(事件定义/订阅及派发,事件处理器等)
注:篇尾我会附上Github源码地址(开发工具是VS2017/19,.NET CORE 2.2)
Asp.net Core 自带的Ioc容器用起来不大方便,本系列引入Autofac作为Ioc/DI容器,先简单介绍下几个常规用法
首先创建一个Asp.net core web api应用程序,新建一个.Net Standard项目Base.Ioc用于管理Ioc/DI操作,并添加下图的Nuget依赖
添加扩展类AutofacExtensions,添加如下方法(这里引入了AspectCore动态代理后续实现Aop会用到)
public static IServiceProvider UseAutofac<TModule>(this IServiceCollection services) where TModule : Module, new() { ContainerBuilder builder = new ContainerBuilder(); builder.Populate(services); builder.RegisterModule<TModule>();
//引入AspectCore.Extensions.Autofac builder.RegisterDynamicProxy(); IContainer container = builder.Build(); return new AutofacServiceProvider(container); }
在Startup.cs文件的ConfigureServices中替换Asp.net Core自带Ioc容器:
// This method gets called by the runtime. Use this method to add services to the container.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
//替换Ioc容器,并扩展Autofac模块注册
return services.UseAutofac<WebModule>();
}
上面替换Ioc容器的时候,引入了Autofac的模块自动注入功能
public class WebModule : Module { protected override void Load(ContainerBuilder builder) { //builder.RegisterType<AutoFacManager>(); //builder.RegisterType<Worker>().As<IPerson>();
//扫描程序集自动注册
builder.RegisterAssembly(ThisAssembly);
}
}
在Base.Ioc项目中添加AutofacInjectionExtensions.cs文件
/// <summary> /// Autofac自动注入 /// </summary> /// <param name="builder"></param> /// <param name="assembly"></param> public static void RegisterAssembly(this ContainerBuilder builder, Assembly assembly) { foreach (var type in assembly.ExportedTypes) { if (type.IsPublic && !type.IsAbstract && type.IsClass) { var interfaces = type.GetInterfaces(); IList<Type> transientList = new List<Type>(); IList<Type> scopeList = new List<Type>(); IList<Type> singletonList = new List<Type>(); foreach (var intrType in interfaces) { if (intrType.IsGenericType) { if (intrType.IsAssignableTo<IDependencyInstance>()) { transientList.Add(intrType); } else if (intrType.IsAssignableTo<IScopeInstance>()) { scopeList.Add(intrType); } else if (intrType.IsAssignableTo<ISingletonInstance>()) { singletonList.Add(intrType); } } else { if (intrType.IsAssignableTo<IDependencyInstance>()) { transientList.Add(intrType); } else if (intrType.IsAssignableTo<IScopeInstance>()) { scopeList.Add(intrType); } else if (intrType.IsAssignableTo<ISingletonInstance>()) { singletonList.Add(intrType); } } } if (type.IsGenericType) { if (transientList.Count > 0) { builder.RegisterGeneric(type).As(transientList.ToArray()).InstancePerDependency(); } if (scopeList.Count > 0) { builder.RegisterGeneric(type).As(scopeList.ToArray()).InstancePerLifetimeScope(); } if (singletonList.Count > 0) { builder.RegisterGeneric(type).As(singletonList.ToArray()).SingleInstance(); } //泛型 if (type.IsAssignableTo<IDependencyInstance>()) { builder.RegisterGeneric(type).AsSelf().InstancePerDependency(); } else if (type.IsAssignableTo<IScopeInstance>()) { builder.RegisterGeneric(type).AsSelf().InstancePerLifetimeScope(); } else if (type.IsAssignableTo<ISingletonInstance>()) { builder.RegisterGeneric(type).AsSelf().SingleInstance(); } } else { if (transientList.Count > 0) { builder.RegisterType(type).As(transientList.ToArray()).InstancePerDependency(); } if (scopeList.Count > 0) { builder.RegisterType(type).As(scopeList.ToArray()).InstancePerLifetimeScope(); } if (singletonList.Count > 0) { builder.RegisterType(type).As(singletonList.ToArray()).SingleInstance(); } // if (type.IsAssignableTo<IDependencyInstance>()) { builder.RegisterType(type).AsSelf().InstancePerDependency(); } else if (type.IsAssignableTo<IScopeInstance>()) { builder.RegisterType(type).AsSelf().InstancePerLifetimeScope(); } else if (type.IsAssignableTo<ISingletonInstance>()) { builder.RegisterType(type).AsSelf().SingleInstance(); } } } } }
上面一段代码用到了IDependencyInstance/IScopeInstance/ISingletonInstance三个接口,分别用于瞬时/Scope/单例的服务标识。
大概说明下这段代码的作用:通过扫描传入的程序集获取外部可见的Public类型的Type(这里我们指的是类),扫描该类的所有继承了服务标识接口,并注册为对应的服务
至此,Ioc容器已替换为Autofac
EventBus
事件总线实现发布/订阅功能,首先定义IEvent/IEventHandler,IEventHandler 定义了事件处理方法
public interface IEvent { Guid Id { get; } /// <summary> /// 时间戳 /// </summary> long Timestamp { get; } }
public interface IEventHandler { /// <summary> /// 处理事件 /// </summary> /// <param name="event"></param> /// <param name="cancellationToken"></param> /// <returns></returns> Task<bool> HandleAsync(IEvent @event, CancellationToken cancellationToken = default(CancellationToken));
/// <summary>
/// 可否处理
/// </summary>
/// <param name="event"></param>
/// <returns></returns>
bool CanHandle(IEvent @event);
}
接着定义发布/订阅及事件总线接口
public interface IEventPublisher { /// <summary> /// 发布事件 /// </summary> /// <typeparam name="TEvent"></typeparam> /// <param name="event"></param> /// <param name="cancellationToken"></param> /// <returns></returns> Task PublishAsync<TEvent>(TEvent @event, CancellationToken cancellationToken = default(CancellationToken)) where TEvent : IEvent; }
public interface IEventSubscriber { /// <summary> /// 事件订阅 /// </summary> void Subscribe(); }
public interface IEventBus : IEventSubscriber, IEventPublisher { }
EventBus实现发布/订阅器,并在事件发布的同时通知相应的事件处理器进行相关处理。
这里引入了"消息队列"的概念(当然目前我们只是用来模拟消息队列,后续会引入RabbitMQ来实现)
相关代码如下:
internal sealed class EventQueue { public event EventHandler<EventProcessedEventArgs> EventPushed; public EventQueue() { } public void Push(IEvent @event) { OnMessagePushed(new EventProcessedEventArgs(@event)); } private void OnMessagePushed(EventProcessedEventArgs e) { this.EventPushed?.Invoke(this, e); } }
/// <summary> /// 消息事件参数 /// </summary> public class EventProcessedEventArgs : EventArgs { public IEvent Event { get; } public EventProcessedEventArgs(IEvent @event) { Event = @event; } }
public class EventBus : IEventBus { private readonly EventQueue eventQueue = new EventQueue(); private readonly IEnumerable<IEventHandler> eventHandlers; public EventBus(IEnumerable<IEventHandler> eventHandlers) { this.eventHandlers = eventHandlers; } /// <summary> /// 发布事件到队列时触发处理事件 /// </summary> /// <param name="sendere"></param> /// <param name="e"></param> private void EventQueue_EventPushed(object sendere, EventProcessedEventArgs e) { (from eh in this.eventHandlers where eh.CanHandle(e.Event) select eh).ToList().ForEach(async eh => await eh.HandleAsync(e.Event)); } public Task PublishAsync<TEvent>(TEvent @event, CancellationToken cancellationToken = default(CancellationToken)) where TEvent : IEvent => Task.Factory.StartNew(() => eventQueue.Push(@event)); /// <summary> /// 事件订阅(订阅队列上的事件) /// </summary> public void Subscribe() { eventQueue.EventPushed += EventQueue_EventPushed; }
上面的代码中EventQueue的Push方法被调用时,会触发EventPushed事件,在EventBus中,我们注册了EventQueue的EventPushed事件,即最终会触发EventBus的EventQueue_EventPushed事件,进而通过事件处理器来处理(这块详细说明,请阅读DaxNet-事件驱动型架构实现一)
到此,消息总线机制处理完成,接下来我们创建一个Web API应用程序来演示消息发布/订阅及处理
上面我们定义了IEvent/IEventHandler,这里我们先在WebAPI 项目中来实现
public class CustomerCreatedEvent : IEvent { public CustomerCreatedEvent(string customerName) { this.Id = Guid.NewGuid(); this.Timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); this.CustomerName = customerName; } public Guid Id { get; } public long Timestamp { get; } public string CustomerName { get; } }
public class CustomerCreatedEventHandler : IEventHandler<CustomerCreatedEvent> { public bool CanHandle(IEvent @event) => @event.GetType().Equals(typeof(CustomerCreatedEvent)); public Task<bool> HandleAsync(CustomerCreatedEvent @event, CancellationToken cancellationToken = default(CancellationToken)) { return Task.FromResult(true); } public Task<bool> HandleAsync(IEvent @event, CancellationToken cancellationToken = default(CancellationToken)) => CanHandle(@event) ? HandleAsync((CustomerCreatedEvent)@event, cancellationToken) : Task.FromResult(false); }
值得说明下的是,在实现IEventHandler的时候,利用了泛型接口IEventHandler<T>来建立IEventHandler对于IEvent的依赖,因为事件处理器最终处理的必然是某个事件。
OK,现在新建一个Controller
[Route("api/[controller]")] public class CustomersController : Controller {private readonly IEventBus _eventBus; public CustomersController(IEventBus eventBus) { _eventBus = eventBus; } // 创建新的客户信息 [HttpPost] public async Task<IActionResult> Create([FromBody] CustomerDto model) { var name = model.Name; if (string.IsNullOrEmpty(name)) { return BadRequest(); }
//这里其他业务处理...
await _eventBus.PublishAsync(new CustomerCreatedEvent(name));
} }
上面的CustomersController构造函数中由Ioc注入了eventBus,需要注意的是,引用eventBus前,需要在Startup.cs中注册对应的服务,我们这里用到的是Autofac的模块化注册。
利用Web API下的WebModule.cs引用SimpleEventBus中的EventBusModule
public class WebModule : Module { protected override void Load(ContainerBuilder builder) {
//扫描程序集自动注册 builder.RegisterAssembly(ThisAssembly);
//注册模块化EventBusModule builder.RegisterModule<EventBusModule>(); } }
public class EventBusModule : Module { protected override void Load(ContainerBuilder builder) {
//扫描程序集自动注册 builder.RegisterAssembly(ThisAssembly); } }
关于扫描程序集自动注册,上面Ioc段落有详细说明,这里就不再啰嗦。
到此为止,编码工作告一段落,运行Web API,利用Postman或PowerShell 自带的命令 Invoke-WebRequest模拟http请求,结果如下图:
请求进来,数据加载到Dto中
EventBus的事件发布时调用EventQueue的Push函数,同时会触发EventPushed事件,通过对应的时间处理器处理事件
最终,消息在事件处理器中进行相关处理
回顾
回顾一下,本篇开头介绍了Autofac替换Asp.Net Core自带Ioc容器,Autofac的模块注册/泛型/服务注册等;然后介绍了事件总线的工作流程:事件发布到总线中,通过消息队列触发注册到总线中的事件处理器处理事件消息;最后,我们利用Web API 展示了程序的运行过程。
因为篇幅有限,代码中关于Storage的部分,涉及到了仓储的概念,我想到时放到领域设计部分一起介绍。
源码
本篇涉及的源码在Github的https://github.com/ChuanGoing/Start.git 的SimpleEventBus分支可以找到。