From 1a0be21c3310dc2da492110758eaac2fcd88c67c Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Thu, 4 Apr 2024 00:37:20 +0200 Subject: [PATCH] More perf work, more tests, docs and sample --- Mediator.sln | 18 ++ README.md | 47 +++++- .../Notification/NotificationBenchmarks.cs | 43 ++--- samples/Directory.Build.props | 10 ++ samples/Showcase/Program.cs | 28 +++- .../NotificationPublisher.csproj | 38 +++++ .../basic/NotificationPublisher/Program.cs | 88 ++++++++++ samples/basic/NotificationPublisher/README.md | 19 +++ src/Mediator/INotificationPublisher.cs | 77 ++++++--- src/Mediator/Mediator.csproj | 1 + src/Mediator/Module.cs | 3 + .../Publishers/ForeachAwaitPublisher.cs | 129 ++++++++++++-- .../Publishers/TaskWhenAllPublisher.cs | 58 ++++--- ...Tests.Test_Showcase#Mediator.g.verified.cs | 10 +- .../NotificationHandlersCollectionTests.cs | 157 ++++++++++++++++++ 15 files changed, 630 insertions(+), 96 deletions(-) create mode 100644 samples/Directory.Build.props create mode 100644 samples/basic/NotificationPublisher/NotificationPublisher.csproj create mode 100644 samples/basic/NotificationPublisher/Program.cs create mode 100644 samples/basic/NotificationPublisher/README.md create mode 100644 src/Mediator/Module.cs create mode 100644 test/Mediator.Tests/NotificationHandlersCollectionTests.cs diff --git a/Mediator.sln b/Mediator.sln index 02ed265..ddbb5ff 100644 --- a/Mediator.sln +++ b/Mediator.sln @@ -97,6 +97,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InternalMessages.Applicatio EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InternalMessages.Domain", "samples\apps\InternalMessages\InternalMessages.Domain\InternalMessages.Domain.csproj", "{1A16060A-3393-4404-A3AC-63E10B3648DE}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "basic", "basic", "{686F96A2-0D44-4A5B-9A7C-78608D95E5DB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NotificationPublisher", "samples\basic\NotificationPublisher\NotificationPublisher.csproj", "{FF68E713-7FA9-42F4-8D0F-81A4387BECCD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -407,6 +411,18 @@ Global {1A16060A-3393-4404-A3AC-63E10B3648DE}.Release|x64.Build.0 = Release|Any CPU {1A16060A-3393-4404-A3AC-63E10B3648DE}.Release|x86.ActiveCfg = Release|Any CPU {1A16060A-3393-4404-A3AC-63E10B3648DE}.Release|x86.Build.0 = Release|Any CPU + {FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Debug|x64.ActiveCfg = Debug|Any CPU + {FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Debug|x64.Build.0 = Debug|Any CPU + {FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Debug|x86.ActiveCfg = Debug|Any CPU + {FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Debug|x86.Build.0 = Debug|Any CPU + {FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Release|Any CPU.Build.0 = Release|Any CPU + {FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Release|x64.ActiveCfg = Release|Any CPU + {FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Release|x64.Build.0 = Release|Any CPU + {FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Release|x86.ActiveCfg = Release|Any CPU + {FF68E713-7FA9-42F4-8D0F-81A4387BECCD}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -440,6 +456,8 @@ Global {10E7F076-1B64-4DF6-9FA0-399BBD87F42F} = {D7662382-63B6-4E3D-A6CE-8EC19E473265} {B4EE2FF6-3D88-45B5-9F43-9BD2474F2181} = {D7662382-63B6-4E3D-A6CE-8EC19E473265} {1A16060A-3393-4404-A3AC-63E10B3648DE} = {D7662382-63B6-4E3D-A6CE-8EC19E473265} + {686F96A2-0D44-4A5B-9A7C-78608D95E5DB} = {D3569CDD-7E19-429E-B9AD-75CC05F6C4AA} + {FF68E713-7FA9-42F4-8D0F-81A4387BECCD} = {686F96A2-0D44-4A5B-9A7C-78608D95E5DB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D45B5457-4190-49B6-BF89-7FA5F4C8ABE2} diff --git a/README.md b/README.md index 2f24ab0..f4e9fc9 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,8 @@ See this great video by [@Elfocrash / Nick Chapsas](https://github.com/Elfocrash - [4.6. Use notifications](#46-use-notifications) - [4.7. Polymorphic dispatch with notification handlers](#47-polymorphic-dispatch-with-notification-handlers) - [4.8. Notification handlers also support open generics](#48-notification-handlers-also-support-open-generics) - - [4.9. Use streaming messages](#49-use-streaming-messages) + - [4.9. Notification publishers](#49-notification-publishers) + - [4.10. Use streaming messages](#410-use-streaming-messages) - [5. Diagnostics](#5-diagnostics) - [6. Differences from MediatR](#6-differences-from-mediatr) - [7. Versioning](#7-versioning) @@ -451,8 +452,50 @@ public sealed class GenericNotificationHandler : INotificationHan } ``` +### 4.9. Notification publishers -### 4.9. Use streaming messages +Notification publishers are responsible for dispatching notifications to a collection of handlers. +There are two built in implementations: + +* `ForeachAwaitPublisher` - the default, dispatches the notifications to handlers in order 1-by-1 +* `TaskWhenAllPublisher` - dispatches notifications in parallel + +Both of these try to be efficient by handling a number of special cases (early exit on sync completion, single-handler, array of handlers). +Below we implement a custom one by simply using `Task.WhenAll`. + +```csharp +services.AddMediator(options => +{ + options.NotificationPublisherType = typeof(FireAndForgetNotificationPublisher); +}); + +public sealed class FireAndForgetNotificationPublisher : INotificationPublisher +{ + public async ValueTask Publish( + NotificationHandlers handlers, + TNotification notification, + CancellationToken cancellationToken + ) + where TNotification : INotification + { + try + { + await Task.WhenAll(handlers.Select(handler => handler.Handle(notification, cancellationToken).AsTask())); + } + catch (Exception ex) + { + // Notifications should be fire-and-forget, we just need to log it! + // This way we don't have to worry about exceptions bubbling up when publishing notifications + Console.Error.WriteLine(ex); + + // NOTE: not necessarily saying this is a good idea! + } + } +} +``` + + +### 4.10. Use streaming messages Since version 1.* of this library there is support for streaming using `IAsyncEnumerable`. diff --git a/benchmarks/Mediator.Benchmarks/Notification/NotificationBenchmarks.cs b/benchmarks/Mediator.Benchmarks/Notification/NotificationBenchmarks.cs index 3bbfc33..88ac9a9 100644 --- a/benchmarks/Mediator.Benchmarks/Notification/NotificationBenchmarks.cs +++ b/benchmarks/Mediator.Benchmarks/Notification/NotificationBenchmarks.cs @@ -153,12 +153,6 @@ [new MsBuildArgument("/p:ExtraDefineConstants=Mediator_Publisher_TaskWhenAll")] private MultiHandlerAsync2 _multiHandlerAsync2; private MultiHandlersAsyncNotification _multiHandlersAsyncNotification; - // [Params(Mediator.ServiceLifetime)] - // public ServiceLifetime ServiceLifetime { get; set; } - - // [Params(Mediator.NotificationPublisherName)] - // public string NotificationPublisherType { get; set; } - public enum ScenarioType { SingleHandlerSync, @@ -233,10 +227,9 @@ public Task Publish_Notification_MediatR() { return Scenario switch { - ScenarioType.SingleHandlerSync => _mediatr.Publish(_singleHandlerNotification, CancellationToken.None), - ScenarioType.MultiHandlersSync => _mediatr.Publish(_multiHandlersNotification, CancellationToken.None), - ScenarioType.MultiHandlersAsync - => _mediatr.Publish(_multiHandlersAsyncNotification, CancellationToken.None), + ScenarioType.SingleHandlerSync => _mediatr.Publish(_singleHandlerNotification), + ScenarioType.MultiHandlersSync => _mediatr.Publish(_multiHandlersNotification), + ScenarioType.MultiHandlersAsync => _mediatr.Publish(_multiHandlersAsyncNotification), }; } @@ -245,10 +238,9 @@ public ValueTask Publish_Notification_IMediator() { return Scenario switch { - ScenarioType.SingleHandlerSync => _mediator.Publish(_singleHandlerNotification, CancellationToken.None), - ScenarioType.MultiHandlersSync => _mediator.Publish(_multiHandlersNotification, CancellationToken.None), - ScenarioType.MultiHandlersAsync - => _mediator.Publish(_multiHandlersAsyncNotification, CancellationToken.None), + ScenarioType.SingleHandlerSync => _mediator.Publish(_singleHandlerNotification), + ScenarioType.MultiHandlersSync => _mediator.Publish(_multiHandlersNotification), + ScenarioType.MultiHandlersAsync => _mediator.Publish(_multiHandlersAsyncNotification), }; } @@ -257,12 +249,9 @@ public ValueTask Publish_Notification_Mediator() { return Scenario switch { - ScenarioType.SingleHandlerSync - => _concreteMediator.Publish(_singleHandlerNotification, CancellationToken.None), - ScenarioType.MultiHandlersSync - => _concreteMediator.Publish(_multiHandlersNotification, CancellationToken.None), - ScenarioType.MultiHandlersAsync - => _concreteMediator.Publish(_multiHandlersAsyncNotification, CancellationToken.None), + ScenarioType.SingleHandlerSync => _concreteMediator.Publish(_singleHandlerNotification), + ScenarioType.MultiHandlersSync => _concreteMediator.Publish(_multiHandlersNotification), + ScenarioType.MultiHandlersAsync => _concreteMediator.Publish(_multiHandlersAsyncNotification), }; } @@ -272,11 +261,11 @@ public ValueTask Publish_Notification_Baseline() switch (Scenario) { case ScenarioType.SingleHandlerSync: - return _singleHandler.Handle(_singleHandlerNotification, CancellationToken.None); + return _singleHandler.Handle(_singleHandlerNotification, default); case ScenarioType.MultiHandlersSync: - _multiHandler0.Handle(_multiHandlersNotification, CancellationToken.None).GetAwaiter().GetResult(); - _multiHandler1.Handle(_multiHandlersNotification, CancellationToken.None).GetAwaiter().GetResult(); - _multiHandler2.Handle(_multiHandlersNotification, CancellationToken.None).GetAwaiter().GetResult(); + _multiHandler0.Handle(_multiHandlersNotification, default).GetAwaiter().GetResult(); + _multiHandler1.Handle(_multiHandlersNotification, default).GetAwaiter().GetResult(); + _multiHandler2.Handle(_multiHandlersNotification, default).GetAwaiter().GetResult(); return default; case ScenarioType.MultiHandlersAsync: return AwaitMultipleHandlersAsync(); @@ -286,9 +275,9 @@ public ValueTask Publish_Notification_Baseline() async ValueTask AwaitMultipleHandlersAsync() { - await _multiHandlerAsync0.Handle(_multiHandlersAsyncNotification, CancellationToken.None); - await _multiHandlerAsync1.Handle(_multiHandlersAsyncNotification, CancellationToken.None); - await _multiHandlerAsync2.Handle(_multiHandlersAsyncNotification, CancellationToken.None); + await _multiHandlerAsync0.Handle(_multiHandlersAsyncNotification, default); + await _multiHandlerAsync1.Handle(_multiHandlersAsyncNotification, default); + await _multiHandlerAsync2.Handle(_multiHandlersAsyncNotification, default); } } } diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props new file mode 100644 index 0000000..765bf97 --- /dev/null +++ b/samples/Directory.Build.props @@ -0,0 +1,10 @@ + + + + + + all + runtime; build; native; contentfiles; analyzers + + + diff --git a/samples/Showcase/Program.cs b/samples/Showcase/Program.cs index 66e7fb2..7ebaf0b 100644 --- a/samples/Showcase/Program.cs +++ b/samples/Showcase/Program.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Mediator; @@ -7,7 +8,10 @@ var services = new ServiceCollection(); -services.AddMediator(); +services.AddMediator(options => +{ + options.NotificationPublisherType = typeof(FireAndForgetNotificationPublisher); +}); // Ordering of pipeline behavior registrations matter! services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(ErrorLoggerHandler<,>)); @@ -139,3 +143,25 @@ public ValueTask Handle(TNotification notification, CancellationToken cancellati return default; } } + +public sealed class FireAndForgetNotificationPublisher : INotificationPublisher +{ + public async ValueTask Publish( + NotificationHandlers handlers, + TNotification notification, + CancellationToken cancellationToken + ) + where TNotification : INotification + { + try + { + await Task.WhenAll(handlers.Select(handler => handler.Handle(notification, cancellationToken).AsTask())); + } + catch (Exception ex) + { + // Notifications should be fire-and-forget, we just need to log it! + // This way we don't have to worry about exceptions bubbling up when publishing notifications + Console.Error.WriteLine(ex); + } + } +} diff --git a/samples/basic/NotificationPublisher/NotificationPublisher.csproj b/samples/basic/NotificationPublisher/NotificationPublisher.csproj new file mode 100644 index 0000000..b3fee41 --- /dev/null +++ b/samples/basic/NotificationPublisher/NotificationPublisher.csproj @@ -0,0 +1,38 @@ + + + + Exe + net8.0 + disable + + + true + + Generated + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/basic/NotificationPublisher/Program.cs b/samples/basic/NotificationPublisher/Program.cs new file mode 100644 index 0000000..64ee37d --- /dev/null +++ b/samples/basic/NotificationPublisher/Program.cs @@ -0,0 +1,88 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Mediator; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +services.AddMediator(options => +{ + options.NotificationPublisherType = typeof(MyNotificationPublisher); +}); + +var serviceProvider = services.BuildServiceProvider(); + +var mediator = serviceProvider.GetRequiredService(); + +var id = Guid.NewGuid(); +var notification = new Notification(id); + +Console.WriteLine("Publishing!"); +Console.WriteLine("-----------------------------------"); + +await mediator.Publish(notification); + +Console.WriteLine("-----------------------------------"); +Console.WriteLine("Finished publishing!"); + +return 0; + +// +// Here are the types used +// + +public sealed class MyNotificationPublisher : INotificationPublisher +{ + public async ValueTask Publish( + NotificationHandlers handlers, + TNotification notification, + CancellationToken cancellationToken + ) + where TNotification : INotification + { + try + { + // IsSingleHandler is a convenience method to check if there is only one handler + // so that we can early exist. Used for optimization purposes by the built in implementations. + if (handlers.IsSingleHandler(out var singleHandler)) + { + await singleHandler.Handle(notification, cancellationToken); + return; + } + // IsArray is a convenience method to check if the handlers are an array (for the built-in DI container in BCL, this is the case) + // so that we can iterate and/or index directly. Used for optimization purposes by the built in implementations. + else if (handlers.IsArray(out var array)) + { + foreach (var handler in array) + { + await handler.Handle(notification, cancellationToken); + } + } + else + { + // Or we can just box the tasks and await them all + await Task.WhenAll( + handlers.Select(handler => handler.Handle(notification, cancellationToken).AsTask()) + ); + } + } + catch (Exception ex) + { + // Notifications should be fire-and-forget, we just need to log it! + Console.Error.WriteLine(ex); + } + } +} + +public sealed record Notification(Guid Id) : INotification; + +public sealed class MyNotificationHandler : INotificationHandler +{ + public ValueTask Handle(Notification notification, CancellationToken cancellationToken) + { + Console.WriteLine($"{GetType().Name} - {notification.Id}"); + throw new Exception("Something went wrong!"); + } +} diff --git a/samples/basic/NotificationPublisher/README.md b/samples/basic/NotificationPublisher/README.md new file mode 100644 index 0000000..b790417 --- /dev/null +++ b/samples/basic/NotificationPublisher/README.md @@ -0,0 +1,19 @@ +## NotificationPublisher + +Simple showcase of using a custom notification publisher, by implementing `INotificationPublisher`. +The custom publisher catches all exceptions and logs them, a so called fire-and-forget implementation. + +### Build and run + +```console +$ dotnet run +Publishing! +----------------------------------- +MyNotificationHandler - 6ae7d56b-8a2f-404c-a24b-c5df1e6691d2 +System.Exception: Something went wrong! + at MyNotificationHandler.Handle(Notification notification, CancellationToken cancellationToken) in /home/martin/code/private/Mediator/samples/basic/NotificationPublisher/Program.cs:line 79 + at MyNotificationPublisher.Publish[TNotification](NotificationHandlers`1 handlers, TNotification notification, CancellationToken cancellationToken) in /home/martin/code/private/Mediator/samples/basic/NotificationPublisher/Program.cs:line 46 +----------------------------------- +Finished publishing! +``` + diff --git a/src/Mediator/INotificationPublisher.cs b/src/Mediator/INotificationPublisher.cs index e160f37..ef0998d 100644 --- a/src/Mediator/INotificationPublisher.cs +++ b/src/Mediator/INotificationPublisher.cs @@ -1,6 +1,8 @@ +using System.Collections; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace Mediator; @@ -9,7 +11,7 @@ namespace Mediator; /// Contains convenience methods for implementing the in an efficient way. /// /// The type of notification. -public readonly struct NotificationHandlers +public readonly struct NotificationHandlers : IEnumerable> where TNotification : INotification { private readonly IEnumerable> _handlers; @@ -20,7 +22,7 @@ public readonly struct NotificationHandlers /// /// The array of notification handlers, if stored as an array. /// true if the handlers are stored as an array; otherwise, false. - internal readonly bool IsArray([MaybeNullWhen(false)] out INotificationHandler[] handlers) + public readonly bool IsArray([MaybeNullWhen(false)] out INotificationHandler[] handlers) { if (_isArray) { @@ -55,6 +57,7 @@ public readonly bool IsSingleHandler([MaybeNullWhen(false)] out INotificationHan { if (IsArray(out var handlers) && handlers.Length == 1) { + // MemoryMarshal.GetArrayDataReference(handlers); handler = handlers[0]; return true; } @@ -65,56 +68,88 @@ public readonly bool IsSingleHandler([MaybeNullWhen(false)] out INotificationHan public readonly Enumerator GetEnumerator() => new Enumerator(in this); - public struct Enumerator + IEnumerator> IEnumerable>.GetEnumerator() => + new Enumerator(in this); + + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(in this); + + public struct Enumerator : IEnumerator> { private readonly NotificationHandlers _handlers; private IEnumerator>? _enumerator; private int _index; + private INotificationHandler? _current; internal Enumerator(in NotificationHandlers handlers) { - _index = -1; _handlers = handlers; + _enumerator = null; + _index = 0; + _current = null; } - public readonly INotificationHandler Current + public readonly INotificationHandler Current => _current!; + + readonly object? IEnumerator.Current { - [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - switch (_handlers.IsArray(out var handlers)) + if ( + (_handlers.IsArray(out var array) && (_index == 0 || _index == array.Length + 1)) + || _enumerator is null + ) { - case true: - return handlers[_index]; - case false: - Debug.Assert(_enumerator is not null); - return _enumerator!.Current; + ThrowHelper.ThrowInvalidOperationException( + "Enumeration has either not started or has already finished." + ); } + return Current; } } + public readonly void Dispose() { } + public bool MoveNext() { switch (_handlers.IsArray(out var handlers)) { case true: - if ((uint)_index + 1 < (uint)handlers.Length) + if ((uint)_index < (uint)handlers.Length) { + _current = handlers[_index]; _index++; return true; } - return false; + return MoveNextRare(handlers); case false: - if (_index == -1) - { - _enumerator = _handlers._handlers.GetEnumerator(); - _index++; - } - Debug.Assert(_enumerator is not null); - return _enumerator!.MoveNext(); + return MoveNextInnerEnumerator(); } } + + private bool MoveNextRare(INotificationHandler[] handlers) + { + _index = handlers.Length + 1; + _current = null; + return false; + } + + private bool MoveNextInnerEnumerator() + { + if (_index == 0) + _enumerator = _handlers._handlers.GetEnumerator(); + _index++; + Debug.Assert(_enumerator is not null); + var result = _enumerator!.MoveNext(); + _current = result ? _enumerator.Current : null; + return result; + } + + public void Reset() + { + _enumerator = null; + _index = 0; + } } } diff --git a/src/Mediator/Mediator.csproj b/src/Mediator/Mediator.csproj index 1f10d6f..3b38362 100644 --- a/src/Mediator/Mediator.csproj +++ b/src/Mediator/Mediator.csproj @@ -7,6 +7,7 @@ Abstractions for the Mediator.SourceGenerator package. + true diff --git a/src/Mediator/Module.cs b/src/Mediator/Module.cs new file mode 100644 index 0000000..f30dbdd --- /dev/null +++ b/src/Mediator/Module.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[module: SkipLocalsInit] diff --git a/src/Mediator/Publishers/ForeachAwaitPublisher.cs b/src/Mediator/Publishers/ForeachAwaitPublisher.cs index 9b2efa1..7ad824a 100644 --- a/src/Mediator/Publishers/ForeachAwaitPublisher.cs +++ b/src/Mediator/Publishers/ForeachAwaitPublisher.cs @@ -17,29 +17,126 @@ CancellationToken cancellationToken if (handlers.IsSingleHandler(out var handler)) return handler.Handle(notification, cancellationToken); - return Publish(handlers, notification, cancellationToken); - - static async ValueTask Publish( - NotificationHandlers handlers, - TNotification notification, - CancellationToken cancellationToken - ) + if (handlers.IsArray(out var handlerArray)) { - List? exceptions = null; - foreach (var handler in handlers) + var task0 = handlerArray[0].Handle(notification, cancellationToken); + if (task0.IsCompletedSuccessfully) { - try + var task1 = handlerArray[1].Handle(notification, cancellationToken); + if (task1.IsCompletedSuccessfully) { - await handler.Handle(notification, cancellationToken); + if (handlerArray.Length == 2) + return default; + + var task2 = handlerArray[2].Handle(notification, cancellationToken); + if (task2.IsCompletedSuccessfully) + { + if (handlerArray.Length == 3) + return default; + + var task3 = handlerArray[3].Handle(notification, cancellationToken); + if (task3.IsCompletedSuccessfully) + { + if (handlerArray.Length == 4) + return default; + + var task4 = handlerArray[4].Handle(notification, cancellationToken); + return PublishArray( + task4.AsTask(), + start: 5, + handlerArray, + notification, + cancellationToken + ); + } + else + { + return PublishArray( + task3.AsTask(), + start: 4, + handlerArray, + notification, + cancellationToken + ); + } + } + else + { + return PublishArray(task2.AsTask(), start: 3, handlerArray, notification, cancellationToken); + } } - catch (Exception ex) + else { - exceptions ??= new List(1); - exceptions.Add(ex); + return PublishArray(task1.AsTask(), start: 2, handlerArray, notification, cancellationToken); } } - if (exceptions is not null) - ThrowHelper.ThrowAggregateException(exceptions); + + return PublishArray(task0.AsTask(), start: 1, handlerArray, notification, cancellationToken); + } + else + { + return PublishNonArray(handlers, notification, cancellationToken); + } + } + + static async ValueTask PublishArray( + Task task0, + int start, + INotificationHandler[] handlers, + TNotification notification, + CancellationToken cancellationToken + ) + where TNotification : INotification + { + List? exceptions = null; + + try + { + await task0; + } + catch (Exception ex) + { + exceptions ??= new List(1); + exceptions.Add(ex); + } + + for (int i = start; i < handlers.Length; i++) + { + try + { + await handlers[i].Handle(notification, cancellationToken); + } + catch (Exception ex) + { + exceptions ??= new List(1); + exceptions.Add(ex); + } + } + if (exceptions is not null) + ThrowHelper.ThrowAggregateException(exceptions); + } + + static async ValueTask PublishNonArray( + NotificationHandlers handlers, + TNotification notification, + CancellationToken cancellationToken + ) + where TNotification : INotification + { + List? exceptions = null; + foreach (var handler in handlers) + { + try + { + await handler.Handle(notification, cancellationToken); + } + catch (Exception ex) + { + exceptions ??= new List(1); + exceptions.Add(ex); + } } + if (exceptions is not null) + ThrowHelper.ThrowAggregateException(exceptions); } } diff --git a/src/Mediator/Publishers/TaskWhenAllPublisher.cs b/src/Mediator/Publishers/TaskWhenAllPublisher.cs index ea235b3..27a597c 100644 --- a/src/Mediator/Publishers/TaskWhenAllPublisher.cs +++ b/src/Mediator/Publishers/TaskWhenAllPublisher.cs @@ -25,7 +25,7 @@ CancellationToken cancellationToken var task1 = handlersArray[1].Handle(notification, cancellationToken); if (task0.IsCompletedSuccessfully && task1.IsCompletedSuccessfully) return default; - return AwaitTasks(task0, task1); + return AwaitTasks(task0.AsTask(), task1.AsTask()); } if (handlersArray.Length == 3) { @@ -34,7 +34,7 @@ CancellationToken cancellationToken var task2 = handlersArray[2].Handle(notification, cancellationToken); if (task0.IsCompletedSuccessfully && task1.IsCompletedSuccessfully && task2.IsCompletedSuccessfully) return default; - return AwaitTasks(task0, task1, task2); + return AwaitTasks(task0.AsTask(), task1.AsTask(), task2.AsTask()); } if (handlersArray.Length == 4) { @@ -49,10 +49,10 @@ CancellationToken cancellationToken && task3.IsCompletedSuccessfully ) return default; - return AwaitTasks(task0, task1, task2, task3); + return AwaitTasks(task0.AsTask(), task1.AsTask(), task2.AsTask(), task3.AsTask()); } - ValueTask[]? tasks = null; + Task[]? tasks = null; var count = 0; foreach (var handler in handlersArray) @@ -61,8 +61,8 @@ CancellationToken cancellationToken if (task.IsCompletedSuccessfully) continue; - tasks ??= new ValueTask[handlersArray.Length]; - tasks[count++] = task; + tasks ??= new Task[handlersArray.Length]; + tasks[count++] = task.AsTask(); } if (tasks is null) @@ -72,25 +72,35 @@ CancellationToken cancellationToken } else { - List? tasks = null; - foreach (var handler in handlers) - { - var task = handler.Handle(notification, cancellationToken); - if (task.IsCompletedSuccessfully) - continue; - - tasks ??= new List(1); - tasks.Add(task); - } + return PublishNonArray(handlers, notification, cancellationToken); + } + } - if (tasks is null) - return default; + static ValueTask PublishNonArray( + NotificationHandlers handlers, + TNotification notification, + CancellationToken cancellationToken + ) + where TNotification : INotification + { + List? tasks = null; + foreach (var handler in handlers) + { + var task = handler.Handle(notification, cancellationToken); + if (task.IsCompletedSuccessfully) + continue; - return AwaitTaskList(tasks); + tasks ??= new List(1); + tasks.Add(task.AsTask()); } + + if (tasks is null) + return default; + + return AwaitTaskList(tasks); } - static async ValueTask AwaitTasks(ValueTask task0, ValueTask task1) + static async ValueTask AwaitTasks(Task task0, Task task1) { List? exceptions = null; @@ -117,7 +127,7 @@ static async ValueTask AwaitTasks(ValueTask task0, ValueTask task1) ThrowHelper.ThrowAggregateException(exceptions); } - static async ValueTask AwaitTasks(ValueTask task0, ValueTask task1, ValueTask task2) + static async ValueTask AwaitTasks(Task task0, Task task1, Task task2) { List? exceptions = null; @@ -153,7 +163,7 @@ static async ValueTask AwaitTasks(ValueTask task0, ValueTask task1, ValueTask ta ThrowHelper.ThrowAggregateException(exceptions); } - static async ValueTask AwaitTasks(ValueTask task0, ValueTask task1, ValueTask task2, ValueTask task3) + static async ValueTask AwaitTasks(Task task0, Task task1, Task task2, Task task3) { List? exceptions = null; @@ -198,7 +208,7 @@ static async ValueTask AwaitTasks(ValueTask task0, ValueTask task1, ValueTask ta ThrowHelper.ThrowAggregateException(exceptions); } - static async ValueTask AwaitTaskArray(ValueTask[] tasks, int count) + static async ValueTask AwaitTaskArray(Task[] tasks, int count) { List? exceptions = null; for (int i = 0; i < count; i++) @@ -218,7 +228,7 @@ static async ValueTask AwaitTaskArray(ValueTask[] tasks, int count) ThrowHelper.ThrowAggregateException(exceptions); } - static async ValueTask AwaitTaskList(List tasks) + static async ValueTask AwaitTaskList(List tasks) { List? exceptions = null; for (int i = 0; i < tasks.Count; i++) diff --git a/test/Mediator.SourceGenerator.Tests/_snapshots/SampleTests.Test_Showcase#Mediator.g.verified.cs b/test/Mediator.SourceGenerator.Tests/_snapshots/SampleTests.Test_Showcase#Mediator.g.verified.cs index 151b691..aa7bdec 100644 --- a/test/Mediator.SourceGenerator.Tests/_snapshots/SampleTests.Test_Showcase#Mediator.g.verified.cs +++ b/test/Mediator.SourceGenerator.Tests/_snapshots/SampleTests.Test_Showcase#Mediator.g.verified.cs @@ -81,8 +81,8 @@ public static IServiceCollection AddMediator(this IServiceCollection services, g services.Add(new SD(typeof(global::Mediator.INotificationHandler<>), typeof(global::GenericNotificationHandler<>), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); - services.Add(new SD(typeof(global::Mediator.ForeachAwaitPublisher), typeof(global::Mediator.ForeachAwaitPublisher), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); - services.TryAdd(new SD(typeof(global::Mediator.INotificationPublisher), sp => sp.GetRequiredService(), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + services.Add(new SD(typeof(global::FireAndForgetNotificationPublisher), typeof(global::FireAndForgetNotificationPublisher), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); + services.TryAdd(new SD(typeof(global::Mediator.INotificationPublisher), sp => sp.GetRequiredService(), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); services.Add(new SD(typeof(global::Mediator.IContainerProbe), typeof(global::Mediator.ContainerProbe0), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); services.Add(new SD(typeof(global::Mediator.IContainerProbe), typeof(global::Mediator.ContainerProbe1), global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)); @@ -476,7 +476,7 @@ public sealed partial class Mediator : global::Mediator.IMediator, global::Media /// /// The name of the notification publisher service that was configured. /// - public const string NotificationPublisherName = "ForeachAwaitPublisher"; + public const string NotificationPublisherName = "FireAndForgetNotificationPublisher"; /// /// Constructor for DI, should not be used by consumer. @@ -557,7 +557,7 @@ private readonly struct DICache public readonly global::Mediator.INotificationHandler[] Handlers_For_ErrorMessage; public readonly global::Mediator.INotificationHandler[] Handlers_For_SuccessfulMessage; - public readonly global::Mediator.ForeachAwaitPublisher InternalNotificationPublisherImpl; + public readonly global::FireAndForgetNotificationPublisher InternalNotificationPublisherImpl; public DICache(global::System.IServiceProvider sp, global::Mediator.ContainerMetadata containerMetadata) { @@ -594,7 +594,7 @@ public DICache(global::System.IServiceProvider sp, global::Mediator.ContainerMet } - InternalNotificationPublisherImpl = sp.GetRequiredService(); + InternalNotificationPublisherImpl = sp.GetRequiredService(); } } diff --git a/test/Mediator.Tests/NotificationHandlersCollectionTests.cs b/test/Mediator.Tests/NotificationHandlersCollectionTests.cs new file mode 100644 index 0000000..9b63692 --- /dev/null +++ b/test/Mediator.Tests/NotificationHandlersCollectionTests.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; + +namespace Mediator.Tests; + +public class NotificationHandlersCollectionTests +{ + static IEnumerable> GetHandlers(int count, bool isArray) + { + var handlers = new List>(count); + for (var i = 0; i < count; i++) + handlers.Add(new FakeNotificationHandler()); + + return isArray ? handlers.ToArray() : handlers; + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Test_IsSingleHandler_ReturnsTrue_WhenSingleHandler_And_Array(bool isArray) + { + var input = GetHandlers(1, isArray); + var handlers = new NotificationHandlers(input, isArray); + + var isSingle = handlers.IsSingleHandler(out var singleHandler); + + if (isArray) + { + Assert.True(isSingle); + input.Single().Should().BeSameAs(singleHandler); + } + else + { + Assert.False(isSingle); + Assert.Null(singleHandler); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Test_IsSingleHandler_ReturnsFalse_WhenMultipleHandlers(bool isArray) + { + var input = GetHandlers(2, isArray); + var handlers = new NotificationHandlers(input, isArray); + + var isSingle = handlers.IsSingleHandler(out var singleHandler); + + Assert.False(isSingle); + Assert.Null(singleHandler); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Test_IsArray(bool isArray) + { + var input = GetHandlers(2, isArray); + var handlers = new NotificationHandlers(input, isArray); + + var result = handlers.IsArray(out var handlersArray); + + if (isArray) + { + Assert.True(result); + Assert.True(handlersArray is INotificationHandler[]); + Assert.NotNull(handlersArray); + handlersArray.Length.Should().Be(2); + handlersArray.Should().BeSameAs(input); + } + else + { + Assert.False(result); + Assert.Null(handlersArray); + } + } + + [Theory] + [InlineData(1, true)] + [InlineData(1, false)] + [InlineData(2, true)] + [InlineData(2, false)] + [InlineData(3, true)] + [InlineData(3, false)] + public void Test_Enumerator(int count, bool isArray) + { + var input = GetHandlers(count, isArray); + var handlers = new NotificationHandlers(input, isArray); + + using var enumerator = handlers.GetEnumerator(); + + for (int j = 0; j < 3; j++) + { + for (var i = 0; i < count; i++) + { + enumerator.MoveNext().Should().BeTrue(); + enumerator.Current.Should().BeSameAs(input.ElementAt(i)); + } + enumerator.MoveNext().Should().BeFalse(); + enumerator.Reset(); + } + } + + [Theory] + [InlineData(1, true)] + [InlineData(1, false)] + [InlineData(3, true)] + [InlineData(3, false)] + public void Test_Enumerator_Throws_WhenEnumerationHasNotStarted(int count, bool isArray) + { + var input = GetHandlers(count, isArray); + var handlers = new NotificationHandlers(input, isArray); + + using var enumerator = handlers.GetEnumerator(); + Action action = () => _ = ((IEnumerator)enumerator).Current; + action.Should().Throw(); + action = () => _ = enumerator.Current; + action.Should().NotThrow(); + + var list = new List([1, 2, 3]); + using var listEnumerator = list.GetEnumerator(); + action = () => _ = ((IEnumerator)listEnumerator).Current; + action.Should().Throw(); + action = () => _ = listEnumerator.Current; + action.Should().NotThrow(); + + // ^ don't know why, just keeping consistent behavior... + } + + [Theory] + [InlineData(3, true)] + [InlineData(3, false)] + public void Test_Linq_ToArray(int count, bool isArray) + { + var input = GetHandlers(count, isArray); + var handlers = new NotificationHandlers(input, isArray); + + var result = handlers.ToArray(); + + result.Should().BeEquivalentTo(input); + } + + public class FakeNotification : INotification { } + + public class FakeNotificationHandler : INotificationHandler + { + public ValueTask Handle(FakeNotification notification, CancellationToken cancellationToken) + { + return default; + } + } +}