Browse Source

Merge pull request #25200 from abpframework/maliming/dynamic-events-docs-fix

docs: fix dynamic events subscription pattern — use `IocEventHandlerFactory` instead of raw handler instances
pull/25201/head
Halil İbrahim Kalkan 2 months ago
committed by GitHub
parent
commit
9286a6641b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 32
      docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/POST.md
  2. 49
      docs/en/framework/infrastructure/event-bus/distributed/index.md
  3. 110
      framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs

32
docs/en/Community-Articles/2026-03-23-Dynamic-Events-in-ABP/POST.md

@ -72,40 +72,45 @@ Publishing by name with `new { OrderId = ..., CustomerEmail = ... }` triggers th
Dynamic subscription lets you register event handlers at runtime, using a string event name.
The recommended approach is to use `IocEventHandlerFactory`, which is the same mechanism ABP uses internally for typed handlers. It creates a new DI scope for each event, resolves a fresh handler instance, calls `HandleEventAsync`, then disposes the scope — so the handler can use normal constructor injection without any manual scope management:
```csharp
public override async Task OnApplicationInitializationAsync(
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddTransient<PartnerOrderHandler>();
}
public override void OnApplicationInitialization(
ApplicationInitializationContext context)
{
var eventBus = context.ServiceProvider
.GetRequiredService<IDistributedEventBus>();
var scopeFactory = context.ServiceProvider
.GetRequiredService<IServiceScopeFactory>();
// Subscribe to a dynamic event — no event class needed
eventBus.Subscribe("PartnerOrderReceived",
new PartnerOrderHandler(context.ServiceProvider));
new IocEventHandlerFactory(scopeFactory, typeof(PartnerOrderHandler)));
}
```
The handler implements `IDistributedEventHandler<DynamicEventData>`:
The handler implements `IDistributedEventHandler<DynamicEventData>` and injects its dependencies normally:
```csharp
public class PartnerOrderHandler : IDistributedEventHandler<DynamicEventData>
{
private readonly IServiceProvider _serviceProvider;
private readonly IPartnerOrderProcessor _orderProcessor;
public PartnerOrderHandler(IServiceProvider serviceProvider)
public PartnerOrderHandler(IPartnerOrderProcessor orderProcessor)
{
_serviceProvider = serviceProvider;
_orderProcessor = orderProcessor;
}
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);
await _orderProcessor.ProcessAsync(eventData.EventName, eventData.Data);
}
}
```
@ -115,7 +120,7 @@ public class PartnerOrderHandler : IDistributedEventHandler<DynamicEventData>
- **`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.
> `Subscribe` returns an `IDisposable`. Call `Dispose()` to unsubscribe the handler at runtime. For application-lifetime subscriptions, prefer module initialization (`OnApplicationInitializationAsync`) over subscribing inside an application service.
## Mixed Typed and Dynamic Handlers
@ -126,7 +131,8 @@ Typed and dynamic handlers coexist naturally. When both are registered for the s
eventBus.Subscribe<OrderPlacedEto, OrderEmailHandler>();
// Dynamic handler — receives DynamicEventData for the same event
eventBus.Subscribe("OrderPlaced", new AuditLogHandler());
eventBus.Subscribe("OrderPlaced",
new IocEventHandlerFactory(scopeFactory, typeof(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.

49
docs/en/framework/infrastructure/event-bus/distributed/index.md

@ -757,7 +757,44 @@ await distributedEventBus.PublishAsync(
### Subscribing to Dynamic Events
Use the `Subscribe` overload that accepts a string event name:
The recommended way to subscribe is to implement `IDistributedEventHandler<DynamicEventData>` and use `IocEventHandlerFactory`. This mirrors how ABP manages typed handlers — it creates a new DI scope per event, resolves a fresh handler instance, calls `HandleEventAsync`, then disposes the scope:
````csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddTransient<MyDynamicEventHandler>();
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var eventBus = context.ServiceProvider.GetRequiredService<IDistributedEventBus>();
var scopeFactory = context.ServiceProvider.GetRequiredService<IServiceScopeFactory>();
eventBus.Subscribe("MyDynamicEvent", new IocEventHandlerFactory(scopeFactory, typeof(MyDynamicEventHandler)));
}
````
The handler uses normal constructor injection — no manual scope management needed:
````csharp
public class MyDynamicEventHandler : IDistributedEventHandler<DynamicEventData>
{
private readonly IMyService _myService;
public MyDynamicEventHandler(IMyService myService)
{
_myService = myService;
}
public async Task HandleEventAsync(DynamicEventData eventData)
{
await _myService.ProcessAsync(eventData.EventName, eventData.Data);
}
}
````
`Subscribe` returns an `IDisposable`. Call `Dispose()` to unsubscribe at runtime.
For simple stateless handlers that do not need DI services, you can also use `SingleInstanceHandlerFactory` with an inline handler:
````csharp
var subscription = distributedEventBus.Subscribe(
@ -765,10 +802,8 @@ var subscription = distributedEventBus.Subscribe(
new SingleInstanceHandlerFactory(
new ActionEventHandler<DynamicEventData>(eventData =>
{
// Access the event name and raw data
var name = eventData.EventName;
var data = eventData.Data;
return Task.CompletedTask;
})));
@ -776,13 +811,7 @@ var subscription = distributedEventBus.Subscribe(
subscription.Dispose();
````
You can also subscribe using a typed distributed event handler:
````csharp
distributedEventBus.Subscribe("MyDynamicEvent", myDistributedEventHandler);
````
Where `myDistributedEventHandler` implements `IDistributedEventHandler<DynamicEventData>`.
> Do not inject `IServiceProvider` directly into a `SingleInstanceHandlerFactory`-based handler. Since the same instance is reused for every event, resolving scoped services directly from the root container causes a captive dependency and may throw a scope validation exception in development. Use `IocEventHandlerFactory` instead.
### Mixed Typed and Dynamic Handlers

110
framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_Test.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.EventBus.Local;
@ -372,6 +373,57 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase
}
}
[Fact]
public async Task IocEventHandlerFactory_Should_Create_New_Scope_And_Dispose_Handler_Per_Event()
{
var handleCount = 0;
var disposeCount = 0;
var eventName = "IocEvent-" + Guid.NewGuid().ToString("N");
var services = new ServiceCollection();
services.AddSingleton<ITestCounterService>(
new TestCounterService(() => handleCount++, () => disposeCount++));
services.AddTransient<DynamicIocEventHandlerWithCounter>();
var provider = services.BuildServiceProvider();
var scopeFactory = provider.GetRequiredService<IServiceScopeFactory>();
using var subscription = DistributedEventBus.Subscribe(
eventName,
new IocEventHandlerFactory(scopeFactory, typeof(DynamicIocEventHandlerWithCounter)));
await DistributedEventBus.PublishAsync(eventName, new { Value = 1 });
await DistributedEventBus.PublishAsync(eventName, new { Value = 2 });
await DistributedEventBus.PublishAsync(eventName, new { Value = 3 });
// Handler is invoked exactly once per event.
Assert.Equal(3, handleCount);
// Handler is disposed at least once per event (the scope is always cleaned up).
Assert.True(disposeCount >= handleCount);
}
[Fact]
public async Task IocEventHandlerFactory_Should_Resolve_DI_Services_In_Handler_Constructor()
{
var callCount = 0;
var eventName = "IocEvent-" + Guid.NewGuid().ToString("N");
var services = new ServiceCollection();
services.AddSingleton<ITestCounterService>(new TestCounterService(() => callCount++));
services.AddTransient<DynamicIocEventHandlerWithService>();
var provider = services.BuildServiceProvider();
var scopeFactory = provider.GetRequiredService<IServiceScopeFactory>();
using var subscription = DistributedEventBus.Subscribe(
eventName,
new IocEventHandlerFactory(scopeFactory, typeof(DynamicIocEventHandlerWithService)));
await DistributedEventBus.PublishAsync(eventName, new { Value = 1 });
await DistributedEventBus.PublishAsync(eventName, new { Value = 2 });
// The handler resolved ITestCounterService via constructor injection
Assert.Equal(2, callCount);
}
class TestDynamicDistributedEventHandler : IDistributedEventHandler<DynamicEventData>
{
private readonly Action _onHandle;
@ -387,4 +439,62 @@ public class LocalDistributedEventBus_Test : LocalDistributedEventBusTestBase
return Task.CompletedTask;
}
}
interface ITestCounterService
{
void OnHandle();
void OnDispose();
}
class TestCounterService : ITestCounterService
{
private readonly Action _onHandle;
private readonly Action? _onDispose;
public TestCounterService(Action onHandle, Action? onDispose = null)
{
_onHandle = onHandle;
_onDispose = onDispose;
}
public void OnHandle() => _onHandle();
public void OnDispose() => _onDispose?.Invoke();
}
class DynamicIocEventHandlerWithCounter : IDistributedEventHandler<DynamicEventData>, IDisposable
{
private readonly ITestCounterService _counterService;
public DynamicIocEventHandlerWithCounter(ITestCounterService counterService)
{
_counterService = counterService;
}
public Task HandleEventAsync(DynamicEventData eventData)
{
_counterService.OnHandle();
return Task.CompletedTask;
}
public void Dispose()
{
_counterService.OnDispose();
}
}
class DynamicIocEventHandlerWithService : IDistributedEventHandler<DynamicEventData>
{
private readonly ITestCounterService _counterService;
public DynamicIocEventHandlerWithService(ITestCounterService counterService)
{
_counterService = counterService;
}
public Task HandleEventAsync(DynamicEventData eventData)
{
_counterService.OnHandle();
return Task.CompletedTask;
}
}
}

Loading…
Cancel
Save