Skip to content

Commit

Permalink
More thoughts & CI tests
Browse files Browse the repository at this point in the history
  • Loading branch information
aritchie committed Jun 1, 2024
1 parent 6d617c1 commit e654c90
Show file tree
Hide file tree
Showing 14 changed files with 187 additions and 108 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<IEvent> can handle ALL events?
4 changes: 2 additions & 2 deletions src/Directory.build.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
<EnableDefaultCompileItems>true</EnableDefaultCompileItems>
<Description>Shiny Mediator - a mediator pattern but for apps</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://shinylib.net</PackageProjectUrl>
<PackageProjectUrl>https://github.com/shinyorg/mediator</PackageProjectUrl>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>readme.md</PackageReadmeFile>
<PackageReleaseNotes>https://shinylib.net</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/shinyorg/mediator</PackageReleaseNotes>
<PackageTags>mediator maui blazor</PackageTags>
<RepositoryUrl>https://github.com/shinyorg/mediator</RepositoryUrl>
<RepositoryType>git</RepositoryType>
Expand Down
13 changes: 0 additions & 13 deletions src/Shiny.Mediator/IErrorHandler.cs

This file was deleted.

13 changes: 13 additions & 0 deletions src/Shiny.Mediator/IEventExceptionHandler.cs
Original file line number Diff line number Diff line change
@@ -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
);
22 changes: 22 additions & 0 deletions src/Shiny.Mediator/IRequestExceptionHandler.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
21 changes: 12 additions & 9 deletions src/Shiny.Mediator/IRequestMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// namespace Shiny.Mediator;
//
// public interface IRequestMiddle<TRequest> where TRequest : notnull, IRequest
// {
// Task Process(TRequest request, IRequestPipeline<TRequest> 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<T>? Could use an IRequest<Void>?
public interface IRequestMiddleware<TRequest, TResult> where TRequest : IRequest<TResult>
{
// intercept with connectivity, if offline go to cache, if online go to remote
// if went to remote, post execute stores to cache
Task<TResult> Process(TRequest request, IRequestHandler<TRequest, TResult> handler);
}
19 changes: 9 additions & 10 deletions src/Shiny.Mediator/Impl/Mediator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ IEnumerable<IEventCollector> collectors

public async Task Send<TRequest>(TRequest request, CancellationToken cancellationToken = default) where TRequest : IRequest
{
// TODO: middleware execution should support contravariance
using var scope = services.CreateScope();
var handlers = scope.ServiceProvider.GetServices<IRequestHandler<TRequest>>().ToList();
AssertRequestHandlers(handlers.Count, request);
Expand All @@ -31,6 +32,7 @@ public async Task<TResult> Send<TResult>(IRequest<TResult> 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);
Expand All @@ -39,8 +41,7 @@ public async Task<TResult> Send<TResult>(IRequest<TResult> request, Cancellation
var handleMethod = handlerType.GetMethod("Handle", BindingFlags.Instance | BindingFlags.Public)!;
var resultTask = (Task<TResult>)handleMethod.Invoke(handler, [request, cancellationToken])!;
var result = await resultTask.ConfigureAwait(false);

// TODO: pipelines

return result;
}

Expand All @@ -55,6 +56,8 @@ public async Task Publish<TEvent>(
// allow registered services to be transient/scoped/singleton
using var scope = services.CreateScope();
var handlers = scope.ServiceProvider.GetServices<IEventHandler<TEvent>>().ToList();
//var globalHandlers = scope.ServiceProvider.GetServices<IEventHandler<IEvent>>().ToList();

AppendHandlersIf(handlers, this.subscriptions);
foreach (var collector in collectors)
AppendHandlersIf(handlers, collector);
Expand All @@ -65,7 +68,6 @@ public async Task Publish<TEvent>(
Task executor = null!;
if (executeInParallel)
{
// TODO: pipelines? error management?
executor = Task.WhenAll(handlers.Select(x => x.Handle(@event, cancellationToken)).ToList());
}
else
Expand All @@ -85,7 +87,7 @@ await handler
});
}

// TODO: pipelines
// TODO: middleware
if (fireAndForget)
{
this.FireAndForget(executor);
Expand Down Expand Up @@ -121,13 +123,10 @@ async void FireAndForget(Task task)
static void AppendHandlersIf<TEvent>(List<IEventHandler<TEvent>> list, IEventCollector collector) where TEvent : IEvent
{
var handlers = collector.GetHandlers<TEvent>();
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);
}
}

Expand Down
9 changes: 9 additions & 0 deletions src/Shiny.Mediator/Middleware/TimedMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// namespace Shiny.Mediator.Middleware;
//
// public class TimedMiddleware<IRequest<TResult>> : IRequestMiddleware<IRequest<TResult>>
// {
// public Task Process(IRequest request, IRequestHandler<IRequest> handler)
// {
// throw new NotImplementedException();
// }
// }
6 changes: 6 additions & 0 deletions src/Shiny.Mediator/Unit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Shiny.Mediator;

public sealed class Unit
{
public static Unit Value { get; } = new();
}
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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<IMediator>();
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<Test1RequestHandler>();
services.AddSingletonAsImplementedInterfaces<Test2RequestHandler>();
var sp = services.BuildServiceProvider();
var mediator = sp.GetRequiredService<IMediator>();
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()
Expand All @@ -85,7 +45,7 @@ public async Task Events_SubscriptionFired()
var mediator = sp.GetRequiredService<IMediator>();

var tcs = new TaskCompletionSource();
mediator.Subscribe<TestEvent>((@event, ct) =>
mediator.Subscribe<TestEvent>((_, _) =>
{
tcs.SetResult();
return Task.CompletedTask;
Expand All @@ -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<CatchAllEventHandler>();
var sp = services.BuildServiceProvider();
var mediator = sp.GetRequiredService<IMediator>();

await mediator.Publish(new TestEvent());
CatchAllEventHandler.Executed.Should().BeTrue();
}
}

public class TestEvent : IEvent
{
public int Delay { get; set; }
}


public class Test1RequestHandler : IRequestHandler<TestRequest>
{
public async Task Handle(TestRequest request, CancellationToken cancellationToken)
{
if (request.Delay > 0)
await Task.Delay(request.Delay);
}
}


public class Test2RequestHandler : IRequestHandler<TestRequest>
{
public async Task Handle(TestRequest request, CancellationToken cancellationToken)
{
}
}

public class Test1EventHandler : IEventHandler<TestEvent>
{
public async Task Handle(TestEvent @event, CancellationToken cancellationToken)
Expand All @@ -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<IEvent>
{
public static bool Executed { get; set; }
public Task Handle(IEvent @event, CancellationToken cancellationToken)
{
Executed = true;
return Task.CompletedTask;
}
}
2 changes: 0 additions & 2 deletions tests/Shiny.Mediator.Tests/EventHandlers.cs

This file was deleted.

2 changes: 2 additions & 0 deletions tests/Shiny.Mediator.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
global using Xunit;
global using Shiny.Mediator;
global using Microsoft.Extensions.DependencyInjection;
global using System.Diagnostics;
global using FluentAssertions;
Loading

0 comments on commit e654c90

Please sign in to comment.