From 040cba6b74f1fcd773d39f9374c743a1da9b5b84 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 2 Apr 2026 15:40:35 +0800 Subject: [PATCH] docs: fix dynamic events article and update distributed event bus docs --- .../2026-03-23-Dynamic-Events-in-ABP/POST.md | 32 ++--- .../event-bus/distributed/index.md | 49 ++++++-- .../LocalDistributedEventBus_Test.cs | 110 ++++++++++++++++++ 3 files changed, 168 insertions(+), 23 deletions(-) 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 index cdc1a03f58..5bf893bf1b 100644 --- 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 @@ -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(); +} + +public override void OnApplicationInitialization( ApplicationInitializationContext context) { var eventBus = context.ServiceProvider .GetRequiredService(); + var scopeFactory = context.ServiceProvider + .GetRequiredService(); // 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`: +The handler implements `IDistributedEventHandler` and injects its dependencies normally: ```csharp public class PartnerOrderHandler : IDistributedEventHandler { - 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(); - - await orderProcessor.ProcessAsync(eventData.EventName, eventData.Data); + await _orderProcessor.ProcessAsync(eventData.EventName, eventData.Data); } } ``` @@ -115,7 +120,7 @@ public class PartnerOrderHandler : IDistributedEventHandler - **`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(); // 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. diff --git a/docs/en/framework/infrastructure/event-bus/distributed/index.md b/docs/en/framework/infrastructure/event-bus/distributed/index.md index 5f2c7b11ec..54baeefcac 100644 --- a/docs/en/framework/infrastructure/event-bus/distributed/index.md +++ b/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` 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(); +} + +public override void OnApplicationInitialization(ApplicationInitializationContext context) +{ + var eventBus = context.ServiceProvider.GetRequiredService(); + var scopeFactory = context.ServiceProvider.GetRequiredService(); + eventBus.Subscribe("MyDynamicEvent", new IocEventHandlerFactory(scopeFactory, typeof(MyDynamicEventHandler))); +} +```` + +The handler uses normal constructor injection — no manual scope management needed: + +````csharp +public class MyDynamicEventHandler : IDistributedEventHandler +{ + 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(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`. +> 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 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 b77f282a85..173d5a8531 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,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( + new TestCounterService(() => handleCount++, () => disposeCount++)); + services.AddTransient(); + var provider = services.BuildServiceProvider(); + var scopeFactory = provider.GetRequiredService(); + + 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(new TestCounterService(() => callCount++)); + services.AddTransient(); + var provider = services.BuildServiceProvider(); + var scopeFactory = provider.GetRequiredService(); + + 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 { 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, 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 + { + private readonly ITestCounterService _counterService; + + public DynamicIocEventHandlerWithService(ITestCounterService counterService) + { + _counterService = counterService; + } + + public Task HandleEventAsync(DynamicEventData eventData) + { + _counterService.OnHandle(); + return Task.CompletedTask; + } + } }