From c56dcb29099daac7b9aa42f440f18ba4a7593b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Tue, 3 Mar 2026 16:33:17 +0300 Subject: [PATCH 01/22] Add PublishAsync overload to publish by name Introduce a new IEventBus.PublishAsync(string eventName, ...) overload and make EventBusBase declare it. Implementations for Azure, Dapr, Kafka, RabbitMQ, Rebus, LocalDistributedEventBus and LocalEventBus resolve the event Type from an EventTypes map and delegate to the existing type-based PublishAsync. LocalEventBus now maintains an EventTypes dictionary (populated on Subscribe) to map event names to types. Unknown event names now throw an AbpException. --- .../Volo/Abp/EventBus/IEventBus.cs | 4 +++- .../EventBus/Azure/AzureDistributedEventBus.cs | 11 +++++++++++ .../Abp/EventBus/Dapr/DaprDistributedEventBus.cs | 11 +++++++++++ .../EventBus/Kafka/KafkaDistributedEventBus.cs | 11 +++++++++++ .../RabbitMq/RabbitMqDistributedEventBus.cs | 11 +++++++++++ .../EventBus/Rebus/RebusDistributedEventBus.cs | 11 +++++++++++ .../Distributed/LocalDistributedEventBus.cs | 11 +++++++++++ .../Volo/Abp/EventBus/EventBusBase.cs | 2 ++ .../Volo/Abp/EventBus/Local/LocalEventBus.cs | 16 ++++++++++++++++ 9 files changed, 87 insertions(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs index f1ceae8617..938fd4d97b 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs @@ -23,6 +23,8 @@ public interface IEventBus /// True, to publish the event at the end of the current unit of work, if available /// The task to handle async operation Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true); + + Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true); /// /// Registers to an event. @@ -117,4 +119,4 @@ public interface IEventBus /// /// Event type void UnsubscribeAll(Type eventType); -} +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index 32b07f1b82..da997f23f4 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -250,6 +250,17 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen .Locking(factories => factories.Clear()); } + public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) + { + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType == null) + { + throw new AbpException($"Unknown event name: {eventName}"); + } + + return PublishAsync(eventType, eventData, onUnitOfWorkComplete); + } + protected async override Task PublishToEventBusAsync(Type eventType, object eventData) { await PublishAsync(EventNameAttribute.GetNameOrDefault(eventType), eventData); diff --git a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs index 7c77340dda..548467d1c5 100644 --- a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs @@ -129,6 +129,17 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear()); } + public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) + { + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType == null) + { + throw new AbpException($"Unknown event name: {eventName}"); + } + + return PublishAsync(eventType, eventData, onUnitOfWorkComplete); + } + protected async override Task PublishToEventBusAsync(Type eventType, object eventData) { await PublishToDaprAsync(eventType, eventData, null, CorrelationIdProvider.Get()); diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs index a3376d9429..5909614fb3 100644 --- a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs @@ -168,6 +168,17 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear()); } + public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) + { + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType == null) + { + throw new AbpException($"Unknown event name: {eventName}"); + } + + return PublishAsync(eventType, eventData, onUnitOfWorkComplete); + } + protected override async Task PublishToEventBusAsync(Type eventType, object eventData) { var headers = new Headers diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs index 3a647388b5..c58e40a1ec 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs @@ -193,6 +193,17 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear()); } + public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) + { + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType == null) + { + throw new AbpException($"Unknown event name: {eventName}"); + } + + return PublishAsync(eventType, eventData, onUnitOfWorkComplete); + } + protected async override Task PublishToEventBusAsync(Type eventType, object eventData) { await PublishAsync(eventType, eventData, correlationId: CorrelationIdProvider.Get()); diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs index 7a3d79e7d8..a90e166289 100644 --- a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs @@ -160,6 +160,17 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen } } + public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) + { + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType == null) + { + throw new AbpException($"Unknown event name: {eventName}"); + } + + return PublishAsync(eventType, eventData, onUnitOfWorkComplete); + } + protected async override Task PublishToEventBusAsync(Type eventType, object eventData) { var headers = new Dictionary(); diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs index 843fb4f8ea..abcd2dbd05 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs @@ -134,6 +134,17 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen await PublishToEventBusAsync(eventType, eventData); } + public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) + { + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType == null) + { + throw new AbpException($"Unknown event name: {eventName}"); + } + + return PublishAsync(eventType, eventData, onUnitOfWorkComplete); + } + protected async override Task PublishToEventBusAsync(Type eventType, object eventData) { if (await AddToInboxAsync(Guid.NewGuid().ToString(), EventNameAttribute.GetNameOrDefault(eventType), eventType, eventData, null)) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs index c8ce733b22..42d35ad314 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs @@ -117,6 +117,8 @@ public abstract class EventBusBase : IEventBus await PublishToEventBusAsync(eventType, eventData); } + public abstract Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true); + protected abstract Task PublishToEventBusAsync(Type eventType, object eventData); protected abstract void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord); diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs index 7123ff340a..8631eaf5cf 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs @@ -29,6 +29,8 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency protected AbpLocalEventBusOptions Options { get; } protected ConcurrentDictionary> HandlerFactories { get; } + + protected ConcurrentDictionary EventTypes { get; } public LocalEventBus( IOptions options, @@ -42,6 +44,7 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency Logger = NullLogger.Instance; HandlerFactories = new ConcurrentDictionary>(); + EventTypes = new ConcurrentDictionary(); SubscribeHandlers(Options.Handlers); } @@ -54,6 +57,8 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency /// public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { + EventTypes.GetOrAdd(EventNameAttribute.GetNameOrDefault(eventType), eventType); + GetOrCreateHandlerFactories(eventType) .Locking(factories => { @@ -121,6 +126,17 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear()); } + public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) + { + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType == null) + { + throw new AbpException($"Unknown event name: {eventName}"); + } + + return PublishAsync(eventType, eventData, onUnitOfWorkComplete); + } + protected override async Task PublishToEventBusAsync(Type eventType, object eventData) { await PublishAsync(new LocalEventMessage(Guid.NewGuid(), eventData, eventType)); From 58e0c2f609e20d733abf761141895c39f5821729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Wed, 4 Mar 2026 01:17:58 +0300 Subject: [PATCH 02/22] Add anonymous event support to EventBus Introduce AnonymousEventData and add support for anonymous (name-based) events across the event bus implementations. Adds string-based Subscribe/Unsubscribe APIs, anonymous handler factories, and handling in distributed providers (Azure, Dapr, Kafka, RabbitMQ, Rebus) and local buses. Update EventBusBase and DistributedEventBusBase to resolve event names/data (GetEventName/GetEventData/ResolveEventForPublishing) and route/serialize/deserialize anonymous payloads. Also add AnonymousEventHandlerFactoryUnregistrar and minimal NullDistributedEventBus implementations, plus tests for anonymous local events. --- .../Volo/Abp/EventBus/AnonymousEventData.cs | 103 ++++++++++ .../Volo/Abp/EventBus/IEventBus.cs | 4 + .../Volo/Abp/EventBus/Local/ILocalEventBus.cs | 2 + .../Azure/AzureDistributedEventBus.cs | 98 +++++++++- .../EventBus/Dapr/DaprDistributedEventBus.cs | 66 ++++++- .../Kafka/KafkaDistributedEventBus.cs | 105 ++++++++-- .../RabbitMq/RabbitMqDistributedEventBus.cs | 100 ++++++++-- .../Rebus/RebusDistributedEventBus.cs | 78 +++++++- .../Distributed/DistributedEventBusBase.cs | 42 +++- .../Distributed/LocalDistributedEventBus.cs | 67 ++++++- .../Distributed/NullDistributedEventBus.cs | 17 +- .../Volo/Abp/EventBus/EventBusBase.cs | 63 +++++- .../EventHandlerFactoryUnregistrar.cs | 19 ++ .../Volo/Abp/EventBus/Local/LocalEventBus.cs | 99 +++++++++- .../Abp/EventBus/Local/NullLocalEventBus.cs | 20 ++ .../LocalDistributedEventBus_Test.cs | 179 ++++++++++++++++++ .../Local/LocalEventBus_Anonymous_Test.cs | 162 ++++++++++++++++ 17 files changed, 1140 insertions(+), 84 deletions(-) create mode 100644 framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs create mode 100644 framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs new file mode 100644 index 0000000000..c4b5d1c3fd --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; + +namespace Volo.Abp.EventBus; + +[Serializable] +public class AnonymousEventData +{ + public string EventName { get; } + public object Data { get; } + + private JsonElement? _cachedJsonElement; + + public AnonymousEventData(string eventName, object data) + { + EventName = eventName; + Data = data; + } + + public object ConvertToTypedObject() + { + return ConvertElement(GetJsonElement()); + } + + public T ConvertToTypedObject() + { + if (Data is T typedData) + { + return typedData; + } + + return GetJsonElement().Deserialize() + ?? throw new InvalidOperationException($"Failed to deserialize AnonymousEventData to {typeof(T).FullName}."); + } + + public object ConvertToTypedObject(Type type) + { + if (type.IsInstanceOfType(Data)) + { + return Data; + } + + return GetJsonElement().Deserialize(type) + ?? throw new InvalidOperationException($"Failed to deserialize AnonymousEventData to {type.FullName}."); + } + + private JsonElement GetJsonElement() + { + if (_cachedJsonElement.HasValue) + { + return _cachedJsonElement.Value; + } + + if (Data is JsonElement existingElement) + { + _cachedJsonElement = existingElement; + return existingElement; + } + + _cachedJsonElement = JsonSerializer.SerializeToElement(Data); + return _cachedJsonElement.Value; + } + + private static object ConvertElement(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + { + var obj = new Dictionary(); + foreach (var property in element.EnumerateObject()) + { + obj[property.Name] = property.Value.ValueKind == JsonValueKind.Null + ? null + : ConvertElement(property.Value); + } + return obj; + } + case JsonValueKind.Array: + return element.EnumerateArray() + .Select(item => item.ValueKind == JsonValueKind.Null ? null : (object?)ConvertElement(item)) + .ToList(); + case JsonValueKind.String: + return element.GetString()!; + case JsonValueKind.Number when element.TryGetInt64(out var longValue): + return longValue; + case JsonValueKind.Number when element.TryGetDecimal(out var decimalValue): + return decimalValue; + case JsonValueKind.Number when element.TryGetDouble(out var doubleValue): + return doubleValue; + case JsonValueKind.True: + return true; + case JsonValueKind.False: + return false; + case JsonValueKind.Null: + case JsonValueKind.Undefined: + default: + return null!; + } + } +} diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs index 938fd4d97b..fa17528750 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs @@ -52,6 +52,8 @@ public interface IEventBus /// Event type /// Object to handle the event IDisposable Subscribe(Type eventType, IEventHandler handler); + + IDisposable Subscribe(string eventName, IEventHandlerFactory handler); /// /// Registers to an event. @@ -106,6 +108,8 @@ public interface IEventBus /// Event type /// Factory object that is registered before void Unsubscribe(Type eventType, IEventHandlerFactory factory); + + void Unsubscribe(string eventName, IEventHandlerFactory factory); /// /// Unregisters all event handlers of given event type. diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventBus.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventBus.cs index e691b6c58c..d816d61c14 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventBus.cs @@ -23,4 +23,6 @@ public interface ILocalEventBus : IEventBus /// Event type /// List GetEventHandlerFactories(Type eventType); + + List GetAnonymousEventHandlerFactories(string eventName); } diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index da997f23f4..8cf42cc796 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Azure.Messaging.ServiceBus; using Microsoft.Extensions.DependencyInjection; @@ -29,6 +30,7 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected IAzureServiceBusSerializer Serializer { get; } protected ConcurrentDictionary> HandlerFactories { get; } protected ConcurrentDictionary EventTypes { get; } + protected ConcurrentDictionary> AnonymousHandlerFactories { get; } protected IAzureServiceBusMessageConsumer Consumer { get; private set; } = default!; public AzureDistributedEventBus( @@ -61,6 +63,7 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen PublisherPool = publisherPool; HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); + AnonymousHandlerFactories = new ConcurrentDictionary>(); } public void Initialize() @@ -81,14 +84,25 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen { return; } + var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(message.Body.ToArray(), eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(eventName)) + { + var data = Serializer.Deserialize(message.Body.ToArray()); + eventData = new AnonymousEventData(eventName, data); + eventType = typeof(AnonymousEventData); + } + else { return; } - var eventData = Serializer.Deserialize(message.Body.ToArray(), eventType); - if (await AddToInboxAsync(message.MessageId, eventName, eventType, eventData, message.CorrelationId)) { return; @@ -162,12 +176,22 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen public async override Task ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); - if (eventType == null) + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + { + var element = Serializer.Deserialize(incomingEvent.EventData); + eventData = new AnonymousEventData(incomingEvent.EventName, element); + eventType = typeof(AnonymousEventData); + } + else { return; } - - var eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); var exceptions = new List(); using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) { @@ -253,17 +277,25 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) + var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + } + + if (AnonymousHandlerFactories.ContainsKey(eventName)) { - throw new AbpException($"Unknown event name: {eventName}"); + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); } - return PublishAsync(eventType, eventData, onUnitOfWorkComplete); + throw new AbpException($"Unknown event name: {eventName}"); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) { - await PublishAsync(EventNameAttribute.GetNameOrDefault(eventType), eventData); + var (eventName, resolvedData) = ResolveEventForPublishing(eventType, eventData); + await PublishAsync(eventName, resolvedData); } protected override void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord) @@ -312,6 +344,52 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen .ToArray(); } + protected override Type? GetEventTypeByEventName(string eventName) + { + return EventTypes.GetOrDefault(eventName); + } + + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => + { + if (!handler.IsInFactories(factories)) + { + factories.Add(handler); + } + }); + + return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + } + + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + } + + protected override IEnumerable GetAnonymousHandlerFactories(string eventName) + { + var result = new List(); + + var eventType = GetEventTypeByEventName(eventName); + if (eventType != null) + { + result.AddRange(GetHandlerFactories(eventType)); + } + + foreach (var handlerFactory in AnonymousHandlerFactories.Where(hf => hf.Key == eventName)) + { + result.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + } + + return result; + } + + private List GetOrCreateAnonymousHandlerFactories(string eventName) + { + return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); + } + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { return handlerEventType == targetEventType || handlerEventType.IsAssignableFrom(targetEventType); diff --git a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs index 548467d1c5..f036514115 100644 --- a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -28,6 +28,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend protected ConcurrentDictionary> HandlerFactories { get; } protected ConcurrentDictionary EventTypes { get; } + protected ConcurrentDictionary> AnonymousHandlerFactories { get; } public DaprDistributedEventBus( IServiceScopeFactory serviceScopeFactory, @@ -58,6 +59,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); + AnonymousHandlerFactories = new ConcurrentDictionary>(); } public void Initialize() @@ -132,17 +134,25 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) + var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + } + + if (AnonymousHandlerFactories.ContainsKey(eventName)) { - throw new AbpException($"Unknown event name: {eventName}"); + return PublishAsync(typeof(AnonymousEventData), new AnonymousEventData(eventName, eventData), onUnitOfWorkComplete); } - return PublishAsync(eventType, eventData, onUnitOfWorkComplete); + throw new AbpException($"Unknown event name: {eventName}"); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) { - await PublishToDaprAsync(eventType, eventData, null, CorrelationIdProvider.Get()); + var (eventName, resolvedData) = ResolveEventForPublishing(eventType, eventData); + await PublishToDaprAsync(eventName, resolvedData, null, CorrelationIdProvider.Get()); } protected override void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord) @@ -162,6 +172,52 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend return handlerFactoryList.ToArray(); } + protected override Type? GetEventTypeByEventName(string eventName) + { + return EventTypes.GetOrDefault(eventName); + } + + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => + { + if (!handler.IsInFactories(factories)) + { + factories.Add(handler); + } + }); + + return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + } + + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + } + + protected override IEnumerable GetAnonymousHandlerFactories(string eventName) + { + var result = new List(); + + var eventType = GetEventTypeByEventName(eventName); + if (eventType != null) + { + result.AddRange(GetHandlerFactories(eventType)); + } + + foreach (var handlerFactory in AnonymousHandlerFactories.Where(hf => hf.Key == eventName)) + { + result.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + } + + return result; + } + + private List GetOrCreateAnonymousHandlerFactories(string eventName) + { + return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); + } + public async override Task PublishFromOutboxAsync(OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig) { var eventType = GetEventType(outgoingEvent.EventName); diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs index 5909614fb3..17d4bccc12 100644 --- a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Confluent.Kafka; using Microsoft.Extensions.DependencyInjection; @@ -29,6 +30,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected IProducerPool ProducerPool { get; } protected ConcurrentDictionary> HandlerFactories { get; } protected ConcurrentDictionary EventTypes { get; } + protected ConcurrentDictionary> AnonymousHandlerFactories { get; } protected IKafkaMessageConsumer Consumer { get; private set; } = default!; public KafkaDistributedEventBus( @@ -63,6 +65,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); + AnonymousHandlerFactories = new ConcurrentDictionary>(); } public void Initialize() @@ -80,14 +83,25 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen { var eventName = message.Key; var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) - { - return; - } var messageId = message.GetMessageId(); - var eventData = Serializer.Deserialize(message.Value, eventType); var correlationId = message.GetCorrelationId(); + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(message.Value, eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(eventName)) + { + var jsonElement = JsonSerializer.Deserialize(message.Value); + eventData = new AnonymousEventData(eventName, jsonElement); + eventType = typeof(AnonymousEventData); + } + else + { + return; + } if (await AddToInboxAsync(messageId, eventName, eventType, eventData, correlationId)) { @@ -171,12 +185,19 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) + var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + + if (eventType != null) { - throw new AbpException($"Unknown event name: {eventName}"); + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); } - return PublishAsync(eventType, eventData, onUnitOfWorkComplete); + if (AnonymousHandlerFactories.ContainsKey(eventName)) + { + return PublishAsync(typeof(AnonymousEventData), new AnonymousEventData(eventName, eventData), onUnitOfWorkComplete); + } + + throw new AbpException($"Unknown event name: {eventName}"); } protected override async Task PublishToEventBusAsync(Type eventType, object eventData) @@ -289,12 +310,22 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen InboxConfig inboxConfig) { var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); - if (eventType == null) + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + { + var jsonElement = JsonSerializer.Deserialize(incomingEvent.EventData); + eventData = new AnonymousEventData(incomingEvent.EventName, jsonElement); + eventType = typeof(AnonymousEventData); + } + else { return; } - - var eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); var exceptions = new List(); using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) { @@ -313,8 +344,8 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen private async Task PublishAsync(string topicName, Type eventType, object eventData, Headers headers) { - var eventName = EventNameAttribute.GetNameOrDefault(eventType); - var body = Serializer.Serialize(eventData); + var (eventName, resolvedData) = ResolveEventForPublishing(eventType, eventData); + var body = Serializer.Serialize(resolvedData); var result = await PublishAsync(topicName, eventName, body, headers); if (result.Status != PersistenceStatus.Persisted) @@ -374,6 +405,52 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen return handlerFactoryList.ToArray(); } + protected override Type? GetEventTypeByEventName(string eventName) + { + return EventTypes.GetOrDefault(eventName); + } + + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => + { + if (!handler.IsInFactories(factories)) + { + factories.Add(handler); + } + }); + + return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + } + + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + } + + protected override IEnumerable GetAnonymousHandlerFactories(string eventName) + { + var result = new List(); + + var eventType = GetEventTypeByEventName(eventName); + if (eventType != null) + { + result.AddRange(GetHandlerFactories(eventType)); + } + + foreach (var handlerFactory in AnonymousHandlerFactories.Where(hf => hf.Key == eventName)) + { + result.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + } + + return result; + } + + private List GetOrCreateAnonymousHandlerFactories(string eventName) + { + return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); + } + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { //Should trigger same type diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs index c58e40a1ec..928c99762d 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -33,6 +34,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis //TODO: Accessing to the List may not be thread-safe! protected ConcurrentDictionary> HandlerFactories { get; } protected ConcurrentDictionary EventTypes { get; } + protected ConcurrentDictionary> AnonymousHandlerFactories { get; } protected IRabbitMqMessageConsumerFactory MessageConsumerFactory { get; } protected IRabbitMqMessageConsumer Consumer { get; private set; } = default!; @@ -70,6 +72,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); + AnonymousHandlerFactories = new ConcurrentDictionary>(); } public virtual void Initialize() @@ -101,13 +104,23 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis { var eventName = ea.RoutingKey; var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(ea.Body.ToArray(), eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(eventName)) + { + var jsonElement = JsonSerializer.Deserialize(ea.Body.ToArray()); + eventData = new AnonymousEventData(eventName, jsonElement); + eventType = typeof(AnonymousEventData); + } + else { return; } - var eventData = Serializer.Deserialize(ea.Body.ToArray(), eventType); - var correlationId = ea.BasicProperties.CorrelationId; if (await AddToInboxAsync(ea.BasicProperties.MessageId, eventName, eventType, eventData, correlationId)) { @@ -196,12 +209,19 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) + var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + + if (eventType != null) { - throw new AbpException($"Unknown event name: {eventName}"); + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); } - return PublishAsync(eventType, eventData, onUnitOfWorkComplete); + if (AnonymousHandlerFactories.ContainsKey(eventName)) + { + return PublishAsync(typeof(AnonymousEventData), new AnonymousEventData(eventName, eventData), onUnitOfWorkComplete); + } + + throw new AbpException($"Unknown event name: {eventName}"); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) @@ -267,12 +287,22 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis InboxConfig inboxConfig) { var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); - if (eventType == null) + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + { + var jsonElement = JsonSerializer.Deserialize(incomingEvent.EventData); + eventData = new AnonymousEventData(incomingEvent.EventName, jsonElement); + eventType = typeof(AnonymousEventData); + } + else { return; } - - var eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); var exceptions = new List(); using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) { @@ -296,8 +326,8 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis Guid? eventId = null, string? correlationId = null) { - var eventName = EventNameAttribute.GetNameOrDefault(eventType); - var body = Serializer.Serialize(eventData); + var (eventName, resolvedData) = ResolveEventForPublishing(eventType, eventData); + var body = Serializer.Serialize(resolvedData); return PublishAsync(eventName, body, headersArguments, eventId, correlationId); } @@ -424,6 +454,52 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis return handlerFactoryList.ToArray(); } + protected override Type? GetEventTypeByEventName(string eventName) + { + return EventTypes.GetOrDefault(eventName); + } + + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => + { + if (!handler.IsInFactories(factories)) + { + factories.Add(handler); + } + }); + + return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + } + + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + } + + protected override IEnumerable GetAnonymousHandlerFactories(string eventName) + { + var result = new List(); + + var eventType = GetEventTypeByEventName(eventName); + if (eventType != null) + { + result.AddRange(GetHandlerFactories(eventType)); + } + + foreach (var handlerFactory in AnonymousHandlerFactories.Where(hf => hf.Key == eventName)) + { + result.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + } + + return result; + } + + private List GetOrCreateAnonymousHandlerFactories(string eventName) + { + return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); + } + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { //Should trigger same type diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs index a90e166289..a1f9e0116e 100644 --- a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -31,6 +31,7 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen //TODO: Accessing to the List may not be thread-safe! protected ConcurrentDictionary> HandlerFactories { get; } protected ConcurrentDictionary EventTypes { get; } + protected ConcurrentDictionary> AnonymousHandlerFactories { get; } protected AbpRebusEventBusOptions AbpRebusEventBusOptions { get; } public RebusDistributedEventBus( @@ -63,6 +64,7 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); + AnonymousHandlerFactories = new ConcurrentDictionary>(); } public void Initialize() @@ -163,12 +165,19 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) + var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + } + + if (AnonymousHandlerFactories.ContainsKey(eventName)) { - throw new AbpException($"Unknown event name: {eventName}"); + return PublishAsync(typeof(AnonymousEventData), new AnonymousEventData(eventName, eventData), onUnitOfWorkComplete); } - return PublishAsync(eventType, eventData, onUnitOfWorkComplete); + throw new AbpException($"Unknown event name: {eventName}"); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) @@ -240,6 +249,52 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen return handlerFactoryList.ToArray(); } + protected override Type? GetEventTypeByEventName(string eventName) + { + return EventTypes.GetOrDefault(eventName); + } + + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => + { + if (!handler.IsInFactories(factories)) + { + factories.Add(handler); + } + }); + + return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + } + + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + } + + protected override IEnumerable GetAnonymousHandlerFactories(string eventName) + { + var result = new List(); + + var eventType = GetEventTypeByEventName(eventName); + if (eventType != null) + { + result.AddRange(GetHandlerFactories(eventType)); + } + + foreach (var handlerFactory in AnonymousHandlerFactories.Where(hf => hf.Key == eventName)) + { + result.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + } + + return result; + } + + private List GetOrCreateAnonymousHandlerFactories(string eventName) + { + return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); + } + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { //Should trigger same type @@ -317,12 +372,21 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen InboxConfig inboxConfig) { var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); - if (eventType == null) + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + { + eventData = new AnonymousEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData, typeof(object))); + eventType = typeof(AnonymousEventData); + } + else { return; } - - var eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); var exceptions = new List(); using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) { diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs index 3668193c9b..ff9d587769 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs @@ -90,8 +90,8 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB await TriggerDistributedEventSentAsync(new DistributedEventSent() { Source = DistributedEventSource.Direct, - EventName = EventNameAttribute.GetNameOrDefault(eventType), - EventData = eventData + EventName = GetEventName(eventType, eventData), + EventData = GetEventData(eventData) }); } @@ -124,7 +124,8 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB if (outboxConfig.Selector == null || outboxConfig.Selector(eventType)) { var eventOutbox = (IEventOutbox)unitOfWork.ServiceProvider.GetRequiredService(outboxConfig.ImplementationType); - var eventName = EventNameAttribute.GetNameOrDefault(eventType); + var eventName = GetEventName(eventType, eventData); + eventData = GetEventData(eventData); await OnAddToOutboxAsync(eventName, eventType, eventData); @@ -181,8 +182,6 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB { if (await eventInbox.ExistsByMessageIdAsync(messageId!)) { - // Message already exists in the inbox, no need to add again. - // This can happen in case of retries from the sender side. addToInbox = true; continue; } @@ -212,8 +211,8 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB await TriggerDistributedEventReceivedAsync(new DistributedEventReceived { Source = DistributedEventSource.Direct, - EventName = EventNameAttribute.GetNameOrDefault(eventType), - EventData = eventData + EventName = GetEventName(eventType, eventData), + EventData = GetEventData(eventData) }); await TriggerHandlersAsync(eventType, eventData); @@ -224,8 +223,8 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB await TriggerDistributedEventReceivedAsync(new DistributedEventReceived { Source = DistributedEventSource.Inbox, - EventName = EventNameAttribute.GetNameOrDefault(eventType), - EventData = eventData + EventName = GetEventName(eventType, eventData), + EventData = GetEventData(eventData) }); await TriggerHandlersAsync(eventType, eventData, exceptions, inboxConfig); @@ -254,4 +253,29 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB // ignored } } + + protected virtual string GetEventName(Type eventType, object eventData) + { + if (eventData is AnonymousEventData anonymousEventData) + { + return anonymousEventData.EventName; + } + + return EventNameAttribute.GetNameOrDefault(eventType); + } + + protected virtual object GetEventData(object eventData) + { + if (eventData is AnonymousEventData anonymousEventData) + { + return anonymousEventData.ConvertToTypedObject(); + } + + return eventData; + } + + protected virtual (string EventName, object EventData) ResolveEventForPublishing(Type eventType, object eventData) + { + return (GetEventName(eventType, eventData), GetEventData(eventData)); + } } diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs index abcd2dbd05..3cb012b58b 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Reflection; using System.Text; using System.Text.Json; -using System.Text.Unicode; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -26,6 +25,8 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen { protected ConcurrentDictionary EventTypes { get; } + protected ConcurrentDictionary AnonymousEventNames { get; } + public LocalDistributedEventBus( IServiceScopeFactory serviceScopeFactory, ICurrentTenant currentTenant, @@ -47,6 +48,7 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen correlationIdProvider) { EventTypes = new ConcurrentDictionary(); + AnonymousEventNames = new ConcurrentDictionary(); Subscribe(abpDistributedEventBusOptions.Value.Handlers); } @@ -71,6 +73,12 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen } } + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + AnonymousEventNames.GetOrAdd(eventName, true); + return LocalEventBus.Subscribe(eventName, handler); + } + public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { var eventName = EventNameAttribute.GetNameOrDefault(eventType); @@ -93,6 +101,11 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen LocalEventBus.Unsubscribe(eventType, factory); } + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + LocalEventBus.Unsubscribe(eventName, factory); + } + public override void UnsubscribeAll(Type eventType) { LocalEventBus.UnsubscribeAll(eventType); @@ -120,15 +133,15 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen await TriggerDistributedEventSentAsync(new DistributedEventSent() { Source = DistributedEventSource.Direct, - EventName = EventNameAttribute.GetNameOrDefault(eventType), - EventData = eventData + EventName = GetEventName(eventType, eventData), + EventData = GetEventData(eventData) }); await TriggerDistributedEventReceivedAsync(new DistributedEventReceived { Source = DistributedEventSource.Direct, - EventName = EventNameAttribute.GetNameOrDefault(eventType), - EventData = eventData + EventName = GetEventName(eventType, eventData), + EventData = GetEventData(eventData) }); await PublishToEventBusAsync(eventType, eventData); @@ -137,12 +150,21 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) + + var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + } + + var isAnonymous = AnonymousEventNames.ContainsKey(eventName); + if (!isAnonymous) { throw new AbpException($"Unknown event name: {eventName}"); } - return PublishAsync(eventType, eventData, onUnitOfWorkComplete); + return PublishAsync(typeof(AnonymousEventData), new AnonymousEventData(eventName, eventData), onUnitOfWorkComplete); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) @@ -179,7 +201,13 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen var eventType = EventTypes.GetOrDefault(outgoingEvent.EventName); if (eventType == null) { - return; + var isAnonymous = AnonymousEventNames.ContainsKey(outgoingEvent.EventName); + if (!isAnonymous) + { + return; + } + + eventType = typeof(AnonymousEventData); } var eventData = JsonSerializer.Deserialize(Encoding.UTF8.GetString(outgoingEvent.EventData), eventType)!; @@ -204,7 +232,13 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); if (eventType == null) { - return; + var isAnonymous = AnonymousEventNames.ContainsKey(incomingEvent.EventName); + if (!isAnonymous) + { + return; + } + + eventType = typeof(AnonymousEventData); } var eventData = JsonSerializer.Deserialize(incomingEvent.EventData, eventType); @@ -226,7 +260,10 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { - EventTypes.GetOrAdd(eventName, eventType); + if (eventType != typeof(AnonymousEventData)) + { + EventTypes.GetOrAdd(eventName, eventType); + } return base.OnAddToOutboxAsync(eventName, eventType, eventData); } @@ -234,4 +271,14 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen { return LocalEventBus.GetEventHandlerFactories(eventType); } + + protected override IEnumerable GetAnonymousHandlerFactories(string eventName) + { + return LocalEventBus.GetAnonymousEventHandlerFactories(eventName); + } + + protected override Type? GetEventTypeByEventName(string eventName) + { + return EventTypes.GetOrDefault(eventName); + } } diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs index bf64839863..12a4b3235b 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; namespace Volo.Abp.EventBus.Distributed; @@ -12,6 +12,11 @@ public sealed class NullDistributedEventBus : IDistributedEventBus } + public Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) + { + return Task.CompletedTask; + } + public IDisposable Subscribe(Func action) where TEvent : class { return NullDisposable.Instance; @@ -32,6 +37,11 @@ public sealed class NullDistributedEventBus : IDistributedEventBus return NullDisposable.Instance; } + public IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + return NullDisposable.Instance; + } + public IDisposable Subscribe(IEventHandlerFactory factory) where TEvent : class { return NullDisposable.Instance; @@ -67,6 +77,11 @@ public sealed class NullDistributedEventBus : IDistributedEventBus } + public void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + + } + public void UnsubscribeAll() where TEvent : class { diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs index 42d35ad314..b5283f6208 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs @@ -57,6 +57,8 @@ public abstract class EventBusBase : IEventBus return Subscribe(eventType, new SingleInstanceHandlerFactory(handler)); } + public abstract IDisposable Subscribe(string eventName, IEventHandlerFactory handler); + /// public virtual IDisposable Subscribe(IEventHandlerFactory factory) where TEvent : class { @@ -83,6 +85,8 @@ public abstract class EventBusBase : IEventBus public abstract void Unsubscribe(Type eventType, IEventHandlerFactory factory); + public abstract void Unsubscribe(string eventName, IEventHandlerFactory factory); + /// public virtual void UnsubscribeAll() where TEvent : class { @@ -139,31 +143,68 @@ public abstract class EventBusBase : IEventBus { await new SynchronizationContextRemover(); - foreach (var handlerFactories in GetHandlerFactories(eventType).ToList()) + var (handlerFactoriesList, actualEventType) = ResolveHandlerFactories(eventType, eventData); + + foreach (var handlerFactories in handlerFactoriesList) { foreach (var handlerFactory in handlerFactories.EventHandlerFactories.ToList()) { - await TriggerHandlerAsync(handlerFactory, handlerFactories.EventType, eventData, exceptions, inboxConfig); + var resolvedEventData = ResolveEventDataForHandler(eventData, eventType, handlerFactories.EventType); + await TriggerHandlerAsync(handlerFactory, handlerFactories.EventType, resolvedEventData, exceptions, inboxConfig); } } - //Implements generic argument inheritance. See IEventDataWithInheritableGenericArgument - if (eventType.GetTypeInfo().IsGenericType && - eventType.GetGenericArguments().Length == 1 && - typeof(IEventDataWithInheritableGenericArgument).IsAssignableFrom(eventType)) + if (actualEventType != null && + actualEventType.GetTypeInfo().IsGenericType && + actualEventType.GetGenericArguments().Length == 1 && + typeof(IEventDataWithInheritableGenericArgument).IsAssignableFrom(actualEventType)) { - var genericArg = eventType.GetGenericArguments()[0]; + var resolvedEventData = eventData is AnonymousEventData aed + ? aed.ConvertToTypedObject(actualEventType) + : eventData; + + var genericArg = actualEventType.GetGenericArguments()[0]; var baseArg = genericArg.GetTypeInfo().BaseType; if (baseArg != null) { - var baseEventType = eventType.GetGenericTypeDefinition().MakeGenericType(baseArg); - var constructorArgs = ((IEventDataWithInheritableGenericArgument)eventData).GetConstructorArgs(); + var baseEventType = actualEventType.GetGenericTypeDefinition().MakeGenericType(baseArg); + var constructorArgs = ((IEventDataWithInheritableGenericArgument)resolvedEventData).GetConstructorArgs(); var baseEventData = Activator.CreateInstance(baseEventType, constructorArgs)!; await PublishToEventBusAsync(baseEventType, baseEventData); } } } + protected virtual (List Factories, Type? ActualEventType) ResolveHandlerFactories( + Type eventType, + object eventData) + { + if (eventData is AnonymousEventData anonymousEventData) + { + return ( + GetAnonymousHandlerFactories(anonymousEventData.EventName).ToList(), + GetEventTypeByEventName(anonymousEventData.EventName) + ); + } + + return (GetHandlerFactories(eventType).ToList(), eventType); + } + + protected virtual object ResolveEventDataForHandler(object eventData, Type sourceEventType, Type handlerEventType) + { + if (eventData is AnonymousEventData anonymousEventData && handlerEventType != typeof(AnonymousEventData)) + { + return anonymousEventData.ConvertToTypedObject(handlerEventType); + } + + if (handlerEventType == typeof(AnonymousEventData) && eventData is not AnonymousEventData) + { + return new AnonymousEventData(EventNameAttribute.GetNameOrDefault(sourceEventType), eventData); + } + + return eventData; + } + protected void ThrowOriginalExceptions(Type eventType, List exceptions) { if (exceptions.Count == 1) @@ -200,6 +241,10 @@ public abstract class EventBusBase : IEventBus protected abstract IEnumerable GetHandlerFactories(Type eventType); + protected abstract IEnumerable GetAnonymousHandlerFactories(string eventName); + + protected abstract Type? GetEventTypeByEventName(string eventName); + protected virtual async Task TriggerHandlerAsync(IEventHandlerFactory asyncHandlerFactory, Type eventType, object eventData, List exceptions, InboxConfig? inboxConfig = null) { diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs index f94d7a4ed1..4c58978b75 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs @@ -23,3 +23,22 @@ public class EventHandlerFactoryUnregistrar : IDisposable _eventBus.Unsubscribe(_eventType, _factory); } } + +public class AnonymousEventHandlerFactoryUnregistrar : IDisposable +{ + private readonly IEventBus _eventBus; + private readonly string _eventName; + private readonly IEventHandlerFactory _factory; + + public AnonymousEventHandlerFactoryUnregistrar(IEventBus eventBus, string eventName, IEventHandlerFactory factory) + { + _eventBus = eventBus; + _eventName = eventName; + _factory = factory; + } + + public void Dispose() + { + _eventBus.Unsubscribe(_eventName, _factory); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs index 8631eaf5cf..0d6fbace63 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs @@ -29,8 +29,10 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency protected AbpLocalEventBusOptions Options { get; } protected ConcurrentDictionary> HandlerFactories { get; } - - protected ConcurrentDictionary EventTypes { get; } + + protected ConcurrentDictionary EventTypes { get; } + + protected ConcurrentDictionary> AnonymousEventHandlerFactories { get; } public LocalEventBus( IOptions options, @@ -45,6 +47,7 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); + AnonymousEventHandlerFactories = new ConcurrentDictionary>(); SubscribeHandlers(Options.Handlers); } @@ -54,11 +57,24 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency return Subscribe(typeof(TEvent), handler); } + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => + { + if (!handler.IsInFactories(factories)) + { + factories.Add(handler); + } + }); + + return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + } + /// public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { EventTypes.GetOrAdd(EventNameAttribute.GetNameOrDefault(eventType), eventType); - + GetOrCreateHandlerFactories(eventType) .Locking(factories => { @@ -120,6 +136,11 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Remove(factory)); } + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + } + /// public override void UnsubscribeAll(Type eventType) { @@ -129,12 +150,21 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) + + var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + } + + var isAnonymous = AnonymousEventHandlerFactories.ContainsKey(eventName); + if (!isAnonymous) { throw new AbpException($"Unknown event name: {eventName}"); } - return PublishAsync(eventType, eventData, onUnitOfWorkComplete); + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); } protected override async Task PublishToEventBusAsync(Type eventType, object eventData) @@ -157,9 +187,16 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency return GetHandlerFactories(eventType).ToList(); } + public virtual List GetAnonymousEventHandlerFactories(string eventName) + { + return GetAnonymousHandlerFactories(eventName).ToList(); + } + protected override IEnumerable GetHandlerFactories(Type eventType) { var handlerFactoryList = new List>(); + var eventNames = EventTypes.Where(x => ShouldTriggerEventForHandler(eventType, x.Value)).Select(x => x.Key).ToList(); + foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) { foreach (var factory in handlerFactory.Value) @@ -171,23 +208,71 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency } } + foreach (var handlerFactory in AnonymousEventHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) + { + foreach (var factory in handlerFactory.Value) + { + handlerFactoryList.Add(new Tuple( + factory, + typeof(AnonymousEventData), + ReflectionHelper.GetAttributesOfMemberOrDeclaringType(factory.GetHandler().EventHandler.GetType()).FirstOrDefault()?.Order ?? 0)); + } + } + return handlerFactoryList.OrderBy(x => x.Item3).Select(x => new EventTypeWithEventHandlerFactories(x.Item2, new List {x.Item1})).ToArray(); } + protected override IEnumerable GetAnonymousHandlerFactories(string eventName) + { + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType != null) + { + return GetHandlerFactories(eventType); + } + + var handlerFactoryList = new List>(); + + foreach (var handlerFactory in AnonymousEventHandlerFactories.Where(aehf => aehf.Key == eventName)) + { + foreach (var factory in handlerFactory.Value) + { + using var handler = factory.GetHandler(); + var handlerType = handler.EventHandler.GetType(); + handlerFactoryList.Add(new Tuple( + factory, + typeof(AnonymousEventData), + ReflectionHelper + .GetAttributesOfMemberOrDeclaringType(handlerType) + .FirstOrDefault()?.Order ?? 0)); + } + } + + return handlerFactoryList.OrderBy(x => x.Item3).Select(x => + new EventTypeWithEventHandlerFactories(x.Item2, new List { x.Item1 })).ToArray(); + } + + protected override Type? GetEventTypeByEventName(string eventName) + { + return EventTypes.GetOrDefault(eventName); + } + private List GetOrCreateHandlerFactories(Type eventType) { return HandlerFactories.GetOrAdd(eventType, (type) => new List()); } + private List GetOrCreateAnonymousHandlerFactories(string eventName) + { + return AnonymousEventHandlerFactories.GetOrAdd(eventName, (name) => new List()); + } + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { - //Should trigger same type if (handlerEventType == targetEventType) { return true; } - //Should trigger for inherited types if (handlerEventType.IsAssignableFrom(targetEventType)) { return true; diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs index 3ffcd911ce..76e61a263e 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs @@ -13,6 +13,11 @@ public sealed class NullLocalEventBus : ILocalEventBus } + public Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) + { + return Task.CompletedTask; + } + public IDisposable Subscribe(Func action) where TEvent : class { return NullDisposable.Instance; @@ -28,6 +33,11 @@ public sealed class NullLocalEventBus : ILocalEventBus return new List(); } + public List GetAnonymousEventHandlerFactories(string eventName) + { + return new List(); + } + public IDisposable Subscribe() where TEvent : class where THandler : IEventHandler, new() { return NullDisposable.Instance; @@ -38,6 +48,11 @@ public sealed class NullLocalEventBus : ILocalEventBus return NullDisposable.Instance; } + public IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + return NullDisposable.Instance; + } + public IDisposable Subscribe(IEventHandlerFactory factory) where TEvent : class { return NullDisposable.Instance; @@ -73,6 +88,11 @@ public sealed class NullLocalEventBus : ILocalEventBus } + public void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + + } + public void UnsubscribeAll() where TEvent : class { diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs index 88c515f8dc..c306b6c37d 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Shouldly; using Volo.Abp.Domain.Entities.Events.Distributed; @@ -23,6 +24,184 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase Assert.Equal(3, MySimpleDistributedTransientEventHandler.DisposeCount); } + [Fact] + public async Task Should_Handle_Typed_Handler_When_Published_With_EventName() + { + DistributedEventBus.Subscribe(); + + var eventName = EventNameAttribute.GetNameOrDefault(); + await DistributedEventBus.PublishAsync(eventName, new MySimpleEventData(1)); + await DistributedEventBus.PublishAsync(eventName, new Dictionary() + { + {"Value", 2} + }); + await DistributedEventBus.PublishAsync(eventName, new { Value = 3 }); + + Assert.Equal(3, MySimpleDistributedTransientEventHandler.HandleCount); + Assert.Equal(3, MySimpleDistributedTransientEventHandler.DisposeCount); + } + + [Fact] + public async Task Should_Handle_Anonymous_Handler_When_Published_With_EventName() + { + var handleCount = 0; + + DistributedEventBus.Subscribe("MyEvent", + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + handleCount++; + await Task.CompletedTask; + }))); + + await DistributedEventBus.PublishAsync("MyEvent", new MySimpleEventData(1)); + await DistributedEventBus.PublishAsync("MyEvent", new Dictionary() + { + {"Value", 2} + }); + await DistributedEventBus.PublishAsync("MyEvent", new { Value = 3 }); + await DistributedEventBus.PublishAsync("MyEvent", new[] { 1, 2, 3 }); + + Assert.Equal(4, handleCount); + } + + [Fact] + public async Task Should_Handle_Anonymous_Handler_When_Published_With_AnonymousEventData() + { + var handleCount = 0; + + DistributedEventBus.Subscribe("MyEvent", + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + handleCount++; + d.ConvertToTypedObject().ShouldNotBeNull(); + await Task.CompletedTask; + }))); + + await DistributedEventBus.PublishAsync(new AnonymousEventData("MyEvent", new MySimpleEventData(1))); + await DistributedEventBus.PublishAsync(new AnonymousEventData("MyEvent", new Dictionary() + { + {"Value", 2} + })); + await DistributedEventBus.PublishAsync(new AnonymousEventData("MyEvent", new { Value = 3 })); + + Assert.Equal(3, handleCount); + } + + [Fact] + public async Task Should_Handle_Typed_Handler_When_Published_With_AnonymousEventData() + { + DistributedEventBus.Subscribe(); + + var eventName = EventNameAttribute.GetNameOrDefault(); + + await DistributedEventBus.PublishAsync(new AnonymousEventData(eventName, new MySimpleEventData(1))); + await DistributedEventBus.PublishAsync(new AnonymousEventData(eventName, new Dictionary() + { + {"Value", 2} + })); + await DistributedEventBus.PublishAsync(new AnonymousEventData(eventName, new { Value = 3 })); + + Assert.Equal(3, MySimpleDistributedTransientEventHandler.HandleCount); + } + + [Fact] + public async Task Should_Trigger_Both_Typed_And_Anonymous_Handlers_For_Typed_Event() + { + DistributedEventBus.Subscribe(); + + var eventName = EventNameAttribute.GetNameOrDefault(); + + var anonymousHandleCount = 0; + + DistributedEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + anonymousHandleCount++; + await Task.CompletedTask; + }))); + + await DistributedEventBus.PublishAsync(new MySimpleEventData(1)); + await DistributedEventBus.PublishAsync(new MySimpleEventData(2)); + await DistributedEventBus.PublishAsync(new MySimpleEventData(3)); + + Assert.Equal(3, MySimpleDistributedTransientEventHandler.HandleCount); + Assert.Equal(3, anonymousHandleCount); + } + + [Fact] + public async Task Should_Trigger_Both_Handlers_For_Mixed_Typed_And_Anonymous_Publish() + { + DistributedEventBus.Subscribe(); + + var eventName = EventNameAttribute.GetNameOrDefault(); + + var anonymousHandleCount = 0; + + DistributedEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + anonymousHandleCount++; + await Task.CompletedTask; + }))); + + await DistributedEventBus.PublishAsync(new MySimpleEventData(1)); + await DistributedEventBus.PublishAsync(new AnonymousEventData(eventName, new Dictionary() + { + {"Value", 2} + })); + await DistributedEventBus.PublishAsync(new AnonymousEventData(eventName, new { Value = 3 })); + + Assert.Equal(3, MySimpleDistributedTransientEventHandler.HandleCount); + Assert.Equal(3, anonymousHandleCount); + } + + [Fact] + public async Task Should_Unsubscribe_Anonymous_Handler() + { + var handleCount = 0; + + var handler = new ActionEventHandler(async (d) => + { + handleCount++; + await Task.CompletedTask; + }); + var factory = new SingleInstanceHandlerFactory(handler); + + var disposable = DistributedEventBus.Subscribe("MyEvent", factory); + + await DistributedEventBus.PublishAsync("MyEvent", new { Value = 1 }); + Assert.Equal(1, handleCount); + + disposable.Dispose(); + + await Assert.ThrowsAsync(() => + DistributedEventBus.PublishAsync("MyEvent", new { Value = 2 })); + Assert.Equal(1, handleCount); + } + + [Fact] + public async Task Should_Throw_For_Unknown_Event_Name() + { + await Assert.ThrowsAsync(() => + DistributedEventBus.PublishAsync("NonExistentEvent", new { Value = 1 })); + } + + [Fact] + public async Task Should_Convert_AnonymousEventData_To_Typed_Object() + { + MySimpleEventData? receivedData = null; + + DistributedEventBus.Subscribe(async (data) => + { + receivedData = data; + await Task.CompletedTask; + }); + + var eventName = EventNameAttribute.GetNameOrDefault(); + await DistributedEventBus.PublishAsync(eventName, new { Value = 42 }); + + receivedData.ShouldNotBeNull(); + receivedData.Value.ShouldBe(42); + } + [Fact] public async Task Should_Change_TenantId_If_EventData_Is_MultiTenant() { diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs new file mode 100644 index 0000000000..a0b3e12e61 --- /dev/null +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs @@ -0,0 +1,162 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace Volo.Abp.EventBus.Local; + +public class LocalEventBus_Anonymous_Test : EventBusTestBase +{ + [Fact] + public async Task Should_Handle_Anonymous_Handler_With_EventName() + { + var handleCount = 0; + + LocalEventBus.Subscribe("TestEvent", + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + handleCount++; + d.EventName.ShouldBe("TestEvent"); + await Task.CompletedTask; + }))); + + await LocalEventBus.PublishAsync("TestEvent", new { Value = 1 }); + await LocalEventBus.PublishAsync("TestEvent", new { Value = 2 }); + + handleCount.ShouldBe(2); + } + + [Fact] + public async Task Should_Handle_Typed_Handler_When_Published_With_EventName() + { + var handleCount = 0; + + LocalEventBus.Subscribe(async (data) => + { + handleCount++; + await Task.CompletedTask; + }); + + var eventName = EventNameAttribute.GetNameOrDefault(); + await LocalEventBus.PublishAsync(eventName, new MySimpleEventData(42)); + + handleCount.ShouldBe(1); + } + + [Fact] + public async Task Should_Convert_Dictionary_To_Typed_Handler() + { + MySimpleEventData? receivedData = null; + + LocalEventBus.Subscribe(async (data) => + { + receivedData = data; + await Task.CompletedTask; + }); + + var eventName = EventNameAttribute.GetNameOrDefault(); + await LocalEventBus.PublishAsync(eventName, new Dictionary + { + { "Value", 42 } + }); + + receivedData.ShouldNotBeNull(); + receivedData.Value.ShouldBe(42); + } + + [Fact] + public async Task Should_Trigger_Both_Typed_And_Anonymous_Handlers() + { + var typedHandleCount = 0; + var anonymousHandleCount = 0; + + LocalEventBus.Subscribe(async (data) => + { + typedHandleCount++; + await Task.CompletedTask; + }); + + var eventName = EventNameAttribute.GetNameOrDefault(); + + LocalEventBus.Subscribe(eventName, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + anonymousHandleCount++; + await Task.CompletedTask; + }))); + + await LocalEventBus.PublishAsync(new MySimpleEventData(1)); + + typedHandleCount.ShouldBe(1); + anonymousHandleCount.ShouldBe(1); + } + + [Fact] + public async Task Should_Unsubscribe_Anonymous_Handler() + { + var handleCount = 0; + + var handler = new ActionEventHandler(async (d) => + { + handleCount++; + await Task.CompletedTask; + }); + var factory = new SingleInstanceHandlerFactory(handler); + + var disposable = LocalEventBus.Subscribe("TestEvent", factory); + + await LocalEventBus.PublishAsync("TestEvent", new { Value = 1 }); + handleCount.ShouldBe(1); + + disposable.Dispose(); + + await Assert.ThrowsAsync(() => + LocalEventBus.PublishAsync("TestEvent", new { Value = 2 })); + handleCount.ShouldBe(1); + } + + [Fact] + public async Task Should_Throw_For_Unknown_Event_Name() + { + await Assert.ThrowsAsync(() => + LocalEventBus.PublishAsync("NonExistentEvent", new { Value = 1 })); + } + + [Fact] + public async Task Should_ConvertToTypedObject_In_Anonymous_Handler() + { + object? receivedData = null; + + LocalEventBus.Subscribe("TestEvent", + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + receivedData = d.ConvertToTypedObject(); + await Task.CompletedTask; + }))); + + await LocalEventBus.PublishAsync("TestEvent", new { Name = "Hello", Count = 42 }); + + receivedData.ShouldNotBeNull(); + var dict = receivedData.ShouldBeOfType>(); + dict["Name"].ShouldBe("Hello"); + dict["Count"].ShouldBe(42L); + } + + [Fact] + public async Task Should_ConvertToTypedObject_Generic_In_Anonymous_Handler() + { + MySimpleEventData? receivedData = null; + + LocalEventBus.Subscribe("TestEvent", + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + receivedData = d.ConvertToTypedObject(); + await Task.CompletedTask; + }))); + + await LocalEventBus.PublishAsync("TestEvent", new MySimpleEventData(99)); + + receivedData.ShouldNotBeNull(); + receivedData.Value.ShouldBe(99); + } +} From d7f30156916497c18b4943aeefc5f243d2fa1f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Wed, 4 Mar 2026 02:02:49 +0300 Subject: [PATCH 03/22] Use subscriptions and unique event names in tests Update event bus tests to avoid cross-test interference and ensure proper cleanup. In LocalDistributedEventBus_Test and LocalEventBus_Anonymous_Test: reset static handler state in test constructor, subscribe with IDisposable (using var subscription) so handlers are disposed after each test, replace hard-coded event names with generated unique event names, add missing System import, and adjust assertions (remove expectation of AbpException on publish after dispose). Also ensure local event bus subscriptions are stored/disposed. These changes make tests isolated and robust. --- .../LocalDistributedEventBus_Test.cs | 63 +++++++++++-------- .../Local/LocalEventBus_Anonymous_Test.cs | 36 ++++++----- 2 files changed, 56 insertions(+), 43 deletions(-) diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs index c306b6c37d..5ceb4b1bb5 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs @@ -11,10 +11,17 @@ namespace Volo.Abp.EventBus.Distributed; public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase { + public LocalDistributedEventBus_Test() + { + MySimpleDistributedTransientEventHandler.HandleCount = 0; + MySimpleDistributedTransientEventHandler.DisposeCount = 0; + MySimpleDistributedSingleInstanceEventHandler.TenantId = null; + } + [Fact] public async Task Should_Call_Handler_AndDispose() { - DistributedEventBus.Subscribe(); + using var subscription = DistributedEventBus.Subscribe(); await DistributedEventBus.PublishAsync(new MySimpleEventData(1)); await DistributedEventBus.PublishAsync(new MySimpleEventData(2)); @@ -27,7 +34,7 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase [Fact] public async Task Should_Handle_Typed_Handler_When_Published_With_EventName() { - DistributedEventBus.Subscribe(); + using var subscription = DistributedEventBus.Subscribe(); var eventName = EventNameAttribute.GetNameOrDefault(); await DistributedEventBus.PublishAsync(eventName, new MySimpleEventData(1)); @@ -45,21 +52,22 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase public async Task Should_Handle_Anonymous_Handler_When_Published_With_EventName() { var handleCount = 0; + var eventName = "MyEvent-" + Guid.NewGuid().ToString("N"); - DistributedEventBus.Subscribe("MyEvent", + using var subscription = DistributedEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { handleCount++; await Task.CompletedTask; }))); - await DistributedEventBus.PublishAsync("MyEvent", new MySimpleEventData(1)); - await DistributedEventBus.PublishAsync("MyEvent", new Dictionary() + await DistributedEventBus.PublishAsync(eventName, new MySimpleEventData(1)); + await DistributedEventBus.PublishAsync(eventName, new Dictionary() { {"Value", 2} }); - await DistributedEventBus.PublishAsync("MyEvent", new { Value = 3 }); - await DistributedEventBus.PublishAsync("MyEvent", new[] { 1, 2, 3 }); + await DistributedEventBus.PublishAsync(eventName, new { Value = 3 }); + await DistributedEventBus.PublishAsync(eventName, new[] { 1, 2, 3 }); Assert.Equal(4, handleCount); } @@ -68,8 +76,9 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase public async Task Should_Handle_Anonymous_Handler_When_Published_With_AnonymousEventData() { var handleCount = 0; + var eventName = "MyEvent-" + Guid.NewGuid().ToString("N"); - DistributedEventBus.Subscribe("MyEvent", + using var subscription = DistributedEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { handleCount++; @@ -77,12 +86,12 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase await Task.CompletedTask; }))); - await DistributedEventBus.PublishAsync(new AnonymousEventData("MyEvent", new MySimpleEventData(1))); - await DistributedEventBus.PublishAsync(new AnonymousEventData("MyEvent", new Dictionary() + await DistributedEventBus.PublishAsync(new AnonymousEventData(eventName, new MySimpleEventData(1))); + await DistributedEventBus.PublishAsync(new AnonymousEventData(eventName, new Dictionary() { {"Value", 2} })); - await DistributedEventBus.PublishAsync(new AnonymousEventData("MyEvent", new { Value = 3 })); + await DistributedEventBus.PublishAsync(new AnonymousEventData(eventName, new { Value = 3 })); Assert.Equal(3, handleCount); } @@ -90,7 +99,7 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase [Fact] public async Task Should_Handle_Typed_Handler_When_Published_With_AnonymousEventData() { - DistributedEventBus.Subscribe(); + using var subscription = DistributedEventBus.Subscribe(); var eventName = EventNameAttribute.GetNameOrDefault(); @@ -107,13 +116,13 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase [Fact] public async Task Should_Trigger_Both_Typed_And_Anonymous_Handlers_For_Typed_Event() { - DistributedEventBus.Subscribe(); + using var typedSubscription = DistributedEventBus.Subscribe(); var eventName = EventNameAttribute.GetNameOrDefault(); var anonymousHandleCount = 0; - DistributedEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + using var anonymousSubscription = DistributedEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { anonymousHandleCount++; await Task.CompletedTask; @@ -130,13 +139,13 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase [Fact] public async Task Should_Trigger_Both_Handlers_For_Mixed_Typed_And_Anonymous_Publish() { - DistributedEventBus.Subscribe(); + using var typedSubscription = DistributedEventBus.Subscribe(); var eventName = EventNameAttribute.GetNameOrDefault(); var anonymousHandleCount = 0; - DistributedEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + using var anonymousSubscription = DistributedEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { anonymousHandleCount++; await Task.CompletedTask; @@ -157,6 +166,7 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase public async Task Should_Unsubscribe_Anonymous_Handler() { var handleCount = 0; + var eventName = "MyEvent-" + Guid.NewGuid().ToString("N"); var handler = new ActionEventHandler(async (d) => { @@ -165,15 +175,14 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase }); var factory = new SingleInstanceHandlerFactory(handler); - var disposable = DistributedEventBus.Subscribe("MyEvent", factory); + var disposable = DistributedEventBus.Subscribe(eventName, factory); - await DistributedEventBus.PublishAsync("MyEvent", new { Value = 1 }); + await DistributedEventBus.PublishAsync(eventName, new { Value = 1 }); Assert.Equal(1, handleCount); disposable.Dispose(); - await Assert.ThrowsAsync(() => - DistributedEventBus.PublishAsync("MyEvent", new { Value = 2 })); + await DistributedEventBus.PublishAsync(eventName, new { Value = 2 }); Assert.Equal(1, handleCount); } @@ -189,7 +198,7 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase { MySimpleEventData? receivedData = null; - DistributedEventBus.Subscribe(async (data) => + using var subscription = DistributedEventBus.Subscribe(async (data) => { receivedData = data; await Task.CompletedTask; @@ -207,7 +216,7 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase { var tenantId = Guid.NewGuid(); - DistributedEventBus.Subscribe(GetRequiredService()); + using var subscription = DistributedEventBus.Subscribe(GetRequiredService()); await DistributedEventBus.PublishAsync(new MySimpleEventData(3, tenantId)); @@ -219,7 +228,7 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase { var tenantId = Guid.NewGuid(); - DistributedEventBus.Subscribe>(GetRequiredService()); + using var subscription = DistributedEventBus.Subscribe>(GetRequiredService()); await DistributedEventBus.PublishAsync(new MySimpleEventData(3, tenantId)); @@ -231,7 +240,7 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase { var tenantId = Guid.NewGuid(); - DistributedEventBus.Subscribe(GetRequiredService()); + using var subscription = DistributedEventBus.Subscribe(GetRequiredService()); await DistributedEventBus.PublishAsync(new MySimpleEto { @@ -249,10 +258,10 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase { var localEventBus = GetRequiredService(); - localEventBus.Subscribe(); - localEventBus.Subscribe(); + using var distributedEventSentSubscription = localEventBus.Subscribe(); + using var distributedEventReceivedSubscription = localEventBus.Subscribe(); - DistributedEventBus.Subscribe(); + using var subscription = DistributedEventBus.Subscribe(); using (var uow = GetRequiredService().Begin()) { diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs index a0b3e12e61..7d3e5748e9 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Threading.Tasks; using Shouldly; @@ -11,17 +12,18 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase public async Task Should_Handle_Anonymous_Handler_With_EventName() { var handleCount = 0; + var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); - LocalEventBus.Subscribe("TestEvent", + using var subscription = LocalEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { handleCount++; - d.EventName.ShouldBe("TestEvent"); + d.EventName.ShouldBe(eventName); await Task.CompletedTask; }))); - await LocalEventBus.PublishAsync("TestEvent", new { Value = 1 }); - await LocalEventBus.PublishAsync("TestEvent", new { Value = 2 }); + await LocalEventBus.PublishAsync(eventName, new { Value = 1 }); + await LocalEventBus.PublishAsync(eventName, new { Value = 2 }); handleCount.ShouldBe(2); } @@ -31,7 +33,7 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase { var handleCount = 0; - LocalEventBus.Subscribe(async (data) => + using var subscription = LocalEventBus.Subscribe(async (data) => { handleCount++; await Task.CompletedTask; @@ -48,7 +50,7 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase { MySimpleEventData? receivedData = null; - LocalEventBus.Subscribe(async (data) => + using var subscription = LocalEventBus.Subscribe(async (data) => { receivedData = data; await Task.CompletedTask; @@ -70,7 +72,7 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase var typedHandleCount = 0; var anonymousHandleCount = 0; - LocalEventBus.Subscribe(async (data) => + using var typedSubscription = LocalEventBus.Subscribe(async (data) => { typedHandleCount++; await Task.CompletedTask; @@ -78,7 +80,7 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase var eventName = EventNameAttribute.GetNameOrDefault(); - LocalEventBus.Subscribe(eventName, + using var anonymousSubscription = LocalEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { anonymousHandleCount++; @@ -95,6 +97,7 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase public async Task Should_Unsubscribe_Anonymous_Handler() { var handleCount = 0; + var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); var handler = new ActionEventHandler(async (d) => { @@ -103,15 +106,14 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase }); var factory = new SingleInstanceHandlerFactory(handler); - var disposable = LocalEventBus.Subscribe("TestEvent", factory); + var disposable = LocalEventBus.Subscribe(eventName, factory); - await LocalEventBus.PublishAsync("TestEvent", new { Value = 1 }); + await LocalEventBus.PublishAsync(eventName, new { Value = 1 }); handleCount.ShouldBe(1); disposable.Dispose(); - await Assert.ThrowsAsync(() => - LocalEventBus.PublishAsync("TestEvent", new { Value = 2 })); + await LocalEventBus.PublishAsync(eventName, new { Value = 2 }); handleCount.ShouldBe(1); } @@ -126,15 +128,16 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase public async Task Should_ConvertToTypedObject_In_Anonymous_Handler() { object? receivedData = null; + var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); - LocalEventBus.Subscribe("TestEvent", + using var subscription = LocalEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { receivedData = d.ConvertToTypedObject(); await Task.CompletedTask; }))); - await LocalEventBus.PublishAsync("TestEvent", new { Name = "Hello", Count = 42 }); + await LocalEventBus.PublishAsync(eventName, new { Name = "Hello", Count = 42 }); receivedData.ShouldNotBeNull(); var dict = receivedData.ShouldBeOfType>(); @@ -146,15 +149,16 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase public async Task Should_ConvertToTypedObject_Generic_In_Anonymous_Handler() { MySimpleEventData? receivedData = null; + var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); - LocalEventBus.Subscribe("TestEvent", + using var subscription = LocalEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { receivedData = d.ConvertToTypedObject(); await Task.CompletedTask; }))); - await LocalEventBus.PublishAsync("TestEvent", new MySimpleEventData(99)); + await LocalEventBus.PublishAsync(eventName, new MySimpleEventData(99)); receivedData.ShouldNotBeNull(); receivedData.Value.ShouldBe(99); From c67b596506eca03743d9cd8396910be6e59ca15e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Wed, 4 Mar 2026 12:13:59 +0300 Subject: [PATCH 04/22] Use ABP Serializer for anonymous event data Replace direct System.Text.Json usage with the ABP Serializer for anonymous event payloads (deserialize to object) and remove the unused System.Text.Json using. Rework Subscribe(string, IEventHandlerFactory) to avoid duplicate handler registration, return a NullDisposable when already registered, add the consumer binding when the first anonymous handler is added (note: TODO for multi-threading), and keep the new unregistrar. Prevent AnonymousEventData from being added to EventTypes when adding to the outbox. Remove the old Subscribe implementation accordingly. --- .../RabbitMq/RabbitMqDistributedEventBus.cs | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs index 928c99762d..b34e97d62b 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -112,9 +111,8 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis } else if (AnonymousHandlerFactories.ContainsKey(eventName)) { - var jsonElement = JsonSerializer.Deserialize(ea.Body.ToArray()); - eventData = new AnonymousEventData(eventName, jsonElement); eventType = typeof(AnonymousEventData); + eventData = new AnonymousEventData(eventName, Serializer.Deserialize(ea.Body.ToArray())); } else { @@ -151,6 +149,25 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis return new EventHandlerFactoryUnregistrar(this, eventType, factory); } + + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); + + if (handler.IsInFactories(handlerFactories)) + { + return NullDisposable.Instance; + } + + handlerFactories.Add(handler); + + if (handlerFactories.Count == 1) //TODO: Multi-threading! + { + Consumer.BindAsync(eventName); + } + + return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + } /// public override void Unsubscribe(Func action) @@ -218,7 +235,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis if (AnonymousHandlerFactories.ContainsKey(eventName)) { - return PublishAsync(typeof(AnonymousEventData), new AnonymousEventData(eventName, eventData), onUnitOfWorkComplete); + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); } throw new AbpException($"Unknown event name: {eventName}"); @@ -295,8 +312,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis } else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) { - var jsonElement = JsonSerializer.Deserialize(incomingEvent.EventData); - eventData = new AnonymousEventData(incomingEvent.EventName, jsonElement); + eventData = new AnonymousEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData)); eventType = typeof(AnonymousEventData); } else @@ -423,7 +439,10 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { - EventTypes.GetOrAdd(eventName, eventType); + if (typeof(AnonymousEventData) != eventType) + { + EventTypes.GetOrAdd(eventName, eventType); + } return base.OnAddToOutboxAsync(eventName, eventType, eventData); } @@ -459,19 +478,6 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis return EventTypes.GetOrDefault(eventName); } - public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) - { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => - { - if (!handler.IsInFactories(factories)) - { - factories.Add(handler); - } - }); - - return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); - } - public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); From aae1e330ba04baf90e06a9ae7aa86d2c626e66ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Wed, 4 Mar 2026 12:42:15 +0300 Subject: [PATCH 05/22] Anonymous subscriptions and deserialization fixes Add and centralize Subscribe/Unsubscribe(string, IEventHandlerFactory) implementations for Azure and Kafka to avoid duplicate anonymous handler registrations (checks IsInFactories / returns NullDisposable or skips adding). Switch Kafka anonymous payload handling from JsonElement to the generic Serializer.Deserialize to preserve original types. Refactor RabbitMQ handler resolution to include anonymous handler factories by matching event names and return handler list early when concrete event type is found. Update DistributedEventBusBase to use ResolveEventForPublishing to obtain event name and data together, and ensure GetEventData is applied at the correct point when processing incoming events. --- .../Azure/AzureDistributedEventBus.cs | 38 ++++++++-------- .../Kafka/KafkaDistributedEventBus.cs | 44 +++++++++---------- .../RabbitMq/RabbitMqDistributedEventBus.cs | 16 ++++--- .../Distributed/DistributedEventBusBase.cs | 5 ++- 4 files changed, 55 insertions(+), 48 deletions(-) diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index 8cf42cc796..cb85a7bf41 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -221,6 +221,20 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen return new EventHandlerFactoryUnregistrar(this, eventType, factory); } + + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); + + if (handler.IsInFactories(handlerFactories)) + { + return NullDisposable.Instance; + } + + handlerFactories.Add(handler); + + return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + } public override void Unsubscribe(Func action) { @@ -267,6 +281,12 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen GetOrCreateHandlerFactories(eventType) .Locking(factories => factories.Remove(factory)); } + + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => factories.Remove(factory)); + } public override void UnsubscribeAll(Type eventType) { @@ -349,24 +369,6 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen return EventTypes.GetOrDefault(eventName); } - public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) - { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => - { - if (!handler.IsInFactories(factories)) - { - factories.Add(handler); - } - }); - - return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); - } - - public override void Unsubscribe(string eventName, IEventHandlerFactory factory) - { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); - } - protected override IEnumerable GetAnonymousHandlerFactories(string eventName) { var result = new List(); diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs index 17d4bccc12..0be4e22bf6 100644 --- a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs @@ -94,8 +94,8 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen } else if (AnonymousHandlerFactories.ContainsKey(eventName)) { - var jsonElement = JsonSerializer.Deserialize(message.Value); - eventData = new AnonymousEventData(eventName, jsonElement); + var element = Serializer.Deserialize(message.Value); + eventData = new AnonymousEventData(eventName, element); eventType = typeof(AnonymousEventData); } else @@ -127,6 +127,19 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen return new EventHandlerFactoryUnregistrar(this, eventType, factory); } + + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => + { + if (!handler.IsInFactories(factories)) + { + factories.Add(handler); + } + }); + + return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + } /// public override void Unsubscribe(Func action) @@ -175,6 +188,11 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen { GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Remove(factory)); } + + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + } /// public override void UnsubscribeAll(Type eventType) @@ -318,8 +336,8 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen } else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) { - var jsonElement = JsonSerializer.Deserialize(incomingEvent.EventData); - eventData = new AnonymousEventData(incomingEvent.EventName, jsonElement); + var element = Serializer.Deserialize(incomingEvent.EventData); + eventData = new AnonymousEventData(incomingEvent.EventName, element); eventType = typeof(AnonymousEventData); } else @@ -410,24 +428,6 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen return EventTypes.GetOrDefault(eventName); } - public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) - { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => - { - if (!handler.IsInFactories(factories)) - { - factories.Add(handler); - } - }); - - return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); - } - - public override void Unsubscribe(string eventName, IEventHandlerFactory factory) - { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); - } - protected override IEnumerable GetAnonymousHandlerFactories(string eventName) { var result = new List(); diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs index b34e97d62b..31207da4ab 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs @@ -458,16 +458,20 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis } ); } - + protected override IEnumerable GetHandlerFactories(Type eventType) { var handlerFactoryList = new List(); + var eventNames = EventTypes.Where(x => ShouldTriggerEventForHandler(eventType, x.Value)).Select(x => x.Key).ToList(); + + foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) + { + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + } - foreach (var handlerFactory in - HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) + foreach (var handlerFactory in AnonymousHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) { - handlerFactoryList.Add( - new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); } return handlerFactoryList.ToArray(); @@ -490,7 +494,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis var eventType = GetEventTypeByEventName(eventName); if (eventType != null) { - result.AddRange(GetHandlerFactories(eventType)); + return GetHandlerFactories(eventType); } foreach (var handlerFactory in AnonymousHandlerFactories.Where(hf => hf.Key == eventName)) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs index ff9d587769..9dca5a7466 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs @@ -124,8 +124,7 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB if (outboxConfig.Selector == null || outboxConfig.Selector(eventType)) { var eventOutbox = (IEventOutbox)unitOfWork.ServiceProvider.GetRequiredService(outboxConfig.ImplementationType); - var eventName = GetEventName(eventType, eventData); - eventData = GetEventData(eventData); + (var eventName, eventData) = ResolveEventForPublishing(eventType, eventData); await OnAddToOutboxAsync(eventName, eventType, eventData); @@ -186,6 +185,8 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB continue; } } + + eventData = GetEventData(eventData); var incomingEventInfo = new IncomingEventInfo( GuidGenerator.Create(), From 7672598bee1792c0ef5db29eef2b6ba87105aad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Wed, 4 Mar 2026 13:48:02 +0300 Subject: [PATCH 06/22] Add anonymous name-based event support Introduce AnonymousEventData and add name-based (string) event publish/subscribe APIs across the event bus. Key changes: - New AnonymousEventData class with conversion helpers (ConvertToTypedObject/ConvertToTypedObject/ConvertToTypedObject(Type)) and caching for JsonElement payloads. - Extended IEventBus and IDistributedEventBus interfaces with PublishAsync(string, ...), Subscribe/Unsubscribe/UnsubscribeAll overloads that accept string event names and factories/handlers. - DistributedEventBusBase implements name-based PublishAsync and Subscribe overloads and adapts anonymous event publishing to typed flows. - Updated concrete distributed bus implementations (Azure, Dapr, Kafka, RabbitMQ, Rebus, Local) to support anonymous handlers, inbox/outbox processing for anonymous events, serialization helpers, and handler-factory management. Changes include deduplication when registering factories, removal helpers for single-instance handlers, and preserving EventTypes mapping (AnonymousEventData is not added to EventTypes). - Fixed/centralized logic for mapping event names <-> event types, handler lookup (including anonymous handlers), and outbox/inbox processing so both typed and anonymous (name-based) events are handled consistently. Compatibility: existing typed event handling is preserved; new string-based APIs allow publishing and subscribing to events identified only by name. --- .../Volo/Abp/EventBus/AnonymousEventData.cs | 37 +++ .../Distributed/IDistributedEventBus.cs | 42 ++- .../Volo/Abp/EventBus/IEventBus.cs | 40 +++ .../Volo/Abp/EventBus/Local/ILocalEventBus.cs | 5 + .../Azure/AzureDistributedEventBus.cs | 291 +++++++++-------- .../EventBus/Dapr/DaprDistributedEventBus.cs | 187 +++++++---- .../Kafka/KafkaDistributedEventBus.cs | 67 ++-- .../RabbitMq/RabbitMqDistributedEventBus.cs | 25 +- .../Rebus/RebusDistributedEventBus.cs | 300 ++++++++++-------- .../Distributed/DistributedEventBusBase.cs | 28 ++ .../Distributed/LocalDistributedEventBus.cs | 32 +- .../Distributed/NullDistributedEventBus.cs | 29 ++ .../Volo/Abp/EventBus/EventBusBase.cs | 15 + .../EventHandlerFactoryUnregistrar.cs | 3 + .../Volo/Abp/EventBus/Local/LocalEventBus.cs | 24 ++ .../Abp/EventBus/Local/NullLocalEventBus.cs | 22 +- 16 files changed, 796 insertions(+), 351 deletions(-) diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs index c4b5d1c3fd..1db318b40e 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs @@ -5,25 +5,54 @@ using System.Text.Json; namespace Volo.Abp.EventBus; +/// +/// Wraps arbitrary event data with a string-based event name for anonymous (type-less) event handling. +/// Acts as both an envelope and event type for events that are identified by name rather than CLR type. +/// [Serializable] public class AnonymousEventData { + /// + /// The string-based name that identifies the event. + /// public string EventName { get; } + + /// + /// The raw event data payload. Can be a CLR object, , or any serializable object. + /// public object Data { get; } private JsonElement? _cachedJsonElement; + /// + /// Creates a new instance of . + /// + /// The string-based name that identifies the event + /// The raw event data payload public AnonymousEventData(string eventName, object data) { EventName = eventName; Data = data; } + /// + /// Converts the to a loosely-typed object graph + /// (dictionaries for objects, lists for arrays, primitives for values). + /// + /// A CLR object representation of the event data public object ConvertToTypedObject() { return ConvertElement(GetJsonElement()); } + /// + /// Converts the to a strongly-typed object. + /// Returns the data directly if it is already of type , + /// otherwise deserializes from JSON. + /// + /// Target type to convert to + /// The deserialized object of type + /// Thrown when deserialization fails public T ConvertToTypedObject() { if (Data is T typedData) @@ -35,6 +64,14 @@ public class AnonymousEventData ?? throw new InvalidOperationException($"Failed to deserialize AnonymousEventData to {typeof(T).FullName}."); } + /// + /// Converts the to the specified . + /// Returns the data directly if it is already an instance of the target type, + /// otherwise deserializes from JSON. + /// + /// Target type to convert to + /// The deserialized object + /// Thrown when deserialization fails public object ConvertToTypedObject(Type type) { if (type.IsInstanceOfType(Data)) diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IDistributedEventBus.cs index c84855e0ea..99dfe243b4 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IDistributedEventBus.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; namespace Volo.Abp.EventBus.Distributed; @@ -14,15 +14,55 @@ public interface IDistributedEventBus : IEventBus IDisposable Subscribe(IDistributedEventHandler handler) where TEvent : class; + /// + /// Triggers an event. + /// + /// Event type + /// Related data for the event + /// True, to publish the event at the end of the current unit of work, if available + /// True, to use the outbox pattern for reliable event publishing + /// The task to handle async operation Task PublishAsync( TEvent eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) where TEvent : class; + /// + /// Triggers an event. + /// + /// Event type + /// Related data for the event + /// True, to publish the event at the end of the current unit of work, if available + /// True, to use the outbox pattern for reliable event publishing + /// The task to handle async operation Task PublishAsync( Type eventType, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true); + + /// + /// Registers to an event by its string-based event name. + /// Same (given) instance of the handler is used for all event occurrences. + /// Wraps the handler as . + /// + /// Name of the event + /// Object to handle the event + IDisposable Subscribe(string eventName, IDistributedEventHandler handler); + + /// + /// Triggers an event by its string-based event name. + /// Used for anonymous (type-less) event publishing over distributed event bus. + /// + /// Name of the event + /// Related data for the event + /// True, to publish the event at the end of the current unit of work, if available + /// True, to use the outbox pattern for reliable event publishing + /// The task to handle async operation + Task PublishAsync( + string eventName, + object eventData, + bool onUnitOfWorkComplete = true, + bool useOutbox = true); } diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs index fa17528750..9c38b5e7dd 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs @@ -24,6 +24,14 @@ public interface IEventBus /// The task to handle async operation Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true); + /// + /// Triggers an event by its string-based event name. + /// Used for anonymous (type-less) event publishing. + /// + /// Name of the event + /// Related data for the event + /// True, to publish the event at the end of the current unit of work, if available + /// The task to handle async operation Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true); /// @@ -53,6 +61,20 @@ public interface IEventBus /// Object to handle the event IDisposable Subscribe(Type eventType, IEventHandler handler); + /// + /// Registers to an event by its string-based event name. + /// Same (given) instance of the handler is used for all event occurrences. + /// + /// Name of the event + /// Object to handle the event + IDisposable Subscribe(string eventName, IEventHandler handler); + + /// + /// Registers to an event by its string-based event name. + /// Given factory is used to create/release handlers. + /// + /// Name of the event + /// A factory to create/release handlers IDisposable Subscribe(string eventName, IEventHandlerFactory handler); /// @@ -109,8 +131,20 @@ public interface IEventBus /// Factory object that is registered before void Unsubscribe(Type eventType, IEventHandlerFactory factory); + /// + /// Unregisters from an event by its string-based event name. + /// + /// Name of the event + /// Factory object that is registered before void Unsubscribe(string eventName, IEventHandlerFactory factory); + /// + /// Unregisters from an event by its string-based event name. + /// + /// Name of the event + /// Handler object that is registered before + void Unsubscribe(string eventName, IEventHandler handler); + /// /// Unregisters all event handlers of given event type. /// @@ -123,4 +157,10 @@ public interface IEventBus /// /// Event type void UnsubscribeAll(Type eventType); + + /// + /// Unregisters all event handlers of given string-based event name. + /// + /// Name of the event + void UnsubscribeAll(string eventName); } \ No newline at end of file diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventBus.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventBus.cs index d816d61c14..dafa8d4a4d 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventBus.cs @@ -24,5 +24,10 @@ public interface ILocalEventBus : IEventBus /// List GetEventHandlerFactories(Type eventType); + /// + /// Gets the list of event handler factories for the given string-based event name. + /// + /// Name of the event + /// List of event handler factories registered for the given event name List GetAnonymousEventHandlerFactories(string eventName); } diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index cb85a7bf41..020757ad07 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -114,100 +114,6 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen } } - public async override Task PublishFromOutboxAsync(OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig) - { - await PublishAsync(outgoingEvent.EventName, outgoingEvent.EventData, outgoingEvent.GetCorrelationId(), outgoingEvent.Id); - - using (CorrelationIdProvider.Change(outgoingEvent.GetCorrelationId())) - { - await TriggerDistributedEventSentAsync(new DistributedEventSent() - { - Source = DistributedEventSource.Outbox, - EventName = outgoingEvent.EventName, - EventData = outgoingEvent.EventData - }); - } - } - - public async override Task PublishManyFromOutboxAsync(IEnumerable outgoingEvents, OutboxConfig outboxConfig) - { - var outgoingEventArray = outgoingEvents.ToArray(); - - var publisher = await PublisherPool.GetAsync( - Options.TopicName, - Options.ConnectionName); - - using var messageBatch = await publisher.CreateMessageBatchAsync(); - - foreach (var outgoingEvent in outgoingEventArray) - { - var message = new ServiceBusMessage(outgoingEvent.EventData) { Subject = outgoingEvent.EventName }; - - if (message.MessageId.IsNullOrWhiteSpace()) - { - message.MessageId = outgoingEvent.Id.ToString(); - } - - message.CorrelationId = outgoingEvent.GetCorrelationId(); - - if (!messageBatch.TryAddMessage(message)) - { - throw new AbpException( - "The message is too large to fit in the batch. Set AbpEventBusBoxesOptions.OutboxWaitingEventMaxCount to reduce the number"); - } - } - - await publisher.SendMessagesAsync(messageBatch); - - foreach (var outgoingEvent in outgoingEventArray) - { - using (CorrelationIdProvider.Change(outgoingEvent.GetCorrelationId())) - { - await TriggerDistributedEventSentAsync(new DistributedEventSent() - { - Source = DistributedEventSource.Outbox, - EventName = outgoingEvent.EventName, - EventData = outgoingEvent.EventData - }); - } - } - } - - public async override Task ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig) - { - var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); - object eventData; - - if (eventType != null) - { - eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); - } - else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) - { - var element = Serializer.Deserialize(incomingEvent.EventData); - eventData = new AnonymousEventData(incomingEvent.EventName, element); - eventType = typeof(AnonymousEventData); - } - else - { - return; - } - var exceptions = new List(); - using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) - { - await TriggerHandlersFromInboxAsync(eventType, eventData, exceptions, inboxConfig); - } - if (exceptions.Any()) - { - ThrowOriginalExceptions(eventType, exceptions); - } - } - - protected override byte[] Serialize(object eventData) - { - return Serializer.Serialize(eventData); - } - public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { var handlerFactories = GetOrCreateHandlerFactories(eventType); @@ -221,16 +127,17 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen return new EventHandlerFactoryUnregistrar(this, eventType, factory); } - + + /// public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); - + if (handler.IsInFactories(handlerFactories)) { return NullDisposable.Instance; } - + handlerFactories.Add(handler); return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); @@ -281,19 +188,15 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen GetOrCreateHandlerFactories(eventType) .Locking(factories => factories.Remove(factory)); } - - public override void Unsubscribe(string eventName, IEventHandlerFactory factory) - { - GetOrCreateAnonymousHandlerFactories(eventName) - .Locking(factories => factories.Remove(factory)); - } + /// public override void UnsubscribeAll(Type eventType) { GetOrCreateHandlerFactories(eventType) .Locking(factories => factories.Clear()); } + /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); @@ -323,6 +226,100 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen unitOfWork.AddOrReplaceDistributedEvent(eventRecord); } + public async override Task PublishFromOutboxAsync(OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig) + { + await PublishAsync(outgoingEvent.EventName, outgoingEvent.EventData, outgoingEvent.GetCorrelationId(), outgoingEvent.Id); + + using (CorrelationIdProvider.Change(outgoingEvent.GetCorrelationId())) + { + await TriggerDistributedEventSentAsync(new DistributedEventSent() + { + Source = DistributedEventSource.Outbox, + EventName = outgoingEvent.EventName, + EventData = outgoingEvent.EventData + }); + } + } + + public async override Task PublishManyFromOutboxAsync(IEnumerable outgoingEvents, OutboxConfig outboxConfig) + { + var outgoingEventArray = outgoingEvents.ToArray(); + + var publisher = await PublisherPool.GetAsync( + Options.TopicName, + Options.ConnectionName); + + using var messageBatch = await publisher.CreateMessageBatchAsync(); + + foreach (var outgoingEvent in outgoingEventArray) + { + var message = new ServiceBusMessage(outgoingEvent.EventData) { Subject = outgoingEvent.EventName }; + + if (message.MessageId.IsNullOrWhiteSpace()) + { + message.MessageId = outgoingEvent.Id.ToString(); + } + + message.CorrelationId = outgoingEvent.GetCorrelationId(); + + if (!messageBatch.TryAddMessage(message)) + { + throw new AbpException( + "The message is too large to fit in the batch. Set AbpEventBusBoxesOptions.OutboxWaitingEventMaxCount to reduce the number"); + } + } + + await publisher.SendMessagesAsync(messageBatch); + + foreach (var outgoingEvent in outgoingEventArray) + { + using (CorrelationIdProvider.Change(outgoingEvent.GetCorrelationId())) + { + await TriggerDistributedEventSentAsync(new DistributedEventSent() + { + Source = DistributedEventSource.Outbox, + EventName = outgoingEvent.EventName, + EventData = outgoingEvent.EventData + }); + } + } + } + + public async override Task ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig) + { + var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + { + var element = Serializer.Deserialize(incomingEvent.EventData); + eventData = new AnonymousEventData(incomingEvent.EventName, element); + eventType = typeof(AnonymousEventData); + } + else + { + return; + } + var exceptions = new List(); + using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) + { + await TriggerHandlersFromInboxAsync(eventType, eventData, exceptions, inboxConfig); + } + if (exceptions.Any()) + { + ThrowOriginalExceptions(eventType, exceptions); + } + } + + protected override byte[] Serialize(object eventData) + { + return Serializer.Serialize(eventData); + } + protected virtual Task PublishAsync(string eventName, object eventData) { var body = Serializer.Serialize(eventData); @@ -355,13 +352,44 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen await publisher.SendMessageAsync(message); } + protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) + { + if (typeof(AnonymousEventData) != eventType) + { + EventTypes.GetOrAdd(eventName, eventType); + } + return base.OnAddToOutboxAsync(eventName, eventType, eventData); + } + + private List GetOrCreateHandlerFactories(Type eventType) + { + return HandlerFactories.GetOrAdd( + eventType, + type => + { + var eventName = EventNameAttribute.GetNameOrDefault(type); + EventTypes.GetOrAdd(eventName, eventType); + return new List(); + } + ); + } + protected override IEnumerable GetHandlerFactories(Type eventType) { - return HandlerFactories - .Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key)) - .Select(handlerFactory => - new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)) - .ToArray(); + var handlerFactoryList = new List(); + var eventNames = EventTypes.Where(x => ShouldTriggerEventForHandler(eventType, x.Value)).Select(x => x.Key).ToList(); + + foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) + { + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + } + + foreach (var handlerFactory in AnonymousHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) + { + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + } + + return handlerFactoryList.ToArray(); } protected override Type? GetEventTypeByEventName(string eventName) @@ -369,6 +397,34 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen return EventTypes.GetOrDefault(eventName); } + /// + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => factories.Remove(factory)); + } + + /// + public override void Unsubscribe(string eventName, IEventHandler handler) + { + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); + } + + /// + public override void UnsubscribeAll(string eventName) + { + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => factories.Clear()); + } + protected override IEnumerable GetAnonymousHandlerFactories(string eventName) { var result = new List(); @@ -396,23 +452,4 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen { return handlerEventType == targetEventType || handlerEventType.IsAssignableFrom(targetEventType); } - - protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) - { - EventTypes.GetOrAdd(eventName, eventType); - return base.OnAddToOutboxAsync(eventName, eventType, eventData); - } - - private List GetOrCreateHandlerFactories(Type eventType) - { - return HandlerFactories.GetOrAdd( - eventType, - type => - { - var eventName = EventNameAttribute.GetNameOrDefault(type); - EventTypes.GetOrAdd(eventName, eventType); - return new List(); - } - ); - } } diff --git a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs index f036514115..62789a8408 100644 --- a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs @@ -81,6 +81,21 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend return new EventHandlerFactoryUnregistrar(this, eventType, factory); } + /// + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); + + if (handler.IsInFactories(handlerFactories)) + { + return NullDisposable.Instance; + } + + handlerFactories.Add(handler); + + return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + } + public override void Unsubscribe(Func action) { Check.NotNull(action, nameof(action)); @@ -131,6 +146,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear()); } + /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); @@ -143,7 +159,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend if (AnonymousHandlerFactories.ContainsKey(eventName)) { - return PublishAsync(typeof(AnonymousEventData), new AnonymousEventData(eventName, eventData), onUnitOfWorkComplete); + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); } throw new AbpException($"Unknown event name: {eventName}"); @@ -160,73 +176,25 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend unitOfWork.AddOrReplaceDistributedEvent(eventRecord); } - protected override IEnumerable GetHandlerFactories(Type eventType) - { - var handlerFactoryList = new List(); - - foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) - { - handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); - } - - return handlerFactoryList.ToArray(); - } - - protected override Type? GetEventTypeByEventName(string eventName) - { - return EventTypes.GetOrDefault(eventName); - } - - public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) - { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => - { - if (!handler.IsInFactories(factories)) - { - factories.Add(handler); - } - }); - - return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); - } - - public override void Unsubscribe(string eventName, IEventHandlerFactory factory) - { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); - } - - protected override IEnumerable GetAnonymousHandlerFactories(string eventName) + public async override Task PublishFromOutboxAsync(OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig) { - var result = new List(); + var eventType = EventTypes.GetOrDefault(outgoingEvent.EventName); + object eventData; - var eventType = GetEventTypeByEventName(eventName); if (eventType != null) { - result.AddRange(GetHandlerFactories(eventType)); + eventData = Serializer.Deserialize(outgoingEvent.EventData, eventType); } - - foreach (var handlerFactory in AnonymousHandlerFactories.Where(hf => hf.Key == eventName)) + else if (AnonymousHandlerFactories.ContainsKey(outgoingEvent.EventName)) { - result.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + eventData = Serializer.Deserialize(outgoingEvent.EventData, typeof(object)); } - - return result; - } - - private List GetOrCreateAnonymousHandlerFactories(string eventName) - { - return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); - } - - public async override Task PublishFromOutboxAsync(OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig) - { - var eventType = GetEventType(outgoingEvent.EventName); - if (eventType == null) + else { return; } - await PublishToDaprAsync(outgoingEvent.EventName, Serializer.Deserialize(outgoingEvent.EventData, eventType), outgoingEvent.Id, outgoingEvent.GetCorrelationId()); + await PublishToDaprAsync(outgoingEvent.EventName, eventData, outgoingEvent.Id, outgoingEvent.GetCorrelationId()); using (CorrelationIdProvider.Change(outgoingEvent.GetCorrelationId())) { @@ -262,13 +230,23 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public async override Task ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { - var eventType = GetEventType(incomingEvent.EventName); - if (eventType == null) + var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + { + eventData = new AnonymousEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData, typeof(object))); + eventType = typeof(AnonymousEventData); + } + else { return; } - var eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); var exceptions = new List(); using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) { @@ -285,9 +263,24 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend return Serializer.Serialize(eventData); } + protected virtual async Task PublishToDaprAsync(Type eventType, object eventData, Guid? messageId = null, string? correlationId = null) + { + await PublishToDaprAsync(EventNameAttribute.GetNameOrDefault(eventType), eventData, messageId, correlationId); + } + + protected virtual async Task PublishToDaprAsync(string eventName, object eventData, Guid? messageId = null, string? correlationId = null) + { + var client = await DaprClientFactory.CreateAsync(); + var data = new AbpDaprEventData(DaprEventBusOptions.PubSubName, eventName, (messageId ?? GuidGenerator.Create()).ToString("N"), Serializer.SerializeToString(eventData), correlationId); + await client.PublishEventAsync(pubsubName: DaprEventBusOptions.PubSubName, topicName: eventName, data: data); + } + protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { - EventTypes.GetOrAdd(eventName, eventType); + if (typeof(AnonymousEventData) != eventType) + { + EventTypes.GetOrAdd(eventName, eventType); + } return base.OnAddToOutboxAsync(eventName, eventType, eventData); } @@ -304,21 +297,81 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend ); } + protected override IEnumerable GetHandlerFactories(Type eventType) + { + var handlerFactoryList = new List(); + var eventNames = EventTypes.Where(x => ShouldTriggerEventForHandler(eventType, x.Value)).Select(x => x.Key).ToList(); + + foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) + { + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + } + + foreach (var handlerFactory in AnonymousHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) + { + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + } + + return handlerFactoryList.ToArray(); + } + + protected override Type? GetEventTypeByEventName(string eventName) + { + return EventTypes.GetOrDefault(eventName); + } + public Type? GetEventType(string eventName) { return EventTypes.GetOrDefault(eventName); } - protected virtual async Task PublishToDaprAsync(Type eventType, object eventData, Guid? messageId = null, string? correlationId = null) + /// + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - await PublishToDaprAsync(EventNameAttribute.GetNameOrDefault(eventType), eventData, messageId, correlationId); + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); } - protected virtual async Task PublishToDaprAsync(string eventName, object eventData, Guid? messageId = null, string? correlationId = null) + /// + public override void Unsubscribe(string eventName, IEventHandler handler) { - var client = await DaprClientFactory.CreateAsync(); - var data = new AbpDaprEventData(DaprEventBusOptions.PubSubName, eventName, (messageId ?? GuidGenerator.Create()).ToString("N"), Serializer.SerializeToString(eventData), correlationId); - await client.PublishEventAsync(pubsubName: DaprEventBusOptions.PubSubName, topicName: eventName, data: data); + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); + } + + /// + public override void UnsubscribeAll(string eventName) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); + } + + protected override IEnumerable GetAnonymousHandlerFactories(string eventName) + { + var result = new List(); + + var eventType = GetEventTypeByEventName(eventName); + if (eventType != null) + { + result.AddRange(GetHandlerFactories(eventType)); + } + + foreach (var handlerFactory in AnonymousHandlerFactories.Where(hf => hf.Key == eventName)) + { + result.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + } + + return result; + } + + private List GetOrCreateAnonymousHandlerFactories(string eventName) + { + return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); } private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs index 0be4e22bf6..e7e44e449d 100644 --- a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs @@ -127,16 +127,18 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen return new EventHandlerFactoryUnregistrar(this, eventType, factory); } - + + /// public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => + var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); + + if (handler.IsInFactories(handlerFactories)) { - if (!handler.IsInFactories(factories)) - { - factories.Add(handler); - } - }); + return NullDisposable.Instance; + } + + handlerFactories.Add(handler); return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); } @@ -188,11 +190,6 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen { GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Remove(factory)); } - - public override void Unsubscribe(string eventName, IEventHandlerFactory factory) - { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); - } /// public override void UnsubscribeAll(Type eventType) @@ -200,6 +197,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear()); } + /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); @@ -212,7 +210,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen if (AnonymousHandlerFactories.ContainsKey(eventName)) { - return PublishAsync(typeof(AnonymousEventData), new AnonymousEventData(eventName, eventData), onUnitOfWorkComplete); + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); } throw new AbpException($"Unknown event name: {eventName}"); @@ -392,7 +390,10 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { - EventTypes.GetOrAdd(eventName, eventType); + if (typeof(AnonymousEventData) != eventType) + { + EventTypes.GetOrAdd(eventName, eventType); + } return base.OnAddToOutboxAsync(eventName, eventType, eventData); } @@ -412,12 +413,16 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected override IEnumerable GetHandlerFactories(Type eventType) { var handlerFactoryList = new List(); + var eventNames = EventTypes.Where(x => ShouldTriggerEventForHandler(eventType, x.Value)).Select(x => x.Key).ToList(); - foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key)) - ) + foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) { - handlerFactoryList.Add( - new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + } + + foreach (var handlerFactory in AnonymousHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) + { + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); } return handlerFactoryList.ToArray(); @@ -428,6 +433,32 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen return EventTypes.GetOrDefault(eventName); } + /// + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + } + + /// + public override void Unsubscribe(string eventName, IEventHandler handler) + { + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); + } + + /// + public override void UnsubscribeAll(string eventName) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); + } + protected override IEnumerable GetAnonymousHandlerFactories(string eventName) { var result = new List(); diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs index 31207da4ab..771dae9f09 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs @@ -149,7 +149,8 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis return new EventHandlerFactoryUnregistrar(this, eventType, factory); } - + + /// public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); @@ -223,6 +224,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear()); } + /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); @@ -482,11 +484,32 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis return EventTypes.GetOrDefault(eventName); } + /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); } + /// + public override void Unsubscribe(string eventName, IEventHandler handler) + { + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); + } + + /// + public override void UnsubscribeAll(string eventName) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); + } + protected override IEnumerable GetAnonymousHandlerFactories(string eventName) { var result = new List(); diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs index a1f9e0116e..14ed2762e9 100644 --- a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs @@ -72,6 +72,23 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen SubscribeHandlers(AbpDistributedEventBusOptions.Handlers); } + public async Task ProcessEventAsync(Type eventType, object eventData) + { + var messageId = MessageContext.Current.TransportMessage.GetMessageId(); + var eventName = EventNameAttribute.GetNameOrDefault(eventType); + var correlationId = MessageContext.Current.Headers.GetOrDefault(EventBusConsts.CorrelationIdHeaderName); + + if (await AddToInboxAsync(messageId, eventName, eventType, eventData, correlationId)) + { + return; + } + + using (CorrelationIdProvider.Change(correlationId)) + { + await TriggerHandlersDirectAsync(eventType, eventData); + } + } + public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { var handlerFactories = GetOrCreateHandlerFactories(eventType); @@ -91,6 +108,21 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen return new EventHandlerFactoryUnregistrar(this, eventType, factory); } + /// + public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + { + var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); + + if (handler.IsInFactories(handlerFactories)) + { + return NullDisposable.Instance; + } + + handlerFactories.Add(handler); + + return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + } + public override void Unsubscribe(Func action) { Check.NotNull(action, nameof(action)); @@ -145,23 +177,7 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen Rebus.Unsubscribe(eventType); } - public async Task ProcessEventAsync(Type eventType, object eventData) - { - var messageId = MessageContext.Current.TransportMessage.GetMessageId(); - var eventName = EventNameAttribute.GetNameOrDefault(eventType); - var correlationId = MessageContext.Current.Headers.GetOrDefault(EventBusConsts.CorrelationIdHeaderName); - - if (await AddToInboxAsync(messageId, eventName, eventType, eventData, correlationId)) - { - return; - } - - using (CorrelationIdProvider.Change(correlationId)) - { - await TriggerHandlersDirectAsync(eventType, eventData); - } - } - + /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); @@ -174,7 +190,7 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen if (AnonymousHandlerFactories.ContainsKey(eventName)) { - return PublishAsync(typeof(AnonymousEventData), new AnonymousEventData(eventName, eventData), onUnitOfWorkComplete); + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); } throw new AbpException($"Unknown event name: {eventName}"); @@ -190,6 +206,111 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen await PublishAsync(eventType, eventData, headersArguments: headers); } + protected override void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord) + { + unitOfWork.AddOrReplaceDistributedEvent(eventRecord); + } + + public async override Task PublishFromOutboxAsync( + OutgoingEventInfo outgoingEvent, + OutboxConfig outboxConfig) + { + var eventType = EventTypes.GetOrDefault(outgoingEvent.EventName); + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(outgoingEvent.EventData, eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(outgoingEvent.EventName)) + { + eventData = Serializer.Deserialize(outgoingEvent.EventData, typeof(object)); + eventType = typeof(AnonymousEventData); + } + else + { + return; + } + + var headers = new Dictionary(); + if (outgoingEvent.GetCorrelationId() != null) + { + headers.Add(EventBusConsts.CorrelationIdHeaderName, outgoingEvent.GetCorrelationId()!); + } + + await PublishAsync(eventType, eventData, eventId: outgoingEvent.Id, headersArguments: headers); + + using (CorrelationIdProvider.Change(outgoingEvent.GetCorrelationId())) + { + await TriggerDistributedEventSentAsync(new DistributedEventSent() { + Source = DistributedEventSource.Outbox, + EventName = outgoingEvent.EventName, + EventData = outgoingEvent.EventData + }); + } + } + + public async override Task PublishManyFromOutboxAsync(IEnumerable outgoingEvents, OutboxConfig outboxConfig) + { + var outgoingEventArray = outgoingEvents.ToArray(); + + using (var scope = new RebusTransactionScope()) + { + foreach (var outgoingEvent in outgoingEventArray) + { + await PublishFromOutboxAsync(outgoingEvent, outboxConfig); + + using (CorrelationIdProvider.Change(outgoingEvent.GetCorrelationId())) + { + await TriggerDistributedEventSentAsync(new DistributedEventSent() + { + Source = DistributedEventSource.Outbox, + EventName = outgoingEvent.EventName, + EventData = outgoingEvent.EventData + }); + } + } + + await scope.CompleteAsync(); + } + } + + public async override Task ProcessFromInboxAsync( + IncomingEventInfo incomingEvent, + InboxConfig inboxConfig) + { + var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + { + eventData = new AnonymousEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData, typeof(object))); + eventType = typeof(AnonymousEventData); + } + else + { + return; + } + var exceptions = new List(); + using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) + { + await TriggerHandlersFromInboxAsync(eventType, eventData, exceptions, inboxConfig); + } + if (exceptions.Any()) + { + ThrowOriginalExceptions(eventType, exceptions); + } + } + + protected override byte[] Serialize(object eventData) + { + return Serializer.Serialize(eventData); + } + protected virtual async Task PublishAsync( Type eventType, object eventData, @@ -211,14 +332,12 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen await Rebus.Publish(eventData, headersArguments); } - protected override void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord) - { - unitOfWork.AddOrReplaceDistributedEvent(eventRecord); - } - protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { - EventTypes.GetOrAdd(eventName, eventType); + if (typeof(AnonymousEventData) != eventType) + { + EventTypes.GetOrAdd(eventName, eventType); + } return base.OnAddToOutboxAsync(eventName, eventType, eventData); } @@ -238,12 +357,16 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected override IEnumerable GetHandlerFactories(Type eventType) { var handlerFactoryList = new List(); + var eventNames = EventTypes.Where(x => ShouldTriggerEventForHandler(eventType, x.Value)).Select(x => x.Key).ToList(); + + foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) + { + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + } - foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key)) - ) + foreach (var handlerFactory in AnonymousHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) { - handlerFactoryList.Add( - new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); } return handlerFactoryList.ToArray(); @@ -254,22 +377,30 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen return EventTypes.GetOrDefault(eventName); } - public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) + /// + public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => - { - if (!handler.IsInFactories(factories)) - { - factories.Add(handler); - } - }); + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + } - return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + /// + public override void Unsubscribe(string eventName, IEventHandler handler) + { + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); } - public override void Unsubscribe(string eventName, IEventHandlerFactory factory) + /// + public override void UnsubscribeAll(string eventName) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); } protected override IEnumerable GetAnonymousHandlerFactories(string eventName) @@ -311,95 +442,4 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen return false; } - - public async override Task PublishFromOutboxAsync( - OutgoingEventInfo outgoingEvent, - OutboxConfig outboxConfig) - { - var eventType = EventTypes.GetOrDefault(outgoingEvent.EventName); - if (eventType == null) - { - return; - } - - var eventData = Serializer.Deserialize(outgoingEvent.EventData, eventType); - - var headers = new Dictionary(); - if (outgoingEvent.GetCorrelationId() != null) - { - headers.Add(EventBusConsts.CorrelationIdHeaderName, outgoingEvent.GetCorrelationId()!); - } - - await PublishAsync(eventType, eventData, eventId: outgoingEvent.Id, headersArguments: headers); - - using (CorrelationIdProvider.Change(outgoingEvent.GetCorrelationId())) - { - await TriggerDistributedEventSentAsync(new DistributedEventSent() { - Source = DistributedEventSource.Outbox, - EventName = outgoingEvent.EventName, - EventData = outgoingEvent.EventData - }); - } - } - - public async override Task PublishManyFromOutboxAsync(IEnumerable outgoingEvents, OutboxConfig outboxConfig) - { - var outgoingEventArray = outgoingEvents.ToArray(); - - using (var scope = new RebusTransactionScope()) - { - foreach (var outgoingEvent in outgoingEventArray) - { - await PublishFromOutboxAsync(outgoingEvent, outboxConfig); - - using (CorrelationIdProvider.Change(outgoingEvent.GetCorrelationId())) - { - await TriggerDistributedEventSentAsync(new DistributedEventSent() - { - Source = DistributedEventSource.Outbox, - EventName = outgoingEvent.EventName, - EventData = outgoingEvent.EventData - }); - } - } - - await scope.CompleteAsync(); - } - } - - public async override Task ProcessFromInboxAsync( - IncomingEventInfo incomingEvent, - InboxConfig inboxConfig) - { - var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); - object eventData; - - if (eventType != null) - { - eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); - } - else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) - { - eventData = new AnonymousEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData, typeof(object))); - eventType = typeof(AnonymousEventData); - } - else - { - return; - } - var exceptions = new List(); - using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) - { - await TriggerHandlersFromInboxAsync(eventType, eventData, exceptions, inboxConfig); - } - if (exceptions.Any()) - { - ThrowOriginalExceptions(eventType, exceptions); - } - } - - protected override byte[] Serialize(object eventData) - { - return Serializer.Serialize(eventData); - } } diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs index 9dca5a7466..35162a0007 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs @@ -43,16 +43,25 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB CorrelationIdProvider = correlationIdProvider; } + /// public virtual IDisposable Subscribe(IDistributedEventHandler handler) where TEvent : class { return Subscribe(typeof(TEvent), handler); } + /// + public virtual IDisposable Subscribe(string eventName, IDistributedEventHandler handler) + { + return Subscribe(eventName, (IEventHandler)handler); + } + + /// public override Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true) { return PublishAsync(eventType, eventData, onUnitOfWorkComplete, useOutbox: true); } + /// public virtual Task PublishAsync( TEvent eventData, bool onUnitOfWorkComplete = true, @@ -62,6 +71,7 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB return PublishAsync(typeof(TEvent), eventData, onUnitOfWorkComplete, useOutbox); } + /// public virtual async Task PublishAsync( Type eventType, object eventData, @@ -95,6 +105,24 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB }); } + /// + public virtual Task PublishAsync( + string eventName, + object eventData, + bool onUnitOfWorkComplete = true, + bool useOutbox = true) + { + var eventType = GetEventTypeByEventName(eventName); + var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete, useOutbox); + } + + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete, useOutbox); + } + public abstract Task PublishFromOutboxAsync( OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs index 3cb012b58b..922bc42909 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs @@ -73,12 +73,14 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen } } + /// public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { AnonymousEventNames.GetOrAdd(eventName, true); return LocalEventBus.Subscribe(eventName, handler); } + /// public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { var eventName = EventNameAttribute.GetNameOrDefault(eventType); @@ -101,16 +103,31 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen LocalEventBus.Unsubscribe(eventType, factory); } + /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { LocalEventBus.Unsubscribe(eventName, factory); } + /// + public override void Unsubscribe(string eventName, IEventHandler handler) + { + LocalEventBus.Unsubscribe(eventName, handler); + } + + /// public override void UnsubscribeAll(Type eventType) { LocalEventBus.UnsubscribeAll(eventType); } + /// + public override void UnsubscribeAll(string eventName) + { + LocalEventBus.UnsubscribeAll(eventName); + } + + /// public async override Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) { if (onUnitOfWorkComplete && UnitOfWorkManager.Current != null) @@ -147,24 +164,29 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen await PublishToEventBusAsync(eventType, eventData); } + /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { - var eventType = EventTypes.GetOrDefault(eventName); + return PublishAsync(eventName, eventData, onUnitOfWorkComplete, useOutbox: true); + } + /// + public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) + { + var eventType = EventTypes.GetOrDefault(eventName); var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); if (eventType != null) { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete, useOutbox); } - var isAnonymous = AnonymousEventNames.ContainsKey(eventName); - if (!isAnonymous) + if (!AnonymousEventNames.ContainsKey(eventName)) { throw new AbpException($"Unknown event name: {eventName}"); } - return PublishAsync(typeof(AnonymousEventData), new AnonymousEventData(eventName, eventData), onUnitOfWorkComplete); + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete, useOutbox); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs index 12a4b3235b..2640ddefab 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs @@ -12,6 +12,7 @@ public sealed class NullDistributedEventBus : IDistributedEventBus } + /// public Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { return Task.CompletedTask; @@ -37,11 +38,24 @@ public sealed class NullDistributedEventBus : IDistributedEventBus return NullDisposable.Instance; } + /// + public IDisposable Subscribe(string eventName, IEventHandler handler) + { + return NullDisposable.Instance; + } + + /// public IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { return NullDisposable.Instance; } + /// + public IDisposable Subscribe(string eventName, IDistributedEventHandler handler) + { + return NullDisposable.Instance; + } + public IDisposable Subscribe(IEventHandlerFactory factory) where TEvent : class { return NullDisposable.Instance; @@ -77,9 +91,14 @@ public sealed class NullDistributedEventBus : IDistributedEventBus } + /// public void Unsubscribe(string eventName, IEventHandlerFactory factory) { + } + /// + public void Unsubscribe(string eventName, IEventHandler handler) + { } public void UnsubscribeAll() where TEvent : class @@ -89,7 +108,11 @@ public sealed class NullDistributedEventBus : IDistributedEventBus public void UnsubscribeAll(Type eventType) { + } + /// + public void UnsubscribeAll(string eventName) + { } public Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true) where TEvent : class @@ -111,4 +134,10 @@ public sealed class NullDistributedEventBus : IDistributedEventBus { return Task.CompletedTask; } + + /// + public Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) + { + return Task.CompletedTask; + } } diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs index b5283f6208..8c344f5aa7 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs @@ -57,6 +57,13 @@ public abstract class EventBusBase : IEventBus return Subscribe(eventType, new SingleInstanceHandlerFactory(handler)); } + /// + public virtual IDisposable Subscribe(string eventName, IEventHandler handler) + { + return Subscribe(eventName, new SingleInstanceHandlerFactory(handler)); + } + + /// public abstract IDisposable Subscribe(string eventName, IEventHandlerFactory handler); /// @@ -85,8 +92,12 @@ public abstract class EventBusBase : IEventBus public abstract void Unsubscribe(Type eventType, IEventHandlerFactory factory); + /// public abstract void Unsubscribe(string eventName, IEventHandlerFactory factory); + /// + public abstract void Unsubscribe(string eventName, IEventHandler handler); + /// public virtual void UnsubscribeAll() where TEvent : class { @@ -96,6 +107,9 @@ public abstract class EventBusBase : IEventBus /// public abstract void UnsubscribeAll(Type eventType); + /// + public abstract void UnsubscribeAll(string eventName); + /// public Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true) where TEvent : class @@ -121,6 +135,7 @@ public abstract class EventBusBase : IEventBus await PublishToEventBusAsync(eventType, eventData); } + /// public abstract Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true); protected abstract Task PublishToEventBusAsync(Type eventType, object eventData); diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs index 4c58978b75..19095d8d9b 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs @@ -24,6 +24,9 @@ public class EventHandlerFactoryUnregistrar : IDisposable } } +/// +/// Used to unregister an for a string-based event name on method. +/// public class AnonymousEventHandlerFactoryUnregistrar : IDisposable { private readonly IEventBus _eventBus; diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs index 0d6fbace63..989191528c 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs @@ -57,6 +57,7 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency return Subscribe(typeof(TEvent), handler); } + /// public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => @@ -136,17 +137,39 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Remove(factory)); } + /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); } + /// + public override void Unsubscribe(string eventName, IEventHandler handler) + { + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); + } + /// public override void UnsubscribeAll(Type eventType) { GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear()); } + /// + public override void UnsubscribeAll(string eventName) + { + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); + } + + /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); @@ -187,6 +210,7 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency return GetHandlerFactories(eventType).ToList(); } + /// public virtual List GetAnonymousEventHandlerFactories(string eventName) { return GetAnonymousHandlerFactories(eventName).ToList(); diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs index 76e61a263e..e3d28198d4 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -13,6 +13,7 @@ public sealed class NullLocalEventBus : ILocalEventBus } + /// public Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { return Task.CompletedTask; @@ -33,6 +34,7 @@ public sealed class NullLocalEventBus : ILocalEventBus return new List(); } + /// public List GetAnonymousEventHandlerFactories(string eventName) { return new List(); @@ -48,6 +50,13 @@ public sealed class NullLocalEventBus : ILocalEventBus return NullDisposable.Instance; } + /// + public IDisposable Subscribe(string eventName, IEventHandler handler) + { + return NullDisposable.Instance; + } + + /// public IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { return NullDisposable.Instance; @@ -88,9 +97,14 @@ public sealed class NullLocalEventBus : ILocalEventBus } + /// public void Unsubscribe(string eventName, IEventHandlerFactory factory) { - + } + + /// + public void Unsubscribe(string eventName, IEventHandler handler) + { } public void UnsubscribeAll() where TEvent : class @@ -100,7 +114,11 @@ public sealed class NullLocalEventBus : ILocalEventBus public void UnsubscribeAll(Type eventType) { + } + /// + public void UnsubscribeAll(string eventName) + { } public Task PublishAsync(TEvent eventData, bool onUnitOfWorkComplete = true) where TEvent : class From 71eece82e543793dc7ac381d93bd3c64b7817342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Wed, 4 Mar 2026 14:33:54 +0300 Subject: [PATCH 07/22] Anonymous Rebus events support; demo bootstrap RebusDistributedEventBus: properly handle AnonymousEventData by extracting its EventName when processing, wrap deserialized anonymous payloads into AnonymousEventData, and subscribe to AnonymousEventData when the first anonymous handler is registered (note: a TODO about multi-threading remains). DistDemoApp.MongoDbRebus Program: replace the previous Host/Serilog-driven async Main with an ABP application bootstrap using AbpApplicationFactory. The new Main initializes the ABP app, runs DemoService.CreateTodoItemAsync via AsyncHelper.RunSync, and then shuts down; prior Serilog/host startup code has been commented out and ABP logging/Serilog services are wired up. --- .../Rebus/RebusDistributedEventBus.cs | 17 +++- .../DistDemoApp.MongoDbRebus/Program.cs | 78 +++++++++++++------ 2 files changed, 68 insertions(+), 27 deletions(-) diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs index 14ed2762e9..c4f1a141e6 100644 --- a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs @@ -75,7 +75,15 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen public async Task ProcessEventAsync(Type eventType, object eventData) { var messageId = MessageContext.Current.TransportMessage.GetMessageId(); - var eventName = EventNameAttribute.GetNameOrDefault(eventType); + string eventName; + if (eventType == typeof(AnonymousEventData) && eventData is AnonymousEventData anonymousEventData) + { + eventName = anonymousEventData.EventName; + } + else + { + eventName = EventNameAttribute.GetNameOrDefault(eventType); + } var correlationId = MessageContext.Current.Headers.GetOrDefault(EventBusConsts.CorrelationIdHeaderName); if (await AddToInboxAsync(messageId, eventName, eventType, eventData, correlationId)) @@ -119,6 +127,11 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen } handlerFactories.Add(handler); + + if (AnonymousHandlerFactories.Count == 1) //TODO: Multi-threading! + { + Rebus.Subscribe(typeof(AnonymousEventData)); + } return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); } @@ -224,7 +237,7 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen } else if (AnonymousHandlerFactories.ContainsKey(outgoingEvent.EventName)) { - eventData = Serializer.Deserialize(outgoingEvent.EventData, typeof(object)); + eventData = new AnonymousEventData(outgoingEvent.EventName, Serializer.Deserialize(outgoingEvent.EventData, typeof(object))); eventType = typeof(AnonymousEventData); } else diff --git a/test/DistEvents/DistDemoApp.MongoDbRebus/Program.cs b/test/DistEvents/DistDemoApp.MongoDbRebus/Program.cs index a79ad1b1bf..9f8c1e6c56 100644 --- a/test/DistEvents/DistDemoApp.MongoDbRebus/Program.cs +++ b/test/DistEvents/DistDemoApp.MongoDbRebus/Program.cs @@ -4,39 +4,67 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Serilog; using Serilog.Events; +using Volo.Abp; +using Volo.Abp.Threading; namespace DistDemoApp { public class Program { - public static async Task Main(string[] args) + public static void Main(string[] args) { - Log.Logger = new LoggerConfiguration() -#if DEBUG - .MinimumLevel.Debug() -#else - .MinimumLevel.Information() -#endif - .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) - .Enrich.FromLogContext() - .WriteTo.Async(c => c.File("Logs/logs.txt")) - .WriteTo.Async(c => c.Console()) - .CreateLogger(); +// Log.Logger = new LoggerConfiguration() +// #if DEBUG +// .MinimumLevel.Debug() +// #else +// .MinimumLevel.Information() +// #endif +// .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) +// .Enrich.FromLogContext() +// .WriteTo.Async(c => c.File("Logs/logs.txt")) +// .WriteTo.Async(c => c.Console()) +// .CreateLogger(); +// +// try +// { +// Log.Information("Starting console host."); +// await CreateHostBuilder(args).RunConsoleAsync(); +// return 0; +// } +// catch (Exception ex) +// { +// Log.Fatal(ex, "Host terminated unexpectedly!"); +// return 1; +// } +// finally +// { +// Log.CloseAndFlush(); +// } - try + using (var application = AbpApplicationFactory.Create(options => + { + options.UseAutofac(); + options.Services.AddSerilog((serviceProvider, c) => + { + // c.Enrich.FromLogContext() + // .WriteTo.Async(c => c.File("Logs/logs.txt")) + // .WriteTo.Async(c => c.Console()) + // .WriteTo.AbpStudio(serviceProvider); + }); + options.Services.AddLogging(c => c.AddSerilog()); + })) { - Log.Information("Starting console host."); - await CreateHostBuilder(args).RunConsoleAsync(); - return 0; - } - catch (Exception ex) - { - Log.Fatal(ex, "Host terminated unexpectedly!"); - return 1; - } - finally - { - Log.CloseAndFlush(); + Log.Information("Starting Volo.AbpIo.DbMigrator."); + + application.Initialize(); + + AsyncHelper.RunSync( + () => application + .ServiceProvider + .GetRequiredService().CreateTodoItemAsync() + ); + + application.Shutdown(); } } From d221e90c8978f6eb3fbbe058da50fe8b220e05c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Wed, 4 Mar 2026 14:46:06 +0300 Subject: [PATCH 08/22] Support anonymous Dapr events and use dynamic inbox key Add support for anonymous events in the ASP.NET Core Dapr event bus module: when a topic is identified as anonymous, deserialize payloads as object and forward them as AnonymousEventData to handlers. In DaprDistributedEventBus, use GetEventName(eventType, eventData) when adding to the inbox (so dynamic/topic-based names are respected) and expose IsAnonymousEvent(eventName) to detect anonymous topics. --- .../Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs | 8 ++++++++ .../Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs | 7 ++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs index fba9e12707..f770f5fbb2 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs @@ -102,6 +102,10 @@ public class AbpAspNetCoreMvcDaprEventBusModule : AbpModule { var eventData = daprSerializer.Deserialize(daprEventData.JsonData, eventType); await distributedEventBus.TriggerHandlersAsync(eventType, eventData, daprEventData.MessageId, daprEventData.CorrelationId); + }else if (distributedEventBus.IsAnonymousEvent(daprEventData.Topic)) + { + var eventData = daprSerializer.Deserialize(daprEventData.JsonData, typeof(object)); + await distributedEventBus.TriggerHandlersAsync(typeof(AnonymousEventData), new AnonymousEventData(daprEventData.Topic, eventData), daprEventData.MessageId, daprEventData.CorrelationId); } } else @@ -111,6 +115,10 @@ public class AbpAspNetCoreMvcDaprEventBusModule : AbpModule { var eventData = daprSerializer.Deserialize(data, eventType); await distributedEventBus.TriggerHandlersAsync(eventType, eventData); + }else if (distributedEventBus.IsAnonymousEvent(topic)) + { + var eventData = daprSerializer.Deserialize(data, typeof(object)); + await distributedEventBus.TriggerHandlersAsync(typeof(AnonymousEventData), new AnonymousEventData(topic, eventData)); } } diff --git a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs index 62789a8408..1231daf632 100644 --- a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs @@ -217,7 +217,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public virtual async Task TriggerHandlersAsync(Type eventType, object eventData, string? messageId = null, string? correlationId = null) { - if (await AddToInboxAsync(messageId, EventNameAttribute.GetNameOrDefault(eventType), eventType, eventData, correlationId)) + if (await AddToInboxAsync(messageId, GetEventName(eventType, eventData), eventType, eventData, correlationId)) { return; } @@ -324,6 +324,11 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend { return EventTypes.GetOrDefault(eventName); } + + public bool IsAnonymousEvent(string eventName) + { + return AnonymousHandlerFactories.ContainsKey(eventName); + } /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) From 92b2e84f2f247c73153d73629903c0a95206ff06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Wed, 4 Mar 2026 20:11:59 +0300 Subject: [PATCH 09/22] Add DistEvents demo apps and migrations Introduce a new DistEvents test/demo suite and supporting infra. - Add AspNetCoreDapr and AzureEmulator sample apps (modules, programs, controllers, event handlers, appsettings). - Add persistence projects for EntityFrameworkCore and MongoDB, including EF Core migrations and updated model snapshot. - Update EfCoreRabbitMq project to support selectable DB provider (DistDemoDbProvider) and refactor RabbitMQ/Dapr event-bus configuration. - Add shared demo utilities (scenario runner/profile/hosted service), Dapr pubsub component, docker-compose and Service Bus emulator config. - Add comprehensive Visual Studio/.NET .gitignore and tweak .claude local permissions to allow "Bash(git show:*)". --- .claude/settings.local.json | 3 +- test/DistEvents/.gitignore | 398 ++++++++++++++++++ .../DistDemoApp.AspNetCoreDapr.csproj | 39 ++ .../DistDemoAppAspNetCoreDaprModule.cs | 67 +++ .../DistDemoApp.AspNetCoreDapr/Program.cs | 12 + .../ProviderScenarioController.cs | 23 + .../ProviderScenarioEventHandler.cs | 17 + .../appsettings.json | 14 + .../DistDemoApp.AzureEmulator.csproj | 37 ++ .../DistDemoAppAzureEmulatorModule.cs | 51 +++ .../EmulatorProcessorPool.cs | 79 ++++ .../EmulatorPublisherPool.cs | 64 +++ .../DistDemoApp.AzureEmulator/Program.cs | 32 ++ .../appsettings.json | 22 + .../DistDemoApp.EfCoreRabbitMq.csproj | 18 +- .../DistDemoAppEfCoreRabbitMqModule.cs | 53 ++- .../20260304073807_Update-10.2.Designer.cs | 182 ++++++++ .../Migrations/20260304073807_Update-10.2.cs | 233 ++++++++++ .../Migrations/TodoDbContextModelSnapshot.cs | 43 +- .../DistDemoApp.EfCoreRabbitMq/Program.cs | 65 +-- .../TodoDbContext.cs | 34 -- .../TodoDbContextFactory.cs | 29 -- .../appsettings.json | 2 +- .../DistDemoApp.MongoDbKafka.csproj | 18 +- .../DistDemoAppMongoDbKafkaModule.cs | 39 +- .../DistDemoApp.MongoDbKafka/Program.cs | 65 +-- .../TodoMongoDbContext.cs | 26 -- .../DistDemoApp.MongoDbKafka/appsettings.json | 2 +- .../DistDemoApp.MongoDbRebus.csproj | 18 +- .../DistDemoAppMongoDbRebusModule.cs | 38 +- .../DistDemoApp.MongoDbRebus/Program.cs | 53 +-- .../TodoMongoDbContext.cs | 19 - .../DistDemoApp.MongoDbRebus/appsettings.json | 2 +- ...App.Persistence.EntityFrameworkCore.csproj | 18 + ...EntityFrameworkCoreInfrastructureModule.cs | 12 + ...ityFrameworkServiceCollectionExtensions.cs | 21 + .../TodoDbContext.cs | 34 ++ .../TodoDbContextFactory.cs | 28 ++ .../DistDemoApp.Persistence.MongoDb.csproj | 14 + .../DistDemoAppMongoDbInfrastructureModule.cs | 12 + .../DistDemoMongoDbContext.cs | 26 ++ ...istDemoMongoServiceCollectionExtensions.cs | 42 ++ .../DistDemoApp.Shared/DemoService.cs | 22 +- .../DistDemoAppHostedService.cs | 9 +- .../DistDemoAppSharedModule.cs | 4 +- .../DistEventScenarioProfile.cs | 60 +++ .../DistEventScenarioRunner.cs | 134 ++++++ .../IDistEventScenarioRunner.cs | 8 + .../ProviderScenarioEvent.cs | 9 + test/DistEvents/DistEventsDemo.slnx | 4 + test/DistEvents/dapr/components/pubsub.yaml | 12 + test/DistEvents/docker-compose.yml | 131 ++++++ .../servicebus-emulator/Config.json | 22 + 53 files changed, 2073 insertions(+), 346 deletions(-) create mode 100644 test/DistEvents/.gitignore create mode 100644 test/DistEvents/DistDemoApp.AspNetCoreDapr/DistDemoApp.AspNetCoreDapr.csproj create mode 100644 test/DistEvents/DistDemoApp.AspNetCoreDapr/DistDemoAppAspNetCoreDaprModule.cs create mode 100644 test/DistEvents/DistDemoApp.AspNetCoreDapr/Program.cs create mode 100644 test/DistEvents/DistDemoApp.AspNetCoreDapr/ProviderScenarioController.cs create mode 100644 test/DistEvents/DistDemoApp.AspNetCoreDapr/ProviderScenarioEventHandler.cs create mode 100644 test/DistEvents/DistDemoApp.AspNetCoreDapr/appsettings.json create mode 100644 test/DistEvents/DistDemoApp.AzureEmulator/DistDemoApp.AzureEmulator.csproj create mode 100644 test/DistEvents/DistDemoApp.AzureEmulator/DistDemoAppAzureEmulatorModule.cs create mode 100644 test/DistEvents/DistDemoApp.AzureEmulator/EmulatorProcessorPool.cs create mode 100644 test/DistEvents/DistDemoApp.AzureEmulator/EmulatorPublisherPool.cs create mode 100644 test/DistEvents/DistDemoApp.AzureEmulator/Program.cs create mode 100644 test/DistEvents/DistDemoApp.AzureEmulator/appsettings.json create mode 100644 test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20260304073807_Update-10.2.Designer.cs create mode 100644 test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20260304073807_Update-10.2.cs delete mode 100644 test/DistEvents/DistDemoApp.EfCoreRabbitMq/TodoDbContext.cs delete mode 100644 test/DistEvents/DistDemoApp.EfCoreRabbitMq/TodoDbContextFactory.cs delete mode 100644 test/DistEvents/DistDemoApp.MongoDbKafka/TodoMongoDbContext.cs delete mode 100644 test/DistEvents/DistDemoApp.MongoDbRebus/TodoMongoDbContext.cs create mode 100644 test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoApp.Persistence.EntityFrameworkCore.csproj create mode 100644 test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoAppEntityFrameworkCoreInfrastructureModule.cs create mode 100644 test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoEntityFrameworkServiceCollectionExtensions.cs create mode 100644 test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/TodoDbContext.cs create mode 100644 test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/TodoDbContextFactory.cs create mode 100644 test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoApp.Persistence.MongoDb.csproj create mode 100644 test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoAppMongoDbInfrastructureModule.cs create mode 100644 test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoMongoDbContext.cs create mode 100644 test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoMongoServiceCollectionExtensions.cs create mode 100644 test/DistEvents/DistDemoApp.Shared/DistEventScenarioProfile.cs create mode 100644 test/DistEvents/DistDemoApp.Shared/DistEventScenarioRunner.cs create mode 100644 test/DistEvents/DistDemoApp.Shared/IDistEventScenarioRunner.cs create mode 100644 test/DistEvents/DistDemoApp.Shared/ProviderScenarioEvent.cs create mode 100644 test/DistEvents/dapr/components/pubsub.yaml create mode 100644 test/DistEvents/docker-compose.yml create mode 100644 test/DistEvents/servicebus-emulator/Config.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 1eff521897..4b5b456a93 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,8 @@ "permissions": { "allow": [ "Bash(yarn nx g:*)", - "Bash(npx vitest:*)" + "Bash(npx vitest:*)", + "Bash(git show:*)" ] } } diff --git a/test/DistEvents/.gitignore b/test/DistEvents/.gitignore new file mode 100644 index 0000000000..8dd4607a4b --- /dev/null +++ b/test/DistEvents/.gitignore @@ -0,0 +1,398 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml \ No newline at end of file diff --git a/test/DistEvents/DistDemoApp.AspNetCoreDapr/DistDemoApp.AspNetCoreDapr.csproj b/test/DistEvents/DistDemoApp.AspNetCoreDapr/DistDemoApp.AspNetCoreDapr.csproj new file mode 100644 index 0000000000..8801131096 --- /dev/null +++ b/test/DistEvents/DistDemoApp.AspNetCoreDapr/DistDemoApp.AspNetCoreDapr.csproj @@ -0,0 +1,39 @@ + + + + net10.0 + enable + enable + DistDemoApp + MongoDb + + + + + + + + + + + + + + + + + + $(DefineConstants);DISTDEMO_USE_MONGODB + + + + $(DefineConstants);DISTDEMO_USE_EFCORE + + + + + Always + + + + diff --git a/test/DistEvents/DistDemoApp.AspNetCoreDapr/DistDemoAppAspNetCoreDaprModule.cs b/test/DistEvents/DistDemoApp.AspNetCoreDapr/DistDemoAppAspNetCoreDaprModule.cs new file mode 100644 index 0000000000..843a4af81b --- /dev/null +++ b/test/DistEvents/DistDemoApp.AspNetCoreDapr/DistDemoAppAspNetCoreDaprModule.cs @@ -0,0 +1,67 @@ +using Dapr; +using Volo.Abp; +using Volo.Abp.AspNetCore.Mvc.Dapr.EventBus; +using Volo.Abp.Autofac; +using Volo.Abp.Dapr; +using Volo.Abp.EventBus.Dapr; +using Volo.Abp.Modularity; + +namespace DistDemoApp; + +#if DISTDEMO_USE_MONGODB +[DependsOn( + typeof(DistDemoAppMongoDbInfrastructureModule), + typeof(AbpAspNetCoreMvcDaprEventBusModule), + typeof(DistDemoAppSharedModule), + typeof(AbpAutofacModule) +)] +#else +[DependsOn( + typeof(DistDemoAppEntityFrameworkCoreInfrastructureModule), + typeof(AbpAspNetCoreMvcDaprEventBusModule), + typeof(DistDemoAppSharedModule), + typeof(AbpAutofacModule) +)] +#endif +public class DistDemoAppAspNetCoreDaprModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { +#if DISTDEMO_USE_MONGODB + context.ConfigureDistDemoMongoInfrastructure(); +#else + context.ConfigureDistDemoEntityFrameworkInfrastructure(); +#endif + + Configure(options => + { + options.HttpEndpoint = "http://localhost:3500"; + }); + + Configure(options => + { + options.PubSubName = "pubsub"; + }); + } + + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var app = context.GetApplicationBuilder(); + var env = context.GetEnvironment(); + + if (env.IsDevelopment()) + { + app.UseExceptionHandler("/Error"); + } + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + app.UseRouting(); + app.UseCloudEvents(); + app.UseConfiguredEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapSubscribeHandler(); + }); + } +} diff --git a/test/DistEvents/DistDemoApp.AspNetCoreDapr/Program.cs b/test/DistEvents/DistDemoApp.AspNetCoreDapr/Program.cs new file mode 100644 index 0000000000..f5d3f8c8ef --- /dev/null +++ b/test/DistEvents/DistDemoApp.AspNetCoreDapr/Program.cs @@ -0,0 +1,12 @@ +using DistDemoApp; +using Dapr; +using Volo.Abp; + +var builder = WebApplication.CreateBuilder(args); +builder.Host.UseAutofac(); + +await builder.AddApplicationAsync(); + +var app = builder.Build(); +await app.InitializeApplicationAsync(); +await app.RunAsync("http://localhost:8090"); diff --git a/test/DistEvents/DistDemoApp.AspNetCoreDapr/ProviderScenarioController.cs b/test/DistEvents/DistDemoApp.AspNetCoreDapr/ProviderScenarioController.cs new file mode 100644 index 0000000000..ff6de4f5b6 --- /dev/null +++ b/test/DistEvents/DistDemoApp.AspNetCoreDapr/ProviderScenarioController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; + +namespace DistDemoApp; + +[ApiController] +[Route("api/dist-demo/dapr")] +public class ProviderScenarioController : AbpController +{ + private readonly IDistEventScenarioRunner _scenarioRunner; + + public ProviderScenarioController(IDistEventScenarioRunner scenarioRunner) + { + _scenarioRunner = scenarioRunner; + } + + [HttpGet] + public async Task RunAsync() + { + await _scenarioRunner.RunAsync(DistEventScenarioProfile.DaprWeb()); + return Ok(new { Status = "ScenarioCompleted", Profile = "dapr-web" }); + } +} diff --git a/test/DistEvents/DistDemoApp.AspNetCoreDapr/ProviderScenarioEventHandler.cs b/test/DistEvents/DistDemoApp.AspNetCoreDapr/ProviderScenarioEventHandler.cs new file mode 100644 index 0000000000..b26844d7c5 --- /dev/null +++ b/test/DistEvents/DistDemoApp.AspNetCoreDapr/ProviderScenarioEventHandler.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Distributed; + +namespace DistDemoApp; + +public class ProviderScenarioEventHandler : + IDistributedEventHandler, + ITransientDependency +{ + public Task HandleEventAsync(ProviderScenarioEvent eventData) + { + Console.WriteLine($"Dapr ASP.NET Core handler received ProviderScenarioEvent: {eventData.Value}"); + return Task.CompletedTask; + } +} diff --git a/test/DistEvents/DistDemoApp.AspNetCoreDapr/appsettings.json b/test/DistEvents/DistDemoApp.AspNetCoreDapr/appsettings.json new file mode 100644 index 0000000000..711c4a34f7 --- /dev/null +++ b/test/DistEvents/DistDemoApp.AspNetCoreDapr/appsettings.json @@ -0,0 +1,14 @@ +{ + "ConnectionStrings": { + "Default": "mongodb://localhost:27017/DistEventsDemo?retryWrites=false" + }, + "Dapr": { + "HttpEndpoint": "http://localhost:3500" + }, + "DaprEventBus": { + "PubSubName": "pubsub" + }, + "Redis": { + "Configuration": "127.0.0.1" + } +} diff --git a/test/DistEvents/DistDemoApp.AzureEmulator/DistDemoApp.AzureEmulator.csproj b/test/DistEvents/DistDemoApp.AzureEmulator/DistDemoApp.AzureEmulator.csproj new file mode 100644 index 0000000000..d96ef9fda5 --- /dev/null +++ b/test/DistEvents/DistDemoApp.AzureEmulator/DistDemoApp.AzureEmulator.csproj @@ -0,0 +1,37 @@ + + + + Exe + net10.0 + DistDemoApp + MongoDb + + + + + + + + + + + + + + + + + $(DefineConstants);DISTDEMO_USE_MONGODB + + + + $(DefineConstants);DISTDEMO_USE_EFCORE + + + + + Always + + + + diff --git a/test/DistEvents/DistDemoApp.AzureEmulator/DistDemoAppAzureEmulatorModule.cs b/test/DistEvents/DistDemoApp.AzureEmulator/DistDemoAppAzureEmulatorModule.cs new file mode 100644 index 0000000000..2dd0c59c36 --- /dev/null +++ b/test/DistEvents/DistDemoApp.AzureEmulator/DistDemoAppAzureEmulatorModule.cs @@ -0,0 +1,51 @@ +using Azure.Messaging.ServiceBus; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.EventBus.Azure; +using Volo.Abp.Modularity; +using Volo.Abp.AzureServiceBus; + +namespace DistDemoApp; + +#if DISTDEMO_USE_MONGODB +[DependsOn( + typeof(DistDemoAppMongoDbInfrastructureModule), + typeof(AbpEventBusAzureModule), + typeof(DistDemoAppSharedModule) +)] +#else +[DependsOn( + typeof(DistDemoAppEntityFrameworkCoreInfrastructureModule), + typeof(AbpEventBusAzureModule), + typeof(DistDemoAppSharedModule) +)] +#endif +public class DistDemoAppAzureEmulatorModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddSingleton(); + context.Services.AddSingleton(); +#if DISTDEMO_USE_MONGODB + context.ConfigureDistDemoMongoInfrastructure(); +#else + context.ConfigureDistDemoEntityFrameworkInfrastructure(); +#endif + + Configure(options => + { + options.Connections.Default.ConnectionString = + "Endpoint=sb://localhost:5673;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"; + options.Connections.Default.Processor = new ServiceBusProcessorOptions + { + AutoCompleteMessages = false + }; + }); + + Configure(options => + { + options.ConnectionName = "Default"; + options.SubscriberName = "DistDemoAzureSubscriber"; + options.TopicName = "DistDemoAzureTopic"; + }); + } +} diff --git a/test/DistEvents/DistDemoApp.AzureEmulator/EmulatorProcessorPool.cs b/test/DistEvents/DistDemoApp.AzureEmulator/EmulatorProcessorPool.cs new file mode 100644 index 0000000000..84891ae804 --- /dev/null +++ b/test/DistEvents/DistDemoApp.AzureEmulator/EmulatorProcessorPool.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Volo.Abp.AzureServiceBus; +using Volo.Abp.DependencyInjection; + +namespace DistDemoApp; + +public class EmulatorProcessorPool : IProcessorPool, ISingletonDependency +{ + public ILogger Logger { get; set; } + + private bool _isDisposed; + private readonly AbpAzureServiceBusOptions _options; + private readonly IConnectionPool _connectionPool; + private readonly ConcurrentDictionary> _processors; + + public EmulatorProcessorPool( + IOptions options, + IConnectionPool connectionPool) + { + _options = options.Value; + _connectionPool = connectionPool; + _processors = new ConcurrentDictionary>(); + Logger = NullLogger.Instance; + } + + public Task GetAsync(string subscriptionName, string topicName, string connectionName) + { + var processor = _processors.GetOrAdd( + $"{topicName}-{subscriptionName}", + new Lazy(() => + { + var config = _options.Connections.GetOrDefault(connectionName); + var client = _connectionPool.GetClient(connectionName); + return client.CreateProcessor(topicName, subscriptionName, config.Processor); + }) + ).Value; + + return Task.FromResult(processor); + } + + public async ValueTask DisposeAsync() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + if (!_processors.Any()) + { + return; + } + + foreach (var item in _processors.Values) + { + var processor = item.Value; + if (processor.IsProcessing) + { + await processor.StopProcessingAsync(); + } + + if (!processor.IsClosed) + { + await processor.CloseAsync(); + } + + await processor.DisposeAsync(); + } + + _processors.Clear(); + } +} diff --git a/test/DistEvents/DistDemoApp.AzureEmulator/EmulatorPublisherPool.cs b/test/DistEvents/DistDemoApp.AzureEmulator/EmulatorPublisherPool.cs new file mode 100644 index 0000000000..f3c9c0a854 --- /dev/null +++ b/test/DistEvents/DistDemoApp.AzureEmulator/EmulatorPublisherPool.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading.Tasks; +using Azure.Messaging.ServiceBus; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.AzureServiceBus; +using Volo.Abp.DependencyInjection; + +#nullable enable +namespace DistDemoApp; + +public class EmulatorPublisherPool : IPublisherPool, ISingletonDependency +{ + public ILogger Logger { get; set; } + + private bool _isDisposed; + private readonly IConnectionPool _connectionPool; + private readonly ConcurrentDictionary> _publishers; + + public EmulatorPublisherPool(IConnectionPool connectionPool) + { + _connectionPool = connectionPool; + _publishers = new ConcurrentDictionary>(); + Logger = NullLogger.Instance; + } + + public Task GetAsync(string topicName, string? connectionName) + { + var sender = _publishers.GetOrAdd( + topicName, + new Lazy(() => + { + var client = _connectionPool.GetClient(connectionName); + return client.CreateSender(topicName); + }) + ).Value; + + return Task.FromResult(sender); + } + + public async ValueTask DisposeAsync() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + if (!_publishers.Any()) + { + return; + } + + foreach (var publisher in _publishers.Values) + { + await publisher.Value.CloseAsync(); + await publisher.Value.DisposeAsync(); + } + + _publishers.Clear(); + } +} diff --git a/test/DistEvents/DistDemoApp.AzureEmulator/Program.cs b/test/DistEvents/DistDemoApp.AzureEmulator/Program.cs new file mode 100644 index 0000000000..a5375dad49 --- /dev/null +++ b/test/DistEvents/DistDemoApp.AzureEmulator/Program.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.DependencyInjection; +using Serilog; +using Volo.Abp; +using Volo.Abp.Threading; + +namespace DistDemoApp; + +public class Program +{ + public static void Main(string[] args) + { + using var application = AbpApplicationFactory.Create(options => + { + options.UseAutofac(); + options.Services.AddSerilog((serviceProvider, configuration) => + { + }); + options.Services.AddLogging(c => c.AddSerilog()); + }); + + Log.Information("Starting DistDemoApp.AzureEmulator."); + + application.Initialize(); + + AsyncHelper.RunSync(() => application + .ServiceProvider + .GetRequiredService() + .RunAsync(DistEventScenarioProfile.AzureEmulator())); + + application.Shutdown(); + } +} diff --git a/test/DistEvents/DistDemoApp.AzureEmulator/appsettings.json b/test/DistEvents/DistDemoApp.AzureEmulator/appsettings.json new file mode 100644 index 0000000000..4502f1e483 --- /dev/null +++ b/test/DistEvents/DistDemoApp.AzureEmulator/appsettings.json @@ -0,0 +1,22 @@ +{ + "ConnectionStrings": { + "Default": "mongodb://localhost:27017/DistEventsDemo?retryWrites=false" + }, + "Azure": { + "ServiceBus": { + "Connections": { + "Default": { + "ConnectionString": "Endpoint=sb://localhost:5673;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;" + } + } + }, + "EventBus": { + "ConnectionName": "Default", + "SubscriberName": "DistDemoAzureSubscriber", + "TopicName": "DistDemoAzureTopic" + } + }, + "Redis": { + "Configuration": "127.0.0.1" + } +} diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/DistDemoApp.EfCoreRabbitMq.csproj b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/DistDemoApp.EfCoreRabbitMq.csproj index c3b8719b3f..156de09a79 100644 --- a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/DistDemoApp.EfCoreRabbitMq.csproj +++ b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/DistDemoApp.EfCoreRabbitMq.csproj @@ -4,14 +4,30 @@ Exe net10.0 DistDemoApp + EntityFrameworkCore - + + + + + + + + + + $(DefineConstants);DISTDEMO_USE_MONGODB + + + + $(DefineConstants);DISTDEMO_USE_EFCORE + + runtime; build; native; contentfiles; analyzers diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/DistDemoAppEfCoreRabbitMqModule.cs b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/DistDemoAppEfCoreRabbitMqModule.cs index da2e7a1445..363373441d 100644 --- a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/DistDemoAppEfCoreRabbitMqModule.cs +++ b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/DistDemoAppEfCoreRabbitMqModule.cs @@ -1,44 +1,59 @@ using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.EntityFrameworkCore; -using Volo.Abp.EntityFrameworkCore.DistributedEvents; -using Volo.Abp.EntityFrameworkCore.SqlServer; using Volo.Abp.EventBus.Distributed; using Volo.Abp.EventBus.RabbitMq; using Volo.Abp.Modularity; +using Volo.Abp.RabbitMQ; namespace DistDemoApp { +#if DISTDEMO_USE_MONGODB [DependsOn( - typeof(AbpEntityFrameworkCoreSqlServerModule), + typeof(DistDemoAppMongoDbInfrastructureModule), typeof(AbpEventBusRabbitMqModule), typeof(DistDemoAppSharedModule) )] +#else + [DependsOn( + typeof(DistDemoAppEntityFrameworkCoreInfrastructureModule), + typeof(AbpEventBusRabbitMqModule), + typeof(DistDemoAppSharedModule) + )] +#endif public class DistDemoAppEfCoreRabbitMqModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAbpDbContext(options => +#if DISTDEMO_USE_MONGODB + context.ConfigureDistDemoMongoInfrastructure(); +#else + context.ConfigureDistDemoEntityFrameworkInfrastructure(); +#endif + + Configure(options => { - options.AddDefaultRepositories(); + // options.Outboxes.Configure(config => + // { + // config.UseDbContext(); + // }); + // + // options.Inboxes.Configure(config => + // { + // config.UseDbContext(); + // }); }); - Configure(options => + Configure(options => { - options.UseSqlServer(); + options.Connections.Default.HostName = "localhost"; }); - - Configure(options => + + Configure(options => { - options.Outboxes.Configure(config => - { - config.UseDbContext(); - }); - - options.Inboxes.Configure(config => - { - config.UseDbContext(); - }); + options.ConnectionName = "Default"; + options.ClientName = "DistDemoApp"; + options.ExchangeName = "DistDemo"; }); + } } } \ No newline at end of file diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20260304073807_Update-10.2.Designer.cs b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20260304073807_Update-10.2.Designer.cs new file mode 100644 index 0000000000..718cebec33 --- /dev/null +++ b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20260304073807_Update-10.2.Designer.cs @@ -0,0 +1,182 @@ +// +using System; +using DistDemoApp; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Volo.Abp.EntityFrameworkCore; + +#nullable disable + +namespace DistDemoApp.Migrations +{ + [DbContext(typeof(TodoDbContext))] + [Migration("20260304073807_Update-10.2")] + partial class Update102 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("DistDemoApp.TodoItem", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("Text") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); + + modelBuilder.Entity("DistDemoApp.TodoSummary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)") + .HasColumnName("ConcurrencyStamp"); + + b.Property("Day") + .HasColumnType("tinyint"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("Month") + .HasColumnType("tinyint"); + + b.Property("TotalCount") + .HasColumnType("int"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("TodoSummaries"); + }); + + modelBuilder.Entity("Volo.Abp.EntityFrameworkCore.DistributedEvents.IncomingEventRecord", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("EventData") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("EventName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.Property("HandledTime") + .HasColumnType("datetime2"); + + b.Property("MessageId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("NextRetryTime") + .HasColumnType("datetime2"); + + b.Property("RetryCount") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("MessageId"); + + b.HasIndex("Status", "CreationTime"); + + b.ToTable("AbpEventInbox", (string)null); + }); + + modelBuilder.Entity("Volo.Abp.EntityFrameworkCore.DistributedEvents.OutgoingEventRecord", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("EventData") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("EventName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("ExtraProperties") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("ExtraProperties"); + + b.HasKey("Id"); + + b.HasIndex("CreationTime"); + + b.ToTable("AbpEventOutbox", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20260304073807_Update-10.2.cs b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20260304073807_Update-10.2.cs new file mode 100644 index 0000000000..a3bcf90174 --- /dev/null +++ b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20260304073807_Update-10.2.cs @@ -0,0 +1,233 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DistDemoApp.Migrations +{ + /// + public partial class Update102 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_AbpEventInbox_Processed_CreationTime", + table: "AbpEventInbox"); + + migrationBuilder.DropColumn( + name: "Processed", + table: "AbpEventInbox"); + + migrationBuilder.RenameColumn( + name: "ProcessedTime", + table: "AbpEventInbox", + newName: "NextRetryTime"); + + migrationBuilder.AlterColumn( + name: "ExtraProperties", + table: "TodoSummaries", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ConcurrencyStamp", + table: "TodoSummaries", + type: "nvarchar(40)", + maxLength: 40, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(40)", + oldMaxLength: 40, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ExtraProperties", + table: "TodoItems", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ConcurrencyStamp", + table: "TodoItems", + type: "nvarchar(40)", + maxLength: 40, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(40)", + oldMaxLength: 40, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ExtraProperties", + table: "AbpEventOutbox", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MessageId", + table: "AbpEventInbox", + type: "nvarchar(450)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ExtraProperties", + table: "AbpEventInbox", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "HandledTime", + table: "AbpEventInbox", + type: "datetime2", + nullable: true); + + migrationBuilder.AddColumn( + name: "RetryCount", + table: "AbpEventInbox", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "Status", + table: "AbpEventInbox", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateIndex( + name: "IX_AbpEventOutbox_CreationTime", + table: "AbpEventOutbox", + column: "CreationTime"); + + migrationBuilder.CreateIndex( + name: "IX_AbpEventInbox_Status_CreationTime", + table: "AbpEventInbox", + columns: new[] { "Status", "CreationTime" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_AbpEventOutbox_CreationTime", + table: "AbpEventOutbox"); + + migrationBuilder.DropIndex( + name: "IX_AbpEventInbox_Status_CreationTime", + table: "AbpEventInbox"); + + migrationBuilder.DropColumn( + name: "HandledTime", + table: "AbpEventInbox"); + + migrationBuilder.DropColumn( + name: "RetryCount", + table: "AbpEventInbox"); + + migrationBuilder.DropColumn( + name: "Status", + table: "AbpEventInbox"); + + migrationBuilder.RenameColumn( + name: "NextRetryTime", + table: "AbpEventInbox", + newName: "ProcessedTime"); + + migrationBuilder.AlterColumn( + name: "ExtraProperties", + table: "TodoSummaries", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.AlterColumn( + name: "ConcurrencyStamp", + table: "TodoSummaries", + type: "nvarchar(40)", + maxLength: 40, + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(40)", + oldMaxLength: 40); + + migrationBuilder.AlterColumn( + name: "ExtraProperties", + table: "TodoItems", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.AlterColumn( + name: "ConcurrencyStamp", + table: "TodoItems", + type: "nvarchar(40)", + maxLength: 40, + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(40)", + oldMaxLength: 40); + + migrationBuilder.AlterColumn( + name: "ExtraProperties", + table: "AbpEventOutbox", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.AlterColumn( + name: "MessageId", + table: "AbpEventInbox", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AlterColumn( + name: "ExtraProperties", + table: "AbpEventInbox", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.AddColumn( + name: "Processed", + table: "AbpEventInbox", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateIndex( + name: "IX_AbpEventInbox_Processed_CreationTime", + table: "AbpEventInbox", + columns: new[] { "Processed", "CreationTime" }); + } + } +} diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/TodoDbContextModelSnapshot.cs b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/TodoDbContextModelSnapshot.cs index 57e8c14442..1f12a89a1c 100644 --- a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/TodoDbContextModelSnapshot.cs +++ b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/TodoDbContextModelSnapshot.cs @@ -7,6 +7,8 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Volo.Abp.EntityFrameworkCore; +#nullable disable + namespace DistDemoApp.Migrations { [DbContext(typeof(TodoDbContext))] @@ -17,9 +19,10 @@ namespace DistDemoApp.Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("ProductVersion", "5.0.9") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); modelBuilder.Entity("DistDemoApp.TodoItem", b => { @@ -28,6 +31,7 @@ namespace DistDemoApp.Migrations b.Property("ConcurrencyStamp") .IsConcurrencyToken() + .IsRequired() .HasMaxLength(40) .HasColumnType("nvarchar(40)") .HasColumnName("ConcurrencyStamp"); @@ -41,6 +45,7 @@ namespace DistDemoApp.Migrations .HasColumnName("CreatorId"); b.Property("ExtraProperties") + .IsRequired() .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); @@ -58,11 +63,13 @@ namespace DistDemoApp.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("int") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); b.Property("ConcurrencyStamp") .IsConcurrencyToken() + .IsRequired() .HasMaxLength(40) .HasColumnType("nvarchar(40)") .HasColumnName("ConcurrencyStamp"); @@ -71,6 +78,7 @@ namespace DistDemoApp.Migrations .HasColumnType("tinyint"); b.Property("ExtraProperties") + .IsRequired() .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); @@ -107,25 +115,33 @@ namespace DistDemoApp.Migrations .HasColumnType("nvarchar(256)"); b.Property("ExtraProperties") + .IsRequired() .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); + b.Property("HandledTime") + .HasColumnType("datetime2"); + b.Property("MessageId") + .IsRequired() .HasColumnType("nvarchar(450)"); - b.Property("Processed") - .HasColumnType("bit"); - - b.Property("ProcessedTime") + b.Property("NextRetryTime") .HasColumnType("datetime2"); + b.Property("RetryCount") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + b.HasKey("Id"); b.HasIndex("MessageId"); - b.HasIndex("Processed", "CreationTime"); + b.HasIndex("Status", "CreationTime"); - b.ToTable("AbpEventInbox"); + b.ToTable("AbpEventInbox", (string)null); }); modelBuilder.Entity("Volo.Abp.EntityFrameworkCore.DistributedEvents.OutgoingEventRecord", b => @@ -147,12 +163,15 @@ namespace DistDemoApp.Migrations .HasColumnType("nvarchar(256)"); b.Property("ExtraProperties") + .IsRequired() .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); b.HasKey("Id"); - b.ToTable("AbpEventOutbox"); + b.HasIndex("CreationTime"); + + b.ToTable("AbpEventOutbox", (string)null); }); #pragma warning restore 612, 618 } diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Program.cs b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Program.cs index 597b29ae22..3cee486e96 100644 --- a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Program.cs +++ b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Program.cs @@ -1,57 +1,34 @@ -using System; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Serilog; -using Serilog.Events; +using Volo.Abp; +using Volo.Abp.Threading; namespace DistDemoApp { public class Program { - public static async Task Main(string[] args) + public static void Main(string[] args) { - Log.Logger = new LoggerConfiguration() -#if DEBUG - .MinimumLevel.Debug() -#else - .MinimumLevel.Information() -#endif - .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) - .Enrich.FromLogContext() - .WriteTo.Async(c => c.File("Logs/logs.txt")) - .WriteTo.Async(c => c.Console()) - .CreateLogger(); - - try - { - Log.Information("Starting console host."); - await CreateHostBuilder(args).RunConsoleAsync(); - return 0; - } - catch (Exception ex) - { - Log.Fatal(ex, "Host terminated unexpectedly!"); - return 1; - } - finally + using (var application = AbpApplicationFactory.Create(options => + { + options.UseAutofac(); + options.Services.AddSerilog((_, _) => + { + }); + options.Services.AddLogging(c => c.AddSerilog()); + })) { - Log.CloseAndFlush(); - } + Log.Information("Starting DistDemoApp.EfCoreRabbitMq."); + application.Initialize(); - } + AsyncHelper.RunSync( + () => application + .ServiceProvider + .GetRequiredService().CreateTodoItemAsync() + ); - internal static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .UseAutofac() - .UseSerilog() - .ConfigureAppConfiguration((context, config) => - { - //setup your additional configuration sources - }) - .ConfigureServices((hostContext, services) => - { - services.AddApplication(); - }); + application.Shutdown(); + } + } } } diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/TodoDbContext.cs b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/TodoDbContext.cs deleted file mode 100644 index 5a1ddd2c3f..0000000000 --- a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/TodoDbContext.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Volo.Abp.Domain.Entities; -using Volo.Abp.EntityFrameworkCore; -using Volo.Abp.EntityFrameworkCore.DistributedEvents; - -namespace DistDemoApp -{ - public class TodoDbContext : AbpDbContext, IHasEventOutbox, IHasEventInbox - { - public DbSet TodoItems { get; set; } - public DbSet TodoSummaries { get; set; } - public DbSet OutgoingEvents { get; set; } - public DbSet IncomingEvents { get; set; } - - public TodoDbContext(DbContextOptions options) - : base(options) - { - - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.ConfigureEventOutbox(); - modelBuilder.ConfigureEventInbox(); - - modelBuilder.Entity(b => - { - b.Property(x => x.Text).IsRequired().HasMaxLength(128); - }); - } - } -} \ No newline at end of file diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/TodoDbContextFactory.cs b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/TodoDbContextFactory.cs deleted file mode 100644 index 97be637acc..0000000000 --- a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/TodoDbContextFactory.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.IO; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; -using Microsoft.Extensions.Configuration; - -namespace DistDemoApp -{ - public class TodoDbContextFactory : IDesignTimeDbContextFactory - { - public TodoDbContext CreateDbContext(string[] args) - { - var configuration = BuildConfiguration(); - - var builder = new DbContextOptionsBuilder() - .UseSqlServer(configuration.GetConnectionString("Default")); - - return new TodoDbContext(builder.Options); - } - - private static IConfigurationRoot BuildConfiguration() - { - var builder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", optional: false); - - return builder.Build(); - } - } -} \ No newline at end of file diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/appsettings.json b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/appsettings.json index 393be04a6c..91cc668167 100644 --- a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/appsettings.json +++ b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=DistEventsDemo;Trusted_Connection=True;TrustServerCertificate=True" + "Default": "Server=localhost,1433;Database=DistEventsDemo;User Id=sa;Password=AbpDemo_123456;TrustServerCertificate=True" }, "RabbitMQ": { "Connections": { diff --git a/test/DistEvents/DistDemoApp.MongoDbKafka/DistDemoApp.MongoDbKafka.csproj b/test/DistEvents/DistDemoApp.MongoDbKafka/DistDemoApp.MongoDbKafka.csproj index abeb206500..ad384a51dd 100644 --- a/test/DistEvents/DistDemoApp.MongoDbKafka/DistDemoApp.MongoDbKafka.csproj +++ b/test/DistEvents/DistDemoApp.MongoDbKafka/DistDemoApp.MongoDbKafka.csproj @@ -4,14 +4,30 @@ Exe net10.0 DistDemoApp + MongoDb - + + + + + + + + + + $(DefineConstants);DISTDEMO_USE_MONGODB + + + + $(DefineConstants);DISTDEMO_USE_EFCORE + + Always diff --git a/test/DistEvents/DistDemoApp.MongoDbKafka/DistDemoAppMongoDbKafkaModule.cs b/test/DistEvents/DistDemoApp.MongoDbKafka/DistDemoAppMongoDbKafkaModule.cs index b2e41b6ca7..b9439da27b 100644 --- a/test/DistEvents/DistDemoApp.MongoDbKafka/DistDemoAppMongoDbKafkaModule.cs +++ b/test/DistEvents/DistDemoApp.MongoDbKafka/DistDemoAppMongoDbKafkaModule.cs @@ -1,37 +1,42 @@ -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.EventBus.Distributed; using Volo.Abp.EventBus.Kafka; +using Volo.Abp.Kafka; using Volo.Abp.Modularity; -using Volo.Abp.MongoDB; -using Volo.Abp.MongoDB.DistributedEvents; namespace DistDemoApp { +#if DISTDEMO_USE_MONGODB [DependsOn( - typeof(AbpMongoDbModule), + typeof(DistDemoAppMongoDbInfrastructureModule), typeof(AbpEventBusKafkaModule), typeof(DistDemoAppSharedModule) )] +#else + [DependsOn( + typeof(DistDemoAppEntityFrameworkCoreInfrastructureModule), + typeof(AbpEventBusKafkaModule), + typeof(DistDemoAppSharedModule) + )] +#endif public class DistDemoAppMongoDbKafkaModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddMongoDbContext(options => +#if DISTDEMO_USE_MONGODB + context.ConfigureDistDemoMongoInfrastructure(); +#else + context.ConfigureDistDemoEntityFrameworkInfrastructure(); +#endif + + Configure(options => { - options.AddDefaultRepositories(); + options.Connections.Default.BootstrapServers = "localhost:9092"; }); - Configure(options => + Configure(options => { - options.Outboxes.Configure(config => - { - config.UseMongoDbContext(); - }); - - options.Inboxes.Configure(config => - { - config.UseMongoDbContext(); - }); + options.ConnectionName = "Default"; + options.TopicName = "DistDemoTopic"; + options.GroupId = "DistDemoApp"; }); } } diff --git a/test/DistEvents/DistDemoApp.MongoDbKafka/Program.cs b/test/DistEvents/DistDemoApp.MongoDbKafka/Program.cs index b048c17389..4d324bce0a 100644 --- a/test/DistEvents/DistDemoApp.MongoDbKafka/Program.cs +++ b/test/DistEvents/DistDemoApp.MongoDbKafka/Program.cs @@ -1,57 +1,36 @@ -using System; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Serilog; -using Serilog.Events; +using Volo.Abp; +using Volo.Abp.Threading; namespace DistDemoApp { public class Program { - public static async Task Main(string[] args) + public static void Main(string[] args) { - Log.Logger = new LoggerConfiguration() -#if DEBUG - .MinimumLevel.Debug() -#else - .MinimumLevel.Information() -#endif - .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) - .Enrich.FromLogContext() - .WriteTo.Async(c => c.File("Logs/logs.txt")) - .WriteTo.Async(c => c.Console()) - .CreateLogger(); - - try - { - Log.Information("Starting console host."); - await CreateHostBuilder(args).RunConsoleAsync(); - return 0; - } - catch (Exception ex) - { - Log.Fatal(ex, "Host terminated unexpectedly!"); - return 1; - } - finally + using (var application = AbpApplicationFactory.Create(options => + { + options.UseAutofac(); + options.Services.AddSerilog((_, _) => + { + }); + options.Services.AddLogging(c => c.AddSerilog()); + })) { - Log.CloseAndFlush(); + Log.Information("Starting DistDemoApp.MongoDbKafka."); + + application.Initialize(); + + AsyncHelper.RunSync( + () => application + .ServiceProvider + .GetRequiredService().CreateTodoItemAsync() + ); + + application.Shutdown(); } } - - internal static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .UseAutofac() - .UseSerilog() - .ConfigureAppConfiguration((context, config) => - { - //setup your additional configuration sources - }) - .ConfigureServices((hostContext, services) => - { - services.AddApplication(); - }); } } diff --git a/test/DistEvents/DistDemoApp.MongoDbKafka/TodoMongoDbContext.cs b/test/DistEvents/DistDemoApp.MongoDbKafka/TodoMongoDbContext.cs deleted file mode 100644 index a7f1b78f86..0000000000 --- a/test/DistEvents/DistDemoApp.MongoDbKafka/TodoMongoDbContext.cs +++ /dev/null @@ -1,26 +0,0 @@ -using MongoDB.Driver; -using Volo.Abp.Data; -using Volo.Abp.MongoDB; -using Volo.Abp.MongoDB.DistributedEvents; - -namespace DistDemoApp -{ - [ConnectionStringName("Default")] - public class TodoMongoDbContext : AbpMongoDbContext, IHasEventOutbox, IHasEventInbox - { - public IMongoCollection TodoItems => Collection(); - public IMongoCollection TodoSummaries => Collection(); - - public IMongoCollection OutgoingEvents - { - get => Collection(); - set {} - } - public IMongoCollection IncomingEvents - { - get => Collection(); - set {} - } - } - -} diff --git a/test/DistEvents/DistDemoApp.MongoDbKafka/appsettings.json b/test/DistEvents/DistDemoApp.MongoDbKafka/appsettings.json index f9ee345d5a..d6d1351b25 100644 --- a/test/DistEvents/DistDemoApp.MongoDbKafka/appsettings.json +++ b/test/DistEvents/DistDemoApp.MongoDbKafka/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "Default": "mongodb://localhost:27018,localhost:27019,localhost:27020/DistEventsDemo" + "Default": "mongodb://localhost:27017/DistEventsDemo?retryWrites=false" }, "Kafka": { "Connections": { diff --git a/test/DistEvents/DistDemoApp.MongoDbRebus/DistDemoApp.MongoDbRebus.csproj b/test/DistEvents/DistDemoApp.MongoDbRebus/DistDemoApp.MongoDbRebus.csproj index 0b04220e3e..49b6d118d2 100644 --- a/test/DistEvents/DistDemoApp.MongoDbRebus/DistDemoApp.MongoDbRebus.csproj +++ b/test/DistEvents/DistDemoApp.MongoDbRebus/DistDemoApp.MongoDbRebus.csproj @@ -4,14 +4,30 @@ Exe net10.0 DistDemoApp + MongoDb - + + + + + + + + + + $(DefineConstants);DISTDEMO_USE_MONGODB + + + + $(DefineConstants);DISTDEMO_USE_EFCORE + + Always diff --git a/test/DistEvents/DistDemoApp.MongoDbRebus/DistDemoAppMongoDbRebusModule.cs b/test/DistEvents/DistDemoApp.MongoDbRebus/DistDemoAppMongoDbRebusModule.cs index 21dab7b9b5..702ffc3f62 100644 --- a/test/DistEvents/DistDemoApp.MongoDbRebus/DistDemoAppMongoDbRebusModule.cs +++ b/test/DistEvents/DistDemoApp.MongoDbRebus/DistDemoAppMongoDbRebusModule.cs @@ -1,19 +1,22 @@ -using Microsoft.Extensions.DependencyInjection; -using Rebus.Persistence.InMem; using Rebus.Transport.InMem; -using Volo.Abp.EventBus.Distributed; using Volo.Abp.EventBus.Rebus; using Volo.Abp.Modularity; -using Volo.Abp.MongoDB; -using Volo.Abp.MongoDB.DistributedEvents; namespace DistDemoApp { +#if DISTDEMO_USE_MONGODB [DependsOn( - typeof(AbpMongoDbModule), + typeof(DistDemoAppMongoDbInfrastructureModule), typeof(AbpEventBusRebusModule), typeof(DistDemoAppSharedModule) )] +#else + [DependsOn( + typeof(DistDemoAppEntityFrameworkCoreInfrastructureModule), + typeof(AbpEventBusRebusModule), + typeof(DistDemoAppSharedModule) + )] +#endif public class DistDemoAppMongoDbRebusModule : AbpModule { public override void PreConfigureServices(ServiceConfigurationContext context) @@ -24,30 +27,17 @@ namespace DistDemoApp options.Configurer = rebusConfigurer => { rebusConfigurer.Transport(t => t.UseInMemoryTransport(new InMemNetwork(), "eventbus")); - rebusConfigurer.Subscriptions(s => s.StoreInMemory()); }; }); } public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddMongoDbContext(options => - { - options.AddDefaultRepositories(); - }); - - Configure(options => - { - options.Outboxes.Configure(config => - { - config.UseMongoDbContext(); - }); - - options.Inboxes.Configure(config => - { - config.UseMongoDbContext(); - }); - }); +#if DISTDEMO_USE_MONGODB + context.ConfigureDistDemoMongoInfrastructure(); +#else + context.ConfigureDistDemoEntityFrameworkInfrastructure(); +#endif } } } diff --git a/test/DistEvents/DistDemoApp.MongoDbRebus/Program.cs b/test/DistEvents/DistDemoApp.MongoDbRebus/Program.cs index 9f8c1e6c56..17a11b5f17 100644 --- a/test/DistEvents/DistDemoApp.MongoDbRebus/Program.cs +++ b/test/DistEvents/DistDemoApp.MongoDbRebus/Program.cs @@ -1,9 +1,5 @@ -using System; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Serilog; -using Serilog.Events; using Volo.Abp; using Volo.Abp.Threading; @@ -13,48 +9,16 @@ namespace DistDemoApp { public static void Main(string[] args) { -// Log.Logger = new LoggerConfiguration() -// #if DEBUG -// .MinimumLevel.Debug() -// #else -// .MinimumLevel.Information() -// #endif -// .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) -// .Enrich.FromLogContext() -// .WriteTo.Async(c => c.File("Logs/logs.txt")) -// .WriteTo.Async(c => c.Console()) -// .CreateLogger(); -// -// try -// { -// Log.Information("Starting console host."); -// await CreateHostBuilder(args).RunConsoleAsync(); -// return 0; -// } -// catch (Exception ex) -// { -// Log.Fatal(ex, "Host terminated unexpectedly!"); -// return 1; -// } -// finally -// { -// Log.CloseAndFlush(); -// } - using (var application = AbpApplicationFactory.Create(options => { options.UseAutofac(); - options.Services.AddSerilog((serviceProvider, c) => + options.Services.AddSerilog((_, _) => { - // c.Enrich.FromLogContext() - // .WriteTo.Async(c => c.File("Logs/logs.txt")) - // .WriteTo.Async(c => c.Console()) - // .WriteTo.AbpStudio(serviceProvider); }); options.Services.AddLogging(c => c.AddSerilog()); })) { - Log.Information("Starting Volo.AbpIo.DbMigrator."); + Log.Information("Starting DistDemoApp.MongoDbRebus."); application.Initialize(); @@ -68,18 +32,5 @@ namespace DistDemoApp } } - - internal static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .UseAutofac() - .UseSerilog() - .ConfigureAppConfiguration((context, config) => - { - //setup your additional configuration sources - }) - .ConfigureServices((hostContext, services) => - { - services.AddApplication(); - }); } } diff --git a/test/DistEvents/DistDemoApp.MongoDbRebus/TodoMongoDbContext.cs b/test/DistEvents/DistDemoApp.MongoDbRebus/TodoMongoDbContext.cs deleted file mode 100644 index 95370bb4d2..0000000000 --- a/test/DistEvents/DistDemoApp.MongoDbRebus/TodoMongoDbContext.cs +++ /dev/null @@ -1,19 +0,0 @@ -using MongoDB.Driver; -using Volo.Abp.Data; -using Volo.Abp.MongoDB; -using Volo.Abp.MongoDB.DistributedEvents; - -namespace DistDemoApp -{ - [ConnectionStringName("Default")] - public class TodoMongoDbContext : AbpMongoDbContext, IHasEventOutbox, IHasEventInbox - { - public IMongoCollection TodoItems => Collection(); - public IMongoCollection TodoSummaries => Collection(); - - public IMongoCollection OutgoingEvents => Collection(); - - public IMongoCollection IncomingEvents => Collection(); - } - -} diff --git a/test/DistEvents/DistDemoApp.MongoDbRebus/appsettings.json b/test/DistEvents/DistDemoApp.MongoDbRebus/appsettings.json index f9ee345d5a..d6d1351b25 100644 --- a/test/DistEvents/DistDemoApp.MongoDbRebus/appsettings.json +++ b/test/DistEvents/DistDemoApp.MongoDbRebus/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "Default": "mongodb://localhost:27018,localhost:27019,localhost:27020/DistEventsDemo" + "Default": "mongodb://localhost:27017/DistEventsDemo?retryWrites=false" }, "Kafka": { "Connections": { diff --git a/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoApp.Persistence.EntityFrameworkCore.csproj b/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoApp.Persistence.EntityFrameworkCore.csproj new file mode 100644 index 0000000000..2adc9e976e --- /dev/null +++ b/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoApp.Persistence.EntityFrameworkCore.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + DistDemoApp + EntityFrameworkCore + + + + + + + + + + + + diff --git a/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoAppEntityFrameworkCoreInfrastructureModule.cs b/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoAppEntityFrameworkCoreInfrastructureModule.cs new file mode 100644 index 0000000000..1c7c094953 --- /dev/null +++ b/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoAppEntityFrameworkCoreInfrastructureModule.cs @@ -0,0 +1,12 @@ +using Volo.Abp.EntityFrameworkCore.SqlServer; +using Volo.Abp.Modularity; + +namespace DistDemoApp; + +[DependsOn( + typeof(AbpEntityFrameworkCoreSqlServerModule), + typeof(DistDemoAppSharedModule) +)] +public class DistDemoAppEntityFrameworkCoreInfrastructureModule : AbpModule +{ +} diff --git a/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoEntityFrameworkServiceCollectionExtensions.cs b/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoEntityFrameworkServiceCollectionExtensions.cs new file mode 100644 index 0000000000..f2aa5f874e --- /dev/null +++ b/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoEntityFrameworkServiceCollectionExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Modularity; + +namespace DistDemoApp; + +public static class DistDemoEntityFrameworkServiceCollectionExtensions +{ + public static void ConfigureDistDemoEntityFrameworkInfrastructure(this ServiceConfigurationContext context) + { + context.Services.AddAbpDbContext(options => + { + options.AddDefaultRepositories(); + }); + + context.Services.Configure(options => + { + options.UseSqlServer(); + }); + } +} diff --git a/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/TodoDbContext.cs b/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/TodoDbContext.cs new file mode 100644 index 0000000000..a352e4a5b0 --- /dev/null +++ b/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/TodoDbContext.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore.DistributedEvents; + +namespace DistDemoApp; + +public class TodoDbContext : AbpDbContext, IHasEventOutbox, IHasEventInbox +{ + public DbSet TodoItems { get; set; } = null!; + + public DbSet TodoSummaries { get; set; } = null!; + + public DbSet OutgoingEvents { get; set; } = null!; + + public DbSet IncomingEvents { get; set; } = null!; + + public TodoDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.ConfigureEventOutbox(); + modelBuilder.ConfigureEventInbox(); + + modelBuilder.Entity(b => + { + b.Property(x => x.Text).IsRequired().HasMaxLength(128); + }); + } +} diff --git a/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/TodoDbContextFactory.cs b/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/TodoDbContextFactory.cs new file mode 100644 index 0000000000..082c85bb29 --- /dev/null +++ b/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/TodoDbContextFactory.cs @@ -0,0 +1,28 @@ +using System.IO; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; + +namespace DistDemoApp; + +public class TodoDbContextFactory : IDesignTimeDbContextFactory +{ + public TodoDbContext CreateDbContext(string[] args) + { + var configuration = BuildConfiguration(); + + var builder = new DbContextOptionsBuilder() + .UseSqlServer(configuration.GetConnectionString("Default")); + + return new TodoDbContext(builder.Options); + } + + private static IConfigurationRoot BuildConfiguration() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false); + + return builder.Build(); + } +} diff --git a/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoApp.Persistence.MongoDb.csproj b/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoApp.Persistence.MongoDb.csproj new file mode 100644 index 0000000000..8c9e3e14c5 --- /dev/null +++ b/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoApp.Persistence.MongoDb.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + DistDemoApp + MongoDb + + + + + + + + diff --git a/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoAppMongoDbInfrastructureModule.cs b/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoAppMongoDbInfrastructureModule.cs new file mode 100644 index 0000000000..390badca6f --- /dev/null +++ b/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoAppMongoDbInfrastructureModule.cs @@ -0,0 +1,12 @@ +using Volo.Abp.Modularity; +using Volo.Abp.MongoDB; + +namespace DistDemoApp; + +[DependsOn( + typeof(AbpMongoDbModule), + typeof(DistDemoAppSharedModule) +)] +public class DistDemoAppMongoDbInfrastructureModule : AbpModule +{ +} diff --git a/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoMongoDbContext.cs b/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoMongoDbContext.cs new file mode 100644 index 0000000000..4c63400dc3 --- /dev/null +++ b/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoMongoDbContext.cs @@ -0,0 +1,26 @@ +using MongoDB.Driver; +using Volo.Abp.Data; +using Volo.Abp.MongoDB; +using Volo.Abp.MongoDB.DistributedEvents; + +namespace DistDemoApp; + +[ConnectionStringName("Default")] +public class DistDemoMongoDbContext : AbpMongoDbContext, IHasEventOutbox, IHasEventInbox +{ + public IMongoCollection TodoItems => Collection(); + + public IMongoCollection TodoSummaries => Collection(); + + public IMongoCollection OutgoingEvents + { + get => Collection(); + set { } + } + + public IMongoCollection IncomingEvents + { + get => Collection(); + set { } + } +} diff --git a/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoMongoServiceCollectionExtensions.cs b/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoMongoServiceCollectionExtensions.cs new file mode 100644 index 0000000000..48a8285b54 --- /dev/null +++ b/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoMongoServiceCollectionExtensions.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Data; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.Modularity; +using Volo.Abp.MongoDB; +using Volo.Abp.MongoDB.DistributedEvents; + +namespace DistDemoApp; + +public static class DistDemoMongoServiceCollectionExtensions +{ + private const string DefaultMongoConnectionString = "mongodb://localhost:27017/DistEventsDemo?retryWrites=false"; + + public static void ConfigureDistDemoMongoInfrastructure(this ServiceConfigurationContext context) + { + context.Services.AddMongoDbContext(options => + { + options.AddDefaultRepositories(); + }); + + context.Services.Configure(options => + { + if (string.IsNullOrWhiteSpace(options.ConnectionStrings.Default)) + { + options.ConnectionStrings.Default = DefaultMongoConnectionString; + } + }); + + context.Services.Configure(options => + { + options.Outboxes.Configure(config => + { + config.UseMongoDbContext(); + }); + + options.Inboxes.Configure(config => + { + config.UseMongoDbContext(); + }); + }); + } +} diff --git a/test/DistEvents/DistDemoApp.Shared/DemoService.cs b/test/DistEvents/DistDemoApp.Shared/DemoService.cs index c970485252..cc6d6f64c6 100644 --- a/test/DistEvents/DistDemoApp.Shared/DemoService.cs +++ b/test/DistEvents/DistDemoApp.Shared/DemoService.cs @@ -1,29 +1,21 @@ -using System; +using System; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; namespace DistDemoApp { public class DemoService : ITransientDependency { - private readonly IRepository _todoItemRepository; + private readonly IDistEventScenarioRunner _scenarioRunner; - public DemoService(IRepository todoItemRepository) + public DemoService(IDistEventScenarioRunner scenarioRunner) { - _todoItemRepository = todoItemRepository; + _scenarioRunner = scenarioRunner; } - - public async Task CreateTodoItemAsync() + + public virtual async Task CreateTodoItemAsync() { - var todoItem = await _todoItemRepository.InsertAsync( - new TodoItem - { - Text = "todo item " + DateTime.Now.Ticks - } - ); - - Console.WriteLine("Created a new todo item: " + todoItem); + await _scenarioRunner.RunAsync(DistEventScenarioProfile.Default()); } } } \ No newline at end of file diff --git a/test/DistEvents/DistDemoApp.Shared/DistDemoAppHostedService.cs b/test/DistEvents/DistDemoApp.Shared/DistDemoAppHostedService.cs index ba72d6902a..90f27cb638 100644 --- a/test/DistEvents/DistDemoApp.Shared/DistDemoAppHostedService.cs +++ b/test/DistEvents/DistDemoApp.Shared/DistDemoAppHostedService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; @@ -11,21 +11,22 @@ namespace DistDemoApp private readonly IAbpApplicationWithExternalServiceProvider _application; private readonly IServiceProvider _serviceProvider; private readonly DemoService _demoService; + private readonly IHostApplicationLifetime _hostApplicationLifetime; public DistDemoAppHostedService( IAbpApplicationWithExternalServiceProvider application, IServiceProvider serviceProvider, - DemoService demoService) + DemoService demoService, + IHostApplicationLifetime hostApplicationLifetime) { _application = application; _serviceProvider = serviceProvider; _demoService = demoService; + _hostApplicationLifetime = hostApplicationLifetime; } public async Task StartAsync(CancellationToken cancellationToken) { - _application.Initialize(_serviceProvider); - await _demoService.CreateTodoItemAsync(); } diff --git a/test/DistEvents/DistDemoApp.Shared/DistDemoAppSharedModule.cs b/test/DistEvents/DistDemoApp.Shared/DistDemoAppSharedModule.cs index 936264e828..09c458bf85 100644 --- a/test/DistEvents/DistDemoApp.Shared/DistDemoAppSharedModule.cs +++ b/test/DistEvents/DistDemoApp.Shared/DistDemoAppSharedModule.cs @@ -1,4 +1,4 @@ -using Medallion.Threading; +using Medallion.Threading; using Medallion.Threading.Redis; using Microsoft.Extensions.DependencyInjection; using StackExchange.Redis; @@ -21,7 +21,7 @@ namespace DistDemoApp { var configuration = context.Services.GetConfiguration(); - context.Services.AddHostedService(); + // context.Services.AddHostedService(); Configure(options => { diff --git a/test/DistEvents/DistDemoApp.Shared/DistEventScenarioProfile.cs b/test/DistEvents/DistDemoApp.Shared/DistEventScenarioProfile.cs new file mode 100644 index 0000000000..5dc360cfe9 --- /dev/null +++ b/test/DistEvents/DistDemoApp.Shared/DistEventScenarioProfile.cs @@ -0,0 +1,60 @@ +namespace DistDemoApp; + +public class DistEventScenarioProfile +{ + public string Name { get; set; } = "default"; + + public string AnonymousOnlyEventName { get; set; } = "dist-demo.anonymous-only"; + + public string AnonymousOnlyMessage { get; set; } = "hello-anonymous"; + + public int TypedFromTypedValue { get; set; } = 7; + + public int TypedFromAnonymousValue { get; set; } = 11; + + public bool EnableTypedFromTypedScenario { get; set; } = true; + + public bool EnableTypedFromAnonymousScenario { get; set; } = true; + + public bool EnableAnonymousOnlyScenario { get; set; } = true; + + public bool OnUnitOfWorkComplete { get; set; } = true; + + public bool UseOutbox { get; set; } = true; + + public bool UseUnitOfWork { get; set; } = true; + + public int WarmupDelayMs { get; set; } = 1500; + + public int TimeoutSeconds { get; set; } = 60; + + public static DistEventScenarioProfile Default() + { + return new DistEventScenarioProfile(); + } + + public static DistEventScenarioProfile DaprWeb() + { + return new DistEventScenarioProfile + { + Name = "dapr-web", + AnonymousOnlyEventName = "dist-demo.dapr.anonymous-only", + AnonymousOnlyMessage = "hello-dapr-web", + EnableTypedFromTypedScenario = false, + EnableTypedFromAnonymousScenario = false, + EnableAnonymousOnlyScenario = false + }; + } + + public static DistEventScenarioProfile AzureEmulator() + { + return new DistEventScenarioProfile + { + Name = "azure-emulator", + AnonymousOnlyEventName = "DistDemoApp.Azure.AnonymousOnly", + AnonymousOnlyMessage = "hello-azure-emulator", + TypedFromTypedValue = 21, + TypedFromAnonymousValue = 34 + }; + } +} diff --git a/test/DistEvents/DistDemoApp.Shared/DistEventScenarioRunner.cs b/test/DistEvents/DistDemoApp.Shared/DistEventScenarioRunner.cs new file mode 100644 index 0000000000..a9601335f8 --- /dev/null +++ b/test/DistEvents/DistDemoApp.Shared/DistEventScenarioRunner.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.Uow; + +namespace DistDemoApp; + +public class DistEventScenarioRunner : IDistEventScenarioRunner, ITransientDependency +{ + private readonly IDistributedEventBus _distributedEventBus; + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public DistEventScenarioRunner( + IDistributedEventBus distributedEventBus, + IUnitOfWorkManager unitOfWorkManager) + { + _distributedEventBus = distributedEventBus; + _unitOfWorkManager = unitOfWorkManager; + } + + public async Task RunAsync(DistEventScenarioProfile profile) + { + var typedEventName = EventNameAttribute.GetNameOrDefault(); + + var typedFromTypedPublish = profile.EnableTypedFromTypedScenario + ? new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + : null; + var typedFromAnonymousPublish = profile.EnableTypedFromAnonymousScenario + ? new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + : null; + var anonymousOnlyPublish = profile.EnableAnonymousOnlyScenario + ? new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + : null; + + using var typedSubscription = _distributedEventBus.Subscribe(eventData => + { + if (typedFromTypedPublish != null && eventData.Value == profile.TypedFromTypedValue) + { + typedFromTypedPublish.TrySetResult(eventData.Value); + } + + if (typedFromAnonymousPublish != null && eventData.Value == profile.TypedFromAnonymousValue) + { + typedFromAnonymousPublish.TrySetResult(eventData.Value); + } + + return Task.CompletedTask; + }); + + IDisposable? anonymousOnlySubscription = null; + if (profile.EnableAnonymousOnlyScenario) + { + anonymousOnlySubscription = _distributedEventBus.Subscribe( + profile.AnonymousOnlyEventName, + new SingleInstanceHandlerFactory( + new ActionEventHandler(eventData => + { + var converted = eventData.ConvertToTypedObject(); + if (converted is Dictionary payload && + payload.TryGetValue("Message", out var message) && + message?.ToString() == profile.AnonymousOnlyMessage) + { + anonymousOnlyPublish!.TrySetResult(true); + } + + return Task.CompletedTask; + }))); + } + + await Task.Delay(profile.WarmupDelayMs); + + if (profile.UseUnitOfWork) + { + using var uow = _unitOfWorkManager.Begin(); + await PublishScenarioEventsAsync(profile, typedEventName); + await uow.CompleteAsync(); + } + else + { + await PublishScenarioEventsAsync(profile, typedEventName); + } + + if (typedFromTypedPublish != null) + { + await typedFromTypedPublish.Task.WaitAsync(TimeSpan.FromSeconds(profile.TimeoutSeconds)); + } + + if (typedFromAnonymousPublish != null) + { + await typedFromAnonymousPublish.Task.WaitAsync(TimeSpan.FromSeconds(profile.TimeoutSeconds)); + } + + if (anonymousOnlyPublish != null) + { + await anonymousOnlyPublish.Task.WaitAsync(TimeSpan.FromSeconds(profile.TimeoutSeconds)); + } + + anonymousOnlySubscription?.Dispose(); + + Console.WriteLine($"All distributed event scenarios passed ({profile.Name})."); + } + + private async Task PublishScenarioEventsAsync(DistEventScenarioProfile profile, string typedEventName) + { + if (profile.EnableTypedFromTypedScenario) + { + await _distributedEventBus.PublishAsync( + new ProviderScenarioEvent { Value = profile.TypedFromTypedValue }, + onUnitOfWorkComplete: profile.OnUnitOfWorkComplete, + useOutbox: profile.UseOutbox); + } + + if (profile.EnableTypedFromAnonymousScenario) + { + await _distributedEventBus.PublishAsync( + typedEventName, + new { Value = profile.TypedFromAnonymousValue }, + onUnitOfWorkComplete: profile.OnUnitOfWorkComplete, + useOutbox: profile.UseOutbox); + } + + if (profile.EnableAnonymousOnlyScenario) + { + await _distributedEventBus.PublishAsync( + profile.AnonymousOnlyEventName, + new { Message = profile.AnonymousOnlyMessage }, + onUnitOfWorkComplete: profile.OnUnitOfWorkComplete, + useOutbox: profile.UseOutbox); + } + } +} diff --git a/test/DistEvents/DistDemoApp.Shared/IDistEventScenarioRunner.cs b/test/DistEvents/DistDemoApp.Shared/IDistEventScenarioRunner.cs new file mode 100644 index 0000000000..6961ecdbaf --- /dev/null +++ b/test/DistEvents/DistDemoApp.Shared/IDistEventScenarioRunner.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace DistDemoApp; + +public interface IDistEventScenarioRunner +{ + Task RunAsync(DistEventScenarioProfile profile); +} diff --git a/test/DistEvents/DistDemoApp.Shared/ProviderScenarioEvent.cs b/test/DistEvents/DistDemoApp.Shared/ProviderScenarioEvent.cs new file mode 100644 index 0000000000..08dd2102c2 --- /dev/null +++ b/test/DistEvents/DistDemoApp.Shared/ProviderScenarioEvent.cs @@ -0,0 +1,9 @@ +using Volo.Abp.EventBus; + +namespace DistDemoApp; + +[EventName("DistDemoApp.ProviderScenarioEvent")] +public class ProviderScenarioEvent +{ + public int Value { get; set; } +} diff --git a/test/DistEvents/DistEventsDemo.slnx b/test/DistEvents/DistEventsDemo.slnx index 595e2e5d3e..d31f2b3262 100644 --- a/test/DistEvents/DistEventsDemo.slnx +++ b/test/DistEvents/DistEventsDemo.slnx @@ -1,5 +1,9 @@ + + + + diff --git a/test/DistEvents/dapr/components/pubsub.yaml b/test/DistEvents/dapr/components/pubsub.yaml new file mode 100644 index 0000000000..669a6149b5 --- /dev/null +++ b/test/DistEvents/dapr/components/pubsub.yaml @@ -0,0 +1,12 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: pubsub +spec: + type: pubsub.redis + version: v1 + metadata: + - name: redisHost + value: redis:6379 + - name: redisPassword + value: "" diff --git a/test/DistEvents/docker-compose.yml b/test/DistEvents/docker-compose.yml new file mode 100644 index 0000000000..916b6542f7 --- /dev/null +++ b/test/DistEvents/docker-compose.yml @@ -0,0 +1,131 @@ +services: + rabbitmq: + image: rabbitmq:3.13-management + container_name: distevents-rabbitmq + ports: + - "5672:5672" + - "15672:15672" + environment: + RABBITMQ_DEFAULT_USER: guest + RABBITMQ_DEFAULT_PASS: guest + + zookeeper: + image: confluentinc/cp-zookeeper:7.6.0 + container_name: distevents-zookeeper + ports: + - "2181:2181" + environment: + ZOOKEEPER_CLIENT_PORT: "2181" + ZOOKEEPER_TICK_TIME: "2000" + + kafka: + image: confluentinc/cp-kafka:7.6.0 + container_name: distevents-kafka + depends_on: + - zookeeper + ports: + - "9092:9092" + environment: + KAFKA_BROKER_ID: "1" + KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181" + KAFKA_LISTENERS: "PLAINTEXT://0.0.0.0:9092" + KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://localhost:9092" + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "PLAINTEXT:PLAINTEXT" + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: "1" + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" + + mongodb: + image: mongo:7 + container_name: distevents-mongodb + command: ["mongod", "--replSet", "rs0", "--bind_ip_all"] + ports: + - "27017:27017" + healthcheck: + test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"] + interval: 5s + timeout: 3s + retries: 20 + + mongodb-rs-init: + image: mongo:7 + container_name: distevents-mongodb-rs-init + depends_on: + mongodb: + condition: service_healthy + restart: "no" + command: + [ + "mongosh", + "--host", "mongodb:27017", + "--quiet", + "--eval", + "try { rs.status() } catch (e) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" + ] + + redis: + image: redis:7-alpine + container_name: distevents-redis + ports: + - "6379:6379" + + dapr-placement: + image: daprio/dapr:1.14.4 + container_name: distevents-dapr-placement + command: ["./placement", "-port", "50006"] + ports: + - "50006:50006" + + daprd: + image: daprio/dapr:1.14.4 + container_name: distevents-daprd + depends_on: + - redis + - dapr-placement + command: + [ + "./daprd", + "--app-id", "dist-demo-dapr", + "--app-port", "8090", + "--app-channel-address", "host.docker.internal", + "--app-protocol", "http", + "--resources-path", "/components", + "--placement-host-address", "dapr-placement:50006", + "--dapr-http-port", "3500", + "--dapr-grpc-port", "50001" + ] + volumes: + - ./dapr/components:/components + ports: + - "3500:3500" + - "50001:50001" + + servicebus-sql: + image: mcr.microsoft.com/azure-sql-edge:latest + container_name: distevents-servicebus-sql + environment: + ACCEPT_EULA: "Y" + MSSQL_SA_PASSWORD: "AbpDemo_123456" + + servicebus-emulator: + image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest + container_name: distevents-servicebus-emulator + depends_on: + - servicebus-sql + environment: + SQL_SERVER: servicebus-sql + MSSQL_SA_PASSWORD: "AbpDemo_123456" + ACCEPT_EULA: "Y" + volumes: + - ./servicebus-emulator/Config.json:/ServiceBus_Emulator/ConfigFiles/Config.json + ports: + - "5673:5672" + - "5300:5300" + + sqlserver: + image: mcr.microsoft.com/mssql/server:2022-latest + container_name: distevents-sqlserver + ports: + - "1433:1433" + environment: + ACCEPT_EULA: "Y" + MSSQL_SA_PASSWORD: "AbpDemo_123456" diff --git a/test/DistEvents/servicebus-emulator/Config.json b/test/DistEvents/servicebus-emulator/Config.json new file mode 100644 index 0000000000..39908ff27d --- /dev/null +++ b/test/DistEvents/servicebus-emulator/Config.json @@ -0,0 +1,22 @@ +{ + "UserConfig": { + "Namespaces": [ + { + "Name": "sbemulatorns", + "Topics": [ + { + "Name": "DistDemoAzureTopic", + "Subscriptions": [ + { + "Name": "DistDemoAzureSubscriber" + } + ] + } + ] + } + ], + "Logging": { + "Type": "File" + } + } +} From 721dbe0f9bfdf0e693201c273a1e67211e959012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Tue, 10 Mar 2026 13:02:52 +0300 Subject: [PATCH 10/22] Refactor anonymous event handling and subscriptions Centralize and simplify anonymous event handling across transports. Introduces AnonymousEventDataConverter and changes AnonymousEventData to store optional JsonData with a FromJson factory. Adds helpers (CreateAnonymousEnvelope, TryPublishTypedByEventNameAsync, TryResolveStoredEventData, TryResolveIncomingEvent) and unifies handling logic in Dapr/Azure/Kafka/RabbitMQ/Rebus implementations. Also deduplicates subscription registration using locking, cleans up empty anonymous handler maps, and removes duplicated JSON conversion code. Tests updated to match the new anonymous event semantics. --- .../AbpAspNetCoreMvcDaprEventBusModule.cs | 81 +++++-- .../Volo/Abp/EventBus/AnonymousEventData.cs | 115 ++-------- .../EventBus/AnonymousEventDataConverter.cs | 95 ++++++++ .../Azure/AzureDistributedEventBus.cs | 181 +++++++++++----- .../EventBus/Dapr/DaprDistributedEventBus.cs | 187 +++++++++++----- .../Kafka/KafkaDistributedEventBus.cs | 179 ++++++++++----- .../RabbitMq/RabbitMqDistributedEventBus.cs | 188 +++++++++++----- .../Rebus/RebusDistributedEventBus.cs | 203 ++++++++++++------ .../Distributed/DistributedEventBusBase.cs | 74 +++++-- .../Distributed/LocalDistributedEventBus.cs | 139 ++++++++---- .../Volo/Abp/EventBus/EventBusBase.cs | 16 +- .../Volo/Abp/EventBus/Local/LocalEventBus.cs | 129 ++++++++--- .../LocalDistributedEventBus_Test.cs | 144 ++++++++++++- .../Local/LocalEventBus_Anonymous_Test.cs | 36 +++- 14 files changed, 1261 insertions(+), 506 deletions(-) create mode 100644 framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventDataConverter.cs diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs index f770f5fbb2..079ed1287a 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; @@ -96,30 +96,11 @@ public class AbpAspNetCoreMvcDaprEventBusModule : AbpModule if (IsAbpDaprEventData(data)) { - var daprEventData = daprSerializer.Deserialize(data, typeof(AbpDaprEventData)).As(); - var eventType = distributedEventBus.GetEventType(daprEventData.Topic); - if (eventType != null) - { - var eventData = daprSerializer.Deserialize(daprEventData.JsonData, eventType); - await distributedEventBus.TriggerHandlersAsync(eventType, eventData, daprEventData.MessageId, daprEventData.CorrelationId); - }else if (distributedEventBus.IsAnonymousEvent(daprEventData.Topic)) - { - var eventData = daprSerializer.Deserialize(daprEventData.JsonData, typeof(object)); - await distributedEventBus.TriggerHandlersAsync(typeof(AnonymousEventData), new AnonymousEventData(daprEventData.Topic, eventData), daprEventData.MessageId, daprEventData.CorrelationId); - } + await TryHandleAbpDaprEnvelopeAsync(distributedEventBus, daprSerializer, data); } else { - var eventType = distributedEventBus.GetEventType(topic); - if (eventType != null) - { - var eventData = daprSerializer.Deserialize(data, eventType); - await distributedEventBus.TriggerHandlersAsync(eventType, eventData); - }else if (distributedEventBus.IsAnonymousEvent(topic)) - { - var eventData = daprSerializer.Deserialize(data, typeof(object)); - await distributedEventBus.TriggerHandlersAsync(typeof(AnonymousEventData), new AnonymousEventData(topic, eventData)); - } + await TryHandleDirectDaprEventAsync(distributedEventBus, daprSerializer, topic!, data); } httpContext.Response.StatusCode = 200; @@ -136,4 +117,60 @@ public class AbpAspNetCoreMvcDaprEventBusModule : AbpModule objects.Any(x => x.Name.Equals("JsonData", StringComparison.CurrentCultureIgnoreCase)) && objects.Any(x => x.Name.Equals("CorrelationId", StringComparison.CurrentCultureIgnoreCase)); } + + private static async Task TryHandleAbpDaprEnvelopeAsync( + DaprDistributedEventBus distributedEventBus, + IDaprSerializer daprSerializer, + string data) + { + var daprEventData = daprSerializer.Deserialize(data, typeof(AbpDaprEventData)).As(); + if (!TryResolveIncomingEvent(distributedEventBus, daprSerializer, daprEventData.Topic, daprEventData.JsonData, out var eventType, out var eventData)) + { + return; + } + + await distributedEventBus.TriggerHandlersAsync(eventType, eventData, daprEventData.MessageId, daprEventData.CorrelationId); + } + + private static async Task TryHandleDirectDaprEventAsync( + DaprDistributedEventBus distributedEventBus, + IDaprSerializer daprSerializer, + string topic, + string data) + { + if (!TryResolveIncomingEvent(distributedEventBus, daprSerializer, topic, data, out var eventType, out var eventData)) + { + return; + } + + await distributedEventBus.TriggerHandlersAsync(eventType, eventData); + } + + private static bool TryResolveIncomingEvent( + DaprDistributedEventBus distributedEventBus, + IDaprSerializer daprSerializer, + string topic, + string data, + out Type eventType, + out object eventData) + { + var typedEventType = distributedEventBus.GetEventType(topic); + if (typedEventType != null) + { + eventType = typedEventType; + eventData = daprSerializer.Deserialize(data, typedEventType); + return true; + } + + if (!distributedEventBus.IsAnonymousEvent(topic)) + { + eventType = default!; + eventData = default!; + return false; + } + + eventType = typeof(AnonymousEventData); + eventData = AnonymousEventData.FromJson(topic, data); + return true; + } } diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs index 1db318b40e..255b4cffff 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; - namespace Volo.Abp.EventBus; /// @@ -18,11 +14,14 @@ public class AnonymousEventData public string EventName { get; } /// - /// The raw event data payload. Can be a CLR object, , or any serializable object. + /// The raw event data payload. Can be a CLR object, , or any serializable object. /// - public object Data { get; } + internal object Data { get; } - private JsonElement? _cachedJsonElement; + /// + /// The raw JSON payload when the event is created from transport data. + /// + public string? JsonData { get; } /// /// Creates a new instance of . @@ -36,105 +35,17 @@ public class AnonymousEventData } /// - /// Converts the to a loosely-typed object graph - /// (dictionaries for objects, lists for arrays, primitives for values). - /// - /// A CLR object representation of the event data - public object ConvertToTypedObject() - { - return ConvertElement(GetJsonElement()); - } - - /// - /// Converts the to a strongly-typed object. - /// Returns the data directly if it is already of type , - /// otherwise deserializes from JSON. - /// - /// Target type to convert to - /// The deserialized object of type - /// Thrown when deserialization fails - public T ConvertToTypedObject() - { - if (Data is T typedData) - { - return typedData; - } - - return GetJsonElement().Deserialize() - ?? throw new InvalidOperationException($"Failed to deserialize AnonymousEventData to {typeof(T).FullName}."); - } - - /// - /// Converts the to the specified . - /// Returns the data directly if it is already an instance of the target type, - /// otherwise deserializes from JSON. + /// Creates a new instance of from raw JSON. /// - /// Target type to convert to - /// The deserialized object - /// Thrown when deserialization fails - public object ConvertToTypedObject(Type type) + public static AnonymousEventData FromJson(string eventName, string jsonData) { - if (type.IsInstanceOfType(Data)) - { - return Data; - } - - return GetJsonElement().Deserialize(type) - ?? throw new InvalidOperationException($"Failed to deserialize AnonymousEventData to {type.FullName}."); + return new AnonymousEventData(eventName, data: null!, jsonData); } - private JsonElement GetJsonElement() + private AnonymousEventData(string eventName, object data, string? jsonData) { - if (_cachedJsonElement.HasValue) - { - return _cachedJsonElement.Value; - } - - if (Data is JsonElement existingElement) - { - _cachedJsonElement = existingElement; - return existingElement; - } - - _cachedJsonElement = JsonSerializer.SerializeToElement(Data); - return _cachedJsonElement.Value; - } - - private static object ConvertElement(JsonElement element) - { - switch (element.ValueKind) - { - case JsonValueKind.Object: - { - var obj = new Dictionary(); - foreach (var property in element.EnumerateObject()) - { - obj[property.Name] = property.Value.ValueKind == JsonValueKind.Null - ? null - : ConvertElement(property.Value); - } - return obj; - } - case JsonValueKind.Array: - return element.EnumerateArray() - .Select(item => item.ValueKind == JsonValueKind.Null ? null : (object?)ConvertElement(item)) - .ToList(); - case JsonValueKind.String: - return element.GetString()!; - case JsonValueKind.Number when element.TryGetInt64(out var longValue): - return longValue; - case JsonValueKind.Number when element.TryGetDecimal(out var decimalValue): - return decimalValue; - case JsonValueKind.Number when element.TryGetDouble(out var doubleValue): - return doubleValue; - case JsonValueKind.True: - return true; - case JsonValueKind.False: - return false; - case JsonValueKind.Null: - case JsonValueKind.Undefined: - default: - return null!; - } + EventName = eventName; + Data = data; + JsonData = jsonData; } } diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventDataConverter.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventDataConverter.cs new file mode 100644 index 0000000000..e4b9234ccd --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventDataConverter.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; + +namespace Volo.Abp.EventBus; + +public static class AnonymousEventDataConverter +{ + public static T ConvertToTypedObject(AnonymousEventData eventData) + { + if (eventData.Data is T typedData) + { + return typedData; + } + + return ParseJsonElement(eventData).Deserialize() + ?? throw new InvalidOperationException($"Failed to deserialize AnonymousEventData to {typeof(T).FullName}."); + } + + public static object ConvertToTypedObject(AnonymousEventData eventData, Type type) + { + if (type.IsInstanceOfType(eventData.Data)) + { + return eventData.Data; + } + + return ParseJsonElement(eventData).Deserialize(type) + ?? throw new InvalidOperationException($"Failed to deserialize AnonymousEventData to {type.FullName}."); + } + + public static object ConvertToLooseObject(AnonymousEventData eventData) + { + return ConvertElement(ParseJsonElement(eventData)); + } + + public static string GetJsonData(AnonymousEventData eventData) + { + return eventData.JsonData ?? ParseJsonElement(eventData).GetRawText(); + } + + private static JsonElement ParseJsonElement(AnonymousEventData eventData) + { + if (eventData.Data is JsonElement existingElement) + { + return existingElement; + } + + if (eventData.JsonData != null) + { + return JsonDocument.Parse(eventData.JsonData).RootElement.Clone(); + } + + return JsonSerializer.SerializeToElement(eventData.Data); + } + + private static object ConvertElement(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + { + var obj = new Dictionary(); + foreach (var property in element.EnumerateObject()) + { + obj[property.Name] = property.Value.ValueKind == JsonValueKind.Null + ? null + : ConvertElement(property.Value); + } + + return obj; + } + case JsonValueKind.Array: + return element.EnumerateArray() + .Select(item => item.ValueKind == JsonValueKind.Null ? null : (object?)ConvertElement(item)) + .ToList(); + case JsonValueKind.String: + return element.GetString()!; + case JsonValueKind.Number when element.TryGetInt64(out var longValue): + return longValue; + case JsonValueKind.Number when element.TryGetDecimal(out var decimalValue): + return decimalValue; + case JsonValueKind.Number when element.TryGetDouble(out var doubleValue): + return doubleValue; + case JsonValueKind.True: + return true; + case JsonValueKind.False: + return false; + case JsonValueKind.Null: + case JsonValueKind.Undefined: + default: + return null!; + } + } +} diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index 020757ad07..83eba73fa3 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Threading.Tasks; using Azure.Messaging.ServiceBus; using Microsoft.Extensions.DependencyInjection; @@ -92,10 +91,9 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen { eventData = Serializer.Deserialize(message.Body.ToArray(), eventType); } - else if (AnonymousHandlerFactories.ContainsKey(eventName)) + else if (HasAnonymousHandlers(eventName)) { - var data = Serializer.Deserialize(message.Body.ToArray()); - eventData = new AnonymousEventData(eventName, data); + eventData = CreateAnonymousEventData(eventName, Serializer.Deserialize(message.Body.ToArray())); eventType = typeof(AnonymousEventData); } else @@ -117,14 +115,21 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { var handlerFactories = GetOrCreateHandlerFactories(eventType); + var added = false; + handlerFactories.Locking(factories => + { + if (!factory.IsInFactories(factories)) + { + factories.Add(factory); + added = true; + } + }); - if (factory.IsInFactories(handlerFactories)) + if (!added) { return NullDisposable.Instance; } - handlerFactories.Add(factory); - return new EventHandlerFactoryUnregistrar(this, eventType, factory); } @@ -132,14 +137,21 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); + var added = false; + handlerFactories.Locking(factories => + { + if (!handler.IsInFactories(factories)) + { + factories.Add(handler); + added = true; + } + }); - if (handler.IsInFactories(handlerFactories)) + if (!added) { return NullDisposable.Instance; } - handlerFactories.Add(handler); - return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); } @@ -199,20 +211,9 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { - var eventType = EventTypes.GetOrDefault(eventName); - var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); - - if (eventType != null) - { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); - } - - if (AnonymousHandlerFactories.ContainsKey(eventName)) - { - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); - } - - throw new AbpException($"Unknown event name: {eventName}"); + var anonymousEventData = CreateAnonymousEnvelope(eventName, eventData); + return TryPublishTypedByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete) + ?? PublishAnonymousByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) @@ -287,20 +288,7 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen public async override Task ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { - var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); - object eventData; - - if (eventType != null) - { - eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); - } - else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) - { - var element = Serializer.Deserialize(incomingEvent.EventData); - eventData = new AnonymousEventData(incomingEvent.EventName, element); - eventType = typeof(AnonymousEventData); - } - else + if (!TryResolveStoredEventData(incomingEvent.EventName, incomingEvent.EventData, out var eventType, out var eventData)) { return; } @@ -400,29 +388,45 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - GetOrCreateAnonymousHandlerFactories(eventName) - .Locking(factories => factories.Remove(factory)); + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => factories.Remove(factory)); + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - GetOrCreateAnonymousHandlerFactories(eventName) - .Locking(factories => - { - factories.RemoveAll( - factory => - factory is SingleInstanceHandlerFactory singleFactory && - singleFactory.HandlerInstance == handler - ); - }); + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); + + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } /// public override void UnsubscribeAll(string eventName) { - GetOrCreateAnonymousHandlerFactories(eventName) - .Locking(factories => factories.Clear()); + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => factories.Clear()); + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } protected override IEnumerable GetAnonymousHandlerFactories(string eventName) @@ -448,6 +452,81 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); } + private AnonymousEventData CreateAnonymousEnvelope(string eventName, object eventData) + { + return eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + } + + private Task? TryPublishTypedByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) + { + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType == null) + { + return null; + } + + var typedEventData = AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, eventType); + return PublishAsync(eventType, typedEventData, onUnitOfWorkComplete); + } + + private Task PublishAnonymousByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) + { + if (!HasAnonymousHandlers(eventName)) + { + return Task.CompletedTask; + } + + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + } + + private bool TryResolveStoredEventData(string eventName, byte[] payload, out Type eventType, out object eventData) + { + eventType = EventTypes.GetOrDefault(eventName)!; + if (eventType != null) + { + eventData = Serializer.Deserialize(payload, eventType); + return true; + } + + if (!HasAnonymousHandlers(eventName)) + { + eventData = default!; + eventType = default!; + return false; + } + + eventType = typeof(AnonymousEventData); + eventData = CreateAnonymousEventData(eventName, payload); + return true; + } + + private bool HasAnonymousHandlers(string eventName) + { + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return false; + } + + var hasHandlers = false; + handlerFactories.Locking(factories => hasHandlers = factories.Count > 0); + if (!hasHandlers) + { + AnonymousHandlerFactories.TryRemove(eventName, out _); + } + + return hasHandlers; + } + + private void CleanupAnonymousHandlerFactoriesIfEmpty(string eventName, List handlerFactories) + { + var isEmpty = false; + handlerFactories.Locking(factories => isEmpty = factories.Count == 0); + if (isEmpty) + { + AnonymousHandlerFactories.TryRemove(eventName, out _); + } + } + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { return handlerEventType == targetEventType || handlerEventType.IsAssignableFrom(targetEventType); diff --git a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs index 1231daf632..13edc0106d 100644 --- a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs @@ -70,14 +70,21 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { var handlerFactories = GetOrCreateHandlerFactories(eventType); + var added = false; + handlerFactories.Locking(factories => + { + if (!factory.IsInFactories(factories)) + { + factories.Add(factory); + added = true; + } + }); - if (factory.IsInFactories(handlerFactories)) + if (!added) { return NullDisposable.Instance; } - handlerFactories.Add(factory); - return new EventHandlerFactoryUnregistrar(this, eventType, factory); } @@ -85,14 +92,21 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); + var added = false; + handlerFactories.Locking(factories => + { + if (!handler.IsInFactories(factories)) + { + factories.Add(handler); + added = true; + } + }); - if (handler.IsInFactories(handlerFactories)) + if (!added) { return NullDisposable.Instance; } - handlerFactories.Add(handler); - return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); } @@ -149,20 +163,9 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { - var eventType = EventTypes.GetOrDefault(eventName); - var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); - - if (eventType != null) - { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); - } - - if (AnonymousHandlerFactories.ContainsKey(eventName)) - { - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); - } - - throw new AbpException($"Unknown event name: {eventName}"); + var anonymousEventData = CreateAnonymousEnvelope(eventName, eventData); + return TryPublishTypedByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete) + ?? PublishAnonymousByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) @@ -178,18 +181,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public async override Task PublishFromOutboxAsync(OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig) { - var eventType = EventTypes.GetOrDefault(outgoingEvent.EventName); - object eventData; - - if (eventType != null) - { - eventData = Serializer.Deserialize(outgoingEvent.EventData, eventType); - } - else if (AnonymousHandlerFactories.ContainsKey(outgoingEvent.EventName)) - { - eventData = Serializer.Deserialize(outgoingEvent.EventData, typeof(object)); - } - else + if (!TryResolveStoredEventData(outgoingEvent.EventName, outgoingEvent.EventData, out var eventType, out var eventData)) { return; } @@ -230,19 +222,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public async override Task ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { - var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); - object eventData; - - if (eventType != null) - { - eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); - } - else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) - { - eventData = new AnonymousEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData, typeof(object))); - eventType = typeof(AnonymousEventData); - } - else + if (!TryResolveStoredEventData(incomingEvent.EventName, incomingEvent.EventData, out var eventType, out var eventData)) { return; } @@ -327,33 +307,51 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public bool IsAnonymousEvent(string eventName) { - return AnonymousHandlerFactories.ContainsKey(eventName); + return HasAnonymousHandlers(eventName); } /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => factories.Remove(factory)); + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - GetOrCreateAnonymousHandlerFactories(eventName) - .Locking(factories => - { - factories.RemoveAll( - factory => - factory is SingleInstanceHandlerFactory singleFactory && - singleFactory.HandlerInstance == handler - ); - }); + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); + + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } /// public override void UnsubscribeAll(string eventName) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => factories.Clear()); + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } protected override IEnumerable GetAnonymousHandlerFactories(string eventName) @@ -379,6 +377,81 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); } + private AnonymousEventData CreateAnonymousEnvelope(string eventName, object eventData) + { + return eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + } + + private Task? TryPublishTypedByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) + { + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType == null) + { + return null; + } + + var typedEventData = AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, eventType); + return PublishAsync(eventType, typedEventData, onUnitOfWorkComplete); + } + + private Task PublishAnonymousByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) + { + if (!HasAnonymousHandlers(eventName)) + { + return Task.CompletedTask; + } + + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + } + + private bool TryResolveStoredEventData(string eventName, byte[] payload, out Type eventType, out object eventData) + { + eventType = EventTypes.GetOrDefault(eventName)!; + if (eventType != null) + { + eventData = Serializer.Deserialize(payload, eventType); + return true; + } + + if (!HasAnonymousHandlers(eventName)) + { + eventData = default!; + eventType = default!; + return false; + } + + eventType = typeof(AnonymousEventData); + eventData = CreateAnonymousEventData(eventName, payload); + return true; + } + + private bool HasAnonymousHandlers(string eventName) + { + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return false; + } + + var hasHandlers = false; + handlerFactories.Locking(factories => hasHandlers = factories.Count > 0); + if (!hasHandlers) + { + AnonymousHandlerFactories.TryRemove(eventName, out _); + } + + return hasHandlers; + } + + private void CleanupAnonymousHandlerFactoriesIfEmpty(string eventName, List handlerFactories) + { + var isEmpty = false; + handlerFactories.Locking(factories => isEmpty = factories.Count == 0); + if (isEmpty) + { + AnonymousHandlerFactories.TryRemove(eventName, out _); + } + } + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { //Should trigger same type diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs index e7e44e449d..be82c603c1 100644 --- a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Threading.Tasks; using Confluent.Kafka; using Microsoft.Extensions.DependencyInjection; @@ -92,10 +91,9 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen { eventData = Serializer.Deserialize(message.Value, eventType); } - else if (AnonymousHandlerFactories.ContainsKey(eventName)) + else if (HasAnonymousHandlers(eventName)) { - var element = Serializer.Deserialize(message.Value); - eventData = new AnonymousEventData(eventName, element); + eventData = CreateAnonymousEventData(eventName, message.Value); eventType = typeof(AnonymousEventData); } else @@ -117,14 +115,21 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { var handlerFactories = GetOrCreateHandlerFactories(eventType); + var added = false; + handlerFactories.Locking(factories => + { + if (!factory.IsInFactories(factories)) + { + factories.Add(factory); + added = true; + } + }); - if (factory.IsInFactories(handlerFactories)) + if (!added) { return NullDisposable.Instance; } - handlerFactories.Add(factory); - return new EventHandlerFactoryUnregistrar(this, eventType, factory); } @@ -132,14 +137,21 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); + var added = false; + handlerFactories.Locking(factories => + { + if (!handler.IsInFactories(factories)) + { + factories.Add(handler); + added = true; + } + }); - if (handler.IsInFactories(handlerFactories)) + if (!added) { return NullDisposable.Instance; } - handlerFactories.Add(handler); - return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); } @@ -200,20 +212,9 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { - var eventType = EventTypes.GetOrDefault(eventName); - var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); - - if (eventType != null) - { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); - } - - if (AnonymousHandlerFactories.ContainsKey(eventName)) - { - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); - } - - throw new AbpException($"Unknown event name: {eventName}"); + var anonymousEventData = CreateAnonymousEnvelope(eventName, eventData); + return TryPublishTypedByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete) + ?? PublishAnonymousByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete); } protected override async Task PublishToEventBusAsync(Type eventType, object eventData) @@ -325,20 +326,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { - var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); - object eventData; - - if (eventType != null) - { - eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); - } - else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) - { - var element = Serializer.Deserialize(incomingEvent.EventData); - eventData = new AnonymousEventData(incomingEvent.EventName, element); - eventType = typeof(AnonymousEventData); - } - else + if (!TryResolveStoredEventData(incomingEvent.EventName, incomingEvent.EventData, out var eventType, out var eventData)) { return; } @@ -436,27 +424,45 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => factories.Remove(factory)); + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - GetOrCreateAnonymousHandlerFactories(eventName) - .Locking(factories => - { - factories.RemoveAll( - factory => - factory is SingleInstanceHandlerFactory singleFactory && - singleFactory.HandlerInstance == handler - ); - }); + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); + + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } /// public override void UnsubscribeAll(string eventName) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => factories.Clear()); + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } protected override IEnumerable GetAnonymousHandlerFactories(string eventName) @@ -482,6 +488,81 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); } + private AnonymousEventData CreateAnonymousEnvelope(string eventName, object eventData) + { + return eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + } + + private Task? TryPublishTypedByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) + { + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType == null) + { + return null; + } + + var typedEventData = AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, eventType); + return PublishAsync(eventType, typedEventData, onUnitOfWorkComplete); + } + + private Task PublishAnonymousByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) + { + if (!HasAnonymousHandlers(eventName)) + { + return Task.CompletedTask; + } + + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + } + + private bool TryResolveStoredEventData(string eventName, byte[] payload, out Type eventType, out object eventData) + { + eventType = EventTypes.GetOrDefault(eventName)!; + if (eventType != null) + { + eventData = Serializer.Deserialize(payload, eventType); + return true; + } + + if (!HasAnonymousHandlers(eventName)) + { + eventData = default!; + eventType = default!; + return false; + } + + eventType = typeof(AnonymousEventData); + eventData = CreateAnonymousEventData(eventName, payload); + return true; + } + + private bool HasAnonymousHandlers(string eventName) + { + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return false; + } + + var hasHandlers = false; + handlerFactories.Locking(factories => hasHandlers = factories.Count > 0); + if (!hasHandlers) + { + AnonymousHandlerFactories.TryRemove(eventName, out _); + } + + return hasHandlers; + } + + private void CleanupAnonymousHandlerFactoriesIfEmpty(string eventName, List handlerFactories) + { + var isEmpty = false; + handlerFactories.Locking(factories => isEmpty = factories.Count == 0); + if (isEmpty) + { + AnonymousHandlerFactories.TryRemove(eventName, out _); + } + } + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { //Should trigger same type diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs index 771dae9f09..097f2cbbe7 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs @@ -109,10 +109,10 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis { eventData = Serializer.Deserialize(ea.Body.ToArray(), eventType); } - else if (AnonymousHandlerFactories.ContainsKey(eventName)) + else if (HasAnonymousHandlers(eventName)) { eventType = typeof(AnonymousEventData); - eventData = new AnonymousEventData(eventName, Serializer.Deserialize(ea.Body.ToArray())); + eventData = CreateAnonymousEventData(eventName, ea.Body.ToArray()); } else { @@ -134,15 +134,24 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { var handlerFactories = GetOrCreateHandlerFactories(eventType); + var added = false; + var isFirstHandler = false; + handlerFactories.Locking(factories => + { + if (!factory.IsInFactories(factories)) + { + isFirstHandler = factories.Count == 0; + factories.Add(factory); + added = true; + } + }); - if (factory.IsInFactories(handlerFactories)) + if (!added) { return NullDisposable.Instance; } - handlerFactories.Add(factory); - - if (handlerFactories.Count == 1) //TODO: Multi-threading! + if (isFirstHandler) { Consumer.BindAsync(EventNameAttribute.GetNameOrDefault(eventType)); } @@ -154,15 +163,24 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); - - if (handler.IsInFactories(handlerFactories)) + var added = false; + var isFirstHandler = false; + handlerFactories.Locking(factories => + { + if (!handler.IsInFactories(factories)) + { + isFirstHandler = factories.Count == 0; + factories.Add(handler); + added = true; + } + }); + + if (!added) { return NullDisposable.Instance; } - - handlerFactories.Add(handler); - - if (handlerFactories.Count == 1) //TODO: Multi-threading! + + if (isFirstHandler) { Consumer.BindAsync(eventName); } @@ -227,20 +245,9 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { - var eventType = EventTypes.GetOrDefault(eventName); - var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); - - if (eventType != null) - { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); - } - - if (AnonymousHandlerFactories.ContainsKey(eventName)) - { - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); - } - - throw new AbpException($"Unknown event name: {eventName}"); + var anonymousEventData = CreateAnonymousEnvelope(eventName, eventData); + return TryPublishTypedByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete) + ?? PublishAnonymousByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) @@ -305,19 +312,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { - var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); - object eventData; - - if (eventType != null) - { - eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); - } - else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) - { - eventData = new AnonymousEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData)); - eventType = typeof(AnonymousEventData); - } - else + if (!TryResolveStoredEventData(incomingEvent.EventName, incomingEvent.EventData, out var eventType, out var eventData)) { return; } @@ -487,27 +482,45 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => factories.Remove(factory)); + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - GetOrCreateAnonymousHandlerFactories(eventName) - .Locking(factories => - { - factories.RemoveAll( - factory => - factory is SingleInstanceHandlerFactory singleFactory && - singleFactory.HandlerInstance == handler - ); - }); + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); + + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } /// public override void UnsubscribeAll(string eventName) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => factories.Clear()); + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } protected override IEnumerable GetAnonymousHandlerFactories(string eventName) @@ -533,6 +546,81 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); } + private AnonymousEventData CreateAnonymousEnvelope(string eventName, object eventData) + { + return eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + } + + private Task? TryPublishTypedByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) + { + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType == null) + { + return null; + } + + var typedEventData = AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, eventType); + return PublishAsync(eventType, typedEventData, onUnitOfWorkComplete); + } + + private Task PublishAnonymousByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) + { + if (!HasAnonymousHandlers(eventName)) + { + return Task.CompletedTask; + } + + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + } + + private bool TryResolveStoredEventData(string eventName, byte[] payload, out Type eventType, out object eventData) + { + eventType = EventTypes.GetOrDefault(eventName)!; + if (eventType != null) + { + eventData = Serializer.Deserialize(payload, eventType); + return true; + } + + if (!HasAnonymousHandlers(eventName)) + { + eventData = default!; + eventType = default!; + return false; + } + + eventType = typeof(AnonymousEventData); + eventData = CreateAnonymousEventData(eventName, payload); + return true; + } + + private bool HasAnonymousHandlers(string eventName) + { + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return false; + } + + var hasHandlers = false; + handlerFactories.Locking(factories => hasHandlers = factories.Count > 0); + if (!hasHandlers) + { + AnonymousHandlerFactories.TryRemove(eventName, out _); + } + + return hasHandlers; + } + + private void CleanupAnonymousHandlerFactoriesIfEmpty(string eventName, List handlerFactories) + { + var isEmpty = false; + handlerFactories.Locking(factories => isEmpty = factories.Count == 0); + if (isEmpty) + { + AnonymousHandlerFactories.TryRemove(eventName, out _); + } + } + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { //Should trigger same type diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs index c4f1a141e6..bf1bd1a585 100644 --- a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs @@ -100,15 +100,24 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { var handlerFactories = GetOrCreateHandlerFactories(eventType); + var added = false; + var isFirstHandler = false; + handlerFactories.Locking(factories => + { + if (!factory.IsInFactories(factories)) + { + isFirstHandler = factories.Count == 0; + factories.Add(factory); + added = true; + } + }); - if (factory.IsInFactories(handlerFactories)) + if (!added) { return NullDisposable.Instance; } - handlerFactories.Add(factory); - - if (handlerFactories.Count == 1) //TODO: Multi-threading! + if (isFirstHandler) { Rebus.Subscribe(eventType); } @@ -120,15 +129,24 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); + var added = false; + var isFirstHandler = false; + handlerFactories.Locking(factories => + { + if (!handler.IsInFactories(factories)) + { + isFirstHandler = factories.Count == 0; + factories.Add(handler); + added = true; + } + }); - if (handler.IsInFactories(handlerFactories)) + if (!added) { return NullDisposable.Instance; } - handlerFactories.Add(handler); - - if (AnonymousHandlerFactories.Count == 1) //TODO: Multi-threading! + if (isFirstHandler && AnonymousHandlerFactories.Count == 1) { Rebus.Subscribe(typeof(AnonymousEventData)); } @@ -193,20 +211,9 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { - var eventType = EventTypes.GetOrDefault(eventName); - var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); - - if (eventType != null) - { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); - } - - if (AnonymousHandlerFactories.ContainsKey(eventName)) - { - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); - } - - throw new AbpException($"Unknown event name: {eventName}"); + var anonymousEventData = CreateAnonymousEnvelope(eventName, eventData); + return TryPublishTypedByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete) + ?? PublishAnonymousByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) @@ -228,19 +235,7 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig) { - var eventType = EventTypes.GetOrDefault(outgoingEvent.EventName); - object eventData; - - if (eventType != null) - { - eventData = Serializer.Deserialize(outgoingEvent.EventData, eventType); - } - else if (AnonymousHandlerFactories.ContainsKey(outgoingEvent.EventName)) - { - eventData = new AnonymousEventData(outgoingEvent.EventName, Serializer.Deserialize(outgoingEvent.EventData, typeof(object))); - eventType = typeof(AnonymousEventData); - } - else + if (!TryResolveStoredEventData(outgoingEvent.EventName, outgoingEvent.EventData, out var eventType, out var eventData)) { return; } @@ -273,15 +268,6 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen { await PublishFromOutboxAsync(outgoingEvent, outboxConfig); - using (CorrelationIdProvider.Change(outgoingEvent.GetCorrelationId())) - { - await TriggerDistributedEventSentAsync(new DistributedEventSent() - { - Source = DistributedEventSource.Outbox, - EventName = outgoingEvent.EventName, - EventData = outgoingEvent.EventData - }); - } } await scope.CompleteAsync(); @@ -292,19 +278,7 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { - var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); - object eventData; - - if (eventType != null) - { - eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); - } - else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) - { - eventData = new AnonymousEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData, typeof(object))); - eventType = typeof(AnonymousEventData); - } - else + if (!TryResolveStoredEventData(incomingEvent.EventName, incomingEvent.EventData, out var eventType, out var eventData)) { return; } @@ -393,27 +367,45 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => factories.Remove(factory)); + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - GetOrCreateAnonymousHandlerFactories(eventName) - .Locking(factories => - { - factories.RemoveAll( - factory => - factory is SingleInstanceHandlerFactory singleFactory && - singleFactory.HandlerInstance == handler - ); - }); + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); + + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } /// public override void UnsubscribeAll(string eventName) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => factories.Clear()); + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } protected override IEnumerable GetAnonymousHandlerFactories(string eventName) @@ -439,6 +431,81 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); } + private AnonymousEventData CreateAnonymousEnvelope(string eventName, object eventData) + { + return eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + } + + private Task? TryPublishTypedByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) + { + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType == null) + { + return null; + } + + var typedEventData = AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, eventType); + return PublishAsync(eventType, typedEventData, onUnitOfWorkComplete); + } + + private Task PublishAnonymousByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) + { + if (!HasAnonymousHandlers(eventName)) + { + return Task.CompletedTask; + } + + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + } + + private bool TryResolveStoredEventData(string eventName, byte[] payload, out Type eventType, out object eventData) + { + eventType = EventTypes.GetOrDefault(eventName)!; + if (eventType != null) + { + eventData = Serializer.Deserialize(payload, eventType); + return true; + } + + if (!HasAnonymousHandlers(eventName)) + { + eventData = default!; + eventType = default!; + return false; + } + + eventType = typeof(AnonymousEventData); + eventData = CreateAnonymousEventData(eventName, payload); + return true; + } + + private bool HasAnonymousHandlers(string eventName) + { + if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return false; + } + + var hasHandlers = false; + handlerFactories.Locking(factories => hasHandlers = factories.Count > 0); + if (!hasHandlers) + { + AnonymousHandlerFactories.TryRemove(eventName, out _); + } + + return hasHandlers; + } + + private void CleanupAnonymousHandlerFactoriesIfEmpty(string eventName, List handlerFactories) + { + var isEmpty = false; + handlerFactories.Locking(factories => isEmpty = factories.Count == 0); + if (isEmpty) + { + AnonymousHandlerFactories.TryRemove(eventName, out _); + } + } + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { //Should trigger same type diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs index 35162a0007..a5ffb87245 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -112,15 +113,9 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB bool onUnitOfWorkComplete = true, bool useOutbox = true) { - var eventType = GetEventTypeByEventName(eventName); - var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); - - if (eventType != null) - { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete, useOutbox); - } - - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete, useOutbox); + var anonymousEventData = CreateAnonymousEnvelope(eventName, eventData); + return TryPublishTypedByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete, useOutbox) + ?? PublishAnonymousByEventNameAsync(anonymousEventData, onUnitOfWorkComplete, useOutbox); } public abstract Task PublishFromOutboxAsync( @@ -152,14 +147,14 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB if (outboxConfig.Selector == null || outboxConfig.Selector(eventType)) { var eventOutbox = (IEventOutbox)unitOfWork.ServiceProvider.GetRequiredService(outboxConfig.ImplementationType); - (var eventName, eventData) = ResolveEventForPublishing(eventType, eventData); + var (eventName, resolvedEventData) = ResolveEventForPublishing(eventType, eventData); - await OnAddToOutboxAsync(eventName, eventType, eventData); + await OnAddToOutboxAsync(eventName, eventType, resolvedEventData); var outgoingEventInfo = new OutgoingEventInfo( GuidGenerator.Create(), eventName, - Serialize(eventData), + SerializeEventData(resolvedEventData), Clock.Now ); @@ -214,13 +209,11 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB } } - eventData = GetEventData(eventData); - var incomingEventInfo = new IncomingEventInfo( GuidGenerator.Create(), messageId!, eventName, - Serialize(eventData), + SerializeEventData(eventData), Clock.Now ); incomingEventInfo.SetCorrelationId(correlationId!); @@ -235,6 +228,55 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB protected abstract byte[] Serialize(object eventData); + protected virtual byte[] SerializeEventData(object eventData) + { + if (eventData is AnonymousEventData anonymousEventData) + { + return Encoding.UTF8.GetBytes(AnonymousEventDataConverter.GetJsonData(anonymousEventData)); + } + + return Serialize(eventData); + } + + protected virtual AnonymousEventData CreateAnonymousEventData(string eventName, byte[] eventData) + { + return AnonymousEventData.FromJson(eventName, Encoding.UTF8.GetString(eventData)); + } + + protected virtual AnonymousEventData CreateAnonymousEventData(string eventName, string eventData) + { + return AnonymousEventData.FromJson(eventName, eventData); + } + + protected virtual AnonymousEventData CreateAnonymousEnvelope(string eventName, object eventData) + { + return eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + } + + protected virtual Task? TryPublishTypedByEventNameAsync( + string eventName, + AnonymousEventData anonymousEventData, + bool onUnitOfWorkComplete, + bool useOutbox) + { + var eventType = GetEventTypeByEventName(eventName); + if (eventType == null) + { + return null; + } + + var typedEventData = AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, eventType); + return PublishAsync(eventType, typedEventData, onUnitOfWorkComplete, useOutbox); + } + + protected virtual Task PublishAnonymousByEventNameAsync( + AnonymousEventData anonymousEventData, + bool onUnitOfWorkComplete, + bool useOutbox) + { + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete, useOutbox); + } + protected virtual async Task TriggerHandlersDirectAsync(Type eventType, object eventData) { await TriggerDistributedEventReceivedAsync(new DistributedEventReceived @@ -297,7 +339,7 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB { if (eventData is AnonymousEventData anonymousEventData) { - return anonymousEventData.ConvertToTypedObject(); + return AnonymousEventDataConverter.ConvertToLooseObject(anonymousEventData); } return eventData; diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs index 922bc42909..ede70885ff 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs @@ -77,7 +77,8 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { AnonymousEventNames.GetOrAdd(eventName, true); - return LocalEventBus.Subscribe(eventName, handler); + LocalEventBus.Subscribe(eventName, handler); + return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); } /// @@ -85,7 +86,8 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen { var eventName = EventNameAttribute.GetNameOrDefault(eventType); EventTypes.GetOrAdd(eventName, eventType); - return LocalEventBus.Subscribe(eventType, factory); + LocalEventBus.Subscribe(eventType, factory); + return new EventHandlerFactoryUnregistrar(this, eventType, factory); } public override void Unsubscribe(Func action) @@ -107,12 +109,14 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { LocalEventBus.Unsubscribe(eventName, factory); + CleanupAnonymousEventName(eventName); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { LocalEventBus.Unsubscribe(eventName, handler); + CleanupAnonymousEventName(eventName); } /// @@ -125,6 +129,7 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override void UnsubscribeAll(string eventName) { LocalEventBus.UnsubscribeAll(eventName); + CleanupAnonymousEventName(eventName); } /// @@ -173,25 +178,14 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) { - var eventType = EventTypes.GetOrDefault(eventName); - var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); - - if (eventType != null) - { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete, useOutbox); - } - - if (!AnonymousEventNames.ContainsKey(eventName)) - { - throw new AbpException($"Unknown event name: {eventName}"); - } - - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete, useOutbox); + var anonymousEventData = CreateAnonymousEnvelope(eventName, eventData); + return TryPublishTypedByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete, useOutbox) + ?? PublishAnonymousByEventNameAsync(anonymousEventData, onUnitOfWorkComplete, useOutbox); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) { - if (await AddToInboxAsync(Guid.NewGuid().ToString(), EventNameAttribute.GetNameOrDefault(eventType), eventType, eventData, null)) + if (await AddToInboxAsync(Guid.NewGuid().ToString(), GetEventName(eventType, eventData), eventType, eventData, null)) { return; } @@ -220,19 +214,12 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen EventData = outgoingEvent.EventData }); - var eventType = EventTypes.GetOrDefault(outgoingEvent.EventName); - if (eventType == null) + if (!TryResolveStoredEventType(outgoingEvent.EventName, out var eventType)) { - var isAnonymous = AnonymousEventNames.ContainsKey(outgoingEvent.EventName); - if (!isAnonymous) - { - return; - } - - eventType = typeof(AnonymousEventData); + return; } - var eventData = JsonSerializer.Deserialize(Encoding.UTF8.GetString(outgoingEvent.EventData), eventType)!; + var eventData = DeserializeStoredEventData(outgoingEvent.EventName, outgoingEvent.EventData, eventType); if (await AddToInboxAsync(Guid.NewGuid().ToString(), outgoingEvent.EventName, eventType, eventData, null)) { return; @@ -251,19 +238,12 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen public async override Task ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { - var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); - if (eventType == null) + if (!TryResolveStoredEventType(incomingEvent.EventName, out var eventType)) { - var isAnonymous = AnonymousEventNames.ContainsKey(incomingEvent.EventName); - if (!isAnonymous) - { - return; - } - - eventType = typeof(AnonymousEventData); + return; } - var eventData = JsonSerializer.Deserialize(incomingEvent.EventData, eventType); + var eventData = DeserializeStoredEventData(incomingEvent.EventName, incomingEvent.EventData, eventType); var exceptions = new List(); using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) { @@ -303,4 +283,89 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen { return EventTypes.GetOrDefault(eventName); } + + protected override AnonymousEventData CreateAnonymousEnvelope(string eventName, object eventData) + { + return eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + } + + protected override Task? TryPublishTypedByEventNameAsync( + string eventName, + AnonymousEventData anonymousEventData, + bool onUnitOfWorkComplete, + bool useOutbox) + { + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType == null) + { + return null; + } + + var typedEventData = AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, eventType); + return PublishAsync(eventType, typedEventData, onUnitOfWorkComplete, useOutbox); + } + + protected override Task PublishAnonymousByEventNameAsync( + AnonymousEventData anonymousEventData, + bool onUnitOfWorkComplete, + bool useOutbox) + { + if (!HasAnonymousEventName(anonymousEventData.EventName)) + { + return Task.CompletedTask; + } + + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete, useOutbox); + } + + protected virtual bool TryResolveStoredEventType(string eventName, out Type eventType) + { + eventType = EventTypes.GetOrDefault(eventName)!; + if (eventType != null) + { + return true; + } + + if (!HasAnonymousEventName(eventName)) + { + return false; + } + + eventType = typeof(AnonymousEventData); + return true; + } + + protected virtual object DeserializeStoredEventData(string eventName, byte[] eventData, Type eventType) + { + if (eventType == typeof(AnonymousEventData)) + { + return CreateAnonymousEventData(eventName, eventData); + } + + return JsonSerializer.Deserialize(Encoding.UTF8.GetString(eventData), eventType)!; + } + + protected virtual void CleanupAnonymousEventName(string eventName) + { + if (!LocalEventBus.GetAnonymousEventHandlerFactories(eventName).Any()) + { + AnonymousEventNames.TryRemove(eventName, out _); + } + } + + protected virtual bool HasAnonymousEventName(string eventName) + { + if (!AnonymousEventNames.ContainsKey(eventName)) + { + return false; + } + + if (!LocalEventBus.GetAnonymousEventHandlerFactories(eventName).Any()) + { + AnonymousEventNames.TryRemove(eventName, out _); + return false; + } + + return true; + } } diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs index 8c344f5aa7..b4b589d6dd 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs @@ -174,9 +174,7 @@ public abstract class EventBusBase : IEventBus actualEventType.GetGenericArguments().Length == 1 && typeof(IEventDataWithInheritableGenericArgument).IsAssignableFrom(actualEventType)) { - var resolvedEventData = eventData is AnonymousEventData aed - ? aed.ConvertToTypedObject(actualEventType) - : eventData; + var resolvedEventData = ResolveActualEventData(eventData, actualEventType); var genericArg = actualEventType.GetGenericArguments()[0]; var baseArg = genericArg.GetTypeInfo().BaseType; @@ -209,7 +207,7 @@ public abstract class EventBusBase : IEventBus { if (eventData is AnonymousEventData anonymousEventData && handlerEventType != typeof(AnonymousEventData)) { - return anonymousEventData.ConvertToTypedObject(handlerEventType); + return AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, handlerEventType); } if (handlerEventType == typeof(AnonymousEventData) && eventData is not AnonymousEventData) @@ -220,6 +218,16 @@ public abstract class EventBusBase : IEventBus return eventData; } + protected virtual object ResolveActualEventData(object eventData, Type actualEventType) + { + if (eventData is AnonymousEventData anonymousEventData) + { + return AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, actualEventType); + } + + return eventData; + } + protected void ThrowOriginalExceptions(Type eventType, List exceptions) { if (exceptions.Count == 1) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs index 989191528c..e8f0715d8d 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs @@ -60,7 +60,8 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency /// public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => + var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); + handlerFactories.Locking(factories => { if (!handler.IsInFactories(factories)) { @@ -140,21 +141,33 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + if (!TryGetAnonymousHandlerFactories(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => factories.Remove(factory)); + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - GetOrCreateAnonymousHandlerFactories(eventName) - .Locking(factories => - { - factories.RemoveAll( - factory => - factory is SingleInstanceHandlerFactory singleFactory && - singleFactory.HandlerInstance == handler - ); - }); + if (!TryGetAnonymousHandlerFactories(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); + + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } /// @@ -166,28 +179,21 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency /// public override void UnsubscribeAll(string eventName) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); + if (!TryGetAnonymousHandlerFactories(eventName, out var handlerFactories)) + { + return; + } + + handlerFactories.Locking(factories => factories.Clear()); + CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); } /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { - var eventType = EventTypes.GetOrDefault(eventName); - - var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); - - if (eventType != null) - { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); - } - - var isAnonymous = AnonymousEventHandlerFactories.ContainsKey(eventName); - if (!isAnonymous) - { - throw new AbpException($"Unknown event name: {eventName}"); - } - - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + var anonymousEventData = CreateAnonymousEnvelope(eventName, eventData); + return TryPublishTypedByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete) + ?? PublishAnonymousByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete); } protected override async Task PublishToEventBusAsync(Type eventType, object eventData) @@ -225,10 +231,11 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency { foreach (var factory in handlerFactory.Value) { + using var handler = factory.GetHandler(); handlerFactoryList.Add(new Tuple( factory, handlerFactory.Key, - ReflectionHelper.GetAttributesOfMemberOrDeclaringType(factory.GetHandler().EventHandler.GetType()).FirstOrDefault()?.Order ?? 0)); + ReflectionHelper.GetAttributesOfMemberOrDeclaringType(handler.EventHandler.GetType()).FirstOrDefault()?.Order ?? 0)); } } @@ -236,10 +243,11 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency { foreach (var factory in handlerFactory.Value) { + using var handler = factory.GetHandler(); handlerFactoryList.Add(new Tuple( factory, typeof(AnonymousEventData), - ReflectionHelper.GetAttributesOfMemberOrDeclaringType(factory.GetHandler().EventHandler.GetType()).FirstOrDefault()?.Order ?? 0)); + ReflectionHelper.GetAttributesOfMemberOrDeclaringType(handler.EventHandler.GetType()).FirstOrDefault()?.Order ?? 0)); } } @@ -290,6 +298,67 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency return AnonymousEventHandlerFactories.GetOrAdd(eventName, (name) => new List()); } + private bool TryGetAnonymousHandlerFactories(string eventName, out List handlerFactories) + { + return AnonymousEventHandlerFactories.TryGetValue(eventName, out handlerFactories!); + } + + private AnonymousEventData CreateAnonymousEnvelope(string eventName, object eventData) + { + return eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + } + + private Task? TryPublishTypedByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) + { + var eventType = EventTypes.GetOrDefault(eventName); + if (eventType == null) + { + return null; + } + + var typedEventData = AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, eventType); + return PublishAsync(eventType, typedEventData, onUnitOfWorkComplete); + } + + private Task PublishAnonymousByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) + { + if (!HasAnonymousHandlers(eventName)) + { + return Task.CompletedTask; + } + + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + } + + private bool HasAnonymousHandlers(string eventName) + { + if (!AnonymousEventHandlerFactories.TryGetValue(eventName, out var handlerFactories)) + { + return false; + } + + var hasHandlers = false; + handlerFactories.Locking(factories => hasHandlers = factories.Count > 0); + + if (!hasHandlers) + { + AnonymousEventHandlerFactories.TryRemove(eventName, out _); + } + + return hasHandlers; + } + + private void CleanupAnonymousHandlerFactoriesIfEmpty(string eventName, List handlerFactories) + { + var isEmpty = false; + handlerFactories.Locking(factories => isEmpty = factories.Count == 0); + + if (isEmpty) + { + AnonymousEventHandlerFactories.TryRemove(eventName, out _); + } + } + private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { if (handlerEventType == targetEventType) diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs index 5ceb4b1bb5..673454c6be 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text; using System.Threading.Tasks; using Shouldly; using Volo.Abp.Domain.Entities.Events.Distributed; @@ -21,20 +22,32 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase [Fact] public async Task Should_Call_Handler_AndDispose() { - using var subscription = DistributedEventBus.Subscribe(); + var handleCount = 0; + var disposeCount = 0; + var factory = new CountingDistributedEventHandlerFactory( + () => handleCount++, + () => disposeCount++); + + using var subscription = DistributedEventBus.Subscribe(typeof(MySimpleEventData), factory); await DistributedEventBus.PublishAsync(new MySimpleEventData(1)); await DistributedEventBus.PublishAsync(new MySimpleEventData(2)); await DistributedEventBus.PublishAsync(new MySimpleEventData(3)); - Assert.Equal(3, MySimpleDistributedTransientEventHandler.HandleCount); - Assert.Equal(3, MySimpleDistributedTransientEventHandler.DisposeCount); + Assert.Equal(3, handleCount); + Assert.Equal(3, disposeCount); } [Fact] public async Task Should_Handle_Typed_Handler_When_Published_With_EventName() { - using var subscription = DistributedEventBus.Subscribe(); + var handleCount = 0; + var disposeCount = 0; + var factory = new CountingDistributedEventHandlerFactory( + () => handleCount++, + () => disposeCount++); + + using var subscription = DistributedEventBus.Subscribe(typeof(MySimpleEventData), factory); var eventName = EventNameAttribute.GetNameOrDefault(); await DistributedEventBus.PublishAsync(eventName, new MySimpleEventData(1)); @@ -44,8 +57,8 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase }); await DistributedEventBus.PublishAsync(eventName, new { Value = 3 }); - Assert.Equal(3, MySimpleDistributedTransientEventHandler.HandleCount); - Assert.Equal(3, MySimpleDistributedTransientEventHandler.DisposeCount); + Assert.Equal(3, handleCount); + Assert.Equal(3, disposeCount); } [Fact] @@ -82,7 +95,7 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { handleCount++; - d.ConvertToTypedObject().ShouldNotBeNull(); + AnonymousEventDataConverter.ConvertToLooseObject(d).ShouldNotBeNull(); await Task.CompletedTask; }))); @@ -187,16 +200,15 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase } [Fact] - public async Task Should_Throw_For_Unknown_Event_Name() + public async Task Should_Ignore_Unknown_Event_Name() { - await Assert.ThrowsAsync(() => - DistributedEventBus.PublishAsync("NonExistentEvent", new { Value = 1 })); + await DistributedEventBus.PublishAsync("NonExistentEvent", new { Value = 1 }); } [Fact] public async Task Should_Convert_AnonymousEventData_To_Typed_Object() { - MySimpleEventData? receivedData = null; + MySimpleEventData receivedData = null!; using var subscription = DistributedEventBus.Subscribe(async (data) => { @@ -211,6 +223,63 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase receivedData.Value.ShouldBe(42); } + [Fact] + public async Task Should_Rehydrate_Anonymous_Event_From_Outbox_Using_Raw_Json() + { + var localDistributedEventBus = GetRequiredService(); + AnonymousEventData receivedData = null!; + var eventName = "MyEvent-" + Guid.NewGuid().ToString("N"); + + using var subscription = localDistributedEventBus.Subscribe(eventName, + new SingleInstanceHandlerFactory(new ActionEventHandler(async data => + { + receivedData = data; + await Task.CompletedTask; + }))); + + var outgoingEvent = new OutgoingEventInfo( + Guid.NewGuid(), + eventName, + Encoding.UTF8.GetBytes("{\"Value\":42}"), + DateTime.UtcNow); + + await localDistributedEventBus.PublishFromOutboxAsync(outgoingEvent, new OutboxConfig("Test") { DatabaseName = "Test" }); + + receivedData.ShouldNotBeNull(); + receivedData.EventName.ShouldBe(eventName); + AnonymousEventDataConverter.GetJsonData(receivedData).ShouldBe("{\"Value\":42}"); + AnonymousEventDataConverter.ConvertToTypedObject(receivedData).Value.ShouldBe(42); + } + + [Fact] + public async Task Should_Rehydrate_Anonymous_Event_From_Inbox_Using_Raw_Json() + { + var localDistributedEventBus = GetRequiredService(); + AnonymousEventData receivedData = null!; + var eventName = "MyEvent-" + Guid.NewGuid().ToString("N"); + + using var subscription = localDistributedEventBus.Subscribe(eventName, + new SingleInstanceHandlerFactory(new ActionEventHandler(async data => + { + receivedData = data; + await Task.CompletedTask; + }))); + + var incomingEvent = new IncomingEventInfo( + Guid.NewGuid(), + Guid.NewGuid().ToString("N"), + eventName, + Encoding.UTF8.GetBytes("\"hello\""), + DateTime.UtcNow); + + await localDistributedEventBus.ProcessFromInboxAsync(incomingEvent, new InboxConfig("Test") { DatabaseName = "Test" }); + + receivedData.ShouldNotBeNull(); + receivedData.EventName.ShouldBe(eventName); + AnonymousEventDataConverter.GetJsonData(receivedData).ShouldBe("\"hello\""); + AnonymousEventDataConverter.ConvertToTypedObject(receivedData).ShouldBe("hello"); + } + [Fact] public async Task Should_Change_TenantId_If_EventData_Is_MultiTenant() { @@ -309,4 +378,57 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase } } + private sealed class CountingDistributedEventHandlerFactory : IEventHandlerFactory + { + private readonly Action _handleAction; + private readonly Action _disposeAction; + + public CountingDistributedEventHandlerFactory(Action handleAction, Action disposeAction) + { + _handleAction = handleAction; + _disposeAction = disposeAction; + } + + public IEventHandlerDisposeWrapper GetHandler() + { + var wasHandled = false; + return new EventHandlerDisposeWrapper( + new CountingDistributedEventHandler( + _handleAction, + () => wasHandled = true), + () => + { + if (wasHandled) + { + _disposeAction(); + } + } + ); + } + + public bool IsInFactories(List handlerFactories) + { + return handlerFactories.Contains(this); + } + } + + private sealed class CountingDistributedEventHandler : IDistributedEventHandler + { + private readonly Action _handleAction; + private readonly Action _markHandled; + + public CountingDistributedEventHandler(Action handleAction, Action markHandled) + { + _handleAction = handleAction; + _markHandled = markHandled; + } + + public Task HandleEventAsync(MySimpleEventData eventData) + { + _markHandled(); + _handleAction(); + return Task.CompletedTask; + } + } + } diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs index 7d3e5748e9..dfcd74bed8 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs @@ -48,7 +48,7 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase [Fact] public async Task Should_Convert_Dictionary_To_Typed_Handler() { - MySimpleEventData? receivedData = null; + MySimpleEventData receivedData = null!; using var subscription = LocalEventBus.Subscribe(async (data) => { @@ -118,29 +118,28 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase } [Fact] - public async Task Should_Throw_For_Unknown_Event_Name() + public async Task Should_Ignore_Unknown_Event_Name() { - await Assert.ThrowsAsync(() => - LocalEventBus.PublishAsync("NonExistentEvent", new { Value = 1 })); + await LocalEventBus.PublishAsync("NonExistentEvent", new { Value = 1 }); } [Fact] public async Task Should_ConvertToTypedObject_In_Anonymous_Handler() { - object? receivedData = null; + object receivedData = null!; var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); using var subscription = LocalEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { - receivedData = d.ConvertToTypedObject(); + receivedData = AnonymousEventDataConverter.ConvertToLooseObject(d); await Task.CompletedTask; }))); await LocalEventBus.PublishAsync(eventName, new { Name = "Hello", Count = 42 }); receivedData.ShouldNotBeNull(); - var dict = receivedData.ShouldBeOfType>(); + var dict = receivedData.ShouldBeOfType>(); dict["Name"].ShouldBe("Hello"); dict["Count"].ShouldBe(42L); } @@ -148,13 +147,13 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase [Fact] public async Task Should_ConvertToTypedObject_Generic_In_Anonymous_Handler() { - MySimpleEventData? receivedData = null; + MySimpleEventData receivedData = null!; var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); using var subscription = LocalEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { - receivedData = d.ConvertToTypedObject(); + receivedData = AnonymousEventDataConverter.ConvertToTypedObject(d); await Task.CompletedTask; }))); @@ -163,4 +162,23 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase receivedData.ShouldNotBeNull(); receivedData.Value.ShouldBe(99); } + + [Fact] + public void Should_Roundtrip_Raw_Json_Without_Object_Deserialization() + { + var eventData = AnonymousEventData.FromJson("TestEvent", "{\"Value\":42,\"Name\":\"hello\"}"); + + eventData.EventName.ShouldBe("TestEvent"); + AnonymousEventDataConverter.GetJsonData(eventData).ShouldBe("{\"Value\":42,\"Name\":\"hello\"}"); + AnonymousEventDataConverter.ConvertToTypedObject(eventData).Value.ShouldBe(42); + } + + [Fact] + public void Should_Preserve_String_Payload_As_Raw_Json() + { + var eventData = AnonymousEventData.FromJson("TestEvent", "\"hello\""); + + AnonymousEventDataConverter.GetJsonData(eventData).ShouldBe("\"hello\""); + AnonymousEventDataConverter.ConvertToTypedObject(eventData).ShouldBe("hello"); + } } From 318c40a0ee7297d68bb390dbd50c6420d442ee72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Tue, 10 Mar 2026 14:04:22 +0300 Subject: [PATCH 11/22] Get handler types safely and change anon conversion Add GetHandlerType(IEventHandlerFactory) to determine handler Type from known factory implementations (SingleInstance, Ioc, Transient) instead of instantiating handlers. Update LocalEventBus to use this helper when reading LocalEventHandlerOrderAttribute to avoid unnecessary handler creation/disposal. Also switch DistEventScenarioRunner to use AnonymousEventDataConverter.ConvertToLooseObject(...) for anonymous event payload conversion. --- .../Volo/Abp/EventBus/Local/LocalEventBus.cs | 32 +++++++++++++++---- .../DistEventScenarioRunner.cs | 2 +- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs index e8f0715d8d..3281b95bd5 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs @@ -231,11 +231,11 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency { foreach (var factory in handlerFactory.Value) { - using var handler = factory.GetHandler(); + var handlerType = GetHandlerType(factory); handlerFactoryList.Add(new Tuple( factory, handlerFactory.Key, - ReflectionHelper.GetAttributesOfMemberOrDeclaringType(handler.EventHandler.GetType()).FirstOrDefault()?.Order ?? 0)); + ReflectionHelper.GetAttributesOfMemberOrDeclaringType(handlerType).FirstOrDefault()?.Order ?? 0)); } } @@ -243,11 +243,11 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency { foreach (var factory in handlerFactory.Value) { - using var handler = factory.GetHandler(); + var handlerType = GetHandlerType(factory); handlerFactoryList.Add(new Tuple( factory, typeof(AnonymousEventData), - ReflectionHelper.GetAttributesOfMemberOrDeclaringType(handler.EventHandler.GetType()).FirstOrDefault()?.Order ?? 0)); + ReflectionHelper.GetAttributesOfMemberOrDeclaringType(handlerType).FirstOrDefault()?.Order ?? 0)); } } @@ -268,8 +268,7 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency { foreach (var factory in handlerFactory.Value) { - using var handler = factory.GetHandler(); - var handlerType = handler.EventHandler.GetType(); + var handlerType = GetHandlerType(factory); handlerFactoryList.Add(new Tuple( factory, typeof(AnonymousEventData), @@ -330,6 +329,27 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); } + private static Type GetHandlerType(IEventHandlerFactory factory) + { + if (factory is SingleInstanceHandlerFactory singleInstanceHandlerFactory) + { + return singleInstanceHandlerFactory.HandlerInstance.GetType(); + } + + if (factory is IocEventHandlerFactory iocEventHandlerFactory) + { + return iocEventHandlerFactory.HandlerType; + } + + if (factory is TransientEventHandlerFactory transientEventHandlerFactory) + { + return transientEventHandlerFactory.HandlerType; + } + + using var handler = factory.GetHandler(); + return handler.EventHandler.GetType(); + } + private bool HasAnonymousHandlers(string eventName) { if (!AnonymousEventHandlerFactories.TryGetValue(eventName, out var handlerFactories)) diff --git a/test/DistEvents/DistDemoApp.Shared/DistEventScenarioRunner.cs b/test/DistEvents/DistDemoApp.Shared/DistEventScenarioRunner.cs index a9601335f8..2bb09937e5 100644 --- a/test/DistEvents/DistDemoApp.Shared/DistEventScenarioRunner.cs +++ b/test/DistEvents/DistDemoApp.Shared/DistEventScenarioRunner.cs @@ -58,7 +58,7 @@ public class DistEventScenarioRunner : IDistEventScenarioRunner, ITransientDepen new SingleInstanceHandlerFactory( new ActionEventHandler(eventData => { - var converted = eventData.ConvertToTypedObject(); + var converted = AnonymousEventDataConverter.ConvertToLooseObject(eventData); if (converted is Dictionary payload && payload.TryGetValue("Message", out var message) && message?.ToString() == profile.AnonymousOnlyMessage) From fb47069209640ed5fe296e8860752d38f4f7683c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Tue, 10 Mar 2026 20:00:34 +0300 Subject: [PATCH 12/22] Unify anonymous event handling across buses Add native anonymous event support and simplify handling across transports. AnonymousEventData now contains conversion helpers (ConvertToTypedObject/ConvertToTypedObject/ConvertToTypedObject -> loose typed object), caching JSON elements and replacing the removed AnonymousEventDataConverter. Multiple distributed event bus implementations (Azure, Dapr, Kafka, RabbitMQ, Rebus) were updated to: detect anonymous handlers via AnonymousHandlerFactories, construct AnonymousEventData when appropriate, resolve event types at publish/process time, simplify Subscribe/Unsubscribe logic (avoid duplicate-factory checks using IsInFactories then add), and throw on unknown event names in PublishAsync. AbpAspNetCoreMvcDaprEventBusModule was refactored to deserialize and trigger handlers inline for both envelope and direct Dapr events. Tests updated accordingly and a small cursor hook state file was added. --- .../AbpAspNetCoreMvcDaprEventBusModule.cs | 81 ++----- .../Volo/Abp/EventBus/AnonymousEventData.cs | 115 ++++++++-- .../EventBus/AnonymousEventDataConverter.cs | 95 -------- .../Azure/AzureDistributedEventBus.cs | 181 +++++----------- .../EventBus/Dapr/DaprDistributedEventBus.cs | 187 +++++----------- .../Kafka/KafkaDistributedEventBus.cs | 179 +++++---------- .../RabbitMq/RabbitMqDistributedEventBus.cs | 188 +++++----------- .../Rebus/RebusDistributedEventBus.cs | 203 ++++++------------ .../Distributed/DistributedEventBusBase.cs | 74 ++----- .../Distributed/LocalDistributedEventBus.cs | 164 ++++++-------- .../Volo/Abp/EventBus/EventBusBase.cs | 16 +- .../Volo/Abp/EventBus/Local/LocalEventBus.cs | 153 +++---------- .../LocalDistributedEventBus_Test.cs | 144 +------------ .../Local/LocalEventBus_Anonymous_Test.cs | 36 +--- 14 files changed, 531 insertions(+), 1285 deletions(-) delete mode 100644 framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventDataConverter.cs diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs index 079ed1287a..f770f5fbb2 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; @@ -96,11 +96,30 @@ public class AbpAspNetCoreMvcDaprEventBusModule : AbpModule if (IsAbpDaprEventData(data)) { - await TryHandleAbpDaprEnvelopeAsync(distributedEventBus, daprSerializer, data); + var daprEventData = daprSerializer.Deserialize(data, typeof(AbpDaprEventData)).As(); + var eventType = distributedEventBus.GetEventType(daprEventData.Topic); + if (eventType != null) + { + var eventData = daprSerializer.Deserialize(daprEventData.JsonData, eventType); + await distributedEventBus.TriggerHandlersAsync(eventType, eventData, daprEventData.MessageId, daprEventData.CorrelationId); + }else if (distributedEventBus.IsAnonymousEvent(daprEventData.Topic)) + { + var eventData = daprSerializer.Deserialize(daprEventData.JsonData, typeof(object)); + await distributedEventBus.TriggerHandlersAsync(typeof(AnonymousEventData), new AnonymousEventData(daprEventData.Topic, eventData), daprEventData.MessageId, daprEventData.CorrelationId); + } } else { - await TryHandleDirectDaprEventAsync(distributedEventBus, daprSerializer, topic!, data); + var eventType = distributedEventBus.GetEventType(topic); + if (eventType != null) + { + var eventData = daprSerializer.Deserialize(data, eventType); + await distributedEventBus.TriggerHandlersAsync(eventType, eventData); + }else if (distributedEventBus.IsAnonymousEvent(topic)) + { + var eventData = daprSerializer.Deserialize(data, typeof(object)); + await distributedEventBus.TriggerHandlersAsync(typeof(AnonymousEventData), new AnonymousEventData(topic, eventData)); + } } httpContext.Response.StatusCode = 200; @@ -117,60 +136,4 @@ public class AbpAspNetCoreMvcDaprEventBusModule : AbpModule objects.Any(x => x.Name.Equals("JsonData", StringComparison.CurrentCultureIgnoreCase)) && objects.Any(x => x.Name.Equals("CorrelationId", StringComparison.CurrentCultureIgnoreCase)); } - - private static async Task TryHandleAbpDaprEnvelopeAsync( - DaprDistributedEventBus distributedEventBus, - IDaprSerializer daprSerializer, - string data) - { - var daprEventData = daprSerializer.Deserialize(data, typeof(AbpDaprEventData)).As(); - if (!TryResolveIncomingEvent(distributedEventBus, daprSerializer, daprEventData.Topic, daprEventData.JsonData, out var eventType, out var eventData)) - { - return; - } - - await distributedEventBus.TriggerHandlersAsync(eventType, eventData, daprEventData.MessageId, daprEventData.CorrelationId); - } - - private static async Task TryHandleDirectDaprEventAsync( - DaprDistributedEventBus distributedEventBus, - IDaprSerializer daprSerializer, - string topic, - string data) - { - if (!TryResolveIncomingEvent(distributedEventBus, daprSerializer, topic, data, out var eventType, out var eventData)) - { - return; - } - - await distributedEventBus.TriggerHandlersAsync(eventType, eventData); - } - - private static bool TryResolveIncomingEvent( - DaprDistributedEventBus distributedEventBus, - IDaprSerializer daprSerializer, - string topic, - string data, - out Type eventType, - out object eventData) - { - var typedEventType = distributedEventBus.GetEventType(topic); - if (typedEventType != null) - { - eventType = typedEventType; - eventData = daprSerializer.Deserialize(data, typedEventType); - return true; - } - - if (!distributedEventBus.IsAnonymousEvent(topic)) - { - eventType = default!; - eventData = default!; - return false; - } - - eventType = typeof(AnonymousEventData); - eventData = AnonymousEventData.FromJson(topic, data); - return true; - } } diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs index 255b4cffff..1db318b40e 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs @@ -1,4 +1,8 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; + namespace Volo.Abp.EventBus; /// @@ -14,14 +18,11 @@ public class AnonymousEventData public string EventName { get; } /// - /// The raw event data payload. Can be a CLR object, , or any serializable object. + /// The raw event data payload. Can be a CLR object, , or any serializable object. /// - internal object Data { get; } + public object Data { get; } - /// - /// The raw JSON payload when the event is created from transport data. - /// - public string? JsonData { get; } + private JsonElement? _cachedJsonElement; /// /// Creates a new instance of . @@ -35,17 +36,105 @@ public class AnonymousEventData } /// - /// Creates a new instance of from raw JSON. + /// Converts the to a loosely-typed object graph + /// (dictionaries for objects, lists for arrays, primitives for values). /// - public static AnonymousEventData FromJson(string eventName, string jsonData) + /// A CLR object representation of the event data + public object ConvertToTypedObject() { - return new AnonymousEventData(eventName, data: null!, jsonData); + return ConvertElement(GetJsonElement()); } - private AnonymousEventData(string eventName, object data, string? jsonData) + /// + /// Converts the to a strongly-typed object. + /// Returns the data directly if it is already of type , + /// otherwise deserializes from JSON. + /// + /// Target type to convert to + /// The deserialized object of type + /// Thrown when deserialization fails + public T ConvertToTypedObject() { - EventName = eventName; - Data = data; - JsonData = jsonData; + if (Data is T typedData) + { + return typedData; + } + + return GetJsonElement().Deserialize() + ?? throw new InvalidOperationException($"Failed to deserialize AnonymousEventData to {typeof(T).FullName}."); + } + + /// + /// Converts the to the specified . + /// Returns the data directly if it is already an instance of the target type, + /// otherwise deserializes from JSON. + /// + /// Target type to convert to + /// The deserialized object + /// Thrown when deserialization fails + public object ConvertToTypedObject(Type type) + { + if (type.IsInstanceOfType(Data)) + { + return Data; + } + + return GetJsonElement().Deserialize(type) + ?? throw new InvalidOperationException($"Failed to deserialize AnonymousEventData to {type.FullName}."); + } + + private JsonElement GetJsonElement() + { + if (_cachedJsonElement.HasValue) + { + return _cachedJsonElement.Value; + } + + if (Data is JsonElement existingElement) + { + _cachedJsonElement = existingElement; + return existingElement; + } + + _cachedJsonElement = JsonSerializer.SerializeToElement(Data); + return _cachedJsonElement.Value; + } + + private static object ConvertElement(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + { + var obj = new Dictionary(); + foreach (var property in element.EnumerateObject()) + { + obj[property.Name] = property.Value.ValueKind == JsonValueKind.Null + ? null + : ConvertElement(property.Value); + } + return obj; + } + case JsonValueKind.Array: + return element.EnumerateArray() + .Select(item => item.ValueKind == JsonValueKind.Null ? null : (object?)ConvertElement(item)) + .ToList(); + case JsonValueKind.String: + return element.GetString()!; + case JsonValueKind.Number when element.TryGetInt64(out var longValue): + return longValue; + case JsonValueKind.Number when element.TryGetDecimal(out var decimalValue): + return decimalValue; + case JsonValueKind.Number when element.TryGetDouble(out var doubleValue): + return doubleValue; + case JsonValueKind.True: + return true; + case JsonValueKind.False: + return false; + case JsonValueKind.Null: + case JsonValueKind.Undefined: + default: + return null!; + } } } diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventDataConverter.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventDataConverter.cs deleted file mode 100644 index e4b9234ccd..0000000000 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventDataConverter.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; - -namespace Volo.Abp.EventBus; - -public static class AnonymousEventDataConverter -{ - public static T ConvertToTypedObject(AnonymousEventData eventData) - { - if (eventData.Data is T typedData) - { - return typedData; - } - - return ParseJsonElement(eventData).Deserialize() - ?? throw new InvalidOperationException($"Failed to deserialize AnonymousEventData to {typeof(T).FullName}."); - } - - public static object ConvertToTypedObject(AnonymousEventData eventData, Type type) - { - if (type.IsInstanceOfType(eventData.Data)) - { - return eventData.Data; - } - - return ParseJsonElement(eventData).Deserialize(type) - ?? throw new InvalidOperationException($"Failed to deserialize AnonymousEventData to {type.FullName}."); - } - - public static object ConvertToLooseObject(AnonymousEventData eventData) - { - return ConvertElement(ParseJsonElement(eventData)); - } - - public static string GetJsonData(AnonymousEventData eventData) - { - return eventData.JsonData ?? ParseJsonElement(eventData).GetRawText(); - } - - private static JsonElement ParseJsonElement(AnonymousEventData eventData) - { - if (eventData.Data is JsonElement existingElement) - { - return existingElement; - } - - if (eventData.JsonData != null) - { - return JsonDocument.Parse(eventData.JsonData).RootElement.Clone(); - } - - return JsonSerializer.SerializeToElement(eventData.Data); - } - - private static object ConvertElement(JsonElement element) - { - switch (element.ValueKind) - { - case JsonValueKind.Object: - { - var obj = new Dictionary(); - foreach (var property in element.EnumerateObject()) - { - obj[property.Name] = property.Value.ValueKind == JsonValueKind.Null - ? null - : ConvertElement(property.Value); - } - - return obj; - } - case JsonValueKind.Array: - return element.EnumerateArray() - .Select(item => item.ValueKind == JsonValueKind.Null ? null : (object?)ConvertElement(item)) - .ToList(); - case JsonValueKind.String: - return element.GetString()!; - case JsonValueKind.Number when element.TryGetInt64(out var longValue): - return longValue; - case JsonValueKind.Number when element.TryGetDecimal(out var decimalValue): - return decimalValue; - case JsonValueKind.Number when element.TryGetDouble(out var doubleValue): - return doubleValue; - case JsonValueKind.True: - return true; - case JsonValueKind.False: - return false; - case JsonValueKind.Null: - case JsonValueKind.Undefined: - default: - return null!; - } - } -} diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index 83eba73fa3..020757ad07 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Azure.Messaging.ServiceBus; using Microsoft.Extensions.DependencyInjection; @@ -91,9 +92,10 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen { eventData = Serializer.Deserialize(message.Body.ToArray(), eventType); } - else if (HasAnonymousHandlers(eventName)) + else if (AnonymousHandlerFactories.ContainsKey(eventName)) { - eventData = CreateAnonymousEventData(eventName, Serializer.Deserialize(message.Body.ToArray())); + var data = Serializer.Deserialize(message.Body.ToArray()); + eventData = new AnonymousEventData(eventName, data); eventType = typeof(AnonymousEventData); } else @@ -115,21 +117,14 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { var handlerFactories = GetOrCreateHandlerFactories(eventType); - var added = false; - handlerFactories.Locking(factories => - { - if (!factory.IsInFactories(factories)) - { - factories.Add(factory); - added = true; - } - }); - if (!added) + if (factory.IsInFactories(handlerFactories)) { return NullDisposable.Instance; } + handlerFactories.Add(factory); + return new EventHandlerFactoryUnregistrar(this, eventType, factory); } @@ -137,21 +132,14 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); - var added = false; - handlerFactories.Locking(factories => - { - if (!handler.IsInFactories(factories)) - { - factories.Add(handler); - added = true; - } - }); - if (!added) + if (handler.IsInFactories(handlerFactories)) { return NullDisposable.Instance; } + handlerFactories.Add(handler); + return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); } @@ -211,9 +199,20 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { - var anonymousEventData = CreateAnonymousEnvelope(eventName, eventData); - return TryPublishTypedByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete) - ?? PublishAnonymousByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete); + var eventType = EventTypes.GetOrDefault(eventName); + var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + } + + if (AnonymousHandlerFactories.ContainsKey(eventName)) + { + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + } + + throw new AbpException($"Unknown event name: {eventName}"); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) @@ -288,7 +287,20 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen public async override Task ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { - if (!TryResolveStoredEventData(incomingEvent.EventName, incomingEvent.EventData, out var eventType, out var eventData)) + var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + { + var element = Serializer.Deserialize(incomingEvent.EventData); + eventData = new AnonymousEventData(incomingEvent.EventName, element); + eventType = typeof(AnonymousEventData); + } + else { return; } @@ -388,45 +400,29 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => factories.Remove(factory)); - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => factories.Remove(factory)); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => - { - factories.RemoveAll( - factory => - factory is SingleInstanceHandlerFactory singleFactory && - singleFactory.HandlerInstance == handler - ); - }); - - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); } /// public override void UnsubscribeAll(string eventName) { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => factories.Clear()); - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => factories.Clear()); } protected override IEnumerable GetAnonymousHandlerFactories(string eventName) @@ -452,81 +448,6 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); } - private AnonymousEventData CreateAnonymousEnvelope(string eventName, object eventData) - { - return eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); - } - - private Task? TryPublishTypedByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) - { - var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) - { - return null; - } - - var typedEventData = AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, eventType); - return PublishAsync(eventType, typedEventData, onUnitOfWorkComplete); - } - - private Task PublishAnonymousByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) - { - if (!HasAnonymousHandlers(eventName)) - { - return Task.CompletedTask; - } - - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); - } - - private bool TryResolveStoredEventData(string eventName, byte[] payload, out Type eventType, out object eventData) - { - eventType = EventTypes.GetOrDefault(eventName)!; - if (eventType != null) - { - eventData = Serializer.Deserialize(payload, eventType); - return true; - } - - if (!HasAnonymousHandlers(eventName)) - { - eventData = default!; - eventType = default!; - return false; - } - - eventType = typeof(AnonymousEventData); - eventData = CreateAnonymousEventData(eventName, payload); - return true; - } - - private bool HasAnonymousHandlers(string eventName) - { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return false; - } - - var hasHandlers = false; - handlerFactories.Locking(factories => hasHandlers = factories.Count > 0); - if (!hasHandlers) - { - AnonymousHandlerFactories.TryRemove(eventName, out _); - } - - return hasHandlers; - } - - private void CleanupAnonymousHandlerFactoriesIfEmpty(string eventName, List handlerFactories) - { - var isEmpty = false; - handlerFactories.Locking(factories => isEmpty = factories.Count == 0); - if (isEmpty) - { - AnonymousHandlerFactories.TryRemove(eventName, out _); - } - } - private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { return handlerEventType == targetEventType || handlerEventType.IsAssignableFrom(targetEventType); diff --git a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs index 13edc0106d..1231daf632 100644 --- a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs @@ -70,21 +70,14 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { var handlerFactories = GetOrCreateHandlerFactories(eventType); - var added = false; - handlerFactories.Locking(factories => - { - if (!factory.IsInFactories(factories)) - { - factories.Add(factory); - added = true; - } - }); - if (!added) + if (factory.IsInFactories(handlerFactories)) { return NullDisposable.Instance; } + handlerFactories.Add(factory); + return new EventHandlerFactoryUnregistrar(this, eventType, factory); } @@ -92,21 +85,14 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); - var added = false; - handlerFactories.Locking(factories => - { - if (!handler.IsInFactories(factories)) - { - factories.Add(handler); - added = true; - } - }); - if (!added) + if (handler.IsInFactories(handlerFactories)) { return NullDisposable.Instance; } + handlerFactories.Add(handler); + return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); } @@ -163,9 +149,20 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { - var anonymousEventData = CreateAnonymousEnvelope(eventName, eventData); - return TryPublishTypedByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete) - ?? PublishAnonymousByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete); + var eventType = EventTypes.GetOrDefault(eventName); + var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + } + + if (AnonymousHandlerFactories.ContainsKey(eventName)) + { + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + } + + throw new AbpException($"Unknown event name: {eventName}"); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) @@ -181,7 +178,18 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public async override Task PublishFromOutboxAsync(OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig) { - if (!TryResolveStoredEventData(outgoingEvent.EventName, outgoingEvent.EventData, out var eventType, out var eventData)) + var eventType = EventTypes.GetOrDefault(outgoingEvent.EventName); + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(outgoingEvent.EventData, eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(outgoingEvent.EventName)) + { + eventData = Serializer.Deserialize(outgoingEvent.EventData, typeof(object)); + } + else { return; } @@ -222,7 +230,19 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public async override Task ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { - if (!TryResolveStoredEventData(incomingEvent.EventName, incomingEvent.EventData, out var eventType, out var eventData)) + var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + { + eventData = new AnonymousEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData, typeof(object))); + eventType = typeof(AnonymousEventData); + } + else { return; } @@ -307,51 +327,33 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public bool IsAnonymousEvent(string eventName) { - return HasAnonymousHandlers(eventName); + return AnonymousHandlerFactories.ContainsKey(eventName); } /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => factories.Remove(factory)); - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => - { - factories.RemoveAll( - factory => - factory is SingleInstanceHandlerFactory singleFactory && - singleFactory.HandlerInstance == handler - ); - }); - - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); } /// public override void UnsubscribeAll(string eventName) { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => factories.Clear()); - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); } protected override IEnumerable GetAnonymousHandlerFactories(string eventName) @@ -377,81 +379,6 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); } - private AnonymousEventData CreateAnonymousEnvelope(string eventName, object eventData) - { - return eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); - } - - private Task? TryPublishTypedByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) - { - var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) - { - return null; - } - - var typedEventData = AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, eventType); - return PublishAsync(eventType, typedEventData, onUnitOfWorkComplete); - } - - private Task PublishAnonymousByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) - { - if (!HasAnonymousHandlers(eventName)) - { - return Task.CompletedTask; - } - - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); - } - - private bool TryResolveStoredEventData(string eventName, byte[] payload, out Type eventType, out object eventData) - { - eventType = EventTypes.GetOrDefault(eventName)!; - if (eventType != null) - { - eventData = Serializer.Deserialize(payload, eventType); - return true; - } - - if (!HasAnonymousHandlers(eventName)) - { - eventData = default!; - eventType = default!; - return false; - } - - eventType = typeof(AnonymousEventData); - eventData = CreateAnonymousEventData(eventName, payload); - return true; - } - - private bool HasAnonymousHandlers(string eventName) - { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return false; - } - - var hasHandlers = false; - handlerFactories.Locking(factories => hasHandlers = factories.Count > 0); - if (!hasHandlers) - { - AnonymousHandlerFactories.TryRemove(eventName, out _); - } - - return hasHandlers; - } - - private void CleanupAnonymousHandlerFactoriesIfEmpty(string eventName, List handlerFactories) - { - var isEmpty = false; - handlerFactories.Locking(factories => isEmpty = factories.Count == 0); - if (isEmpty) - { - AnonymousHandlerFactories.TryRemove(eventName, out _); - } - } - private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { //Should trigger same type diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs index be82c603c1..e7e44e449d 100644 --- a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Confluent.Kafka; using Microsoft.Extensions.DependencyInjection; @@ -91,9 +92,10 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen { eventData = Serializer.Deserialize(message.Value, eventType); } - else if (HasAnonymousHandlers(eventName)) + else if (AnonymousHandlerFactories.ContainsKey(eventName)) { - eventData = CreateAnonymousEventData(eventName, message.Value); + var element = Serializer.Deserialize(message.Value); + eventData = new AnonymousEventData(eventName, element); eventType = typeof(AnonymousEventData); } else @@ -115,21 +117,14 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { var handlerFactories = GetOrCreateHandlerFactories(eventType); - var added = false; - handlerFactories.Locking(factories => - { - if (!factory.IsInFactories(factories)) - { - factories.Add(factory); - added = true; - } - }); - if (!added) + if (factory.IsInFactories(handlerFactories)) { return NullDisposable.Instance; } + handlerFactories.Add(factory); + return new EventHandlerFactoryUnregistrar(this, eventType, factory); } @@ -137,21 +132,14 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); - var added = false; - handlerFactories.Locking(factories => - { - if (!handler.IsInFactories(factories)) - { - factories.Add(handler); - added = true; - } - }); - if (!added) + if (handler.IsInFactories(handlerFactories)) { return NullDisposable.Instance; } + handlerFactories.Add(handler); + return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); } @@ -212,9 +200,20 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { - var anonymousEventData = CreateAnonymousEnvelope(eventName, eventData); - return TryPublishTypedByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete) - ?? PublishAnonymousByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete); + var eventType = EventTypes.GetOrDefault(eventName); + var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + } + + if (AnonymousHandlerFactories.ContainsKey(eventName)) + { + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + } + + throw new AbpException($"Unknown event name: {eventName}"); } protected override async Task PublishToEventBusAsync(Type eventType, object eventData) @@ -326,7 +325,20 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { - if (!TryResolveStoredEventData(incomingEvent.EventName, incomingEvent.EventData, out var eventType, out var eventData)) + var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + { + var element = Serializer.Deserialize(incomingEvent.EventData); + eventData = new AnonymousEventData(incomingEvent.EventName, element); + eventType = typeof(AnonymousEventData); + } + else { return; } @@ -424,45 +436,27 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => factories.Remove(factory)); - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => - { - factories.RemoveAll( - factory => - factory is SingleInstanceHandlerFactory singleFactory && - singleFactory.HandlerInstance == handler - ); - }); - - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); } /// public override void UnsubscribeAll(string eventName) { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => factories.Clear()); - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); } protected override IEnumerable GetAnonymousHandlerFactories(string eventName) @@ -488,81 +482,6 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); } - private AnonymousEventData CreateAnonymousEnvelope(string eventName, object eventData) - { - return eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); - } - - private Task? TryPublishTypedByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) - { - var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) - { - return null; - } - - var typedEventData = AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, eventType); - return PublishAsync(eventType, typedEventData, onUnitOfWorkComplete); - } - - private Task PublishAnonymousByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) - { - if (!HasAnonymousHandlers(eventName)) - { - return Task.CompletedTask; - } - - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); - } - - private bool TryResolveStoredEventData(string eventName, byte[] payload, out Type eventType, out object eventData) - { - eventType = EventTypes.GetOrDefault(eventName)!; - if (eventType != null) - { - eventData = Serializer.Deserialize(payload, eventType); - return true; - } - - if (!HasAnonymousHandlers(eventName)) - { - eventData = default!; - eventType = default!; - return false; - } - - eventType = typeof(AnonymousEventData); - eventData = CreateAnonymousEventData(eventName, payload); - return true; - } - - private bool HasAnonymousHandlers(string eventName) - { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return false; - } - - var hasHandlers = false; - handlerFactories.Locking(factories => hasHandlers = factories.Count > 0); - if (!hasHandlers) - { - AnonymousHandlerFactories.TryRemove(eventName, out _); - } - - return hasHandlers; - } - - private void CleanupAnonymousHandlerFactoriesIfEmpty(string eventName, List handlerFactories) - { - var isEmpty = false; - handlerFactories.Locking(factories => isEmpty = factories.Count == 0); - if (isEmpty) - { - AnonymousHandlerFactories.TryRemove(eventName, out _); - } - } - private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { //Should trigger same type diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs index 097f2cbbe7..771dae9f09 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs @@ -109,10 +109,10 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis { eventData = Serializer.Deserialize(ea.Body.ToArray(), eventType); } - else if (HasAnonymousHandlers(eventName)) + else if (AnonymousHandlerFactories.ContainsKey(eventName)) { eventType = typeof(AnonymousEventData); - eventData = CreateAnonymousEventData(eventName, ea.Body.ToArray()); + eventData = new AnonymousEventData(eventName, Serializer.Deserialize(ea.Body.ToArray())); } else { @@ -134,24 +134,15 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { var handlerFactories = GetOrCreateHandlerFactories(eventType); - var added = false; - var isFirstHandler = false; - handlerFactories.Locking(factories => - { - if (!factory.IsInFactories(factories)) - { - isFirstHandler = factories.Count == 0; - factories.Add(factory); - added = true; - } - }); - if (!added) + if (factory.IsInFactories(handlerFactories)) { return NullDisposable.Instance; } - if (isFirstHandler) + handlerFactories.Add(factory); + + if (handlerFactories.Count == 1) //TODO: Multi-threading! { Consumer.BindAsync(EventNameAttribute.GetNameOrDefault(eventType)); } @@ -163,24 +154,15 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); - var added = false; - var isFirstHandler = false; - handlerFactories.Locking(factories => - { - if (!handler.IsInFactories(factories)) - { - isFirstHandler = factories.Count == 0; - factories.Add(handler); - added = true; - } - }); - - if (!added) + + if (handler.IsInFactories(handlerFactories)) { return NullDisposable.Instance; } - - if (isFirstHandler) + + handlerFactories.Add(handler); + + if (handlerFactories.Count == 1) //TODO: Multi-threading! { Consumer.BindAsync(eventName); } @@ -245,9 +227,20 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { - var anonymousEventData = CreateAnonymousEnvelope(eventName, eventData); - return TryPublishTypedByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete) - ?? PublishAnonymousByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete); + var eventType = EventTypes.GetOrDefault(eventName); + var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + } + + if (AnonymousHandlerFactories.ContainsKey(eventName)) + { + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + } + + throw new AbpException($"Unknown event name: {eventName}"); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) @@ -312,7 +305,19 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { - if (!TryResolveStoredEventData(incomingEvent.EventName, incomingEvent.EventData, out var eventType, out var eventData)) + var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + { + eventData = new AnonymousEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData)); + eventType = typeof(AnonymousEventData); + } + else { return; } @@ -482,45 +487,27 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => factories.Remove(factory)); - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => - { - factories.RemoveAll( - factory => - factory is SingleInstanceHandlerFactory singleFactory && - singleFactory.HandlerInstance == handler - ); - }); - - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); } /// public override void UnsubscribeAll(string eventName) { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => factories.Clear()); - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); } protected override IEnumerable GetAnonymousHandlerFactories(string eventName) @@ -546,81 +533,6 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); } - private AnonymousEventData CreateAnonymousEnvelope(string eventName, object eventData) - { - return eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); - } - - private Task? TryPublishTypedByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) - { - var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) - { - return null; - } - - var typedEventData = AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, eventType); - return PublishAsync(eventType, typedEventData, onUnitOfWorkComplete); - } - - private Task PublishAnonymousByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) - { - if (!HasAnonymousHandlers(eventName)) - { - return Task.CompletedTask; - } - - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); - } - - private bool TryResolveStoredEventData(string eventName, byte[] payload, out Type eventType, out object eventData) - { - eventType = EventTypes.GetOrDefault(eventName)!; - if (eventType != null) - { - eventData = Serializer.Deserialize(payload, eventType); - return true; - } - - if (!HasAnonymousHandlers(eventName)) - { - eventData = default!; - eventType = default!; - return false; - } - - eventType = typeof(AnonymousEventData); - eventData = CreateAnonymousEventData(eventName, payload); - return true; - } - - private bool HasAnonymousHandlers(string eventName) - { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return false; - } - - var hasHandlers = false; - handlerFactories.Locking(factories => hasHandlers = factories.Count > 0); - if (!hasHandlers) - { - AnonymousHandlerFactories.TryRemove(eventName, out _); - } - - return hasHandlers; - } - - private void CleanupAnonymousHandlerFactoriesIfEmpty(string eventName, List handlerFactories) - { - var isEmpty = false; - handlerFactories.Locking(factories => isEmpty = factories.Count == 0); - if (isEmpty) - { - AnonymousHandlerFactories.TryRemove(eventName, out _); - } - } - private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { //Should trigger same type diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs index bf1bd1a585..c4f1a141e6 100644 --- a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs @@ -100,24 +100,15 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override IDisposable Subscribe(Type eventType, IEventHandlerFactory factory) { var handlerFactories = GetOrCreateHandlerFactories(eventType); - var added = false; - var isFirstHandler = false; - handlerFactories.Locking(factories => - { - if (!factory.IsInFactories(factories)) - { - isFirstHandler = factories.Count == 0; - factories.Add(factory); - added = true; - } - }); - if (!added) + if (factory.IsInFactories(handlerFactories)) { return NullDisposable.Instance; } - if (isFirstHandler) + handlerFactories.Add(factory); + + if (handlerFactories.Count == 1) //TODO: Multi-threading! { Rebus.Subscribe(eventType); } @@ -129,24 +120,15 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); - var added = false; - var isFirstHandler = false; - handlerFactories.Locking(factories => - { - if (!handler.IsInFactories(factories)) - { - isFirstHandler = factories.Count == 0; - factories.Add(handler); - added = true; - } - }); - if (!added) + if (handler.IsInFactories(handlerFactories)) { return NullDisposable.Instance; } - if (isFirstHandler && AnonymousHandlerFactories.Count == 1) + handlerFactories.Add(handler); + + if (AnonymousHandlerFactories.Count == 1) //TODO: Multi-threading! { Rebus.Subscribe(typeof(AnonymousEventData)); } @@ -211,9 +193,20 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { - var anonymousEventData = CreateAnonymousEnvelope(eventName, eventData); - return TryPublishTypedByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete) - ?? PublishAnonymousByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete); + var eventType = EventTypes.GetOrDefault(eventName); + var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + } + + if (AnonymousHandlerFactories.ContainsKey(eventName)) + { + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + } + + throw new AbpException($"Unknown event name: {eventName}"); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) @@ -235,7 +228,19 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig) { - if (!TryResolveStoredEventData(outgoingEvent.EventName, outgoingEvent.EventData, out var eventType, out var eventData)) + var eventType = EventTypes.GetOrDefault(outgoingEvent.EventName); + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(outgoingEvent.EventData, eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(outgoingEvent.EventName)) + { + eventData = new AnonymousEventData(outgoingEvent.EventName, Serializer.Deserialize(outgoingEvent.EventData, typeof(object))); + eventType = typeof(AnonymousEventData); + } + else { return; } @@ -268,6 +273,15 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen { await PublishFromOutboxAsync(outgoingEvent, outboxConfig); + using (CorrelationIdProvider.Change(outgoingEvent.GetCorrelationId())) + { + await TriggerDistributedEventSentAsync(new DistributedEventSent() + { + Source = DistributedEventSource.Outbox, + EventName = outgoingEvent.EventName, + EventData = outgoingEvent.EventData + }); + } } await scope.CompleteAsync(); @@ -278,7 +292,19 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { - if (!TryResolveStoredEventData(incomingEvent.EventName, incomingEvent.EventData, out var eventType, out var eventData)) + var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); + object eventData; + + if (eventType != null) + { + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + } + else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + { + eventData = new AnonymousEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData, typeof(object))); + eventType = typeof(AnonymousEventData); + } + else { return; } @@ -367,45 +393,27 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => factories.Remove(factory)); - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => - { - factories.RemoveAll( - factory => - factory is SingleInstanceHandlerFactory singleFactory && - singleFactory.HandlerInstance == handler - ); - }); - - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); } /// public override void UnsubscribeAll(string eventName) { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => factories.Clear()); - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); } protected override IEnumerable GetAnonymousHandlerFactories(string eventName) @@ -431,81 +439,6 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); } - private AnonymousEventData CreateAnonymousEnvelope(string eventName, object eventData) - { - return eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); - } - - private Task? TryPublishTypedByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) - { - var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) - { - return null; - } - - var typedEventData = AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, eventType); - return PublishAsync(eventType, typedEventData, onUnitOfWorkComplete); - } - - private Task PublishAnonymousByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) - { - if (!HasAnonymousHandlers(eventName)) - { - return Task.CompletedTask; - } - - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); - } - - private bool TryResolveStoredEventData(string eventName, byte[] payload, out Type eventType, out object eventData) - { - eventType = EventTypes.GetOrDefault(eventName)!; - if (eventType != null) - { - eventData = Serializer.Deserialize(payload, eventType); - return true; - } - - if (!HasAnonymousHandlers(eventName)) - { - eventData = default!; - eventType = default!; - return false; - } - - eventType = typeof(AnonymousEventData); - eventData = CreateAnonymousEventData(eventName, payload); - return true; - } - - private bool HasAnonymousHandlers(string eventName) - { - if (!AnonymousHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return false; - } - - var hasHandlers = false; - handlerFactories.Locking(factories => hasHandlers = factories.Count > 0); - if (!hasHandlers) - { - AnonymousHandlerFactories.TryRemove(eventName, out _); - } - - return hasHandlers; - } - - private void CleanupAnonymousHandlerFactoriesIfEmpty(string eventName, List handlerFactories) - { - var isEmpty = false; - handlerFactories.Locking(factories => isEmpty = factories.Count == 0); - if (isEmpty) - { - AnonymousHandlerFactories.TryRemove(eventName, out _); - } - } - private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { //Should trigger same type diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs index a5ffb87245..88ce5662f8 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -113,9 +112,15 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB bool onUnitOfWorkComplete = true, bool useOutbox = true) { - var anonymousEventData = CreateAnonymousEnvelope(eventName, eventData); - return TryPublishTypedByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete, useOutbox) - ?? PublishAnonymousByEventNameAsync(anonymousEventData, onUnitOfWorkComplete, useOutbox); + var eventType = GetEventTypeByEventName(eventName); + var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete, useOutbox); + } + + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete, useOutbox); } public abstract Task PublishFromOutboxAsync( @@ -147,14 +152,14 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB if (outboxConfig.Selector == null || outboxConfig.Selector(eventType)) { var eventOutbox = (IEventOutbox)unitOfWork.ServiceProvider.GetRequiredService(outboxConfig.ImplementationType); - var (eventName, resolvedEventData) = ResolveEventForPublishing(eventType, eventData); + (var eventName, eventData) = ResolveEventForPublishing(eventType, eventData); - await OnAddToOutboxAsync(eventName, eventType, resolvedEventData); + await OnAddToOutboxAsync(eventName, eventType, eventData); var outgoingEventInfo = new OutgoingEventInfo( GuidGenerator.Create(), eventName, - SerializeEventData(resolvedEventData), + Serialize(eventData), Clock.Now ); @@ -209,11 +214,13 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB } } + eventData = GetEventData(eventData); + var incomingEventInfo = new IncomingEventInfo( GuidGenerator.Create(), messageId!, eventName, - SerializeEventData(eventData), + Serialize(eventData), Clock.Now ); incomingEventInfo.SetCorrelationId(correlationId!); @@ -228,55 +235,6 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB protected abstract byte[] Serialize(object eventData); - protected virtual byte[] SerializeEventData(object eventData) - { - if (eventData is AnonymousEventData anonymousEventData) - { - return Encoding.UTF8.GetBytes(AnonymousEventDataConverter.GetJsonData(anonymousEventData)); - } - - return Serialize(eventData); - } - - protected virtual AnonymousEventData CreateAnonymousEventData(string eventName, byte[] eventData) - { - return AnonymousEventData.FromJson(eventName, Encoding.UTF8.GetString(eventData)); - } - - protected virtual AnonymousEventData CreateAnonymousEventData(string eventName, string eventData) - { - return AnonymousEventData.FromJson(eventName, eventData); - } - - protected virtual AnonymousEventData CreateAnonymousEnvelope(string eventName, object eventData) - { - return eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); - } - - protected virtual Task? TryPublishTypedByEventNameAsync( - string eventName, - AnonymousEventData anonymousEventData, - bool onUnitOfWorkComplete, - bool useOutbox) - { - var eventType = GetEventTypeByEventName(eventName); - if (eventType == null) - { - return null; - } - - var typedEventData = AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, eventType); - return PublishAsync(eventType, typedEventData, onUnitOfWorkComplete, useOutbox); - } - - protected virtual Task PublishAnonymousByEventNameAsync( - AnonymousEventData anonymousEventData, - bool onUnitOfWorkComplete, - bool useOutbox) - { - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete, useOutbox); - } - protected virtual async Task TriggerHandlersDirectAsync(Type eventType, object eventData) { await TriggerDistributedEventReceivedAsync(new DistributedEventReceived @@ -339,7 +297,7 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB { if (eventData is AnonymousEventData anonymousEventData) { - return AnonymousEventDataConverter.ConvertToLooseObject(anonymousEventData); + return anonymousEventData.Data; } return eventData; diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs index ede70885ff..c78b30260e 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs @@ -3,7 +3,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -77,8 +76,7 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { AnonymousEventNames.GetOrAdd(eventName, true); - LocalEventBus.Subscribe(eventName, handler); - return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + return LocalEventBus.Subscribe(eventName, handler); } /// @@ -86,8 +84,7 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen { var eventName = EventNameAttribute.GetNameOrDefault(eventType); EventTypes.GetOrAdd(eventName, eventType); - LocalEventBus.Subscribe(eventType, factory); - return new EventHandlerFactoryUnregistrar(this, eventType, factory); + return LocalEventBus.Subscribe(eventType, factory); } public override void Unsubscribe(Func action) @@ -109,14 +106,12 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { LocalEventBus.Unsubscribe(eventName, factory); - CleanupAnonymousEventName(eventName); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { LocalEventBus.Unsubscribe(eventName, handler); - CleanupAnonymousEventName(eventName); } /// @@ -129,7 +124,6 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override void UnsubscribeAll(string eventName) { LocalEventBus.UnsubscribeAll(eventName); - CleanupAnonymousEventName(eventName); } /// @@ -178,14 +172,25 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) { - var anonymousEventData = CreateAnonymousEnvelope(eventName, eventData); - return TryPublishTypedByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete, useOutbox) - ?? PublishAnonymousByEventNameAsync(anonymousEventData, onUnitOfWorkComplete, useOutbox); + var eventType = EventTypes.GetOrDefault(eventName); + var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete, useOutbox); + } + + if (!AnonymousEventNames.ContainsKey(eventName)) + { + throw new AbpException($"Unknown event name: {eventName}"); + } + + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete, useOutbox); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) { - if (await AddToInboxAsync(Guid.NewGuid().ToString(), GetEventName(eventType, eventData), eventType, eventData, null)) + if (await AddToInboxAsync(Guid.NewGuid().ToString(), EventNameAttribute.GetNameOrDefault(eventType), eventType, eventData, null)) { return; } @@ -214,12 +219,30 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen EventData = outgoingEvent.EventData }); - if (!TryResolveStoredEventType(outgoingEvent.EventName, out var eventType)) + var eventType = EventTypes.GetOrDefault(outgoingEvent.EventName); + if (eventType == null) { - return; + var isAnonymous = AnonymousEventNames.ContainsKey(outgoingEvent.EventName); + if (!isAnonymous) + { + return; + } + + eventType = typeof(AnonymousEventData); + } + + object eventData; + if (eventType == typeof(AnonymousEventData)) + { + eventData = new AnonymousEventData( + outgoingEvent.EventName, + JsonSerializer.Deserialize(outgoingEvent.EventData)!); + } + else + { + eventData = JsonSerializer.Deserialize(outgoingEvent.EventData, eventType)!; } - var eventData = DeserializeStoredEventData(outgoingEvent.EventName, outgoingEvent.EventData, eventType); if (await AddToInboxAsync(Guid.NewGuid().ToString(), outgoingEvent.EventName, eventType, eventData, null)) { return; @@ -238,12 +261,30 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen public async override Task ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig) { - if (!TryResolveStoredEventType(incomingEvent.EventName, out var eventType)) + var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); + if (eventType == null) { - return; + var isAnonymous = AnonymousEventNames.ContainsKey(incomingEvent.EventName); + if (!isAnonymous) + { + return; + } + + eventType = typeof(AnonymousEventData); + } + + object eventData; + if (eventType == typeof(AnonymousEventData)) + { + eventData = new AnonymousEventData( + incomingEvent.EventName, + JsonSerializer.Deserialize(incomingEvent.EventData)!); + } + else + { + eventData = JsonSerializer.Deserialize(incomingEvent.EventData, eventType)!; } - var eventData = DeserializeStoredEventData(incomingEvent.EventName, incomingEvent.EventData, eventType); var exceptions = new List(); using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) { @@ -257,7 +298,7 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected override byte[] Serialize(object eventData) { - return Encoding.UTF8.GetBytes(JsonSerializer.Serialize(eventData)); + return JsonSerializer.SerializeToUtf8Bytes(eventData); } protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) @@ -283,89 +324,4 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen { return EventTypes.GetOrDefault(eventName); } - - protected override AnonymousEventData CreateAnonymousEnvelope(string eventName, object eventData) - { - return eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); - } - - protected override Task? TryPublishTypedByEventNameAsync( - string eventName, - AnonymousEventData anonymousEventData, - bool onUnitOfWorkComplete, - bool useOutbox) - { - var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) - { - return null; - } - - var typedEventData = AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, eventType); - return PublishAsync(eventType, typedEventData, onUnitOfWorkComplete, useOutbox); - } - - protected override Task PublishAnonymousByEventNameAsync( - AnonymousEventData anonymousEventData, - bool onUnitOfWorkComplete, - bool useOutbox) - { - if (!HasAnonymousEventName(anonymousEventData.EventName)) - { - return Task.CompletedTask; - } - - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete, useOutbox); - } - - protected virtual bool TryResolveStoredEventType(string eventName, out Type eventType) - { - eventType = EventTypes.GetOrDefault(eventName)!; - if (eventType != null) - { - return true; - } - - if (!HasAnonymousEventName(eventName)) - { - return false; - } - - eventType = typeof(AnonymousEventData); - return true; - } - - protected virtual object DeserializeStoredEventData(string eventName, byte[] eventData, Type eventType) - { - if (eventType == typeof(AnonymousEventData)) - { - return CreateAnonymousEventData(eventName, eventData); - } - - return JsonSerializer.Deserialize(Encoding.UTF8.GetString(eventData), eventType)!; - } - - protected virtual void CleanupAnonymousEventName(string eventName) - { - if (!LocalEventBus.GetAnonymousEventHandlerFactories(eventName).Any()) - { - AnonymousEventNames.TryRemove(eventName, out _); - } - } - - protected virtual bool HasAnonymousEventName(string eventName) - { - if (!AnonymousEventNames.ContainsKey(eventName)) - { - return false; - } - - if (!LocalEventBus.GetAnonymousEventHandlerFactories(eventName).Any()) - { - AnonymousEventNames.TryRemove(eventName, out _); - return false; - } - - return true; - } } diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs index b4b589d6dd..8c344f5aa7 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs @@ -174,7 +174,9 @@ public abstract class EventBusBase : IEventBus actualEventType.GetGenericArguments().Length == 1 && typeof(IEventDataWithInheritableGenericArgument).IsAssignableFrom(actualEventType)) { - var resolvedEventData = ResolveActualEventData(eventData, actualEventType); + var resolvedEventData = eventData is AnonymousEventData aed + ? aed.ConvertToTypedObject(actualEventType) + : eventData; var genericArg = actualEventType.GetGenericArguments()[0]; var baseArg = genericArg.GetTypeInfo().BaseType; @@ -207,7 +209,7 @@ public abstract class EventBusBase : IEventBus { if (eventData is AnonymousEventData anonymousEventData && handlerEventType != typeof(AnonymousEventData)) { - return AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, handlerEventType); + return anonymousEventData.ConvertToTypedObject(handlerEventType); } if (handlerEventType == typeof(AnonymousEventData) && eventData is not AnonymousEventData) @@ -218,16 +220,6 @@ public abstract class EventBusBase : IEventBus return eventData; } - protected virtual object ResolveActualEventData(object eventData, Type actualEventType) - { - if (eventData is AnonymousEventData anonymousEventData) - { - return AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, actualEventType); - } - - return eventData; - } - protected void ThrowOriginalExceptions(Type eventType, List exceptions) { if (exceptions.Count == 1) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs index 3281b95bd5..989191528c 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs @@ -60,8 +60,7 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency /// public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { - var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); - handlerFactories.Locking(factories => + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => { if (!handler.IsInFactories(factories)) { @@ -141,33 +140,21 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - if (!TryGetAnonymousHandlerFactories(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => factories.Remove(factory)); - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - if (!TryGetAnonymousHandlerFactories(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => - { - factories.RemoveAll( - factory => - factory is SingleInstanceHandlerFactory singleFactory && - singleFactory.HandlerInstance == handler - ); - }); - - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName) + .Locking(factories => + { + factories.RemoveAll( + factory => + factory is SingleInstanceHandlerFactory singleFactory && + singleFactory.HandlerInstance == handler + ); + }); } /// @@ -179,21 +166,28 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency /// public override void UnsubscribeAll(string eventName) { - if (!TryGetAnonymousHandlerFactories(eventName, out var handlerFactories)) - { - return; - } - - handlerFactories.Locking(factories => factories.Clear()); - CleanupAnonymousHandlerFactoriesIfEmpty(eventName, handlerFactories); + GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); } /// public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { - var anonymousEventData = CreateAnonymousEnvelope(eventName, eventData); - return TryPublishTypedByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete) - ?? PublishAnonymousByEventNameAsync(eventName, anonymousEventData, onUnitOfWorkComplete); + var eventType = EventTypes.GetOrDefault(eventName); + + var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + + if (eventType != null) + { + return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + } + + var isAnonymous = AnonymousEventHandlerFactories.ContainsKey(eventName); + if (!isAnonymous) + { + throw new AbpException($"Unknown event name: {eventName}"); + } + + return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); } protected override async Task PublishToEventBusAsync(Type eventType, object eventData) @@ -231,11 +225,10 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency { foreach (var factory in handlerFactory.Value) { - var handlerType = GetHandlerType(factory); handlerFactoryList.Add(new Tuple( factory, handlerFactory.Key, - ReflectionHelper.GetAttributesOfMemberOrDeclaringType(handlerType).FirstOrDefault()?.Order ?? 0)); + ReflectionHelper.GetAttributesOfMemberOrDeclaringType(factory.GetHandler().EventHandler.GetType()).FirstOrDefault()?.Order ?? 0)); } } @@ -243,11 +236,10 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency { foreach (var factory in handlerFactory.Value) { - var handlerType = GetHandlerType(factory); handlerFactoryList.Add(new Tuple( factory, typeof(AnonymousEventData), - ReflectionHelper.GetAttributesOfMemberOrDeclaringType(handlerType).FirstOrDefault()?.Order ?? 0)); + ReflectionHelper.GetAttributesOfMemberOrDeclaringType(factory.GetHandler().EventHandler.GetType()).FirstOrDefault()?.Order ?? 0)); } } @@ -268,7 +260,8 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency { foreach (var factory in handlerFactory.Value) { - var handlerType = GetHandlerType(factory); + using var handler = factory.GetHandler(); + var handlerType = handler.EventHandler.GetType(); handlerFactoryList.Add(new Tuple( factory, typeof(AnonymousEventData), @@ -297,88 +290,6 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency return AnonymousEventHandlerFactories.GetOrAdd(eventName, (name) => new List()); } - private bool TryGetAnonymousHandlerFactories(string eventName, out List handlerFactories) - { - return AnonymousEventHandlerFactories.TryGetValue(eventName, out handlerFactories!); - } - - private AnonymousEventData CreateAnonymousEnvelope(string eventName, object eventData) - { - return eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); - } - - private Task? TryPublishTypedByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) - { - var eventType = EventTypes.GetOrDefault(eventName); - if (eventType == null) - { - return null; - } - - var typedEventData = AnonymousEventDataConverter.ConvertToTypedObject(anonymousEventData, eventType); - return PublishAsync(eventType, typedEventData, onUnitOfWorkComplete); - } - - private Task PublishAnonymousByEventNameAsync(string eventName, AnonymousEventData anonymousEventData, bool onUnitOfWorkComplete) - { - if (!HasAnonymousHandlers(eventName)) - { - return Task.CompletedTask; - } - - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); - } - - private static Type GetHandlerType(IEventHandlerFactory factory) - { - if (factory is SingleInstanceHandlerFactory singleInstanceHandlerFactory) - { - return singleInstanceHandlerFactory.HandlerInstance.GetType(); - } - - if (factory is IocEventHandlerFactory iocEventHandlerFactory) - { - return iocEventHandlerFactory.HandlerType; - } - - if (factory is TransientEventHandlerFactory transientEventHandlerFactory) - { - return transientEventHandlerFactory.HandlerType; - } - - using var handler = factory.GetHandler(); - return handler.EventHandler.GetType(); - } - - private bool HasAnonymousHandlers(string eventName) - { - if (!AnonymousEventHandlerFactories.TryGetValue(eventName, out var handlerFactories)) - { - return false; - } - - var hasHandlers = false; - handlerFactories.Locking(factories => hasHandlers = factories.Count > 0); - - if (!hasHandlers) - { - AnonymousEventHandlerFactories.TryRemove(eventName, out _); - } - - return hasHandlers; - } - - private void CleanupAnonymousHandlerFactoriesIfEmpty(string eventName, List handlerFactories) - { - var isEmpty = false; - handlerFactories.Locking(factories => isEmpty = factories.Count == 0); - - if (isEmpty) - { - AnonymousEventHandlerFactories.TryRemove(eventName, out _); - } - } - private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) { if (handlerEventType == targetEventType) diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs index 673454c6be..5ceb4b1bb5 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using Shouldly; using Volo.Abp.Domain.Entities.Events.Distributed; @@ -22,32 +21,20 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase [Fact] public async Task Should_Call_Handler_AndDispose() { - var handleCount = 0; - var disposeCount = 0; - var factory = new CountingDistributedEventHandlerFactory( - () => handleCount++, - () => disposeCount++); - - using var subscription = DistributedEventBus.Subscribe(typeof(MySimpleEventData), factory); + using var subscription = DistributedEventBus.Subscribe(); await DistributedEventBus.PublishAsync(new MySimpleEventData(1)); await DistributedEventBus.PublishAsync(new MySimpleEventData(2)); await DistributedEventBus.PublishAsync(new MySimpleEventData(3)); - Assert.Equal(3, handleCount); - Assert.Equal(3, disposeCount); + Assert.Equal(3, MySimpleDistributedTransientEventHandler.HandleCount); + Assert.Equal(3, MySimpleDistributedTransientEventHandler.DisposeCount); } [Fact] public async Task Should_Handle_Typed_Handler_When_Published_With_EventName() { - var handleCount = 0; - var disposeCount = 0; - var factory = new CountingDistributedEventHandlerFactory( - () => handleCount++, - () => disposeCount++); - - using var subscription = DistributedEventBus.Subscribe(typeof(MySimpleEventData), factory); + using var subscription = DistributedEventBus.Subscribe(); var eventName = EventNameAttribute.GetNameOrDefault(); await DistributedEventBus.PublishAsync(eventName, new MySimpleEventData(1)); @@ -57,8 +44,8 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase }); await DistributedEventBus.PublishAsync(eventName, new { Value = 3 }); - Assert.Equal(3, handleCount); - Assert.Equal(3, disposeCount); + Assert.Equal(3, MySimpleDistributedTransientEventHandler.HandleCount); + Assert.Equal(3, MySimpleDistributedTransientEventHandler.DisposeCount); } [Fact] @@ -95,7 +82,7 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { handleCount++; - AnonymousEventDataConverter.ConvertToLooseObject(d).ShouldNotBeNull(); + d.ConvertToTypedObject().ShouldNotBeNull(); await Task.CompletedTask; }))); @@ -200,15 +187,16 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase } [Fact] - public async Task Should_Ignore_Unknown_Event_Name() + public async Task Should_Throw_For_Unknown_Event_Name() { - await DistributedEventBus.PublishAsync("NonExistentEvent", new { Value = 1 }); + await Assert.ThrowsAsync(() => + DistributedEventBus.PublishAsync("NonExistentEvent", new { Value = 1 })); } [Fact] public async Task Should_Convert_AnonymousEventData_To_Typed_Object() { - MySimpleEventData receivedData = null!; + MySimpleEventData? receivedData = null; using var subscription = DistributedEventBus.Subscribe(async (data) => { @@ -223,63 +211,6 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase receivedData.Value.ShouldBe(42); } - [Fact] - public async Task Should_Rehydrate_Anonymous_Event_From_Outbox_Using_Raw_Json() - { - var localDistributedEventBus = GetRequiredService(); - AnonymousEventData receivedData = null!; - var eventName = "MyEvent-" + Guid.NewGuid().ToString("N"); - - using var subscription = localDistributedEventBus.Subscribe(eventName, - new SingleInstanceHandlerFactory(new ActionEventHandler(async data => - { - receivedData = data; - await Task.CompletedTask; - }))); - - var outgoingEvent = new OutgoingEventInfo( - Guid.NewGuid(), - eventName, - Encoding.UTF8.GetBytes("{\"Value\":42}"), - DateTime.UtcNow); - - await localDistributedEventBus.PublishFromOutboxAsync(outgoingEvent, new OutboxConfig("Test") { DatabaseName = "Test" }); - - receivedData.ShouldNotBeNull(); - receivedData.EventName.ShouldBe(eventName); - AnonymousEventDataConverter.GetJsonData(receivedData).ShouldBe("{\"Value\":42}"); - AnonymousEventDataConverter.ConvertToTypedObject(receivedData).Value.ShouldBe(42); - } - - [Fact] - public async Task Should_Rehydrate_Anonymous_Event_From_Inbox_Using_Raw_Json() - { - var localDistributedEventBus = GetRequiredService(); - AnonymousEventData receivedData = null!; - var eventName = "MyEvent-" + Guid.NewGuid().ToString("N"); - - using var subscription = localDistributedEventBus.Subscribe(eventName, - new SingleInstanceHandlerFactory(new ActionEventHandler(async data => - { - receivedData = data; - await Task.CompletedTask; - }))); - - var incomingEvent = new IncomingEventInfo( - Guid.NewGuid(), - Guid.NewGuid().ToString("N"), - eventName, - Encoding.UTF8.GetBytes("\"hello\""), - DateTime.UtcNow); - - await localDistributedEventBus.ProcessFromInboxAsync(incomingEvent, new InboxConfig("Test") { DatabaseName = "Test" }); - - receivedData.ShouldNotBeNull(); - receivedData.EventName.ShouldBe(eventName); - AnonymousEventDataConverter.GetJsonData(receivedData).ShouldBe("\"hello\""); - AnonymousEventDataConverter.ConvertToTypedObject(receivedData).ShouldBe("hello"); - } - [Fact] public async Task Should_Change_TenantId_If_EventData_Is_MultiTenant() { @@ -378,57 +309,4 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase } } - private sealed class CountingDistributedEventHandlerFactory : IEventHandlerFactory - { - private readonly Action _handleAction; - private readonly Action _disposeAction; - - public CountingDistributedEventHandlerFactory(Action handleAction, Action disposeAction) - { - _handleAction = handleAction; - _disposeAction = disposeAction; - } - - public IEventHandlerDisposeWrapper GetHandler() - { - var wasHandled = false; - return new EventHandlerDisposeWrapper( - new CountingDistributedEventHandler( - _handleAction, - () => wasHandled = true), - () => - { - if (wasHandled) - { - _disposeAction(); - } - } - ); - } - - public bool IsInFactories(List handlerFactories) - { - return handlerFactories.Contains(this); - } - } - - private sealed class CountingDistributedEventHandler : IDistributedEventHandler - { - private readonly Action _handleAction; - private readonly Action _markHandled; - - public CountingDistributedEventHandler(Action handleAction, Action markHandled) - { - _handleAction = handleAction; - _markHandled = markHandled; - } - - public Task HandleEventAsync(MySimpleEventData eventData) - { - _markHandled(); - _handleAction(); - return Task.CompletedTask; - } - } - } diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs index dfcd74bed8..7d3e5748e9 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs @@ -48,7 +48,7 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase [Fact] public async Task Should_Convert_Dictionary_To_Typed_Handler() { - MySimpleEventData receivedData = null!; + MySimpleEventData? receivedData = null; using var subscription = LocalEventBus.Subscribe(async (data) => { @@ -118,28 +118,29 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase } [Fact] - public async Task Should_Ignore_Unknown_Event_Name() + public async Task Should_Throw_For_Unknown_Event_Name() { - await LocalEventBus.PublishAsync("NonExistentEvent", new { Value = 1 }); + await Assert.ThrowsAsync(() => + LocalEventBus.PublishAsync("NonExistentEvent", new { Value = 1 })); } [Fact] public async Task Should_ConvertToTypedObject_In_Anonymous_Handler() { - object receivedData = null!; + object? receivedData = null; var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); using var subscription = LocalEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { - receivedData = AnonymousEventDataConverter.ConvertToLooseObject(d); + receivedData = d.ConvertToTypedObject(); await Task.CompletedTask; }))); await LocalEventBus.PublishAsync(eventName, new { Name = "Hello", Count = 42 }); receivedData.ShouldNotBeNull(); - var dict = receivedData.ShouldBeOfType>(); + var dict = receivedData.ShouldBeOfType>(); dict["Name"].ShouldBe("Hello"); dict["Count"].ShouldBe(42L); } @@ -147,13 +148,13 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase [Fact] public async Task Should_ConvertToTypedObject_Generic_In_Anonymous_Handler() { - MySimpleEventData receivedData = null!; + MySimpleEventData? receivedData = null; var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); using var subscription = LocalEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { - receivedData = AnonymousEventDataConverter.ConvertToTypedObject(d); + receivedData = d.ConvertToTypedObject(); await Task.CompletedTask; }))); @@ -162,23 +163,4 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase receivedData.ShouldNotBeNull(); receivedData.Value.ShouldBe(99); } - - [Fact] - public void Should_Roundtrip_Raw_Json_Without_Object_Deserialization() - { - var eventData = AnonymousEventData.FromJson("TestEvent", "{\"Value\":42,\"Name\":\"hello\"}"); - - eventData.EventName.ShouldBe("TestEvent"); - AnonymousEventDataConverter.GetJsonData(eventData).ShouldBe("{\"Value\":42,\"Name\":\"hello\"}"); - AnonymousEventDataConverter.ConvertToTypedObject(eventData).Value.ShouldBe(42); - } - - [Fact] - public void Should_Preserve_String_Payload_As_Raw_Json() - { - var eventData = AnonymousEventData.FromJson("TestEvent", "\"hello\""); - - AnonymousEventDataConverter.GetJsonData(eventData).ShouldBe("\"hello\""); - AnonymousEventDataConverter.ConvertToTypedObject(eventData).ShouldBe("hello"); - } } From 38dad060cf049431cd7bf186b9d96b4532a7dfc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Tue, 10 Mar 2026 20:20:29 +0300 Subject: [PATCH 13/22] Update LocalDistributedEventBus.cs --- .../Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs index c78b30260e..6d2cb6405d 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs @@ -190,7 +190,7 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected async override Task PublishToEventBusAsync(Type eventType, object eventData) { - if (await AddToInboxAsync(Guid.NewGuid().ToString(), EventNameAttribute.GetNameOrDefault(eventType), eventType, eventData, null)) + if (await AddToInboxAsync(Guid.NewGuid().ToString(), GetEventName(eventType, eventData), eventType, eventData, null)) { return; } From fb6f4722ffc3701a47fe588e17bfbbb5907a526c Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 22 Mar 2026 15:51:02 +0800 Subject: [PATCH 14/22] refactor: Replace AnonymousEventData with DynamicEventData across the event bus implementation --- .../event-bus/distributed/index.md | 80 +++++++++++++++++++ .../infrastructure/event-bus/local/index.md | 51 ++++++++++++ .../AbpAspNetCoreMvcDaprEventBusModule.cs | 10 ++- .../Distributed/IDistributedEventBus.cs | 6 +- ...nymousEventData.cs => DynamicEventData.cs} | 46 +++-------- .../Volo/Abp/EventBus/IEventBus.cs | 2 +- .../Volo/Abp/EventBus/Local/ILocalEventBus.cs | 2 +- .../Azure/AzureDistributedEventBus.cs | 50 ++++++------ .../EventBus/Dapr/DaprDistributedEventBus.cs | 50 ++++++------ .../Kafka/KafkaDistributedEventBus.cs | 50 ++++++------ .../RabbitMq/RabbitMqDistributedEventBus.cs | 50 ++++++------ .../Rebus/RebusDistributedEventBus.cs | 58 +++++++------- .../Distributed/DistributedEventBusBase.cs | 16 ++-- .../Distributed/LocalDistributedEventBus.cs | 40 +++++----- .../Distributed/NullDistributedEventBus.cs | 2 +- .../Volo/Abp/EventBus/EventBusBase.cs | 18 ++--- .../EventHandlerFactoryUnregistrar.cs | 4 +- .../Volo/Abp/EventBus/Local/LocalEventBus.cs | 42 +++++----- .../Abp/EventBus/Local/NullLocalEventBus.cs | 2 +- .../LocalDistributedEventBus_Test.cs | 52 ++++++------ ..._Test.cs => LocalEventBus_Dynamic_Test.cs} | 30 +++---- .../DistEventScenarioProfile.cs | 24 +++--- .../DistEventScenarioRunner.cs | 44 +++++----- 23 files changed, 420 insertions(+), 309 deletions(-) rename framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/{AnonymousEventData.cs => DynamicEventData.cs} (63%) rename framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/{LocalEventBus_Anonymous_Test.cs => LocalEventBus_Dynamic_Test.cs} (84%) diff --git a/docs/en/framework/infrastructure/event-bus/distributed/index.md b/docs/en/framework/infrastructure/event-bus/distributed/index.md index e5f96bba10..8fbf91465e 100644 --- a/docs/en/framework/infrastructure/event-bus/distributed/index.md +++ b/docs/en/framework/infrastructure/event-bus/distributed/index.md @@ -721,6 +721,86 @@ Configure(options => }); ```` +## Dynamic (String-Based) Events + +In addition to the type-safe event system described above, ABP also supports **dynamic events** that are identified by a string name rather than a CLR type. This is useful for scenarios where event types are not known at compile time, such as integrating with external systems or building plugin architectures. + +### Publishing Dynamic Events + +Use the `PublishAsync` overload that accepts a string event name: + +````csharp +await distributedEventBus.PublishAsync( + "MyDynamicEvent", + new Dictionary + { + ["UserId"] = 42, + ["Name"] = "John" + } +); +```` + +If a typed event exists with the given name (via `EventNameAttribute` or convention), the data is automatically deserialized and routed to the typed handler. Otherwise, it is delivered as a `DynamicEventData` to dynamic handlers. + +You can also control `onUnitOfWorkComplete` and `useOutbox` parameters: + +````csharp +await distributedEventBus.PublishAsync( + "MyDynamicEvent", + new { UserId = 42, Name = "John" }, + onUnitOfWorkComplete: true, + useOutbox: true +); +```` + +### Subscribing to Dynamic Events + +Use the `Subscribe` overload that accepts a string event name: + +````csharp +var subscription = distributedEventBus.Subscribe( + "MyDynamicEvent", + new SingleInstanceHandlerFactory( + new ActionEventHandler(eventData => + { + // Access the event name + var name = eventData.EventName; + + // Convert to a loosely-typed object (Dictionary/List/primitives) + var obj = eventData.ConvertToTypedObject(); + + // Or convert to a strongly-typed object + var typed = eventData.ConvertToTypedObject(); + + return Task.CompletedTask; + }))); + +// Unsubscribe when done +subscription.Dispose(); +```` + +You can also subscribe using a typed distributed event handler: + +````csharp +distributedEventBus.Subscribe("MyDynamicEvent", myDistributedEventHandler); +```` + +Where `myDistributedEventHandler` implements `IDistributedEventHandler`. + +### Mixed Typed and Dynamic Handlers + +When both a typed handler and a dynamic handler are registered for the same event name, **both** handlers are triggered. The typed handler receives the deserialized typed data, while the dynamic handler receives a `DynamicEventData` wrapper. + +### DynamicEventData Class + +The `DynamicEventData` class wraps the event payload with a string-based event name: + +- **`EventName`**: The string name that identifies the event. +- **`Data`**: The raw event data payload. +- **`ConvertToTypedObject()`**: Converts data to a loosely-typed object graph (dictionaries, lists, primitives). +- **`ConvertToTypedObject()`**: Deserializes data to a strongly-typed `T` object. +- **`ConvertToTypedObject(Type type)`**: Deserializes data to the specified type. + ## See Also * [Local Event Bus](../local) diff --git a/docs/en/framework/infrastructure/event-bus/local/index.md b/docs/en/framework/infrastructure/event-bus/local/index.md index b20718c51f..8545797049 100644 --- a/docs/en/framework/infrastructure/event-bus/local/index.md +++ b/docs/en/framework/infrastructure/event-bus/local/index.md @@ -249,6 +249,57 @@ If you set it to `false`, the `EntityUpdatedEventData` will not be published > This option is only used for the EF Core. +## Dynamic (String-Based) Events + +In addition to the type-safe event system described above, ABP also supports **dynamic events** that are identified by a string name rather than a CLR type. This is useful for scenarios where event types are not known at compile time. + +### Publishing Dynamic Events + +Use the `PublishAsync` overload that accepts a string event name: + +````csharp +await localEventBus.PublishAsync( + "MyDynamicEvent", + new Dictionary + { + ["UserId"] = 42, + ["Name"] = "John" + } +); +```` + +If a typed event exists with the given name (via `EventNameAttribute` or convention), the data is automatically converted and routed to the typed handler. Otherwise, it is delivered as a `DynamicEventData` to dynamic handlers. + +### Subscribing to Dynamic Events + +Use the `Subscribe` overload that accepts a string event name: + +````csharp +var subscription = localEventBus.Subscribe( + "MyDynamicEvent", + new SingleInstanceHandlerFactory( + new ActionEventHandler(eventData => + { + // Access the event name + var name = eventData.EventName; + + // Convert to a loosely-typed object (Dictionary/List/primitives) + var obj = eventData.ConvertToTypedObject(); + + // Or convert to a strongly-typed object + var typed = eventData.ConvertToTypedObject(); + + return Task.CompletedTask; + }))); + +// Unsubscribe when done +subscription.Dispose(); +```` + +### Mixed Typed and Dynamic Handlers + +When both a typed handler and a dynamic handler are registered for the same event name, **both** handlers are triggered. The typed handler receives the converted typed data, while the dynamic handler receives a `DynamicEventData` wrapper. + ## See Also * [Distributed Event Bus](../distributed) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs index f770f5fbb2..19ca7dc71f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs @@ -102,10 +102,11 @@ public class AbpAspNetCoreMvcDaprEventBusModule : AbpModule { var eventData = daprSerializer.Deserialize(daprEventData.JsonData, eventType); await distributedEventBus.TriggerHandlersAsync(eventType, eventData, daprEventData.MessageId, daprEventData.CorrelationId); - }else if (distributedEventBus.IsAnonymousEvent(daprEventData.Topic)) + } + else if (distributedEventBus.IsDynamicEvent(daprEventData.Topic)) { var eventData = daprSerializer.Deserialize(daprEventData.JsonData, typeof(object)); - await distributedEventBus.TriggerHandlersAsync(typeof(AnonymousEventData), new AnonymousEventData(daprEventData.Topic, eventData), daprEventData.MessageId, daprEventData.CorrelationId); + await distributedEventBus.TriggerHandlersAsync(typeof(DynamicEventData), new DynamicEventData(daprEventData.Topic, eventData), daprEventData.MessageId, daprEventData.CorrelationId); } } else @@ -115,10 +116,11 @@ public class AbpAspNetCoreMvcDaprEventBusModule : AbpModule { var eventData = daprSerializer.Deserialize(data, eventType); await distributedEventBus.TriggerHandlersAsync(eventType, eventData); - }else if (distributedEventBus.IsAnonymousEvent(topic)) + } + else if (distributedEventBus.IsDynamicEvent(topic)) { var eventData = daprSerializer.Deserialize(data, typeof(object)); - await distributedEventBus.TriggerHandlersAsync(typeof(AnonymousEventData), new AnonymousEventData(topic, eventData)); + await distributedEventBus.TriggerHandlersAsync(typeof(DynamicEventData), new DynamicEventData(topic, eventData)); } } diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IDistributedEventBus.cs index 99dfe243b4..9fdab66aee 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IDistributedEventBus.cs @@ -45,15 +45,15 @@ public interface IDistributedEventBus : IEventBus /// /// Registers to an event by its string-based event name. /// Same (given) instance of the handler is used for all event occurrences. - /// Wraps the handler as . + /// Wraps the handler as . /// /// Name of the event /// Object to handle the event - IDisposable Subscribe(string eventName, IDistributedEventHandler handler); + IDisposable Subscribe(string eventName, IDistributedEventHandler handler); /// /// Triggers an event by its string-based event name. - /// Used for anonymous (type-less) event publishing over distributed event bus. + /// Used for dynamic (type-less) event publishing over distributed event bus. /// /// Name of the event /// Related data for the event diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/DynamicEventData.cs similarity index 63% rename from framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs rename to framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/DynamicEventData.cs index 1db318b40e..536102ddfc 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/AnonymousEventData.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/DynamicEventData.cs @@ -6,53 +6,35 @@ using System.Text.Json; namespace Volo.Abp.EventBus; /// -/// Wraps arbitrary event data with a string-based event name for anonymous (type-less) event handling. -/// Acts as both an envelope and event type for events that are identified by name rather than CLR type. +/// Wraps arbitrary event data with a string-based event name for dynamic (type-less) event handling. /// -[Serializable] -public class AnonymousEventData +public class DynamicEventData { - /// - /// The string-based name that identifies the event. - /// public string EventName { get; } - /// - /// The raw event data payload. Can be a CLR object, , or any serializable object. - /// public object Data { get; } private JsonElement? _cachedJsonElement; - /// - /// Creates a new instance of . - /// - /// The string-based name that identifies the event - /// The raw event data payload - public AnonymousEventData(string eventName, object data) + public DynamicEventData(string eventName, object data) { - EventName = eventName; - Data = data; + EventName = Check.NotNullOrWhiteSpace(eventName, nameof(eventName)); + Data = Check.NotNull(data, nameof(data)); } /// - /// Converts the to a loosely-typed object graph + /// Converts to a loosely-typed object graph /// (dictionaries for objects, lists for arrays, primitives for values). /// - /// A CLR object representation of the event data public object ConvertToTypedObject() { return ConvertElement(GetJsonElement()); } /// - /// Converts the to a strongly-typed object. - /// Returns the data directly if it is already of type , - /// otherwise deserializes from JSON. + /// Converts to a strongly-typed object. + /// Returns the data directly if it is already of type . /// - /// Target type to convert to - /// The deserialized object of type - /// Thrown when deserialization fails public T ConvertToTypedObject() { if (Data is T typedData) @@ -61,17 +43,13 @@ public class AnonymousEventData } return GetJsonElement().Deserialize() - ?? throw new InvalidOperationException($"Failed to deserialize AnonymousEventData to {typeof(T).FullName}."); + ?? throw new InvalidOperationException($"Failed to deserialize DynamicEventData to {typeof(T).FullName}."); } /// - /// Converts the to the specified . - /// Returns the data directly if it is already an instance of the target type, - /// otherwise deserializes from JSON. + /// Converts to the specified . + /// Returns the data directly if it is already an instance of the target type. /// - /// Target type to convert to - /// The deserialized object - /// Thrown when deserialization fails public object ConvertToTypedObject(Type type) { if (type.IsInstanceOfType(Data)) @@ -80,7 +58,7 @@ public class AnonymousEventData } return GetJsonElement().Deserialize(type) - ?? throw new InvalidOperationException($"Failed to deserialize AnonymousEventData to {type.FullName}."); + ?? throw new InvalidOperationException($"Failed to deserialize DynamicEventData to {type.FullName}."); } private JsonElement GetJsonElement() diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs index 9c38b5e7dd..28b6c9567e 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs @@ -26,7 +26,7 @@ public interface IEventBus /// /// Triggers an event by its string-based event name. - /// Used for anonymous (type-less) event publishing. + /// Used for dynamic (type-less) event publishing. /// /// Name of the event /// Related data for the event diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventBus.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventBus.cs index dafa8d4a4d..733b2dafca 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Local/ILocalEventBus.cs @@ -29,5 +29,5 @@ public interface ILocalEventBus : IEventBus /// /// Name of the event /// List of event handler factories registered for the given event name - List GetAnonymousEventHandlerFactories(string eventName); + List GetDynamicEventHandlerFactories(string eventName); } diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index 020757ad07..715cf6c003 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -30,7 +30,7 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected IAzureServiceBusSerializer Serializer { get; } protected ConcurrentDictionary> HandlerFactories { get; } protected ConcurrentDictionary EventTypes { get; } - protected ConcurrentDictionary> AnonymousHandlerFactories { get; } + protected ConcurrentDictionary> DynamicHandlerFactories { get; } protected IAzureServiceBusMessageConsumer Consumer { get; private set; } = default!; public AzureDistributedEventBus( @@ -63,7 +63,7 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen PublisherPool = publisherPool; HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); - AnonymousHandlerFactories = new ConcurrentDictionary>(); + DynamicHandlerFactories = new ConcurrentDictionary>(); } public void Initialize() @@ -92,11 +92,11 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen { eventData = Serializer.Deserialize(message.Body.ToArray(), eventType); } - else if (AnonymousHandlerFactories.ContainsKey(eventName)) + else if (DynamicHandlerFactories.ContainsKey(eventName)) { var data = Serializer.Deserialize(message.Body.ToArray()); - eventData = new AnonymousEventData(eventName, data); - eventType = typeof(AnonymousEventData); + eventData = new DynamicEventData(eventName, data); + eventType = typeof(DynamicEventData); } else { @@ -131,7 +131,7 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { - var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); + var handlerFactories = GetOrCreateDynamicHandlerFactories(eventName); if (handler.IsInFactories(handlerFactories)) { @@ -140,7 +140,7 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen handlerFactories.Add(handler); - return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + return new DynamicEventHandlerFactoryUnregistrar(this, eventName, handler); } public override void Unsubscribe(Func action) @@ -200,16 +200,16 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); - var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); if (eventType != null) { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + return PublishAsync(eventType, dynamicEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); } - if (AnonymousHandlerFactories.ContainsKey(eventName)) + if (DynamicHandlerFactories.ContainsKey(eventName)) { - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); } throw new AbpException($"Unknown event name: {eventName}"); @@ -294,11 +294,11 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen { eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); } - else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + else if (DynamicHandlerFactories.ContainsKey(incomingEvent.EventName)) { var element = Serializer.Deserialize(incomingEvent.EventData); - eventData = new AnonymousEventData(incomingEvent.EventName, element); - eventType = typeof(AnonymousEventData); + eventData = new DynamicEventData(incomingEvent.EventName, element); + eventType = typeof(DynamicEventData); } else { @@ -354,7 +354,7 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { - if (typeof(AnonymousEventData) != eventType) + if (typeof(DynamicEventData) != eventType) { EventTypes.GetOrAdd(eventName, eventType); } @@ -384,9 +384,9 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); } - foreach (var handlerFactory in AnonymousHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) + foreach (var handlerFactory in DynamicHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) { - handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); } return handlerFactoryList.ToArray(); @@ -400,14 +400,14 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - GetOrCreateAnonymousHandlerFactories(eventName) + GetOrCreateDynamicHandlerFactories(eventName) .Locking(factories => factories.Remove(factory)); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - GetOrCreateAnonymousHandlerFactories(eventName) + GetOrCreateDynamicHandlerFactories(eventName) .Locking(factories => { factories.RemoveAll( @@ -421,11 +421,11 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override void UnsubscribeAll(string eventName) { - GetOrCreateAnonymousHandlerFactories(eventName) + GetOrCreateDynamicHandlerFactories(eventName) .Locking(factories => factories.Clear()); } - protected override IEnumerable GetAnonymousHandlerFactories(string eventName) + protected override IEnumerable GetDynamicHandlerFactories(string eventName) { var result = new List(); @@ -435,17 +435,17 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen result.AddRange(GetHandlerFactories(eventType)); } - foreach (var handlerFactory in AnonymousHandlerFactories.Where(hf => hf.Key == eventName)) + foreach (var handlerFactory in DynamicHandlerFactories.Where(hf => hf.Key == eventName)) { - result.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + result.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); } return result; } - private List GetOrCreateAnonymousHandlerFactories(string eventName) + private List GetOrCreateDynamicHandlerFactories(string eventName) { - return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); + return DynamicHandlerFactories.GetOrAdd(eventName, _ => new List()); } private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) diff --git a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs index 1231daf632..e2d4ca5648 100644 --- a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs @@ -28,7 +28,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend protected ConcurrentDictionary> HandlerFactories { get; } protected ConcurrentDictionary EventTypes { get; } - protected ConcurrentDictionary> AnonymousHandlerFactories { get; } + protected ConcurrentDictionary> DynamicHandlerFactories { get; } public DaprDistributedEventBus( IServiceScopeFactory serviceScopeFactory, @@ -59,7 +59,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); - AnonymousHandlerFactories = new ConcurrentDictionary>(); + DynamicHandlerFactories = new ConcurrentDictionary>(); } public void Initialize() @@ -84,7 +84,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend /// public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { - var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); + var handlerFactories = GetOrCreateDynamicHandlerFactories(eventName); if (handler.IsInFactories(handlerFactories)) { @@ -93,7 +93,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend handlerFactories.Add(handler); - return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + return new DynamicEventHandlerFactoryUnregistrar(this, eventName, handler); } public override void Unsubscribe(Func action) @@ -150,16 +150,16 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); - var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); if (eventType != null) { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + return PublishAsync(eventType, dynamicEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); } - if (AnonymousHandlerFactories.ContainsKey(eventName)) + if (DynamicHandlerFactories.ContainsKey(eventName)) { - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); } throw new AbpException($"Unknown event name: {eventName}"); @@ -185,7 +185,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend { eventData = Serializer.Deserialize(outgoingEvent.EventData, eventType); } - else if (AnonymousHandlerFactories.ContainsKey(outgoingEvent.EventName)) + else if (DynamicHandlerFactories.ContainsKey(outgoingEvent.EventName)) { eventData = Serializer.Deserialize(outgoingEvent.EventData, typeof(object)); } @@ -237,10 +237,10 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend { eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); } - else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + else if (DynamicHandlerFactories.ContainsKey(incomingEvent.EventName)) { - eventData = new AnonymousEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData, typeof(object))); - eventType = typeof(AnonymousEventData); + eventData = new DynamicEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData, typeof(object))); + eventType = typeof(DynamicEventData); } else { @@ -277,7 +277,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { - if (typeof(AnonymousEventData) != eventType) + if (typeof(DynamicEventData) != eventType) { EventTypes.GetOrAdd(eventName, eventType); } @@ -307,9 +307,9 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); } - foreach (var handlerFactory in AnonymousHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) + foreach (var handlerFactory in DynamicHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) { - handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); } return handlerFactoryList.ToArray(); @@ -325,21 +325,21 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend return EventTypes.GetOrDefault(eventName); } - public bool IsAnonymousEvent(string eventName) + public bool IsDynamicEvent(string eventName) { - return AnonymousHandlerFactories.ContainsKey(eventName); + return DynamicHandlerFactories.ContainsKey(eventName); } /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - GetOrCreateAnonymousHandlerFactories(eventName) + GetOrCreateDynamicHandlerFactories(eventName) .Locking(factories => { factories.RemoveAll( @@ -353,10 +353,10 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend /// public override void UnsubscribeAll(string eventName) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Clear()); } - protected override IEnumerable GetAnonymousHandlerFactories(string eventName) + protected override IEnumerable GetDynamicHandlerFactories(string eventName) { var result = new List(); @@ -366,17 +366,17 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend result.AddRange(GetHandlerFactories(eventType)); } - foreach (var handlerFactory in AnonymousHandlerFactories.Where(hf => hf.Key == eventName)) + foreach (var handlerFactory in DynamicHandlerFactories.Where(hf => hf.Key == eventName)) { - result.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + result.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); } return result; } - private List GetOrCreateAnonymousHandlerFactories(string eventName) + private List GetOrCreateDynamicHandlerFactories(string eventName) { - return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); + return DynamicHandlerFactories.GetOrAdd(eventName, _ => new List()); } private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs index e7e44e449d..50b59fc28b 100644 --- a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs @@ -30,7 +30,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected IProducerPool ProducerPool { get; } protected ConcurrentDictionary> HandlerFactories { get; } protected ConcurrentDictionary EventTypes { get; } - protected ConcurrentDictionary> AnonymousHandlerFactories { get; } + protected ConcurrentDictionary> DynamicHandlerFactories { get; } protected IKafkaMessageConsumer Consumer { get; private set; } = default!; public KafkaDistributedEventBus( @@ -65,7 +65,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); - AnonymousHandlerFactories = new ConcurrentDictionary>(); + DynamicHandlerFactories = new ConcurrentDictionary>(); } public void Initialize() @@ -92,11 +92,11 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen { eventData = Serializer.Deserialize(message.Value, eventType); } - else if (AnonymousHandlerFactories.ContainsKey(eventName)) + else if (DynamicHandlerFactories.ContainsKey(eventName)) { var element = Serializer.Deserialize(message.Value); - eventData = new AnonymousEventData(eventName, element); - eventType = typeof(AnonymousEventData); + eventData = new DynamicEventData(eventName, element); + eventType = typeof(DynamicEventData); } else { @@ -131,7 +131,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { - var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); + var handlerFactories = GetOrCreateDynamicHandlerFactories(eventName); if (handler.IsInFactories(handlerFactories)) { @@ -140,7 +140,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen handlerFactories.Add(handler); - return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + return new DynamicEventHandlerFactoryUnregistrar(this, eventName, handler); } /// @@ -201,16 +201,16 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); - var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); if (eventType != null) { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + return PublishAsync(eventType, dynamicEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); } - if (AnonymousHandlerFactories.ContainsKey(eventName)) + if (DynamicHandlerFactories.ContainsKey(eventName)) { - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); } throw new AbpException($"Unknown event name: {eventName}"); @@ -332,11 +332,11 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen { eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); } - else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + else if (DynamicHandlerFactories.ContainsKey(incomingEvent.EventName)) { var element = Serializer.Deserialize(incomingEvent.EventData); - eventData = new AnonymousEventData(incomingEvent.EventName, element); - eventType = typeof(AnonymousEventData); + eventData = new DynamicEventData(incomingEvent.EventName, element); + eventType = typeof(DynamicEventData); } else { @@ -390,7 +390,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { - if (typeof(AnonymousEventData) != eventType) + if (typeof(DynamicEventData) != eventType) { EventTypes.GetOrAdd(eventName, eventType); } @@ -420,9 +420,9 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); } - foreach (var handlerFactory in AnonymousHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) + foreach (var handlerFactory in DynamicHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) { - handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); } return handlerFactoryList.ToArray(); @@ -436,13 +436,13 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - GetOrCreateAnonymousHandlerFactories(eventName) + GetOrCreateDynamicHandlerFactories(eventName) .Locking(factories => { factories.RemoveAll( @@ -456,10 +456,10 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override void UnsubscribeAll(string eventName) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Clear()); } - protected override IEnumerable GetAnonymousHandlerFactories(string eventName) + protected override IEnumerable GetDynamicHandlerFactories(string eventName) { var result = new List(); @@ -469,17 +469,17 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen result.AddRange(GetHandlerFactories(eventType)); } - foreach (var handlerFactory in AnonymousHandlerFactories.Where(hf => hf.Key == eventName)) + foreach (var handlerFactory in DynamicHandlerFactories.Where(hf => hf.Key == eventName)) { - result.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + result.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); } return result; } - private List GetOrCreateAnonymousHandlerFactories(string eventName) + private List GetOrCreateDynamicHandlerFactories(string eventName) { - return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); + return DynamicHandlerFactories.GetOrAdd(eventName, _ => new List()); } private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs index 771dae9f09..98710f7487 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs @@ -33,7 +33,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis //TODO: Accessing to the List may not be thread-safe! protected ConcurrentDictionary> HandlerFactories { get; } protected ConcurrentDictionary EventTypes { get; } - protected ConcurrentDictionary> AnonymousHandlerFactories { get; } + protected ConcurrentDictionary> DynamicHandlerFactories { get; } protected IRabbitMqMessageConsumerFactory MessageConsumerFactory { get; } protected IRabbitMqMessageConsumer Consumer { get; private set; } = default!; @@ -71,7 +71,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); - AnonymousHandlerFactories = new ConcurrentDictionary>(); + DynamicHandlerFactories = new ConcurrentDictionary>(); } public virtual void Initialize() @@ -109,10 +109,10 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis { eventData = Serializer.Deserialize(ea.Body.ToArray(), eventType); } - else if (AnonymousHandlerFactories.ContainsKey(eventName)) + else if (DynamicHandlerFactories.ContainsKey(eventName)) { - eventType = typeof(AnonymousEventData); - eventData = new AnonymousEventData(eventName, Serializer.Deserialize(ea.Body.ToArray())); + eventType = typeof(DynamicEventData); + eventData = new DynamicEventData(eventName, Serializer.Deserialize(ea.Body.ToArray())); } else { @@ -153,7 +153,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis /// public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { - var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); + var handlerFactories = GetOrCreateDynamicHandlerFactories(eventName); if (handler.IsInFactories(handlerFactories)) { @@ -167,7 +167,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis Consumer.BindAsync(eventName); } - return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + return new DynamicEventHandlerFactoryUnregistrar(this, eventName, handler); } /// @@ -228,16 +228,16 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); - var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); if (eventType != null) { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + return PublishAsync(eventType, dynamicEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); } - if (AnonymousHandlerFactories.ContainsKey(eventName)) + if (DynamicHandlerFactories.ContainsKey(eventName)) { - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); } throw new AbpException($"Unknown event name: {eventName}"); @@ -312,10 +312,10 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis { eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); } - else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + else if (DynamicHandlerFactories.ContainsKey(incomingEvent.EventName)) { - eventData = new AnonymousEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData)); - eventType = typeof(AnonymousEventData); + eventData = new DynamicEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData)); + eventType = typeof(DynamicEventData); } else { @@ -441,7 +441,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { - if (typeof(AnonymousEventData) != eventType) + if (typeof(DynamicEventData) != eventType) { EventTypes.GetOrAdd(eventName, eventType); } @@ -471,9 +471,9 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); } - foreach (var handlerFactory in AnonymousHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) + foreach (var handlerFactory in DynamicHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) { - handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); } return handlerFactoryList.ToArray(); @@ -487,13 +487,13 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - GetOrCreateAnonymousHandlerFactories(eventName) + GetOrCreateDynamicHandlerFactories(eventName) .Locking(factories => { factories.RemoveAll( @@ -507,10 +507,10 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis /// public override void UnsubscribeAll(string eventName) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Clear()); } - protected override IEnumerable GetAnonymousHandlerFactories(string eventName) + protected override IEnumerable GetDynamicHandlerFactories(string eventName) { var result = new List(); @@ -520,17 +520,17 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis return GetHandlerFactories(eventType); } - foreach (var handlerFactory in AnonymousHandlerFactories.Where(hf => hf.Key == eventName)) + foreach (var handlerFactory in DynamicHandlerFactories.Where(hf => hf.Key == eventName)) { - result.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + result.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); } return result; } - private List GetOrCreateAnonymousHandlerFactories(string eventName) + private List GetOrCreateDynamicHandlerFactories(string eventName) { - return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); + return DynamicHandlerFactories.GetOrAdd(eventName, _ => new List()); } private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs index c4f1a141e6..d382585c6d 100644 --- a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs @@ -31,7 +31,7 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen //TODO: Accessing to the List may not be thread-safe! protected ConcurrentDictionary> HandlerFactories { get; } protected ConcurrentDictionary EventTypes { get; } - protected ConcurrentDictionary> AnonymousHandlerFactories { get; } + protected ConcurrentDictionary> DynamicHandlerFactories { get; } protected AbpRebusEventBusOptions AbpRebusEventBusOptions { get; } public RebusDistributedEventBus( @@ -64,7 +64,7 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); - AnonymousHandlerFactories = new ConcurrentDictionary>(); + DynamicHandlerFactories = new ConcurrentDictionary>(); } public void Initialize() @@ -76,9 +76,9 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen { var messageId = MessageContext.Current.TransportMessage.GetMessageId(); string eventName; - if (eventType == typeof(AnonymousEventData) && eventData is AnonymousEventData anonymousEventData) + if (eventType == typeof(DynamicEventData) && eventData is DynamicEventData dynamicEventData) { - eventName = anonymousEventData.EventName; + eventName = dynamicEventData.EventName; } else { @@ -119,7 +119,7 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { - var handlerFactories = GetOrCreateAnonymousHandlerFactories(eventName); + var handlerFactories = GetOrCreateDynamicHandlerFactories(eventName); if (handler.IsInFactories(handlerFactories)) { @@ -128,12 +128,12 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen handlerFactories.Add(handler); - if (AnonymousHandlerFactories.Count == 1) //TODO: Multi-threading! + if (DynamicHandlerFactories.Count == 1) //TODO: Multi-threading! { - Rebus.Subscribe(typeof(AnonymousEventData)); + Rebus.Subscribe(typeof(DynamicEventData)); } - return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + return new DynamicEventHandlerFactoryUnregistrar(this, eventName, handler); } public override void Unsubscribe(Func action) @@ -194,16 +194,16 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); - var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); if (eventType != null) { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + return PublishAsync(eventType, dynamicEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); } - if (AnonymousHandlerFactories.ContainsKey(eventName)) + if (DynamicHandlerFactories.ContainsKey(eventName)) { - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); } throw new AbpException($"Unknown event name: {eventName}"); @@ -235,10 +235,10 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen { eventData = Serializer.Deserialize(outgoingEvent.EventData, eventType); } - else if (AnonymousHandlerFactories.ContainsKey(outgoingEvent.EventName)) + else if (DynamicHandlerFactories.ContainsKey(outgoingEvent.EventName)) { - eventData = new AnonymousEventData(outgoingEvent.EventName, Serializer.Deserialize(outgoingEvent.EventData, typeof(object))); - eventType = typeof(AnonymousEventData); + eventData = new DynamicEventData(outgoingEvent.EventName, Serializer.Deserialize(outgoingEvent.EventData, typeof(object))); + eventType = typeof(DynamicEventData); } else { @@ -299,10 +299,10 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen { eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); } - else if (AnonymousHandlerFactories.ContainsKey(incomingEvent.EventName)) + else if (DynamicHandlerFactories.ContainsKey(incomingEvent.EventName)) { - eventData = new AnonymousEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData, typeof(object))); - eventType = typeof(AnonymousEventData); + eventData = new DynamicEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData, typeof(object))); + eventType = typeof(DynamicEventData); } else { @@ -347,7 +347,7 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { - if (typeof(AnonymousEventData) != eventType) + if (typeof(DynamicEventData) != eventType) { EventTypes.GetOrAdd(eventName, eventType); } @@ -377,9 +377,9 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); } - foreach (var handlerFactory in AnonymousHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) + foreach (var handlerFactory in DynamicHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) { - handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); } return handlerFactoryList.ToArray(); @@ -393,13 +393,13 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - GetOrCreateAnonymousHandlerFactories(eventName) + GetOrCreateDynamicHandlerFactories(eventName) .Locking(factories => { factories.RemoveAll( @@ -413,10 +413,10 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override void UnsubscribeAll(string eventName) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Clear()); } - protected override IEnumerable GetAnonymousHandlerFactories(string eventName) + protected override IEnumerable GetDynamicHandlerFactories(string eventName) { var result = new List(); @@ -426,17 +426,17 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen result.AddRange(GetHandlerFactories(eventType)); } - foreach (var handlerFactory in AnonymousHandlerFactories.Where(hf => hf.Key == eventName)) + foreach (var handlerFactory in DynamicHandlerFactories.Where(hf => hf.Key == eventName)) { - result.Add(new EventTypeWithEventHandlerFactories(typeof(AnonymousEventData), handlerFactory.Value)); + result.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); } return result; } - private List GetOrCreateAnonymousHandlerFactories(string eventName) + private List GetOrCreateDynamicHandlerFactories(string eventName) { - return AnonymousHandlerFactories.GetOrAdd(eventName, _ => new List()); + return DynamicHandlerFactories.GetOrAdd(eventName, _ => new List()); } private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs index 88ce5662f8..2bec5815e9 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs @@ -50,7 +50,7 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB } /// - public virtual IDisposable Subscribe(string eventName, IDistributedEventHandler handler) + public virtual IDisposable Subscribe(string eventName, IDistributedEventHandler handler) { return Subscribe(eventName, (IEventHandler)handler); } @@ -113,14 +113,14 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB bool useOutbox = true) { var eventType = GetEventTypeByEventName(eventName); - var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); if (eventType != null) { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete, useOutbox); + return PublishAsync(eventType, dynamicEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete, useOutbox); } - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete, useOutbox); + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete, useOutbox); } public abstract Task PublishFromOutboxAsync( @@ -285,9 +285,9 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB protected virtual string GetEventName(Type eventType, object eventData) { - if (eventData is AnonymousEventData anonymousEventData) + if (eventData is DynamicEventData dynamicEventData) { - return anonymousEventData.EventName; + return dynamicEventData.EventName; } return EventNameAttribute.GetNameOrDefault(eventType); @@ -295,9 +295,9 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB protected virtual object GetEventData(object eventData) { - if (eventData is AnonymousEventData anonymousEventData) + if (eventData is DynamicEventData dynamicEventData) { - return anonymousEventData.Data; + return dynamicEventData.Data; } return eventData; diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs index 6d2cb6405d..f92d453a82 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs @@ -24,7 +24,7 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen { protected ConcurrentDictionary EventTypes { get; } - protected ConcurrentDictionary AnonymousEventNames { get; } + protected ConcurrentDictionary DynamicEventNames { get; } public LocalDistributedEventBus( IServiceScopeFactory serviceScopeFactory, @@ -47,7 +47,7 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen correlationIdProvider) { EventTypes = new ConcurrentDictionary(); - AnonymousEventNames = new ConcurrentDictionary(); + DynamicEventNames = new ConcurrentDictionary(); Subscribe(abpDistributedEventBusOptions.Value.Handlers); } @@ -75,7 +75,7 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen /// public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { - AnonymousEventNames.GetOrAdd(eventName, true); + DynamicEventNames.GetOrAdd(eventName, true); return LocalEventBus.Subscribe(eventName, handler); } @@ -173,19 +173,19 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) { var eventType = EventTypes.GetOrDefault(eventName); - var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); if (eventType != null) { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete, useOutbox); + return PublishAsync(eventType, dynamicEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete, useOutbox); } - if (!AnonymousEventNames.ContainsKey(eventName)) + if (!DynamicEventNames.ContainsKey(eventName)) { throw new AbpException($"Unknown event name: {eventName}"); } - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete, useOutbox); + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete, useOutbox); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) @@ -222,19 +222,19 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen var eventType = EventTypes.GetOrDefault(outgoingEvent.EventName); if (eventType == null) { - var isAnonymous = AnonymousEventNames.ContainsKey(outgoingEvent.EventName); - if (!isAnonymous) + var isDynamic = DynamicEventNames.ContainsKey(outgoingEvent.EventName); + if (!isDynamic) { return; } - eventType = typeof(AnonymousEventData); + eventType = typeof(DynamicEventData); } object eventData; - if (eventType == typeof(AnonymousEventData)) + if (eventType == typeof(DynamicEventData)) { - eventData = new AnonymousEventData( + eventData = new DynamicEventData( outgoingEvent.EventName, JsonSerializer.Deserialize(outgoingEvent.EventData)!); } @@ -264,19 +264,19 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); if (eventType == null) { - var isAnonymous = AnonymousEventNames.ContainsKey(incomingEvent.EventName); - if (!isAnonymous) + var isDynamic = DynamicEventNames.ContainsKey(incomingEvent.EventName); + if (!isDynamic) { return; } - eventType = typeof(AnonymousEventData); + eventType = typeof(DynamicEventData); } object eventData; - if (eventType == typeof(AnonymousEventData)) + if (eventType == typeof(DynamicEventData)) { - eventData = new AnonymousEventData( + eventData = new DynamicEventData( incomingEvent.EventName, JsonSerializer.Deserialize(incomingEvent.EventData)!); } @@ -303,7 +303,7 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { - if (eventType != typeof(AnonymousEventData)) + if (eventType != typeof(DynamicEventData)) { EventTypes.GetOrAdd(eventName, eventType); } @@ -315,9 +315,9 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen return LocalEventBus.GetEventHandlerFactories(eventType); } - protected override IEnumerable GetAnonymousHandlerFactories(string eventName) + protected override IEnumerable GetDynamicHandlerFactories(string eventName) { - return LocalEventBus.GetAnonymousEventHandlerFactories(eventName); + return LocalEventBus.GetDynamicEventHandlerFactories(eventName); } protected override Type? GetEventTypeByEventName(string eventName) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs index 2640ddefab..73636f49a5 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs @@ -51,7 +51,7 @@ public sealed class NullDistributedEventBus : IDistributedEventBus } /// - public IDisposable Subscribe(string eventName, IDistributedEventHandler handler) + public IDisposable Subscribe(string eventName, IDistributedEventHandler handler) { return NullDisposable.Instance; } diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs index 8c344f5aa7..6a8e357373 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs @@ -174,7 +174,7 @@ public abstract class EventBusBase : IEventBus actualEventType.GetGenericArguments().Length == 1 && typeof(IEventDataWithInheritableGenericArgument).IsAssignableFrom(actualEventType)) { - var resolvedEventData = eventData is AnonymousEventData aed + var resolvedEventData = eventData is DynamicEventData aed ? aed.ConvertToTypedObject(actualEventType) : eventData; @@ -194,11 +194,11 @@ public abstract class EventBusBase : IEventBus Type eventType, object eventData) { - if (eventData is AnonymousEventData anonymousEventData) + if (eventData is DynamicEventData dynamicEventData) { return ( - GetAnonymousHandlerFactories(anonymousEventData.EventName).ToList(), - GetEventTypeByEventName(anonymousEventData.EventName) + GetDynamicHandlerFactories(dynamicEventData.EventName).ToList(), + GetEventTypeByEventName(dynamicEventData.EventName) ); } @@ -207,14 +207,14 @@ public abstract class EventBusBase : IEventBus protected virtual object ResolveEventDataForHandler(object eventData, Type sourceEventType, Type handlerEventType) { - if (eventData is AnonymousEventData anonymousEventData && handlerEventType != typeof(AnonymousEventData)) + if (eventData is DynamicEventData dynamicEventData && handlerEventType != typeof(DynamicEventData)) { - return anonymousEventData.ConvertToTypedObject(handlerEventType); + return dynamicEventData.ConvertToTypedObject(handlerEventType); } - if (handlerEventType == typeof(AnonymousEventData) && eventData is not AnonymousEventData) + if (handlerEventType == typeof(DynamicEventData) && eventData is not DynamicEventData) { - return new AnonymousEventData(EventNameAttribute.GetNameOrDefault(sourceEventType), eventData); + return new DynamicEventData(EventNameAttribute.GetNameOrDefault(sourceEventType), eventData); } return eventData; @@ -256,7 +256,7 @@ public abstract class EventBusBase : IEventBus protected abstract IEnumerable GetHandlerFactories(Type eventType); - protected abstract IEnumerable GetAnonymousHandlerFactories(string eventName); + protected abstract IEnumerable GetDynamicHandlerFactories(string eventName); protected abstract Type? GetEventTypeByEventName(string eventName); diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs index 19095d8d9b..cd5c7b74f6 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs @@ -27,13 +27,13 @@ public class EventHandlerFactoryUnregistrar : IDisposable /// /// Used to unregister an for a string-based event name on method. /// -public class AnonymousEventHandlerFactoryUnregistrar : IDisposable +public class DynamicEventHandlerFactoryUnregistrar : IDisposable { private readonly IEventBus _eventBus; private readonly string _eventName; private readonly IEventHandlerFactory _factory; - public AnonymousEventHandlerFactoryUnregistrar(IEventBus eventBus, string eventName, IEventHandlerFactory factory) + public DynamicEventHandlerFactoryUnregistrar(IEventBus eventBus, string eventName, IEventHandlerFactory factory) { _eventBus = eventBus; _eventName = eventName; diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs index 989191528c..d56a81fb39 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs @@ -32,7 +32,7 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency protected ConcurrentDictionary EventTypes { get; } - protected ConcurrentDictionary> AnonymousEventHandlerFactories { get; } + protected ConcurrentDictionary> DynamicEventHandlerFactories { get; } public LocalEventBus( IOptions options, @@ -47,7 +47,7 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); - AnonymousEventHandlerFactories = new ConcurrentDictionary>(); + DynamicEventHandlerFactories = new ConcurrentDictionary>(); SubscribeHandlers(Options.Handlers); } @@ -60,7 +60,7 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency /// public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => { if (!handler.IsInFactories(factories)) { @@ -68,7 +68,7 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency } }); - return new AnonymousEventHandlerFactoryUnregistrar(this, eventName, handler); + return new DynamicEventHandlerFactoryUnregistrar(this, eventName, handler); } /// @@ -140,13 +140,13 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - GetOrCreateAnonymousHandlerFactories(eventName) + GetOrCreateDynamicHandlerFactories(eventName) .Locking(factories => { factories.RemoveAll( @@ -166,7 +166,7 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency /// public override void UnsubscribeAll(string eventName) { - GetOrCreateAnonymousHandlerFactories(eventName).Locking(factories => factories.Clear()); + GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Clear()); } /// @@ -174,20 +174,20 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency { var eventType = EventTypes.GetOrDefault(eventName); - var anonymousEventData = eventData as AnonymousEventData ?? new AnonymousEventData(eventName, eventData); + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); if (eventType != null) { - return PublishAsync(eventType, anonymousEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + return PublishAsync(eventType, dynamicEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); } - var isAnonymous = AnonymousEventHandlerFactories.ContainsKey(eventName); - if (!isAnonymous) + var isDynamic = DynamicEventHandlerFactories.ContainsKey(eventName); + if (!isDynamic) { throw new AbpException($"Unknown event name: {eventName}"); } - return PublishAsync(typeof(AnonymousEventData), anonymousEventData, onUnitOfWorkComplete); + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); } protected override async Task PublishToEventBusAsync(Type eventType, object eventData) @@ -211,9 +211,9 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency } /// - public virtual List GetAnonymousEventHandlerFactories(string eventName) + public virtual List GetDynamicEventHandlerFactories(string eventName) { - return GetAnonymousHandlerFactories(eventName).ToList(); + return GetDynamicHandlerFactories(eventName).ToList(); } protected override IEnumerable GetHandlerFactories(Type eventType) @@ -232,13 +232,13 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency } } - foreach (var handlerFactory in AnonymousEventHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) + foreach (var handlerFactory in DynamicEventHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) { foreach (var factory in handlerFactory.Value) { handlerFactoryList.Add(new Tuple( factory, - typeof(AnonymousEventData), + typeof(DynamicEventData), ReflectionHelper.GetAttributesOfMemberOrDeclaringType(factory.GetHandler().EventHandler.GetType()).FirstOrDefault()?.Order ?? 0)); } } @@ -246,7 +246,7 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency return handlerFactoryList.OrderBy(x => x.Item3).Select(x => new EventTypeWithEventHandlerFactories(x.Item2, new List {x.Item1})).ToArray(); } - protected override IEnumerable GetAnonymousHandlerFactories(string eventName) + protected override IEnumerable GetDynamicHandlerFactories(string eventName) { var eventType = EventTypes.GetOrDefault(eventName); if (eventType != null) @@ -256,7 +256,7 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency var handlerFactoryList = new List>(); - foreach (var handlerFactory in AnonymousEventHandlerFactories.Where(aehf => aehf.Key == eventName)) + foreach (var handlerFactory in DynamicEventHandlerFactories.Where(aehf => aehf.Key == eventName)) { foreach (var factory in handlerFactory.Value) { @@ -264,7 +264,7 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency var handlerType = handler.EventHandler.GetType(); handlerFactoryList.Add(new Tuple( factory, - typeof(AnonymousEventData), + typeof(DynamicEventData), ReflectionHelper .GetAttributesOfMemberOrDeclaringType(handlerType) .FirstOrDefault()?.Order ?? 0)); @@ -285,9 +285,9 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency return HandlerFactories.GetOrAdd(eventType, (type) => new List()); } - private List GetOrCreateAnonymousHandlerFactories(string eventName) + private List GetOrCreateDynamicHandlerFactories(string eventName) { - return AnonymousEventHandlerFactories.GetOrAdd(eventName, (name) => new List()); + return DynamicEventHandlerFactories.GetOrAdd(eventName, (name) => new List()); } private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs index e3d28198d4..682b49d939 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs @@ -35,7 +35,7 @@ public sealed class NullLocalEventBus : ILocalEventBus } /// - public List GetAnonymousEventHandlerFactories(string eventName) + public List GetDynamicEventHandlerFactories(string eventName) { return new List(); } diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs index 5ceb4b1bb5..aa16a5542d 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs @@ -49,13 +49,13 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase } [Fact] - public async Task Should_Handle_Anonymous_Handler_When_Published_With_EventName() + public async Task Should_Handle_Dynamic_Handler_When_Published_With_EventName() { var handleCount = 0; var eventName = "MyEvent-" + Guid.NewGuid().ToString("N"); using var subscription = DistributedEventBus.Subscribe(eventName, - new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { handleCount++; await Task.CompletedTask; @@ -73,58 +73,58 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase } [Fact] - public async Task Should_Handle_Anonymous_Handler_When_Published_With_AnonymousEventData() + public async Task Should_Handle_Dynamic_Handler_When_Published_With_DynamicEventData() { var handleCount = 0; var eventName = "MyEvent-" + Guid.NewGuid().ToString("N"); using var subscription = DistributedEventBus.Subscribe(eventName, - new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { handleCount++; d.ConvertToTypedObject().ShouldNotBeNull(); await Task.CompletedTask; }))); - await DistributedEventBus.PublishAsync(new AnonymousEventData(eventName, new MySimpleEventData(1))); - await DistributedEventBus.PublishAsync(new AnonymousEventData(eventName, new Dictionary() + await DistributedEventBus.PublishAsync(new DynamicEventData(eventName, new MySimpleEventData(1))); + await DistributedEventBus.PublishAsync(new DynamicEventData(eventName, new Dictionary() { {"Value", 2} })); - await DistributedEventBus.PublishAsync(new AnonymousEventData(eventName, new { Value = 3 })); + await DistributedEventBus.PublishAsync(new DynamicEventData(eventName, new { Value = 3 })); Assert.Equal(3, handleCount); } [Fact] - public async Task Should_Handle_Typed_Handler_When_Published_With_AnonymousEventData() + public async Task Should_Handle_Typed_Handler_When_Published_With_DynamicEventData() { using var subscription = DistributedEventBus.Subscribe(); var eventName = EventNameAttribute.GetNameOrDefault(); - await DistributedEventBus.PublishAsync(new AnonymousEventData(eventName, new MySimpleEventData(1))); - await DistributedEventBus.PublishAsync(new AnonymousEventData(eventName, new Dictionary() + await DistributedEventBus.PublishAsync(new DynamicEventData(eventName, new MySimpleEventData(1))); + await DistributedEventBus.PublishAsync(new DynamicEventData(eventName, new Dictionary() { {"Value", 2} })); - await DistributedEventBus.PublishAsync(new AnonymousEventData(eventName, new { Value = 3 })); + await DistributedEventBus.PublishAsync(new DynamicEventData(eventName, new { Value = 3 })); Assert.Equal(3, MySimpleDistributedTransientEventHandler.HandleCount); } [Fact] - public async Task Should_Trigger_Both_Typed_And_Anonymous_Handlers_For_Typed_Event() + public async Task Should_Trigger_Both_Typed_And_Dynamic_Handlers_For_Typed_Event() { using var typedSubscription = DistributedEventBus.Subscribe(); var eventName = EventNameAttribute.GetNameOrDefault(); - var anonymousHandleCount = 0; + var dynamicHandleCount = 0; - using var anonymousSubscription = DistributedEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + using var dynamicSubscription = DistributedEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { - anonymousHandleCount++; + dynamicHandleCount++; await Task.CompletedTask; }))); @@ -133,42 +133,42 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase await DistributedEventBus.PublishAsync(new MySimpleEventData(3)); Assert.Equal(3, MySimpleDistributedTransientEventHandler.HandleCount); - Assert.Equal(3, anonymousHandleCount); + Assert.Equal(3, dynamicHandleCount); } [Fact] - public async Task Should_Trigger_Both_Handlers_For_Mixed_Typed_And_Anonymous_Publish() + public async Task Should_Trigger_Both_Handlers_For_Mixed_Typed_And_Dynamic_Publish() { using var typedSubscription = DistributedEventBus.Subscribe(); var eventName = EventNameAttribute.GetNameOrDefault(); - var anonymousHandleCount = 0; + var dynamicHandleCount = 0; - using var anonymousSubscription = DistributedEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + using var dynamicSubscription = DistributedEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { - anonymousHandleCount++; + dynamicHandleCount++; await Task.CompletedTask; }))); await DistributedEventBus.PublishAsync(new MySimpleEventData(1)); - await DistributedEventBus.PublishAsync(new AnonymousEventData(eventName, new Dictionary() + await DistributedEventBus.PublishAsync(new DynamicEventData(eventName, new Dictionary() { {"Value", 2} })); - await DistributedEventBus.PublishAsync(new AnonymousEventData(eventName, new { Value = 3 })); + await DistributedEventBus.PublishAsync(new DynamicEventData(eventName, new { Value = 3 })); Assert.Equal(3, MySimpleDistributedTransientEventHandler.HandleCount); - Assert.Equal(3, anonymousHandleCount); + Assert.Equal(3, dynamicHandleCount); } [Fact] - public async Task Should_Unsubscribe_Anonymous_Handler() + public async Task Should_Unsubscribe_Dynamic_Handler() { var handleCount = 0; var eventName = "MyEvent-" + Guid.NewGuid().ToString("N"); - var handler = new ActionEventHandler(async (d) => + var handler = new ActionEventHandler(async (d) => { handleCount++; await Task.CompletedTask; @@ -194,7 +194,7 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase } [Fact] - public async Task Should_Convert_AnonymousEventData_To_Typed_Object() + public async Task Should_Convert_DynamicEventData_To_Typed_Object() { MySimpleEventData? receivedData = null; diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Dynamic_Test.cs similarity index 84% rename from framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs rename to framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Dynamic_Test.cs index 7d3e5748e9..1b0e7790a9 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Anonymous_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Dynamic_Test.cs @@ -6,16 +6,16 @@ using Xunit; namespace Volo.Abp.EventBus.Local; -public class LocalEventBus_Anonymous_Test : EventBusTestBase +public class LocalEventBus_Dynamic_Test : EventBusTestBase { [Fact] - public async Task Should_Handle_Anonymous_Handler_With_EventName() + public async Task Should_Handle_Dynamic_Handler_With_EventName() { var handleCount = 0; var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); using var subscription = LocalEventBus.Subscribe(eventName, - new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { handleCount++; d.EventName.ShouldBe(eventName); @@ -67,10 +67,10 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase } [Fact] - public async Task Should_Trigger_Both_Typed_And_Anonymous_Handlers() + public async Task Should_Trigger_Both_Typed_And_Dynamic_Handlers() { var typedHandleCount = 0; - var anonymousHandleCount = 0; + var dynamicHandleCount = 0; using var typedSubscription = LocalEventBus.Subscribe(async (data) => { @@ -80,26 +80,26 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase var eventName = EventNameAttribute.GetNameOrDefault(); - using var anonymousSubscription = LocalEventBus.Subscribe(eventName, - new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + using var dynamicSubscription = LocalEventBus.Subscribe(eventName, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { - anonymousHandleCount++; + dynamicHandleCount++; await Task.CompletedTask; }))); await LocalEventBus.PublishAsync(new MySimpleEventData(1)); typedHandleCount.ShouldBe(1); - anonymousHandleCount.ShouldBe(1); + dynamicHandleCount.ShouldBe(1); } [Fact] - public async Task Should_Unsubscribe_Anonymous_Handler() + public async Task Should_Unsubscribe_Dynamic_Handler() { var handleCount = 0; var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); - var handler = new ActionEventHandler(async (d) => + var handler = new ActionEventHandler(async (d) => { handleCount++; await Task.CompletedTask; @@ -125,13 +125,13 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase } [Fact] - public async Task Should_ConvertToTypedObject_In_Anonymous_Handler() + public async Task Should_ConvertToTypedObject_In_Dynamic_Handler() { object? receivedData = null; var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); using var subscription = LocalEventBus.Subscribe(eventName, - new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { receivedData = d.ConvertToTypedObject(); await Task.CompletedTask; @@ -146,13 +146,13 @@ public class LocalEventBus_Anonymous_Test : EventBusTestBase } [Fact] - public async Task Should_ConvertToTypedObject_Generic_In_Anonymous_Handler() + public async Task Should_ConvertToTypedObject_Generic_In_Dynamic_Handler() { MySimpleEventData? receivedData = null; var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); using var subscription = LocalEventBus.Subscribe(eventName, - new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { receivedData = d.ConvertToTypedObject(); await Task.CompletedTask; diff --git a/test/DistEvents/DistDemoApp.Shared/DistEventScenarioProfile.cs b/test/DistEvents/DistDemoApp.Shared/DistEventScenarioProfile.cs index 5dc360cfe9..c6239cff31 100644 --- a/test/DistEvents/DistDemoApp.Shared/DistEventScenarioProfile.cs +++ b/test/DistEvents/DistDemoApp.Shared/DistEventScenarioProfile.cs @@ -4,19 +4,19 @@ public class DistEventScenarioProfile { public string Name { get; set; } = "default"; - public string AnonymousOnlyEventName { get; set; } = "dist-demo.anonymous-only"; + public string DynamicOnlyEventName { get; set; } = "dist-demo.dynamic-only"; - public string AnonymousOnlyMessage { get; set; } = "hello-anonymous"; + public string DynamicOnlyMessage { get; set; } = "hello-dynamic"; public int TypedFromTypedValue { get; set; } = 7; - public int TypedFromAnonymousValue { get; set; } = 11; + public int TypedFromDynamicValue { get; set; } = 11; public bool EnableTypedFromTypedScenario { get; set; } = true; - public bool EnableTypedFromAnonymousScenario { get; set; } = true; + public bool EnableTypedFromDynamicScenario { get; set; } = true; - public bool EnableAnonymousOnlyScenario { get; set; } = true; + public bool EnableDynamicOnlyScenario { get; set; } = true; public bool OnUnitOfWorkComplete { get; set; } = true; @@ -38,11 +38,11 @@ public class DistEventScenarioProfile return new DistEventScenarioProfile { Name = "dapr-web", - AnonymousOnlyEventName = "dist-demo.dapr.anonymous-only", - AnonymousOnlyMessage = "hello-dapr-web", + DynamicOnlyEventName = "dist-demo.dapr.dynamic-only", + DynamicOnlyMessage = "hello-dapr-web", EnableTypedFromTypedScenario = false, - EnableTypedFromAnonymousScenario = false, - EnableAnonymousOnlyScenario = false + EnableTypedFromDynamicScenario = false, + EnableDynamicOnlyScenario = false }; } @@ -51,10 +51,10 @@ public class DistEventScenarioProfile return new DistEventScenarioProfile { Name = "azure-emulator", - AnonymousOnlyEventName = "DistDemoApp.Azure.AnonymousOnly", - AnonymousOnlyMessage = "hello-azure-emulator", + DynamicOnlyEventName = "DistDemoApp.Azure.DynamicOnly", + DynamicOnlyMessage = "hello-azure-emulator", TypedFromTypedValue = 21, - TypedFromAnonymousValue = 34 + TypedFromDynamicValue = 34 }; } } diff --git a/test/DistEvents/DistDemoApp.Shared/DistEventScenarioRunner.cs b/test/DistEvents/DistDemoApp.Shared/DistEventScenarioRunner.cs index 2bb09937e5..a4015a0012 100644 --- a/test/DistEvents/DistDemoApp.Shared/DistEventScenarioRunner.cs +++ b/test/DistEvents/DistDemoApp.Shared/DistEventScenarioRunner.cs @@ -28,10 +28,10 @@ public class DistEventScenarioRunner : IDistEventScenarioRunner, ITransientDepen var typedFromTypedPublish = profile.EnableTypedFromTypedScenario ? new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) : null; - var typedFromAnonymousPublish = profile.EnableTypedFromAnonymousScenario + var typedFromDynamicPublish = profile.EnableTypedFromDynamicScenario ? new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) : null; - var anonymousOnlyPublish = profile.EnableAnonymousOnlyScenario + var dynamicOnlyPublish = profile.EnableDynamicOnlyScenario ? new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) : null; @@ -42,28 +42,28 @@ public class DistEventScenarioRunner : IDistEventScenarioRunner, ITransientDepen typedFromTypedPublish.TrySetResult(eventData.Value); } - if (typedFromAnonymousPublish != null && eventData.Value == profile.TypedFromAnonymousValue) + if (typedFromDynamicPublish != null && eventData.Value == profile.TypedFromDynamicValue) { - typedFromAnonymousPublish.TrySetResult(eventData.Value); + typedFromDynamicPublish.TrySetResult(eventData.Value); } return Task.CompletedTask; }); - IDisposable? anonymousOnlySubscription = null; - if (profile.EnableAnonymousOnlyScenario) + IDisposable? dynamicOnlySubscription = null; + if (profile.EnableDynamicOnlyScenario) { - anonymousOnlySubscription = _distributedEventBus.Subscribe( - profile.AnonymousOnlyEventName, + dynamicOnlySubscription = _distributedEventBus.Subscribe( + profile.DynamicOnlyEventName, new SingleInstanceHandlerFactory( - new ActionEventHandler(eventData => + new ActionEventHandler(eventData => { - var converted = AnonymousEventDataConverter.ConvertToLooseObject(eventData); + var converted = DynamicEventDataConverter.ConvertToLooseObject(eventData); if (converted is Dictionary payload && payload.TryGetValue("Message", out var message) && - message?.ToString() == profile.AnonymousOnlyMessage) + message?.ToString() == profile.DynamicOnlyMessage) { - anonymousOnlyPublish!.TrySetResult(true); + dynamicOnlyPublish!.TrySetResult(true); } return Task.CompletedTask; @@ -88,17 +88,17 @@ public class DistEventScenarioRunner : IDistEventScenarioRunner, ITransientDepen await typedFromTypedPublish.Task.WaitAsync(TimeSpan.FromSeconds(profile.TimeoutSeconds)); } - if (typedFromAnonymousPublish != null) + if (typedFromDynamicPublish != null) { - await typedFromAnonymousPublish.Task.WaitAsync(TimeSpan.FromSeconds(profile.TimeoutSeconds)); + await typedFromDynamicPublish.Task.WaitAsync(TimeSpan.FromSeconds(profile.TimeoutSeconds)); } - if (anonymousOnlyPublish != null) + if (dynamicOnlyPublish != null) { - await anonymousOnlyPublish.Task.WaitAsync(TimeSpan.FromSeconds(profile.TimeoutSeconds)); + await dynamicOnlyPublish.Task.WaitAsync(TimeSpan.FromSeconds(profile.TimeoutSeconds)); } - anonymousOnlySubscription?.Dispose(); + dynamicOnlySubscription?.Dispose(); Console.WriteLine($"All distributed event scenarios passed ({profile.Name})."); } @@ -113,20 +113,20 @@ public class DistEventScenarioRunner : IDistEventScenarioRunner, ITransientDepen useOutbox: profile.UseOutbox); } - if (profile.EnableTypedFromAnonymousScenario) + if (profile.EnableTypedFromDynamicScenario) { await _distributedEventBus.PublishAsync( typedEventName, - new { Value = profile.TypedFromAnonymousValue }, + new { Value = profile.TypedFromDynamicValue }, onUnitOfWorkComplete: profile.OnUnitOfWorkComplete, useOutbox: profile.UseOutbox); } - if (profile.EnableAnonymousOnlyScenario) + if (profile.EnableDynamicOnlyScenario) { await _distributedEventBus.PublishAsync( - profile.AnonymousOnlyEventName, - new { Message = profile.AnonymousOnlyMessage }, + profile.DynamicOnlyEventName, + new { Message = profile.DynamicOnlyMessage }, onUnitOfWorkComplete: profile.OnUnitOfWorkComplete, useOutbox: profile.UseOutbox); } From 29dee66e908261fa6d52c12ba39011e69221dbd6 Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 22 Mar 2026 19:45:21 +0800 Subject: [PATCH 15/22] refactor: Update DynamicEventData handling and improve documentation across event bus implementations --- .../event-bus/distributed/index.md | 18 +-- .../infrastructure/event-bus/local/index.md | 16 +-- .../Volo/Abp/EventBus/DynamicEventData.cs | 101 ---------------- .../Azure/AzureDistributedEventBus.cs | 9 +- .../EventBus/Dapr/DaprDistributedEventBus.cs | 2 +- .../Kafka/KafkaDistributedEventBus.cs | 8 +- .../RabbitMq/RabbitMqDistributedEventBus.cs | 5 +- .../Rebus/RebusDistributedEventBus.cs | 2 +- .../Distributed/DistributedEventBusBase.cs | 2 +- .../Distributed/LocalDistributedEventBus.cs | 12 +- .../Volo/Abp/EventBus/EventBusBase.cs | 16 ++- .../Volo/Abp/EventBus/Local/LocalEventBus.cs | 2 +- .../LocalDistributedEventBus_Test.cs | 80 ++++++++++++- .../Local/LocalEventBus_Dynamic_Test.cs | 110 ++++++++++++++++-- 14 files changed, 229 insertions(+), 154 deletions(-) diff --git a/docs/en/framework/infrastructure/event-bus/distributed/index.md b/docs/en/framework/infrastructure/event-bus/distributed/index.md index 8fbf91465e..74179bb30c 100644 --- a/docs/en/framework/infrastructure/event-bus/distributed/index.md +++ b/docs/en/framework/infrastructure/event-bus/distributed/index.md @@ -763,14 +763,9 @@ var subscription = distributedEventBus.Subscribe( new SingleInstanceHandlerFactory( new ActionEventHandler(eventData => { - // Access the event name + // Access the event name and raw data var name = eventData.EventName; - - // Convert to a loosely-typed object (Dictionary/List/primitives) - var obj = eventData.ConvertToTypedObject(); - - // Or convert to a strongly-typed object - var typed = eventData.ConvertToTypedObject(); + var data = eventData.Data; return Task.CompletedTask; }))); @@ -789,17 +784,16 @@ Where `myDistributedEventHandler` implements `IDistributedEventHandler()`**: Deserializes data to a strongly-typed `T` object. -- **`ConvertToTypedObject(Type type)`**: Deserializes data to the specified type. + +> If a typed handler exists for the same event name, the framework automatically converts the data to the expected type using the event bus serialization pipeline. Dynamic handlers receive the raw `Data` as-is. ## See Also diff --git a/docs/en/framework/infrastructure/event-bus/local/index.md b/docs/en/framework/infrastructure/event-bus/local/index.md index 8545797049..bc8af41a06 100644 --- a/docs/en/framework/infrastructure/event-bus/local/index.md +++ b/docs/en/framework/infrastructure/event-bus/local/index.md @@ -280,14 +280,9 @@ var subscription = localEventBus.Subscribe( new SingleInstanceHandlerFactory( new ActionEventHandler(eventData => { - // Access the event name + // Access the event name and raw data var name = eventData.EventName; - - // Convert to a loosely-typed object (Dictionary/List/primitives) - var obj = eventData.ConvertToTypedObject(); - - // Or convert to a strongly-typed object - var typed = eventData.ConvertToTypedObject(); + var data = eventData.Data; return Task.CompletedTask; }))); @@ -296,6 +291,13 @@ var subscription = localEventBus.Subscribe( subscription.Dispose(); ```` +The `DynamicEventData` class is a simple data object with two properties: + +- **`EventName`**: The string name that identifies the event. +- **`Data`**: The raw event data payload. + +> If a typed handler exists for the same event name, the framework automatically converts the data to the expected type. Dynamic handlers receive the raw `Data` as-is. + ### Mixed Typed and Dynamic Handlers When both a typed handler and a dynamic handler are registered for the same event name, **both** handlers are triggered. The typed handler receives the converted typed data, while the dynamic handler receives a `DynamicEventData` wrapper. diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/DynamicEventData.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/DynamicEventData.cs index 536102ddfc..1af2f600a3 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/DynamicEventData.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/DynamicEventData.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; - namespace Volo.Abp.EventBus; /// @@ -14,105 +9,9 @@ public class DynamicEventData public object Data { get; } - private JsonElement? _cachedJsonElement; - public DynamicEventData(string eventName, object data) { EventName = Check.NotNullOrWhiteSpace(eventName, nameof(eventName)); Data = Check.NotNull(data, nameof(data)); } - - /// - /// Converts to a loosely-typed object graph - /// (dictionaries for objects, lists for arrays, primitives for values). - /// - public object ConvertToTypedObject() - { - return ConvertElement(GetJsonElement()); - } - - /// - /// Converts to a strongly-typed object. - /// Returns the data directly if it is already of type . - /// - public T ConvertToTypedObject() - { - if (Data is T typedData) - { - return typedData; - } - - return GetJsonElement().Deserialize() - ?? throw new InvalidOperationException($"Failed to deserialize DynamicEventData to {typeof(T).FullName}."); - } - - /// - /// Converts to the specified . - /// Returns the data directly if it is already an instance of the target type. - /// - public object ConvertToTypedObject(Type type) - { - if (type.IsInstanceOfType(Data)) - { - return Data; - } - - return GetJsonElement().Deserialize(type) - ?? throw new InvalidOperationException($"Failed to deserialize DynamicEventData to {type.FullName}."); - } - - private JsonElement GetJsonElement() - { - if (_cachedJsonElement.HasValue) - { - return _cachedJsonElement.Value; - } - - if (Data is JsonElement existingElement) - { - _cachedJsonElement = existingElement; - return existingElement; - } - - _cachedJsonElement = JsonSerializer.SerializeToElement(Data); - return _cachedJsonElement.Value; - } - - private static object ConvertElement(JsonElement element) - { - switch (element.ValueKind) - { - case JsonValueKind.Object: - { - var obj = new Dictionary(); - foreach (var property in element.EnumerateObject()) - { - obj[property.Name] = property.Value.ValueKind == JsonValueKind.Null - ? null - : ConvertElement(property.Value); - } - return obj; - } - case JsonValueKind.Array: - return element.EnumerateArray() - .Select(item => item.ValueKind == JsonValueKind.Null ? null : (object?)ConvertElement(item)) - .ToList(); - case JsonValueKind.String: - return element.GetString()!; - case JsonValueKind.Number when element.TryGetInt64(out var longValue): - return longValue; - case JsonValueKind.Number when element.TryGetDecimal(out var decimalValue): - return decimalValue; - case JsonValueKind.Number when element.TryGetDouble(out var doubleValue): - return doubleValue; - case JsonValueKind.True: - return true; - case JsonValueKind.False: - return false; - case JsonValueKind.Null: - case JsonValueKind.Undefined: - default: - return null!; - } - } } diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index 715cf6c003..e613502018 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -94,8 +94,8 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen } else if (DynamicHandlerFactories.ContainsKey(eventName)) { - var data = Serializer.Deserialize(message.Body.ToArray()); - eventData = new DynamicEventData(eventName, data); + var rawBytes = message.Body.ToArray(); + eventData = new DynamicEventData(eventName, Serializer.Deserialize(rawBytes)); eventType = typeof(DynamicEventData); } else @@ -204,7 +204,7 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen if (eventType != null) { - return PublishAsync(eventType, dynamicEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); } if (DynamicHandlerFactories.ContainsKey(eventName)) @@ -296,8 +296,7 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen } else if (DynamicHandlerFactories.ContainsKey(incomingEvent.EventName)) { - var element = Serializer.Deserialize(incomingEvent.EventData); - eventData = new DynamicEventData(incomingEvent.EventName, element); + eventData = new DynamicEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData)); eventType = typeof(DynamicEventData); } else diff --git a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs index e2d4ca5648..37e34d864b 100644 --- a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs @@ -154,7 +154,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend if (eventType != null) { - return PublishAsync(eventType, dynamicEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); } if (DynamicHandlerFactories.ContainsKey(eventName)) diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs index 50b59fc28b..ff7f9f6816 100644 --- a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs @@ -94,8 +94,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen } else if (DynamicHandlerFactories.ContainsKey(eventName)) { - var element = Serializer.Deserialize(message.Value); - eventData = new DynamicEventData(eventName, element); + eventData = new DynamicEventData(eventName, Serializer.Deserialize(message.Value)); eventType = typeof(DynamicEventData); } else @@ -205,7 +204,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen if (eventType != null) { - return PublishAsync(eventType, dynamicEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); } if (DynamicHandlerFactories.ContainsKey(eventName)) @@ -334,8 +333,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen } else if (DynamicHandlerFactories.ContainsKey(incomingEvent.EventName)) { - var element = Serializer.Deserialize(incomingEvent.EventData); - eventData = new DynamicEventData(incomingEvent.EventName, element); + eventData = new DynamicEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData)); eventType = typeof(DynamicEventData); } else diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs index 98710f7487..747c0ef4b3 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs @@ -111,8 +111,9 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis } else if (DynamicHandlerFactories.ContainsKey(eventName)) { + var rawBytes = ea.Body.ToArray(); eventType = typeof(DynamicEventData); - eventData = new DynamicEventData(eventName, Serializer.Deserialize(ea.Body.ToArray())); + eventData = new DynamicEventData(eventName, Serializer.Deserialize(rawBytes)); } else { @@ -232,7 +233,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis if (eventType != null) { - return PublishAsync(eventType, dynamicEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); } if (DynamicHandlerFactories.ContainsKey(eventName)) diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs index d382585c6d..52c0de77c9 100644 --- a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs @@ -198,7 +198,7 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen if (eventType != null) { - return PublishAsync(eventType, dynamicEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); } if (DynamicHandlerFactories.ContainsKey(eventName)) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs index 2bec5815e9..c4d186e2b3 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/DistributedEventBusBase.cs @@ -117,7 +117,7 @@ public abstract class DistributedEventBusBase : EventBusBase, IDistributedEventB if (eventType != null) { - return PublishAsync(eventType, dynamicEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete, useOutbox); + return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete, useOutbox); } return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete, useOutbox); diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs index f92d453a82..f675f71b58 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs @@ -177,7 +177,7 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen if (eventType != null) { - return PublishAsync(eventType, dynamicEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete, useOutbox); + return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete, useOutbox); } if (!DynamicEventNames.ContainsKey(eventName)) @@ -236,11 +236,11 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen { eventData = new DynamicEventData( outgoingEvent.EventName, - JsonSerializer.Deserialize(outgoingEvent.EventData)!); + System.Text.Json.JsonSerializer.Deserialize(outgoingEvent.EventData)!); } else { - eventData = JsonSerializer.Deserialize(outgoingEvent.EventData, eventType)!; + eventData = System.Text.Json.JsonSerializer.Deserialize(outgoingEvent.EventData, eventType)!; } if (await AddToInboxAsync(Guid.NewGuid().ToString(), outgoingEvent.EventName, eventType, eventData, null)) @@ -278,11 +278,11 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen { eventData = new DynamicEventData( incomingEvent.EventName, - JsonSerializer.Deserialize(incomingEvent.EventData)!); + System.Text.Json.JsonSerializer.Deserialize(incomingEvent.EventData)!); } else { - eventData = JsonSerializer.Deserialize(incomingEvent.EventData, eventType)!; + eventData = System.Text.Json.JsonSerializer.Deserialize(incomingEvent.EventData, eventType)!; } var exceptions = new List(); @@ -298,7 +298,7 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected override byte[] Serialize(object eventData) { - return JsonSerializer.SerializeToUtf8Bytes(eventData); + return System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(eventData); } protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs index 6a8e357373..41fe39fb3f 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using System.Text.Json; using Volo.Abp.Collections; using Volo.Abp.DynamicProxy; using Volo.Abp.EventBus.Distributed; @@ -175,7 +176,7 @@ public abstract class EventBusBase : IEventBus typeof(IEventDataWithInheritableGenericArgument).IsAssignableFrom(actualEventType)) { var resolvedEventData = eventData is DynamicEventData aed - ? aed.ConvertToTypedObject(actualEventType) + ? ConvertDynamicEventData(aed.Data, actualEventType) : eventData; var genericArg = actualEventType.GetGenericArguments()[0]; @@ -209,7 +210,7 @@ public abstract class EventBusBase : IEventBus { if (eventData is DynamicEventData dynamicEventData && handlerEventType != typeof(DynamicEventData)) { - return dynamicEventData.ConvertToTypedObject(handlerEventType); + return ConvertDynamicEventData(dynamicEventData.Data, handlerEventType); } if (handlerEventType == typeof(DynamicEventData) && eventData is not DynamicEventData) @@ -220,6 +221,17 @@ public abstract class EventBusBase : IEventBus return eventData; } + protected virtual object ConvertDynamicEventData(object data, Type targetType) + { + if (targetType.IsInstanceOfType(data)) + { + return data; + } + + var json = JsonSerializer.Serialize(data); + return JsonSerializer.Deserialize(json, targetType)!; + } + protected void ThrowOriginalExceptions(Type eventType, List exceptions) { if (exceptions.Count == 1) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs index d56a81fb39..5d8947dc40 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs @@ -178,7 +178,7 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency if (eventType != null) { - return PublishAsync(eventType, dynamicEventData.ConvertToTypedObject(eventType), onUnitOfWorkComplete); + return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); } var isDynamic = DynamicEventHandlerFactories.ContainsKey(eventName); diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs index aa16a5542d..d6f4c2275c 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs @@ -82,7 +82,7 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { handleCount++; - d.ConvertToTypedObject().ShouldNotBeNull(); + d.Data.ShouldNotBeNull(); await Task.CompletedTask; }))); @@ -211,6 +211,69 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase receivedData.Value.ShouldBe(42); } + [Fact] + public async Task Should_Subscribe_With_IDistributedEventHandler() + { + var handleCount = 0; + var eventName = "MyEvent-" + Guid.NewGuid().ToString("N"); + + using var subscription = DistributedEventBus.Subscribe(eventName, + new TestDynamicDistributedEventHandler(() => handleCount++)); + + await DistributedEventBus.PublishAsync(eventName, new { Value = 1 }); + await DistributedEventBus.PublishAsync(eventName, new { Value = 2 }); + + Assert.Equal(2, handleCount); + } + + [Fact] + public async Task Should_Handle_Multiple_Dynamic_Events_Independently() + { + var countA = 0; + var countB = 0; + var eventNameA = "EventA-" + Guid.NewGuid().ToString("N"); + var eventNameB = "EventB-" + Guid.NewGuid().ToString("N"); + + using var subA = DistributedEventBus.Subscribe(eventNameA, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + countA++; + await Task.CompletedTask; + }))); + + using var subB = DistributedEventBus.Subscribe(eventNameB, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + countB++; + await Task.CompletedTask; + }))); + + await DistributedEventBus.PublishAsync(eventNameA, new { Value = 1 }); + await DistributedEventBus.PublishAsync(eventNameB, new { Value = 2 }); + await DistributedEventBus.PublishAsync(eventNameA, new { Value = 3 }); + + Assert.Equal(2, countA); + Assert.Equal(1, countB); + } + + [Fact] + public async Task Should_Receive_EventName_In_DynamicEventData() + { + string? receivedEventName = null; + var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); + + using var subscription = DistributedEventBus.Subscribe(eventName, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + receivedEventName = d.EventName; + await Task.CompletedTask; + }))); + + await DistributedEventBus.PublishAsync(eventName, new { Value = 1 }); + + receivedEventName.ShouldBe(eventName); + } + [Fact] public async Task Should_Change_TenantId_If_EventData_Is_MultiTenant() { @@ -309,4 +372,19 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase } } + class TestDynamicDistributedEventHandler : IDistributedEventHandler + { + private readonly Action _onHandle; + + public TestDynamicDistributedEventHandler(Action onHandle) + { + _onHandle = onHandle; + } + + public Task HandleEventAsync(DynamicEventData eventData) + { + _onHandle(); + return Task.CompletedTask; + } + } } diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Dynamic_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Dynamic_Test.cs index 1b0e7790a9..c9d5180186 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Dynamic_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Dynamic_Test.cs @@ -125,7 +125,7 @@ public class LocalEventBus_Dynamic_Test : EventBusTestBase } [Fact] - public async Task Should_ConvertToTypedObject_In_Dynamic_Handler() + public async Task Should_Access_Data_In_Dynamic_Handler() { object? receivedData = null; var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); @@ -133,34 +133,126 @@ public class LocalEventBus_Dynamic_Test : EventBusTestBase using var subscription = LocalEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { - receivedData = d.ConvertToTypedObject(); + receivedData = d.Data; await Task.CompletedTask; }))); await LocalEventBus.PublishAsync(eventName, new { Name = "Hello", Count = 42 }); receivedData.ShouldNotBeNull(); - var dict = receivedData.ShouldBeOfType>(); - dict["Name"].ShouldBe("Hello"); - dict["Count"].ShouldBe(42L); } [Fact] - public async Task Should_ConvertToTypedObject_Generic_In_Dynamic_Handler() + public async Task Should_Receive_Typed_Data_Via_Typed_Handler_From_Dynamic_Publish() { MySimpleEventData? receivedData = null; + var eventName = EventNameAttribute.GetNameOrDefault(); + + using var subscription = LocalEventBus.Subscribe(async (d) => + { + receivedData = d; + await Task.CompletedTask; + }); + + await LocalEventBus.PublishAsync(eventName, new MySimpleEventData(99)); + + receivedData.ShouldNotBeNull(); + receivedData.Value.ShouldBe(99); + } + + [Fact] + public async Task Should_Unsubscribe_All_Dynamic_Handlers() + { + var handleCount = 0; + var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); + + LocalEventBus.Subscribe(eventName, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + handleCount++; + await Task.CompletedTask; + }))); + + LocalEventBus.Subscribe(eventName, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + handleCount++; + await Task.CompletedTask; + }))); + + await LocalEventBus.PublishAsync(eventName, new { Value = 1 }); + handleCount.ShouldBe(2); + + LocalEventBus.UnsubscribeAll(eventName); + + // After UnsubscribeAll, publishing still works (key exists) but no handlers are invoked + await LocalEventBus.PublishAsync(eventName, new { Value = 2 }); + handleCount.ShouldBe(2); + } + + [Fact] + public async Task Should_Handle_Multiple_Dynamic_Events_Independently() + { + var countA = 0; + var countB = 0; + var eventNameA = "EventA-" + Guid.NewGuid().ToString("N"); + var eventNameB = "EventB-" + Guid.NewGuid().ToString("N"); + + using var subA = LocalEventBus.Subscribe(eventNameA, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + countA++; + await Task.CompletedTask; + }))); + + using var subB = LocalEventBus.Subscribe(eventNameB, + new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => + { + countB++; + await Task.CompletedTask; + }))); + + await LocalEventBus.PublishAsync(eventNameA, new { Value = 1 }); + await LocalEventBus.PublishAsync(eventNameB, new { Value = 2 }); + await LocalEventBus.PublishAsync(eventNameA, new { Value = 3 }); + + countA.ShouldBe(2); + countB.ShouldBe(1); + } + + [Fact] + public async Task Should_Receive_EventName_In_DynamicEventData() + { + string? receivedEventName = null; var eventName = "TestEvent-" + Guid.NewGuid().ToString("N"); using var subscription = LocalEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new ActionEventHandler(async (d) => { - receivedData = d.ConvertToTypedObject(); + receivedEventName = d.EventName; await Task.CompletedTask; }))); - await LocalEventBus.PublishAsync(eventName, new MySimpleEventData(99)); + await LocalEventBus.PublishAsync(eventName, new { Value = 1 }); + + receivedEventName.ShouldBe(eventName); + } + + [Fact] + public async Task Should_Convert_Anonymous_Object_To_Typed_Handler() + { + MySimpleEventData? receivedData = null; + var eventName = EventNameAttribute.GetNameOrDefault(); + + using var subscription = LocalEventBus.Subscribe(async (d) => + { + receivedData = d; + await Task.CompletedTask; + }); + + await LocalEventBus.PublishAsync(eventName, new { Value = 77 }); receivedData.ShouldNotBeNull(); - receivedData.Value.ShouldBe(99); + receivedData.Value.ShouldBe(77); } } From 20d85a44bb71285762893ed063e2184282095d95 Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 22 Mar 2026 19:54:30 +0800 Subject: [PATCH 16/22] fix: Add missing newlines at the end of several files in the event bus implementation --- .../Volo/Abp/EventBus/IEventBus.cs | 2 +- .../Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs | 6 +++--- .../Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs | 6 +++--- .../Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs | 6 +++--- .../Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs | 6 +++--- .../Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs index 28b6c9567e..5430f20155 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/IEventBus.cs @@ -163,4 +163,4 @@ public interface IEventBus /// /// Name of the event void UnsubscribeAll(string eventName); -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index e613502018..0004bce24b 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -426,14 +426,14 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected override IEnumerable GetDynamicHandlerFactories(string eventName) { - var result = new List(); - var eventType = GetEventTypeByEventName(eventName); if (eventType != null) { - result.AddRange(GetHandlerFactories(eventType)); + return GetHandlerFactories(eventType); } + var result = new List(); + foreach (var handlerFactory in DynamicHandlerFactories.Where(hf => hf.Key == eventName)) { result.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); diff --git a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs index 37e34d864b..3f4f1780db 100644 --- a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs @@ -358,14 +358,14 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend protected override IEnumerable GetDynamicHandlerFactories(string eventName) { - var result = new List(); - var eventType = GetEventTypeByEventName(eventName); if (eventType != null) { - result.AddRange(GetHandlerFactories(eventType)); + return GetHandlerFactories(eventType); } + var result = new List(); + foreach (var handlerFactory in DynamicHandlerFactories.Where(hf => hf.Key == eventName)) { result.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs index ff7f9f6816..792442aa06 100644 --- a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs @@ -459,14 +459,14 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected override IEnumerable GetDynamicHandlerFactories(string eventName) { - var result = new List(); - var eventType = GetEventTypeByEventName(eventName); if (eventType != null) { - result.AddRange(GetHandlerFactories(eventType)); + return GetHandlerFactories(eventType); } + var result = new List(); + foreach (var handlerFactory in DynamicHandlerFactories.Where(hf => hf.Key == eventName)) { result.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs index 52c0de77c9..f192eeda4b 100644 --- a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs @@ -418,14 +418,14 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen protected override IEnumerable GetDynamicHandlerFactories(string eventName) { - var result = new List(); - var eventType = GetEventTypeByEventName(eventName); if (eventType != null) { - result.AddRange(GetHandlerFactories(eventType)); + return GetHandlerFactories(eventType); } + var result = new List(); + foreach (var handlerFactory in DynamicHandlerFactories.Where(hf => hf.Key == eventName)) { result.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs index cd5c7b74f6..ffbf14d25f 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerFactoryUnregistrar.cs @@ -44,4 +44,4 @@ public class DynamicEventHandlerFactoryUnregistrar : IDisposable { _eventBus.Unsubscribe(_eventName, _factory); } -} \ No newline at end of file +} From ca9b3c54db0249be3213b9e42112e1f69bcb928b Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 22 Mar 2026 20:37:34 +0800 Subject: [PATCH 17/22] refactor: Remove dynamic event support from Dapr distributed event bus implementation --- .../event-bus/distributed/index.md | 2 + .../AbpAspNetCoreMvcDaprEventBusModule.cs | 10 -- .../EventBus/Dapr/DaprDistributedEventBus.cs | 96 ++++--------------- 3 files changed, 22 insertions(+), 86 deletions(-) diff --git a/docs/en/framework/infrastructure/event-bus/distributed/index.md b/docs/en/framework/infrastructure/event-bus/distributed/index.md index 74179bb30c..5f2c7b11ec 100644 --- a/docs/en/framework/infrastructure/event-bus/distributed/index.md +++ b/docs/en/framework/infrastructure/event-bus/distributed/index.md @@ -725,6 +725,8 @@ Configure(options => In addition to the type-safe event system described above, ABP also supports **dynamic events** that are identified by a string name rather than a CLR type. This is useful for scenarios where event types are not known at compile time, such as integrating with external systems or building plugin architectures. +> **Note:** Dynamic event subscriptions are supported by RabbitMQ, Kafka, Azure Service Bus, and Rebus providers. The **Dapr provider does not support dynamic events** because Dapr requires topic subscriptions to be declared at application startup and cannot add subscriptions at runtime. Attempting to call `Subscribe(string, ...)` on the Dapr provider will throw an `AbpException`. + ### Publishing Dynamic Events Use the `PublishAsync` overload that accepts a string event name: diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs index 19ca7dc71f..fba9e12707 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus/Volo/Abp/AspNetCore/Mvc/Dapr/EventBus/AbpAspNetCoreMvcDaprEventBusModule.cs @@ -103,11 +103,6 @@ public class AbpAspNetCoreMvcDaprEventBusModule : AbpModule var eventData = daprSerializer.Deserialize(daprEventData.JsonData, eventType); await distributedEventBus.TriggerHandlersAsync(eventType, eventData, daprEventData.MessageId, daprEventData.CorrelationId); } - else if (distributedEventBus.IsDynamicEvent(daprEventData.Topic)) - { - var eventData = daprSerializer.Deserialize(daprEventData.JsonData, typeof(object)); - await distributedEventBus.TriggerHandlersAsync(typeof(DynamicEventData), new DynamicEventData(daprEventData.Topic, eventData), daprEventData.MessageId, daprEventData.CorrelationId); - } } else { @@ -117,11 +112,6 @@ public class AbpAspNetCoreMvcDaprEventBusModule : AbpModule var eventData = daprSerializer.Deserialize(data, eventType); await distributedEventBus.TriggerHandlersAsync(eventType, eventData); } - else if (distributedEventBus.IsDynamicEvent(topic)) - { - var eventData = daprSerializer.Deserialize(data, typeof(object)); - await distributedEventBus.TriggerHandlersAsync(typeof(DynamicEventData), new DynamicEventData(topic, eventData)); - } } httpContext.Response.StatusCode = 200; diff --git a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs index 3f4f1780db..d122d3e5e3 100644 --- a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs @@ -28,7 +28,6 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend protected ConcurrentDictionary> HandlerFactories { get; } protected ConcurrentDictionary EventTypes { get; } - protected ConcurrentDictionary> DynamicHandlerFactories { get; } public DaprDistributedEventBus( IServiceScopeFactory serviceScopeFactory, @@ -59,7 +58,6 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend HandlerFactories = new ConcurrentDictionary>(); EventTypes = new ConcurrentDictionary(); - DynamicHandlerFactories = new ConcurrentDictionary>(); } public void Initialize() @@ -84,16 +82,10 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend /// public override IDisposable Subscribe(string eventName, IEventHandlerFactory handler) { - var handlerFactories = GetOrCreateDynamicHandlerFactories(eventName); - - if (handler.IsInFactories(handlerFactories)) - { - return NullDisposable.Instance; - } - - handlerFactories.Add(handler); - - return new DynamicEventHandlerFactoryUnregistrar(this, eventName, handler); + throw new AbpException( + "Dapr distributed event bus does not support dynamic event subscriptions. " + + "Dapr requires topic subscriptions to be declared at startup and cannot add subscriptions at runtime. " + + "Use a typed event handler (IDistributedEventHandler) instead."); } public override void Unsubscribe(Func action) @@ -150,19 +142,16 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public override Task PublishAsync(string eventName, object eventData, bool onUnitOfWorkComplete = true) { var eventType = EventTypes.GetOrDefault(eventName); - var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); - if (eventType != null) { + var dynamicEventData = eventData as DynamicEventData ?? new DynamicEventData(eventName, eventData); return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); } - if (DynamicHandlerFactories.ContainsKey(eventName)) - { - return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); - } - - throw new AbpException($"Unknown event name: {eventName}"); + throw new AbpException( + "Dapr distributed event bus does not support dynamic event publishing. " + + "Dapr requires topic subscriptions to be declared at startup. " + + "Use a typed event (PublishAsync) or ensure the event name matches a registered typed event."); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) @@ -179,21 +168,12 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public async override Task PublishFromOutboxAsync(OutgoingEventInfo outgoingEvent, OutboxConfig outboxConfig) { var eventType = EventTypes.GetOrDefault(outgoingEvent.EventName); - object eventData; - - if (eventType != null) - { - eventData = Serializer.Deserialize(outgoingEvent.EventData, eventType); - } - else if (DynamicHandlerFactories.ContainsKey(outgoingEvent.EventName)) - { - eventData = Serializer.Deserialize(outgoingEvent.EventData, typeof(object)); - } - else + if (eventType == null) { return; } + var eventData = Serializer.Deserialize(outgoingEvent.EventData, eventType); await PublishToDaprAsync(outgoingEvent.EventName, eventData, outgoingEvent.Id, outgoingEvent.GetCorrelationId()); using (CorrelationIdProvider.Change(outgoingEvent.GetCorrelationId())) @@ -233,20 +213,13 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend var eventType = EventTypes.GetOrDefault(incomingEvent.EventName); object eventData; - if (eventType != null) - { - eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); - } - else if (DynamicHandlerFactories.ContainsKey(incomingEvent.EventName)) - { - eventData = new DynamicEventData(incomingEvent.EventName, Serializer.Deserialize(incomingEvent.EventData, typeof(object))); - eventType = typeof(DynamicEventData); - } - else + if (eventType == null) { return; } + eventData = Serializer.Deserialize(incomingEvent.EventData, eventType); + var exceptions = new List(); using (CorrelationIdProvider.Change(incomingEvent.GetCorrelationId())) { @@ -277,10 +250,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend protected override Task OnAddToOutboxAsync(string eventName, Type eventType, object eventData) { - if (typeof(DynamicEventData) != eventType) - { - EventTypes.GetOrAdd(eventName, eventType); - } + EventTypes.GetOrAdd(eventName, eventType); return base.OnAddToOutboxAsync(eventName, eventType, eventData); } @@ -300,18 +270,12 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend protected override IEnumerable GetHandlerFactories(Type eventType) { var handlerFactoryList = new List(); - var eventNames = EventTypes.Where(x => ShouldTriggerEventForHandler(eventType, x.Value)).Select(x => x.Key).ToList(); foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key))) { handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value)); } - foreach (var handlerFactory in DynamicHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key))) - { - handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); - } - return handlerFactoryList.ToArray(); } @@ -327,33 +291,25 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend public bool IsDynamicEvent(string eventName) { - return DynamicHandlerFactories.ContainsKey(eventName); + return false; } /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) { - GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Remove(factory)); + throw new AbpException("Dapr distributed event bus does not support dynamic event subscriptions."); } /// public override void Unsubscribe(string eventName, IEventHandler handler) { - GetOrCreateDynamicHandlerFactories(eventName) - .Locking(factories => - { - factories.RemoveAll( - factory => - factory is SingleInstanceHandlerFactory singleFactory && - singleFactory.HandlerInstance == handler - ); - }); + throw new AbpException("Dapr distributed event bus does not support dynamic event subscriptions."); } /// public override void UnsubscribeAll(string eventName) { - GetOrCreateDynamicHandlerFactories(eventName).Locking(factories => factories.Clear()); + throw new AbpException("Dapr distributed event bus does not support dynamic event subscriptions."); } protected override IEnumerable GetDynamicHandlerFactories(string eventName) @@ -364,19 +320,7 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend return GetHandlerFactories(eventType); } - var result = new List(); - - foreach (var handlerFactory in DynamicHandlerFactories.Where(hf => hf.Key == eventName)) - { - result.Add(new EventTypeWithEventHandlerFactories(typeof(DynamicEventData), handlerFactory.Value)); - } - - return result; - } - - private List GetOrCreateDynamicHandlerFactories(string eventName) - { - return DynamicHandlerFactories.GetOrAdd(eventName, _ => new List()); + return Array.Empty(); } private static bool ShouldTriggerEventForHandler(Type targetEventType, Type handlerEventType) From 3658bf3a49497e7f31540eaadc5df502cc6f3f8c Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 22 Mar 2026 21:28:30 +0800 Subject: [PATCH 18/22] Remove `DistDemoApp`. --- test/DistEvents/.gitignore | 398 ------------------ .../DistDemoApp.AspNetCoreDapr.csproj | 39 -- .../DistDemoAppAspNetCoreDaprModule.cs | 67 --- .../DistDemoApp.AspNetCoreDapr/Program.cs | 12 - .../ProviderScenarioController.cs | 23 - .../ProviderScenarioEventHandler.cs | 17 - .../appsettings.json | 14 - .../DistDemoApp.AzureEmulator.csproj | 37 -- .../DistDemoAppAzureEmulatorModule.cs | 51 --- .../EmulatorProcessorPool.cs | 79 ---- .../EmulatorPublisherPool.cs | 64 --- .../DistDemoApp.AzureEmulator/Program.cs | 32 -- .../appsettings.json | 22 - .../DistDemoApp.EfCoreRabbitMq.csproj | 44 -- .../DistDemoAppEfCoreRabbitMqModule.cs | 59 --- ...0910152547_Added_Boxes_Initial.Designer.cs | 162 ------- .../20210910152547_Added_Boxes_Initial.cs | 103 ----- .../20260304073807_Update-10.2.Designer.cs | 182 -------- .../Migrations/20260304073807_Update-10.2.cs | 233 ---------- .../Migrations/TodoDbContextModelSnapshot.cs | 179 -------- .../DistDemoApp.EfCoreRabbitMq/Program.cs | 34 -- .../appsettings.json | 19 - .../DistDemoApp.MongoDbKafka.csproj | 37 -- .../DistDemoAppMongoDbKafkaModule.cs | 43 -- .../DistDemoApp.MongoDbKafka/Program.cs | 36 -- .../DistDemoApp.MongoDbKafka/appsettings.json | 19 - .../DistDemoApp.MongoDbRebus.csproj | 37 -- .../DistDemoAppMongoDbRebusModule.cs | 43 -- .../DistDemoApp.MongoDbRebus/Program.cs | 36 -- .../DistDemoApp.MongoDbRebus/appsettings.json | 19 - ...App.Persistence.EntityFrameworkCore.csproj | 18 - ...EntityFrameworkCoreInfrastructureModule.cs | 12 - ...ityFrameworkServiceCollectionExtensions.cs | 21 - .../TodoDbContext.cs | 34 -- .../TodoDbContextFactory.cs | 28 -- .../DistDemoApp.Persistence.MongoDb.csproj | 14 - .../DistDemoAppMongoDbInfrastructureModule.cs | 12 - .../DistDemoMongoDbContext.cs | 26 -- ...istDemoMongoServiceCollectionExtensions.cs | 42 -- .../DistDemoApp.Shared/DemoService.cs | 21 - .../DistDemoApp.Shared.csproj | 23 - .../DistDemoAppHostedService.cs | 40 -- .../DistDemoAppSharedModule.cs | 39 -- .../DistEventScenarioProfile.cs | 60 --- .../DistEventScenarioRunner.cs | 134 ------ .../IDistEventScenarioRunner.cs | 8 - .../ProviderScenarioEvent.cs | 9 - .../DistDemoApp.Shared/TodoEventHandler.cs | 63 --- .../DistEvents/DistDemoApp.Shared/TodoItem.cs | 15 - .../DistDemoApp.Shared/TodoItemEto.cs | 12 - .../TodoItemObjectMapper.cs | 24 -- .../DistDemoApp.Shared/TodoSummary.cs | 41 -- test/DistEvents/DistEventsDemo.slnx | 10 - test/DistEvents/dapr/components/pubsub.yaml | 12 - test/DistEvents/docker-compose.yml | 131 ------ .../servicebus-emulator/Config.json | 22 - 56 files changed, 3011 deletions(-) delete mode 100644 test/DistEvents/.gitignore delete mode 100644 test/DistEvents/DistDemoApp.AspNetCoreDapr/DistDemoApp.AspNetCoreDapr.csproj delete mode 100644 test/DistEvents/DistDemoApp.AspNetCoreDapr/DistDemoAppAspNetCoreDaprModule.cs delete mode 100644 test/DistEvents/DistDemoApp.AspNetCoreDapr/Program.cs delete mode 100644 test/DistEvents/DistDemoApp.AspNetCoreDapr/ProviderScenarioController.cs delete mode 100644 test/DistEvents/DistDemoApp.AspNetCoreDapr/ProviderScenarioEventHandler.cs delete mode 100644 test/DistEvents/DistDemoApp.AspNetCoreDapr/appsettings.json delete mode 100644 test/DistEvents/DistDemoApp.AzureEmulator/DistDemoApp.AzureEmulator.csproj delete mode 100644 test/DistEvents/DistDemoApp.AzureEmulator/DistDemoAppAzureEmulatorModule.cs delete mode 100644 test/DistEvents/DistDemoApp.AzureEmulator/EmulatorProcessorPool.cs delete mode 100644 test/DistEvents/DistDemoApp.AzureEmulator/EmulatorPublisherPool.cs delete mode 100644 test/DistEvents/DistDemoApp.AzureEmulator/Program.cs delete mode 100644 test/DistEvents/DistDemoApp.AzureEmulator/appsettings.json delete mode 100644 test/DistEvents/DistDemoApp.EfCoreRabbitMq/DistDemoApp.EfCoreRabbitMq.csproj delete mode 100644 test/DistEvents/DistDemoApp.EfCoreRabbitMq/DistDemoAppEfCoreRabbitMqModule.cs delete mode 100644 test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20210910152547_Added_Boxes_Initial.Designer.cs delete mode 100644 test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20210910152547_Added_Boxes_Initial.cs delete mode 100644 test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20260304073807_Update-10.2.Designer.cs delete mode 100644 test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20260304073807_Update-10.2.cs delete mode 100644 test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/TodoDbContextModelSnapshot.cs delete mode 100644 test/DistEvents/DistDemoApp.EfCoreRabbitMq/Program.cs delete mode 100644 test/DistEvents/DistDemoApp.EfCoreRabbitMq/appsettings.json delete mode 100644 test/DistEvents/DistDemoApp.MongoDbKafka/DistDemoApp.MongoDbKafka.csproj delete mode 100644 test/DistEvents/DistDemoApp.MongoDbKafka/DistDemoAppMongoDbKafkaModule.cs delete mode 100644 test/DistEvents/DistDemoApp.MongoDbKafka/Program.cs delete mode 100644 test/DistEvents/DistDemoApp.MongoDbKafka/appsettings.json delete mode 100644 test/DistEvents/DistDemoApp.MongoDbRebus/DistDemoApp.MongoDbRebus.csproj delete mode 100644 test/DistEvents/DistDemoApp.MongoDbRebus/DistDemoAppMongoDbRebusModule.cs delete mode 100644 test/DistEvents/DistDemoApp.MongoDbRebus/Program.cs delete mode 100644 test/DistEvents/DistDemoApp.MongoDbRebus/appsettings.json delete mode 100644 test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoApp.Persistence.EntityFrameworkCore.csproj delete mode 100644 test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoAppEntityFrameworkCoreInfrastructureModule.cs delete mode 100644 test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoEntityFrameworkServiceCollectionExtensions.cs delete mode 100644 test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/TodoDbContext.cs delete mode 100644 test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/TodoDbContextFactory.cs delete mode 100644 test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoApp.Persistence.MongoDb.csproj delete mode 100644 test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoAppMongoDbInfrastructureModule.cs delete mode 100644 test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoMongoDbContext.cs delete mode 100644 test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoMongoServiceCollectionExtensions.cs delete mode 100644 test/DistEvents/DistDemoApp.Shared/DemoService.cs delete mode 100644 test/DistEvents/DistDemoApp.Shared/DistDemoApp.Shared.csproj delete mode 100644 test/DistEvents/DistDemoApp.Shared/DistDemoAppHostedService.cs delete mode 100644 test/DistEvents/DistDemoApp.Shared/DistDemoAppSharedModule.cs delete mode 100644 test/DistEvents/DistDemoApp.Shared/DistEventScenarioProfile.cs delete mode 100644 test/DistEvents/DistDemoApp.Shared/DistEventScenarioRunner.cs delete mode 100644 test/DistEvents/DistDemoApp.Shared/IDistEventScenarioRunner.cs delete mode 100644 test/DistEvents/DistDemoApp.Shared/ProviderScenarioEvent.cs delete mode 100644 test/DistEvents/DistDemoApp.Shared/TodoEventHandler.cs delete mode 100644 test/DistEvents/DistDemoApp.Shared/TodoItem.cs delete mode 100644 test/DistEvents/DistDemoApp.Shared/TodoItemEto.cs delete mode 100644 test/DistEvents/DistDemoApp.Shared/TodoItemObjectMapper.cs delete mode 100644 test/DistEvents/DistDemoApp.Shared/TodoSummary.cs delete mode 100644 test/DistEvents/DistEventsDemo.slnx delete mode 100644 test/DistEvents/dapr/components/pubsub.yaml delete mode 100644 test/DistEvents/docker-compose.yml delete mode 100644 test/DistEvents/servicebus-emulator/Config.json diff --git a/test/DistEvents/.gitignore b/test/DistEvents/.gitignore deleted file mode 100644 index 8dd4607a4b..0000000000 --- a/test/DistEvents/.gitignore +++ /dev/null @@ -1,398 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider -*.sln.iml \ No newline at end of file diff --git a/test/DistEvents/DistDemoApp.AspNetCoreDapr/DistDemoApp.AspNetCoreDapr.csproj b/test/DistEvents/DistDemoApp.AspNetCoreDapr/DistDemoApp.AspNetCoreDapr.csproj deleted file mode 100644 index 8801131096..0000000000 --- a/test/DistEvents/DistDemoApp.AspNetCoreDapr/DistDemoApp.AspNetCoreDapr.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - net10.0 - enable - enable - DistDemoApp - MongoDb - - - - - - - - - - - - - - - - - - $(DefineConstants);DISTDEMO_USE_MONGODB - - - - $(DefineConstants);DISTDEMO_USE_EFCORE - - - - - Always - - - - diff --git a/test/DistEvents/DistDemoApp.AspNetCoreDapr/DistDemoAppAspNetCoreDaprModule.cs b/test/DistEvents/DistDemoApp.AspNetCoreDapr/DistDemoAppAspNetCoreDaprModule.cs deleted file mode 100644 index 843a4af81b..0000000000 --- a/test/DistEvents/DistDemoApp.AspNetCoreDapr/DistDemoAppAspNetCoreDaprModule.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Dapr; -using Volo.Abp; -using Volo.Abp.AspNetCore.Mvc.Dapr.EventBus; -using Volo.Abp.Autofac; -using Volo.Abp.Dapr; -using Volo.Abp.EventBus.Dapr; -using Volo.Abp.Modularity; - -namespace DistDemoApp; - -#if DISTDEMO_USE_MONGODB -[DependsOn( - typeof(DistDemoAppMongoDbInfrastructureModule), - typeof(AbpAspNetCoreMvcDaprEventBusModule), - typeof(DistDemoAppSharedModule), - typeof(AbpAutofacModule) -)] -#else -[DependsOn( - typeof(DistDemoAppEntityFrameworkCoreInfrastructureModule), - typeof(AbpAspNetCoreMvcDaprEventBusModule), - typeof(DistDemoAppSharedModule), - typeof(AbpAutofacModule) -)] -#endif -public class DistDemoAppAspNetCoreDaprModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { -#if DISTDEMO_USE_MONGODB - context.ConfigureDistDemoMongoInfrastructure(); -#else - context.ConfigureDistDemoEntityFrameworkInfrastructure(); -#endif - - Configure(options => - { - options.HttpEndpoint = "http://localhost:3500"; - }); - - Configure(options => - { - options.PubSubName = "pubsub"; - }); - } - - public override void OnApplicationInitialization(ApplicationInitializationContext context) - { - var app = context.GetApplicationBuilder(); - var env = context.GetEnvironment(); - - if (env.IsDevelopment()) - { - app.UseExceptionHandler("/Error"); - } - - app.UseHttpsRedirection(); - app.UseStaticFiles(); - app.UseRouting(); - app.UseCloudEvents(); - app.UseConfiguredEndpoints(endpoints => - { - endpoints.MapControllers(); - endpoints.MapSubscribeHandler(); - }); - } -} diff --git a/test/DistEvents/DistDemoApp.AspNetCoreDapr/Program.cs b/test/DistEvents/DistDemoApp.AspNetCoreDapr/Program.cs deleted file mode 100644 index f5d3f8c8ef..0000000000 --- a/test/DistEvents/DistDemoApp.AspNetCoreDapr/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using DistDemoApp; -using Dapr; -using Volo.Abp; - -var builder = WebApplication.CreateBuilder(args); -builder.Host.UseAutofac(); - -await builder.AddApplicationAsync(); - -var app = builder.Build(); -await app.InitializeApplicationAsync(); -await app.RunAsync("http://localhost:8090"); diff --git a/test/DistEvents/DistDemoApp.AspNetCoreDapr/ProviderScenarioController.cs b/test/DistEvents/DistDemoApp.AspNetCoreDapr/ProviderScenarioController.cs deleted file mode 100644 index ff6de4f5b6..0000000000 --- a/test/DistEvents/DistDemoApp.AspNetCoreDapr/ProviderScenarioController.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Volo.Abp.AspNetCore.Mvc; - -namespace DistDemoApp; - -[ApiController] -[Route("api/dist-demo/dapr")] -public class ProviderScenarioController : AbpController -{ - private readonly IDistEventScenarioRunner _scenarioRunner; - - public ProviderScenarioController(IDistEventScenarioRunner scenarioRunner) - { - _scenarioRunner = scenarioRunner; - } - - [HttpGet] - public async Task RunAsync() - { - await _scenarioRunner.RunAsync(DistEventScenarioProfile.DaprWeb()); - return Ok(new { Status = "ScenarioCompleted", Profile = "dapr-web" }); - } -} diff --git a/test/DistEvents/DistDemoApp.AspNetCoreDapr/ProviderScenarioEventHandler.cs b/test/DistEvents/DistDemoApp.AspNetCoreDapr/ProviderScenarioEventHandler.cs deleted file mode 100644 index b26844d7c5..0000000000 --- a/test/DistEvents/DistDemoApp.AspNetCoreDapr/ProviderScenarioEventHandler.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.EventBus.Distributed; - -namespace DistDemoApp; - -public class ProviderScenarioEventHandler : - IDistributedEventHandler, - ITransientDependency -{ - public Task HandleEventAsync(ProviderScenarioEvent eventData) - { - Console.WriteLine($"Dapr ASP.NET Core handler received ProviderScenarioEvent: {eventData.Value}"); - return Task.CompletedTask; - } -} diff --git a/test/DistEvents/DistDemoApp.AspNetCoreDapr/appsettings.json b/test/DistEvents/DistDemoApp.AspNetCoreDapr/appsettings.json deleted file mode 100644 index 711c4a34f7..0000000000 --- a/test/DistEvents/DistDemoApp.AspNetCoreDapr/appsettings.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "ConnectionStrings": { - "Default": "mongodb://localhost:27017/DistEventsDemo?retryWrites=false" - }, - "Dapr": { - "HttpEndpoint": "http://localhost:3500" - }, - "DaprEventBus": { - "PubSubName": "pubsub" - }, - "Redis": { - "Configuration": "127.0.0.1" - } -} diff --git a/test/DistEvents/DistDemoApp.AzureEmulator/DistDemoApp.AzureEmulator.csproj b/test/DistEvents/DistDemoApp.AzureEmulator/DistDemoApp.AzureEmulator.csproj deleted file mode 100644 index d96ef9fda5..0000000000 --- a/test/DistEvents/DistDemoApp.AzureEmulator/DistDemoApp.AzureEmulator.csproj +++ /dev/null @@ -1,37 +0,0 @@ - - - - Exe - net10.0 - DistDemoApp - MongoDb - - - - - - - - - - - - - - - - - $(DefineConstants);DISTDEMO_USE_MONGODB - - - - $(DefineConstants);DISTDEMO_USE_EFCORE - - - - - Always - - - - diff --git a/test/DistEvents/DistDemoApp.AzureEmulator/DistDemoAppAzureEmulatorModule.cs b/test/DistEvents/DistDemoApp.AzureEmulator/DistDemoAppAzureEmulatorModule.cs deleted file mode 100644 index 2dd0c59c36..0000000000 --- a/test/DistEvents/DistDemoApp.AzureEmulator/DistDemoAppAzureEmulatorModule.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Azure.Messaging.ServiceBus; -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.EventBus.Azure; -using Volo.Abp.Modularity; -using Volo.Abp.AzureServiceBus; - -namespace DistDemoApp; - -#if DISTDEMO_USE_MONGODB -[DependsOn( - typeof(DistDemoAppMongoDbInfrastructureModule), - typeof(AbpEventBusAzureModule), - typeof(DistDemoAppSharedModule) -)] -#else -[DependsOn( - typeof(DistDemoAppEntityFrameworkCoreInfrastructureModule), - typeof(AbpEventBusAzureModule), - typeof(DistDemoAppSharedModule) -)] -#endif -public class DistDemoAppAzureEmulatorModule : AbpModule -{ - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.AddSingleton(); - context.Services.AddSingleton(); -#if DISTDEMO_USE_MONGODB - context.ConfigureDistDemoMongoInfrastructure(); -#else - context.ConfigureDistDemoEntityFrameworkInfrastructure(); -#endif - - Configure(options => - { - options.Connections.Default.ConnectionString = - "Endpoint=sb://localhost:5673;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"; - options.Connections.Default.Processor = new ServiceBusProcessorOptions - { - AutoCompleteMessages = false - }; - }); - - Configure(options => - { - options.ConnectionName = "Default"; - options.SubscriberName = "DistDemoAzureSubscriber"; - options.TopicName = "DistDemoAzureTopic"; - }); - } -} diff --git a/test/DistEvents/DistDemoApp.AzureEmulator/EmulatorProcessorPool.cs b/test/DistEvents/DistDemoApp.AzureEmulator/EmulatorProcessorPool.cs deleted file mode 100644 index 84891ae804..0000000000 --- a/test/DistEvents/DistDemoApp.AzureEmulator/EmulatorProcessorPool.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Threading.Tasks; -using Azure.Messaging.ServiceBus; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Volo.Abp.AzureServiceBus; -using Volo.Abp.DependencyInjection; - -namespace DistDemoApp; - -public class EmulatorProcessorPool : IProcessorPool, ISingletonDependency -{ - public ILogger Logger { get; set; } - - private bool _isDisposed; - private readonly AbpAzureServiceBusOptions _options; - private readonly IConnectionPool _connectionPool; - private readonly ConcurrentDictionary> _processors; - - public EmulatorProcessorPool( - IOptions options, - IConnectionPool connectionPool) - { - _options = options.Value; - _connectionPool = connectionPool; - _processors = new ConcurrentDictionary>(); - Logger = NullLogger.Instance; - } - - public Task GetAsync(string subscriptionName, string topicName, string connectionName) - { - var processor = _processors.GetOrAdd( - $"{topicName}-{subscriptionName}", - new Lazy(() => - { - var config = _options.Connections.GetOrDefault(connectionName); - var client = _connectionPool.GetClient(connectionName); - return client.CreateProcessor(topicName, subscriptionName, config.Processor); - }) - ).Value; - - return Task.FromResult(processor); - } - - public async ValueTask DisposeAsync() - { - if (_isDisposed) - { - return; - } - - _isDisposed = true; - if (!_processors.Any()) - { - return; - } - - foreach (var item in _processors.Values) - { - var processor = item.Value; - if (processor.IsProcessing) - { - await processor.StopProcessingAsync(); - } - - if (!processor.IsClosed) - { - await processor.CloseAsync(); - } - - await processor.DisposeAsync(); - } - - _processors.Clear(); - } -} diff --git a/test/DistEvents/DistDemoApp.AzureEmulator/EmulatorPublisherPool.cs b/test/DistEvents/DistDemoApp.AzureEmulator/EmulatorPublisherPool.cs deleted file mode 100644 index f3c9c0a854..0000000000 --- a/test/DistEvents/DistDemoApp.AzureEmulator/EmulatorPublisherPool.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Threading.Tasks; -using Azure.Messaging.ServiceBus; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Volo.Abp.AzureServiceBus; -using Volo.Abp.DependencyInjection; - -#nullable enable -namespace DistDemoApp; - -public class EmulatorPublisherPool : IPublisherPool, ISingletonDependency -{ - public ILogger Logger { get; set; } - - private bool _isDisposed; - private readonly IConnectionPool _connectionPool; - private readonly ConcurrentDictionary> _publishers; - - public EmulatorPublisherPool(IConnectionPool connectionPool) - { - _connectionPool = connectionPool; - _publishers = new ConcurrentDictionary>(); - Logger = NullLogger.Instance; - } - - public Task GetAsync(string topicName, string? connectionName) - { - var sender = _publishers.GetOrAdd( - topicName, - new Lazy(() => - { - var client = _connectionPool.GetClient(connectionName); - return client.CreateSender(topicName); - }) - ).Value; - - return Task.FromResult(sender); - } - - public async ValueTask DisposeAsync() - { - if (_isDisposed) - { - return; - } - - _isDisposed = true; - if (!_publishers.Any()) - { - return; - } - - foreach (var publisher in _publishers.Values) - { - await publisher.Value.CloseAsync(); - await publisher.Value.DisposeAsync(); - } - - _publishers.Clear(); - } -} diff --git a/test/DistEvents/DistDemoApp.AzureEmulator/Program.cs b/test/DistEvents/DistDemoApp.AzureEmulator/Program.cs deleted file mode 100644 index a5375dad49..0000000000 --- a/test/DistEvents/DistDemoApp.AzureEmulator/Program.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Serilog; -using Volo.Abp; -using Volo.Abp.Threading; - -namespace DistDemoApp; - -public class Program -{ - public static void Main(string[] args) - { - using var application = AbpApplicationFactory.Create(options => - { - options.UseAutofac(); - options.Services.AddSerilog((serviceProvider, configuration) => - { - }); - options.Services.AddLogging(c => c.AddSerilog()); - }); - - Log.Information("Starting DistDemoApp.AzureEmulator."); - - application.Initialize(); - - AsyncHelper.RunSync(() => application - .ServiceProvider - .GetRequiredService() - .RunAsync(DistEventScenarioProfile.AzureEmulator())); - - application.Shutdown(); - } -} diff --git a/test/DistEvents/DistDemoApp.AzureEmulator/appsettings.json b/test/DistEvents/DistDemoApp.AzureEmulator/appsettings.json deleted file mode 100644 index 4502f1e483..0000000000 --- a/test/DistEvents/DistDemoApp.AzureEmulator/appsettings.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "ConnectionStrings": { - "Default": "mongodb://localhost:27017/DistEventsDemo?retryWrites=false" - }, - "Azure": { - "ServiceBus": { - "Connections": { - "Default": { - "ConnectionString": "Endpoint=sb://localhost:5673;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;" - } - } - }, - "EventBus": { - "ConnectionName": "Default", - "SubscriberName": "DistDemoAzureSubscriber", - "TopicName": "DistDemoAzureTopic" - } - }, - "Redis": { - "Configuration": "127.0.0.1" - } -} diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/DistDemoApp.EfCoreRabbitMq.csproj b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/DistDemoApp.EfCoreRabbitMq.csproj deleted file mode 100644 index 156de09a79..0000000000 --- a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/DistDemoApp.EfCoreRabbitMq.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - Exe - net10.0 - DistDemoApp - EntityFrameworkCore - - - - - - - - - - - - - - - - - $(DefineConstants);DISTDEMO_USE_MONGODB - - - - $(DefineConstants);DISTDEMO_USE_EFCORE - - - - - runtime; build; native; contentfiles; analyzers - compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native - - - - - - Always - - - - diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/DistDemoAppEfCoreRabbitMqModule.cs b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/DistDemoAppEfCoreRabbitMqModule.cs deleted file mode 100644 index 363373441d..0000000000 --- a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/DistDemoAppEfCoreRabbitMqModule.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.EventBus.Distributed; -using Volo.Abp.EventBus.RabbitMq; -using Volo.Abp.Modularity; -using Volo.Abp.RabbitMQ; - -namespace DistDemoApp -{ -#if DISTDEMO_USE_MONGODB - [DependsOn( - typeof(DistDemoAppMongoDbInfrastructureModule), - typeof(AbpEventBusRabbitMqModule), - typeof(DistDemoAppSharedModule) - )] -#else - [DependsOn( - typeof(DistDemoAppEntityFrameworkCoreInfrastructureModule), - typeof(AbpEventBusRabbitMqModule), - typeof(DistDemoAppSharedModule) - )] -#endif - public class DistDemoAppEfCoreRabbitMqModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { -#if DISTDEMO_USE_MONGODB - context.ConfigureDistDemoMongoInfrastructure(); -#else - context.ConfigureDistDemoEntityFrameworkInfrastructure(); -#endif - - Configure(options => - { - // options.Outboxes.Configure(config => - // { - // config.UseDbContext(); - // }); - // - // options.Inboxes.Configure(config => - // { - // config.UseDbContext(); - // }); - }); - - Configure(options => - { - options.Connections.Default.HostName = "localhost"; - }); - - Configure(options => - { - options.ConnectionName = "Default"; - options.ClientName = "DistDemoApp"; - options.ExchangeName = "DistDemo"; - }); - - } - } -} \ No newline at end of file diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20210910152547_Added_Boxes_Initial.Designer.cs b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20210910152547_Added_Boxes_Initial.Designer.cs deleted file mode 100644 index 292cff66f9..0000000000 --- a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20210910152547_Added_Boxes_Initial.Designer.cs +++ /dev/null @@ -1,162 +0,0 @@ -// -using System; -using DistDemoApp; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Volo.Abp.EntityFrameworkCore; - -namespace DistDemoApp.Migrations -{ - [DbContext(typeof(TodoDbContext))] - [Migration("20210910152547_Added_Boxes_Initial")] - partial class Added_Boxes_Initial - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("ProductVersion", "5.0.9") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - modelBuilder.Entity("DistDemoApp.TodoItem", b => - { - b.Property("Id") - .HasColumnType("uniqueidentifier"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasMaxLength(40) - .HasColumnType("nvarchar(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("datetime2") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .HasColumnType("nvarchar(max)") - .HasColumnName("ExtraProperties"); - - b.Property("Text") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("nvarchar(128)"); - - b.HasKey("Id"); - - b.ToTable("TodoItems"); - }); - - modelBuilder.Entity("DistDemoApp.TodoSummary", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int") - .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasMaxLength(40) - .HasColumnType("nvarchar(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("Day") - .HasColumnType("tinyint"); - - b.Property("ExtraProperties") - .HasColumnType("nvarchar(max)") - .HasColumnName("ExtraProperties"); - - b.Property("Month") - .HasColumnType("tinyint"); - - b.Property("TotalCount") - .HasColumnType("int"); - - b.Property("Year") - .HasColumnType("int"); - - b.HasKey("Id"); - - b.ToTable("TodoSummaries"); - }); - - modelBuilder.Entity("Volo.Abp.EntityFrameworkCore.DistributedEvents.IncomingEventRecord", b => - { - b.Property("Id") - .HasColumnType("uniqueidentifier"); - - b.Property("CreationTime") - .HasColumnType("datetime2") - .HasColumnName("CreationTime"); - - b.Property("EventData") - .IsRequired() - .HasColumnType("varbinary(max)"); - - b.Property("EventName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); - - b.Property("ExtraProperties") - .HasColumnType("nvarchar(max)") - .HasColumnName("ExtraProperties"); - - b.Property("MessageId") - .HasColumnType("nvarchar(450)"); - - b.Property("Processed") - .HasColumnType("bit"); - - b.Property("ProcessedTime") - .HasColumnType("datetime2"); - - b.HasKey("Id"); - - b.HasIndex("MessageId"); - - b.HasIndex("Processed", "CreationTime"); - - b.ToTable("AbpEventInbox"); - }); - - modelBuilder.Entity("Volo.Abp.EntityFrameworkCore.DistributedEvents.OutgoingEventRecord", b => - { - b.Property("Id") - .HasColumnType("uniqueidentifier"); - - b.Property("CreationTime") - .HasColumnType("datetime2") - .HasColumnName("CreationTime"); - - b.Property("EventData") - .IsRequired() - .HasColumnType("varbinary(max)"); - - b.Property("EventName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); - - b.Property("ExtraProperties") - .HasColumnType("nvarchar(max)") - .HasColumnName("ExtraProperties"); - - b.HasKey("Id"); - - b.ToTable("AbpEventOutbox"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20210910152547_Added_Boxes_Initial.cs b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20210910152547_Added_Boxes_Initial.cs deleted file mode 100644 index 9094eaa8c9..0000000000 --- a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20210910152547_Added_Boxes_Initial.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace DistDemoApp.Migrations -{ - public partial class Added_Boxes_Initial : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "AbpEventInbox", - columns: table => new - { - Id = table.Column(type: "uniqueidentifier", nullable: false), - ExtraProperties = table.Column(type: "nvarchar(max)", nullable: true), - MessageId = table.Column(type: "nvarchar(450)", nullable: true), - EventName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), - EventData = table.Column(type: "varbinary(max)", nullable: false), - CreationTime = table.Column(type: "datetime2", nullable: false), - Processed = table.Column(type: "bit", nullable: false), - ProcessedTime = table.Column(type: "datetime2", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_AbpEventInbox", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "AbpEventOutbox", - columns: table => new - { - Id = table.Column(type: "uniqueidentifier", nullable: false), - ExtraProperties = table.Column(type: "nvarchar(max)", nullable: true), - EventName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), - EventData = table.Column(type: "varbinary(max)", nullable: false), - CreationTime = table.Column(type: "datetime2", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_AbpEventOutbox", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "TodoItems", - columns: table => new - { - Id = table.Column(type: "uniqueidentifier", nullable: false), - Text = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), - ExtraProperties = table.Column(type: "nvarchar(max)", nullable: true), - ConcurrencyStamp = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: true), - CreationTime = table.Column(type: "datetime2", nullable: false), - CreatorId = table.Column(type: "uniqueidentifier", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_TodoItems", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "TodoSummaries", - columns: table => new - { - Id = table.Column(type: "int", nullable: false) - .Annotation("SqlServer:Identity", "1, 1"), - Year = table.Column(type: "int", nullable: false), - Month = table.Column(type: "tinyint", nullable: false), - Day = table.Column(type: "tinyint", nullable: false), - TotalCount = table.Column(type: "int", nullable: false), - ExtraProperties = table.Column(type: "nvarchar(max)", nullable: true), - ConcurrencyStamp = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_TodoSummaries", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_AbpEventInbox_MessageId", - table: "AbpEventInbox", - column: "MessageId"); - - migrationBuilder.CreateIndex( - name: "IX_AbpEventInbox_Processed_CreationTime", - table: "AbpEventInbox", - columns: new[] { "Processed", "CreationTime" }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "AbpEventInbox"); - - migrationBuilder.DropTable( - name: "AbpEventOutbox"); - - migrationBuilder.DropTable( - name: "TodoItems"); - - migrationBuilder.DropTable( - name: "TodoSummaries"); - } - } -} diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20260304073807_Update-10.2.Designer.cs b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20260304073807_Update-10.2.Designer.cs deleted file mode 100644 index 718cebec33..0000000000 --- a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20260304073807_Update-10.2.Designer.cs +++ /dev/null @@ -1,182 +0,0 @@ -// -using System; -using DistDemoApp; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Volo.Abp.EntityFrameworkCore; - -#nullable disable - -namespace DistDemoApp.Migrations -{ - [DbContext(typeof(TodoDbContext))] - [Migration("20260304073807_Update-10.2")] - partial class Update102 - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) - .HasAnnotation("ProductVersion", "10.0.2") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("DistDemoApp.TodoItem", b => - { - b.Property("Id") - .HasColumnType("uniqueidentifier"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("nvarchar(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("datetime2") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasColumnName("ExtraProperties"); - - b.Property("Text") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("nvarchar(128)"); - - b.HasKey("Id"); - - b.ToTable("TodoItems"); - }); - - modelBuilder.Entity("DistDemoApp.TodoSummary", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("nvarchar(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("Day") - .HasColumnType("tinyint"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasColumnName("ExtraProperties"); - - b.Property("Month") - .HasColumnType("tinyint"); - - b.Property("TotalCount") - .HasColumnType("int"); - - b.Property("Year") - .HasColumnType("int"); - - b.HasKey("Id"); - - b.ToTable("TodoSummaries"); - }); - - modelBuilder.Entity("Volo.Abp.EntityFrameworkCore.DistributedEvents.IncomingEventRecord", b => - { - b.Property("Id") - .HasColumnType("uniqueidentifier"); - - b.Property("CreationTime") - .HasColumnType("datetime2") - .HasColumnName("CreationTime"); - - b.Property("EventData") - .IsRequired() - .HasColumnType("varbinary(max)"); - - b.Property("EventName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasColumnName("ExtraProperties"); - - b.Property("HandledTime") - .HasColumnType("datetime2"); - - b.Property("MessageId") - .IsRequired() - .HasColumnType("nvarchar(450)"); - - b.Property("NextRetryTime") - .HasColumnType("datetime2"); - - b.Property("RetryCount") - .HasColumnType("int"); - - b.Property("Status") - .HasColumnType("int"); - - b.HasKey("Id"); - - b.HasIndex("MessageId"); - - b.HasIndex("Status", "CreationTime"); - - b.ToTable("AbpEventInbox", (string)null); - }); - - modelBuilder.Entity("Volo.Abp.EntityFrameworkCore.DistributedEvents.OutgoingEventRecord", b => - { - b.Property("Id") - .HasColumnType("uniqueidentifier"); - - b.Property("CreationTime") - .HasColumnType("datetime2") - .HasColumnName("CreationTime"); - - b.Property("EventData") - .IsRequired() - .HasColumnType("varbinary(max)"); - - b.Property("EventName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasColumnName("ExtraProperties"); - - b.HasKey("Id"); - - b.HasIndex("CreationTime"); - - b.ToTable("AbpEventOutbox", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20260304073807_Update-10.2.cs b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20260304073807_Update-10.2.cs deleted file mode 100644 index a3bcf90174..0000000000 --- a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20260304073807_Update-10.2.cs +++ /dev/null @@ -1,233 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace DistDemoApp.Migrations -{ - /// - public partial class Update102 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_AbpEventInbox_Processed_CreationTime", - table: "AbpEventInbox"); - - migrationBuilder.DropColumn( - name: "Processed", - table: "AbpEventInbox"); - - migrationBuilder.RenameColumn( - name: "ProcessedTime", - table: "AbpEventInbox", - newName: "NextRetryTime"); - - migrationBuilder.AlterColumn( - name: "ExtraProperties", - table: "TodoSummaries", - type: "nvarchar(max)", - nullable: false, - defaultValue: "", - oldClrType: typeof(string), - oldType: "nvarchar(max)", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ConcurrencyStamp", - table: "TodoSummaries", - type: "nvarchar(40)", - maxLength: 40, - nullable: false, - defaultValue: "", - oldClrType: typeof(string), - oldType: "nvarchar(40)", - oldMaxLength: 40, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ExtraProperties", - table: "TodoItems", - type: "nvarchar(max)", - nullable: false, - defaultValue: "", - oldClrType: typeof(string), - oldType: "nvarchar(max)", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ConcurrencyStamp", - table: "TodoItems", - type: "nvarchar(40)", - maxLength: 40, - nullable: false, - defaultValue: "", - oldClrType: typeof(string), - oldType: "nvarchar(40)", - oldMaxLength: 40, - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ExtraProperties", - table: "AbpEventOutbox", - type: "nvarchar(max)", - nullable: false, - defaultValue: "", - oldClrType: typeof(string), - oldType: "nvarchar(max)", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "MessageId", - table: "AbpEventInbox", - type: "nvarchar(450)", - nullable: false, - defaultValue: "", - oldClrType: typeof(string), - oldType: "nvarchar(450)", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ExtraProperties", - table: "AbpEventInbox", - type: "nvarchar(max)", - nullable: false, - defaultValue: "", - oldClrType: typeof(string), - oldType: "nvarchar(max)", - oldNullable: true); - - migrationBuilder.AddColumn( - name: "HandledTime", - table: "AbpEventInbox", - type: "datetime2", - nullable: true); - - migrationBuilder.AddColumn( - name: "RetryCount", - table: "AbpEventInbox", - type: "int", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "Status", - table: "AbpEventInbox", - type: "int", - nullable: false, - defaultValue: 0); - - migrationBuilder.CreateIndex( - name: "IX_AbpEventOutbox_CreationTime", - table: "AbpEventOutbox", - column: "CreationTime"); - - migrationBuilder.CreateIndex( - name: "IX_AbpEventInbox_Status_CreationTime", - table: "AbpEventInbox", - columns: new[] { "Status", "CreationTime" }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_AbpEventOutbox_CreationTime", - table: "AbpEventOutbox"); - - migrationBuilder.DropIndex( - name: "IX_AbpEventInbox_Status_CreationTime", - table: "AbpEventInbox"); - - migrationBuilder.DropColumn( - name: "HandledTime", - table: "AbpEventInbox"); - - migrationBuilder.DropColumn( - name: "RetryCount", - table: "AbpEventInbox"); - - migrationBuilder.DropColumn( - name: "Status", - table: "AbpEventInbox"); - - migrationBuilder.RenameColumn( - name: "NextRetryTime", - table: "AbpEventInbox", - newName: "ProcessedTime"); - - migrationBuilder.AlterColumn( - name: "ExtraProperties", - table: "TodoSummaries", - type: "nvarchar(max)", - nullable: true, - oldClrType: typeof(string), - oldType: "nvarchar(max)"); - - migrationBuilder.AlterColumn( - name: "ConcurrencyStamp", - table: "TodoSummaries", - type: "nvarchar(40)", - maxLength: 40, - nullable: true, - oldClrType: typeof(string), - oldType: "nvarchar(40)", - oldMaxLength: 40); - - migrationBuilder.AlterColumn( - name: "ExtraProperties", - table: "TodoItems", - type: "nvarchar(max)", - nullable: true, - oldClrType: typeof(string), - oldType: "nvarchar(max)"); - - migrationBuilder.AlterColumn( - name: "ConcurrencyStamp", - table: "TodoItems", - type: "nvarchar(40)", - maxLength: 40, - nullable: true, - oldClrType: typeof(string), - oldType: "nvarchar(40)", - oldMaxLength: 40); - - migrationBuilder.AlterColumn( - name: "ExtraProperties", - table: "AbpEventOutbox", - type: "nvarchar(max)", - nullable: true, - oldClrType: typeof(string), - oldType: "nvarchar(max)"); - - migrationBuilder.AlterColumn( - name: "MessageId", - table: "AbpEventInbox", - type: "nvarchar(450)", - nullable: true, - oldClrType: typeof(string), - oldType: "nvarchar(450)"); - - migrationBuilder.AlterColumn( - name: "ExtraProperties", - table: "AbpEventInbox", - type: "nvarchar(max)", - nullable: true, - oldClrType: typeof(string), - oldType: "nvarchar(max)"); - - migrationBuilder.AddColumn( - name: "Processed", - table: "AbpEventInbox", - type: "bit", - nullable: false, - defaultValue: false); - - migrationBuilder.CreateIndex( - name: "IX_AbpEventInbox_Processed_CreationTime", - table: "AbpEventInbox", - columns: new[] { "Processed", "CreationTime" }); - } - } -} diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/TodoDbContextModelSnapshot.cs b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/TodoDbContextModelSnapshot.cs deleted file mode 100644 index 1f12a89a1c..0000000000 --- a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/TodoDbContextModelSnapshot.cs +++ /dev/null @@ -1,179 +0,0 @@ -// -using System; -using DistDemoApp; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Volo.Abp.EntityFrameworkCore; - -#nullable disable - -namespace DistDemoApp.Migrations -{ - [DbContext(typeof(TodoDbContext))] - partial class TodoDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) - .HasAnnotation("ProductVersion", "10.0.2") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("DistDemoApp.TodoItem", b => - { - b.Property("Id") - .HasColumnType("uniqueidentifier"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("nvarchar(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("CreationTime") - .HasColumnType("datetime2") - .HasColumnName("CreationTime"); - - b.Property("CreatorId") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatorId"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasColumnName("ExtraProperties"); - - b.Property("Text") - .IsRequired() - .HasMaxLength(128) - .HasColumnType("nvarchar(128)"); - - b.HasKey("Id"); - - b.ToTable("TodoItems"); - }); - - modelBuilder.Entity("DistDemoApp.TodoSummary", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .IsRequired() - .HasMaxLength(40) - .HasColumnType("nvarchar(40)") - .HasColumnName("ConcurrencyStamp"); - - b.Property("Day") - .HasColumnType("tinyint"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasColumnName("ExtraProperties"); - - b.Property("Month") - .HasColumnType("tinyint"); - - b.Property("TotalCount") - .HasColumnType("int"); - - b.Property("Year") - .HasColumnType("int"); - - b.HasKey("Id"); - - b.ToTable("TodoSummaries"); - }); - - modelBuilder.Entity("Volo.Abp.EntityFrameworkCore.DistributedEvents.IncomingEventRecord", b => - { - b.Property("Id") - .HasColumnType("uniqueidentifier"); - - b.Property("CreationTime") - .HasColumnType("datetime2") - .HasColumnName("CreationTime"); - - b.Property("EventData") - .IsRequired() - .HasColumnType("varbinary(max)"); - - b.Property("EventName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasColumnName("ExtraProperties"); - - b.Property("HandledTime") - .HasColumnType("datetime2"); - - b.Property("MessageId") - .IsRequired() - .HasColumnType("nvarchar(450)"); - - b.Property("NextRetryTime") - .HasColumnType("datetime2"); - - b.Property("RetryCount") - .HasColumnType("int"); - - b.Property("Status") - .HasColumnType("int"); - - b.HasKey("Id"); - - b.HasIndex("MessageId"); - - b.HasIndex("Status", "CreationTime"); - - b.ToTable("AbpEventInbox", (string)null); - }); - - modelBuilder.Entity("Volo.Abp.EntityFrameworkCore.DistributedEvents.OutgoingEventRecord", b => - { - b.Property("Id") - .HasColumnType("uniqueidentifier"); - - b.Property("CreationTime") - .HasColumnType("datetime2") - .HasColumnName("CreationTime"); - - b.Property("EventData") - .IsRequired() - .HasColumnType("varbinary(max)"); - - b.Property("EventName") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); - - b.Property("ExtraProperties") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasColumnName("ExtraProperties"); - - b.HasKey("Id"); - - b.HasIndex("CreationTime"); - - b.ToTable("AbpEventOutbox", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Program.cs b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Program.cs deleted file mode 100644 index 3cee486e96..0000000000 --- a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/Program.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Serilog; -using Volo.Abp; -using Volo.Abp.Threading; - -namespace DistDemoApp -{ - public class Program - { - public static void Main(string[] args) - { - using (var application = AbpApplicationFactory.Create(options => - { - options.UseAutofac(); - options.Services.AddSerilog((_, _) => - { - }); - options.Services.AddLogging(c => c.AddSerilog()); - })) - { - Log.Information("Starting DistDemoApp.EfCoreRabbitMq."); - application.Initialize(); - - AsyncHelper.RunSync( - () => application - .ServiceProvider - .GetRequiredService().CreateTodoItemAsync() - ); - - application.Shutdown(); - } - } - } -} diff --git a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/appsettings.json b/test/DistEvents/DistDemoApp.EfCoreRabbitMq/appsettings.json deleted file mode 100644 index 91cc668167..0000000000 --- a/test/DistEvents/DistDemoApp.EfCoreRabbitMq/appsettings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "ConnectionStrings": { - "Default": "Server=localhost,1433;Database=DistEventsDemo;User Id=sa;Password=AbpDemo_123456;TrustServerCertificate=True" - }, - "RabbitMQ": { - "Connections": { - "Default": { - "HostName": "localhost" - } - }, - "EventBus": { - "ClientName": "DistDemoApp", - "ExchangeName": "DistDemo" - } - }, - "Redis": { - "Configuration": "127.0.0.1" - } -} diff --git a/test/DistEvents/DistDemoApp.MongoDbKafka/DistDemoApp.MongoDbKafka.csproj b/test/DistEvents/DistDemoApp.MongoDbKafka/DistDemoApp.MongoDbKafka.csproj deleted file mode 100644 index ad384a51dd..0000000000 --- a/test/DistEvents/DistDemoApp.MongoDbKafka/DistDemoApp.MongoDbKafka.csproj +++ /dev/null @@ -1,37 +0,0 @@ - - - - Exe - net10.0 - DistDemoApp - MongoDb - - - - - - - - - - - - - - - - - $(DefineConstants);DISTDEMO_USE_MONGODB - - - - $(DefineConstants);DISTDEMO_USE_EFCORE - - - - - Always - - - - diff --git a/test/DistEvents/DistDemoApp.MongoDbKafka/DistDemoAppMongoDbKafkaModule.cs b/test/DistEvents/DistDemoApp.MongoDbKafka/DistDemoAppMongoDbKafkaModule.cs deleted file mode 100644 index b9439da27b..0000000000 --- a/test/DistEvents/DistDemoApp.MongoDbKafka/DistDemoAppMongoDbKafkaModule.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Volo.Abp.EventBus.Kafka; -using Volo.Abp.Kafka; -using Volo.Abp.Modularity; - -namespace DistDemoApp -{ -#if DISTDEMO_USE_MONGODB - [DependsOn( - typeof(DistDemoAppMongoDbInfrastructureModule), - typeof(AbpEventBusKafkaModule), - typeof(DistDemoAppSharedModule) - )] -#else - [DependsOn( - typeof(DistDemoAppEntityFrameworkCoreInfrastructureModule), - typeof(AbpEventBusKafkaModule), - typeof(DistDemoAppSharedModule) - )] -#endif - public class DistDemoAppMongoDbKafkaModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { -#if DISTDEMO_USE_MONGODB - context.ConfigureDistDemoMongoInfrastructure(); -#else - context.ConfigureDistDemoEntityFrameworkInfrastructure(); -#endif - - Configure(options => - { - options.Connections.Default.BootstrapServers = "localhost:9092"; - }); - - Configure(options => - { - options.ConnectionName = "Default"; - options.TopicName = "DistDemoTopic"; - options.GroupId = "DistDemoApp"; - }); - } - } -} diff --git a/test/DistEvents/DistDemoApp.MongoDbKafka/Program.cs b/test/DistEvents/DistDemoApp.MongoDbKafka/Program.cs deleted file mode 100644 index 4d324bce0a..0000000000 --- a/test/DistEvents/DistDemoApp.MongoDbKafka/Program.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Serilog; -using Volo.Abp; -using Volo.Abp.Threading; - -namespace DistDemoApp -{ - public class Program - { - public static void Main(string[] args) - { - using (var application = AbpApplicationFactory.Create(options => - { - options.UseAutofac(); - options.Services.AddSerilog((_, _) => - { - }); - options.Services.AddLogging(c => c.AddSerilog()); - })) - { - Log.Information("Starting DistDemoApp.MongoDbKafka."); - - application.Initialize(); - - AsyncHelper.RunSync( - () => application - .ServiceProvider - .GetRequiredService().CreateTodoItemAsync() - ); - - application.Shutdown(); - } - - } - } -} diff --git a/test/DistEvents/DistDemoApp.MongoDbKafka/appsettings.json b/test/DistEvents/DistDemoApp.MongoDbKafka/appsettings.json deleted file mode 100644 index d6d1351b25..0000000000 --- a/test/DistEvents/DistDemoApp.MongoDbKafka/appsettings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "ConnectionStrings": { - "Default": "mongodb://localhost:27017/DistEventsDemo?retryWrites=false" - }, - "Kafka": { - "Connections": { - "Default": { - "BootstrapServers": "localhost:9092" - } - }, - "EventBus": { - "GroupId": "DistDemoApp", - "TopicName": "DistDemoTopic" - } - }, - "Redis": { - "Configuration": "127.0.0.1" - } -} diff --git a/test/DistEvents/DistDemoApp.MongoDbRebus/DistDemoApp.MongoDbRebus.csproj b/test/DistEvents/DistDemoApp.MongoDbRebus/DistDemoApp.MongoDbRebus.csproj deleted file mode 100644 index 49b6d118d2..0000000000 --- a/test/DistEvents/DistDemoApp.MongoDbRebus/DistDemoApp.MongoDbRebus.csproj +++ /dev/null @@ -1,37 +0,0 @@ - - - - Exe - net10.0 - DistDemoApp - MongoDb - - - - - - - - - - - - - - - - - $(DefineConstants);DISTDEMO_USE_MONGODB - - - - $(DefineConstants);DISTDEMO_USE_EFCORE - - - - - Always - - - - diff --git a/test/DistEvents/DistDemoApp.MongoDbRebus/DistDemoAppMongoDbRebusModule.cs b/test/DistEvents/DistDemoApp.MongoDbRebus/DistDemoAppMongoDbRebusModule.cs deleted file mode 100644 index 702ffc3f62..0000000000 --- a/test/DistEvents/DistDemoApp.MongoDbRebus/DistDemoAppMongoDbRebusModule.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Rebus.Transport.InMem; -using Volo.Abp.EventBus.Rebus; -using Volo.Abp.Modularity; - -namespace DistDemoApp -{ -#if DISTDEMO_USE_MONGODB - [DependsOn( - typeof(DistDemoAppMongoDbInfrastructureModule), - typeof(AbpEventBusRebusModule), - typeof(DistDemoAppSharedModule) - )] -#else - [DependsOn( - typeof(DistDemoAppEntityFrameworkCoreInfrastructureModule), - typeof(AbpEventBusRebusModule), - typeof(DistDemoAppSharedModule) - )] -#endif - public class DistDemoAppMongoDbRebusModule : AbpModule - { - public override void PreConfigureServices(ServiceConfigurationContext context) - { - PreConfigure(options => - { - options.InputQueueName = "eventbus"; - options.Configurer = rebusConfigurer => - { - rebusConfigurer.Transport(t => t.UseInMemoryTransport(new InMemNetwork(), "eventbus")); - }; - }); - } - - public override void ConfigureServices(ServiceConfigurationContext context) - { -#if DISTDEMO_USE_MONGODB - context.ConfigureDistDemoMongoInfrastructure(); -#else - context.ConfigureDistDemoEntityFrameworkInfrastructure(); -#endif - } - } -} diff --git a/test/DistEvents/DistDemoApp.MongoDbRebus/Program.cs b/test/DistEvents/DistDemoApp.MongoDbRebus/Program.cs deleted file mode 100644 index 17a11b5f17..0000000000 --- a/test/DistEvents/DistDemoApp.MongoDbRebus/Program.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Serilog; -using Volo.Abp; -using Volo.Abp.Threading; - -namespace DistDemoApp -{ - public class Program - { - public static void Main(string[] args) - { - using (var application = AbpApplicationFactory.Create(options => - { - options.UseAutofac(); - options.Services.AddSerilog((_, _) => - { - }); - options.Services.AddLogging(c => c.AddSerilog()); - })) - { - Log.Information("Starting DistDemoApp.MongoDbRebus."); - - application.Initialize(); - - AsyncHelper.RunSync( - () => application - .ServiceProvider - .GetRequiredService().CreateTodoItemAsync() - ); - - application.Shutdown(); - } - - } - } -} diff --git a/test/DistEvents/DistDemoApp.MongoDbRebus/appsettings.json b/test/DistEvents/DistDemoApp.MongoDbRebus/appsettings.json deleted file mode 100644 index d6d1351b25..0000000000 --- a/test/DistEvents/DistDemoApp.MongoDbRebus/appsettings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "ConnectionStrings": { - "Default": "mongodb://localhost:27017/DistEventsDemo?retryWrites=false" - }, - "Kafka": { - "Connections": { - "Default": { - "BootstrapServers": "localhost:9092" - } - }, - "EventBus": { - "GroupId": "DistDemoApp", - "TopicName": "DistDemoTopic" - } - }, - "Redis": { - "Configuration": "127.0.0.1" - } -} diff --git a/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoApp.Persistence.EntityFrameworkCore.csproj b/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoApp.Persistence.EntityFrameworkCore.csproj deleted file mode 100644 index 2adc9e976e..0000000000 --- a/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoApp.Persistence.EntityFrameworkCore.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net10.0 - DistDemoApp - EntityFrameworkCore - - - - - - - - - - - - diff --git a/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoAppEntityFrameworkCoreInfrastructureModule.cs b/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoAppEntityFrameworkCoreInfrastructureModule.cs deleted file mode 100644 index 1c7c094953..0000000000 --- a/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoAppEntityFrameworkCoreInfrastructureModule.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Volo.Abp.EntityFrameworkCore.SqlServer; -using Volo.Abp.Modularity; - -namespace DistDemoApp; - -[DependsOn( - typeof(AbpEntityFrameworkCoreSqlServerModule), - typeof(DistDemoAppSharedModule) -)] -public class DistDemoAppEntityFrameworkCoreInfrastructureModule : AbpModule -{ -} diff --git a/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoEntityFrameworkServiceCollectionExtensions.cs b/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoEntityFrameworkServiceCollectionExtensions.cs deleted file mode 100644 index f2aa5f874e..0000000000 --- a/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/DistDemoEntityFrameworkServiceCollectionExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.EntityFrameworkCore; -using Volo.Abp.Modularity; - -namespace DistDemoApp; - -public static class DistDemoEntityFrameworkServiceCollectionExtensions -{ - public static void ConfigureDistDemoEntityFrameworkInfrastructure(this ServiceConfigurationContext context) - { - context.Services.AddAbpDbContext(options => - { - options.AddDefaultRepositories(); - }); - - context.Services.Configure(options => - { - options.UseSqlServer(); - }); - } -} diff --git a/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/TodoDbContext.cs b/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/TodoDbContext.cs deleted file mode 100644 index a352e4a5b0..0000000000 --- a/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/TodoDbContext.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Volo.Abp.EntityFrameworkCore; -using Volo.Abp.EntityFrameworkCore.DistributedEvents; - -namespace DistDemoApp; - -public class TodoDbContext : AbpDbContext, IHasEventOutbox, IHasEventInbox -{ - public DbSet TodoItems { get; set; } = null!; - - public DbSet TodoSummaries { get; set; } = null!; - - public DbSet OutgoingEvents { get; set; } = null!; - - public DbSet IncomingEvents { get; set; } = null!; - - public TodoDbContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.ConfigureEventOutbox(); - modelBuilder.ConfigureEventInbox(); - - modelBuilder.Entity(b => - { - b.Property(x => x.Text).IsRequired().HasMaxLength(128); - }); - } -} diff --git a/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/TodoDbContextFactory.cs b/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/TodoDbContextFactory.cs deleted file mode 100644 index 082c85bb29..0000000000 --- a/test/DistEvents/DistDemoApp.Persistence.EntityFrameworkCore/TodoDbContextFactory.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.IO; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; -using Microsoft.Extensions.Configuration; - -namespace DistDemoApp; - -public class TodoDbContextFactory : IDesignTimeDbContextFactory -{ - public TodoDbContext CreateDbContext(string[] args) - { - var configuration = BuildConfiguration(); - - var builder = new DbContextOptionsBuilder() - .UseSqlServer(configuration.GetConnectionString("Default")); - - return new TodoDbContext(builder.Options); - } - - private static IConfigurationRoot BuildConfiguration() - { - var builder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", optional: false); - - return builder.Build(); - } -} diff --git a/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoApp.Persistence.MongoDb.csproj b/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoApp.Persistence.MongoDb.csproj deleted file mode 100644 index 8c9e3e14c5..0000000000 --- a/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoApp.Persistence.MongoDb.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net10.0 - DistDemoApp - MongoDb - - - - - - - - diff --git a/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoAppMongoDbInfrastructureModule.cs b/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoAppMongoDbInfrastructureModule.cs deleted file mode 100644 index 390badca6f..0000000000 --- a/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoAppMongoDbInfrastructureModule.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Volo.Abp.Modularity; -using Volo.Abp.MongoDB; - -namespace DistDemoApp; - -[DependsOn( - typeof(AbpMongoDbModule), - typeof(DistDemoAppSharedModule) -)] -public class DistDemoAppMongoDbInfrastructureModule : AbpModule -{ -} diff --git a/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoMongoDbContext.cs b/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoMongoDbContext.cs deleted file mode 100644 index 4c63400dc3..0000000000 --- a/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoMongoDbContext.cs +++ /dev/null @@ -1,26 +0,0 @@ -using MongoDB.Driver; -using Volo.Abp.Data; -using Volo.Abp.MongoDB; -using Volo.Abp.MongoDB.DistributedEvents; - -namespace DistDemoApp; - -[ConnectionStringName("Default")] -public class DistDemoMongoDbContext : AbpMongoDbContext, IHasEventOutbox, IHasEventInbox -{ - public IMongoCollection TodoItems => Collection(); - - public IMongoCollection TodoSummaries => Collection(); - - public IMongoCollection OutgoingEvents - { - get => Collection(); - set { } - } - - public IMongoCollection IncomingEvents - { - get => Collection(); - set { } - } -} diff --git a/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoMongoServiceCollectionExtensions.cs b/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoMongoServiceCollectionExtensions.cs deleted file mode 100644 index 48a8285b54..0000000000 --- a/test/DistEvents/DistDemoApp.Persistence.MongoDb/DistDemoMongoServiceCollectionExtensions.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Data; -using Volo.Abp.EventBus.Distributed; -using Volo.Abp.Modularity; -using Volo.Abp.MongoDB; -using Volo.Abp.MongoDB.DistributedEvents; - -namespace DistDemoApp; - -public static class DistDemoMongoServiceCollectionExtensions -{ - private const string DefaultMongoConnectionString = "mongodb://localhost:27017/DistEventsDemo?retryWrites=false"; - - public static void ConfigureDistDemoMongoInfrastructure(this ServiceConfigurationContext context) - { - context.Services.AddMongoDbContext(options => - { - options.AddDefaultRepositories(); - }); - - context.Services.Configure(options => - { - if (string.IsNullOrWhiteSpace(options.ConnectionStrings.Default)) - { - options.ConnectionStrings.Default = DefaultMongoConnectionString; - } - }); - - context.Services.Configure(options => - { - options.Outboxes.Configure(config => - { - config.UseMongoDbContext(); - }); - - options.Inboxes.Configure(config => - { - config.UseMongoDbContext(); - }); - }); - } -} diff --git a/test/DistEvents/DistDemoApp.Shared/DemoService.cs b/test/DistEvents/DistDemoApp.Shared/DemoService.cs deleted file mode 100644 index cc6d6f64c6..0000000000 --- a/test/DistEvents/DistDemoApp.Shared/DemoService.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; - -namespace DistDemoApp -{ - public class DemoService : ITransientDependency - { - private readonly IDistEventScenarioRunner _scenarioRunner; - - public DemoService(IDistEventScenarioRunner scenarioRunner) - { - _scenarioRunner = scenarioRunner; - } - - public virtual async Task CreateTodoItemAsync() - { - await _scenarioRunner.RunAsync(DistEventScenarioProfile.Default()); - } - } -} \ No newline at end of file diff --git a/test/DistEvents/DistDemoApp.Shared/DistDemoApp.Shared.csproj b/test/DistEvents/DistDemoApp.Shared/DistDemoApp.Shared.csproj deleted file mode 100644 index 36d4d635ce..0000000000 --- a/test/DistEvents/DistDemoApp.Shared/DistDemoApp.Shared.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - net10.0 - DistDemoApp - - - - - - - - - - - - - - - - - - diff --git a/test/DistEvents/DistDemoApp.Shared/DistDemoAppHostedService.cs b/test/DistEvents/DistDemoApp.Shared/DistDemoAppHostedService.cs deleted file mode 100644 index 90f27cb638..0000000000 --- a/test/DistEvents/DistDemoApp.Shared/DistDemoAppHostedService.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Volo.Abp; - -namespace DistDemoApp -{ - public class DistDemoAppHostedService : IHostedService - { - private readonly IAbpApplicationWithExternalServiceProvider _application; - private readonly IServiceProvider _serviceProvider; - private readonly DemoService _demoService; - private readonly IHostApplicationLifetime _hostApplicationLifetime; - - public DistDemoAppHostedService( - IAbpApplicationWithExternalServiceProvider application, - IServiceProvider serviceProvider, - DemoService demoService, - IHostApplicationLifetime hostApplicationLifetime) - { - _application = application; - _serviceProvider = serviceProvider; - _demoService = demoService; - _hostApplicationLifetime = hostApplicationLifetime; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - await _demoService.CreateTodoItemAsync(); - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _application.Shutdown(); - - return Task.CompletedTask; - } - } -} diff --git a/test/DistEvents/DistDemoApp.Shared/DistDemoAppSharedModule.cs b/test/DistEvents/DistDemoApp.Shared/DistDemoAppSharedModule.cs deleted file mode 100644 index 09c458bf85..0000000000 --- a/test/DistEvents/DistDemoApp.Shared/DistDemoAppSharedModule.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Medallion.Threading; -using Medallion.Threading.Redis; -using Microsoft.Extensions.DependencyInjection; -using StackExchange.Redis; -using Volo.Abp.Autofac; -using Volo.Abp.Domain; -using Volo.Abp.Domain.Entities.Events.Distributed; -using Volo.Abp.EventBus; -using Volo.Abp.Modularity; - -namespace DistDemoApp -{ - [DependsOn( - typeof(AbpAutofacModule), - typeof(AbpDddDomainModule), - typeof(AbpEventBusModule) - )] - public class DistDemoAppSharedModule : AbpModule - { - public override void ConfigureServices(ServiceConfigurationContext context) - { - var configuration = context.Services.GetConfiguration(); - - // context.Services.AddHostedService(); - - Configure(options => - { - options.EtoMappings.Add(); - options.AutoEventSelectors.Add(); - }); - - context.Services.AddSingleton(sp => - { - var connection = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]); - return new RedisDistributedSynchronizationProvider(connection.GetDatabase()); - }); - } - } -} diff --git a/test/DistEvents/DistDemoApp.Shared/DistEventScenarioProfile.cs b/test/DistEvents/DistDemoApp.Shared/DistEventScenarioProfile.cs deleted file mode 100644 index c6239cff31..0000000000 --- a/test/DistEvents/DistDemoApp.Shared/DistEventScenarioProfile.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace DistDemoApp; - -public class DistEventScenarioProfile -{ - public string Name { get; set; } = "default"; - - public string DynamicOnlyEventName { get; set; } = "dist-demo.dynamic-only"; - - public string DynamicOnlyMessage { get; set; } = "hello-dynamic"; - - public int TypedFromTypedValue { get; set; } = 7; - - public int TypedFromDynamicValue { get; set; } = 11; - - public bool EnableTypedFromTypedScenario { get; set; } = true; - - public bool EnableTypedFromDynamicScenario { get; set; } = true; - - public bool EnableDynamicOnlyScenario { get; set; } = true; - - public bool OnUnitOfWorkComplete { get; set; } = true; - - public bool UseOutbox { get; set; } = true; - - public bool UseUnitOfWork { get; set; } = true; - - public int WarmupDelayMs { get; set; } = 1500; - - public int TimeoutSeconds { get; set; } = 60; - - public static DistEventScenarioProfile Default() - { - return new DistEventScenarioProfile(); - } - - public static DistEventScenarioProfile DaprWeb() - { - return new DistEventScenarioProfile - { - Name = "dapr-web", - DynamicOnlyEventName = "dist-demo.dapr.dynamic-only", - DynamicOnlyMessage = "hello-dapr-web", - EnableTypedFromTypedScenario = false, - EnableTypedFromDynamicScenario = false, - EnableDynamicOnlyScenario = false - }; - } - - public static DistEventScenarioProfile AzureEmulator() - { - return new DistEventScenarioProfile - { - Name = "azure-emulator", - DynamicOnlyEventName = "DistDemoApp.Azure.DynamicOnly", - DynamicOnlyMessage = "hello-azure-emulator", - TypedFromTypedValue = 21, - TypedFromDynamicValue = 34 - }; - } -} diff --git a/test/DistEvents/DistDemoApp.Shared/DistEventScenarioRunner.cs b/test/DistEvents/DistDemoApp.Shared/DistEventScenarioRunner.cs deleted file mode 100644 index a4015a0012..0000000000 --- a/test/DistEvents/DistDemoApp.Shared/DistEventScenarioRunner.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.EventBus; -using Volo.Abp.EventBus.Distributed; -using Volo.Abp.Uow; - -namespace DistDemoApp; - -public class DistEventScenarioRunner : IDistEventScenarioRunner, ITransientDependency -{ - private readonly IDistributedEventBus _distributedEventBus; - private readonly IUnitOfWorkManager _unitOfWorkManager; - - public DistEventScenarioRunner( - IDistributedEventBus distributedEventBus, - IUnitOfWorkManager unitOfWorkManager) - { - _distributedEventBus = distributedEventBus; - _unitOfWorkManager = unitOfWorkManager; - } - - public async Task RunAsync(DistEventScenarioProfile profile) - { - var typedEventName = EventNameAttribute.GetNameOrDefault(); - - var typedFromTypedPublish = profile.EnableTypedFromTypedScenario - ? new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) - : null; - var typedFromDynamicPublish = profile.EnableTypedFromDynamicScenario - ? new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) - : null; - var dynamicOnlyPublish = profile.EnableDynamicOnlyScenario - ? new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) - : null; - - using var typedSubscription = _distributedEventBus.Subscribe(eventData => - { - if (typedFromTypedPublish != null && eventData.Value == profile.TypedFromTypedValue) - { - typedFromTypedPublish.TrySetResult(eventData.Value); - } - - if (typedFromDynamicPublish != null && eventData.Value == profile.TypedFromDynamicValue) - { - typedFromDynamicPublish.TrySetResult(eventData.Value); - } - - return Task.CompletedTask; - }); - - IDisposable? dynamicOnlySubscription = null; - if (profile.EnableDynamicOnlyScenario) - { - dynamicOnlySubscription = _distributedEventBus.Subscribe( - profile.DynamicOnlyEventName, - new SingleInstanceHandlerFactory( - new ActionEventHandler(eventData => - { - var converted = DynamicEventDataConverter.ConvertToLooseObject(eventData); - if (converted is Dictionary payload && - payload.TryGetValue("Message", out var message) && - message?.ToString() == profile.DynamicOnlyMessage) - { - dynamicOnlyPublish!.TrySetResult(true); - } - - return Task.CompletedTask; - }))); - } - - await Task.Delay(profile.WarmupDelayMs); - - if (profile.UseUnitOfWork) - { - using var uow = _unitOfWorkManager.Begin(); - await PublishScenarioEventsAsync(profile, typedEventName); - await uow.CompleteAsync(); - } - else - { - await PublishScenarioEventsAsync(profile, typedEventName); - } - - if (typedFromTypedPublish != null) - { - await typedFromTypedPublish.Task.WaitAsync(TimeSpan.FromSeconds(profile.TimeoutSeconds)); - } - - if (typedFromDynamicPublish != null) - { - await typedFromDynamicPublish.Task.WaitAsync(TimeSpan.FromSeconds(profile.TimeoutSeconds)); - } - - if (dynamicOnlyPublish != null) - { - await dynamicOnlyPublish.Task.WaitAsync(TimeSpan.FromSeconds(profile.TimeoutSeconds)); - } - - dynamicOnlySubscription?.Dispose(); - - Console.WriteLine($"All distributed event scenarios passed ({profile.Name})."); - } - - private async Task PublishScenarioEventsAsync(DistEventScenarioProfile profile, string typedEventName) - { - if (profile.EnableTypedFromTypedScenario) - { - await _distributedEventBus.PublishAsync( - new ProviderScenarioEvent { Value = profile.TypedFromTypedValue }, - onUnitOfWorkComplete: profile.OnUnitOfWorkComplete, - useOutbox: profile.UseOutbox); - } - - if (profile.EnableTypedFromDynamicScenario) - { - await _distributedEventBus.PublishAsync( - typedEventName, - new { Value = profile.TypedFromDynamicValue }, - onUnitOfWorkComplete: profile.OnUnitOfWorkComplete, - useOutbox: profile.UseOutbox); - } - - if (profile.EnableDynamicOnlyScenario) - { - await _distributedEventBus.PublishAsync( - profile.DynamicOnlyEventName, - new { Message = profile.DynamicOnlyMessage }, - onUnitOfWorkComplete: profile.OnUnitOfWorkComplete, - useOutbox: profile.UseOutbox); - } - } -} diff --git a/test/DistEvents/DistDemoApp.Shared/IDistEventScenarioRunner.cs b/test/DistEvents/DistDemoApp.Shared/IDistEventScenarioRunner.cs deleted file mode 100644 index 6961ecdbaf..0000000000 --- a/test/DistEvents/DistDemoApp.Shared/IDistEventScenarioRunner.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Threading.Tasks; - -namespace DistDemoApp; - -public interface IDistEventScenarioRunner -{ - Task RunAsync(DistEventScenarioProfile profile); -} diff --git a/test/DistEvents/DistDemoApp.Shared/ProviderScenarioEvent.cs b/test/DistEvents/DistDemoApp.Shared/ProviderScenarioEvent.cs deleted file mode 100644 index 08dd2102c2..0000000000 --- a/test/DistEvents/DistDemoApp.Shared/ProviderScenarioEvent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Volo.Abp.EventBus; - -namespace DistDemoApp; - -[EventName("DistDemoApp.ProviderScenarioEvent")] -public class ProviderScenarioEvent -{ - public int Value { get; set; } -} diff --git a/test/DistEvents/DistDemoApp.Shared/TodoEventHandler.cs b/test/DistEvents/DistDemoApp.Shared/TodoEventHandler.cs deleted file mode 100644 index 7a69ceed1e..0000000000 --- a/test/DistEvents/DistDemoApp.Shared/TodoEventHandler.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Entities.Events.Distributed; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.EventBus.Distributed; -using Volo.Abp.Uow; - -namespace DistDemoApp -{ - public class TodoEventHandler : - IDistributedEventHandler>, - IDistributedEventHandler>, - ITransientDependency - { - private readonly IRepository _todoSummaryRepository; - - public TodoEventHandler(IRepository todoSummaryRepository) - { - _todoSummaryRepository = todoSummaryRepository; - } - - [UnitOfWork] - public virtual async Task HandleEventAsync(EntityCreatedEto eventData) - { - var dateTime = eventData.Entity.CreationTime; - var todoSummary = await _todoSummaryRepository.FindAsync( - x => x.Year == dateTime.Year && - x.Month == dateTime.Month && - x.Day == dateTime.Day - ); - - if (todoSummary == null) - { - todoSummary = await _todoSummaryRepository.InsertAsync(new TodoSummary(dateTime)); - } - else - { - todoSummary.Increase(); - await _todoSummaryRepository.UpdateAsync(todoSummary); - } - - Console.WriteLine("Increased total count: " + todoSummary); - } - - public async Task HandleEventAsync(EntityDeletedEto eventData) - { - var dateTime = eventData.Entity.CreationTime; - var todoSummary = await _todoSummaryRepository.FirstOrDefaultAsync( - x => x.Year == dateTime.Year && - x.Month == dateTime.Month && - x.Day == dateTime.Day - ); - - if (todoSummary != null) - { - todoSummary.Decrease(); - await _todoSummaryRepository.UpdateAsync(todoSummary); - Console.WriteLine("Decreased total count: " + todoSummary); - } - } - } -} \ No newline at end of file diff --git a/test/DistEvents/DistDemoApp.Shared/TodoItem.cs b/test/DistEvents/DistDemoApp.Shared/TodoItem.cs deleted file mode 100644 index 9257791efd..0000000000 --- a/test/DistEvents/DistDemoApp.Shared/TodoItem.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using Volo.Abp.Domain.Entities.Auditing; - -namespace DistDemoApp -{ - public class TodoItem : CreationAuditedAggregateRoot - { - public string Text { get; set; } - - public override string ToString() - { - return $"{base.ToString()}, Text = {Text}"; - } - } -} \ No newline at end of file diff --git a/test/DistEvents/DistDemoApp.Shared/TodoItemEto.cs b/test/DistEvents/DistDemoApp.Shared/TodoItemEto.cs deleted file mode 100644 index d593a10347..0000000000 --- a/test/DistEvents/DistDemoApp.Shared/TodoItemEto.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using Volo.Abp.EventBus; - -namespace DistDemoApp -{ - [EventName("todo-item")] - public class TodoItemEto - { - public DateTime CreationTime { get; set; } - public string Text { get; set; } - } -} \ No newline at end of file diff --git a/test/DistEvents/DistDemoApp.Shared/TodoItemObjectMapper.cs b/test/DistEvents/DistDemoApp.Shared/TodoItemObjectMapper.cs deleted file mode 100644 index 167c8e114f..0000000000 --- a/test/DistEvents/DistDemoApp.Shared/TodoItemObjectMapper.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Volo.Abp.DependencyInjection; -using Volo.Abp.ObjectMapping; - -namespace DistDemoApp -{ - public class TodoItemObjectMapper : IObjectMapper, ISingletonDependency - { - public TodoItemEto Map(TodoItem source) - { - return new TodoItemEto - { - Text = source.Text, - CreationTime = source.CreationTime - }; - } - - public TodoItemEto Map(TodoItem source, TodoItemEto destination) - { - destination.Text = source.Text; - destination.CreationTime = source.CreationTime; - return destination; - } - } -} \ No newline at end of file diff --git a/test/DistEvents/DistDemoApp.Shared/TodoSummary.cs b/test/DistEvents/DistDemoApp.Shared/TodoSummary.cs deleted file mode 100644 index 70eaee1847..0000000000 --- a/test/DistEvents/DistDemoApp.Shared/TodoSummary.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using Volo.Abp.Domain.Entities; - -namespace DistDemoApp -{ - public class TodoSummary : AggregateRoot - { - public int Year { get; private set; } - public byte Month { get; private set; } - public byte Day { get; private set; } - public int TotalCount { get; private set; } - - private TodoSummary() - { - - } - - public TodoSummary(DateTime dateTime, int initialCount = 1) - { - Year = dateTime.Year; - Month = (byte)dateTime.Month; - Day = (byte)dateTime.Day; - TotalCount = initialCount; - } - - public void Increase(int amount = 1) - { - TotalCount += amount; - } - - public void Decrease(int amount = 1) - { - TotalCount -= amount; - } - - public override string ToString() - { - return $"{base.ToString()}, {Year}-{Month:00}-{Day:00}: {TotalCount}"; - } - } -} \ No newline at end of file diff --git a/test/DistEvents/DistEventsDemo.slnx b/test/DistEvents/DistEventsDemo.slnx deleted file mode 100644 index d31f2b3262..0000000000 --- a/test/DistEvents/DistEventsDemo.slnx +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/test/DistEvents/dapr/components/pubsub.yaml b/test/DistEvents/dapr/components/pubsub.yaml deleted file mode 100644 index 669a6149b5..0000000000 --- a/test/DistEvents/dapr/components/pubsub.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: dapr.io/v1alpha1 -kind: Component -metadata: - name: pubsub -spec: - type: pubsub.redis - version: v1 - metadata: - - name: redisHost - value: redis:6379 - - name: redisPassword - value: "" diff --git a/test/DistEvents/docker-compose.yml b/test/DistEvents/docker-compose.yml deleted file mode 100644 index 916b6542f7..0000000000 --- a/test/DistEvents/docker-compose.yml +++ /dev/null @@ -1,131 +0,0 @@ -services: - rabbitmq: - image: rabbitmq:3.13-management - container_name: distevents-rabbitmq - ports: - - "5672:5672" - - "15672:15672" - environment: - RABBITMQ_DEFAULT_USER: guest - RABBITMQ_DEFAULT_PASS: guest - - zookeeper: - image: confluentinc/cp-zookeeper:7.6.0 - container_name: distevents-zookeeper - ports: - - "2181:2181" - environment: - ZOOKEEPER_CLIENT_PORT: "2181" - ZOOKEEPER_TICK_TIME: "2000" - - kafka: - image: confluentinc/cp-kafka:7.6.0 - container_name: distevents-kafka - depends_on: - - zookeeper - ports: - - "9092:9092" - environment: - KAFKA_BROKER_ID: "1" - KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181" - KAFKA_LISTENERS: "PLAINTEXT://0.0.0.0:9092" - KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://localhost:9092" - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "PLAINTEXT:PLAINTEXT" - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: "1" - KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" - - mongodb: - image: mongo:7 - container_name: distevents-mongodb - command: ["mongod", "--replSet", "rs0", "--bind_ip_all"] - ports: - - "27017:27017" - healthcheck: - test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"] - interval: 5s - timeout: 3s - retries: 20 - - mongodb-rs-init: - image: mongo:7 - container_name: distevents-mongodb-rs-init - depends_on: - mongodb: - condition: service_healthy - restart: "no" - command: - [ - "mongosh", - "--host", "mongodb:27017", - "--quiet", - "--eval", - "try { rs.status() } catch (e) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" - ] - - redis: - image: redis:7-alpine - container_name: distevents-redis - ports: - - "6379:6379" - - dapr-placement: - image: daprio/dapr:1.14.4 - container_name: distevents-dapr-placement - command: ["./placement", "-port", "50006"] - ports: - - "50006:50006" - - daprd: - image: daprio/dapr:1.14.4 - container_name: distevents-daprd - depends_on: - - redis - - dapr-placement - command: - [ - "./daprd", - "--app-id", "dist-demo-dapr", - "--app-port", "8090", - "--app-channel-address", "host.docker.internal", - "--app-protocol", "http", - "--resources-path", "/components", - "--placement-host-address", "dapr-placement:50006", - "--dapr-http-port", "3500", - "--dapr-grpc-port", "50001" - ] - volumes: - - ./dapr/components:/components - ports: - - "3500:3500" - - "50001:50001" - - servicebus-sql: - image: mcr.microsoft.com/azure-sql-edge:latest - container_name: distevents-servicebus-sql - environment: - ACCEPT_EULA: "Y" - MSSQL_SA_PASSWORD: "AbpDemo_123456" - - servicebus-emulator: - image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest - container_name: distevents-servicebus-emulator - depends_on: - - servicebus-sql - environment: - SQL_SERVER: servicebus-sql - MSSQL_SA_PASSWORD: "AbpDemo_123456" - ACCEPT_EULA: "Y" - volumes: - - ./servicebus-emulator/Config.json:/ServiceBus_Emulator/ConfigFiles/Config.json - ports: - - "5673:5672" - - "5300:5300" - - sqlserver: - image: mcr.microsoft.com/mssql/server:2022-latest - container_name: distevents-sqlserver - ports: - - "1433:1433" - environment: - ACCEPT_EULA: "Y" - MSSQL_SA_PASSWORD: "AbpDemo_123456" diff --git a/test/DistEvents/servicebus-emulator/Config.json b/test/DistEvents/servicebus-emulator/Config.json deleted file mode 100644 index 39908ff27d..0000000000 --- a/test/DistEvents/servicebus-emulator/Config.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "UserConfig": { - "Namespaces": [ - { - "Name": "sbemulatorns", - "Topics": [ - { - "Name": "DistDemoAzureTopic", - "Subscriptions": [ - { - "Name": "DistDemoAzureSubscriber" - } - ] - } - ] - } - ], - "Logging": { - "Type": "File" - } - } -} From b51ee74653d20e047a9efb27908923abfd428f15 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 23 Mar 2026 09:36:07 +0800 Subject: [PATCH 19/22] Remove unused System.Text.Json using from Azure and Kafka providers, remove dead IsDynamicEvent method from Dapr provider --- .../Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs | 1 - .../Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs | 5 ----- .../Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs | 1 - 3 files changed, 7 deletions(-) diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index 0004bce24b..73bf5cfc3d 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Threading.Tasks; using Azure.Messaging.ServiceBus; using Microsoft.Extensions.DependencyInjection; diff --git a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs index d122d3e5e3..5e89571554 100644 --- a/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Dapr/Volo/Abp/EventBus/Dapr/DaprDistributedEventBus.cs @@ -288,11 +288,6 @@ public class DaprDistributedEventBus : DistributedEventBusBase, ISingletonDepend { return EventTypes.GetOrDefault(eventName); } - - public bool IsDynamicEvent(string eventName) - { - return false; - } /// public override void Unsubscribe(string eventName, IEventHandlerFactory factory) diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs index 792442aa06..6a886afbc4 100644 --- a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Threading.Tasks; using Confluent.Kafka; using Microsoft.Extensions.DependencyInjection; From 54f1241099bb1d3ff246e38bc0f8a2640ccc9c3a Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 23 Mar 2026 09:58:36 +0800 Subject: [PATCH 20/22] Allow publishing dynamic events without local subscribers, use IJsonSerializer for ConvertDynamicEventData - Remove "Unknown event name" exception from all providers and LocalEventBus to match typed PublishAsync behavior (no handler = silent, not exception) - Use ABP's IJsonSerializer instead of System.Text.Json for ConvertDynamicEventData to respect configured JSON serialization options --- .../Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs | 7 +------ .../Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs | 7 +------ .../Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs | 7 +------ .../Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs | 7 +------ .../Abp/EventBus/Distributed/LocalDistributedEventBus.cs | 5 ----- .../Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs | 8 +++++--- .../Volo/Abp/EventBus/Local/LocalEventBus.cs | 6 ------ .../EventBus/Distributed/LocalDistributedEventBus_Test.cs | 6 +++--- .../Volo/Abp/EventBus/Local/LocalEventBus_Dynamic_Test.cs | 6 +++--- 9 files changed, 15 insertions(+), 44 deletions(-) diff --git a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs index 73bf5cfc3d..296f353681 100644 --- a/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AzureDistributedEventBus.cs @@ -206,12 +206,7 @@ public class AzureDistributedEventBus : DistributedEventBusBase, ISingletonDepen return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); } - if (DynamicHandlerFactories.ContainsKey(eventName)) - { - return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); - } - - throw new AbpException($"Unknown event name: {eventName}"); + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) diff --git a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs index 6a886afbc4..a5b2b2869a 100644 --- a/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs @@ -206,12 +206,7 @@ public class KafkaDistributedEventBus : DistributedEventBusBase, ISingletonDepen return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); } - if (DynamicHandlerFactories.ContainsKey(eventName)) - { - return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); - } - - throw new AbpException($"Unknown event name: {eventName}"); + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); } protected override async Task PublishToEventBusAsync(Type eventType, object eventData) diff --git a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs index 747c0ef4b3..b18afd2ea8 100644 --- a/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs @@ -236,12 +236,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, IRabbitMqDis return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); } - if (DynamicHandlerFactories.ContainsKey(eventName)) - { - return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); - } - - throw new AbpException($"Unknown event name: {eventName}"); + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) diff --git a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs index f192eeda4b..42ba15a10d 100644 --- a/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs @@ -201,12 +201,7 @@ public class RebusDistributedEventBus : DistributedEventBusBase, ISingletonDepen return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); } - if (DynamicHandlerFactories.ContainsKey(eventName)) - { - return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); - } - - throw new AbpException($"Unknown event name: {eventName}"); + return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); } protected async override Task PublishToEventBusAsync(Type eventType, object eventData) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs index f675f71b58..1d8e1ed680 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs @@ -180,11 +180,6 @@ public class LocalDistributedEventBus : DistributedEventBusBase, ISingletonDepen return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete, useOutbox); } - if (!DynamicEventNames.ContainsKey(eventName)) - { - throw new AbpException($"Unknown event name: {eventName}"); - } - return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete, useOutbox); } diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs index 41fe39fb3f..e59cd95613 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs @@ -6,10 +6,10 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using System.Text.Json; using Volo.Abp.Collections; using Volo.Abp.DynamicProxy; using Volo.Abp.EventBus.Distributed; +using Volo.Abp.Json; using Volo.Abp.MultiTenancy; using Volo.Abp.Reflection; using Volo.Abp.Uow; @@ -228,8 +228,10 @@ public abstract class EventBusBase : IEventBus return data; } - var json = JsonSerializer.Serialize(data); - return JsonSerializer.Deserialize(json, targetType)!; + using var scope = ServiceScopeFactory.CreateScope(); + var jsonSerializer = scope.ServiceProvider.GetRequiredService(); + var json = jsonSerializer.Serialize(data); + return jsonSerializer.Deserialize(targetType, json); } protected void ThrowOriginalExceptions(Type eventType, List exceptions) diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs index 5d8947dc40..729afa2915 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs @@ -181,12 +181,6 @@ public class LocalEventBus : EventBusBase, ILocalEventBus, ISingletonDependency return PublishAsync(eventType, ConvertDynamicEventData(dynamicEventData.Data, eventType), onUnitOfWorkComplete); } - var isDynamic = DynamicEventHandlerFactories.ContainsKey(eventName); - if (!isDynamic) - { - throw new AbpException($"Unknown event name: {eventName}"); - } - return PublishAsync(typeof(DynamicEventData), dynamicEventData, onUnitOfWorkComplete); } diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs index d6f4c2275c..b77f282a85 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs @@ -187,10 +187,10 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase } [Fact] - public async Task Should_Throw_For_Unknown_Event_Name() + public async Task Should_Not_Throw_For_Unknown_Event_Name() { - await Assert.ThrowsAsync(() => - DistributedEventBus.PublishAsync("NonExistentEvent", new { Value = 1 })); + // Publishing to an unknown event name should not throw (consistent with typed PublishAsync behavior) + await DistributedEventBus.PublishAsync("NonExistentEvent", new { Value = 1 }); } [Fact] diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Dynamic_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Dynamic_Test.cs index c9d5180186..c9813b8717 100644 --- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Dynamic_Test.cs +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_Dynamic_Test.cs @@ -118,10 +118,10 @@ public class LocalEventBus_Dynamic_Test : EventBusTestBase } [Fact] - public async Task Should_Throw_For_Unknown_Event_Name() + public async Task Should_Not_Throw_For_Unknown_Event_Name() { - await Assert.ThrowsAsync(() => - LocalEventBus.PublishAsync("NonExistentEvent", new { Value = 1 })); + // Publishing to an unknown event name should not throw (consistent with typed PublishAsync behavior) + await LocalEventBus.PublishAsync("NonExistentEvent", new { Value = 1 }); } [Fact] From 7240846914ac57a45d6e030d4fad4f11e378677f Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 23 Mar 2026 10:11:44 +0800 Subject: [PATCH 21/22] feat: Add documentation for dynamic events in ABP and include cover image --- .../2026-03-23-Dynamic-Events-in-ABP/POST.md | 203 ++++++++++++++++++ .../cover.png | Bin 0 -> 1163035 bytes 2 files changed, 203 insertions(+) create mode 100644 docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/POST.md create mode 100644 docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/cover.png diff --git a/docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/POST.md b/docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/POST.md new file mode 100644 index 0000000000..1195a6953c --- /dev/null +++ b/docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/POST.md @@ -0,0 +1,203 @@ +# Dynamic Events in ABP + +ABP's Event Bus is a core infrastructure piece. The **Local Event Bus** handles in-process communication between services. The **Distributed Event Bus** handles cross-service communication over message brokers like RabbitMQ, Kafka, Azure Service Bus, and Rebus. + +Both are fully type-safe — you define event types at compile time, register handlers via DI, and everything is wired up automatically. This works great, but it has one assumption: **you know all your event types at compile time**. + +In practice, that assumption breaks down in several scenarios: + +- You're building a **plugin system** where third-party modules register their own event types at runtime — you can't pre-define an `IDistributedEventHandler` for every possible plugin event +- Your system receives events from **external systems** (webhooks, IoT devices, partner APIs) where the event schema is defined by the external party, not by your codebase +- You're building a **low-code platform** where end users define event-driven workflows through a visual designer — the event names and payloads are entirely determined at runtime + +ABP's **Dynamic Events** extend the existing `IEventBus` and `IDistributedEventBus` interfaces with string-based publishing and subscription. You can publish events by name, subscribe to events by name, and handle payloads without any compile-time type binding — all while coexisting seamlessly with the existing typed event system. + +## Publishing Events by Name + +The most straightforward use case: publish an event using a string name and an arbitrary payload. + +```csharp +public class OrderAppService : ApplicationService +{ + private readonly IDistributedEventBus _eventBus; + + public OrderAppService(IDistributedEventBus eventBus) + { + _eventBus = eventBus; + } + + public async Task PlaceOrderAsync(PlaceOrderInput input) + { + // Business logic... + + // Publish a dynamic event — no event class needed + await _eventBus.PublishAsync( + "OrderPlaced", + new { OrderId = input.Id, CustomerEmail = input.Email } + ); + } +} +``` + +The payload can be any serializable object — an anonymous type, a `Dictionary`, or even an existing typed class. The event bus serializes the payload and sends it to the broker with the string name as the routing key. + +### What If a Typed Event Already Exists? + +If the string name matches an existing typed event (via `EventNameAttribute`), the framework automatically converts the payload to the typed class and routes it through the **typed pipeline**. Both typed handlers and dynamic handlers are triggered. + +```csharp +[EventName("OrderPlaced")] +public class OrderPlacedEto +{ + public Guid OrderId { get; set; } + public string CustomerEmail { get; set; } +} + +// This handler will still receive the event, with auto-converted data +public class OrderEmailHandler : IDistributedEventHandler +{ + public Task HandleEventAsync(OrderPlacedEto eventData) + { + // eventData.OrderId and eventData.CustomerEmail are populated + return Task.CompletedTask; + } +} +``` + +Publishing by name with `new { OrderId = ..., CustomerEmail = ... }` triggers this typed handler — the framework handles the serialization round-trip. This is especially useful for scenarios where a service needs to emit events without taking a dependency on the project that defines the event type. + +## Subscribing to Dynamic Events + +Dynamic subscription lets you register event handlers at runtime, using a string event name. + +```csharp +public override async Task OnApplicationInitializationAsync( + ApplicationInitializationContext context) +{ + var eventBus = context.ServiceProvider + .GetRequiredService(); + + // Subscribe to a dynamic event — no event class needed + eventBus.Subscribe("PartnerOrderReceived", + new PartnerOrderHandler(context.ServiceProvider)); +} +``` + +The handler implements `IDistributedEventHandler`: + +```csharp +public class PartnerOrderHandler : IDistributedEventHandler +{ + private readonly IServiceProvider _serviceProvider; + + public PartnerOrderHandler(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task HandleEventAsync(DynamicEventData eventData) + { + // eventData.EventName = "PartnerOrderReceived" + // eventData.Data = the raw payload from the broker + + var orderProcessor = _serviceProvider + .GetRequiredService(); + + await orderProcessor.ProcessAsync(eventData.EventName, eventData.Data); + } +} +``` + +`DynamicEventData` is a simple POCO with two properties: + +- **`EventName`** — the string name that identifies the event +- **`Data`** — the raw event data payload (the deserialized `object` from the broker) + +> `Subscribe` returns an `IDisposable`. Call `Dispose()` to unsubscribe the handler at runtime. + +## Mixed Typed and Dynamic Handlers + +Typed and dynamic handlers coexist naturally. When both are registered for the same event name, **both are triggered** — the framework automatically converts the data to the appropriate format for each handler. + +```csharp +// Typed handler — receives OrderPlacedEto +eventBus.Subscribe(); + +// Dynamic handler — receives DynamicEventData for the same event +eventBus.Subscribe("OrderPlaced", new AuditLogHandler()); +``` + +When `OrderPlacedEto` is published (by type or by name), both handlers fire. The typed handler receives a fully deserialized `OrderPlacedEto` object. The dynamic handler receives a `DynamicEventData` wrapping the raw payload. + +This enables a powerful pattern: the core business logic uses typed handlers for safety, while infrastructure concerns (auditing, logging, plugin hooks) use dynamic handlers for flexibility. + +## Outbox Support + +Dynamic events go through the same **outbox/inbox pipeline** as typed events. If you have outbox configured, dynamic events benefit from the same reliability guarantees — they are stored in the outbox table within the same database transaction as your business data, then reliably delivered to the broker by the background worker. + +No additional configuration is needed. The outbox works transparently for both typed and dynamic events: + +```csharp +// This dynamic event goes through the outbox if configured +using var uow = _unitOfWorkManager.Begin(); +await _eventBus.PublishAsync( + "OrderPlaced", + new { OrderId = orderId }, + onUnitOfWorkComplete: true, + useOutbox: true +); +await uow.CompleteAsync(); +``` + +## Local Event Bus + +Dynamic events work on the local event bus too, not just the distributed bus. The API is the same: + +```csharp +var localEventBus = context.ServiceProvider + .GetRequiredService(); + +// Subscribe dynamically +localEventBus.Subscribe("UserActivityTracked", + new SingleInstanceHandlerFactory( + new ActionEventHandler(eventData => + { + // Handle the event + return Task.CompletedTask; + }))); + +// Publish dynamically +await localEventBus.PublishAsync("UserActivityTracked", new +{ + UserId = currentUser.Id, + Action = "PageView", + Url = "/products/42" +}); +``` + +## Provider Support + +Dynamic events work with all distributed event bus providers: + +| Provider | Dynamic Subscribe | Dynamic Publish | +|---|---|---| +| LocalDistributedEventBus (default) | ✅ | ✅ | +| RabbitMQ | ✅ | ✅ | +| Kafka | ✅ | ✅ | +| Rebus | ✅ | ✅ | +| Azure Service Bus | ✅ | ✅ | +| Dapr | ❌ | ❌ | + +Dapr requires topic subscriptions to be declared at application startup and cannot add subscriptions at runtime. Calling `Subscribe(string, ...)` on the Dapr provider throws an `AbpException`. + +## Summary + +`IEventBus.PublishAsync(string, object)` and `IEventBus.Subscribe(string, handler)` let you publish and subscribe to events by name at runtime — no compile-time types required. If the event name matches a typed event, the framework auto-converts the payload and triggers both typed and dynamic handlers. Dynamic events go through the same outbox/inbox pipeline as typed events, so reliability guarantees are preserved. This works across all providers except Dapr, and coexists seamlessly with the existing typed event system. + +## References + +- [Local Event Bus](https://abp.io/docs/latest/framework/infrastructure/event-bus/local) +- [Distributed Event Bus](https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed) +- [RabbitMQ Integration](https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed/rabbitmq) +- [Kafka Integration](https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed/kafka) +- [Dynamic Distributed Events Sample](https://github.com/abpframework/abp-samples/tree/master/DynamicDistributedEvents) diff --git a/docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/cover.png b/docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/cover.png new file mode 100644 index 0000000000000000000000000000000000000000..697371a6c08217eaa880074777f090300b87a351 GIT binary patch literal 1163035 zcmeFX2T+tt(?7Z_OU^+uNX}u&Ip-ufgRtx_IWJK_f@H}_K|nI1pa@EqoHHm&6eMRs z0m=LqkLMiU`<*XT{qK9LZq>I{Tl>sBJ>5M${hOKY-dNo`s(9E`*Z=?kPhCw(9{@nJ z0RT_|m_TF?%WJbOA@cc)j~$IIv?Q!wmaU%Ni=Ta0wu+PTT#;}}uv9kg zQD$WD%edUgd<_vO#YQTG#sGSL!5&k@boQx#xNb{+9#C1UaxmHG7$N%A2mB62ahtJl z@6sG87Z~k`RLWFaO9JZd$_s(H+roMMT|JQ60svC7{vHshGu)fr7H;q8Ce8S@sg;r5 z5hl%OBCHM8_E3a7II7+Af*ahsV+g(H3>Akl%F577`AZ-fxWc_5^!~0cZU_l~X~v&? zC6LcI-F%GnKS{iur5R1Nb?Ft|z2Nl1yu!R-M&xKOn4N^alFF~K$d)vtgSWSb1RtND zpC7NEAg{ZZJs-cgxHunJfKNbx2T8$$2ypX;`180SnEvFT1V=!kz=<_K>X-){tb0${Kh4_JUlK>SAYpW{6o+`Ziq z4(=X*jdnx)G9J>5e1E;>A6HwXAJQRzVNd}N2!IGc7zzM92Z%lKME%);d^!4yO7#ya*MFt* z@p6;6p_C91uoJTt5##5vg~R!IAW-BJSQHNB5fbMIi;4@u#RWx?Z~#z>{bI-d2RkU@ zKM3VTING~GynVdjH}sfLTX(OC2s3o9LKIdChHx`fS#UV2Bsd({mX8WV0Rl+@d%dj< zTEcTqtU4*dg|c-|nYn;qY;;V{D76u35GpEIj~@q&g^p?jLMH(l8Ss;UiEbL$xY)le zsimceRF>FSEr(Yt~~wEMW;7_d-*y-;Rt$uU3zszd3serFdaFbfCyL+EGQrd z<`)t-M>a*l0s??lqvs1!ei6eUq?msdF@zYwtV|U%j$^#gJ?9niid|R>I=3Yf(6g zIXE1}7>tk9g_ZyXB9NwMT-44=nT- z9WaXh7u{cPZwBzW-oToN--E{y3gNLAmEU`lkXKc|Qa z`t21eHdyfQS0E58E;E>493l!4x8s4@3fS?8!UbSFwgSlQR2%{k5)!h53W^EBe|J(; zM*!Fk$PcLL)qXDND>Lt^iTN`o4mI0yEhtJM_4s77Mf9zJip|1xNIOb*oy^253^ z;RQdr@($Tn?R5t4WMk+1>%=brR0ctSz{ z@9{+Z7oIS|=tx)rQ2=1rpWcl`g(=t=%>8E@42+`xM{oB+uEw8U{y)XgPpJO_XO87m ztbyXX`DEBE>SxLj5GNcOK|E{K2-#xR;L^T*7 z^DBHF5yMzA8BgrsE~6vmdtqTaeZ^xEJX8$zfWXYqy^#mV|9<}00{?4)|FyvXTHt>z z@c)SgV929KK;;ina4%zs3!=c`=3095gp6JekPKYP1UPJ`KmjcG&v&kW3L#KQNPdPZI*JCm^!jjwhr1gL?nOZ$0KN%Q1O)hn1;oH$ zb8uKXI4lM12*yLlvWZedfun+eC};o>ef3asfB92^eUrPo;mz+K)Q@A0^_VUOX4Cd; zJikS@%L$^-p}Mq|xo;V+n)%lIn}O_dxTE_^ygHR*55m_O;;(0=|2YZ^1^lZH!XoDR zM1OUO->8M``t7C(L{$ZVm4Ni*rWA4lamIrO>jQZQQv&y-&})@avfy{j*KW1D>a|({ z>4C-&!{i7z>=)lj%F%gRmKy3RLA@ZzK#>JC?!(=-ru=YV9P)-6h5UvizZieUik)La zv@0g8ue5&15%#_ifs@Gk{U-n487uOO^9u+F35wmsiXvbkezBiTQS<-%h5j!BKqx{Y z>8{GyORpMrua;p@MfC_~2{Y#PmX*bq%ex6_a@?NdxpI9HdII!~Z)gs}KrM*u#82(# zoU}BxHKjUNMnz%6;IM&z4gWAgk@4ow@DKOKbicwstp7CR<5~FldAP*Hv4ppJ)Kd;e zYn&!^>%R~Al>X$5#Dy3bX|O-r$jj~jJd`8&8(wH2AR6HJwf-~YdOJ{;AIvNGd}PP+ zO9o^pj?a-@=|iWrl_UCUHE(<3lZziiG7pq?j7ku0ku~3A9w0-m1HL;Fn)d{;nI}^u zIl-Wqc804QMhm$>MqEhtJhuN22TAQNu{|=UVTkY+C~)*fcpJJq z!`%>J1yEmzix~zo7{i!D$3n*MQ9tldFo5~t7{kb}ejo}eKOPw8H>iTclF@N~(P0sy zfbzpaLHEHyKopc;QR>G4AT|n^|3Bc{BWZ36bBsEMwlU1zXmls@x(mZ?+uGaujTu@q zmR0R4@D`verrzK58g)oSNXKs03Q^4)z!dZsQU$>R{9qwLb1*tmRwxQK5Yth2lN{!C;VI=aGn47?$(9yd9V-_kL{U~#Y*zo3vfa;HOvmi)-3I5IaQCM<3aHvDIq z|FV~Y!Ms0ZlCb?L^CoY4BaZ$raq>Ri4mSzG0D41yf&T(_aX7FkXd$sPYKA&?a&q40 zVJoGe4KC|P&iB+Z=c%O!A8-nyf!22($t$)asj1WAOlDM zIPCIYafeL1{AXDe{l1@I{_ZrW4gfG5iM!#^wMEk--dHCtbCQ<@ z3HT1NG}@|77Gjm9-uO{re&3rP%o~JOI+`kkCFJAN!M1_mJE<3`pBOrsTATmQ0bWs~GLSV^u9m7nod~Dt-FsG18GokzeCq=Y=xhftwczup3sK z2NU!?OTwyy9saJ^zbi8$^e>r_e+Oj#lhjC2@Q;Icn4Gtj%~%%oPs`~Su4Z|Ylo_UU zuqTI6J?HH?xy{_#a+n3Y7y}xBDb@|81FBEUx`hxV_wRPIC~fjHCJIf~lU-4@zYyH& zVc=%cxkg!8Y!BHNbME%4WYNSf3Ofgfo&3)eBmZ#HV1oaDkqr4q$o=A?1q%(L2rGGwy2|gj~h{V6@CTzdzCNBT6ZUS|IIJ)xs3Ls+-l;xYUNr{uA8^Rmn z28F9D`7^`B#Gw#7un-SKP!w5kLl!T11o%bmc!UInM8zO55x5Y)t@lqJ*7k5WxEI9R z-OJk1&Cb0L<_2+v`!oJ|F5Xl|exAjuf0EH_Iof(byaMcf;a&)2O%Nd_CL|&(AS@;* zAtojwC=M1D5%Ge0!6Dvom^B1}fP39k4iOIup*~(N{yYe{iyhO={OqJ}lJdNNQ}F$4 zBTxF9Dj3|&o7Yzm<^VxBn9?hb%A>K~-)nB40MXRx2@mlBATIuFyD@AH=Oclp;+cQ2 z=KSwiL!du3L0at>U-K>*DT!-a;-}l~_}3r3;}vuDtevC39LIVpGBxx(xBP=I$A8E7 zMoC^6#2fO9ZCk*k&$BiVS>A+Ay{1=Q-++2jckpLhhxDWD#Oa6DwT^!kz5FTvpS=Hi z1%UE5+PBCrkKp*jBOw1!N&DN7H}PoBuK|CQ-jM$BA6@7#wQuNMZhWZN z3GNSd@%g!`VTEv{J9&9iqN>}uz}@V<9Vl<3{HD~exn2weKIIK(02mBFwPNv;{R@_VdrjP#b;q+kE=5+)BKo3w?k~j3f>-}!KdZj0&@{~tauh4(@ z;q+_Dnt=3xYm+ST`QWt}9{u!eH0$rs28^nQThC^)iujZ}EV=-v_>M=lJThY9psSQ-*|xgEgi3EoBd?P?t@ph6yS9}zikmCIm#au$9hnjfYEP`!Q8*E%TiIbb`>uGM<8=-W&i5RPG=Xt4e_@E{NX8C=>`m9GUcW^ki1?eR5@<-{TA|3^kt3j2l-b>=Ve{ zZx6kN(?rc&zNz1Eo*F-Enm*RAVH1+I3B3EPfoPI^>7$i|uc$75t;Kj~fN~-g{dcv6 z)P3b?H+BEqd6nhx-g~czBNg5}jqCoQOgZLUViFAzo?qKP^x4v?Ty={!K=leAZaum@ zlD#|`7G1bF4Y{KJvHpVhB$W9pMk~wE?Uu{qrmrC*;mx^~vd@>x);{7pDdpL2MQGZ|iWhX1QOj zK5e}^BM7+)xmv*a`rIbYq=rv8L1UjqnIO(cdpVPL)pTy}_6^n9VQ5xc|4%jg17W-LX=Ev z%d(Z0O8m2*=b08FWA zW&^qjBcSbDoB9NyuV=!Z+L@jLoQ40z?`WuRvVpAsZw$h zprfY+E@yl*Psr3m}+e+T+6$zyZiL(qPqRe5^XItAr_b9+6h<~5mbv7-Yc`RWY zQhGa&nw8^@Qz>_#j>kC}JbC`3i6NJz05s08ENRNd#3q+} zU210Xzy&@19nGVJ%M+P=P=Nw7e=@HK$R3^Gh2KG32@Q92#wpAAL>AFkL7r{$BGC;; zozd7IJb6mQpP0^tb>vyp1>X_avyFZ6?I^k0KYH47eSD7GA_qe*ztt$K$AvM@f6uec zNEUBS!LGjl`f&1g%lTgP!qt_v?Db)*a=C`*6BVu6Yq_KXkXGVQft>T%WXQ_CsUENr z<#wU1i$XiJii&dW@$YwLXqsF4oJM2s zi*aP=*}?gVD<`>TH-D`9Wbu)w{uLZ0crFQ?=mQ$r10kN=}t1y(>j_4?{V$ zt_{}OgrSlSZ}r_$Zv|EQq|bLeqTXcUiDO!;zPPh>zoFrWY+?tY^MkQOFy{1Tgf7}$ z^`h}=_`}B#)(Q;tyG^ZCbf3#sy5c56nUdEi{AqNm*23(sv_x|C?7_Z=T!vi`-yK2? z2i}9!u6Zl{k3}q4kQx3{z2eAJl?9er5(|#*@1(bE-!Z11j2;fnl5&JB*6JlBwF>P)AU>?|{?8UTH4 zEq1aX*6IKd%Tjha%tHX++DFM6D$ju#8n*^X^EuR>aS}5`@Ceze@k@y-G!X`Te|T5g z0QQDs`t>Soz6=Tz%R5RwuWoK!5rP_Qj@@}zW23wha+3QAehoMKrpS-+@S|?GR&Gqc z=w>DfPnmcBwAhN7iJ&^4Pt+c316r7b1v-VOX_`Hqo8lTzMBO)|+y})rYOKjTz1a`tDrVC@v^ zL_5!}8Qb>N=UO>r$Roa>)@C*7!ZK1z)Oe+mbt_xaPm-)v%r+5|Mva_U@-Zt}Z67jD zwXlg)t>gs4?dA^4!OZ){A1FOPV0-F*$vWgWAM4RBO%*mZn66FC>mhzzpOH}GYBM{Z zMka0E-Css~OR&3PLwmT#F8r9ZayZ`jUZORqo2E{7`Ph>A!LG_!@DmqVK!XyRS3-T9 zdO&siJ|#n)SI${Hkqc?~{;f8qR-zgMJL>*^&WE6gL$rv=(01vrNJW2Dloo*`>Sd<| zabhpU>Z|?SupWUP75=!d)bDB2=pdaz1QfD16XXiY!PS6UQ$xxuq0&!!I<_H}Fh5L< zRc1TEf*rt)p)8;79|uSy>lism7{X!a$W&G+C9ojvl`4Qu$W5!!f&^%v)8TkkfAV9>wmeFNWtUS|JScNst-YkhB5=wC|-6 zdao-ZT2bFKO@zrnA(i^{yh|f#AVdFWBM{|G%yh%ty^QQpK#j%m=8IRYr|G}3(gWgUFK%81S%)+y5`_Qu=&5zP=`)q65J>=xzyL;>95#RNBs*;F6%>~vfRS4Zf zO51DC594`3hqTls&=Y(=ah>7{QTw``aJ}=0YQat)Q8qHg>v38L7|D;|Wbp;a}a^hW?3kz6xHcKBQlu$?Zj&fsP;LI;dWE0m zk;Lh07Q>Lt<*iqIw9?9JM+5cvYOdCj>2o?Z=(kGY$yMW8bbz}@YA?vCU6jhY+Z<-c zyW_Q=Y-v)j`r~HE=sXN(C|gr3^QIQn)Vo`|F0FK@sghbVv?SQ6jY1nA3-8MjHtIZ# z-=Rvwh}O|cV=wM49&tlwb$ww~b;Q@AhlyBPj#_4^!rszB)_qL9pTFX?46!8ze9jhA z2GimB#Q>9=$?@?PtBsRs%jd3Li=3$4)W@GjXiR1olMm5Fz2i<({T|KD@%hzm9Sc`j zsw4K}k`Pr{eb2h~EZ48y<*Q$EVuUS?JLaCIe0!D|x~I~(?xeA3Gqb6t!~#rnCPwMo z@FnryB_p>n$IBCRZp6UeFHt^h$|hLFy{ili1nSYK47f`2=2u_H@do)}S;bSy?3)Yh zGq!w&_WJonfF}%h-PqEeTCXn0hDNl+MNI^Uv>5W6%r;^dnlg!tMzrsxE|sg)GI3R9 zv375uoECgeiwjlx)v0{l+L8#`^b3c}}n8xo&v-9CPX}gl)b3{{p zNwv>H%QsS^HSq}JxKN?BT|tphwCWOPdo0DgPBpe7jY`9SL=nw=OZuUAg4|0|=)LEK z>tNjP8p>GdMxm4oB4p!C9u@;yj`DN?Z~H7+*TR@vZRPOvJgcHc;_AVKW)Nl3a5*Dd z4Ip&Vb61I2&(2nUu}M;rE|nN-H$Y(tyx{EtNe+`DEdZ0l4qn%W1`d}>skS8#qTOHG zJxGpMFDI!6Bv|(8cGqL0hZBbP22R)(fw{B2Bc%{N+svswVWe+-BXrE(c`!W|;&&R+Z>?g=jd2sp=6#m^kuj{Ad#k zXDszuLG2y%D!u3?@8CUUvU+5>0pmeM31pFC3~$SGu4Zx3%_d}Vkyz4rzU7uJ8+5)a zx^Vx;HZrI@{`7R!!vA(8P)$UTU4}@8fuMGqQ!m+ZuIUG-7u^byEY?a$;ZBI2$%~-rNF06Ge5X>42ET@Z z>;?B~A9*jS>gU7J4h6NN+L=czhk{C7<~qwo6&YUU=#5wef(~@UXkREslkW=dc>_{~ z6}F7B?k82e7ttkTHCfUktOyfy5@y4T+mm}ti6EC_@_+?eYc^{*bpft1R;_OHMEi7I z5<*DwxWLWAD1s(Ec=tznHS$pImRsY~zv(E(vE^+iD_JuH+M%nDUXsA~0*=LwjUKb~ zi)-0UY$;S1O`SAg<5;mLl^7vvWj?zZIvi$9A388Ds=5Pbi5;F&34It^(d23)G{>T~ zdKL4<;*lMz0Q+Oe)9HBE7#|hI_=QCwoZ!bJA8MK)!-B6RVKKDj_!S(Q?iP1~VV|M< zXW=7*h%<>dqm%ZbJ@29?Lc`TW0BDkW1sOv1rXf%j2I zINY0!aWPb&)XmVRcLO~Jbj?{_dPpv*_2KiXPwbe_a%!02Vhyj25D$H9NPf<#)5`-V zYGdRF=ct@5G%Wc(EqRY&*jZe23o5znmpJ0Ds^8yG$@wa|*T}IbmD5js<@8Pecx2AB z@C#ee_r*~cI;`|djhM{Z+h!#AC4EmiA3#6%D&D0n1F1Q&@@v2Sew?!+n|IFYnU3&Fe0stsuB?e- z_}&8;#;?x-wSqdUbV^%FW-8%xqz^;F)GtsGj?Y&rh`xev>Expuj^GN|ID6-mVl9cNH|@(Xt<@-;ljqZ+@yoO))+`GbFf=z&sV z1hw;!gYG6yg8^>8Lnl5p<)a$c+l{9*pD#WIN`*i6=&M}FBQ_J3w#LJX_(FK{mNlH8 zk|`&DKK13cmng9sMfKZ|C@iWBN#5aQVa5SpUeCJ%J16pDcwb)7XMKbp9txV|FLz((pl-k3*9 z>YU=#5cDc90tQSLh7@6gkG#i0+hOVJ>U5neu6NYXpTup`U{kby(BlO!tcA9(=gld} zvGNAFEq~Pd0;=%-tfv$ZEF_jxbqQ)uXz!A=GJKva(4XW=DPE#_3Co{ zX@-Sp4N)mdT0X-Da65G;ps^Tk?J+xNX??vpdpqRf;`F<9$eDVuJ-mqvmRM3NrXOG^ ze^Sw(ci*5{g4Szu5fh#MNyo?`cSJe+hBA-e@hi?8orW)3-~va~_~i=PbkQ=~VoC!0 z_8L(!Vdogf2{kG$C?xrKW@~ymmuTHV>ttye!RTL>M*H=Y38f#awqHRZKg9uKIPin9 zsj&_tRR9l1OWRmd)Dj2#9E@elPB#Lh;sRwT)Bi+!OhZ__7+aK+mCBdU8DtYl6d&=x zaWF@AvcQ&`7bZY0>-Md?Z^Omx3Abp1`tcJd=7gq1r&8B5f07@Crn&Emv#TY)QDp3l zh%^{s5fT19GGVEZi zO0<>2CNZob{FBcvT7LX)gaLD-WNBhj)}?9%$_MH5w?saYjH5m- z>@@Og4j!_?I!{SQZ?os170|HaEz|bup-p6{a&n&SGf>e4Oug0W88+~HEe4#V(BkcP zc8EQeHW$&2n^(;K;S$+qhY@&qn6EX_1S1z+aeF%%klr>iewNj^P7})CW?{wXjI3Hu=K$bokgk6-7>slc?!moV)dEYpB;>( zGH3VaS&0~X1ql5xf8p7}6i0jcnEm;S*lLcMn$1l9&#dz5!3^VStP6SyqpMStl$!>K zjUj=(4;+ldP^=-$k?6elpBNue7E#fmzg>~SUzX>?v~H9B_>rM*IRViZbY90${w2zU61RbXgCQKd^QZ>g=;gV?ep0pr4iCq^MFdqo{sCJMZ& z?%*^E0aWalIQa?8ShGHcF>Z2_y_|e}`Z2bb@AK)yS4JcKt z`Q}DCROG3eq(;W4^TenFxSkQNT;)X0J_p&$Em>O9ZoiF{U}-iEE?&VwYZ*M*>T^=9 zCSV~mG*`+AcA=1Ryw5@Z@PS!fVaaC4vkhMPr#^5Ym%c{Jx`ZiRD0e_l3CrUH|#pie`Q#4c&;?TgbKxR*!h(UcL8 z#F6voolx_H0>97xs8wL*EAaR>(r(OV@9KAx8d zrHLH}OgyvZq$>PM44WV<8#-gNv>rCnQ6oI|hZ9=KfGi5z2BzH16$e3#RD*+kp@T5uSP3Z~M9!-iOcPM0{7P&)RpRL+A6zCl% zMJzgKK|)nQn%O#HYV=KxTgXe&`IqSP@0Z_oWIY#5jE?c1a~|XZOj(G;UfG)ZjJ8Lw z2?`Ll4fX7EOt24RsD^a+$cs@Es(r)QN^xrze)qImLc7u?g=fToU)Ah=LJJ!Yn{)2n zWeUl?;gR)2qddW-|OyYgRzGn&&b9H5)l6aYXeu%*F_~UwOJq%@%3*%2yAoAGAhorLn?aLJa7# z){@jI6xo!F%~d&V$t7m%I_=Mr1&41fe86&8VW$$Yz*Ke7)DN4mkHJ6cwIHx%NU)0@ z!@k$t@0e2)F6@JlmcD&{8n)U&)n_Vc-BkgxVrl*Hc8 zKC#(l)oEeRj{~y@M~?d(IJbp}LywD?1;YsfC|vOYsDoSNlI+(@Be|3K#064Jp*mwpGG)`9aA+swR(#& z<#GXny+a9QM$oHYxlplY;yK*505F4yRXF<_c!$?oyH-q-VcZtqz((!ytUst**!d`K zQ_EzAUB0FXqrX^kqATZbut34;8t-&&mKMyXh$n8Y6|IFHo&rx%w9>`X%qs96ta&O& z4{m2nztCRZA#Yy2P*_1hnI*>G@O<$mA&2N*c07!!dMCwxR>D*%zoKmD#e2Ha@QmFs zUocDX(9{R1uaIz?>eOVx%1k*;-{|}g)o+2et=S_yt4&oP7;(y}(B_pQ|C=XlG5HM4ga@9|Yj zAyoY6a3A*(&xx`)My=1@!e~FC_GHRurI=`M$j`mve_myE<>J6gxrxY}m6HKKixEh{ zhuYg8+awQ%X9D+_6sF+Q-_7?B$j0ja34b{^X8qnaP_dYVpuGsD{Muu3( zK1*d zmQ3{(@eGbRLR=UakMruTP~OXv&(CZ$4cV4n>7t6fwKGXmw&_bVGVY9Fs|lfxE7Zsd z)D@RuLeS)S@uJr@%2z9Na3-8Ckcup{n(=KKZp@R(5vURP$xkS&v|ffzPANh$SwE8H z_1EU60a0Rypo{#MW4esPs<>R#K>}xTB2|QW((Sjf9dlC58AHmW8MB!XTz2w&mYr~sl7Mt?$v#$mrUb3ny&O!RZ*7B`6Grbn}C z$2YM2Ar5ist#}kZ3m$ppdb6xtkIOJwdsM=tGm7@{)8tuP?B0uEKXm*iw;YIPgY}k` zI$lvD&jCi7xZB7{PVIH&Q?wE^%eyHhY|~_T+HuRSf!J;O`#mAE)B^i+@I`v_w-y49 zX{ymm7vs>bIg=<*7&zNQo|W|OYLR{!KC_!D0ie!M==Ij@2g22_RVhrO1qLd0ZJXlJ z66kRZ@98FGrO_s8;sxm=nDAWU9TqDKm^b!Pi^sb1GmGim)i0u1xZ`su#S+hmDWpR){J*zeHwWu_6%O_*EUI)K_3t(GR6%x5XJ106qXTUK#P4u4NX3oWBheRu# zFtEO&j(J}>CTB_j0*U!eEoVz|jw(&1ri<$f9v2Au$mP@F0=|FfjKO+;jkACwE1#q; z$XX*mHg#_nHZa&phG3_L$9qo-8h;=^Exc;2Yu*XueGlwXOWifaeDFHw z5+>*N=YIhRRB<)JzQO5M8=IQ?^`-7++eGe1O0~s)Oj~J5S*#s7`lJ>>(fnB@QfKkzSLGOs1`h})s&Tn(2 zE9U`G9148ZHCRL9+Hx|unvW1pOPLnw8?rU%&pcn|GF^Djd@AR^(-GV~u9+pinxk?1 zP3C~^Od`5EtIM!ioCLQQdyk^Pkel9g0o}J~8^(Lv`vIM-wU5v%Gpt6$1%os~P``0@ zgp1KJBX}5aM`WWT+x73!V}t;wj9EDBtgw{6SkvHU&x_$K*UJ;I-l9I`S@uF()2nlt znsa`F`Sk7BRPeGPNTjWvgUnTxtY$BbcTrXQ^jJ&&BdPjV#*cB~)m@lh`r!ovA)4NX zQW?A|hzhSzKx}3QO5qhT8Rrs-2$7TineM%M5L>{Kx}Pb<%vmS0uyz&Qdb#T!a=l94 zrSc6|!aOo~&(0;m$unCj>ErNG@6Y?u=)COp>1f7+If*eVSWQ2$oPo4wWTY9xpUOtZ zZmR1=AOlX%^I(?Ucz3A;JK!GYZsyQ9kA7Ko)ZfZdgruTXHGqO zUA-tRvfJjR#NxJmL{8w3wG+hlUXzSct4&6R*w_tRvC^Oo2%AZ zdLY?^t1ngB?=ZDV%kNP5RL%@n#s%a zjTbLXO1cy6aWA*JPc{>S6xSRBVoIOfOU~eehPb48xg)J4_OPK7kQj9P#c-I=OzfqJ z5yvfx_b9t**dskCQihou+oAHLGn+Kz5_`pfs&-#_by(>ec}Kx6eKV#M{z5+^%x7zm z4VUJbG(ys^C8`Rm&qoB%q)oMf2@M*P067=$bTP zs7vgbwrv z_H}x*Z#+&u_lrhut*!6m^Z6Y34dfg{1 zL`@-V%SI%2PqnY>^w~Hrl<0d(q9R)_Sxq=qo$1A>-kJSLpSZxb-@A@o<&t!JmNr{6 zvxH>f1_6q6lC`M986~E;8f^uknAADL)rb9!GFHfcp&Tt-9d9p$oGRxy3rw^|Ry3mq zuG;KJg%WneG?mK+9qlH`-rqxBgk;n56FxTWHf@=o3iuLt6W`O&5(4*p!YgV2EBqUt}}e+hf!5&AGFZ_@T#N-Q*Txy#nU-djqRbj?G+AqPtmP-`J(Eg4i$#`G*| zO_jfBS-d@2$(vIu_`#lcnC=q||E{}yd4{$z>m_EeyE#QD=n1b6eJ2hUm9QpToU2q? zM(T7!ErAVSLy{k6mO|NlZ1yyUuBs0M95y)2?%N*M1eMW5>%^#39w^Y=S~DUSfewvP zNF1|{-2a8&KLF=l(P?gBr&g^@i z*x-P_-U5kJsFl8j(_T<->{91vwhCDM*l@RHQay`R@TrrZjS5PGtg;>3=oy}$IdjTmOOZjvjcb?J>7PPd&u;X^a;xKw@@%F? zQQ5NgUIkw_g)^pag(XRCxhQmCG??0jM&MLCj3S_E7G;^anL0$nkoRpF3==x)d8>;X z7i3iUy$PG&pesrITjmQflz5ydSWuUo8-vR6ZvKZP5gpHLZeq5GT2<8b8OAqETev~& zK6R=5%rgUzFoDd}QJeA@qvQ4D#J(~4{)`O{q9mIAK|q$264Mf=J3!6#u3)jyTpA%^ zB7Z!Gc@l^@e6jYe-;w>;)Bp8$d1%q01SbjN-4IS+kl&HdJ$cX_ON0&e$r_V^FHxbEi#koxuE-=!orB(fG z;h9QK`Z}y?*vwJNK~-&suS2P%`q{&wThZ|mkLUe6iHzkvw$PHIZFz@HTqsh<>bBXp zxiu7z>W$3(Hkv4w#Kre{Czvu!7d)bRYehyhk}3pU6oc$P^;+~SMa}h5S!q#5Zlk;= zty9HU5DLd4G&2^V;7wOmW%vx<>T83@LfLk9c$W;A(ZTf<{*FRif`QVW$yi@cwiJNH zP75|hK6!(ZS5MS021M+Itt=n6$c8+ZdIx5-GJM~Y&E5YDez~r;udfu97>a1r_U9}e zGzfSea|q@H6G;WWt^20QlANK?ShU$3(Y7)pJQqo`qC<2RaaloX^U3bPgyJ{t041X^ z=R8#5p7!@6L04fUxP70_Io4e!QzPy;kfy1yRj^4seNq^+(kY&GtenG7g5IjCEFC(YnMcpFU< zhEY%M%WhIU<_9>{eR<;E^2sE`rOhgV_03u|X*UIPMbGyo-fC}w5O0Ij`O#EKb=7`O zb^WxET zE(85|V?~mkBf*bMspPiky4#7CSh})@^O|f^F`d+5V}RT6&SLRf7rS3W&d;r{v3V<4 z$^EKY7Hf|0(RlX~$SW>1UhPjJYiEe2kkd`JAHlJ;ujoaM%;>Y}u`ycWV=I^#eZ0Rp z&DPkfDGhL`HsZF>xw@IDu)rlwbq8lJALWX_k+*$`1>NivgWuwPVEfjQ!g9%_dT7(R zp1!ko3H4ar1f`^ar!$Srn3@qKF6Q2C1-)3%u~l;mw>BesVz0|ww^Zj4W;XX%#j5u` z{Y7D-OkYfu7Nw^1^&bOB5{dHxb?Xl~qqr>SUZVVX4Oe;+=l!JBfb6*`rGnSCwGa>M z>AN^NqdoMeua4n|>b$x{735Mq@z%;LRJ60nr;2P}wfhf@gJU6FTXVv~AAA}T9_Qfh zji44(?G>`SWzw4M4ejg8w7f}Di{FVwbPKD zxmZ02_Dc2~chxD|vhMM+Zn)b^t-R!!ACj9KjGs*->G!`MGHS{VunI9B(E#re-O5bd zaUF1j7JJ9Xygq!;wy^~p!1|EF^s>=ZDTkez|Cz=B%NrK;Ro|3{RHKhsBE#m1-q>8) z97G%WPJ>NkrsLlbL1R2v=Ii~o)#RiesiA~+ncgRi5Zq+t^19Qc9AqYVMZ_rJ-({V% zS+f(H#8gx*KV`|-NG1PTDSX#3E+ujEf!LOrSsrSpGX-|c*WHaWazf6?nKaKLZGYHp z!sNbbK^JN)AGhF=hPTo@75+a|F;Iw&>MID-3KB z7Z&_@{a6cD9ko`0i6`Nlk>Q{C&Vg_lMxFRxFc8vjK6XKLX>^}vO=y4xA5O^virR1#|K>H8Tx^A?DiU7hhCguWuYQ&_@jiarL~Q zz|!$Ab)@h3UAuEz#5?7@^OnzaF25Ta`sH1&{y#=bPLrGDmcDnoH$(UcXoWQqoseRgG`wb&OsN=kc;}5Pem0|jv9cFEj&xi9 z2Ztc}>S$(*pvEI{YCY<~P}BA5Cv$Q}w&EGJk={7dq}9?34-!6we1?a-V(F&e?Bpp3 zF;w|fG&o+^Zd#L-Lh`YDLK&DaFBISHgi5iB^K>5rN2gZu?AFQNYs+V?=3a{|b?l{2 zAatLe$TQyS4fIPF6f{>6zGDOK*qusM?9%Poq|5GbVium($0*kr=}kr*u)*SZVl1db z0-j)7BEgi>30Y}i!+65ao|zbd&hYRhy0S#SA+<%MQg;zqO8_}FTQHzOlP*PQDY~3l zbMGQ*Nb*}~p}Uoh-sGH%%55_s)Ru7|H+8b(jD()){i3Qwx+UHedl5`2b_d%7;ijMl z{EBh@KyW5Jm*B8%KcK&Rz1Ya9h?C~lgQsuUn9KX~m~30z9m>~^O5TX$W+Aw{ z(_%y7+D{$Sy64fc*FHZKzKR*EAQ|+;VJ;wJO=PUDB(20w*j>^ED7^=AtJ5>w0bY|aWnOSKIOMgCDsg75}a>BsykOk& z^{~*4*n|;anqdLUj1j~fOLa<~n#w-+8b2}wKA_xm)oU4b0?jYPlXq5r4bveqdYJ?} zS75I)XG2ziJ>m?Q7ppmWDof=kAYKRHPZ-JL?PuT=3&@Uuv1IaPIof>JT9JYQL>zcM zAVm_NKhvM@yRj@yl5&Bl8=wo|T)+CB?S03qZ6}RmO{*PqFF!$44h;ognKXg$T)yDC3Uq*)dgy0u4)-g78{xd%fePX zJB^KX$)C+1|M=|>KZ5`A(@*Npmmxww0j8{f@-TvqzaGGQ6m9?~OT2m|Kt9%eDDk`q z5Z0}A#3*ue?LkJegQJ}{fGuf!yy4Zd)Abi)0$rMKbxC`+pH5fLI%J2yljNP#w0>yW zBF2&qvK)rdoc>Nj06JXS5Dd149T|s(+Bt49>G* z(QrnuF?=3X3$|%`$WGDmN!gk{oct4nPv6I_()b)rx1*w0)3gmmqkt89)>%PsqMf+c z2Z041cmex6g}k|@dt~u!U4HDco(#^Hu;zZlT;un!^L$2bQ6hw`xzq%ak&I#`33}w2 zay0U&X3&fmYb6GcV{F+&uL6@uAm?MLz|D)}N_aJBhWsIM%D4-Y90c;ZCIPtI7W`@b z5P>ggUtDq!?}AZ@;RDfu3dtlUe8o!V5V7p>Q6}q1#G$gfVTO@sKf<#z*t|657zvQE zyO-14m>PH@S`B4@tOzZT7(Y%|VY5D!wFO{ga8`PDZ5(92m!( zO%NDujV4xF!*Cem9R;7GSBbvK#CJ$|kf*b+^ts=tn!L{cQ<7z0M;L$PP0NP^PZC}~$v%&dBH=y0Q zS#5WA0w1OL(1TsrEG`8_oIBl4-;chT|EE0``=Y%4Djqw>fN??F1I{vFUBM!knBFghTswjUl_a1*#{P9Hhx1%ujCO%Xu|$Y`jig=n&r*F zbN*f|Pk)a*f;cX)PJHCus`()Dx#Bx=jBdAY>W13-XT7!~1=!%4-{qOHgUlqF#*Z}` z&)D>wzxAjouenoChf4$J7O2`0uc z0M4%0JSAm<+aE8Ege4b6iB@4Mz;cZ~#PYe0w}iX8P2KWjXSTCK0Aa;7-^COnzLyP; zp`}`)K`jW}@Q7M7H+D{@J=!|1vPA+e8?FN7*TknOG)MUt^1e30NOnyM5haryrVwaJ z*_?sNG^|U;QRbelgRxt>To}{%+FKPJB+_o*3+SF<6m_e`uJRPXA{+*Rx)AxwW{mji z58r+NFMZ#?{PdIjS%S*`H-KX?JNernkETxk3J`;cB?rI~*hA)gPb|y(Ovuud6u*kD zCE?_SG4Vn`DB<`@gHO2S43T~R6Le|LuvaJ%ewq#v#^!x#HXuDz(+>pwAe){j!*u#Q zoB)=}YXl_fENLbT+0uAYb~us1(vXmZY>l#x?43BV5#D%M?|O;jfjO+B5<^@@`2XkY zOcy1$jV1jBs8tXKoF&54ja-YKlUJ9w%`4K#E|((pQ}&ZmKGzw*FZ38E}ux1Sgiu`MW- zLd2;05cH-?)0s!{As9o6edqw3cSppbArer}KJ}=B-gvvXQ(8 z3%qRE88-uSR5qa67$n?hu;b^KKO`cgElIiVj6wv+$OcPim=~|~LV#cL-e=VIJh31! zyHETy&pIx1TLE|keuPrKU&w5Sa*As(&V4NqGsM&HRF&cg9Xu?M&6+t5CO6|1jWD?b zXnFvb{NC=>VMcz3a?@~rA}H5kJ>MEf%?O$M4y&NmkiiIM7gd4`-QB*(4V{wG>-A1Y z#SOH!q)+yhiL}KD9BY;WCoZ8^!-_j2xBAF;W6Z_RjTAsZ9I;wnF(fl zWd#_}tFtA1rIx^J#rY5$zx2A5#2(cO`}7McJ(1#(nfQ2fr(Dw3dFXNqH)P|e_Z$k7 zdwqh|W#js4^;I>1ol!5IbbHE;E+kOF9}P~eK!0yr;*bhGOJm}N$097Q_MqGjuQ!sg|;tCh(yKRI0qez^$7o|mW`(@LANC`rcAOsa|WD7qP;88 z!6I$^VqI9A36ZYhUI$348jWC5VAzZ!HByT3)YP-V5etL)m{(aXW`s51XyRplXt%AhFdgi+FhwU$aoEM@w?l9Pd3U5C?Xr)SY+m0-f!1Q`H}g%uaPQ3{hTu7n~s z?%z>s0)OvoOIYyfryu_${7Y5`hEJ3R^!4-84JgEO+$#MLm}S)2JMidaY`aKK3i<#W z6)O&tb^_?)stIwRGm&QxH>WwPtcUj_i)aKvqhkQPw?7K7u{oDLZrzXoGc$=J>M0xC zgZiW=-={_lv_}fU2bu7!x74Srr4R^)9H}!xT#pOF5dLq;Lgw_L!dXgum=g>y^u<#5 zOR-e{6J~FZ_Ei5+8cy?Rg&C=azIj)Nj%-6=4nJ4gLwFDvWZ~Frz2fS4!_wjrU-uU< zQFBh;Z2C;k=;E+W^+V6iAa=0Wdz0l^t1iB;)K8XJ^- z-Q%KvOob~v#{v^K2=B3(%FB*ZSfnf}fiXc^7aCA0{!2=gkyz*BoWGF*;+{=&WiF7& zV&PI=XMsQli9Jh#7_{t#hFi~k3=>>3_0k*CeJor94L49CCRGHnd|RZl z_pLn;iy34nno!Gu(2|wI{!y1O%G+36>!gt(&U?5FhSU5l_m(<26&a=T9z^BRXfF)FVmC3m}Ow0e)*Y zUsL(fa^{A94nL%JR-*2fg3ieAWSFb21MFGhNpw2VN|_U=mL+Mx+gv;Q6V3*x6Q@f) z-EE6;P#SUpoKMmupK?qtfh^XigbcvMPLCtz$r)j0oV7Ej7T`E`7G$P={Nqp}T}gY@ zQqNH}ISldzaIAV~3c6v;5enS+O#O=EHXJ|z2ph%5pII=)l=4Uw0zNX_8@KOJvj&^} z2HOuent&NGZdoQ_#-S{{6hAexvM-BB#8`^L$*zl=4^B* zJVra3bam`AMwQfkJEV&>KH5l03ZNKL_t)O_xygqh~^%!J|(imE7prW4jO}M zSF?7Pl@ZF_d=a68Gy+8CcAmyqS~a}Db`5a}yQ{N_lT0nrAI74-wROOindjFpZRE9t z*51rYuUL|^M*+DX;{4ueH4w>5)Ij}Z+%%;TdD0;1S-(frfV9ukbbCh|2HZ(L&Nb`Z z0ni$U!u26ZhpN-V#3N*Bb$D;DXvllx+byvq?A&j>ke6+1YlU3qX+YNMWn0S2Ul*^t zhkJHxJjc}pIjbw#|)-UhIxS2ejGhKtGs^|)K2KPI8v$U`E&%+kVDesUG#a9lI5 zZj{Bj`OsCm#PTPBJFk$%Qcx)`iyct??d-MknAN@G-2pK|oMc|n1yc6lnw?90MN^xA z!IdPH{8UGz!k|15VG)O#{|aBH^Se=`87+2z9k3 zwl*?;Nk9NZ7MS2Fk2b~4W4sAO9&(IIQbi{Q0k3G!me!WGkzuo?-kP+$X2zial>{3( z@(+KYl2p47g|skrANeYCGUKZ62hWfp;zkE)fI6o#M#NGQVO`l}bO4RK7~6=Kxv;HQ z@UM0(%XX%@EB!4f3MGeyF9I7;lqpCD)D%12g%wB$%ibA|x2=V)hy&`)0kX(GhhMo3 z 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
!$6K$wU6sTsYdVuetj+77yMH?K|Ma7ZB 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 YN0-pstw$gc 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