Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions src/Cortex.Mediator/Behaviors/LoggingCommandBehavior.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Cortex.Mediator.Commands;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -9,32 +10,45 @@ namespace Cortex.Mediator.Behaviors
/// <summary>
/// Pipeline behavior for logging command/query execution.
/// </summary>
public class LoggingCommandBehavior<TCommand> : ICommandPipelineBehavior<TCommand>
where TCommand : ICommand
public sealed class LoggingCommandBehavior<TCommand, TResult> : ICommandPipelineBehavior<TCommand, TResult>
where TCommand : ICommand<TResult>
{
private readonly ILogger<LoggingCommandBehavior<TCommand>> _logger;
private readonly ILogger<LoggingCommandBehavior<TCommand, TResult>> _logger;

public LoggingCommandBehavior(ILogger<LoggingCommandBehavior<TCommand>> logger)
public LoggingCommandBehavior(ILogger<LoggingCommandBehavior<TCommand, TResult>> logger)
{
_logger = logger;
}

public async Task Handle(
public async Task<TResult> Handle(
TCommand command,
CommandHandlerDelegate next,
CommandHandlerDelegate<TResult> next,
CancellationToken cancellationToken)
{
var commandName = typeof(TCommand).Name;
_logger.LogInformation("Executing command {CommandName}", commandName);

var stopwatch = Stopwatch.StartNew(); // start timing
try
{
await next();
_logger.LogInformation("Command {CommandName} executed successfully", commandName);
var result = await next();

stopwatch.Stop();
_logger.LogInformation(
"Command {CommandName} executed successfully in {ElapsedMilliseconds} ms",
commandName,
stopwatch.ElapsedMilliseconds);

return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing command {CommandName}", commandName);
stopwatch.Stop();
_logger.LogError(
ex,
"Error executing command {CommandName} after {ElapsedMilliseconds} ms",
commandName,
stopwatch.ElapsedMilliseconds);
throw;
}
}
Expand Down
56 changes: 56 additions & 0 deletions src/Cortex.Mediator/Behaviors/LoggingQueryBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Cortex.Mediator.Queries;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

namespace Cortex.Mediator.Behaviors
{
/// <summary>
/// Pipeline behavior for logging command/query execution.
/// </summary>
public sealed class LoggingQueryBehavior<TQuery, TResult> : IQueryPipelineBehavior<TQuery, TResult>
where TQuery : IQuery<TResult>
{
private readonly ILogger<LoggingQueryBehavior<TQuery, TResult>> _logger;

public LoggingQueryBehavior(ILogger<LoggingQueryBehavior<TQuery, TResult>> logger)
{
_logger = logger;
}

public async Task<TResult> Handle(
TQuery command,
QueryHandlerDelegate<TResult> next,
CancellationToken cancellationToken)
{
var queryName = typeof(TQuery).Name;
_logger.LogInformation("Executing query {QueryName}", queryName);

var stopwatch = Stopwatch.StartNew(); // start timing
try
{
var result = await next();

stopwatch.Stop();
_logger.LogInformation(
"Query {QueryName} executed successfully in {ElapsedMilliseconds} ms",
queryName,
stopwatch.ElapsedMilliseconds);

return result;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(
ex,
"Error executing query {QueryName} after {ElapsedMilliseconds} ms",
queryName,
stopwatch.ElapsedMilliseconds);
throw;
}
}
}
}
39 changes: 0 additions & 39 deletions src/Cortex.Mediator/Behaviors/TransactionCommandBehavior.cs

This file was deleted.

50 changes: 0 additions & 50 deletions src/Cortex.Mediator/Behaviors/ValidationCommandBehavior.cs

This file was deleted.

5 changes: 3 additions & 2 deletions src/Cortex.Mediator/Commands/ICommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
{
/// <summary>
/// Represents a command in the CQRS pattern.
/// Commands are used to change the system state and do not return a value.
/// Commands are used to change the system state and do return a value.
/// Please note that this is not a common practice in CQRS, as commands typically do not return values.
/// </summary>
public interface ICommand
public interface ICommand<TResult>
{
}
}
7 changes: 4 additions & 3 deletions src/Cortex.Mediator/Commands/ICommandHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ namespace Cortex.Mediator.Commands
/// Defines a handler for a command.
/// </summary>
/// <typeparam name="TCommand">The type of command being handled.</typeparam>
public interface ICommandHandler<in TCommand>
where TCommand : ICommand
/// <typeparam name="TResult">The type of command that is being returned.</typeparam>
public interface ICommandHandler<in TCommand, TResult>
where TCommand : ICommand<TResult>
{
/// <summary>
/// Handles the specified command.
/// </summary>
/// <param name="command">The command to handle.</param>
/// <param name="cancellationToken">The cancellation token.</param>
Task Handle(TCommand command, CancellationToken cancellationToken);
Task<TResult> Handle(TCommand command, CancellationToken cancellationToken);
}
}
10 changes: 5 additions & 5 deletions src/Cortex.Mediator/Commands/ICommandPipelineBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ namespace Cortex.Mediator.Commands
/// Defines a pipeline behavior for wrapping command handlers.
/// </summary>
/// <typeparam name="TCommand">The type of command being handled.</typeparam>
public interface ICommandPipelineBehavior<in TCommand>
where TCommand : ICommand
public interface ICommandPipelineBehavior<in TCommand, TResult>
where TCommand : ICommand<TResult>
{
/// <summary>
/// Handles the command and invokes the next behavior in the pipeline.
/// </summary>
Task Handle(
Task<TResult> Handle(
TCommand command,
CommandHandlerDelegate next,
CommandHandlerDelegate<TResult> next,
CancellationToken cancellationToken);
}

/// <summary>
/// Represents a delegate that wraps the command handler execution.
/// </summary>
public delegate Task CommandHandlerDelegate();
public delegate Task<TResult> CommandHandlerDelegate<TResult>();
}
8 changes: 8 additions & 0 deletions src/Cortex.Mediator/Common/Unit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Cortex.Mediator
{
public readonly struct Unit
{
public static readonly Unit Value = new();
public override string ToString() => "()";
}
}
2 changes: 0 additions & 2 deletions src/Cortex.Mediator/Cortex.Mediator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.3" />
Expand Down
43 changes: 41 additions & 2 deletions src/Cortex.Mediator/DependencyInjection/MediatorOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Cortex.Mediator.Commands;
using Cortex.Mediator.Commands;
using Cortex.Mediator.Queries;
using System;
using System.Collections.Generic;
Expand All @@ -11,25 +11,64 @@ public class MediatorOptions
internal List<Type> CommandBehaviors { get; } = new();
internal List<Type> QueryBehaviors { get; } = new();

public bool OnlyPublicClasses { get; set; } = true;


/// <summary>
/// Register a *closed* command pipeline behavior.
/// </summary>
public MediatorOptions AddCommandPipelineBehavior<TBehavior>()
where TBehavior : ICommandPipelineBehavior<ICommand> // Add constraint
where TBehavior : class // Add constraint
{
var behaviorType = typeof(TBehavior);

if (behaviorType.IsGenericTypeDefinition)
{
throw new ArgumentException("Open generic types must be registered using AddOpenCommandPipelineBehavior");
}

var implementsInterface = behaviorType
.GetInterfaces()
.Any(i => i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(ICommandPipelineBehavior<,>));

if (!implementsInterface)
{
throw new ArgumentException("Type must implement ICommandPipelineBehavior<,>");
}

CommandBehaviors.Add(behaviorType);
return this;
}

/// <summary>
/// Register an *open generic* command pipeline behavior, e.g. typeof(LoggingCommandBehavior&lt;,&gt;).
/// </summary>
public MediatorOptions AddOpenCommandPipelineBehavior(Type openGenericBehaviorType)
{
if (!openGenericBehaviorType.IsGenericTypeDefinition)
{
throw new ArgumentException("Type must be an open generic type definition");
}

var implementsInterface = openGenericBehaviorType
.GetInterfaces()
.Any(i => i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(ICommandPipelineBehavior<,>));

// For open generics, interface might not appear in GetInterfaces() yet; check by definition instead.
if (!implementsInterface &&
!(openGenericBehaviorType.IsGenericTypeDefinition &&
openGenericBehaviorType.GetGenericTypeDefinition() == openGenericBehaviorType))
{
// Fall back to checking generic arguments count to give a clear error
var ok = openGenericBehaviorType.GetGenericArguments().Length == 2;
if (!ok)
{
throw new ArgumentException("Type must implement ICommandPipelineBehavior<,>");
}
}

CommandBehaviors.Add(openGenericBehaviorType);
return this;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Cortex.Mediator.Behaviors;
using Cortex.Mediator.Commands;

namespace Cortex.Mediator.DependencyInjection
{
Expand All @@ -8,9 +7,9 @@ public static class MediatorOptionsExtensions
public static MediatorOptions AddDefaultBehaviors(this MediatorOptions options)
{
return options
.AddCommandPipelineBehavior<ValidationCommandBehavior<ICommand>>()
.AddCommandPipelineBehavior<LoggingCommandBehavior<ICommand>>()
.AddCommandPipelineBehavior<TransactionCommandBehavior<ICommand>>();
// Register the open generic logging behavior for commands that return TResult
.AddOpenCommandPipelineBehavior(typeof(LoggingCommandBehavior<,>))
.AddOpenQueryPipelineBehavior(typeof(LoggingQueryBehavior<,>));
}
}
}
Loading
Loading