Skip to content

Commit 7ec9654

Browse files
luylucas10luysantanadevSergi0Martin
authored
Add CancellationToken Support to Mediator Pipeline and Handlers (#5) (#6)
* feat: add CancellationToken support to mediator pipeline and handlers (#5) - Add CancellationToken as optional parameter to IMediatorBus, middleware, and handler interfaces - Propagate token through pipeline and handlers - Refactor method signatures to use default parameter values instead of overloads - Update and add tests to verify cancellation is respected (OperationCanceledException) - Fix generic type typo (TRespose -> TResponse) - All tests passing Closes #5 * refactor: format method signatures for improved readability Set one param per line in classes: - DefaultMediatorBus.cs - IMediatorBus.cs - IMediatorMiddleware.cs - ICommandHandler.cs * refactor: streamline middleware task aggregation for improved clarity * Upgrade version --------- Co-authored-by: luysantana <[email protected]> Co-authored-by: Sergio <[email protected]>
1 parent 9602e25 commit 7ec9654

File tree

13 files changed

+190
-71
lines changed

13 files changed

+190
-71
lines changed

src/Buses/DefaultMediatorBus.cs

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,38 @@
1-
using OpenMediator.Middlewares;
1+
using System.Runtime.CompilerServices;
2+
using OpenMediator.Middlewares;
23

34
namespace OpenMediator.Buses;
4-
55
internal sealed class DefaultMediatorBus(
6-
IServiceProvider _serviceProvider,
6+
IServiceProvider _serviceProvider,
77
IEnumerable<IMediatorMiddleware> _middlewares) : IMediatorBus
88
{
9-
public async Task<TResponse> SendAsync<TCommand, TResponse>(TCommand request)
9+
public async Task<TResponse> SendAsync<TCommand, TResponse>(
10+
TCommand request,
11+
CancellationToken cancellationToken = default)
1012
where TCommand : ICommand<TResponse>
1113
{
12-
var handler = (ICommandHandler<TCommand, TResponse>?)_serviceProvider.GetService(typeof(ICommandHandler<TCommand, TResponse>));
14+
var handler = (ICommandHandler<TCommand, TResponse>?) _serviceProvider.GetService(typeof(ICommandHandler<TCommand, TResponse>));
1315
if (handler == null)
1416
{
1517
throw new InvalidOperationException($"Handler not found for command {typeof(TCommand).Name}");
1618
}
1719

18-
return await ExecuteMiddlewares(request, async () => await handler.HandleAsync(request));
20+
return await ExecuteMiddlewares(request, async () => await handler.HandleAsync(request, cancellationToken), cancellationToken);
1921
}
2022

21-
private async Task<TResponse> ExecuteMiddlewares<TCommand, TResponse>(TCommand command, Func<Task<TResponse>> next)
23+
private async Task<TResponse> ExecuteMiddlewares<TCommand, TResponse>(
24+
TCommand command,
25+
Func<Task<TResponse>> next,
26+
CancellationToken cancellationToken = default)
2227
where TCommand : ICommand<TResponse>
2328
{
24-
var middlewareTask = _middlewares.Aggregate(() => next(), (nextMiddleware, middleware) => async () => await middleware.ExecuteAsync(command, nextMiddleware));
29+
var middlewareTask = _middlewares.Aggregate(() => next(), (nextMiddleware, middleware) => async () => await middleware.ExecuteAsync(command, nextMiddleware, cancellationToken));
2530
return await middlewareTask();
2631
}
2732

28-
public async Task SendAsync<TCommand>(TCommand command)
33+
public async Task SendAsync<TCommand>(
34+
TCommand command,
35+
CancellationToken cancellationToken = default)
2936
where TCommand : ICommand
3037
{
3138
var handler = (ICommandHandler<TCommand>?)_serviceProvider.GetService(typeof(ICommandHandler<TCommand>));
@@ -34,13 +41,16 @@ public async Task SendAsync<TCommand>(TCommand command)
3441
throw new InvalidOperationException($"Handler not found for command {typeof(TCommand).Name}");
3542
}
3643

37-
await ExecuteMiddlewares(command, async () => await handler.HandleAsync(command));
44+
await ExecuteMiddlewares(command, async () => await handler.HandleAsync(command, cancellationToken), cancellationToken);
3845
}
3946

40-
private async Task ExecuteMiddlewares<TCommand>(TCommand command, Func<Task> next)
47+
private async Task ExecuteMiddlewares<TCommand>(
48+
TCommand command,
49+
Func<Task> next,
50+
CancellationToken cancellationToken = default)
4151
where TCommand : ICommand
4252
{
43-
var middlewareTask = _middlewares.Aggregate(() => next(), (nextMiddleware, middleware) => () => middleware.ExecuteAsync(command, nextMiddleware));
53+
var middlewareTask = _middlewares.Aggregate(next, (nextMiddleware, middleware) => () => middleware.ExecuteAsync(command, nextMiddleware, cancellationToken));
4454
await middlewareTask();
4555
}
46-
}
56+
}

src/Buses/IMediatorBus.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
public interface IMediatorBus
44
{
5-
Task<TResponse> SendAsync<TCommand, TResponse>(TCommand request)
5+
Task<TResponse> SendAsync<TCommand, TResponse>(
6+
TCommand request,
7+
CancellationToken cancellationToken = default)
68
where TCommand : ICommand<TResponse>;
79

8-
Task SendAsync<TCommand>(TCommand command)
10+
Task SendAsync<TCommand>(
11+
TCommand command,
12+
CancellationToken cancellationToken = default)
913
where TCommand : ICommand;
1014
}

src/ICommandHandler.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@ namespace OpenMediator;
55
public interface ICommandHandler<in TCommand>
66
where TCommand : ICommand
77
{
8-
Task HandleAsync(TCommand command);
8+
Task HandleAsync(
9+
TCommand command,
10+
CancellationToken cancellationToken = default);
911
}
1012

11-
public interface ICommandHandler<in TCommand, TRespose>
12-
where TCommand : ICommand<TRespose>
13+
public interface ICommandHandler<in TCommand, TResponse>
14+
where TCommand : ICommand<TResponse>
1315
{
14-
Task<TRespose> HandleAsync(TCommand command);
16+
Task<TResponse> HandleAsync(
17+
TCommand command,
18+
CancellationToken cancellationToken = default);
1519
}

src/Middlewares/IMediatorMiddleware.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@ namespace OpenMediator.Middlewares;
22

33
public interface IMediatorMiddleware
44
{
5-
Task ExecuteAsync<TCommand>(TCommand command, Func<Task> next) where TCommand : ICommand;
5+
Task ExecuteAsync<TCommand>(
6+
TCommand command,
7+
Func<Task> next,
8+
CancellationToken cancellationToken = default)
9+
where TCommand : ICommand;
610

7-
Task<TResponse> ExecuteAsync<TCommand, TResponse>(TCommand command, Func<Task<TResponse>> next) where TCommand : ICommand<TResponse>;
11+
Task<TResponse> ExecuteAsync<TCommand, TResponse>(
12+
TCommand command,
13+
Func<Task<TResponse>> next,
14+
CancellationToken cancellationToken = default)
15+
where TCommand : ICommand<TResponse>;
816
}

src/OpenMediator.csproj

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,44 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3-
<PropertyGroup>
4-
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
5-
<ImplicitUsings>enable</ImplicitUsings>
6-
<Nullable>enable</Nullable>
7-
</PropertyGroup>
3+
<PropertyGroup>
4+
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
88

9-
<PropertyGroup>
10-
<Title>OpenMediator</Title>
11-
<PackageTags>mediator;mediator-pattern</PackageTags>
12-
<Description>Alternative for those who do not want to pay for a mediator implementation.</Description>
13-
<Authors>Sergio Martin</Authors>
14-
<Version>1.1.3</Version>
15-
<RepositoryUrl>https://github.com/Sergi0Martin/OpenMediator</RepositoryUrl>
16-
<PackageProjectUrl>https://sergi0martin.github.io/OpenMediator</PackageProjectUrl>
17-
<PackageReadmeFile>README.md</PackageReadmeFile>
18-
<RepositoryType>git</RepositoryType>
19-
<PackageIcon>openmediator-ico-128x128.png</PackageIcon>
20-
<ApplicationIcon>..\openmediator.ico</ApplicationIcon>
21-
</PropertyGroup>
9+
<PropertyGroup>
10+
<Title>OpenMediator</Title>
11+
<PackageTags>mediator;mediator-pattern</PackageTags>
12+
<Description>Alternative for those who do not want to pay for a mediator implementation.</Description>
13+
<Authors>Sergio Martin</Authors>
14+
<Version>1.2.0</Version>
15+
<RepositoryUrl>https://github.com/Sergi0Martin/OpenMediator</RepositoryUrl>
16+
<PackageProjectUrl>https://sergi0martin.github.io/OpenMediator</PackageProjectUrl>
17+
<PackageReadmeFile>README.md</PackageReadmeFile>
18+
<RepositoryType>git</RepositoryType>
19+
<PackageIcon>openmediator-ico-128x128.png</PackageIcon>
20+
<ApplicationIcon>..\openmediator.ico</ApplicationIcon>
21+
</PropertyGroup>
22+
<ItemGroup>
23+
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
24+
<_Parameter1>OpenMediator.Unit.Test</_Parameter1>
25+
</AssemblyAttribute>
26+
</ItemGroup>
27+
<ItemGroup>
28+
<Folder Include="Properties\"/>
29+
<Folder Include="Resources\"/>
30+
<None Include="..\README.md">
31+
<Pack>True</Pack>
32+
<PackagePath>\</PackagePath>
33+
</None>
34+
<None Include="Resources/openmediator-ico-128x128.png">
35+
<Pack>True</Pack>
36+
<PackagePath>\</PackagePath>
37+
</None>
38+
</ItemGroup>
2239

23-
<ItemGroup>
24-
<Folder Include="Properties\" />
25-
<Folder Include="Resources\" />
26-
<None Include="..\README.md">
27-
<Pack>True</Pack>
28-
<PackagePath>\</PackagePath>
29-
</None>
30-
<None Include="Resources/openmediator-ico-128x128.png">
31-
<Pack>True</Pack>
32-
<PackagePath>\</PackagePath>
33-
</None>
34-
</ItemGroup>
35-
36-
<ItemGroup>
37-
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.3" />
38-
</ItemGroup>
40+
<ItemGroup>
41+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.3"/>
42+
</ItemGroup>
3943

4044
</Project>

test/OpenMediator.Shared.Test/Commands/CreateUserCommand.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ public record CreateUserCommand(int Id, string Name) : ICommand;
44

55
public record CreateUserCommandHandler(TestDependency testDependency) : ICommandHandler<CreateUserCommand>
66
{
7-
public async Task HandleAsync(CreateUserCommand command)
7+
public async Task HandleAsync(CreateUserCommand command, CancellationToken cancellationToken = default)
88
{
9-
await Task.Delay(500);
9+
await Task.Delay(500, cancellationToken);
1010
testDependency.Call();
1111
}
1212
}

test/OpenMediator.Shared.Test/Commands/GetUserCommand.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ public record User(int Id, string Name, string Email);
66

77
public record GetUserCommandHandler(TestDependency testDependency) : ICommandHandler<GetUserCommand, User>
88
{
9-
public async Task<User> HandleAsync(GetUserCommand command)
9+
public async Task<User> HandleAsync(GetUserCommand command, CancellationToken cancellationToken = default)
1010
{
11-
await Task.Delay(500);
11+
await Task.Delay(500, cancellationToken);
1212
testDependency.Call();
1313

1414
return new User(
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace OpenMediator.Shared.Test.Commands;
2+
3+
public sealed record LongRunningCommand : ICommand;
4+
5+
public sealed class LongRunningCommandHandler : ICommandHandler<LongRunningCommand>
6+
{
7+
public async Task HandleAsync(LongRunningCommand command, CancellationToken cancellationToken = default)
8+
{
9+
for (var i = 0; i < 10; i++)
10+
{
11+
cancellationToken.ThrowIfCancellationRequested();
12+
await Task.Delay(200, cancellationToken);
13+
}
14+
}
15+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using OpenMediator.Middlewares;
2+
3+
namespace OpenMediator.Shared.Test.MediatorMiddlewares;
4+
5+
public sealed class CancellationMiddleware : IMediatorMiddleware
6+
{
7+
public Task ExecuteAsync<TCommand>(TCommand command, Func<Task> next, CancellationToken cancellationToken = default)
8+
where TCommand : ICommand
9+
{
10+
cancellationToken.ThrowIfCancellationRequested();
11+
return next();
12+
}
13+
14+
public Task<TResponse> ExecuteAsync<TCommand, TResponse>(TCommand command, Func<Task<TResponse>> next, CancellationToken cancellationToken = default)
15+
where TCommand : ICommand<TResponse>
16+
{
17+
cancellationToken.ThrowIfCancellationRequested();
18+
return next();
19+
}
20+
}

test/OpenMediator.Shared.Test/MediatorMiddlewares/CustomMediatorMiddleware.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,32 @@ namespace OpenMediator.Shared.Test.MediatorMiddlewares;
66
[ExcludeFromCodeCoverage]
77
public class CustomMediatorMiddleware(TestDependency testDependency) : IMediatorMiddleware
88
{
9-
public async Task ExecuteAsync<TCommand>(TCommand command, Func<Task> next)
9+
public async Task ExecuteAsync<TCommand>(TCommand command, Func<Task> next, CancellationToken cancellationToken = default)
1010
where TCommand : ICommand
1111
{
1212
// Do something before the command
1313
testDependency.Call();
14-
await Task.Delay(500);
14+
await Task.Delay(500, cancellationToken);
1515

1616
await next();
1717

1818
// Do something after the command
1919
testDependency.Call();
20-
await Task.Delay(500);
20+
await Task.Delay(500, cancellationToken);
2121
}
2222

23-
public async Task<TResponse> ExecuteAsync<TCommand, TResponse>(TCommand command, Func<Task<TResponse>> next)
23+
public async Task<TResponse> ExecuteAsync<TCommand, TResponse>(TCommand command, Func<Task<TResponse>> next, CancellationToken cancellationToken = default)
2424
where TCommand : ICommand<TResponse>
2525
{
2626
// Do something before the command
2727
testDependency.Call();
28-
await Task.Delay(500);
28+
await Task.Delay(500, cancellationToken);
2929

3030
var result = await next();
3131

3232
// Do something after the command
3333
testDependency.Call();
34-
await Task.Delay(500);
34+
await Task.Delay(500, cancellationToken);
3535

3636
return result;
3737
}

0 commit comments

Comments
 (0)