EventBus InMemory 的实践基于eShopOnContainers (二)

前言

最近在工作中遇到了一个需求,会用到EventBus,正好看到eShopOnContainers上有相关的实例,去研究了研究。下面来分享一下用EventBus 来改造一下我们上篇Event发布与实践 中所用的Event

上一篇中讲到Event在发布与订阅模式中的一些实例,接下来实践一下通过把上面的例子改造成EventBus来加深理解。也感谢参考资料中大佬前辈们的思想和精华。

理解事件及其本质

我们所构建的一个场景是这样的:有个CarManager类,其中有个发车了的事件名为CarNotification, 有司机Driver和乘客Passenger这两个类。分别订阅了 发车事件, 这里司机和乘客收到通知后,然后简单的处理,仅仅打印出 司机和乘客的姓名信息。

在上面的场景中,我们可以知道,事件源,事件处理各是什么。

事件源:司机 或者 乘客 类。

事件处理: 打印出 司机或 乘客的信息。

大概花了一张巨丑的图,下面:

006tKfTcgy1fqi4ws4pq0j31240sa420.jpg

开始抽象事件源

接下来,我们可以开始抽象起来了。首先是 事件源:

定义一个所有事件源的父类,名为 EventData: 所有的事件源都需要继承该类

public class EventData{
    public Guid Id{ get; }
    public DateTime CreationDate{ get; }
    
    public EventData(){
        Id = Guid.NewGuid();
        CreationDate = DateTime.Now;
    }
}

然后我们就可以把我们之前的DriverPassenger 用我们定义的事件源来改造一下:

public class CarNotificationEventData : EventData{
    private string _driverName;
    private string _passengerName;
    
    public CarNotification(string driverName, string passengerName){
        _driverName = driverName;
        _passengerName = passengerName;
    }
    
    public string Driver{ get{ return _driverName; } }
    
    public string Passenger{ get{return _passengerName; } }
}

接下来就是抽象 事件处理 了,我们在此定义一个名为 IEventDataHandler 的接口:

public interface IEventHandler<in TEventData> : IEventHandler where TEventData : EventData{
    Task Handle(TEventData eventData);
}

public interface IEventHandler{
    
}
  • 定义了一个泛型接口,参数必须继承自 EventData 类型。
  • 一个方法 Handle 方法,接收的参数也为 EventData 类型。

查看eShopOnContainer的源码时,上面那个为啥定义,继承一个空接口IEventHandler ,当时有点没搞明白,后来继续去看了看源码,发现了下面这段:

var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);

大胆猜测一下,应该是反射时会用到,所以定义了一个空接口

那么我们根据上面的接口,简单改造一下我们的事件处理代码:

public class DriverHandler : IEventHandler<CarNotificationEventData>{
    public void Handle (CarNotificationEventData carNotificationEventData) {
            Console.WriteLine ("Driver Hanlder---------");
            Console.WriteLine (carNotificationEventData.Driver + "\n" + carNotificationEventData.Passenger +
                "\n" + carNotificationEventData.EventDate);
        }
}

偷了个懒,简单了打印了以下事件源中的信息,当然生产环境中,你就根据自己的业务逻辑来进行处理。

当然,改了事件源和事件处理,当然也需要重新对 我们当初 事件 以及 委托的定义。

//修改委托的定义:

public delegate void CarEventDataHandler(CarNotificationEventData eventData);

//修改事件的定义:
public event CarEventDataHandler CarNotification;

实现到上面那步,其实我们还只是刚刚开始,因为你会发现,我们只仅仅把那些事件源和事件处理抽线了出来,在每个事件处理程序中,我们可能还需要通过

但并没有真正上的做到用事件总线来实现。

为了更好的实现下面的事件总线,我们把委托也单独定义到一个class 中:

namespace EventDemo{
    public delegate void CarNotificationDelegate(CatNotificationEventData eventData);
}
实现事件总线

根据 eShopOnContainers上的源码 IEventBusSubscription,我们来实现一个基于InMemory(存在Dictionary 中)的事件总线。

首先想到的 发布与订阅模式,所以呢,这个事件总线里面一定要有可以订阅和移除订阅的方法,还有来个额外的判断当前事件总线是否为空,当然还有一个就是 事件,接下来我们就定义一个 IEventBusSubscription:

 public interface IEventBusSubscriptionsManager {
        bool IsEmpty { get; }

        event CarNotificationDelegate OnEventRemoved;

        void AddSubscription<T, TH> () where T : CarNotificationEventData where TH : IEventHandler<T>;

        void RemoveSubscription<T, TH> (T eventBusData) where T : CarNotificationEventData where TH : IEventHandler<T>;

        bool HasSubscriptionsForEvent<T> () where T : CarNotificationEventData;

        bool HasSubscriptionsForEvent (string eventName);

    }

就简单点,一个订阅方法,一个移除订阅方法。还有一个事件 OnEventRemoved

接下来就是实现了,eShopOnContainer 上面有好多个版本,我在实践中,试了试 InMemory 和 RabbitMQ 的。因为十分贴合我的业务场景,本篇先介绍一下InMemory的实现,因为对RabbitMQ理解的还不是很深入, RabbitMQ版本的后续博文中跟进。

然后我们建立一个InMemoryEventBusSubscriptionsManager 继承至IEventBusSubscripionsManager

public class InMemoryEventBusSubscriptionsManager : IEventBusSubscriptionsManager {
        private readonly Dictionary<string, List<Type>> _handler; //Type is HandlerType
        private readonly List<Type> _eventTypes;
        public event CarNotificationDelegate OnEventRemoved;
        private readonly IServiceProvider _service;

        public InMemoryEventBusSubscriptionsManager (IServiceCollection service) {
            _handler = new Dictionary<string, List<Type>> ();
            _eventTypes = new List<Type> ();
            _service = service.BuildServiceProvider ();
            OnEventRemoved += BeiginProcess;
        }

        public bool IsEmpty => !_handler.Keys.Any ();

        public void AddSubscription<T, TH> ()
        where T : CarNotificationEventData
        where TH : IEventHandler<T> {
            var eventName = GetEventKey<T> ();

            if (!HasSubscriptionsForEvent<T> ()) {
                _handler.Add (eventName, new List<Type> ());
            }

            if (_handler[eventName].Any (t => t == typeof (TH))) {
                throw new ArgumentException (
                    $"Handler Type {typeof(TH).Name} already registered");
            }
            _handler[eventName].Add (typeof (TH));

            _eventTypes.Add (typeof (T));
        }

        public void RemoveSubscription<T, TH> (T eventData)
        where T : CarNotificationEventData
        where TH : IEventHandler<T> {
            var handlerToRemove = FindSubscriptionToRemove<T, TH> ();
            DoRemoveHandler (eventData, handlerToRemove);
        }

        private void DoRemoveHandler (CarNotificationEventData eventData, Type subsToRemove) {
            if (subsToRemove != null) {

                var eventName = eventData.GetType ().Name;

                _handler[eventName].Remove (subsToRemove);
                if (!_handler[eventName].Any ()) {
                    _handler.Remove (eventName);
                    var eventType = _eventTypes.SingleOrDefault (e => e == eventName.GetType ());
                    if (eventType != null) {
                        _eventTypes.Remove (eventType);
                    }
                    RaiseOnEventRemoved (eventData);
                }

            }
        }

        private void RaiseOnEventRemoved (CarNotificationEventData eventData) {
            var handler = OnEventRemoved;
            if (handler != null) {
                OnEventRemoved (eventData);
            }
        }

        private Type FindSubscriptionToRemove<T, TH> ()
        where T : CarNotificationEventData
        where TH : IEventHandler<T> {
            var eventName = GetEventKey<T> ();
            return DoFindSubscriptionToRemove (eventName, typeof (TH));
        }

        private Type DoFindSubscriptionToRemove (string eventName, Type handlerType) {
            if (!HasSubscriptionsForEvent (eventName)) {
                return null;
            }

            return _handler[eventName].SingleOrDefault (s => s == handlerType);

        }
        public bool HasSubscriptionsForEvent<T> () where T : CarNotificationEventData {
            var keyName = GetEventKey<T> ();
            return _handler.ContainsKey (keyName);
        }

        public bool HasSubscriptionsForEvent (string eventName) => _handler.ContainsKey (eventName);

        public string GetEventKey<T> () {
            return typeof (T).Name;
        }

        public Type GetEventTypeByName (string eventName) => _eventTypes.SingleOrDefault (t => t.Name == eventName);

        public async void BeiginProcess (CarNotificationEventData eventData) {
            await Process (eventData);
        }

        private async Task Process (CarNotificationEventData eventBusData) {
            var eventName = eventBusData.GetType ().Name;
            if (HasSubscriptionsForEvent (eventName)) {
                var subscriptions = _handler[eventName];

                foreach (var subscription in subscriptions) {
                    var eventType = GetEventTypeByName (eventName);
                    var handler = _service.GetService (subscription);
                    var concreteType = typeof (IEventHandler<>).MakeGenericType (eventType);

                    await (Task) concreteType.GetMethod ("EventHandle").Invoke (handler, new object[] { eventBusData });
                }
            }
        }
    }

我稍微改动了以下地方的代码,使我的实例更加符合场景上的运行,

  • eShopOnContainer 里面使用了 Autofac 来运用DI,我直接使用了 .net -core 中自带的 IServiceProvider来替代,感觉更加方便
  • 直接对外显示一个public 的BeginProcess 来引发事件,主要为了演示方便。

既然EventBus 都写好,我们可以开始运行了,

//... 
public static void Main (string[] args) {
            #region EventBusRegister Demo

            var serviceProvider = new ServiceCollection ()
                .AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager> ()
                .AddTransient<DriverHandler> ();

            var eventBus = new InMemoryEventBusSubscriptionsManager (serviceProvider);

            RegisterEventBus (eventBus);

            CarNotificationEventData carNotificationEventData = new CarNotificationEventData ("Robert 1", "Passenger 1");

            eventBus.BeiginProcess (carNotificationEventData);
            #endregion
        }
//...

dotnet run 运行一下,得到如下结果:

006tNc79gy1fqlj14v48oj30zy04q3yx.jpg

这样就算大功告成了。接下来会写一篇结合RabbitMQ 的EventBus ,就更加符合生产环境的情景了。文中如果解释的不到位处,欢迎评论中指出,一起探讨。

参考资料

转载于:https://www.cnblogs.com/xiyin/p/8908172.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于简化的微服务架构和Docker容器的Microsoft由Microsoft提供的示例.NET Core参考应用程序。   免责声明 重要说明:此示例应用程序的当前状态是ALPHA,认为这是版本0.1版本的基础,因此,很多方面还有待改进和改变显著而重构当前的代码和实现新的功能。对社区的改进和反馈将受到高度的赞赏和接受。 该参考应用程序提出了一个简化的面向服务的体系结构实现,通过综合应用程序引入诸如.NET Core与Docker容器之类的技术。然而,这个引用应用程序并不是要解决大型和关键任务的分布式系统中的所有问题,只是让开发人员轻松开始使用.NET Core的Docker容器和微服务器的开始。 例如,在了解Docker容器和使用.NET Core开发微服务器之后,下一步(eShopOnContainers还没有涵盖),就是选择像Docker Swarm,Kubernetes或DC / OS(Azure容器服务)或Azure中的微服务集群/协调器在大多数情况下,服务结构将需要对应用程序的配置进行额外的部分更改(尽管当前体系结构应适用于大多数具有较小更改的业务流程)。或将数据库移动到HA云服务,或者在Azure Service Bus或任何其他生产就绪的服务总线市场上实施EventBus。 在将来,我们可能会分配此项目,并针对特定的微服务集群/协调者加上多个版本,并使用额外的云基础架构。 有关可能的新实施的更多信息,请阅读Wiki中未来版本的eShopOnContainers的计划路线图和里程碑,并在ISSUES部分提供反馈,如果您希望看到任何特定的方案得到实施或改进。此外,请随时讨论任何当前的问题。 架构概述:本参考应用程序是在服务器端和客户端的跨平台两种,由于能够根据您的码头工人主机上的Linux或Windows容器运行的.NET的核心服务,并为Xamarin的Android,iOS或正在运行的移动应用Windows / UWP加上客户端网络应用程序的任何浏览器。该架构提出了一种简化的面向服务的架构实现,其中包含多个自主的微服务器(每个拥有自己的数据/ db),并使用Http作为当前的通信协议,在每个微服务器(简单的CRUD vs. DDD / CQRS模式)中实现不同的方法。 它还支持异步通信,用于基于集成事件和事件总线以及路线图中定义的其他功能的多个服务的数据更新传播。 微服务的类型不同,这意味着不同的内部架构模式取决于其目的,如下图所示。   最终将添加与其他框架和No-SQL数据库的其他miroservice样式。这是一个很好的机会,来自社区的拉动请求,例如使用Nancy的新微服务,或者甚至其他语言,如Node,Go,Python或具有MongoDB Azure DocDB兼容性的数据容器,PostgreSQL,RavenDB,Event Store,MySql等。) 数据库服务器/容器的重要说明 在该解决方案当前的开发环境配置中,SQL数据库将自动部署样本数据到单个SQL Server for Linux容器(SQL数据库的单一共享Docker容器),因此整个解决方案可以启动并运行,而无需任何依赖任何云或特定的服务器。每个数据库也可以部署为单个Docker容器,但是在开发机器中,您需要将8GB或更多的内存RAM分配给Docker才能在Docker Linux主机中运行3个SQL Server Docker容器, Docker for Windows“或”Docker for Mac“开发环境。 在将Redis缓存作为开发环境的容器运行时,定义了类似的情况。 但是,在实际的生产环境中,建议您将数据库(在本例中为SQL Server和Redis)设置为HA(高可用性)服务,如Azure SQL数据库,Redis服务或任何其他集群系统。如果要更改为生产配置,只需在HA云或内部部署服务器后更改连接字符串即可。 相关文档和指导 在开发这个参考应用,我们正在创造一个参考指南/电子书名为“构建和开发集装箱和微服务基于.NET应用程序”,其详细阐述了如何开发这种建筑风格(微服务,多克尔容器,领域驱动设计某些微服务)以及其他更简单的架构风格,如可以作为Docker容器生活的单片应用程序。 还有其他电子书专注于容器/ Docker生命周期(DevOps,CI / CD等)与Microsoft Tools,已经发布,另外还有一本关于使用Xamarin.Forms的企业应用程序模式的电子书。您可以下载它们,并在这里开始审查这些指南/电子书:   建筑与发展 集装箱生命周期和CI / CD 应用程序模式与Xamarin.Forms 下载(初稿,在进行的工作) 下载(自2016年后期第一版) 下载(初稿,在进行的工作) 发送反馈给cesardl@microsoft.com 然而,我们鼓励下载和审查“架构与开发电子书”,因为在指导中解释的架构风格和架构模式和技术在解释许多模式实现时使用此参考应用程序,因此您将更好地了解上下文,设计以及在当前架构和内部设计中采取的决策。 应用程序代码概述 在这种回购,你可以找到一个样本参考应用,将帮助您了解如何实现用微服务架构的应用.NET的核心和多克。 示例业务域或场景基于作为多容器应用实现的eShop或电子商务。每个容器都是使用.NET Core运行的ASP.NET Core开发的一个微服务部署(如basket-microservice,catalog-microservice,ordering-microservice和identity-microservice),因此可以在Linux Containers和Windows Containers 。下面的屏幕截图显示了这些微服务器/容器和客户端应用程序的VS Solution结构。 (推荐入门时)开放eShopOnContainers-ServicesAndWebApps.sln为仅包含相关的微服务和Web应用程序的服务器端项目的解决方案。 开放eShopOnContainers-MobileApps.sln为仅包含所述客户端的移动应用项目的解决方案(仅Xamarin移动应用)。它也是基于mocks独立工作的。 打开eShopOnContainers.sln包含所有项目(所有的客户端应用和服务)的解决方案。 最后,这些微服务由多个客户端网络和移动应用程序消耗,如下所述。 MVC应用程序(ASP.NET核心):它的一个MVC应用程序,你可以找到关于如何消费有趣的场景基于HTTP的从C#在服务器端运行的微服务,因为它是一个典型的ASP.NET MVC的核心应用。由于它是一个服务器端应用程序,因此内部Docker Host网络内部名称解析可以访问其他容器/微服务器。 SPA(单页应用):提供了类似的“网上商店的业务功能”,而是开发了角2,打字稿及轻微使用ASP.NET MVC的核心。这是客户端Web应用程序的另一种方法,当您希望拥有更现代的客户端行为时,其行为与每个操作上的典型浏览器往返行为不同,但表现为类似于单页应用程序的单页面应用程序桌面应用使用体验。基于HTTP的微服务的消耗由客户端浏览器中的TypeScript / JavaScript完成,因此客户端调用微服务器来自Docker Host内部网络(从您的网络甚至从互联网)。 Xamarin移动应用(对于iOS,Android和Windows / UWP) :这是一个客户端移动应用程序支持最常见的移动操作系统平台(的iOS,Android和Windows / UWP)。在这种情况下,微服务的消耗是从C#完成的,但是在客户端设备上运行, 为eShopOnContainers设置开发环境 Visual Studio 2017和Windows 这是更直接的入门方式:https://github.com/dotnet/eShopOnContainers/wiki/02.-Setting-eShopOnContainer-solution-up-in-a-Visual-Studio-2017- environment  CLI和Windows 对于喜欢Windows的CLI的用户,使用dotnet CLI,Docker CLI和Windows的VS代码: https://github.com/dotnet/eShopOnContainers/wiki/03.-Setting-the-eShopOnContainers-solution-up-in-a-Windows-CLI-environment-(dotnet-CLI,-Docker-CLI-and-VS-Code) CLI和Mac 对于那些喜欢在Mac上使用CLI的用户,使用dotnet CLI,Docker CLI和VS代码(说明仍然TBD,但类似于Windows CLI):https://github.com/dotnet/eShopOnContainers/wiki/04.-设置eShopOnContainer-in-a-Mac,-VS-Code-and-CLI-environment - (dotnet-CLI,-Docker-CLI-and-VS-Code) 关于测试的Docker容器/图像的注意事项 大部分项目的开发和测试的是(如三月初2017)完成对码头工人的Linux容器在开发机器与“泊坞的Windows”运行,默认是“泊坞的Windows”已安装的Hyper-V的Linux VM(MobiLinuxVM) 。该窗口的容器方案,目前正在实施/测试还没有。应用程序应该能够在基于不同Docker基础映像的Windows Nano Containers上运行,因为.NET Core服务也已经在普通Windows(没有Docker)上运行。该应用程序也使用安装了.NET Core和VS代码的开发MacOS机器在“Docker for Mac”中进行了部分测试,这仍然是使用在Mac上的VM安装程序上运行的Linux容器的“Docker for Windows” 建立。但是,来自社区的Mac环境和Windows Containers的进一步测试和反馈将不胜感激。 发送反馈和建议 如上所述,我们非常感谢您的反馈,改进和想法。您可以在问题部分创建新问题, 或发送电子邮件至eshop_feedback@service.microsoft.com 问题 问题列表:https://github.com/dotnet/eShopOnContainers/issues/107 标签:docker  微服务
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值