Skip to content

Commit

Permalink
Middleware starting to work
Browse files Browse the repository at this point in the history
  • Loading branch information
aritchie committed Jun 3, 2024
1 parent cb4c166 commit 167f115
Show file tree
Hide file tree
Showing 17 changed files with 251 additions and 76 deletions.
21 changes: 21 additions & 0 deletions Sample/MyRequestMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Sample;

// [RegisterMiddleware]
public class MyRequestMiddleware : IRequestMiddleware<MyMessageRequest, MyMessageResponse>
{
public async Task<MyMessageResponse> Process(MyMessageRequest request, Func<Task<MyMessageResponse>> 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<IRequest<object>, object>
{
public Task<object> Process(IRequest<object> request, Func<Task<object>> next, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
4 changes: 2 additions & 2 deletions Sample/TriggerViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
);
Expand Down
8 changes: 7 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<IEvent> can handle ALL events?
* Request Middleware - Covariance/catch-all
10 changes: 6 additions & 4 deletions src/Shiny.Mediator.Contracts/IEvent.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
namespace Shiny.Mediator;

public interface IEvent
{

}
public interface IEvent { }

/// <summary>
/// This is a good base type if you want to make use of covariance in your handlers
/// </summary>
public record Event : IEvent;
2 changes: 1 addition & 1 deletion src/Shiny.Mediator.Contracts/IRequest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Shiny.Mediator;

public interface IRequest
public interface IRequest : IRequest<Unit>
{
}

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class MediatorSourceGenerator : ISourceGenerator
public void Initialize(GeneratorInitializationContext context)

Check warning on line 15 in src/Shiny.Mediator.SourceGenerators/MediatorSourceGenerator.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'MediatorSourceGenerator.Initialize(GeneratorInitializationContext)'
{
context.RegisterForPostInitialization(x => x.AddSource(
"RegisterHandlerAttribute.g.cs",
"MediatorAttributes.g.cs",
SourceText.From(
"""
// <auto-generated>
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ public class RegisterHandlerAttributeSyntaxReceiver : SyntaxReceiver
protected override bool ShouldCollectClassSymbol(INamedTypeSymbol classSymbol)

Check warning on line 11 in src/Shiny.Mediator.SourceGenerators/RegisterHandlerAttributeSyntaxReceiver.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'RegisterHandlerAttributeSyntaxReceiver.ShouldCollectClassSymbol(INamedTypeSymbol)'
{
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;
}
}
4 changes: 2 additions & 2 deletions src/Shiny.Mediator/IRequestHandler.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
namespace Shiny.Mediator;

public interface IRequestHandler<in TRequest> where TRequest : notnull, IRequest
public interface IRequestHandler<TRequest> where TRequest : notnull, IRequest
{
Task Handle(TRequest request, CancellationToken cancellationToken);
}


public interface IRequestHandler<in TRequest, TResult> where TRequest : notnull, IRequest<TResult>
public interface IRequestHandler<TRequest, TResult> where TRequest : notnull, IRequest<TResult>
{
Task<TResult> Handle(TRequest request, CancellationToken cancellationToken);
}
20 changes: 7 additions & 13 deletions src/Shiny.Mediator/IRequestMiddleware.cs
Original file line number Diff line number Diff line change
@@ -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<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, IRequestMiddleware<TRequest, TResult> next, CancellationToken cancellationToken);
// }
using Shiny.Mediator;


public interface IRequestMiddleware<in TRequest, TResult> where TRequest : IRequest<TResult>
{
Task<TResult> Process(TRequest request, Func<Task<TResult>> next, CancellationToken cancellationToken);
}
79 changes: 64 additions & 15 deletions src/Shiny.Mediator/Impl/DefaultRequestSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,84 @@ namespace Shiny.Mediator.Impl;

public class DefaultRequestSender(IServiceProvider services) : IRequestSender
{
public async Task Send<TRequest>(TRequest request, CancellationToken cancellationToken = default) where TRequest : IRequest
public async Task Send<TRequest>(TRequest request, CancellationToken cancellationToken) 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);

// TODO: pipelines
await handlers
.First()
.Handle(request, cancellationToken)
.ConfigureAwait(false);
await this.ExecuteMiddleware(
scope,
(IRequest<Unit>)request,
async () =>
{
await handlers
.First()
.Handle(request, cancellationToken)
.ConfigureAwait(false);
return Unit.Value;
},
cancellationToken
)
.ConfigureAwait(false);
}


public async Task<TResult> Send<TResult>(IRequest<TResult> request, CancellationToken cancellationToken = default)
public async Task<TResult> Request<TResult>(IRequest<TResult> 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<TResult>)handleMethod.Invoke(handler, [request, cancellationToken])!;
var result = await resultTask.ConfigureAwait(false);

Func<Task<TResult>> execute = async () =>
{
var handler = handlers.First();
var handleMethod = handlerType.GetMethod("Handle", BindingFlags.Instance | BindingFlags.Public)!;
var resultTask = (Task<TResult>)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<TResult> ExecuteMiddleware<TRequest, TResult>(
IServiceScope scope,
TRequest request,
Func<Task<TResult>> initialExecute,
CancellationToken cancellationToken
) where TRequest : IRequest<TResult>
{
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<TResult>)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<TResult>)middlewareMethod.Invoke(middleware, [request, next, cancellationToken]);
//
// var result = await next().ConfigureAwait(false);
// return result;
var result = await initialExecute.Invoke().ConfigureAwait(false);
return result;
}

Expand Down
4 changes: 2 additions & 2 deletions src/Shiny.Mediator/Impl/Mediator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ public class Mediator(
IEventPublisher eventPublisher
) : IMediator
{
public Task<TResult> Send<TResult>(IRequest<TResult> request, CancellationToken cancellationToken = default)
=> requestSender.Send(request, cancellationToken);
public Task<TResult> Request<TResult>(IRequest<TResult> request, CancellationToken cancellationToken = default)
=> requestSender.Request(request, cancellationToken);

public Task Send<TRequest>(TRequest request, CancellationToken cancellationToken = default) where TRequest : IRequest
=> requestSender.Send(request, cancellationToken);
Expand Down
4 changes: 1 addition & 3 deletions src/Shiny.Mediator/Infrastructure/IRequestSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ public interface IRequestSender
{
// Task Send(object arg, CancellationToken cancellationToken = default)
// Task<object?> Send(object arg, CancellationToken cancellationToken = default);



/// <summary>
///
Expand All @@ -14,7 +12,7 @@ public interface IRequestSender
/// <param name="cancellationToken"></param>
/// <typeparam name="TResult"></typeparam>
/// <returns></returns>
Task<TResult> Send<TResult>(
Task<TResult> Request<TResult>(
IRequest<TResult> request,
CancellationToken cancellationToken = default
);
Expand Down
18 changes: 18 additions & 0 deletions src/Shiny.Mediator/Middleware/ExceptionHandlerMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Shiny.Mediator.Middleware;

public class ExceptionHandlerMiddleware<TRequest, TResult> : IRequestMiddleware<TRequest, TResult> where TRequest : IRequest<TResult>
{
public async Task<TResult> Process(TRequest request, Func<Task<TResult>> next, CancellationToken cancellationToken)
{
try
{
return await next();

}
catch (Exception ex)
{
// TODO: log, trap?
throw;
}
}
}
34 changes: 17 additions & 17 deletions src/Shiny.Mediator/Middleware/TimedMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
// using System.Diagnostics;
//
// namespace Shiny.Mediator.Middleware;
//
// public class TimedMiddleware<TRequest, TResult> : IRequestMiddleware<TRequest, TResult> where TRequest : IRequest<TResult>
// {
// public async Task<TResult> Process(TRequest request, IRequestMiddleware<TRequest, TResult> 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<TRequest, TResult> : IRequestMiddleware<TRequest, TResult> where TRequest : IRequest<TResult>
{
public async Task<TResult> Process(TRequest request, Func<Task<TResult>> next, CancellationToken cancellationToken)
{
var sw = new Stopwatch();
sw.Start();
var result = await next();
sw.Stop();

// TODO: alert on long?
return result;
}
}

Loading

0 comments on commit 167f115

Please sign in to comment.