feat: add pub/sub support to dispatcher
All checks were successful
default / dotnet-default-workflow (pull_request) Successful in 1m56s
default / dotnet-default-workflow (push) Successful in 1m49s

Notifications are broadcast to zero or more handlers with configurable
publishing strategies (sequential or parallel) and pipeline behavior
support.
This commit is contained in:
Louis Seubert 2026-07-01 22:00:05 +02:00
commit 9232716901
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
27 changed files with 814 additions and 7 deletions

View file

@ -42,6 +42,7 @@ To have a consistent experience across all packages, some public interfaces have
### Added ### Added
- **request.validation:** Support `PropertyPath` JSON converter for string values and dictionary property names - **request.validation:** Support `PropertyPath` JSON converter for string values and dictionary property names
- **request.dispatcher:** Notification (pub/sub) support with `INotification`
### Changed ### Changed

View file

@ -0,0 +1,74 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Microsoft.Extensions.DependencyInjection;
namespace Geekeey.Request.Dispatcher.Tests;
internal sealed class NotificationBehaviourTests
{
[Test]
public async Task I_can_execute_a_single_behaviour_wrapping_the_publisher()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(TestNotificationHandler))
.Add(typeof(TestNotificationBehavior))
.UseSequentialNotificationPublisher());
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<NotificationTestTracker>();
await dispatcher.PublishAsync(new TestNotification { Value = "Wrapped" });
await Assert.That(tracker.Log).Count().IsEqualTo(3);
await Assert.That(tracker.Log[0]).IsEquivalentTo("Before");
await Assert.That(tracker.Log[1]).IsEquivalentTo("Handler:Wrapped");
await Assert.That(tracker.Log[2]).IsEquivalentTo("After");
}
[Test]
public async Task I_can_chain_behaviours_in_registration_order()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(TestNotificationHandler))
.Add(typeof(NotificationChainedBehaviour1))
.Add(typeof(NotificationChainedBehaviour2))
.UseSequentialNotificationPublisher());
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<NotificationTestTracker>();
await dispatcher.PublishAsync(new TestNotification { Value = "Chain" });
// behaviours.Reverse() wrapping means Behaviour1 wraps Behaviour2 wraps publisher
// Execution: Behaviour1 before -> Behaviour2 before -> handler -> Behaviour2 after -> Behaviour1 after
await Assert.That(tracker.Log).Count().IsEqualTo(3);
await Assert.That(tracker.Log[0]).IsEquivalentTo("Behaviour1");
await Assert.That(tracker.Log[1]).IsEquivalentTo("Behaviour2");
await Assert.That(tracker.Log[2]).IsEquivalentTo("Handler:Chain");
}
[Test]
public async Task I_can_see_a_behaviour_that_exits_early_blocks_subsequent_execution()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(TestNotificationHandler))
.Add(typeof(BlockingNotificationBehavior))
.UseSequentialNotificationPublisher());
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<NotificationTestTracker>();
await dispatcher.PublishAsync(new TestNotification { Value = "Blocked" });
// BlockingNotificationBehavior does not call next, so the handler should not execute.
await Assert.That(tracker.Log).Count().IsEqualTo(1);
await Assert.That(tracker.Log[0]).IsEquivalentTo("Blocked");
}
}

View file

@ -0,0 +1,143 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Microsoft.Extensions.DependencyInjection;
namespace Geekeey.Request.Dispatcher.Tests;
internal sealed class NotificationDispatcherTests
{
[Test]
public async Task I_can_publish_a_notification_to_a_single_handler()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(TestNotificationHandler))
.UseSequentialNotificationPublisher());
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<NotificationTestTracker>();
await dispatcher.PublishAsync(new TestNotification { Value = "Hello" });
await Assert.That(tracker.Log).Count().IsEqualTo(1);
await Assert.That(tracker.Log[0]).IsEquivalentTo("Handler:Hello");
}
[Test]
public async Task I_can_publish_a_notification_to_multiple_handlers()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(TestNotificationHandler))
.Add(typeof(SecondTestNotificationHandler))
.UseSequentialNotificationPublisher());
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<NotificationTestTracker>();
await dispatcher.PublishAsync(new TestNotification { Value = "Multi" });
await Assert.That(tracker.Log).Count().IsEqualTo(2);
await Assert.That(tracker.Log[0]).IsEquivalentTo("Handler:Multi");
await Assert.That(tracker.Log[1]).IsEquivalentTo("SecondHandler:Multi");
}
[Test]
public async Task I_can_publish_a_notification_with_zero_handlers()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.UseSequentialNotificationPublisher());
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
await dispatcher.PublishAsync(new TestNotification { Value = "NoOp" });
}
[Test]
public async Task I_can_publish_a_notification_to_an_open_generic_handler()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(OpenNotificationHandler<>))
.UseSequentialNotificationPublisher());
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<NotificationTestTracker>();
await dispatcher.PublishAsync(new TestNotification { Value = "Open" });
await Assert.That(tracker.Log).Count().IsEqualTo(1);
await Assert.That(tracker.Log[0]).IsEquivalentTo("OpenHandler:TestNotification");
}
[Test]
public async Task I_can_see_the_exception_propagate_from_a_failing_handler()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(FailingNotificationHandler))
.Add(typeof(SecondTestNotificationHandler))
.UseSequentialNotificationPublisher());
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
dispatcher.PublishAsync(new TestNotification { Value = "Fail" }));
await Assert.That(ex?.Message).IsEquivalentTo("Handler failed");
}
[Test]
public async Task I_can_see_subsequent_handlers_are_skipped_after_a_handler_failure()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(FailingNotificationHandler))
.Add(typeof(SecondTestNotificationHandler))
.UseSequentialNotificationPublisher());
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<NotificationTestTracker>();
try
{
await dispatcher.PublishAsync(new TestNotification { Value = "Skip" });
}
catch (InvalidOperationException)
{
}
// Failing handler throws, so the second handler should not be executed
await Assert.That(tracker.Log).IsEmpty();
}
[Test]
public async Task I_can_see_inherited_notification_types_are_handled()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(OpenNotificationHandler<>))
.UseSequentialNotificationPublisher());
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<NotificationTestTracker>();
await dispatcher.PublishAsync(new DerivedNotification { Value = "Derived" });
await Assert.That(tracker.Log).Count().IsEqualTo(1);
await Assert.That(tracker.Log[0]).IsEquivalentTo("OpenHandler:DerivedNotification");
}
private sealed class DerivedNotification : TestNotification
{
}
}

View file

@ -0,0 +1,146 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Microsoft.Extensions.DependencyInjection;
namespace Geekeey.Request.Dispatcher.Tests;
internal sealed class NotificationPublisherStrategyTests
{
[Test]
public async Task I_can_use_a_custom_publisher_via_per_call_parameter()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(TestNotificationHandler)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<NotificationTestTracker>();
var customPub = new CustomNotificationPublisher(tracker);
await dispatcher.PublishAsync(new TestNotification { Value = "PerCall" }, publisher: customPub);
await Assert.That(tracker.Log).Count().IsEqualTo(2);
await Assert.That(tracker.Log[0]).IsEquivalentTo("CustomPublisher");
await Assert.That(tracker.Log[1]).IsEquivalentTo("Handler:PerCall");
}
[Test]
public async Task I_can_use_a_custom_publisher_via_di_registration()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(TestNotificationHandler))
.UseNotificationPublisher<CustomNotificationPublisher>());
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<NotificationTestTracker>();
await dispatcher.PublishAsync(new TestNotification { Value = "DI" });
await Assert.That(tracker.Log).Count().IsEqualTo(2);
await Assert.That(tracker.Log[0]).IsEquivalentTo("CustomPublisher");
await Assert.That(tracker.Log[1]).IsEquivalentTo("Handler:DI");
}
[Test]
public async Task I_can_use_the_default_sequential_publisher_when_no_custom_publisher_is_registered()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(TestNotificationHandler))
.UseSequentialNotificationPublisher());
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<NotificationTestTracker>();
await dispatcher.PublishAsync(new TestNotification { Value = "Default" });
await Assert.That(tracker.Log).Count().IsEqualTo(1);
await Assert.That(tracker.Log[0]).IsEquivalentTo("Handler:Default");
}
[Test]
public async Task I_can_see_per_call_publisher_wins_over_di_registered_publisher()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(TestNotificationHandler))
.UseNotificationPublisher<CustomNotificationPublisher>());
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<NotificationTestTracker>();
// Use a different publisher per-call that doesn't log "CustomPublisher"
var otherPub = new OtherNotificationPublisher(tracker);
await dispatcher.PublishAsync(new TestNotification { Value = "Override" }, publisher: otherPub);
await Assert.That(tracker.Log).Count().IsEqualTo(2);
await Assert.That(tracker.Log[0]).IsEquivalentTo("OtherPublisher");
await Assert.That(tracker.Log[1]).IsEquivalentTo("Handler:Override");
}
[Test]
public async Task I_can_use_the_parallel_publisher_via_di_registration()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(TestNotificationHandler))
.Add(typeof(SecondTestNotificationHandler))
.UseParallelNotificationPublisher());
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<NotificationTestTracker>();
await dispatcher.PublishAsync(new TestNotification { Value = "Parallel" });
await Assert.That(tracker.Log).Count().IsEqualTo(2);
await Assert.That(tracker.Log).Contains("Handler:Parallel");
await Assert.That(tracker.Log).Contains("SecondHandler:Parallel");
}
[Test]
public async Task I_can_use_the_parallel_publisher_with_max_degree_of_parallelism()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(TestNotificationHandler))
.Add(typeof(SecondTestNotificationHandler))
.UseParallelNotificationPublisher(maxDegreeOfParallelism: 1));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<NotificationTestTracker>();
await dispatcher.PublishAsync(new TestNotification { Value = "SequentialParallel" });
await Assert.That(tracker.Log).Count().IsEqualTo(2);
}
[Test]
public async Task I_can_see_the_parallel_publisher_collects_all_exceptions()
{
var sc = new ServiceCollection();
sc.AddSingleton<NotificationTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(FailingNotificationHandler))
.Add(typeof(SecondTestNotificationHandler))
.UseParallelNotificationPublisher());
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var tracker = provider.GetRequiredService<NotificationTestTracker>();
var ex = await Assert.ThrowsAsync<AggregateException>(async () =>
await dispatcher.PublishAsync(new TestNotification { Value = "Fail" }));
await Assert.That(ex?.InnerExceptions).Count().IsEqualTo(1);
await Assert.That(ex?.InnerExceptions[0].Message).IsEquivalentTo("Handler failed");
}
}

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher.Tests;
public class BlockingNotificationBehavior(NotificationTestTracker tracker) : INotificationBehavior<TestNotification>
{
public Task HandleAsync(TestNotification notification, NotificationHandlerDelegate<TestNotification> next, CancellationToken cancellationToken)
{
tracker.Log.Add("Blocked");
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,17 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher.Tests;
public class CustomNotificationPublisher(NotificationTestTracker tracker) : INotificationPublisher
{
public async Task PublishAsync<TNotification>(IEnumerable<INotificationHandler<TNotification>> handlers, TNotification notification, CancellationToken cancellationToken) where TNotification : INotification
{
tracker.Log.Add("CustomPublisher");
foreach (var handler in handlers)
{
await handler.HandleAsync(notification, cancellationToken);
}
}
}

View file

@ -0,0 +1,12 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher.Tests;
public class FailingNotificationHandler : INotificationHandler<TestNotification>
{
public Task HandleAsync(TestNotification notification, CancellationToken cancellationToken)
{
throw new InvalidOperationException("Handler failed");
}
}

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher.Tests;
public class NotificationChainedBehaviour1(NotificationTestTracker tracker) : INotificationBehavior<TestNotification>
{
public async Task HandleAsync(TestNotification notification, NotificationHandlerDelegate<TestNotification> next, CancellationToken cancellationToken)
{
tracker.Log.Add("Behaviour1");
await next(notification, cancellationToken);
}
}

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher.Tests;
public class NotificationChainedBehaviour2(NotificationTestTracker tracker) : INotificationBehavior<TestNotification>
{
public async Task HandleAsync(TestNotification notification, NotificationHandlerDelegate<TestNotification> next, CancellationToken cancellationToken)
{
tracker.Log.Add("Behaviour2");
await next(notification, cancellationToken);
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher.Tests;
public class NotificationTestTracker
{
public List<string> Log { get; } = [];
}

View file

@ -0,0 +1,14 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher.Tests;
public class OpenNotificationHandler<T>(NotificationTestTracker tracker) : INotificationHandler<T>
where T : INotification
{
public Task HandleAsync(T notification, CancellationToken cancellationToken)
{
tracker.Log.Add($"OpenHandler:{typeof(T).Name}");
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,17 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher.Tests;
public class OtherNotificationPublisher(NotificationTestTracker tracker) : INotificationPublisher
{
public async Task PublishAsync<TNotification>(IEnumerable<INotificationHandler<TNotification>> handlers, TNotification notification, CancellationToken cancellationToken) where TNotification : INotification
{
tracker.Log.Add("OtherPublisher");
foreach (var handler in handlers)
{
await handler.HandleAsync(notification, cancellationToken);
}
}
}

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher.Tests;
public class SecondTestNotificationHandler(NotificationTestTracker tracker) : INotificationHandler<TestNotification>
{
public Task HandleAsync(TestNotification notification, CancellationToken cancellationToken)
{
tracker.Log.Add($"SecondHandler:{notification.Value}");
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,9 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher.Tests;
public class TestNotification : INotification
{
public string? Value { get; set; }
}

View file

@ -0,0 +1,14 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher.Tests;
public class TestNotificationBehavior(NotificationTestTracker tracker) : INotificationBehavior<TestNotification>
{
public async Task HandleAsync(TestNotification notification, NotificationHandlerDelegate<TestNotification> next, CancellationToken cancellationToken)
{
tracker.Log.Add("Before");
await next(notification, cancellationToken);
tracker.Log.Add("After");
}
}

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher.Tests;
public class TestNotificationHandler(NotificationTestTracker tracker) : INotificationHandler<TestNotification>
{
public Task HandleAsync(TestNotification notification, CancellationToken cancellationToken)
{
tracker.Log.Add($"Handler:{notification.Value}");
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,13 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher;
/// <summary>
/// Represents a notification message that can be published to zero or more
/// <see cref="INotificationHandler{TNotification}"/> handlers.
/// This interface serves as a marker for distinguishing notification types.
/// </summary>
public interface INotification
{
}

View file

@ -0,0 +1,34 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
#pragma warning disable CA1711
#pragma warning disable CA1716
namespace Geekeey.Request.Dispatcher;
/// <summary>
/// Represents a behavior in the notification pipeline, allowing interception, modification,
/// or chaining of notification processing.
/// </summary>
/// <typeparam name="TNotification">The type of notification being processed. Must implement <see cref="INotification"/>.</typeparam>
public interface INotificationBehavior<TNotification> where TNotification : INotification
{
/// <summary>
/// Handles the asynchronous processing of a notification, allowing behavior customization
/// such as interception, modification, or chaining of the notification pipeline.
/// </summary>
/// <param name="notification">The notification instance being processed.</param>
/// <param name="next">The next delegate in the pipeline to execute after the custom behavior.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task HandleAsync(TNotification notification, NotificationHandlerDelegate<TNotification> next, CancellationToken cancellationToken);
}
/// <summary>
/// Represents the delegate responsible for handling notifications in a pipeline.
/// </summary>
/// <typeparam name="TNotification">The type of notification being processed. Must implement <see cref="INotification"/>.</typeparam>
/// <param name="notification">The notification instance being processed.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public delegate Task NotificationHandlerDelegate<TNotification>(TNotification notification, CancellationToken cancellationToken) where TNotification : INotification;

View file

@ -0,0 +1,19 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher;
/// <summary>
/// Defines a handler for processing notifications of a specific type.
/// </summary>
/// <typeparam name="TNotification">The type of notification to handle. Must implement <see cref="INotification"/>.</typeparam>
public interface INotificationHandler<in TNotification> where TNotification : INotification
{
/// <summary>
/// Handles the specified notification asynchronously.
/// </summary>
/// <param name="notification">The notification object to process.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task HandleAsync(TNotification notification, CancellationToken cancellationToken);
}

View file

@ -0,0 +1,20 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Dispatcher;
/// <summary>
/// Defines a strategy for publishing notifications to registered handlers.
/// </summary>
public interface INotificationPublisher
{
/// <summary>
/// Publishes the specified notification to the given collection of handlers.
/// </summary>
/// <typeparam name="TNotification">The type of notification being published. Must implement <see cref="INotification"/>.</typeparam>
/// <param name="handlers">The list of handlers to invoke.</param>
/// <param name="notification">The notification instance to publish.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
/// <returns>A task representing the asynchronous publish operation.</returns>
Task PublishAsync<TNotification>(IEnumerable<INotificationHandler<TNotification>> handlers, TNotification notification, CancellationToken cancellationToken) where TNotification : INotification;
}

Some files were not shown because too many files have changed in this diff Show more