From 167f115332d5795d6db64667447c1d880fbde17b Mon Sep 17 00:00:00 2001 From: Allan Ritchie Date: Mon, 3 Jun 2024 00:45:03 -0400 Subject: [PATCH] Middleware starting to work --- Sample/MyRequestMiddleware.cs | 21 +++++ Sample/TriggerViewModel.cs | 4 +- readme.md | 8 +- src/Shiny.Mediator.Contracts/IEvent.cs | 10 ++- src/Shiny.Mediator.Contracts/IRequest.cs | 2 +- .../Unit.cs | 0 .../MediatorSourceGenerator.cs | 9 ++- .../RegisterHandlerAttributeSyntaxReceiver.cs | 21 +++-- src/Shiny.Mediator/IRequestHandler.cs | 4 +- src/Shiny.Mediator/IRequestMiddleware.cs | 20 ++--- .../Impl/DefaultRequestSender.cs | 79 +++++++++++++++---- src/Shiny.Mediator/Impl/Mediator.cs | 4 +- .../Infrastructure/IRequestSender.cs | 4 +- .../Middleware/ExceptionHandlerMiddleware.cs | 18 +++++ .../Middleware/TimedMiddleware.cs | 34 ++++---- .../Shiny.Mediator.Tests/EventHandlerTests.cs | 12 +-- tests/Shiny.Mediator.Tests/MiddlewareTests.cs | 77 ++++++++++++++++++ 17 files changed, 251 insertions(+), 76 deletions(-) create mode 100644 Sample/MyRequestMiddleware.cs rename src/{Shiny.Mediator => Shiny.Mediator.Contracts}/Unit.cs (100%) create mode 100644 src/Shiny.Mediator/Middleware/ExceptionHandlerMiddleware.cs create mode 100644 tests/Shiny.Mediator.Tests/MiddlewareTests.cs diff --git a/Sample/MyRequestMiddleware.cs b/Sample/MyRequestMiddleware.cs new file mode 100644 index 0000000..98b380c --- /dev/null +++ b/Sample/MyRequestMiddleware.cs @@ -0,0 +1,21 @@ +namespace Sample; + +// [RegisterMiddleware] +public class MyRequestMiddleware : IRequestMiddleware +{ + public async Task Process(MyMessageRequest request, Func> next, CancellationToken cancellationToken) + { + // BEFORE - If connected - pull from API after save to cache ELSE pull from cache + var result = await next(); + // AFTER = cache + return result; + } +} + +public class CatchAllRequestMiddleware : IRequestMiddleware, object> +{ + public Task Process(IRequest request, Func> next, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Sample/TriggerViewModel.cs b/Sample/TriggerViewModel.cs index 63df635..db01c3f 100644 --- a/Sample/TriggerViewModel.cs +++ b/Sample/TriggerViewModel.cs @@ -22,11 +22,11 @@ AppSqliteConnection data this.Arg!, this.FireAndForgetEvents ); - var response = await mediator.Send(request, this.cancelSource.Token); + var result = await mediator.Request(request, this.cancelSource.Token); await data.Log( "TriggerViewModel-Response", new MyMessageEvent( - response.Response, + result.Response, this.FireAndForgetEvents ) ); diff --git a/readme.md b/readme.md index 715bec4..0886bee 100644 --- a/readme.md +++ b/readme.md @@ -155,6 +155,12 @@ public class MyViewModel : BaseViewModel, } ``` +## Request Middleware +TODO + +## Event Covariance +TODO + ## Sample There is a sample in this repo. You do not need any other part of Shiny, Prism, ReactiveUI, etc - those are included as I write things faster with it. Focus on the interfaces from the mediator & the mediator calls itself @@ -170,8 +176,8 @@ Focus on the interfaces from the mediator & the mediator calls itself ## TODO * Explain Event Collectors +* Event Collectors for MAUI execute on main thread? * 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? -* Request Middleware - Covariance/catch-all \ No newline at end of file diff --git a/src/Shiny.Mediator.Contracts/IEvent.cs b/src/Shiny.Mediator.Contracts/IEvent.cs index 4947e87..5df9b32 100644 --- a/src/Shiny.Mediator.Contracts/IEvent.cs +++ b/src/Shiny.Mediator.Contracts/IEvent.cs @@ -1,6 +1,8 @@ namespace Shiny.Mediator; -public interface IEvent -{ - -} \ No newline at end of file +public interface IEvent { } + +/// +/// This is a good base type if you want to make use of covariance in your handlers +/// +public record Event : IEvent; \ No newline at end of file diff --git a/src/Shiny.Mediator.Contracts/IRequest.cs b/src/Shiny.Mediator.Contracts/IRequest.cs index 61d3155..92a01fd 100644 --- a/src/Shiny.Mediator.Contracts/IRequest.cs +++ b/src/Shiny.Mediator.Contracts/IRequest.cs @@ -1,6 +1,6 @@ namespace Shiny.Mediator; -public interface IRequest +public interface IRequest : IRequest { } diff --git a/src/Shiny.Mediator/Unit.cs b/src/Shiny.Mediator.Contracts/Unit.cs similarity index 100% rename from src/Shiny.Mediator/Unit.cs rename to src/Shiny.Mediator.Contracts/Unit.cs diff --git a/src/Shiny.Mediator.SourceGenerators/MediatorSourceGenerator.cs b/src/Shiny.Mediator.SourceGenerators/MediatorSourceGenerator.cs index b1d26f7..87709c1 100644 --- a/src/Shiny.Mediator.SourceGenerators/MediatorSourceGenerator.cs +++ b/src/Shiny.Mediator.SourceGenerators/MediatorSourceGenerator.cs @@ -15,7 +15,7 @@ public class MediatorSourceGenerator : ISourceGenerator public void Initialize(GeneratorInitializationContext context) { context.RegisterForPostInitialization(x => x.AddSource( - "RegisterHandlerAttribute.g.cs", + "MediatorAttributes.g.cs", SourceText.From( """ // @@ -28,7 +28,12 @@ public void Initialize(GeneratorInitializationContext context) [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false)] internal sealed class RegisterHandlerAttribute: System.Attribute { - public RegisterHandlerAttribute() {} + public RegisterHandlerAttribute(bool IsSingleton = true) {} + } + + internal sealed class RegisterMiddlewareAttribute: System.Attribute + { + public RegisterMiddlewareAttribute(bool IsSingleton = true) {} } """, Encoding.UTF8 diff --git a/src/Shiny.Mediator.SourceGenerators/RegisterHandlerAttributeSyntaxReceiver.cs b/src/Shiny.Mediator.SourceGenerators/RegisterHandlerAttributeSyntaxReceiver.cs index 81abb8b..d5348e4 100644 --- a/src/Shiny.Mediator.SourceGenerators/RegisterHandlerAttributeSyntaxReceiver.cs +++ b/src/Shiny.Mediator.SourceGenerators/RegisterHandlerAttributeSyntaxReceiver.cs @@ -11,15 +11,20 @@ public class RegisterHandlerAttributeSyntaxReceiver : SyntaxReceiver protected override bool ShouldCollectClassSymbol(INamedTypeSymbol classSymbol) { var hasAttribute = classSymbol.HasAttribute("RegisterHandlerAttribute"); - if (!hasAttribute) - return false; + if (hasAttribute) + { + // TODO: log error + if (classSymbol.IsImplements("Shiny.Mediator.IEventHandler`1")) + return false; - if (classSymbol.IsImplements("Shiny.Mediator.IEventHandler`1")) - return false; - - if (classSymbol.IsImplements("Shiny.Mediator.IRequestHandler`1")) - return false; - + if (classSymbol.IsImplements("Shiny.Mediator.IRequestHandler`1")) + return false; + } + hasAttribute = classSymbol.HasAttribute("RegisterMiddleware"); + if (hasAttribute) + { + return true; + } return false; } } \ No newline at end of file diff --git a/src/Shiny.Mediator/IRequestHandler.cs b/src/Shiny.Mediator/IRequestHandler.cs index 8836534..f215d55 100644 --- a/src/Shiny.Mediator/IRequestHandler.cs +++ b/src/Shiny.Mediator/IRequestHandler.cs @@ -1,12 +1,12 @@ namespace Shiny.Mediator; -public interface IRequestHandler where TRequest : notnull, IRequest +public interface IRequestHandler where TRequest : notnull, IRequest { Task Handle(TRequest request, CancellationToken cancellationToken); } -public interface IRequestHandler where TRequest : notnull, IRequest +public interface IRequestHandler where TRequest : notnull, IRequest { Task Handle(TRequest request, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Shiny.Mediator/IRequestMiddleware.cs b/src/Shiny.Mediator/IRequestMiddleware.cs index 0536f1b..a67ba5d 100644 --- a/src/Shiny.Mediator/IRequestMiddleware.cs +++ b/src/Shiny.Mediator/IRequestMiddleware.cs @@ -1,13 +1,7 @@ -// 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, IRequestMiddleware next, CancellationToken cancellationToken); -// } \ No newline at end of file +using Shiny.Mediator; + + +public interface IRequestMiddleware where TRequest : IRequest +{ + Task Process(TRequest request, Func> next, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Shiny.Mediator/Impl/DefaultRequestSender.cs b/src/Shiny.Mediator/Impl/DefaultRequestSender.cs index c4fb414..219f925 100644 --- a/src/Shiny.Mediator/Impl/DefaultRequestSender.cs +++ b/src/Shiny.Mediator/Impl/DefaultRequestSender.cs @@ -7,35 +7,84 @@ namespace Shiny.Mediator.Impl; public class DefaultRequestSender(IServiceProvider services) : IRequestSender { - public async Task Send(TRequest request, CancellationToken cancellationToken = default) where TRequest : IRequest + public async Task Send(TRequest request, CancellationToken cancellationToken) where TRequest : IRequest { - // TODO: middleware execution should support contravariance using var scope = services.CreateScope(); var handlers = scope.ServiceProvider.GetServices>().ToList(); AssertRequestHandlers(handlers.Count, request); - // TODO: pipelines - await handlers - .First() - .Handle(request, cancellationToken) - .ConfigureAwait(false); + await this.ExecuteMiddleware( + scope, + (IRequest)request, + async () => + { + await handlers + .First() + .Handle(request, cancellationToken) + .ConfigureAwait(false); + return Unit.Value; + }, + cancellationToken + ) + .ConfigureAwait(false); } - public async Task Send(IRequest request, CancellationToken cancellationToken = default) + public async Task Request(IRequest request, CancellationToken cancellationToken = default) { 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); - var handler = handlers.First(); - var handleMethod = handlerType.GetMethod("Handle", BindingFlags.Instance | BindingFlags.Public)!; - var resultTask = (Task)handleMethod.Invoke(handler, [request, cancellationToken])!; - var result = await resultTask.ConfigureAwait(false); - + Func> execute = async () => + { + var handler = handlers.First(); + var handleMethod = handlerType.GetMethod("Handle", BindingFlags.Instance | BindingFlags.Public)!; + var resultTask = (Task)handleMethod.Invoke(handler, [request, cancellationToken])!; + var result = await resultTask.ConfigureAwait(false); + return result; + }; + var result = await this.ExecuteMiddleware(scope, request, execute, cancellationToken).ConfigureAwait(false); + return result; + } + + + async Task ExecuteMiddleware( + IServiceScope scope, + TRequest request, + Func> initialExecute, + CancellationToken cancellationToken + ) where TRequest : IRequest + { + var middlewareType = typeof(IRequestMiddleware<,>).MakeGenericType(request.GetType(), typeof(TResult)); + var middlewareMethod = middlewareType.GetMethod("Process", BindingFlags.Instance | BindingFlags.Public)!; + var middlewares = scope.ServiceProvider.GetServices(middlewareType).ToList(); + + // middlewares.Reverse(); + // foreach (var middleware in middlewares) + // { + // var next = () => + // { + // return (Task)middlewareMethod.Invoke(middleware, [ + // request, + // next, + // cancellationToken + // ]); + // }; + // } + // + // await next!.Invoke().ConfigureAwait(false); + // we setup execution in reverse - with the top being our start/await point + // middlewares.Reverse(); + // var next = initialExecute; + // + // foreach (var middleware in middlewares) + // next = () => (Task)middlewareMethod.Invoke(middleware, [request, next, cancellationToken]); + // + // var result = await next().ConfigureAwait(false); + // return result; + var result = await initialExecute.Invoke().ConfigureAwait(false); return result; } diff --git a/src/Shiny.Mediator/Impl/Mediator.cs b/src/Shiny.Mediator/Impl/Mediator.cs index b9bb0f5..fab4f9a 100644 --- a/src/Shiny.Mediator/Impl/Mediator.cs +++ b/src/Shiny.Mediator/Impl/Mediator.cs @@ -8,8 +8,8 @@ public class Mediator( IEventPublisher eventPublisher ) : IMediator { - public Task Send(IRequest request, CancellationToken cancellationToken = default) - => requestSender.Send(request, cancellationToken); + public Task Request(IRequest request, CancellationToken cancellationToken = default) + => requestSender.Request(request, cancellationToken); public Task Send(TRequest request, CancellationToken cancellationToken = default) where TRequest : IRequest => requestSender.Send(request, cancellationToken); diff --git a/src/Shiny.Mediator/Infrastructure/IRequestSender.cs b/src/Shiny.Mediator/Infrastructure/IRequestSender.cs index 9c7f49d..1362d63 100644 --- a/src/Shiny.Mediator/Infrastructure/IRequestSender.cs +++ b/src/Shiny.Mediator/Infrastructure/IRequestSender.cs @@ -4,8 +4,6 @@ public interface IRequestSender { // Task Send(object arg, CancellationToken cancellationToken = default) // Task Send(object arg, CancellationToken cancellationToken = default); - - /// /// @@ -14,7 +12,7 @@ public interface IRequestSender /// /// /// - Task Send( + Task Request( IRequest request, CancellationToken cancellationToken = default ); diff --git a/src/Shiny.Mediator/Middleware/ExceptionHandlerMiddleware.cs b/src/Shiny.Mediator/Middleware/ExceptionHandlerMiddleware.cs new file mode 100644 index 0000000..9bf57f6 --- /dev/null +++ b/src/Shiny.Mediator/Middleware/ExceptionHandlerMiddleware.cs @@ -0,0 +1,18 @@ +namespace Shiny.Mediator.Middleware; + +public class ExceptionHandlerMiddleware : IRequestMiddleware where TRequest : IRequest +{ + public async Task Process(TRequest request, Func> next, CancellationToken cancellationToken) + { + try + { + return await next(); + + } + catch (Exception ex) + { + // TODO: log, trap? + throw; + } + } +} \ No newline at end of file diff --git a/src/Shiny.Mediator/Middleware/TimedMiddleware.cs b/src/Shiny.Mediator/Middleware/TimedMiddleware.cs index fd53a17..c8e9e3e 100644 --- a/src/Shiny.Mediator/Middleware/TimedMiddleware.cs +++ b/src/Shiny.Mediator/Middleware/TimedMiddleware.cs @@ -1,18 +1,18 @@ -// using System.Diagnostics; -// -// namespace Shiny.Mediator.Middleware; -// -// public class TimedMiddleware : IRequestMiddleware where TRequest : IRequest -// { -// public async Task Process(TRequest request, IRequestMiddleware next, CancellationToken cancellationToken) -// { -// var sw = new Stopwatch(); -// sw.Start(); -// var result = await next(request, cancellationToken); -// sw.Stop(); -// -// // TODO: alert on long? -// return result; -// } -// } +using System.Diagnostics; + +namespace Shiny.Mediator.Middleware; + +public class TimedMiddleware : IRequestMiddleware where TRequest : IRequest +{ + public async Task Process(TRequest request, Func> next, CancellationToken cancellationToken) + { + var sw = new Stopwatch(); + sw.Start(); + var result = await next(); + sw.Stop(); + + // TODO: alert on long? + return result; + } +} diff --git a/tests/Shiny.Mediator.Tests/EventHandlerTests.cs b/tests/Shiny.Mediator.Tests/EventHandlerTests.cs index 78b6935..23ce178 100644 --- a/tests/Shiny.Mediator.Tests/EventHandlerTests.cs +++ b/tests/Shiny.Mediator.Tests/EventHandlerTests.cs @@ -12,7 +12,7 @@ public EventHandlerTests() [Fact] - public async Task Events_SubscriptionFired() + public async Task SubscriptionFired() { var services = new ServiceCollection(); services.AddShinyMediator(); @@ -32,16 +32,15 @@ public async Task Events_SubscriptionFired() [Fact] - public async Task Events_CatchAllHandler() + public async Task VariantHandler() { - // 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()); + await mediator.Publish(new TestTestEvent()); CatchAllEventHandler.Executed.Should().BeTrue(); } } @@ -67,10 +66,11 @@ public async Task Handle(TestEvent @event, CancellationToken cancellationToken) } } -public class CatchAllEventHandler : IEventHandler +public class TestTestEvent : TestEvent; +public class CatchAllEventHandler : IEventHandler { public static bool Executed { get; set; } - public Task Handle(IEvent @event, CancellationToken cancellationToken) + public Task Handle(TestTestEvent @event, CancellationToken cancellationToken) { Executed = true; return Task.CompletedTask; diff --git a/tests/Shiny.Mediator.Tests/MiddlewareTests.cs b/tests/Shiny.Mediator.Tests/MiddlewareTests.cs new file mode 100644 index 0000000..3b9c7e1 --- /dev/null +++ b/tests/Shiny.Mediator.Tests/MiddlewareTests.cs @@ -0,0 +1,77 @@ +namespace Shiny.Mediator.Tests; + +public class MiddlewareTests +{ + // TODO: test execution chain + public MiddlewareTests() + { + Executed.Constrained = false; + Executed.Variant = false; + } + + + public async Task Constrained() + { + + } + + + [Fact] + public async Task ConstrainedAndOpen() + { + var services = new ServiceCollection(); + services.AddShinyMediator(); + services.AddSingletonAsImplementedInterfaces(); + services.AddSingletonAsImplementedInterfaces(); + services.AddSingleton(typeof(IRequestMiddleware<,>), typeof(VariantRequestMiddleware<,>)); + var sp = services.BuildServiceProvider(); + + var mediator = sp.GetRequiredService(); + var result = await mediator.Request(new MiddlewareResponseRequest()); + Executed.Constrained.Should().BeTrue(); + Executed.Variant.Should().BeTrue(); + } +} + + +public class MiddlewareRequest : IRequest; + +public class MiddlewareResponseRequest : IRequest; + + +public class MiddlewareRequestHandler : IRequestHandler +{ + public Task Handle(MiddlewareRequest request, CancellationToken cancellationToken) + => Task.CompletedTask; +} + +public class MiddlewareRequestResponseHandler : IRequestHandler +{ + public Task Handle(MiddlewareResponseRequest request, CancellationToken cancellationToken) + => Task.FromResult(1); +} + +public static class Executed +{ + // TODO: need to know execution order + public static bool Constrained { get; set; } + public static bool Variant { get; set; } +} +public class ConstrainedMiddleware : IRequestMiddleware +{ + public Task Process(MiddlewareResponseRequest request, Func> next, CancellationToken cancellationToken) + { + Executed.Constrained = true; + return next(); + } +} + +public class VariantRequestMiddleware : IRequestMiddleware + where TRequest : IRequest +{ + public Task Process(TRequest request, Func> next, CancellationToken cancellationToken) + { + Executed.Variant = true; + return next(); + } +} \ No newline at end of file