Compare commits

3 Commits

Author SHA1 Message Date
2fded2809f pipeline cache and dispatch optimizations
All checks were successful
.NET Test / test (push) Successful in 13m14s
.NET Publish / publish (push) Successful in 11m1s
2025-02-04 20:49:05 +04:00
7c3dd84971 readme and publish workflow
All checks were successful
.NET Test / test (push) Successful in 1m0s
.NET Publish / publish (push) Successful in 1m1s
2025-02-02 11:51:55 +04:00
d437d06c09 renamed Behavior 2025-02-02 11:51:34 +04:00
20 changed files with 492 additions and 186 deletions

View File

@@ -25,6 +25,15 @@ tab_width = 4
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = true
#### .NET diagnostic severity
[*.{cs,vb}]
# CS9124: Parameter is captured into the state of the enclosing type and its value is also used to initialize a field, property, or event.
dotnet_diagnostic.CS9124.severity = error
# CS9107: Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
dotnet_diagnostic.CS9107.severity = error
#### .NET Coding Conventions #### #### .NET Coding Conventions ####
[*.{cs,vb}] [*.{cs,vb}]

View File

@@ -0,0 +1,38 @@
name: .NET Publish
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: https://github.com/actions/setup-dotnet@v4
with:
dotnet-version: 9.x
- name: Restore dependencies
run: dotnet restore --nologo
- name: Test
run: dotnet test --nologo --no-restore --configuration Release
- name: Create the package
env:
RELEASE_VERSION: ${{ gitea.ref_name }}
run: >
dotnet pack --no-restore --configuration Release --output nupkgs
`echo $RELEASE_VERSION | sed -E 's|^(v([0-9]+(\.[0-9]+){2}))(-([a-z0-9]+)){1}|/p:ReleaseVersion=\2 /p:VersionSuffix=\5|; s|^(v([0-9]+(\.[0-9]+){2}))$|/p:ReleaseVersion=\2|'`
- name: Publish the package to Gitea
run: dotnet nuget push --source ${{ vars.OUTPUT_NUGET_REGISTRY }} --api-key ${{ secrets.LOCAL_NUGET_PACKAGE_TOKEN }} nupkgs/*.nupkg
- name: Publish the package to NuGet.org
run: dotnet nuget push --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_PACKAGE_TOKEN }} nupkgs/*.nupkg

View File

@@ -7,11 +7,22 @@
<Description>Lightweight, easy-to-use C# library designed to simplify the implementation of the Command Query Responsibility Segregation (CQRS) pattern.</Description> <Description>Lightweight, easy-to-use C# library designed to simplify the implementation of the Command Query Responsibility Segregation (CQRS) pattern.</Description>
<Authors>JustFixMe</Authors> <Authors>JustFixMe</Authors>
<Copyright>Copyright (c) 2025 JustFixMe</Copyright> <Copyright>Copyright (c) 2025 JustFixMe</Copyright>
<RepositoryUrl>https://github.com/JustFixMe/Just.Core/</RepositoryUrl> <RepositoryUrl>https://github.com/JustFixMe/Just.Cqrs/</RepositoryUrl>
<PackageTags>c#;CQRS</PackageTags> <PackageTags>csharp;cqrs;cqrs-pattern;</PackageTags>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageReadmeFile>readme.md</PackageReadmeFile> <PackageReadmeFile>readme.md</PackageReadmeFile>
<ReleaseVersion Condition=" '$(ReleaseVersion)' == '' ">1.0.0</ReleaseVersion>
<VersionSuffix Condition=" '$(VersionSuffix)' != '' ">$(VersionSuffix)</VersionSuffix>
<VersionPrefix Condition=" '$(VersionSuffix)' != '' ">$(ReleaseVersion)</VersionPrefix>
<Version Condition=" '$(VersionSuffix)' == '' ">$(ReleaseVersion)</Version>
<AssemblyVersion>$(ReleaseVersion)</AssemblyVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Include="..\..\readme.md" Pack="true" PackagePath=""/>
<None Include="..\..\LICENSE" Pack="true" PackagePath=""/>
</ItemGroup>
</Project> </Project>

View File

@@ -7,24 +7,90 @@ Inspired by [MediatR](https://github.com/jbogard/MediatR)
## Features ## Features
* Separate dispatching of Commands/Queries * Separate dispatching of Commands/Queries
* Middleware-like behaviours * Middleware-like Behaviors
## Compatibility
**Just.Cqrs** is built for .Net Standard 2.1 and .NET 8.0 and 9.0.
## Getting Started ## Getting Started
### Install from NuGet.org ### Install from NuGet.org
``` ```bash
# install the package using NuGet # install the package using NuGet
dotnet add package Just.Cqrs dotnet add package Just.Cqrs
``` ```
### Register in DI with ```IServiceCollection``` ### Register in DI with ```IServiceCollection```
```cs ```csharp
services.AddCqrs(opt => opt services.AddCqrs(opt => opt
.AddQueryHandler<SomeQueryHandler>() .AddQueryHandler<SomeQueryHandler>()
.AddCommandHandler<SomeCommandHandler>() .AddCommandHandler<SomeCommandHandler>()
.AddBehaviour<SomeBehaviour>() .AddBehavior<SomeQueryBehavior>()
.AddOpenBehaviour(typeof(SomeOpenBehaviour<,>)) .AddOpenBehavior(typeof(LoggingBehavior<,>))
); );
``` ```
## Example Usage
### Define a Query and Handler
```csharp
record GetUserByIdQuery(int UserId) : IKnownQuery<User>;
class GetUserByIdQueryHandler : IQueryHandler<GetUserByIdQuery, User>
{
public ValueTask<User> Handle(GetUserByIdQuery query, CancellationToken cancellationToken)
{
// Fetch user logic here
}
}
// Use Dispatcher to execute the query
class GetUserByIdUseCase(IQueryDispatcher dispatcher)
{
public async Task<IResult> Execute(int userId, CancellationToken cancellationToken)
{
var user = await dispatcher.Dispatch(new GetUserByIdQuery(userId), cancellationToken);
}
}
```
\* *the same principles apply to commands*
### Define a Behavior
```csharp
class LoggingBehavior<TRequest, TResult>(ILogger logger) : IDispatchBehavior<TRequest, TResult>
where TRequest: notnull
{
public async ValueTask<TResult> Handle(
TRequest request,
DispatchFurtherDelegate<TResult> next,
CancellationToken cancellationToken)
{
logger.LogInformation("Handling request: {RequestType}", typeof(TRequest).Name);
var result = await next();
logger.LogInformation("Request handled: {RequestType}", typeof(TRequest).Name);
return result;
}
}
class SomeQueryBehavior : IDispatchBehavior<SomeQuery, SomeQueryResult>
{
public async ValueTask<SomeQueryResult> Handle(
SomeQuery request,
DispatchFurtherDelegate<SomeQueryResult> next,
CancellationToken cancellationToken)
{
// do something
return await next();
}
}
```
## License
**Just.Cqrs** is licensed under the [MIT License](LICENSE).

View File

@@ -1,9 +1,15 @@
using System.Diagnostics.CodeAnalysis;
using Just.Cqrs.Internal; using Just.Cqrs.Internal;
namespace Just.Cqrs; namespace Just.Cqrs;
public interface ICommandHandler<TCommand, TCommandResult> : ICommandHandlerImpl public interface ICommandHandler<TCommand, TCommandResult> : ICommandHandlerImpl, IGenericHandler<TCommand, TCommandResult>
where TCommand : notnull where TCommand : notnull
{ {
ValueTask<TCommandResult> Handle(TCommand command, CancellationToken cancellation); new ValueTask<TCommandResult> Handle(TCommand command, CancellationToken cancellation);
[ExcludeFromCodeCoverage]
ValueTask<TCommandResult> IGenericHandler<TCommand, TCommandResult>.Handle(
TCommand request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
} }

View File

@@ -12,7 +12,7 @@ public delegate ValueTask<TResponse> DispatchFurtherDelegate<TResponse>();
/// <summary> /// <summary>
/// Marker interface for static type checking. Should not be used directly. /// Marker interface for static type checking. Should not be used directly.
/// </summary> /// </summary>
public interface IDispatchBehaviour public interface IDispatchBehavior
{ {
Type RequestType { get; } Type RequestType { get; }
Type ResponseType { get; } Type ResponseType { get; }
@@ -23,7 +23,7 @@ public interface IDispatchBehaviour
/// </summary> /// </summary>
/// <typeparam name="TRequest">Request type</typeparam> /// <typeparam name="TRequest">Request type</typeparam>
/// <typeparam name="TResponse">Result type of dispatching command/query</typeparam> /// <typeparam name="TResponse">Result type of dispatching command/query</typeparam>
public interface IDispatchBehaviour<in TRequest, TResponse> : IDispatchBehaviour public interface IDispatchBehavior<in TRequest, TResponse> : IDispatchBehavior
where TRequest : notnull where TRequest : notnull
{ {
ValueTask<TResponse> Handle( ValueTask<TResponse> Handle(
@@ -32,7 +32,7 @@ public interface IDispatchBehaviour<in TRequest, TResponse> : IDispatchBehaviour
CancellationToken cancellationToken); CancellationToken cancellationToken);
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
Type IDispatchBehaviour.RequestType => typeof(TRequest); Type IDispatchBehavior.RequestType => typeof(TRequest);
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
Type IDispatchBehaviour.ResponseType => typeof(TResponse); Type IDispatchBehavior.ResponseType => typeof(TResponse);
} }

View File

@@ -1,9 +1,15 @@
using System.Diagnostics.CodeAnalysis;
using Just.Cqrs.Internal; using Just.Cqrs.Internal;
namespace Just.Cqrs; namespace Just.Cqrs;
public interface IQueryHandler<TQuery, TQueryResult> : IQueryHandlerImpl public interface IQueryHandler<TQuery, TQueryResult> : IQueryHandlerImpl, IGenericHandler<TQuery, TQueryResult>
where TQuery : notnull where TQuery : notnull
{ {
ValueTask<TQueryResult> Handle(TQuery query, CancellationToken cancellation); new ValueTask<TQueryResult> Handle(TQuery query, CancellationToken cancellation);
[ExcludeFromCodeCoverage]
ValueTask<TQueryResult> IGenericHandler<TQuery, TQueryResult>.Handle(
TQuery request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
} }

View File

@@ -1,3 +1,6 @@
namespace Just.Cqrs.Internal; namespace Just.Cqrs.Internal;
/// <summary>
/// Marker interface for static type checking. Should not be used directly.
/// </summary>
public interface ICommandHandlerImpl { } public interface ICommandHandlerImpl { }

View File

@@ -0,0 +1,10 @@
namespace Just.Cqrs.Internal;
/// <summary>
/// Marker interface for static type checking. Should not be used directly.
/// </summary>
public interface IGenericHandler<TRequest, TResponse>
where TRequest : notnull
{
ValueTask<TResponse> Handle(TRequest request, CancellationToken cancellation);
}

View File

@@ -1,3 +1,6 @@
namespace Just.Cqrs.Internal; namespace Just.Cqrs.Internal;
/// <summary>
/// Marker interface for static type checking. Should not be used directly.
/// </summary>
public interface IQueryHandlerImpl { } public interface IQueryHandlerImpl { }

View File

@@ -8,15 +8,19 @@ namespace Microsoft.Extensions.DependencyInjection;
public static class CqrsServicesExtensions public static class CqrsServicesExtensions
{ {
/// <summary>
/// Adds all configured Command and Query handlers, behaviors and default implementations of <see cref="ICommandDispatcher"/> and <see cref="IQueryDispatcher"/>.
/// </summary>
/// <remarks>
/// If called multiple times <see cref="ICommandDispatcher"/> and <see cref="IQueryDispatcher"/> will still be added once
/// </remarks>
public static IServiceCollection AddCqrs(this IServiceCollection services, Action<CqrsServicesOptions>? configure = null) public static IServiceCollection AddCqrs(this IServiceCollection services, Action<CqrsServicesOptions>? configure = null)
{ {
var options = new CqrsServicesOptions(services); var options = new CqrsServicesOptions(services);
configure?.Invoke(options); configure?.Invoke(options);
services.TryAddKeyedSingleton<IMethodsCache, ConcurrentMethodsCache>(MethodsCacheServiceKey.DispatchCommand); services.TryAddSingleton<IMethodsCache, ConcurrentMethodsCache>();
services.TryAddTransient<ICommandDispatcher, CommandDispatcherImpl>(); services.TryAddTransient<ICommandDispatcher, CommandDispatcherImpl>();
services.TryAddKeyedSingleton<IMethodsCache, ConcurrentMethodsCache>(MethodsCacheServiceKey.DispatchQuery);
services.TryAddTransient<IQueryDispatcher, QueryDispatcherImpl>(); services.TryAddTransient<IQueryDispatcher, QueryDispatcherImpl>();
foreach (var (service, impl, lifetime) in options.CommandHandlers) foreach (var (service, impl, lifetime) in options.CommandHandlers)
@@ -27,7 +31,7 @@ public static class CqrsServicesExtensions
{ {
services.TryAdd(new ServiceDescriptor(service, impl, lifetime)); services.TryAdd(new ServiceDescriptor(service, impl, lifetime));
} }
foreach (var (service, impl, lifetime) in options.Behaviours) foreach (var (service, impl, lifetime) in options.Behaviors)
{ {
services.Add(new ServiceDescriptor(service, impl, lifetime)); services.Add(new ServiceDescriptor(service, impl, lifetime));
} }
@@ -75,43 +79,43 @@ public static class CqrsServicesExtensions
return options; return options;
} }
public static CqrsServicesOptions AddOpenBehaviour(this CqrsServicesOptions options, Type behaviour, ServiceLifetime lifetime = ServiceLifetime.Singleton) public static CqrsServicesOptions AddOpenBehavior(this CqrsServicesOptions options, Type behavior, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{ {
var interfaces = behaviour.FindInterfaces( var interfaces = behavior.FindInterfaces(
static (x, t) => x.IsGenericType && x.GetGenericTypeDefinition() == (Type)t!, static (x, t) => x.IsGenericType && x.GetGenericTypeDefinition() == (Type)t!,
typeof(IDispatchBehaviour<,>)); typeof(IDispatchBehavior<,>));
if (interfaces.Length == 0) if (interfaces.Length == 0)
{ {
throw new ArgumentException("Supplied type does not implement IDispatchBehaviour<,> interface.", nameof(behaviour)); throw new ArgumentException("Supplied type does not implement IDispatchBehavior<,> interface.", nameof(behavior));
} }
if (!behaviour.ContainsGenericParameters) if (!behavior.ContainsGenericParameters)
{ {
throw new ArgumentException("Supplied type is not sutable for open behaviour.", nameof(behaviour)); throw new ArgumentException("Supplied type is not suitable for open Behavior.", nameof(behavior));
} }
options.Behaviours.Add((typeof(IDispatchBehaviour<,>), behaviour, lifetime)); options.Behaviors.Add((typeof(IDispatchBehavior<,>), behavior, lifetime));
return options; return options;
} }
public static CqrsServicesOptions AddBehaviour<TBehaviour>(this CqrsServicesOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton) public static CqrsServicesOptions AddBehavior<TBehavior>(this CqrsServicesOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton)
where TBehaviour : notnull, IDispatchBehaviour where TBehavior : notnull, IDispatchBehavior
{ {
var type = typeof(TBehaviour); var type = typeof(TBehavior);
var interfaces = type.FindInterfaces( var interfaces = type.FindInterfaces(
static (x, t) => x.IsGenericType && x.GetGenericTypeDefinition() == (Type)t!, static (x, t) => x.IsGenericType && x.GetGenericTypeDefinition() == (Type)t!,
typeof(IDispatchBehaviour<,>)); typeof(IDispatchBehavior<,>));
if (interfaces.Length == 0) if (interfaces.Length == 0)
{ {
throw new InvalidOperationException("Supplied type does not implement IDispatchBehaviour<,> interface."); throw new InvalidOperationException("Supplied type does not implement IDispatchBehavior<,> interface.");
} }
foreach (var interfaceType in interfaces) foreach (var interfaceType in interfaces)
{ {
options.Behaviours.Add(( options.Behaviors.Add((
interfaceType, interfaceType,
type, type,
lifetime)); lifetime));

View File

@@ -4,7 +4,7 @@ namespace Just.Cqrs;
public sealed class CqrsServicesOptions(IServiceCollection services) public sealed class CqrsServicesOptions(IServiceCollection services)
{ {
internal readonly List<(Type Service, Type Impl, ServiceLifetime Lifetime)> Behaviours = []; internal readonly List<(Type Service, Type Impl, ServiceLifetime Lifetime)> Behaviors = [];
internal readonly List<(Type Service, Type Impl, ServiceLifetime Lifetime)> CommandHandlers = []; internal readonly List<(Type Service, Type Impl, ServiceLifetime Lifetime)> CommandHandlers = [];
internal readonly List<(Type Service, Type Impl, ServiceLifetime Lifetime)> QueryHandlers = []; internal readonly List<(Type Service, Type Impl, ServiceLifetime Lifetime)> QueryHandlers = [];

View File

@@ -6,27 +6,25 @@ namespace Just.Cqrs.Internal;
internal sealed class CommandDispatcherImpl( internal sealed class CommandDispatcherImpl(
IServiceProvider services, IServiceProvider services,
[FromKeyedServices(MethodsCacheServiceKey.DispatchCommand)]IMethodsCache methodsCache IMethodsCache methodsCache
) : ICommandDispatcher ) : DispatcherBase(methodsCache), ICommandDispatcher
{ {
private static readonly Func<(Type RequestType, Type ResponseType), Delegate> CreateDispatchCommandDelegate;
static CommandDispatcherImpl()
{
var dispatcherType = typeof(CommandDispatcherImpl);
var genericDispatchImplMethod = dispatcherType
.GetMethod(nameof(DispatchCommandImpl), BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new InvalidOperationException($"{nameof(DispatchCommandImpl)} method not found.");
CreateDispatchCommandDelegate = methodsCacheKey => CreateDispatchDelegate(methodsCacheKey, dispatcherType, genericDispatchImplMethod);
}
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
public ValueTask<TCommandResult> Dispatch<TCommandResult>(object command, CancellationToken cancellationToken) public ValueTask<TCommandResult> Dispatch<TCommandResult>(object command, CancellationToken cancellationToken)
=> DispatchCommand<TCommandResult>(command, cancellationToken); => DispatchInternal<TCommandResult>(CreateDispatchCommandDelegate, command, cancellationToken);
public ValueTask<TCommandResult> Dispatch<TCommandResult>(IKnownCommand<TCommandResult> command, CancellationToken cancellationToken) public ValueTask<TCommandResult> Dispatch<TCommandResult>(IKnownCommand<TCommandResult> command, CancellationToken cancellationToken)
=> DispatchCommand<TCommandResult>(command, cancellationToken); => DispatchInternal<TCommandResult>(CreateDispatchCommandDelegate, command, cancellationToken);
private ValueTask<TCommandResult> DispatchCommand<TCommandResult>(object command, CancellationToken cancellationToken)
{
var commandType = command.GetType();
var dispatchCommandMethod = methodsCache.GetOrAdd(commandType, static t => typeof(CommandDispatcherImpl)
.GetMethod(nameof(DispatchCommandImpl), BindingFlags.Instance | BindingFlags.NonPublic)!
.MakeGenericMethod(t, typeof(TCommandResult)));
return (ValueTask<TCommandResult>)dispatchCommandMethod
.Invoke(this, [command, cancellationToken])!;
}
private ValueTask<TCommandResult> DispatchCommandImpl<TCommand, TCommandResult>( private ValueTask<TCommandResult> DispatchCommandImpl<TCommand, TCommandResult>(
TCommand command, TCommand command,
@@ -34,14 +32,8 @@ internal sealed class CommandDispatcherImpl(
where TCommand : notnull where TCommand : notnull
{ {
var handler = services.GetRequiredService<ICommandHandler<TCommand, TCommandResult>>(); var handler = services.GetRequiredService<ICommandHandler<TCommand, TCommandResult>>();
var pipeline = services.GetServices<IDispatchBehaviour<TCommand, TCommandResult>>(); var pipeline = services.GetServices<IDispatchBehavior<TCommand, TCommandResult>>();
using var pipelineEnumerator = pipeline.GetEnumerator();
return DispatchDelegateFactory(pipelineEnumerator).Invoke(); return DispatchDelegateFactory(command, handler, pipeline, cancellationToken).Invoke();
DispatchFurtherDelegate<TCommandResult> DispatchDelegateFactory(IEnumerator<IDispatchBehaviour<TCommand, TCommandResult>> enumerator) =>
enumerator.MoveNext()
? (() => enumerator.Current.Handle(command, DispatchDelegateFactory(enumerator), cancellationToken))
: (() => handler.Handle(command, cancellationToken));
} }
} }

View File

@@ -0,0 +1,67 @@
using System.Linq.Expressions;
using System.Reflection;
namespace Just.Cqrs.Internal;
internal abstract class DispatcherBase(IMethodsCache methodsCache)
{
protected IMethodsCache MethodsCache { get; } = methodsCache;
protected ValueTask<TResult> DispatchInternal<TResult>(
Func<(Type, Type), Delegate> delegateFactory,
object request,
CancellationToken cancellationToken)
{
var cacheKey = (request.GetType(), typeof(TResult));
var dispatchDelegate = (Func<DispatcherBase, object, CancellationToken, ValueTask<TResult>>)
MethodsCache.GetOrAdd(cacheKey, delegateFactory);
return dispatchDelegate(this, request, cancellationToken);
}
protected DispatchFurtherDelegate<TResponse> DispatchDelegateFactory<TRequest, TResponse, THandler>(
TRequest request,
THandler handler,
IEnumerable<IDispatchBehavior<TRequest, TResponse>> behaviors,
CancellationToken cancellationToken)
where TRequest : notnull
where THandler : IGenericHandler<TRequest, TResponse>
{
DispatchFurtherDelegate<TResponse> pipeline = behaviors.Reverse()
.Aggregate<IDispatchBehavior<TRequest, TResponse>, DispatchFurtherDelegate<TResponse>>(
() => handler.Handle(request, cancellationToken),
(next, behavior) => () => behavior.Handle(request, next, cancellationToken)
);
return pipeline;
}
internal static Delegate CreateDispatchDelegate((Type RequestType, Type ResponseType) methodsCacheKey, Type dispatcherType, MethodInfo genericDispatchImplMethod)
{
var dispatcherBaseType = typeof(DispatcherBase);
var (requestType, responseType) = methodsCacheKey;
var dispatchImplMethod = genericDispatchImplMethod.MakeGenericMethod(requestType, responseType);
ParameterExpression[] lambdaParameters =
[
Expression.Parameter(dispatcherBaseType),
Expression.Parameter(typeof(object)),
Expression.Parameter(typeof(CancellationToken)),
];
Expression[] callParameters =
[
Expression.Convert(lambdaParameters[1], requestType),
lambdaParameters[2],
];
var lambdaExpression = Expression.Lambda(
typeof(Func<,,,>).MakeGenericType(
dispatcherBaseType,
typeof(object),
typeof(CancellationToken),
typeof(ValueTask<>).MakeGenericType(responseType)),
Expression.Call(Expression.Convert(lambdaParameters[0], dispatcherType), dispatchImplMethod, callParameters),
lambdaParameters
);
var dispatchQueryDelegate = lambdaExpression.Compile();
return dispatchQueryDelegate;
}
}

View File

@@ -1,17 +1,10 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Reflection;
namespace Just.Cqrs.Internal; namespace Just.Cqrs.Internal;
internal interface IMethodsCache internal interface IMethodsCache
{ {
MethodInfo GetOrAdd(Type key, Func<Type, MethodInfo> valueFactory); Delegate GetOrAdd((Type RequestType, Type ResponseType) key, Func<(Type RequestType, Type ResponseType), Delegate> valueFactory);
} }
internal static class MethodsCacheServiceKey internal sealed class ConcurrentMethodsCache : ConcurrentDictionary<(Type RequestType, Type ResponseType), Delegate>, IMethodsCache;
{
internal const string DispatchQuery = "q";
internal const string DispatchCommand = "c";
}
internal sealed class ConcurrentMethodsCache : ConcurrentDictionary<Type, MethodInfo>, IMethodsCache;

View File

@@ -6,27 +6,25 @@ namespace Just.Cqrs.Internal;
internal sealed class QueryDispatcherImpl( internal sealed class QueryDispatcherImpl(
IServiceProvider services, IServiceProvider services,
[FromKeyedServices(MethodsCacheServiceKey.DispatchQuery)]IMethodsCache methodsCache IMethodsCache methodsCache
) : IQueryDispatcher ) : DispatcherBase(methodsCache), IQueryDispatcher
{ {
private static readonly Func<(Type RequestType, Type ResponseType), Delegate> CreateDispatchQueryDelegate;
static QueryDispatcherImpl()
{
var dispatcherType = typeof(QueryDispatcherImpl);
var genericDispatchImplMethod = dispatcherType
.GetMethod(nameof(DispatchQueryImpl), BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new InvalidOperationException($"{nameof(DispatchQueryImpl)} method not found.");
CreateDispatchQueryDelegate = methodsCacheKey => CreateDispatchDelegate(methodsCacheKey, dispatcherType, genericDispatchImplMethod);
}
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
public ValueTask<TQueryResult> Dispatch<TQueryResult>(object query, CancellationToken cancellationToken) public ValueTask<TQueryResult> Dispatch<TQueryResult>(object query, CancellationToken cancellationToken)
=> DispatchQuery<TQueryResult>(query, cancellationToken); => DispatchInternal<TQueryResult>(CreateDispatchQueryDelegate, query, cancellationToken);
public ValueTask<TQueryResult> Dispatch<TQueryResult>(IKnownQuery<TQueryResult> query, CancellationToken cancellationToken) public ValueTask<TQueryResult> Dispatch<TQueryResult>(IKnownQuery<TQueryResult> query, CancellationToken cancellationToken)
=> DispatchQuery<TQueryResult>(query, cancellationToken); => DispatchInternal<TQueryResult>(CreateDispatchQueryDelegate, query, cancellationToken);
private ValueTask<TQueryResult> DispatchQuery<TQueryResult>(object query, CancellationToken cancellationToken)
{
var queryType = query.GetType();
var dispatchQueryMethod = methodsCache.GetOrAdd(queryType, static t => typeof(QueryDispatcherImpl)
.GetMethod(nameof(DispatchQueryImpl), BindingFlags.Instance | BindingFlags.NonPublic)!
.MakeGenericMethod(t, typeof(TQueryResult)));
return (ValueTask<TQueryResult>)dispatchQueryMethod
.Invoke(this, [query, cancellationToken])!;
}
private ValueTask<TQueryResult> DispatchQueryImpl<TQuery, TQueryResult>( private ValueTask<TQueryResult> DispatchQueryImpl<TQuery, TQueryResult>(
TQuery query, TQuery query,
@@ -34,14 +32,8 @@ internal sealed class QueryDispatcherImpl(
where TQuery : notnull where TQuery : notnull
{ {
var handler = services.GetRequiredService<IQueryHandler<TQuery, TQueryResult>>(); var handler = services.GetRequiredService<IQueryHandler<TQuery, TQueryResult>>();
var pipeline = services.GetServices<IDispatchBehaviour<TQuery, TQueryResult>>(); var pipeline = services.GetServices<IDispatchBehavior<TQuery, TQueryResult>>();
using var pipelineEnumerator = pipeline.GetEnumerator();
return DispatchDelegateFactory(pipelineEnumerator).Invoke(); return DispatchDelegateFactory(query, handler, pipeline, cancellationToken).Invoke();
DispatchFurtherDelegate<TQueryResult> DispatchDelegateFactory(IEnumerator<IDispatchBehaviour<TQuery, TQueryResult>> enumerator) =>
enumerator.MoveNext()
? (() => enumerator.Current.Handle(query, DispatchDelegateFactory(enumerator), cancellationToken))
: (() => handler.Handle(query, cancellationToken));
} }
} }

View File

@@ -6,6 +6,10 @@ namespace Cqrs.Tests.CommandDispatcherImplTests;
public class Dispatch public class Dispatch
{ {
public abstract class TestCommandHandler : ICommandHandler<TestCommand, TestCommandResult>
{
public abstract ValueTask<TestCommandResult> Handle(TestCommand command, CancellationToken cancellation);
}
public class TestCommand : IKnownCommand<TestCommandResult> {} public class TestCommand : IKnownCommand<TestCommandResult> {}
public class TestCommandResult {} public class TestCommandResult {}
@@ -19,7 +23,7 @@ public class Dispatch
var testCommand = new TestCommand(); var testCommand = new TestCommand();
var testCommandResult = new TestCommandResult(); var testCommandResult = new TestCommandResult();
var commandHandler = Substitute.For<ICommandHandler<TestCommand, TestCommandResult>>(); var commandHandler = Substitute.For<TestCommandHandler>();
commandHandler.Handle(testCommand, CancellationToken.None).Returns(testCommandResult); commandHandler.Handle(testCommand, CancellationToken.None).Returns(testCommandResult);
ServiceCollection serviceCollection = ServiceCollection serviceCollection =
@@ -42,12 +46,12 @@ public class Dispatch
await commandHandler.Received(1).Handle(testCommand, CancellationToken.None); await commandHandler.Received(1).Handle(testCommand, CancellationToken.None);
} }
public class TestOpenBehaviour<TRequest, TResponse> : IDispatchBehaviour<TRequest, TResponse> public class TestOpenBehavior<TRequest, TResponse> : IDispatchBehavior<TRequest, TResponse>
where TRequest : notnull where TRequest : notnull
{ {
private readonly Action<TRequest> _callback; private readonly Action<TRequest> _callback;
public TestOpenBehaviour(Action<TRequest> callback) public TestOpenBehavior(Action<TRequest> callback)
{ {
_callback = callback; _callback = callback;
} }
@@ -60,27 +64,27 @@ public class Dispatch
} }
[Fact] [Fact]
public async Task WhenPipelineConfigured_ShouldCallAllBehavioursInOrder() public async Task WhenPipelineConfigured_ShouldCallAllBehaviorsInOrder()
{ {
// Given // Given
var testCommand = new TestCommand(); var testCommand = new TestCommand();
var testCommandResult = new TestCommandResult(); var testCommandResult = new TestCommandResult();
List<string> calls = []; List<string> calls = [];
var commandHandler = Substitute.For<ICommandHandler<TestCommand, TestCommandResult>>(); var commandHandler = Substitute.For<TestCommandHandler>();
commandHandler.Handle(testCommand, CancellationToken.None) commandHandler.Handle(testCommand, CancellationToken.None)
.Returns(testCommandResult) .Returns(testCommandResult)
.AndDoes(_ => calls.Add("commandHandler")); .AndDoes(_ => calls.Add("commandHandler"));
var firstBehaviour = Substitute.For<IDispatchBehaviour<TestCommand, TestCommandResult>>(); var firstBehavior = Substitute.For<IDispatchBehavior<TestCommand, TestCommandResult>>();
firstBehaviour.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>()) firstBehavior.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>())
.Returns(args => ((DispatchFurtherDelegate<TestCommandResult>)args[1]).Invoke()) .Returns(args => ((DispatchFurtherDelegate<TestCommandResult>)args[1]).Invoke())
.AndDoes(_ => calls.Add("firstBehaviour")); .AndDoes(_ => calls.Add("firstBehavior"));
var secondBehaviour = Substitute.For<IDispatchBehaviour<TestCommand, TestCommandResult>>(); var secondBehavior = Substitute.For<IDispatchBehavior<TestCommand, TestCommandResult>>();
secondBehaviour.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>()) secondBehavior.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>())
.Returns(args => ((DispatchFurtherDelegate<TestCommandResult>)args[1]).Invoke()) .Returns(args => ((DispatchFurtherDelegate<TestCommandResult>)args[1]).Invoke())
.AndDoes(_ => calls.Add("secondBehaviour")); .AndDoes(_ => calls.Add("secondBehavior"));
ServiceCollection serviceCollection = ServiceCollection serviceCollection =
[ [
@@ -90,22 +94,22 @@ public class Dispatch
ServiceLifetime.Transient ServiceLifetime.Transient
), ),
new ServiceDescriptor( new ServiceDescriptor(
typeof(IDispatchBehaviour<TestCommand, TestCommandResult>), typeof(IDispatchBehavior<TestCommand, TestCommandResult>),
(IServiceProvider _) => firstBehaviour, (IServiceProvider _) => firstBehavior,
ServiceLifetime.Transient ServiceLifetime.Transient
), ),
new ServiceDescriptor( new ServiceDescriptor(
typeof(IDispatchBehaviour<TestCommand, TestCommandResult>), typeof(IDispatchBehavior<TestCommand, TestCommandResult>),
(IServiceProvider _) => secondBehaviour, (IServiceProvider _) => secondBehavior,
ServiceLifetime.Transient ServiceLifetime.Transient
), ),
new ServiceDescriptor( new ServiceDescriptor(
typeof(IDispatchBehaviour<,>), typeof(IDispatchBehavior<,>),
typeof(TestOpenBehaviour<,>), typeof(TestOpenBehavior<,>),
ServiceLifetime.Transient ServiceLifetime.Transient
), ),
]; ];
serviceCollection.AddTransient<Action<TestCommand>>(_ => (TestCommand _) => calls.Add("thirdBehaviour")); serviceCollection.AddTransient<Action<TestCommand>>(_ => (TestCommand _) => calls.Add("thirdBehavior"));
var services = serviceCollection.BuildServiceProvider(); var services = serviceCollection.BuildServiceProvider();
var sut = new CommandDispatcherImpl(services, new ConcurrentMethodsCache()); var sut = new CommandDispatcherImpl(services, new ConcurrentMethodsCache());
@@ -115,11 +119,11 @@ public class Dispatch
// Then // Then
result.ShouldBeSameAs(testCommandResult); result.ShouldBeSameAs(testCommandResult);
await firstBehaviour.Received(1).Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>()); await firstBehavior.Received(1).Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>());
await secondBehaviour.Received(1).Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>()); await secondBehavior.Received(1).Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>());
await commandHandler.Received(1).Handle(testCommand, CancellationToken.None); await commandHandler.Received(1).Handle(testCommand, CancellationToken.None);
calls.ShouldBe(["firstBehaviour", "secondBehaviour", "thirdBehaviour", "commandHandler"]); calls.ShouldBe(["firstBehavior", "secondBehavior", "thirdBehavior", "commandHandler"]);
} }
[Fact] [Fact]
@@ -131,20 +135,20 @@ public class Dispatch
var testCommandResultAborted = new TestCommandResult(); var testCommandResultAborted = new TestCommandResult();
List<string> calls = []; List<string> calls = [];
var commandHandler = Substitute.For<ICommandHandler<TestCommand, TestCommandResult>>(); var commandHandler = Substitute.For<TestCommandHandler>();
commandHandler.Handle(testCommand, CancellationToken.None) commandHandler.Handle(testCommand, CancellationToken.None)
.Returns(testCommandResult) .Returns(testCommandResult)
.AndDoes(_ => calls.Add("commandHandler")); .AndDoes(_ => calls.Add("commandHandler"));
var firstBehaviour = Substitute.For<IDispatchBehaviour<TestCommand, TestCommandResult>>(); var firstBehavior = Substitute.For<IDispatchBehavior<TestCommand, TestCommandResult>>();
firstBehaviour.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>()) firstBehavior.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>())
.Returns(args => ((DispatchFurtherDelegate<TestCommandResult>)args[1]).Invoke()) .Returns(args => ((DispatchFurtherDelegate<TestCommandResult>)args[1]).Invoke())
.AndDoes(_ => calls.Add("firstBehaviour")); .AndDoes(_ => calls.Add("firstBehavior"));
var secondBehaviour = Substitute.For<IDispatchBehaviour<TestCommand, TestCommandResult>>(); var secondBehavior = Substitute.For<IDispatchBehavior<TestCommand, TestCommandResult>>();
secondBehaviour.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>()) secondBehavior.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>())
.Returns(args => ValueTask.FromResult(testCommandResultAborted)) .Returns(args => ValueTask.FromResult(testCommandResultAborted))
.AndDoes(_ => calls.Add("secondBehaviour")); .AndDoes(_ => calls.Add("secondBehavior"));
ServiceCollection serviceCollection = ServiceCollection serviceCollection =
[ [
@@ -154,22 +158,22 @@ public class Dispatch
ServiceLifetime.Transient ServiceLifetime.Transient
), ),
new ServiceDescriptor( new ServiceDescriptor(
typeof(IDispatchBehaviour<TestCommand, TestCommandResult>), typeof(IDispatchBehavior<TestCommand, TestCommandResult>),
(IServiceProvider _) => firstBehaviour, (IServiceProvider _) => firstBehavior,
ServiceLifetime.Transient ServiceLifetime.Transient
), ),
new ServiceDescriptor( new ServiceDescriptor(
typeof(IDispatchBehaviour<TestCommand, TestCommandResult>), typeof(IDispatchBehavior<TestCommand, TestCommandResult>),
(IServiceProvider _) => secondBehaviour, (IServiceProvider _) => secondBehavior,
ServiceLifetime.Transient ServiceLifetime.Transient
), ),
new ServiceDescriptor( new ServiceDescriptor(
typeof(IDispatchBehaviour<,>), typeof(IDispatchBehavior<,>),
typeof(TestOpenBehaviour<,>), typeof(TestOpenBehavior<,>),
ServiceLifetime.Transient ServiceLifetime.Transient
), ),
]; ];
serviceCollection.AddTransient<Action<TestCommand>>(_ => (TestCommand _) => calls.Add("thirdBehaviour")); serviceCollection.AddTransient<Action<TestCommand>>(_ => (TestCommand _) => calls.Add("thirdBehavior"));
var services = serviceCollection.BuildServiceProvider(); var services = serviceCollection.BuildServiceProvider();
var sut = new CommandDispatcherImpl(services, new ConcurrentMethodsCache()); var sut = new CommandDispatcherImpl(services, new ConcurrentMethodsCache());
@@ -179,10 +183,59 @@ public class Dispatch
// Then // Then
result.ShouldBeSameAs(testCommandResultAborted); result.ShouldBeSameAs(testCommandResultAborted);
await firstBehaviour.Received(1).Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>()); await firstBehavior.Received(1).Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>());
await secondBehaviour.Received(1).Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>()); await secondBehavior.Received(1).Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>());
await commandHandler.Received(0).Handle(testCommand, CancellationToken.None); await commandHandler.Received(0).Handle(testCommand, CancellationToken.None);
calls.ShouldBe(["firstBehaviour", "secondBehaviour"]); calls.ShouldBe(["firstBehavior", "secondBehavior"]);
}
public abstract class AnotherTestCommandHandler : ICommandHandler<TestCommand, AnotherTestCommandResult>
{
public abstract ValueTask<AnotherTestCommandResult> Handle(TestCommand command, CancellationToken cancellation);
}
public class AnotherTestCommandResult {}
[Fact]
public async Task WhenTwoHandlersWithDifferentResultTypesRegisteredForOneCommandType_ShouldCorrectlyDispatchToBoth() // Fix to Cache Key Collision
{
// Given
var testCommand = new TestCommand();
var testCommandResult = new TestCommandResult();
var anotherTestCommandResult = new AnotherTestCommandResult();
var commandHandler = Substitute.For<TestCommandHandler>();
commandHandler.Handle(testCommand, CancellationToken.None).Returns(testCommandResult);
var anotherCommandHandler = Substitute.For<AnotherTestCommandHandler>();
anotherCommandHandler.Handle(testCommand, CancellationToken.None).Returns(anotherTestCommandResult);
ServiceCollection serviceCollection =
[
new ServiceDescriptor(
typeof(ICommandHandler<TestCommand, TestCommandResult>),
(IServiceProvider _) => commandHandler,
ServiceLifetime.Transient
),
new ServiceDescriptor(
typeof(ICommandHandler<TestCommand, AnotherTestCommandResult>),
(IServiceProvider _) => anotherCommandHandler,
ServiceLifetime.Transient
),
];
var services = serviceCollection.BuildServiceProvider();
var sut = new CommandDispatcherImpl(services, new ConcurrentMethodsCache());
// When
var result = await sut.Dispatch(testCommand, CancellationToken.None);
var anotherResult = await sut.Dispatch<AnotherTestCommandResult>(testCommand, CancellationToken.None);
// Then
result.ShouldBeSameAs(testCommandResult);
await commandHandler.Received(1).Handle(testCommand, CancellationToken.None);
anotherResult.ShouldBeSameAs(anotherTestCommandResult);
await anotherCommandHandler.Received(1).Handle(testCommand, CancellationToken.None);
} }
} }

View File

@@ -3,12 +3,12 @@ using Microsoft.Extensions.DependencyInjection;
namespace Cqrs.Tests.CqrsServicesExtensionsTests; namespace Cqrs.Tests.CqrsServicesExtensionsTests;
public class AddBehaviour public class AddBehavior
{ {
public class TestCommand {} public class TestCommand {}
public class TestCommandResult {} public class TestCommandResult {}
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
public class NonGenericTestOpenBehaviour : IDispatchBehaviour<TestCommand, TestCommandResult> public class NonGenericTestOpenBehavior : IDispatchBehavior<TestCommand, TestCommandResult>
{ {
public ValueTask<TestCommandResult> Handle(TestCommand request, DispatchFurtherDelegate<TestCommandResult> next, CancellationToken cancellationToken) public ValueTask<TestCommandResult> Handle(TestCommand request, DispatchFurtherDelegate<TestCommandResult> next, CancellationToken cancellationToken)
{ {
@@ -20,27 +20,27 @@ public class AddBehaviour
[InlineData(ServiceLifetime.Transient)] [InlineData(ServiceLifetime.Transient)]
[InlineData(ServiceLifetime.Scoped)] [InlineData(ServiceLifetime.Scoped)]
[InlineData(ServiceLifetime.Singleton)] [InlineData(ServiceLifetime.Singleton)]
public void WhenCalled_ShouldRegisterDispatchBehaviour(ServiceLifetime lifetime) public void WhenCalled_ShouldRegisterDispatchBehavior(ServiceLifetime lifetime)
{ {
// Given // Given
ServiceCollection services = new(); ServiceCollection services = new();
// When // When
services.AddCqrs(opt => opt services.AddCqrs(opt => opt
.AddBehaviour<NonGenericTestOpenBehaviour>(lifetime)); .AddBehavior<NonGenericTestOpenBehavior>(lifetime));
// Then // Then
services.ShouldContain( services.ShouldContain(
elementPredicate: descriptor => elementPredicate: descriptor =>
descriptor.ServiceType == typeof(IDispatchBehaviour<TestCommand, TestCommandResult>) descriptor.ServiceType == typeof(IDispatchBehavior<TestCommand, TestCommandResult>)
&& descriptor.ImplementationType == typeof(NonGenericTestOpenBehaviour) && descriptor.ImplementationType == typeof(NonGenericTestOpenBehavior)
&& descriptor.Lifetime == lifetime, && descriptor.Lifetime == lifetime,
expectedCount: 1 expectedCount: 1
); );
} }
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
public class InvalidTestBehaviour : IDispatchBehaviour public class InvalidTestBehavior : IDispatchBehavior
{ {
public Type RequestType => throw new NotImplementedException(); public Type RequestType => throw new NotImplementedException();
@@ -57,7 +57,7 @@ public class AddBehaviour
// Then // Then
Should.Throw<InvalidOperationException>(() => services.AddCqrs(opt => opt Should.Throw<InvalidOperationException>(() => services.AddCqrs(opt => opt
.AddBehaviour<InvalidTestBehaviour>()) .AddBehavior<InvalidTestBehavior>())
); );
} }
} }

View File

@@ -3,10 +3,10 @@ using Microsoft.Extensions.DependencyInjection;
namespace Cqrs.Tests.CqrsServicesExtensionsTests; namespace Cqrs.Tests.CqrsServicesExtensionsTests;
public class AddOpenBehaviour public class AddOpenBehavior
{ {
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
public class TestOpenBehaviour<TRequest, TResponse> : IDispatchBehaviour<TRequest, TResponse> public class TestOpenBehavior<TRequest, TResponse> : IDispatchBehavior<TRequest, TResponse>
where TRequest: notnull where TRequest: notnull
{ {
public ValueTask<TResponse> Handle(TRequest request, DispatchFurtherDelegate<TResponse> next, CancellationToken cancellationToken) public ValueTask<TResponse> Handle(TRequest request, DispatchFurtherDelegate<TResponse> next, CancellationToken cancellationToken)
@@ -19,27 +19,27 @@ public class AddOpenBehaviour
[InlineData(ServiceLifetime.Transient)] [InlineData(ServiceLifetime.Transient)]
[InlineData(ServiceLifetime.Scoped)] [InlineData(ServiceLifetime.Scoped)]
[InlineData(ServiceLifetime.Singleton)] [InlineData(ServiceLifetime.Singleton)]
public void WhenCalled_ShouldRegisterOpenDispatchBehaviour(ServiceLifetime lifetime) public void WhenCalled_ShouldRegisterOpenDispatchBehavior(ServiceLifetime lifetime)
{ {
// Given // Given
ServiceCollection services = new(); ServiceCollection services = new();
// When // When
services.AddCqrs(opt => opt services.AddCqrs(opt => opt
.AddOpenBehaviour(typeof(TestOpenBehaviour<,>), lifetime)); .AddOpenBehavior(typeof(TestOpenBehavior<,>), lifetime));
// Then // Then
services.ShouldContain( services.ShouldContain(
elementPredicate: descriptor => elementPredicate: descriptor =>
descriptor.ServiceType == typeof(IDispatchBehaviour<,>) descriptor.ServiceType == typeof(IDispatchBehavior<,>)
&& descriptor.ImplementationType == typeof(TestOpenBehaviour<,>) && descriptor.ImplementationType == typeof(TestOpenBehavior<,>)
&& descriptor.Lifetime == lifetime, && descriptor.Lifetime == lifetime,
expectedCount: 1 expectedCount: 1
); );
} }
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
public class InvalidOpenBehaviour : IDispatchBehaviour public class InvalidOpenBehavior : IDispatchBehavior
{ {
public Type RequestType => throw new NotImplementedException(); public Type RequestType => throw new NotImplementedException();
@@ -53,18 +53,18 @@ public class AddOpenBehaviour
ServiceCollection services = new(); ServiceCollection services = new();
// When // When
var invalidOpenDispatchBehaviourType = typeof(InvalidOpenBehaviour); var invalidOpenDispatchBehaviorType = typeof(InvalidOpenBehavior);
// Then // Then
Should.Throw<ArgumentException>(() => services.AddCqrs(opt => opt Should.Throw<ArgumentException>(() => services.AddCqrs(opt => opt
.AddOpenBehaviour(invalidOpenDispatchBehaviourType)) .AddOpenBehavior(invalidOpenDispatchBehaviorType))
); );
} }
public class TestCommand {} public class TestCommand {}
public class TestCommandResult {} public class TestCommandResult {}
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
public class NonGenericTestOpenBehaviour : IDispatchBehaviour<TestCommand, TestCommandResult> public class NonGenericTestOpenBehavior : IDispatchBehavior<TestCommand, TestCommandResult>
{ {
public ValueTask<TestCommandResult> Handle(TestCommand request, DispatchFurtherDelegate<TestCommandResult> next, CancellationToken cancellationToken) public ValueTask<TestCommandResult> Handle(TestCommand request, DispatchFurtherDelegate<TestCommandResult> next, CancellationToken cancellationToken)
{ {
@@ -79,11 +79,11 @@ public class AddOpenBehaviour
ServiceCollection services = new(); ServiceCollection services = new();
// When // When
var nonGenericOpenDispatchBehaviourType = typeof(NonGenericTestOpenBehaviour); var nonGenericOpenDispatchBehaviorType = typeof(NonGenericTestOpenBehavior);
// Then // Then
Should.Throw<ArgumentException>(() => services.AddCqrs(opt => opt Should.Throw<ArgumentException>(() => services.AddCqrs(opt => opt
.AddOpenBehaviour(nonGenericOpenDispatchBehaviourType)) .AddOpenBehavior(nonGenericOpenDispatchBehaviorType))
); );
} }
} }

View File

@@ -6,6 +6,10 @@ namespace Cqrs.Tests.QueryDispatcherImplTests;
public class Dispatch public class Dispatch
{ {
public abstract class TestQueryHandler : IQueryHandler<TestQuery, TestQueryResult>
{
public abstract ValueTask<TestQueryResult> Handle(TestQuery query, CancellationToken cancellation);
}
public class TestQuery : IKnownQuery<TestQueryResult> {} public class TestQuery : IKnownQuery<TestQueryResult> {}
public class TestQueryResult {} public class TestQueryResult {}
@@ -19,7 +23,7 @@ public class Dispatch
var testQuery = new TestQuery(); var testQuery = new TestQuery();
var testQueryResult = new TestQueryResult(); var testQueryResult = new TestQueryResult();
var queryHandler = Substitute.For<IQueryHandler<TestQuery, TestQueryResult>>(); var queryHandler = Substitute.For<TestQueryHandler>();
queryHandler.Handle(testQuery, CancellationToken.None).Returns(testQueryResult); queryHandler.Handle(testQuery, CancellationToken.None).Returns(testQueryResult);
ServiceCollection serviceCollection = ServiceCollection serviceCollection =
@@ -42,12 +46,12 @@ public class Dispatch
await queryHandler.Received(1).Handle(testQuery, CancellationToken.None); await queryHandler.Received(1).Handle(testQuery, CancellationToken.None);
} }
public class TestOpenBehaviour<TRequest, TResponse> : IDispatchBehaviour<TRequest, TResponse> public class TestOpenBehavior<TRequest, TResponse> : IDispatchBehavior<TRequest, TResponse>
where TRequest : notnull where TRequest : notnull
{ {
private readonly Action<TRequest> _callback; private readonly Action<TRequest> _callback;
public TestOpenBehaviour(Action<TRequest> callback) public TestOpenBehavior(Action<TRequest> callback)
{ {
_callback = callback; _callback = callback;
} }
@@ -60,27 +64,27 @@ public class Dispatch
} }
[Fact] [Fact]
public async Task WhenPipelineConfigured_ShouldCallAllBehavioursInOrder() public async Task WhenPipelineConfigured_ShouldCallAllBehaviorsInOrder()
{ {
// Given // Given
var testQuery = new TestQuery(); var testQuery = new TestQuery();
var testQueryResult = new TestQueryResult(); var testQueryResult = new TestQueryResult();
List<string> calls = []; List<string> calls = [];
var queryHandler = Substitute.For<IQueryHandler<TestQuery, TestQueryResult>>(); var queryHandler = Substitute.For<TestQueryHandler>();
queryHandler.Handle(testQuery, CancellationToken.None) queryHandler.Handle(testQuery, CancellationToken.None)
.Returns(testQueryResult) .Returns(testQueryResult)
.AndDoes(_ => calls.Add("queryHandler")); .AndDoes(_ => calls.Add("queryHandler"));
var firstBehaviour = Substitute.For<IDispatchBehaviour<TestQuery, TestQueryResult>>(); var firstBehavior = Substitute.For<IDispatchBehavior<TestQuery, TestQueryResult>>();
firstBehaviour.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>()) firstBehavior.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>())
.Returns(args => ((DispatchFurtherDelegate<TestQueryResult>)args[1]).Invoke()) .Returns(args => ((DispatchFurtherDelegate<TestQueryResult>)args[1]).Invoke())
.AndDoes(_ => calls.Add("firstBehaviour")); .AndDoes(_ => calls.Add("firstBehavior"));
var secondBehaviour = Substitute.For<IDispatchBehaviour<TestQuery, TestQueryResult>>(); var secondBehavior = Substitute.For<IDispatchBehavior<TestQuery, TestQueryResult>>();
secondBehaviour.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>()) secondBehavior.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>())
.Returns(args => ((DispatchFurtherDelegate<TestQueryResult>)args[1]).Invoke()) .Returns(args => ((DispatchFurtherDelegate<TestQueryResult>)args[1]).Invoke())
.AndDoes(_ => calls.Add("secondBehaviour")); .AndDoes(_ => calls.Add("secondBehavior"));
ServiceCollection serviceCollection = ServiceCollection serviceCollection =
[ [
@@ -90,22 +94,22 @@ public class Dispatch
ServiceLifetime.Transient ServiceLifetime.Transient
), ),
new ServiceDescriptor( new ServiceDescriptor(
typeof(IDispatchBehaviour<TestQuery, TestQueryResult>), typeof(IDispatchBehavior<TestQuery, TestQueryResult>),
(IServiceProvider _) => firstBehaviour, (IServiceProvider _) => firstBehavior,
ServiceLifetime.Transient ServiceLifetime.Transient
), ),
new ServiceDescriptor( new ServiceDescriptor(
typeof(IDispatchBehaviour<TestQuery, TestQueryResult>), typeof(IDispatchBehavior<TestQuery, TestQueryResult>),
(IServiceProvider _) => secondBehaviour, (IServiceProvider _) => secondBehavior,
ServiceLifetime.Transient ServiceLifetime.Transient
), ),
new ServiceDescriptor( new ServiceDescriptor(
typeof(IDispatchBehaviour<,>), typeof(IDispatchBehavior<,>),
typeof(TestOpenBehaviour<,>), typeof(TestOpenBehavior<,>),
ServiceLifetime.Transient ServiceLifetime.Transient
), ),
]; ];
serviceCollection.AddTransient<Action<TestQuery>>(_ => (TestQuery _) => calls.Add("thirdBehaviour")); serviceCollection.AddTransient<Action<TestQuery>>(_ => (TestQuery _) => calls.Add("thirdBehavior"));
var services = serviceCollection.BuildServiceProvider(); var services = serviceCollection.BuildServiceProvider();
var sut = new QueryDispatcherImpl(services, new ConcurrentMethodsCache()); var sut = new QueryDispatcherImpl(services, new ConcurrentMethodsCache());
@@ -115,11 +119,11 @@ public class Dispatch
// Then // Then
result.ShouldBeSameAs(testQueryResult); result.ShouldBeSameAs(testQueryResult);
await firstBehaviour.Received(1).Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>()); await firstBehavior.Received(1).Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>());
await secondBehaviour.Received(1).Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>()); await secondBehavior.Received(1).Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>());
await queryHandler.Received(1).Handle(testQuery, CancellationToken.None); await queryHandler.Received(1).Handle(testQuery, CancellationToken.None);
calls.ShouldBe(["firstBehaviour", "secondBehaviour", "thirdBehaviour", "queryHandler"]); calls.ShouldBe(["firstBehavior", "secondBehavior", "thirdBehavior", "queryHandler"]);
} }
[Fact] [Fact]
@@ -131,20 +135,20 @@ public class Dispatch
var testQueryResultAborted = new TestQueryResult(); var testQueryResultAborted = new TestQueryResult();
List<string> calls = []; List<string> calls = [];
var queryHandler = Substitute.For<IQueryHandler<TestQuery, TestQueryResult>>(); var queryHandler = Substitute.For<TestQueryHandler>();
queryHandler.Handle(testQuery, CancellationToken.None) queryHandler.Handle(testQuery, CancellationToken.None)
.Returns(testQueryResult) .Returns(testQueryResult)
.AndDoes(_ => calls.Add("queryHandler")); .AndDoes(_ => calls.Add("queryHandler"));
var firstBehaviour = Substitute.For<IDispatchBehaviour<TestQuery, TestQueryResult>>(); var firstBehavior = Substitute.For<IDispatchBehavior<TestQuery, TestQueryResult>>();
firstBehaviour.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>()) firstBehavior.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>())
.Returns(args => ((DispatchFurtherDelegate<TestQueryResult>)args[1]).Invoke()) .Returns(args => ((DispatchFurtherDelegate<TestQueryResult>)args[1]).Invoke())
.AndDoes(_ => calls.Add("firstBehaviour")); .AndDoes(_ => calls.Add("firstBehavior"));
var secondBehaviour = Substitute.For<IDispatchBehaviour<TestQuery, TestQueryResult>>(); var secondBehavior = Substitute.For<IDispatchBehavior<TestQuery, TestQueryResult>>();
secondBehaviour.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>()) secondBehavior.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>())
.Returns(args => ValueTask.FromResult(testQueryResultAborted)) .Returns(args => ValueTask.FromResult(testQueryResultAborted))
.AndDoes(_ => calls.Add("secondBehaviour")); .AndDoes(_ => calls.Add("secondBehavior"));
ServiceCollection serviceCollection = ServiceCollection serviceCollection =
[ [
@@ -154,22 +158,22 @@ public class Dispatch
ServiceLifetime.Transient ServiceLifetime.Transient
), ),
new ServiceDescriptor( new ServiceDescriptor(
typeof(IDispatchBehaviour<TestQuery, TestQueryResult>), typeof(IDispatchBehavior<TestQuery, TestQueryResult>),
(IServiceProvider _) => firstBehaviour, (IServiceProvider _) => firstBehavior,
ServiceLifetime.Transient ServiceLifetime.Transient
), ),
new ServiceDescriptor( new ServiceDescriptor(
typeof(IDispatchBehaviour<TestQuery, TestQueryResult>), typeof(IDispatchBehavior<TestQuery, TestQueryResult>),
(IServiceProvider _) => secondBehaviour, (IServiceProvider _) => secondBehavior,
ServiceLifetime.Transient ServiceLifetime.Transient
), ),
new ServiceDescriptor( new ServiceDescriptor(
typeof(IDispatchBehaviour<,>), typeof(IDispatchBehavior<,>),
typeof(TestOpenBehaviour<,>), typeof(TestOpenBehavior<,>),
ServiceLifetime.Transient ServiceLifetime.Transient
), ),
]; ];
serviceCollection.AddTransient<Action<TestQuery>>(_ => (TestQuery _) => calls.Add("thirdBehaviour")); serviceCollection.AddTransient<Action<TestQuery>>(_ => (TestQuery _) => calls.Add("thirdBehavior"));
var services = serviceCollection.BuildServiceProvider(); var services = serviceCollection.BuildServiceProvider();
var sut = new QueryDispatcherImpl(services, new ConcurrentMethodsCache()); var sut = new QueryDispatcherImpl(services, new ConcurrentMethodsCache());
@@ -179,10 +183,59 @@ public class Dispatch
// Then // Then
result.ShouldBeSameAs(testQueryResultAborted); result.ShouldBeSameAs(testQueryResultAborted);
await firstBehaviour.Received(1).Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>()); await firstBehavior.Received(1).Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>());
await secondBehaviour.Received(1).Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>()); await secondBehavior.Received(1).Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>());
await queryHandler.Received(0).Handle(testQuery, CancellationToken.None); await queryHandler.Received(0).Handle(testQuery, CancellationToken.None);
calls.ShouldBe(["firstBehaviour", "secondBehaviour"]); calls.ShouldBe(["firstBehavior", "secondBehavior"]);
}
public abstract class AnotherTestQueryHandler : IQueryHandler<TestQuery, AnotherTestQueryResult>
{
public abstract ValueTask<AnotherTestQueryResult> Handle(TestQuery query, CancellationToken cancellation);
}
public class AnotherTestQueryResult {}
[Fact]
public async Task WhenTwoHandlersWithDifferentResultTypesRegisteredForOneQueryType_ShouldCorrectlyDispatchToBoth() // Fix to Cache Key Collision
{
// Given
var testQuery = new TestQuery();
var testQueryResult = new TestQueryResult();
var anotherTestQueryResult = new AnotherTestQueryResult();
var queryHandler = Substitute.For<TestQueryHandler>();
queryHandler.Handle(testQuery, CancellationToken.None).Returns(testQueryResult);
var anotherQueryHandler = Substitute.For<AnotherTestQueryHandler>();
anotherQueryHandler.Handle(testQuery, CancellationToken.None).Returns(anotherTestQueryResult);
ServiceCollection serviceCollection =
[
new ServiceDescriptor(
typeof(IQueryHandler<TestQuery, TestQueryResult>),
(IServiceProvider _) => queryHandler,
ServiceLifetime.Transient
),
new ServiceDescriptor(
typeof(IQueryHandler<TestQuery, AnotherTestQueryResult>),
(IServiceProvider _) => anotherQueryHandler,
ServiceLifetime.Transient
),
];
var services = serviceCollection.BuildServiceProvider();
var sut = new QueryDispatcherImpl(services, new ConcurrentMethodsCache());
// When
var result = await sut.Dispatch(testQuery, CancellationToken.None);
var anotherResult = await sut.Dispatch<AnotherTestQueryResult>(testQuery, CancellationToken.None);
// Then
result.ShouldBeSameAs(testQueryResult);
await queryHandler.Received(1).Handle(testQuery, CancellationToken.None);
anotherResult.ShouldBeSameAs(anotherTestQueryResult);
await anotherQueryHandler.Received(1).Handle(testQuery, CancellationToken.None);
} }
} }