mirror of https://github.com/abpframework/abp.git
2 changed files with 203 additions and 0 deletions
@ -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<TEvent>` 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<string, object>`, 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<OrderPlacedEto> |
||||
|
{ |
||||
|
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<IDistributedEventBus>(); |
||||
|
|
||||
|
// Subscribe to a dynamic event — no event class needed |
||||
|
eventBus.Subscribe("PartnerOrderReceived", |
||||
|
new PartnerOrderHandler(context.ServiceProvider)); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
The handler implements `IDistributedEventHandler<DynamicEventData>`: |
||||
|
|
||||
|
```csharp |
||||
|
public class PartnerOrderHandler : IDistributedEventHandler<DynamicEventData> |
||||
|
{ |
||||
|
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<IPartnerOrderProcessor>(); |
||||
|
|
||||
|
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<OrderPlacedEto, OrderEmailHandler>(); |
||||
|
|
||||
|
// 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<ILocalEventBus>(); |
||||
|
|
||||
|
// Subscribe dynamically |
||||
|
localEventBus.Subscribe("UserActivityTracked", |
||||
|
new SingleInstanceHandlerFactory( |
||||
|
new ActionEventHandler<DynamicEventData>(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) |
||||
|
After Width: | Height: | Size: 1.1 MiB |
Loading…
Reference in new issue