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 0000000000..697371a6c0 Binary files /dev/null and b/docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/cover.png differ