From e654c90239f86570dee79fb2e0382df0c01189a3 Mon Sep 17 00:00:00 2001 From: Allan Ritchie Date: Sat, 1 Jun 2024 11:05:38 -0400 Subject: [PATCH] More thoughts & CI tests --- .github/workflows/build.yml | 3 + readme.md | 2 + src/Directory.build.props | 4 +- src/Shiny.Mediator/IErrorHandler.cs | 13 --- src/Shiny.Mediator/IEventExceptionHandler.cs | 13 +++ .../IRequestExceptionHandler.cs | 22 ++++ src/Shiny.Mediator/IRequestMiddleware.cs | 21 ++-- src/Shiny.Mediator/Impl/Mediator.cs | 19 ++-- .../Middleware/TimedMiddleware.cs | 9 ++ src/Shiny.Mediator/Unit.cs | 6 + ...{MediatorTests.cs => EventHandlerTests.cs} | 104 ++++++------------ tests/Shiny.Mediator.Tests/EventHandlers.cs | 2 - tests/Shiny.Mediator.Tests/GlobalUsings.cs | 2 + .../RequestHandlerTests.cs | 75 +++++++++++++ 14 files changed, 187 insertions(+), 108 deletions(-) delete mode 100644 src/Shiny.Mediator/IErrorHandler.cs create mode 100644 src/Shiny.Mediator/IEventExceptionHandler.cs create mode 100644 src/Shiny.Mediator/IRequestExceptionHandler.cs create mode 100644 src/Shiny.Mediator/Middleware/TimedMiddleware.cs create mode 100644 src/Shiny.Mediator/Unit.cs rename tests/Shiny.Mediator.Tests/{MediatorTests.cs => EventHandlerTests.cs} (51%) delete mode 100644 tests/Shiny.Mediator.Tests/EventHandlers.cs create mode 100644 tests/Shiny.Mediator.Tests/RequestHandlerTests.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3fca67..f2e9fbd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,6 +49,9 @@ jobs: - name: Build run: dotnet build Build.slnf /restore -m -property:Configuration=Release -property:PublicRelease=true + - name: Test + run: dotnet test tests/Shiny.Mediator.Tests/Shiny.Mediator.Tests.csproj --no-restore --verbosity normal + - name: Post NuGet Artifacts uses: actions/upload-artifact@v2 with: diff --git a/readme.md b/readme.md index 912f102..b4a7d8e 100644 --- a/readme.md +++ b/readme.md @@ -168,3 +168,5 @@ Focus on the interfaces from the mediator & the mediator calls itself * Explain Event Handlers * Streams - IAsyncEnumerable or IObservable * Source Generator Registration + * Need to use a different method or not use extension methods - maybe AddHandlersFromAssemblyName or allow it to be custom named +* IEventHandler can handle ALL events? \ No newline at end of file diff --git a/src/Directory.build.props b/src/Directory.build.props index 77d7439..35d4dfc 100644 --- a/src/Directory.build.props +++ b/src/Directory.build.props @@ -8,10 +8,10 @@ true Shiny Mediator - a mediator pattern but for apps MIT - https://shinylib.net + https://github.com/shinyorg/mediator icon.png readme.md - https://shinylib.net + https://github.com/shinyorg/mediator mediator maui blazor https://github.com/shinyorg/mediator git diff --git a/src/Shiny.Mediator/IErrorHandler.cs b/src/Shiny.Mediator/IErrorHandler.cs deleted file mode 100644 index 6d4217c..0000000 --- a/src/Shiny.Mediator/IErrorHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Shiny.Mediator; - -// TODO: what about T? -public interface IErrorHandler -{ - Task Handle(MediatorErrorContext context); -} - -public record MediatorErrorContext( - object Message, - bool IsCommand, - Exception Exception -); \ No newline at end of file diff --git a/src/Shiny.Mediator/IEventExceptionHandler.cs b/src/Shiny.Mediator/IEventExceptionHandler.cs new file mode 100644 index 0000000..10843ae --- /dev/null +++ b/src/Shiny.Mediator/IEventExceptionHandler.cs @@ -0,0 +1,13 @@ +namespace Shiny.Mediator; + +public interface IEventExceptionHandler +{ + Task Process(EventExceptionContext context); +} + +public record EventExceptionContext( + IEvent Event, + Exception Exception, + bool FireAndForget, // you may wish to propagate the exception out if events are being await for completion + bool Handled +); \ No newline at end of file diff --git a/src/Shiny.Mediator/IRequestExceptionHandler.cs b/src/Shiny.Mediator/IRequestExceptionHandler.cs new file mode 100644 index 0000000..4b74fdf --- /dev/null +++ b/src/Shiny.Mediator/IRequestExceptionHandler.cs @@ -0,0 +1,22 @@ +namespace Shiny.Mediator; + +public interface IRequestExceptionrHandler +{ + // should this be abortable or even async? + Task Handle(RequestExceptionContext context, CancellationToken cancellationToken); +} + +// what about events? +public class RequestExceptionContext +{ + public RequestExceptionContext(object request, Exception exception) + { + this.Request = request; + this.Exception = exception; + } + + + public object Request { get; } + public Exception Exception { get; } + public bool Handled { get; set; } +} \ No newline at end of file diff --git a/src/Shiny.Mediator/IRequestMiddleware.cs b/src/Shiny.Mediator/IRequestMiddleware.cs index 99c6099..e44d718 100644 --- a/src/Shiny.Mediator/IRequestMiddleware.cs +++ b/src/Shiny.Mediator/IRequestMiddleware.cs @@ -1,10 +1,13 @@ -// namespace Shiny.Mediator; -// -// public interface IRequestMiddle where TRequest : notnull, IRequest -// { -// Task Process(TRequest request, IRequestPipeline next, CancellationToken cancellationToken); -// } -// -// -// public record RequestDelegate(object Request); +using Shiny.Mediator; +// TODO: how do I register an "ALL" middleware + +// TODO: execution duration timer + +// TODO: catch all could be IRequest or IRequest? Could use an IRequest? +public interface IRequestMiddleware where TRequest : IRequest +{ + // intercept with connectivity, if offline go to cache, if online go to remote + // if went to remote, post execute stores to cache + Task Process(TRequest request, IRequestHandler handler); +} \ No newline at end of file diff --git a/src/Shiny.Mediator/Impl/Mediator.cs b/src/Shiny.Mediator/Impl/Mediator.cs index e269896..9d313c8 100644 --- a/src/Shiny.Mediator/Impl/Mediator.cs +++ b/src/Shiny.Mediator/Impl/Mediator.cs @@ -15,6 +15,7 @@ IEnumerable collectors public async Task Send(TRequest request, CancellationToken cancellationToken = default) where TRequest : IRequest { + // TODO: middleware execution should support contravariance using var scope = services.CreateScope(); var handlers = scope.ServiceProvider.GetServices>().ToList(); AssertRequestHandlers(handlers.Count, request); @@ -31,6 +32,7 @@ public async Task Send(IRequest request, Cancellation { var handlerType = typeof(IRequestHandler<,>).MakeGenericType(request.GetType(), typeof(TResult)); + // TODO: middleware execution should support contravariance using var scope = services.CreateScope(); var handlers = scope.ServiceProvider.GetServices(handlerType).ToList(); AssertRequestHandlers(handlers.Count, request); @@ -39,8 +41,7 @@ public async Task Send(IRequest request, Cancellation var handleMethod = handlerType.GetMethod("Handle", BindingFlags.Instance | BindingFlags.Public)!; var resultTask = (Task)handleMethod.Invoke(handler, [request, cancellationToken])!; var result = await resultTask.ConfigureAwait(false); - - // TODO: pipelines + return result; } @@ -55,6 +56,8 @@ public async Task Publish( // allow registered services to be transient/scoped/singleton using var scope = services.CreateScope(); var handlers = scope.ServiceProvider.GetServices>().ToList(); + //var globalHandlers = scope.ServiceProvider.GetServices>().ToList(); + AppendHandlersIf(handlers, this.subscriptions); foreach (var collector in collectors) AppendHandlersIf(handlers, collector); @@ -65,7 +68,6 @@ public async Task Publish( Task executor = null!; if (executeInParallel) { - // TODO: pipelines? error management? executor = Task.WhenAll(handlers.Select(x => x.Handle(@event, cancellationToken)).ToList()); } else @@ -85,7 +87,7 @@ await handler }); } - // TODO: pipelines + // TODO: middleware if (fireAndForget) { this.FireAndForget(executor); @@ -121,13 +123,10 @@ async void FireAndForget(Task task) static void AppendHandlersIf(List> list, IEventCollector collector) where TEvent : IEvent { var handlers = collector.GetHandlers(); - if (handlers.Count > 0) + foreach (var handler in handlers) { - foreach (var handler in handlers) - { - if (!list.Contains(handler)) - list.Add(handler); - } + if (!list.Contains(handler)) + list.Add(handler); } } diff --git a/src/Shiny.Mediator/Middleware/TimedMiddleware.cs b/src/Shiny.Mediator/Middleware/TimedMiddleware.cs new file mode 100644 index 0000000..b69852a --- /dev/null +++ b/src/Shiny.Mediator/Middleware/TimedMiddleware.cs @@ -0,0 +1,9 @@ +// namespace Shiny.Mediator.Middleware; +// +// public class TimedMiddleware> : IRequestMiddleware> +// { +// public Task Process(IRequest request, IRequestHandler handler) +// { +// throw new NotImplementedException(); +// } +// } \ No newline at end of file diff --git a/src/Shiny.Mediator/Unit.cs b/src/Shiny.Mediator/Unit.cs new file mode 100644 index 0000000..55368c1 --- /dev/null +++ b/src/Shiny.Mediator/Unit.cs @@ -0,0 +1,6 @@ +namespace Shiny.Mediator; + +public sealed class Unit +{ + public static Unit Value { get; } = new(); +} \ No newline at end of file diff --git a/tests/Shiny.Mediator.Tests/MediatorTests.cs b/tests/Shiny.Mediator.Tests/EventHandlerTests.cs similarity index 51% rename from tests/Shiny.Mediator.Tests/MediatorTests.cs rename to tests/Shiny.Mediator.Tests/EventHandlerTests.cs index 0a84cfc..72c91f1 100644 --- a/tests/Shiny.Mediator.Tests/MediatorTests.cs +++ b/tests/Shiny.Mediator.Tests/EventHandlerTests.cs @@ -1,11 +1,16 @@ -using System.Diagnostics; -using FluentAssertions; + namespace Shiny.Mediator.Tests; -public class MediatorTests +public class EventHandlerTests { + public EventHandlerTests() + { + CatchAllEventHandler.Executed = false; + } + + [Theory] [InlineData(10000, 1000, false, true, false)] [InlineData(3000, 6000, true, false, false)] @@ -29,52 +34,7 @@ public async Task Events_TriggerTypes(int delayMs, int expectedTime, bool timeIs else sw.ElapsedMilliseconds.Should().BeLessOrEqualTo(expectedTime); } - - - [Fact] - public async Task Missing_RequestHandler() - { - try - { - var services = new ServiceCollection(); - services.AddShinyMediator(); - var sp = services.BuildServiceProvider(); - var mediator = sp.GetRequiredService(); - await mediator.Send(new TestRequest()); - Assert.Fail("This should not have passed"); - } - catch (InvalidOperationException ex) - { - ex.Message.Should().Be("No request handler found for Shiny.Mediator.Tests.TestRequest"); - } - } - [Fact] - public async Task Registration_OnlyOneRequestHandler_NoResponse() - { - try - { - var services = new ServiceCollection(); - services.AddShinyMediator(); - services.AddSingletonAsImplementedInterfaces(); - services.AddSingletonAsImplementedInterfaces(); - var sp = services.BuildServiceProvider(); - var mediator = sp.GetRequiredService(); - await mediator.Send(new TestRequest()); - Assert.Fail("This should not have passed"); - } - catch (InvalidOperationException ex) - { - ex.Message.Should().Be("More than 1 request handlers found for Shiny.Mediator.Tests.TestRequest"); - } - } - - // [Fact] - // public void Registration_OnlyOneRequestHandler_WithResponse() - // { - // - // } - [Fact] public async Task Events_SubscriptionFired() @@ -85,7 +45,7 @@ public async Task Events_SubscriptionFired() var mediator = sp.GetRequiredService(); var tcs = new TaskCompletionSource(); - mediator.Subscribe((@event, ct) => + mediator.Subscribe((_, _) => { tcs.SetResult(); return Task.CompletedTask; @@ -94,37 +54,27 @@ public async Task Events_SubscriptionFired() await mediator.Publish(new TestEvent()); tcs.Task.IsCompletedSuccessfully.Should().BeTrue(); } -} -public class TestRequest : IRequest -{ - public int Delay { get; set; } + [Fact] + public async Task Events_CatchAllHandler() + { + // TODO: test against event collectors as well + var services = new ServiceCollection(); + services.AddShinyMediator(); + services.AddSingletonAsImplementedInterfaces(); + var sp = services.BuildServiceProvider(); + var mediator = sp.GetRequiredService(); + + await mediator.Publish(new TestEvent()); + CatchAllEventHandler.Executed.Should().BeTrue(); + } } public class TestEvent : IEvent { public int Delay { get; set; } } - - -public class Test1RequestHandler : IRequestHandler -{ - public async Task Handle(TestRequest request, CancellationToken cancellationToken) - { - if (request.Delay > 0) - await Task.Delay(request.Delay); - } -} - - -public class Test2RequestHandler : IRequestHandler -{ - public async Task Handle(TestRequest request, CancellationToken cancellationToken) - { - } -} - public class Test1EventHandler : IEventHandler { public async Task Handle(TestEvent @event, CancellationToken cancellationToken) @@ -140,4 +90,14 @@ public async Task Handle(TestEvent @event, CancellationToken cancellationToken) if (@event.Delay > 0) await Task.Delay(@event.Delay); } +} + +public class CatchAllEventHandler : IEventHandler +{ + public static bool Executed { get; set; } + public Task Handle(IEvent @event, CancellationToken cancellationToken) + { + Executed = true; + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/tests/Shiny.Mediator.Tests/EventHandlers.cs b/tests/Shiny.Mediator.Tests/EventHandlers.cs deleted file mode 100644 index 84926ee..0000000 --- a/tests/Shiny.Mediator.Tests/EventHandlers.cs +++ /dev/null @@ -1,2 +0,0 @@ -namespace Shiny.Mediator.Tests; - diff --git a/tests/Shiny.Mediator.Tests/GlobalUsings.cs b/tests/Shiny.Mediator.Tests/GlobalUsings.cs index 74acc32..1dfcc70 100644 --- a/tests/Shiny.Mediator.Tests/GlobalUsings.cs +++ b/tests/Shiny.Mediator.Tests/GlobalUsings.cs @@ -1,3 +1,5 @@ global using Xunit; global using Shiny.Mediator; global using Microsoft.Extensions.DependencyInjection; +global using System.Diagnostics; +global using FluentAssertions; diff --git a/tests/Shiny.Mediator.Tests/RequestHandlerTests.cs b/tests/Shiny.Mediator.Tests/RequestHandlerTests.cs new file mode 100644 index 0000000..f4efd6a --- /dev/null +++ b/tests/Shiny.Mediator.Tests/RequestHandlerTests.cs @@ -0,0 +1,75 @@ +namespace Shiny.Mediator.Tests; + + +public class RequestHandlerTests +{ + [Fact] + public async Task Missing_RequestHandler() + { + try + { + var services = new ServiceCollection(); + services.AddShinyMediator(); + var sp = services.BuildServiceProvider(); + var mediator = sp.GetRequiredService(); + await mediator.Send(new TestRequest()); + Assert.Fail("This should not have passed"); + } + catch (InvalidOperationException ex) + { + ex.Message.Should().Be("No request handler found for Shiny.Mediator.Tests.TestRequest"); + } + } + + [Fact] + public async Task Registration_OnlyOneRequestHandler_NoResponse() + { + try + { + var services = new ServiceCollection(); + services.AddShinyMediator(); + services.AddSingletonAsImplementedInterfaces(); + services.AddSingletonAsImplementedInterfaces(); + var sp = services.BuildServiceProvider(); + var mediator = sp.GetRequiredService(); + await mediator.Send(new TestRequest()); + Assert.Fail("This should not have passed"); + } + catch (InvalidOperationException ex) + { + ex.Message.Should().Be("More than 1 request handlers found for Shiny.Mediator.Tests.TestRequest"); + } + } + + // [Fact] + // public void Registration_OnlyOneRequestHandler_WithResponse() + // { + // + // } + +} + + +public class TestRequest : IRequest +{ + public int Delay { get; set; } +} + +public class Test1RequestHandler : IRequestHandler +{ + public async Task Handle(TestRequest request, CancellationToken cancellationToken) + { + if (request.Delay > 0) + await Task.Delay(request.Delay); + } +} + + +public class Test2RequestHandler : IRequestHandler +{ + public async Task Handle(TestRequest request, CancellationToken cancellationToken) + { + } +} + +