Skip to content

String-Based Event Publishing with Dynamic Payload#25023

Merged
salihozkara merged 23 commits into
devfrom
issue-24918-event-bus
Mar 24, 2026
Merged

String-Based Event Publishing with Dynamic Payload#25023
salihozkara merged 23 commits into
devfrom
issue-24918-event-bus

Conversation

@salihozkara

@salihozkara salihozkara commented Mar 5, 2026

Copy link
Copy Markdown
Member

Related #24918

Summary

https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/POST.md

Add dynamic (string-based) event publishing and subscription support to the ABP EventBus. This allows publishing and subscribing to events using string names instead of CLR types, enabling runtime event handling for scenarios where event types are not known at compile time.

Key changes

  • DynamicEventData — Pure POCO envelope class with EventName and Data properties
  • IEventBus — New PublishAsync(string, object), Subscribe(string, ...), Unsubscribe(string, ...), UnsubscribeAll(string) overloads
  • IDistributedEventBus — New Subscribe(string, IDistributedEventHandler<DynamicEventData>) and PublishAsync(string, ..., useOutbox)
  • EventBusBaseResolveHandlerFactories, ResolveEventDataForHandler, ConvertDynamicEventData (virtual) for typed ↔ dynamic data conversion
  • All providers (RabbitMQ, Kafka, Azure, Rebus) — DynamicHandlerFactories dictionary, Subscribe(string), ProcessEventAsync handling
  • Dapr — Throws AbpException for dynamic subscriptions (Dapr requires startup-time topic declaration)
  • Outbox/Inbox — Dynamic events go through the same outbox/inbox pipeline as typed events

Design decisions

  • Dynamic events are NOT a separate system — they reuse the existing typed pipeline (DynamicEventData is treated as a "type")
  • Typed always wins: if a string event name matches a CLR type, data is auto-converted to the typed handler
  • ConvertDynamicEventData is virtual — providers can override it with their own serializer
  • No constructor signature changes — zero new dependencies added
  • DynamicEventData is a pure POCO (17 lines) — no serialization logic, no System.Text.Json dependency in Abstractions

Test results

Unit tests — 51 tests passed

Covers Local EventBus and LocalDistributedEventBus: subscribe, unsubscribe, unsubscribeAll, mixed handlers, typed-from-dynamic publish, dynamic-only publish, data conversion, and more.

Integration tests — 32 scenarios passed across 4 providers

Integration test sample: abpframework/abp-samples#309

Provider Direct Publish (5) Outbox (3) Total
Local (LocalDistributedEventBus) 8/8
RabbitMQ 8/8
Kafka 8/8
Rebus (InMemory) 8/8

Test scenarios per provider:

  1. TypedFromTyped — Typed publish → typed handler
  2. TypedFromDynamic — String-based publish → typed handler (auto-converted)
  3. DynamicOnly — String-based publish → dynamic handler (DynamicEventData)
  4. Unsubscribe — Dynamic handler disposed, no longer receives events
  5. MixedHandlers — Both typed and dynamic handlers triggered for same event
  6. TypedFromTyped (outbox) — Same as Basic server side modularity #1, through outbox pipeline
  7. TypedFromDynamic (outbox) — Same as Basic multi tenancy abstraction #2, through outbox pipeline
  8. DynamicOnly (outbox) — Same as Basic repository implementation #3, through outbox pipeline

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.
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.
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.
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.
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<object> 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.
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<T>/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.
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.
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.
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:*)".
@salihozkara salihozkara added this to the 10.3-preview milestone Mar 5, 2026
Copilot AI review requested due to automatic review settings March 5, 2026 19:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

This comment was marked as outdated.

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.

This comment was marked as outdated.

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.
Add native anonymous event support and simplify handling across transports. AnonymousEventData now contains conversion helpers (ConvertToTypedObject/ConvertToTypedObject<T>/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.
@salihozkara salihozkara requested a review from maliming March 10, 2026 19:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 49 out of 50 changed files in this pull request and generated 12 comments.

Files not reviewed (1)
  • test/DistEvents/DistDemoApp.EfCoreRabbitMq/Migrations/20210910152547_Added_Boxes_Initial.Designer.cs: Language not supported

Comment thread framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs Outdated
Comment thread framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs
maliming and others added 4 commits March 23, 2026 09:36
…remove dead IsDynamicEvent method from Dapr provider
…erializer 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
@github-actions

Copy link
Copy Markdown
Contributor

Images automagically compressed by Calibre's image-actions

Compression reduced images by 73%, saving 828.9 KB.

Filename Before After Improvement Visual comparison
docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/cover.png 1.1 MB 306.9 KB 73.0% View diff

@salihozkara salihozkara enabled auto-merge March 24, 2026 13:04
@salihozkara salihozkara disabled auto-merge March 24, 2026 13:05
@salihozkara salihozkara merged commit a172651 into dev Mar 24, 2026
1 check passed
@salihozkara salihozkara deleted the issue-24918-event-bus branch March 24, 2026 13:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants