Skip to content

Commit b0b2410

Browse files
committed
v2/feature/141 : Cortex.Mediator - Add Non Returning Command Interface (ICommand)
Add support for non-returning CQRS commands Introduced a non-generic `ICommand` interface for commands that do not return values, alongside a corresponding `ICommandHandler<TCommand>` interface. Added `ICommandPipelineBehavior<TCommand>` and `CommandHandlerDelegate` to enable pipeline behaviors for non-returning commands. Updated `MediatorOptions` to manage both returning and non-returning command behaviors, including the addition of `VoidCommandBehaviors`. Enhanced `MediatorOptionsExtensions` and `ServiceCollectionExtensions` to register default and custom behaviors for non-returning commands. Extended `IMediator` with a `SendCommandAsync<TCommand>` method for non-returning commands. Updated the `Mediator` implementation to handle non-returning commands and added a `PipelineBehaviorNextDelegate<TCommand>` for behavior chaining. Added `VoidLoggingCommandBehavior` to log execution details for non-returning commands. Refactored existing code to ensure compatibility with the new non-returning command infrastructure.
1 parent 4a98cd3 commit b0b2410

9 files changed

Lines changed: 195 additions & 33 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using Cortex.Mediator.Commands;
2+
using Microsoft.Extensions.Logging;
3+
using System;
4+
using System.Diagnostics;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
namespace Cortex.Mediator.Behaviors
9+
{
10+
11+
public sealed class LoggingCommandBehavior<TCommand> : ICommandPipelineBehavior<TCommand> where TCommand : ICommand
12+
{
13+
private readonly ILogger<LoggingCommandBehavior<TCommand>> _logger;
14+
15+
public LoggingCommandBehavior(ILogger<LoggingCommandBehavior<TCommand>> logger)
16+
{
17+
_logger = logger;
18+
}
19+
20+
public async Task Handle(
21+
TCommand command,
22+
CommandHandlerDelegate next,
23+
CancellationToken cancellationToken)
24+
{
25+
var commandName = typeof(TCommand).Name;
26+
_logger.LogInformation("Executing command {CommandName}", commandName);
27+
28+
var stopwatch = Stopwatch.StartNew(); // start timing
29+
try
30+
{
31+
await next();
32+
33+
stopwatch.Stop();
34+
_logger.LogInformation(
35+
"Command {CommandName} executed successfully in {ElapsedMilliseconds} ms",
36+
commandName,
37+
stopwatch.ElapsedMilliseconds);
38+
}
39+
catch (Exception ex)
40+
{
41+
stopwatch.Stop();
42+
_logger.LogError(
43+
ex,
44+
"Error executing command {CommandName} after {ElapsedMilliseconds} ms",
45+
commandName,
46+
stopwatch.ElapsedMilliseconds);
47+
throw;
48+
}
49+
}
50+
}
51+
}

src/Cortex.Mediator/Commands/ICommand.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,14 @@
88
public interface ICommand<TResult>
99
{
1010
}
11+
12+
// feature #141
13+
14+
/// <summary>
15+
/// Represents a command in the CQRS pattern.
16+
/// Commands are used to change the system state and do not return a value.
17+
/// </summary>
18+
public interface ICommand
19+
{
20+
}
1121
}

src/Cortex.Mediator/Commands/ICommandHandler.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,23 @@ public interface ICommandHandler<in TCommand, TResult>
1818
/// <param name="cancellationToken">The cancellation token.</param>
1919
Task<TResult> Handle(TCommand command, CancellationToken cancellationToken);
2020
}
21+
22+
23+
24+
// feature #141
25+
26+
/// <summary>
27+
/// Defines a handler for a command.
28+
/// </summary>
29+
/// <typeparam name="TCommand">The type of command being handled.</typeparam>
30+
public interface ICommandHandler<in TCommand>
31+
where TCommand : ICommand
32+
{
33+
/// <summary>
34+
/// Handles the specified command.
35+
/// </summary>
36+
/// <param name="command">The command to handle.</param>
37+
/// <param name="cancellationToken">The cancellation token.</param>
38+
Task Handle(TCommand command, CancellationToken cancellationToken);
39+
}
2140
}

src/Cortex.Mediator/Commands/ICommandPipelineBehavior.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,33 @@ Task<TResult> Handle(
1919
CancellationToken cancellationToken);
2020
}
2121

22+
23+
// For non returning commands
24+
// feature #141
25+
26+
/// <summary>
27+
/// Defines a pipeline behavior for wrapping command handlers.
28+
/// </summary>
29+
/// <typeparam name="TCommand">The type of command being handled.</typeparam>
30+
public interface ICommandPipelineBehavior<in TCommand>
31+
where TCommand : ICommand
32+
{
33+
/// <summary>
34+
/// Handles the command and invokes the next behavior in the pipeline.
35+
/// </summary>
36+
Task Handle(
37+
TCommand command,
38+
CommandHandlerDelegate next,
39+
CancellationToken cancellationToken);
40+
}
41+
2242
/// <summary>
2343
/// Represents a delegate that wraps the command handler execution.
2444
/// </summary>
2545
public delegate Task<TResult> CommandHandlerDelegate<TResult>();
46+
47+
/// <summary>
48+
/// Represents a delegate that wraps the command handler execution.
49+
/// </summary>
50+
public delegate Task CommandHandlerDelegate();
2651
}

src/Cortex.Mediator/DependencyInjection/MediatorOptions.cs

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace Cortex.Mediator.DependencyInjection
99
public class MediatorOptions
1010
{
1111
internal List<Type> CommandBehaviors { get; } = new();
12+
internal List<Type> VoidCommandBehaviors { get; } = new();
1213
internal List<Type> QueryBehaviors { get; } = new();
1314

1415
public bool OnlyPublicClasses { get; set; } = true;
@@ -23,21 +24,25 @@ public MediatorOptions AddCommandPipelineBehavior<TBehavior>()
2324
var behaviorType = typeof(TBehavior);
2425

2526
if (behaviorType.IsGenericTypeDefinition)
26-
{
2727
throw new ArgumentException("Open generic types must be registered using AddOpenCommandPipelineBehavior");
28-
}
2928

30-
var implementsInterface = behaviorType
31-
.GetInterfaces()
32-
.Any(i => i.IsGenericType &&
33-
i.GetGenericTypeDefinition() == typeof(ICommandPipelineBehavior<,>));
29+
var implementsReturning =
30+
behaviorType.GetInterfaces().Any(i => i.IsGenericType &&
31+
i.GetGenericTypeDefinition() == typeof(ICommandPipelineBehavior<,>));
3432

35-
if (!implementsInterface)
36-
{
37-
throw new ArgumentException("Type must implement ICommandPipelineBehavior<,>");
38-
}
33+
var implementsNonReturning =
34+
behaviorType.GetInterfaces().Any(i => i.IsGenericType &&
35+
i.GetGenericTypeDefinition() == typeof(ICommandPipelineBehavior<>));
36+
37+
if (!implementsReturning && !implementsNonReturning)
38+
throw new ArgumentException("Type must implement ICommandPipelineBehavior<,> or ICommandPipelineBehavior<>");
39+
40+
if (implementsReturning)
41+
CommandBehaviors.Add(behaviorType);
42+
43+
if (implementsNonReturning)
44+
VoidCommandBehaviors.Add(behaviorType);
3945

40-
CommandBehaviors.Add(behaviorType);
4146
return this;
4247
}
4348

@@ -47,29 +52,25 @@ public MediatorOptions AddCommandPipelineBehavior<TBehavior>()
4752
public MediatorOptions AddOpenCommandPipelineBehavior(Type openGenericBehaviorType)
4853
{
4954
if (!openGenericBehaviorType.IsGenericTypeDefinition)
50-
{
5155
throw new ArgumentException("Type must be an open generic type definition");
52-
}
5356

54-
var implementsInterface = openGenericBehaviorType
55-
.GetInterfaces()
56-
.Any(i => i.IsGenericType &&
57-
i.GetGenericTypeDefinition() == typeof(ICommandPipelineBehavior<,>));
57+
var implementsReturning =
58+
openGenericBehaviorType.GetInterfaces().Any(i => i.IsGenericType &&
59+
i.GetGenericTypeDefinition() == typeof(ICommandPipelineBehavior<,>));
5860

59-
// For open generics, interface might not appear in GetInterfaces() yet; check by definition instead.
60-
if (!implementsInterface &&
61-
!(openGenericBehaviorType.IsGenericTypeDefinition &&
62-
openGenericBehaviorType.GetGenericTypeDefinition() == openGenericBehaviorType))
63-
{
64-
// Fall back to checking generic arguments count to give a clear error
65-
var ok = openGenericBehaviorType.GetGenericArguments().Length == 2;
66-
if (!ok)
67-
{
68-
throw new ArgumentException("Type must implement ICommandPipelineBehavior<,>");
69-
}
70-
}
61+
var implementsNonReturning =
62+
openGenericBehaviorType.GetInterfaces().Any(i => i.IsGenericType &&
63+
i.GetGenericTypeDefinition() == typeof(ICommandPipelineBehavior<>));
64+
65+
if (!implementsReturning && !implementsNonReturning)
66+
throw new ArgumentException("Type must implement ICommandPipelineBehavior<,> or ICommandPipelineBehavior<>");
67+
68+
if (implementsReturning)
69+
CommandBehaviors.Add(openGenericBehaviorType);
70+
71+
if (implementsNonReturning)
72+
VoidCommandBehaviors.Add(openGenericBehaviorType);
7173

72-
CommandBehaviors.Add(openGenericBehaviorType);
7374
return this;
7475
}
7576

src/Cortex.Mediator/DependencyInjection/MediatorOptionsExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ public static MediatorOptions AddDefaultBehaviors(this MediatorOptions options)
99
return options
1010
// Register the open generic logging behavior for commands that return TResult
1111
.AddOpenCommandPipelineBehavior(typeof(LoggingCommandBehavior<,>))
12-
.AddOpenQueryPipelineBehavior(typeof(LoggingQueryBehavior<,>));
12+
.AddOpenQueryPipelineBehavior(typeof(LoggingQueryBehavior<,>))
13+
.AddOpenCommandPipelineBehavior(typeof(LoggingCommandBehavior<>)); // Add void command logging
1314
}
1415
}
1516
}

src/Cortex.Mediator/DependencyInjection/ServiceCollectionExtensions.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ private static void RegisterHandlers(
4949
.AsImplementedInterfaces()
5050
.WithScopedLifetime());
5151

52+
// feature #141 - Register void command handlers
53+
services.Scan(scan => scan
54+
.FromAssemblies(assemblies)
55+
.AddClasses(classes => classes
56+
.AssignableTo(typeof(ICommandHandler<>)), options.OnlyPublicClasses)
57+
.AsImplementedInterfaces()
58+
.WithScopedLifetime());
59+
5260
services.Scan(scan => scan
5361
.FromAssemblies(assemblies)
5462
.AddClasses(classes => classes
@@ -72,6 +80,12 @@ private static void RegisterPipelineBehaviors(IServiceCollection services, Media
7280
services.AddTransient(typeof(ICommandPipelineBehavior<,>), behaviorType);
7381
}
7482

83+
// feature #141 - Register non-returning command pipeline behaviors
84+
foreach (var behaviorType in options.VoidCommandBehaviors)
85+
{
86+
services.AddTransient(typeof(ICommandPipelineBehavior<>), behaviorType);
87+
}
88+
7589
// Query behaviors (if needed)
7690
foreach (var behaviorType in options.QueryBehaviors)
7791
{

src/Cortex.Mediator/IMediator.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ Task<TResult> SendCommandAsync<TCommand, TResult>(
1616
CancellationToken cancellationToken = default)
1717
where TCommand : ICommand<TResult>;
1818

19+
Task SendCommandAsync<TCommand>(
20+
TCommand command,
21+
CancellationToken cancellationToken = default)
22+
where TCommand : ICommand;
23+
1924
Task<TResult> SendQueryAsync<TQuery, TResult>(
2025
TQuery query,
2126
CancellationToken cancellationToken = default)

src/Cortex.Mediator/Mediator.cs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public Mediator(IServiceProvider serviceProvider)
2222
}
2323

2424
public async Task<TResult> SendCommandAsync<TCommand, TResult>(TCommand command, CancellationToken cancellationToken = default)
25-
where TCommand : ICommand<TResult>
25+
where TCommand : ICommand<TResult>
2626
{
2727
var handler = _serviceProvider.GetRequiredService<ICommandHandler<TCommand, TResult>>();
2828

@@ -31,7 +31,19 @@ public async Task<TResult> SendCommandAsync<TCommand, TResult>(TCommand command,
3131
handler = new PipelineBehaviorNextDelegate<TCommand, TResult>(behavior, handler);
3232
}
3333

34-
return await handler.Handle(command, cancellationToken);
34+
return await handler.Handle(command, cancellationToken);
35+
}
36+
37+
public async Task SendCommandAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default) where TCommand : ICommand
38+
{
39+
var handler = _serviceProvider.GetRequiredService<ICommandHandler<TCommand>>();
40+
41+
foreach (var behavior in _serviceProvider.GetServices<ICommandPipelineBehavior<TCommand>>().Reverse())
42+
{
43+
handler = new PipelineBehaviorNextDelegate<TCommand>(behavior, handler);
44+
}
45+
46+
await handler.Handle(command, cancellationToken);
3547
}
3648

3749
public async Task<TResult> SendQueryAsync<TQuery, TResult>(TQuery query, CancellationToken cancellationToken = default)
@@ -57,6 +69,7 @@ public async Task PublishAsync<TNotification>(
5769
await Task.WhenAll(tasks);
5870
}
5971

72+
6073
private class PipelineBehaviorNextDelegate<TCommand, TResult> : ICommandHandler<TCommand, TResult>
6174
where TCommand : ICommand<TResult>
6275
{
@@ -80,6 +93,29 @@ public Task<TResult> Handle(TCommand command, CancellationToken cancellationToke
8093
}
8194
}
8295

96+
private class PipelineBehaviorNextDelegate<TCommand> : ICommandHandler<TCommand>
97+
where TCommand : ICommand
98+
{
99+
private readonly ICommandPipelineBehavior<TCommand> _behavior;
100+
private readonly ICommandHandler<TCommand> _next;
101+
102+
public PipelineBehaviorNextDelegate(
103+
ICommandPipelineBehavior<TCommand> behavior,
104+
ICommandHandler<TCommand> next)
105+
{
106+
_behavior = behavior;
107+
_next = next;
108+
}
109+
110+
public Task Handle(TCommand command, CancellationToken cancellationToken)
111+
{
112+
return _behavior.Handle(
113+
command,
114+
() => _next.Handle(command, cancellationToken),
115+
cancellationToken);
116+
}
117+
}
118+
83119
private class QueryPipelineBehaviorNextDelegate<TQuery, TResult>
84120
: IQueryHandler<TQuery, TResult>
85121
where TQuery : IQuery<TResult>

0 commit comments

Comments
 (0)