Releases: petar-m/EventBrokerSlim
3.2.0
What's Changed
New Feature: Dynamic Delegate Event Handlers
Delegate handlers can be added or removed after DI container was built. Dynamic delegate handlers are created using DelegateHandlerRegistryBuilder
and support all delegate handler features (retries, wrappers, etc.).
EventBroker registration adds IDynamicEventHandlers
which is used for managing handlers. Adding handlers returns IDynamicHandlerClaimTicket
used to remove the handlers. Since DelegateHandlerRegistryBuilder
can define multiple handlers, all of them will be removed by the IDynamicHandlerClaimTicket
instance.
public class DynamicEventHandlerExample : IDisposable
{
private readonly IDynamicEventHandlers _dynamicEventHandlers;
private readonly IDynamicHandlerClaimTicket _claimTicket;
public DynamicEventHandlerExample(IDynamicEventHandlers dynamicEventHandlers)
{
_dynamicEventHandlers = dynamicEventHandlers;
DelegateHandlerRegistryBuilder handlerRegistryBuilder = new();
// Define two handlers for different events
handlerRegistryBuilder
.RegisterHandler<Event1>(HandleEvent1)
.Builder()
.RegisterHandler<Event2>(HandleEvent2);
// Register with the event broker and keep a claim ticket
_claimTicket = _dynamicEventHandlers.Add(handlerRegistryBuilder);
}
// All delegate features are available, including injecting services registered in DI
private async Task HandleEvent1(Event1 event1, IRetryPolicy retryPolicy, ISomeService someService)
{
// event processing
}
private async Task HandleEvent2(Event2 event2)
{
// event processing
}
public void Dispose()
{
// Remove both event handlers using the IDynamicHandlerClaimTicket
_dynamicEventHandlers.Remove(_claimTicket);
}
}
Important
Make sure handlers are removed if containing classes are ephemeral.
Commits
Full Changelog: 3.1.0...3.2.0
3.1.0
What's Changed
New Feature: Delegate Handlers
Use DelegateHandlerRegistryBuilder
to register delegate as handler:
DelegateHandlerRegistryBuilder builder = new();
builder.RegisterHandler<SomeEvent>(
static async (SomeEvent someEvent, ISomeService service, CancellationToken cancellationToken) =>
{
await service.DoSomething(someEvent, cancellationToken);
});
Delegate must return Task
and can have 0 to 16 parameters. Parameter instances are resolved from DI container scope and passed when the delegate is invoked.
There are few special cases of optional parameters managed by EventBroker
(without being registered in DI container):
TEvent
- an instance of the event being handled. Should match the type of the event the delegate was registered for.IRetryPolicy
- the instance of the retry policy for the handler.CancellationToken
- theEventBroker
cancellation token.INextHandler
- used to call the next wrapper in the chain or the handler if no more wrappers available (see below).
Delegate handlers registration has a decorator-like feature allowing to pipeline multiple delegates. The INextHandler
instance is used to call the next in the pipeline.
builder.RegisterHandler<SomeEvent>(
static async (SomeEvent someEvent, ISomeService someService) => await someService.DoSomething())
.WrapWith(
static async (INextHandler next, ILogger logger)
{
try
{
await next.Execute();
}
catch(Exception ex)
{
logger.LogError(ex);
}
})
.WrapWith(
static async (SomeEvent someEvent, ILogger logger)
{
Stopwatch timer = new();
await next.Execute();
timer.Stop();
logger.LogInformation("{event} handling duration {elapsed}", someEvent, timer.Elapsed);
});
Delegate wrappers are executed from the last registered moving "inwards" toward the handler.
Commits
Full Changelog: 3.0.0...3.1.0
3.0.0
What's Changed
Breaking Changes
IEventHandler<T>
methods now take parameterIRetryPolicy
instead ofRetryPolicy
before:
public interface IEventHandler<TEvent>
{
Task Handle(TEvent @event, RetryPolicy retryPolicy, CancellationToken cancellationToken);
Task OnError(Exception exception, TEvent @event, RetryPolicy retryPolicy, CancellationToken cancellationToken);
}
after:
public interface IEventHandler<TEvent>
{
Task Handle(TEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken);
Task OnError(Exception exception, TEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken);
}
Commits
2.0.0
What's Changed
Breaking Changes
IEventHandler<T>
methods take additional parameterRetryPolicy
before:
public interface IEventHandler<TEvent>
{
Task Handle(TEvent @event, CancellationToken cancellationToken);
Task OnError(Exception exception, TEvent @event, CancellationToken cancellationToken);
}
after:
public interface IEventHandler<TEvent>
{
Task Handle(TEvent @event, RetryPolicy retryPolicy, CancellationToken cancellationToken);
Task OnError(Exception exception, TEvent @event, RetryPolicy retryPolicy, CancellationToken cancellationToken);
}
EventHandlerRegistryBuilder
registration methods renamed to omit 'Keyed' word
before:
services.AddEventHandlers(
x => x.AddKeyedTransient<Event1, EventHandler1>()
.AddKeyedScoped<Event2, EventHandler2>()
.AddKeyedSingleton<Event3, EventHandler3>())
after:
services.AddEventHandlers(
x => x.AddTransient<Event1, EventHandler1>()
.AddScoped<Event2, EventHandler2>()
.AddSingleton<Event3, EventHandler3>())
New Features
Retries
Retrying within event hadler can become a bottleneck. Imagine EventBroker
is restricted to one concurrent handler. An exception is caught in Handle
and retry is attempted after given time interval. Since Handle
is not completed, there is no available "slot" to run other handlers while Handle
is waiting.
Another option will be to use IEventBroker.PublishDeferred
. This will eliminate the bottleneck but will itroduce different problems. The same event will be handled again by all handlers, meaning specaial care should be taken to make all handlers idempotent. Any additional information (e.g. number of retries) needs to be known, it should be carried with the event, introducing accidential complexity.
To avoid these problems, both IEventBroker
Handle
and OnError
methods have RetryPolicy
parameter.
RetryPolicy.RetryAfter()
will schedule a retry only for the handler it is called from, without blocking. After the given time interval an instance of the handler will be resolved from the DI container (from a new scope) and executed with the same event instance.
RetryPolicy.Attempt
is the current retry attempt for a given handler and event.
RetryPolicy.LastDelay
is the time interval before the retry.
RetryPolicy.RetryRequested
is used to coordinate retry request between Handle
and OnError
. RetryPolicy
is passed to both methods to enable error handling and retry request entirely in Handle
method. OnError
can check RetryPolicy.RetryRequested
to know whether Hanlde
had called RetryPolicy.RetryAfter()
.
Caution: the retry will not be exactly after the specified time interval in RetryPolicy.RetryAfter()
. Take into account a tolerance of around 50 milliseconds. Additionally, retry executions respect maximum concurrent handlers setting, meaning a high load can cause additional delay.
Commits
- Update unit tests project dependencies (#8) (522f6a6 by @petar-m)
- Add retry feature (6abda82 by @petar-m)
- Output unit tests and code coverage as job summary (c33eff0 by @petar-m)
- Adjust unit test wait times (195a25d by @petar-m)
- Add global using in unit tests (9f994ad by @petar-m)
- Add Directory.Build.props, enable nullable types for unit tests (4b73463 by @petar-m)
- Update GitHub actions (83c30a3 by @petar-m)
- Use ImmutableArray for event handler descriptors (573e890 by @petar-m)
- Increase wait time on unit test (6477d03 by @petar-m)
- Run Code Cleanup for solution (bb03140 by @petar-m)
- Use NullLogger when logging not configured (283e528 by @petar-m)
- Adjust unit tests wait times (887c946 by @petar-m)
- Adjust unit tests wait times (de77467 by @petar-m)
- Adjust unit tests wait times (e13d843 by @petar-m)
- Remove 'Keyed' from EventHandlerRegistryBuilder methods (2880907 by @petar-m)
- Improve EventsRecorder false positives (2cff12d by @petar-m)
- Adjust unit tests wait times (4a46a0b by @petar-m)
1.0.0
1.0.0-preview3
What's Changed
- Add deferred publishing by @petar-m in #5
- Cancel deferred and pending tasks on shutdown by @petar-m in #6
Full Changelog: 1.0.0-preview2...1.0.0-preview3
1.0.0-preview2
1.0.0-preview1
Initial preview release.