Skip to content

Commit

Permalink
Handle Events With Delegate (#9)
Browse files Browse the repository at this point in the history
* Register a delegate for event handling

* Delegate handler load and exeption handling tests

* Delegate handler registration unit tests

* Delegate execution unit tests

* unit tests

* Stabilize unit tests

* Stabilize unit tests

* Stabilize unit tests

* Stabilize unit tests

* Stabilize unit tests

* Stabilize unit tests

* Stabilize unit tests

* Stabilize unit tests

* Stabilize unit tests

* Stabilize unit tests

* Typos, unused fields, spacing

* Update README

* Update README

* Update build ignore files

* Updare readme
  • Loading branch information
petar-m authored Jun 29, 2024
1 parent 895632d commit 2d172eb
Show file tree
Hide file tree
Showing 50 changed files with 1,822 additions and 271 deletions.
8 changes: 5 additions & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ csharp_style_expression_bodied_local_functions = false:silent
csharp_space_around_binary_operators = before_and_after
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:warning
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
Expand Down Expand Up @@ -119,6 +119,8 @@ dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_namespace_match_folder = true:suggestion
dotnet_style_allow_multiple_blank_lines_experimental = true:silent
dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
dotnet_style_allow_multiple_blank_lines_experimental = false:warning
dotnet_style_allow_statement_immediately_after_block_experimental = false:warning
insert_final_newline = true
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
12 changes: 6 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ on:
push:
branches: [ main ]
paths-ignore:
- "*.md"
- "*.png"
- "*.drawio"
- "README.md"
- "package-readme.md"
- "package-icon.png"
- ".gitignore"
tags:
- '[0-9]+.[0-9]+.[0-9]+*'
pull_request:
branches: [ main ]
paths-ignore:
- "*.md"
- "*.png"
- "*.drawio"
- "README.md"
- "package-readme.md"
- "package-icon.png"
- ".gitignore"

jobs:
Expand Down
238 changes: 186 additions & 52 deletions README.md

Large diffs are not rendered by default.

75 changes: 0 additions & 75 deletions docs/event_broker.drawio

This file was deleted.

Binary file removed docs/event_broker.png
Binary file not shown.
28 changes: 19 additions & 9 deletions package-readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ Features:
- in-memory, in-process
- publishing is *Fire and Forget* style
- events don't have to implement specific interface
- event handlers are runned on a `ThreadPool` threads
- event handlers are executed on a `ThreadPool` threads
- the number of concurrent handlers running can be limited
- built-in retry option
- tightly integrated with Microsoft.Extensions.DependencyInjection
- each handler is resolved and runned in a new DI container scopee
- each handler is resolved and executed in a new DI container scope
- **NEW** event handlers can be delegates

# How does it work

Define an event handler by implementing `IEventHadler<TEvent>` interface:
Implement an event handler by implementing `IEventHandler<TEvent>` interface:

```csharp
public record SomeEvent(string Message);
Expand All @@ -33,18 +34,28 @@ public class SomeEventHandler : IEventHandler<SomeEvent>

public async Task OnError(Exception exception, SomeEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken)
{
// called on unhandled exeption from Handle
// called on unhandled exception from Handle
// optionally use retryPolicy.RetryAfter(TimeSpan)
}
}
```
```

or use `DelegateHandlerRegistryBuilder` to register delegate as handler:

Use `AddEventBroker` extension method to register `IEventBroker` and handlers:
```csharp
DelegateHandlerRegistryBuilder builder = new();
builder.RegisterHandler<SomeEvent>(
static async (SomeEvent someEvent, ISomeService service, CancellationToken cancellationToken) =>
{
await service.DoSomething(someEvent, cancellationToken);
});
```

Add event broker implementation to DI container using `AddEventBroker` extension method and register handlers, optionally add delegate handler registries:

```csharp
serviceCollection.AddEventBroker(
x => x.AddTransient<SomeEvent, SomeEventHandler>());
serviceCollection.AddEventBroker(x => x.AddTransient<SomeEvent, SomeEventHandler>())
.AddSingleton(builder);
```

Inject `IEventBroker` and publish events:
Expand All @@ -66,4 +77,3 @@ class MyClass
}
}
```

Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using M.EventBrokerSlim.Internal;

namespace M.EventBrokerSlim.DependencyInjection;

/// <summary>
/// Used to define event handlers as delegates.
/// </summary>
public class DelegateHandlerRegistryBuilder
{
/// <summary>
/// Registers an event handler delegate returning <see cref="Task"/>.
/// All of the parameters will be resolved from new DI container scope and injected. The scope will be disposed after delegate execution.
/// <br/>Tip: Use <see langword="static"/> keyword for the anonymous delegate to avoid accidential closure.
/// </summary>
/// <remarks>
/// Special objects available (without being registered in DI container):
/// <list type="bullet">
/// <item><typeparamref name="TEvent"/> - an instance of the event being handled.</item>
/// <item><see cref="CancellationToken"/> - a cancellation token that should be used to cancel the work.</item>
/// <item><see cref="IRetryPolicy" /> - provides ability to request a retry for the same event by the handler. Do not keep a reference to this instance, it may be pooled and reused</item>
/// </list>
/// </remarks>
/// <typeparam name="TEvent">The type of the event being handled.</typeparam>
/// <param name="handler">A delegate returning <see cref="Task"/> that will be executed when event of type <typeparamref name="TEvent"/> is published.</param>
/// <returns>Object allowing to fluently continue registering handlers or build handler pipeline.</returns>
/// <exception cref="InvalidOperationException">Thrown when registry is closed for new registrations. Usually after an instance of <see cref="IEventBroker"/> has been resolved.</exception>
public DelegateHandlerWrapperBuilder RegisterHandler<TEvent>(Delegate handler)
{
if(IsClosed)
{
throw new InvalidOperationException("Registry is closed. Please complete registrations before IEventBroker is resolved.");
}

var descriptor = DelegateHelper.BuildDelegateHandlerDescriptor(handler, typeof(TEvent));
HandlerDescriptors.Add(descriptor);
return new DelegateHandlerWrapperBuilder(this, descriptor);
}

/// <summary>
/// Indicates whether the registry can still be used to register event handlers.
/// </summary>
public bool IsClosed { get; private set; }

internal List<DelegateHandlerDescriptor> HandlerDescriptors { get; } = new();

internal static DelegateHandlerRegistry Build(IEnumerable<DelegateHandlerRegistryBuilder> builders)
{
return new DelegateHandlerRegistry(
builders.SelectMany(x =>
{
x.IsClosed = true;
return x.HandlerDescriptors;
}));
}

/// <summary>
/// Used to define pipeline of delegates wrapping the event handler.
/// </summary>
/// <remarks>
/// Order of execution is in reverse of registration order. The last registered is executed first, moving "inwards" toward the handler.
/// </remarks>
public class DelegateHandlerWrapperBuilder
{
private readonly DelegateHandlerRegistryBuilder _builder;
private readonly DelegateHandlerDescriptor _handlerDescriptor;

internal DelegateHandlerWrapperBuilder(DelegateHandlerRegistryBuilder builder, DelegateHandlerDescriptor handlerDescriptor)
{
_builder = builder;
_handlerDescriptor = handlerDescriptor;
}

/// <summary>
/// Returns the current <see cref="DelegateHandlerRegistryBuilder"/> instance, allowing to continue registering event handlers.
/// </summary>
/// <returns>Current <see cref="DelegateHandlerRegistryBuilder"/> instance.</returns>
public DelegateHandlerRegistryBuilder Builder() => _builder;

/// <summary>
/// Registers a delegate returning <see cref="Task"/> executed before the event handler delegate. Use <see cref="INextHandler.Execute"/> to call the next wrapper in the chain.
/// All of the parameters will be resolved from new DI container scope and injected. All wrappers and the handler share the same DI container scope. Order of execution is in reverse of registration order. The last registered is executed first, moving "inwards" toward the handler.
/// <br/>Tip: Use <see langword="static"/> keyword for the anonymous delegate to avoid accidential closure.
/// </summary>
/// <remarks>
/// Special objects available (without being registered in DI container):
/// <list type="bullet">
/// <item><see cref="INextHandler" /> - used to call the next wrapper in the chain or the handler if no more wrappers available. Do not keep a reference to this instance, it may be pooled and reused</item>
/// <item>TEvent - an instance of the event being handled.</item>
/// <item><see cref="CancellationToken"/> - a cancellation token that should be used to cancel the work.</item>
/// <item><see cref="IRetryPolicy" /> - provides ability to request a retry for the same event by the handler. Do not keep a reference to this instance, it may be pooled and reused</item>
/// </list>
/// </remarks>
/// <param name="wrapper">A delegate returning <see cref="Task"/> that will be executed when event of type TEvent is published, before the handler delegate.</param>
/// <returns>Object allowing to fluently continue registering handlers or build handler pipeline.</returns>
/// <exception cref="InvalidOperationException">Thrown when registry is closed for new registrations. Usually after an instance of <see cref="IEventBroker"/> has been resolved.</exception>
public DelegateHandlerWrapperBuilder WrapWith(Delegate wrapper)
{
if(_builder.IsClosed)
{
throw new InvalidOperationException("Registry is closed. Please complete registrations before IEventBroker is resolved.");
}

var descriptor = DelegateHelper.BuildDelegateHandlerDescriptor(wrapper, _handlerDescriptor.EventType);
_handlerDescriptor.Pipeline.Add(descriptor);
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public EventBrokerBuilder WithMaxConcurrentHandlers(int maxConcurrentHandlers)
{
if(maxConcurrentHandlers <= 0)
{
throw new ArgumentOutOfRangeException(nameof(maxConcurrentHandlers), "MaxConcurrentHandlers should be greater than zero");
throw new ArgumentOutOfRangeException(nameof(maxConcurrentHandlers), "MaxConcurrentHandlers should be greater than zero.");
}

_maxConcurrentHandlers = maxConcurrentHandlers;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
namespace M.EventBrokerSlim.DependencyInjection;

/// <summary>
/// Registers EventBorker event handlers in DI container.
/// Registers EventBroker event handlers in DI container.
/// </summary>
public class EventHandlerRegistryBuilder
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public static IServiceCollection AddEventBroker(
x.GetRequiredKeyedService<Channel<object>>(eventBrokerKey),
x.GetRequiredService<IServiceScopeFactory>(),
x.GetRequiredService<EventHandlerRegistry>(),
x.GetRequiredService<DelegateHandlerRegistry>(),
x.GetRequiredKeyedService<CancellationTokenSource>(eventBrokerKey),
x.GetService<ILogger<ThreadPoolEventHandlerRunner>>()));

Expand All @@ -67,6 +68,13 @@ public static IServiceCollection AddEventBroker(
return EventHandlerRegistryBuilder.Build(builders);
});

serviceCollection.AddSingleton(
x =>
{
var builders = x.GetServices<DelegateHandlerRegistryBuilder>();
return DelegateHandlerRegistryBuilder.Build(builders);
});

return serviceCollection;
}

Expand Down
3 changes: 1 addition & 2 deletions src/M.EventBrokerSlim/IEventHandler.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using M.EventBrokerSlim.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

Expand All @@ -22,7 +21,7 @@ public interface IEventHandler<TEvent>
Task Handle(TEvent @event, IRetryPolicy retryPolicy, CancellationToken cancellationToken);

/// <summary>
/// Called when an unhadled exception is caught during execution.
/// Called when an unhandled exception is caught during execution.
/// Exceptions thrown from this method are swallowed.
/// If there is <see cref="ILogger"/> configured in the <see cref="IServiceCollection"/> an Error will be logged.
/// </summary>
Expand Down
Loading

0 comments on commit 2d172eb

Please sign in to comment.