solution files
All checks were successful
.NET Test / test (push) Successful in 6m47s

This commit is contained in:
2025-02-01 20:34:34 +04:00
parent 14937c6964
commit 54ea0925dd
30 changed files with 1388 additions and 0 deletions

13
.config/dotnet-tools.json Normal file
View File

@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-reportgenerator-globaltool": {
"version": "5.4.3",
"commands": [
"reportgenerator"
],
"rollForward": false
}
}
}

View File

@@ -0,0 +1,54 @@
name: .NET Test
on:
push:
branches: [ main ]
tags-ignore:
- '**'
paths-ignore:
- '.vscode'
- 'README.md'
- 'LICENSE'
- '.gitea/workflows/publish-*.yaml'
pull_request:
branches: [ main ]
paths-ignore:
- '.vscode'
- 'README.md'
- 'LICENSE'
- '.gitea/workflows/publish-*.yaml'
jobs:
test:
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: Build
run: dotnet build --nologo --configuration Release --no-restore
- name: Test
run: dotnet test --nologo --configuration Release --no-build --verbosity normal --logger trx --collect:"XPlat Code Coverage"
- name: Restore local tools
run: dotnet tool restore
- name: Generate coverage report
run: dotnet reportgenerator -reports:**/coverage.cobertura.xml -targetdir:./coverage -reporttypes:MarkdownSummary
- name: Upload dotnet test results
#uses: actions/upload-artifact@v4
uses: christopherhx/gitea-upload-artifact@v4
with:
name: coverage-results
path: coverage
if: ${{ always() }}

4
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"dotnet.defaultSolution": "Just.Cqrs.sln",
"dotnetAcquisitionExtension.enableTelemetry": false
}

17
Directory.Build.props Normal file
View File

@@ -0,0 +1,17 @@
<Project>
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Description>Lightweight, easy-to-use C# library designed to simplify the implementation of the Command Query Responsibility Segregation (CQRS) pattern.</Description>
<Authors>JustFixMe</Authors>
<Copyright>Copyright (c) 2025 JustFixMe</Copyright>
<RepositoryUrl>https://github.com/JustFixMe/Just.Core/</RepositoryUrl>
<PackageTags>c#;CQRS</PackageTags>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageReadmeFile>readme.md</PackageReadmeFile>
</PropertyGroup>
</Project>

43
Just.Cqrs.sln Normal file
View File

@@ -0,0 +1,43 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cqrs.Tests", "tests\Cqrs.Tests\Cqrs.Tests.csproj", "{D0C26007-4570-4B6D-9DE2-4DDF8ECE9290}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Just.Cqrs.Abstractions", "src\Just.Cqrs.Abstractions\Just.Cqrs.Abstractions.csproj", "{7CE7979E-9B98-4532-A172-6FC2BE8897F5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Just.Cqrs", "src\Just.Cqrs\Just.Cqrs.csproj", "{C474A3F6-8BF3-48A3-A542-C50024AD292E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D0C26007-4570-4B6D-9DE2-4DDF8ECE9290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D0C26007-4570-4B6D-9DE2-4DDF8ECE9290}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D0C26007-4570-4B6D-9DE2-4DDF8ECE9290}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D0C26007-4570-4B6D-9DE2-4DDF8ECE9290}.Release|Any CPU.Build.0 = Release|Any CPU
{7CE7979E-9B98-4532-A172-6FC2BE8897F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7CE7979E-9B98-4532-A172-6FC2BE8897F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7CE7979E-9B98-4532-A172-6FC2BE8897F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7CE7979E-9B98-4532-A172-6FC2BE8897F5}.Release|Any CPU.Build.0 = Release|Any CPU
{C474A3F6-8BF3-48A3-A542-C50024AD292E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C474A3F6-8BF3-48A3-A542-C50024AD292E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C474A3F6-8BF3-48A3-A542-C50024AD292E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C474A3F6-8BF3-48A3-A542-C50024AD292E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{D0C26007-4570-4B6D-9DE2-4DDF8ECE9290} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{7CE7979E-9B98-4532-A172-6FC2BE8897F5} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{C474A3F6-8BF3-48A3-A542-C50024AD292E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,11 @@
namespace Just.Cqrs;
public interface ICommandDispatcher
{
ValueTask<TCommandResult> Dispatch<TCommandResult>(
object command,
CancellationToken cancellationToken);
ValueTask<TCommandResult> Dispatch<TCommandResult>(
IKnownCommand<TCommandResult> command,
CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,9 @@
using Just.Cqrs.Internal;
namespace Just.Cqrs;
public interface ICommandHandler<TCommand, TCommandResult> : ICommandHandlerImpl
where TCommand : notnull
{
ValueTask<TCommandResult> Handle(TCommand command, CancellationToken cancellation);
}

View File

@@ -0,0 +1,38 @@
using System.Diagnostics.CodeAnalysis;
namespace Just.Cqrs;
/// <summary>
/// Delegate representing the rest of the pipeline
/// </summary>
/// <typeparam name="TResponse">Result type of dispatching command/query</typeparam>
/// <returns>Result of executing the rest of the pipeline</returns>
public delegate ValueTask<TResponse> DispatchFurtherDelegate<TResponse>();
/// <summary>
/// Marker interface for static type checking. Should not be used directly.
/// </summary>
public interface IDispatchBehaviour
{
Type RequestType { get; }
Type ResponseType { get; }
}
/// <summary>
/// Middleware analog for dispatching commands/queries
/// </summary>
/// <typeparam name="TRequest">Request type</typeparam>
/// <typeparam name="TResponse">Result type of dispatching command/query</typeparam>
public interface IDispatchBehaviour<in TRequest, TResponse> : IDispatchBehaviour
where TRequest : notnull
{
ValueTask<TResponse> Handle(
TRequest request,
DispatchFurtherDelegate<TResponse> next,
CancellationToken cancellationToken);
[ExcludeFromCodeCoverage]
Type IDispatchBehaviour.RequestType => typeof(TRequest);
[ExcludeFromCodeCoverage]
Type IDispatchBehaviour.ResponseType => typeof(TResponse);
}

View File

@@ -0,0 +1,7 @@
namespace Just.Cqrs;
/// <summary>
/// Marker interface for Command type
/// </summary>
/// <typeparam name="TResult">Result of dispatching this command</typeparam>
public interface IKnownCommand<TResult>{}

View File

@@ -0,0 +1,7 @@
namespace Just.Cqrs;
/// <summary>
/// Marker interface for Query type
/// </summary>
/// <typeparam name="TResult">Result of dispatching this query</typeparam>
public interface IKnownQuery<TResult>{}

View File

@@ -0,0 +1,12 @@
namespace Just.Cqrs;
public interface IQueryDispatcher
{
ValueTask<TQueryResult> Dispatch<TQueryResult>(
object query,
CancellationToken cancellationToken);
ValueTask<TQueryResult> Dispatch<TQueryResult>(
IKnownQuery<TQueryResult> query,
CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,9 @@
using Just.Cqrs.Internal;
namespace Just.Cqrs;
public interface IQueryHandler<TQuery, TQueryResult> : IQueryHandlerImpl
where TQuery : notnull
{
ValueTask<TQueryResult> Handle(TQuery query, CancellationToken cancellation);
}

View File

@@ -0,0 +1,3 @@
namespace Just.Cqrs.Internal;
public interface ICommandHandlerImpl { }

View File

@@ -0,0 +1,3 @@
namespace Just.Cqrs.Internal;
public interface IQueryHandlerImpl { }

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>latest</LangVersion>
<RootNamespace>Just.Cqrs</RootNamespace>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,121 @@
using Just.Cqrs;
using Just.Cqrs.Internal;
using Microsoft.Extensions.DependencyInjection.Extensions;
#pragma warning disable IDE0130 // Namespace does not match folder structure
namespace Microsoft.Extensions.DependencyInjection;
#pragma warning restore IDE0130
public static class CqrsServicesExtensions
{
public static IServiceCollection AddCqrs(this IServiceCollection services, Action<CqrsServicesOptions>? configure = null)
{
var options = new CqrsServicesOptions(services);
configure?.Invoke(options);
services.TryAddKeyedSingleton<IMethodsCache, ConcurrentMethodsCache>(MethodsCacheServiceKey.DispatchCommand);
services.TryAddTransient<ICommandDispatcher, CommandDispatcherImpl>();
services.TryAddKeyedSingleton<IMethodsCache, ConcurrentMethodsCache>(MethodsCacheServiceKey.DispatchQuery);
services.TryAddTransient<IQueryDispatcher, QueryDispatcherImpl>();
foreach (var (service, impl, lifetime) in options.CommandHandlers)
{
services.TryAdd(new ServiceDescriptor(service, impl, lifetime));
}
foreach (var (service, impl, lifetime) in options.QueryHandlers)
{
services.TryAdd(new ServiceDescriptor(service, impl, lifetime));
}
foreach (var (service, impl, lifetime) in options.Behaviours)
{
services.Add(new ServiceDescriptor(service, impl, lifetime));
}
return services;
}
public static CqrsServicesOptions AddCommandHandler<TCommandHandler>(
this CqrsServicesOptions options,
ServiceLifetime lifetime = ServiceLifetime.Transient)
where TCommandHandler : notnull, ICommandHandlerImpl
{
var type = typeof(TCommandHandler);
var handlerInterfaces = type.FindInterfaces(
static (x, t) => x.IsGenericType && x.GetGenericTypeDefinition() == (Type)t!,
typeof(ICommandHandler<,>));
foreach (var interfaceType in handlerInterfaces)
{
options.CommandHandlers.Add((
interfaceType,
type,
lifetime));
}
return options;
}
public static CqrsServicesOptions AddQueryHandler<TQueryHandler>(
this CqrsServicesOptions options,
ServiceLifetime lifetime = ServiceLifetime.Transient)
where TQueryHandler : notnull, IQueryHandlerImpl
{
var type = typeof(TQueryHandler);
var handlerInterfaces = type.FindInterfaces(
static (x, t) => x.IsGenericType && x.GetGenericTypeDefinition() == (Type)t!,
typeof(IQueryHandler<,>));
foreach (var interfaceType in handlerInterfaces)
{
options.QueryHandlers.Add((
interfaceType,
type,
lifetime));
}
return options;
}
public static CqrsServicesOptions AddOpenBehaviour(this CqrsServicesOptions options, Type behaviour, ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
var interfaces = behaviour.FindInterfaces(
static (x, t) => x.IsGenericType && x.GetGenericTypeDefinition() == (Type)t!,
typeof(IDispatchBehaviour<,>));
if (interfaces.Length == 0)
{
throw new ArgumentException("Supplied type does not implement IDispatchBehaviour<,> interface.", nameof(behaviour));
}
if (!behaviour.ContainsGenericParameters)
{
throw new ArgumentException("Supplied type is not sutable for open behaviour.", nameof(behaviour));
}
options.Behaviours.Add((typeof(IDispatchBehaviour<,>), behaviour, lifetime));
return options;
}
public static CqrsServicesOptions AddBehaviour<TBehaviour>(this CqrsServicesOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton)
where TBehaviour : notnull, IDispatchBehaviour
{
var type = typeof(TBehaviour);
var interfaces = type.FindInterfaces(
static (x, t) => x.IsGenericType && x.GetGenericTypeDefinition() == (Type)t!,
typeof(IDispatchBehaviour<,>));
if (interfaces.Length == 0)
{
throw new InvalidOperationException("Supplied type does not implement IDispatchBehaviour<,> interface.");
}
foreach (var interfaceType in interfaces)
{
options.Behaviours.Add((
interfaceType,
type,
lifetime));
}
return options;
}
}

View File

@@ -0,0 +1,12 @@
using Microsoft.Extensions.DependencyInjection;
namespace Just.Cqrs;
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)> CommandHandlers = [];
internal readonly List<(Type Service, Type Impl, ServiceLifetime Lifetime)> QueryHandlers = [];
public IServiceCollection Services { get; } = services;
}

View File

@@ -0,0 +1,47 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
namespace Just.Cqrs.Internal;
internal sealed class CommandDispatcherImpl(
IServiceProvider services,
[FromKeyedServices(MethodsCacheServiceKey.DispatchCommand)]IMethodsCache methodsCache
) : ICommandDispatcher
{
[ExcludeFromCodeCoverage]
public ValueTask<TCommandResult> Dispatch<TCommandResult>(object command, CancellationToken cancellationToken)
=> DispatchCommand<TCommandResult>(command, cancellationToken);
public ValueTask<TCommandResult> Dispatch<TCommandResult>(IKnownCommand<TCommandResult> command, CancellationToken cancellationToken)
=> DispatchCommand<TCommandResult>(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>(
TCommand command,
CancellationToken cancellationToken)
where TCommand : notnull
{
var handler = services.GetRequiredService<ICommandHandler<TCommand, TCommandResult>>();
var pipeline = services.GetServices<IDispatchBehaviour<TCommand, TCommandResult>>();
using var pipelineEnumerator = pipeline.GetEnumerator();
return DispatchDelegateFactory(pipelineEnumerator).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,17 @@
using System.Collections.Concurrent;
using System.Reflection;
namespace Just.Cqrs.Internal;
internal interface IMethodsCache
{
MethodInfo GetOrAdd(Type key, Func<Type, MethodInfo> valueFactory);
}
internal static class MethodsCacheServiceKey
{
internal const string DispatchQuery = "q";
internal const string DispatchCommand = "c";
}
internal sealed class ConcurrentMethodsCache : ConcurrentDictionary<Type, MethodInfo>, IMethodsCache;

View File

@@ -0,0 +1,47 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
namespace Just.Cqrs.Internal;
internal sealed class QueryDispatcherImpl(
IServiceProvider services,
[FromKeyedServices(MethodsCacheServiceKey.DispatchQuery)]IMethodsCache methodsCache
) : IQueryDispatcher
{
[ExcludeFromCodeCoverage]
public ValueTask<TQueryResult> Dispatch<TQueryResult>(object query, CancellationToken cancellationToken)
=> DispatchQuery<TQueryResult>(query, cancellationToken);
public ValueTask<TQueryResult> Dispatch<TQueryResult>(IKnownQuery<TQueryResult> query, CancellationToken cancellationToken)
=> DispatchQuery<TQueryResult>(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>(
TQuery query,
CancellationToken cancellationToken)
where TQuery : notnull
{
var handler = services.GetRequiredService<IQueryHandler<TQuery, TQueryResult>>();
var pipeline = services.GetServices<IDispatchBehaviour<TQuery, TQueryResult>>();
using var pipelineEnumerator = pipeline.GetEnumerator();
return DispatchDelegateFactory(pipelineEnumerator).Invoke();
DispatchFurtherDelegate<TQueryResult> DispatchDelegateFactory(IEnumerator<IDispatchBehaviour<TQuery, TQueryResult>> enumerator) =>
enumerator.MoveNext()
? (() => enumerator.Current.Handle(query, DispatchDelegateFactory(enumerator), cancellationToken))
: (() => handler.Handle(query, cancellationToken));
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.1;net8.0;net9.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="Cqrs.Tests" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Just.Cqrs.Abstractions/Just.Cqrs.Abstractions.csproj" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework) == 'net9.0' Or $(TargetFramework) == 'netstandard2.1'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework) == 'net8.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,188 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NSubstitute;
namespace Cqrs.Tests.CommandDispatcherImplTests;
public class Dispatch
{
public class TestCommand : IKnownCommand<TestCommandResult> {}
public class TestCommandResult {}
[Theory]
[InlineData(ServiceLifetime.Transient)]
[InlineData(ServiceLifetime.Scoped)]
[InlineData(ServiceLifetime.Singleton)]
public async Task WhenCalled_ShouldExecuteHandler(ServiceLifetime lifetime)
{
// Given
var testCommand = new TestCommand();
var testCommandResult = new TestCommandResult();
var commandHandler = Substitute.For<ICommandHandler<TestCommand, TestCommandResult>>();
commandHandler.Handle(testCommand, CancellationToken.None).Returns(testCommandResult);
ServiceCollection serviceCollection =
[
new ServiceDescriptor(
typeof(ICommandHandler<TestCommand, TestCommandResult>),
(IServiceProvider _) => commandHandler,
lifetime
),
];
var services = serviceCollection.BuildServiceProvider();
var sut = new CommandDispatcherImpl(services, new ConcurrentMethodsCache());
// When
var result = await sut.Dispatch(testCommand, CancellationToken.None);
// Then
result.ShouldBeSameAs(testCommandResult);
await commandHandler.Received(1).Handle(testCommand, CancellationToken.None);
}
public class TestOpenBehaviour<TRequest, TResponse> : IDispatchBehaviour<TRequest, TResponse>
where TRequest : notnull
{
private readonly Action<TRequest> _callback;
public TestOpenBehaviour(Action<TRequest> callback)
{
_callback = callback;
}
public ValueTask<TResponse> Handle(TRequest request, DispatchFurtherDelegate<TResponse> next, CancellationToken cancellationToken)
{
_callback.Invoke(request);
return next();
}
}
[Fact]
public async Task WhenPipelineConfigured_ShouldCallAllBehavioursInOrder()
{
// Given
var testCommand = new TestCommand();
var testCommandResult = new TestCommandResult();
List<string> calls = [];
var commandHandler = Substitute.For<ICommandHandler<TestCommand, TestCommandResult>>();
commandHandler.Handle(testCommand, CancellationToken.None)
.Returns(testCommandResult)
.AndDoes(_ => calls.Add("commandHandler"));
var firstBehaviour = Substitute.For<IDispatchBehaviour<TestCommand, TestCommandResult>>();
firstBehaviour.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>())
.Returns(args => ((DispatchFurtherDelegate<TestCommandResult>)args[1]).Invoke())
.AndDoes(_ => calls.Add("firstBehaviour"));
var secondBehaviour = Substitute.For<IDispatchBehaviour<TestCommand, TestCommandResult>>();
secondBehaviour.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>())
.Returns(args => ((DispatchFurtherDelegate<TestCommandResult>)args[1]).Invoke())
.AndDoes(_ => calls.Add("secondBehaviour"));
ServiceCollection serviceCollection =
[
new ServiceDescriptor(
typeof(ICommandHandler<TestCommand, TestCommandResult>),
(IServiceProvider _) => commandHandler,
ServiceLifetime.Transient
),
new ServiceDescriptor(
typeof(IDispatchBehaviour<TestCommand, TestCommandResult>),
(IServiceProvider _) => firstBehaviour,
ServiceLifetime.Transient
),
new ServiceDescriptor(
typeof(IDispatchBehaviour<TestCommand, TestCommandResult>),
(IServiceProvider _) => secondBehaviour,
ServiceLifetime.Transient
),
new ServiceDescriptor(
typeof(IDispatchBehaviour<,>),
typeof(TestOpenBehaviour<,>),
ServiceLifetime.Transient
),
];
serviceCollection.AddTransient<Action<TestCommand>>(_ => (TestCommand _) => calls.Add("thirdBehaviour"));
var services = serviceCollection.BuildServiceProvider();
var sut = new CommandDispatcherImpl(services, new ConcurrentMethodsCache());
// When
var result = await sut.Dispatch(testCommand, CancellationToken.None);
// Then
result.ShouldBeSameAs(testCommandResult);
await firstBehaviour.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 commandHandler.Received(1).Handle(testCommand, CancellationToken.None);
calls.ShouldBe(["firstBehaviour", "secondBehaviour", "thirdBehaviour", "commandHandler"]);
}
[Fact]
public async Task WhenNextIsNotCalled_ShouldStopExecutingPipeline()
{
// Given
var testCommand = new TestCommand();
var testCommandResult = new TestCommandResult();
var testCommandResultAborted = new TestCommandResult();
List<string> calls = [];
var commandHandler = Substitute.For<ICommandHandler<TestCommand, TestCommandResult>>();
commandHandler.Handle(testCommand, CancellationToken.None)
.Returns(testCommandResult)
.AndDoes(_ => calls.Add("commandHandler"));
var firstBehaviour = Substitute.For<IDispatchBehaviour<TestCommand, TestCommandResult>>();
firstBehaviour.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>())
.Returns(args => ((DispatchFurtherDelegate<TestCommandResult>)args[1]).Invoke())
.AndDoes(_ => calls.Add("firstBehaviour"));
var secondBehaviour = Substitute.For<IDispatchBehaviour<TestCommand, TestCommandResult>>();
secondBehaviour.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>())
.Returns(args => ValueTask.FromResult(testCommandResultAborted))
.AndDoes(_ => calls.Add("secondBehaviour"));
ServiceCollection serviceCollection =
[
new ServiceDescriptor(
typeof(ICommandHandler<TestCommand, TestCommandResult>),
(IServiceProvider _) => commandHandler,
ServiceLifetime.Transient
),
new ServiceDescriptor(
typeof(IDispatchBehaviour<TestCommand, TestCommandResult>),
(IServiceProvider _) => firstBehaviour,
ServiceLifetime.Transient
),
new ServiceDescriptor(
typeof(IDispatchBehaviour<TestCommand, TestCommandResult>),
(IServiceProvider _) => secondBehaviour,
ServiceLifetime.Transient
),
new ServiceDescriptor(
typeof(IDispatchBehaviour<,>),
typeof(TestOpenBehaviour<,>),
ServiceLifetime.Transient
),
];
serviceCollection.AddTransient<Action<TestCommand>>(_ => (TestCommand _) => calls.Add("thirdBehaviour"));
var services = serviceCollection.BuildServiceProvider();
var sut = new CommandDispatcherImpl(services, new ConcurrentMethodsCache());
// When
var result = await sut.Dispatch(testCommand, CancellationToken.None);
// Then
result.ShouldBeSameAs(testCommandResultAborted);
await firstBehaviour.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 commandHandler.Received(0).Handle(testCommand, CancellationToken.None);
calls.ShouldBe(["firstBehaviour", "secondBehaviour"]);
}
}

View File

@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="NSubstitute.Analyzers.CSharp" Version="1.0.17">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup Condition="$(TargetFramework) == 'net9.0' Or $(TargetFramework) == 'netstandard2.1'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework) == 'net8.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../src/Just.Cqrs/Just.Cqrs.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,63 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
namespace Cqrs.Tests.CqrsServicesExtensionsTests;
public class AddBehaviour
{
public class TestCommand {}
public class TestCommandResult {}
[ExcludeFromCodeCoverage]
public class NonGenericTestOpenBehaviour : IDispatchBehaviour<TestCommand, TestCommandResult>
{
public ValueTask<TestCommandResult> Handle(TestCommand request, DispatchFurtherDelegate<TestCommandResult> next, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
[Theory]
[InlineData(ServiceLifetime.Transient)]
[InlineData(ServiceLifetime.Scoped)]
[InlineData(ServiceLifetime.Singleton)]
public void WhenCalled_ShouldRegisterDispatchBehaviour(ServiceLifetime lifetime)
{
// Given
ServiceCollection services = new();
// When
services.AddCqrs(opt => opt
.AddBehaviour<NonGenericTestOpenBehaviour>(lifetime));
// Then
services.ShouldContain(
elementPredicate: descriptor =>
descriptor.ServiceType == typeof(IDispatchBehaviour<TestCommand, TestCommandResult>)
&& descriptor.ImplementationType == typeof(NonGenericTestOpenBehaviour)
&& descriptor.Lifetime == lifetime,
expectedCount: 1
);
}
[ExcludeFromCodeCoverage]
public class InvalidTestBehaviour : IDispatchBehaviour
{
public Type RequestType => throw new NotImplementedException();
public Type ResponseType => throw new NotImplementedException();
}
[Fact]
public void WhenCalledWithInvalidType_ShouldThrow()
{
// Given
ServiceCollection services = new();
// When
// Then
Should.Throw<InvalidOperationException>(() => services.AddCqrs(opt => opt
.AddBehaviour<InvalidTestBehaviour>())
);
}
}

View File

@@ -0,0 +1,124 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
namespace Cqrs.Tests.CqrsServicesExtensionsTests;
public class AddCommandHandler
{
public class TestCommand {}
public class TestCommandResult {}
[ExcludeFromCodeCoverage]
public class TestCommandHandler : ICommandHandler<TestCommand, TestCommandResult>
{
public ValueTask<TestCommandResult> Handle(TestCommand command, CancellationToken cancellation)
{
throw new NotImplementedException();
}
}
[Theory]
[InlineData(ServiceLifetime.Transient)]
[InlineData(ServiceLifetime.Scoped)]
[InlineData(ServiceLifetime.Singleton)]
public void WhenCalled_ShouldRegisterCommandHandler(ServiceLifetime lifetime)
{
// Given
ServiceCollection services = new();
// When
services.AddCqrs(opt => opt.AddCommandHandler<TestCommandHandler>(lifetime));
// Then
services.ShouldContain(
elementPredicate: descriptor =>
descriptor.ServiceType == typeof(ICommandHandler<TestCommand, TestCommandResult>)
&& descriptor.ImplementationType == typeof(TestCommandHandler)
&& descriptor.Lifetime == lifetime,
expectedCount: 1
);
}
[Fact]
public void WhenCalledMultipleTimes_ShouldRegisterCommandHandlerOnce()
{
// Given
ServiceCollection services = new();
// When
services.AddCqrs(opt => opt
.AddCommandHandler<TestCommandHandler>()
.AddCommandHandler<TestCommandHandler>()
.AddCommandHandler<TestCommandHandler>());
services.AddCqrs(opt => opt
.AddCommandHandler<TestCommandHandler>());
// Then
services.ShouldContain(
elementPredicate: descriptor =>
descriptor.ServiceType == typeof(ICommandHandler<TestCommand, TestCommandResult>)
&& descriptor.ImplementationType == typeof(TestCommandHandler)
&& descriptor.Lifetime == ServiceLifetime.Transient,
expectedCount: 1
);
}
public class SecondTestCommand {}
public class SecondTestCommandResult {}
[ExcludeFromCodeCoverage]
public class SecondTestCommandHandler : ICommandHandler<SecondTestCommand, SecondTestCommandResult>
{
public ValueTask<SecondTestCommandResult> Handle(SecondTestCommand command, CancellationToken cancellation)
{
throw new NotImplementedException();
}
}
public class ThirdTestCommand {}
public class ThirdTestCommandResult {}
[ExcludeFromCodeCoverage]
public class ThirdTestCommandHandler : ICommandHandler<ThirdTestCommand, ThirdTestCommandResult>
{
public ValueTask<ThirdTestCommandResult> Handle(ThirdTestCommand command, CancellationToken cancellation)
{
throw new NotImplementedException();
}
}
[Fact]
public void WhenCalledMultipleTimes_ShouldRegisterAllCommandHandlers()
{
// Given
ServiceCollection services = new();
// When
services.AddCqrs(opt => opt
.AddCommandHandler<TestCommandHandler>()
.AddCommandHandler<SecondTestCommandHandler>());
services.AddCqrs(opt => opt
.AddCommandHandler<ThirdTestCommandHandler>());
// Then
services.ShouldContain(
elementPredicate: descriptor =>
descriptor.ServiceType == typeof(ICommandHandler<TestCommand, TestCommandResult>)
&& descriptor.ImplementationType == typeof(TestCommandHandler)
&& descriptor.Lifetime == ServiceLifetime.Transient,
expectedCount: 1
);
services.ShouldContain(
elementPredicate: descriptor =>
descriptor.ServiceType == typeof(ICommandHandler<SecondTestCommand, SecondTestCommandResult>)
&& descriptor.ImplementationType == typeof(SecondTestCommandHandler)
&& descriptor.Lifetime == ServiceLifetime.Transient,
expectedCount: 1
);
services.ShouldContain(
elementPredicate: descriptor =>
descriptor.ServiceType == typeof(ICommandHandler<ThirdTestCommand, ThirdTestCommandResult>)
&& descriptor.ImplementationType == typeof(ThirdTestCommandHandler)
&& descriptor.Lifetime == ServiceLifetime.Transient,
expectedCount: 1
);
}
}

View File

@@ -0,0 +1,89 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
namespace Cqrs.Tests.CqrsServicesExtensionsTests;
public class AddOpenBehaviour
{
[ExcludeFromCodeCoverage]
public class TestOpenBehaviour<TRequest, TResponse> : IDispatchBehaviour<TRequest, TResponse>
where TRequest: notnull
{
public ValueTask<TResponse> Handle(TRequest request, DispatchFurtherDelegate<TResponse> next, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
[Theory]
[InlineData(ServiceLifetime.Transient)]
[InlineData(ServiceLifetime.Scoped)]
[InlineData(ServiceLifetime.Singleton)]
public void WhenCalled_ShouldRegisterOpenDispatchBehaviour(ServiceLifetime lifetime)
{
// Given
ServiceCollection services = new();
// When
services.AddCqrs(opt => opt
.AddOpenBehaviour(typeof(TestOpenBehaviour<,>), lifetime));
// Then
services.ShouldContain(
elementPredicate: descriptor =>
descriptor.ServiceType == typeof(IDispatchBehaviour<,>)
&& descriptor.ImplementationType == typeof(TestOpenBehaviour<,>)
&& descriptor.Lifetime == lifetime,
expectedCount: 1
);
}
[ExcludeFromCodeCoverage]
public class InvalidOpenBehaviour : IDispatchBehaviour
{
public Type RequestType => throw new NotImplementedException();
public Type ResponseType => throw new NotImplementedException();
}
[Fact]
public void WhenCalledWithInvalidType_ShouldThrow()
{
// Given
ServiceCollection services = new();
// When
var invalidOpenDispatchBehaviourType = typeof(InvalidOpenBehaviour);
// Then
Should.Throw<ArgumentException>(() => services.AddCqrs(opt => opt
.AddOpenBehaviour(invalidOpenDispatchBehaviourType))
);
}
public class TestCommand {}
public class TestCommandResult {}
[ExcludeFromCodeCoverage]
public class NonGenericTestOpenBehaviour : IDispatchBehaviour<TestCommand, TestCommandResult>
{
public ValueTask<TestCommandResult> Handle(TestCommand request, DispatchFurtherDelegate<TestCommandResult> next, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
[Fact]
public void WhenCalledWithNonGenericType_ShouldThrow()
{
// Given
ServiceCollection services = new();
// When
var nonGenericOpenDispatchBehaviourType = typeof(NonGenericTestOpenBehaviour);
// Then
Should.Throw<ArgumentException>(() => services.AddCqrs(opt => opt
.AddOpenBehaviour(nonGenericOpenDispatchBehaviourType))
);
}
}

View File

@@ -0,0 +1,124 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
namespace Cqrs.Tests.CqrsServicesExtensionsTests;
public class AddQueryHandler
{
public class TestQuery {}
public class TestQueryResult {}
[ExcludeFromCodeCoverage]
public class TestQueryHandler : IQueryHandler<TestQuery, TestQueryResult>
{
public ValueTask<TestQueryResult> Handle(TestQuery Query, CancellationToken cancellation)
{
throw new NotImplementedException();
}
}
[Theory]
[InlineData(ServiceLifetime.Transient)]
[InlineData(ServiceLifetime.Scoped)]
[InlineData(ServiceLifetime.Singleton)]
public void WhenCalled_ShouldRegisterQueryHandler(ServiceLifetime lifetime)
{
// Given
ServiceCollection services = new();
// When
services.AddCqrs(opt => opt.AddQueryHandler<TestQueryHandler>(lifetime));
// Then
services.ShouldContain(
elementPredicate: descriptor =>
descriptor.ServiceType == typeof(IQueryHandler<TestQuery, TestQueryResult>)
&& descriptor.ImplementationType == typeof(TestQueryHandler)
&& descriptor.Lifetime == lifetime,
expectedCount: 1
);
}
[Fact]
public void WhenCalledMultipleTimes_ShouldRegisterQueryHandlerOnce()
{
// Given
ServiceCollection services = new();
// When
services.AddCqrs(opt => opt
.AddQueryHandler<TestQueryHandler>()
.AddQueryHandler<TestQueryHandler>()
.AddQueryHandler<TestQueryHandler>());
services.AddCqrs(opt => opt
.AddQueryHandler<TestQueryHandler>());
// Then
services.ShouldContain(
elementPredicate: descriptor =>
descriptor.ServiceType == typeof(IQueryHandler<TestQuery, TestQueryResult>)
&& descriptor.ImplementationType == typeof(TestQueryHandler)
&& descriptor.Lifetime == ServiceLifetime.Transient,
expectedCount: 1
);
}
public class SecondTestQuery {}
public class SecondTestQueryResult {}
[ExcludeFromCodeCoverage]
public class SecondTestQueryHandler : IQueryHandler<SecondTestQuery, SecondTestQueryResult>
{
public ValueTask<SecondTestQueryResult> Handle(SecondTestQuery Query, CancellationToken cancellation)
{
throw new NotImplementedException();
}
}
public class ThirdTestQuery {}
public class ThirdTestQueryResult {}
[ExcludeFromCodeCoverage]
public class ThirdTestQueryHandler : IQueryHandler<ThirdTestQuery, ThirdTestQueryResult>
{
public ValueTask<ThirdTestQueryResult> Handle(ThirdTestQuery Query, CancellationToken cancellation)
{
throw new NotImplementedException();
}
}
[Fact]
public void WhenCalledMultipleTimes_ShouldRegisterAllQueryHandlers()
{
// Given
ServiceCollection services = new();
// When
services.AddCqrs(opt => opt
.AddQueryHandler<TestQueryHandler>()
.AddQueryHandler<SecondTestQueryHandler>());
services.AddCqrs(opt => opt
.AddQueryHandler<ThirdTestQueryHandler>());
// Then
services.ShouldContain(
elementPredicate: descriptor =>
descriptor.ServiceType == typeof(IQueryHandler<TestQuery, TestQueryResult>)
&& descriptor.ImplementationType == typeof(TestQueryHandler)
&& descriptor.Lifetime == ServiceLifetime.Transient,
expectedCount: 1
);
services.ShouldContain(
elementPredicate: descriptor =>
descriptor.ServiceType == typeof(IQueryHandler<SecondTestQuery, SecondTestQueryResult>)
&& descriptor.ImplementationType == typeof(SecondTestQueryHandler)
&& descriptor.Lifetime == ServiceLifetime.Transient,
expectedCount: 1
);
services.ShouldContain(
elementPredicate: descriptor =>
descriptor.ServiceType == typeof(IQueryHandler<ThirdTestQuery, ThirdTestQueryResult>)
&& descriptor.ImplementationType == typeof(ThirdTestQueryHandler)
&& descriptor.Lifetime == ServiceLifetime.Transient,
expectedCount: 1
);
}
}

View File

@@ -0,0 +1,63 @@
using Microsoft.Extensions.DependencyInjection;
namespace Cqrs.Tests.CqrsServicesExtensionsTests;
public class AddCqrs
{
[Fact]
public void WhenCalled_ShouldRegisterDispatcherClasses()
{
// Given
ServiceCollection services = new();
// When
services.AddCqrs();
// Then
services.ShouldContain(
elementPredicate: descriptor =>
descriptor.ServiceType == typeof(ICommandDispatcher)
&& descriptor.ImplementationType == typeof(CommandDispatcherImpl)
&& descriptor.Lifetime == ServiceLifetime.Transient,
expectedCount: 1
);
services.ShouldContain(
elementPredicate: descriptor =>
descriptor.ServiceType == typeof(IQueryDispatcher)
&& descriptor.ImplementationType == typeof(QueryDispatcherImpl)
&& descriptor.Lifetime == ServiceLifetime.Transient,
expectedCount: 1
);
}
[Fact]
public void WhenCalledMultipleTimes_ShouldRegisterDispatcherClassesOnce()
{
// Given
ServiceCollection services = new();
// When
services.AddCqrs();
services.AddCqrs();
services.AddCqrs();
services.AddCqrs();
// Then
services.ShouldContain(
elementPredicate: descriptor =>
descriptor.ServiceType == typeof(ICommandDispatcher)
&& descriptor.ImplementationType == typeof(CommandDispatcherImpl)
&& descriptor.Lifetime == ServiceLifetime.Transient,
expectedCount: 1
);
services.ShouldContain(
elementPredicate: descriptor =>
descriptor.ServiceType == typeof(IQueryDispatcher)
&& descriptor.ImplementationType == typeof(QueryDispatcherImpl)
&& descriptor.Lifetime == ServiceLifetime.Transient,
expectedCount: 1
);
}
}

View File

@@ -0,0 +1,188 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NSubstitute;
namespace Cqrs.Tests.QueryDispatcherImplTests;
public class Dispatch
{
public class TestQuery : IKnownQuery<TestQueryResult> {}
public class TestQueryResult {}
[Theory]
[InlineData(ServiceLifetime.Transient)]
[InlineData(ServiceLifetime.Scoped)]
[InlineData(ServiceLifetime.Singleton)]
public async Task WhenCalled_ShouldExecuteHandler(ServiceLifetime lifetime)
{
// Given
var testQuery = new TestQuery();
var testQueryResult = new TestQueryResult();
var queryHandler = Substitute.For<IQueryHandler<TestQuery, TestQueryResult>>();
queryHandler.Handle(testQuery, CancellationToken.None).Returns(testQueryResult);
ServiceCollection serviceCollection =
[
new ServiceDescriptor(
typeof(IQueryHandler<TestQuery, TestQueryResult>),
(IServiceProvider _) => queryHandler,
lifetime
),
];
var services = serviceCollection.BuildServiceProvider();
var sut = new QueryDispatcherImpl(services, new ConcurrentMethodsCache());
// When
var result = await sut.Dispatch(testQuery, CancellationToken.None);
// Then
result.ShouldBeSameAs(testQueryResult);
await queryHandler.Received(1).Handle(testQuery, CancellationToken.None);
}
public class TestOpenBehaviour<TRequest, TResponse> : IDispatchBehaviour<TRequest, TResponse>
where TRequest : notnull
{
private readonly Action<TRequest> _callback;
public TestOpenBehaviour(Action<TRequest> callback)
{
_callback = callback;
}
public ValueTask<TResponse> Handle(TRequest request, DispatchFurtherDelegate<TResponse> next, CancellationToken cancellationToken)
{
_callback.Invoke(request);
return next();
}
}
[Fact]
public async Task WhenPipelineConfigured_ShouldCallAllBehavioursInOrder()
{
// Given
var testQuery = new TestQuery();
var testQueryResult = new TestQueryResult();
List<string> calls = [];
var queryHandler = Substitute.For<IQueryHandler<TestQuery, TestQueryResult>>();
queryHandler.Handle(testQuery, CancellationToken.None)
.Returns(testQueryResult)
.AndDoes(_ => calls.Add("queryHandler"));
var firstBehaviour = Substitute.For<IDispatchBehaviour<TestQuery, TestQueryResult>>();
firstBehaviour.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>())
.Returns(args => ((DispatchFurtherDelegate<TestQueryResult>)args[1]).Invoke())
.AndDoes(_ => calls.Add("firstBehaviour"));
var secondBehaviour = Substitute.For<IDispatchBehaviour<TestQuery, TestQueryResult>>();
secondBehaviour.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>())
.Returns(args => ((DispatchFurtherDelegate<TestQueryResult>)args[1]).Invoke())
.AndDoes(_ => calls.Add("secondBehaviour"));
ServiceCollection serviceCollection =
[
new ServiceDescriptor(
typeof(IQueryHandler<TestQuery, TestQueryResult>),
(IServiceProvider _) => queryHandler,
ServiceLifetime.Transient
),
new ServiceDescriptor(
typeof(IDispatchBehaviour<TestQuery, TestQueryResult>),
(IServiceProvider _) => firstBehaviour,
ServiceLifetime.Transient
),
new ServiceDescriptor(
typeof(IDispatchBehaviour<TestQuery, TestQueryResult>),
(IServiceProvider _) => secondBehaviour,
ServiceLifetime.Transient
),
new ServiceDescriptor(
typeof(IDispatchBehaviour<,>),
typeof(TestOpenBehaviour<,>),
ServiceLifetime.Transient
),
];
serviceCollection.AddTransient<Action<TestQuery>>(_ => (TestQuery _) => calls.Add("thirdBehaviour"));
var services = serviceCollection.BuildServiceProvider();
var sut = new QueryDispatcherImpl(services, new ConcurrentMethodsCache());
// When
var result = await sut.Dispatch(testQuery, CancellationToken.None);
// Then
result.ShouldBeSameAs(testQueryResult);
await firstBehaviour.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 queryHandler.Received(1).Handle(testQuery, CancellationToken.None);
calls.ShouldBe(["firstBehaviour", "secondBehaviour", "thirdBehaviour", "queryHandler"]);
}
[Fact]
public async Task WhenNextIsNotCalled_ShouldStopExecutingPipeline()
{
// Given
var testQuery = new TestQuery();
var testQueryResult = new TestQueryResult();
var testQueryResultAborted = new TestQueryResult();
List<string> calls = [];
var queryHandler = Substitute.For<IQueryHandler<TestQuery, TestQueryResult>>();
queryHandler.Handle(testQuery, CancellationToken.None)
.Returns(testQueryResult)
.AndDoes(_ => calls.Add("queryHandler"));
var firstBehaviour = Substitute.For<IDispatchBehaviour<TestQuery, TestQueryResult>>();
firstBehaviour.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>())
.Returns(args => ((DispatchFurtherDelegate<TestQueryResult>)args[1]).Invoke())
.AndDoes(_ => calls.Add("firstBehaviour"));
var secondBehaviour = Substitute.For<IDispatchBehaviour<TestQuery, TestQueryResult>>();
secondBehaviour.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>())
.Returns(args => ValueTask.FromResult(testQueryResultAborted))
.AndDoes(_ => calls.Add("secondBehaviour"));
ServiceCollection serviceCollection =
[
new ServiceDescriptor(
typeof(IQueryHandler<TestQuery, TestQueryResult>),
(IServiceProvider _) => queryHandler,
ServiceLifetime.Transient
),
new ServiceDescriptor(
typeof(IDispatchBehaviour<TestQuery, TestQueryResult>),
(IServiceProvider _) => firstBehaviour,
ServiceLifetime.Transient
),
new ServiceDescriptor(
typeof(IDispatchBehaviour<TestQuery, TestQueryResult>),
(IServiceProvider _) => secondBehaviour,
ServiceLifetime.Transient
),
new ServiceDescriptor(
typeof(IDispatchBehaviour<,>),
typeof(TestOpenBehaviour<,>),
ServiceLifetime.Transient
),
];
serviceCollection.AddTransient<Action<TestQuery>>(_ => (TestQuery _) => calls.Add("thirdBehaviour"));
var services = serviceCollection.BuildServiceProvider();
var sut = new QueryDispatcherImpl(services, new ConcurrentMethodsCache());
// When
var result = await sut.Dispatch(testQuery, CancellationToken.None);
// Then
result.ShouldBeSameAs(testQueryResultAborted);
await firstBehaviour.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 queryHandler.Received(0).Handle(testQuery, CancellationToken.None);
calls.ShouldBe(["firstBehaviour", "secondBehaviour"]);
}
}

View File

@@ -0,0 +1,3 @@
global using Shouldly;
global using Just.Cqrs;
global using Just.Cqrs.Internal;