This commit is contained in:
13
.config/dotnet-tools.json
Normal file
13
.config/dotnet-tools.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-reportgenerator-globaltool": {
|
||||||
|
"version": "5.4.3",
|
||||||
|
"commands": [
|
||||||
|
"reportgenerator"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
.gitea/workflows/test-dotnet.yaml
Normal file
54
.gitea/workflows/test-dotnet.yaml
Normal 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
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"dotnet.defaultSolution": "Just.Cqrs.sln",
|
||||||
|
"dotnetAcquisitionExtension.enableTelemetry": false
|
||||||
|
}
|
||||||
17
Directory.Build.props
Normal file
17
Directory.Build.props
Normal 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
43
Just.Cqrs.sln
Normal 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
|
||||||
11
src/Just.Cqrs.Abstractions/ICommandDispatcher.cs
Normal file
11
src/Just.Cqrs.Abstractions/ICommandDispatcher.cs
Normal 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);
|
||||||
|
}
|
||||||
9
src/Just.Cqrs.Abstractions/ICommandHandler.cs
Normal file
9
src/Just.Cqrs.Abstractions/ICommandHandler.cs
Normal 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);
|
||||||
|
}
|
||||||
38
src/Just.Cqrs.Abstractions/IDispatchBehaviour.cs
Normal file
38
src/Just.Cqrs.Abstractions/IDispatchBehaviour.cs
Normal 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);
|
||||||
|
}
|
||||||
7
src/Just.Cqrs.Abstractions/IKnownCommand.cs
Normal file
7
src/Just.Cqrs.Abstractions/IKnownCommand.cs
Normal 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>{}
|
||||||
7
src/Just.Cqrs.Abstractions/IKnownQuery.cs
Normal file
7
src/Just.Cqrs.Abstractions/IKnownQuery.cs
Normal 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>{}
|
||||||
12
src/Just.Cqrs.Abstractions/IQueryDispatcher.cs
Normal file
12
src/Just.Cqrs.Abstractions/IQueryDispatcher.cs
Normal 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);
|
||||||
|
}
|
||||||
9
src/Just.Cqrs.Abstractions/IQueryHandler.cs
Normal file
9
src/Just.Cqrs.Abstractions/IQueryHandler.cs
Normal 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Just.Cqrs.Internal;
|
||||||
|
|
||||||
|
public interface ICommandHandlerImpl { }
|
||||||
3
src/Just.Cqrs.Abstractions/Internal/IQueryHandlerImpl.cs
Normal file
3
src/Just.Cqrs.Abstractions/Internal/IQueryHandlerImpl.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Just.Cqrs.Internal;
|
||||||
|
|
||||||
|
public interface IQueryHandlerImpl { }
|
||||||
10
src/Just.Cqrs.Abstractions/Just.Cqrs.Abstractions.csproj
Normal file
10
src/Just.Cqrs.Abstractions/Just.Cqrs.Abstractions.csproj
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
|
||||||
|
<RootNamespace>Just.Cqrs</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
121
src/Just.Cqrs/CqrsServicesExtensions.cs
Normal file
121
src/Just.Cqrs/CqrsServicesExtensions.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/Just.Cqrs/CqrsServicesOptions.cs
Normal file
12
src/Just.Cqrs/CqrsServicesOptions.cs
Normal 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;
|
||||||
|
}
|
||||||
47
src/Just.Cqrs/Internal/CommandDispatcherImpl.cs
Normal file
47
src/Just.Cqrs/Internal/CommandDispatcherImpl.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/Just.Cqrs/Internal/IMethodsCache.cs
Normal file
17
src/Just.Cqrs/Internal/IMethodsCache.cs
Normal 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;
|
||||||
47
src/Just.Cqrs/Internal/QueryDispatcherImpl.cs
Normal file
47
src/Just.Cqrs/Internal/QueryDispatcherImpl.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/Just.Cqrs/Just.Cqrs.csproj
Normal file
23
src/Just.Cqrs/Just.Cqrs.csproj
Normal 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>
|
||||||
188
tests/Cqrs.Tests/CommandDispatcherImplTests/Dispatch.cs
Normal file
188
tests/Cqrs.Tests/CommandDispatcherImplTests/Dispatch.cs
Normal 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"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
tests/Cqrs.Tests/Cqrs.Tests.csproj
Normal file
39
tests/Cqrs.Tests/Cqrs.Tests.csproj
Normal 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>
|
||||||
63
tests/Cqrs.Tests/CqrsServicesExtensionsTests/AddBehaviour.cs
Normal file
63
tests/Cqrs.Tests/CqrsServicesExtensionsTests/AddBehaviour.cs
Normal 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>())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
124
tests/Cqrs.Tests/CqrsServicesExtensionsTests/AddQueryHandler.cs
Normal file
124
tests/Cqrs.Tests/CqrsServicesExtensionsTests/AddQueryHandler.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
tests/Cqrs.Tests/CqrsServicesExtensionsTests/AddSqrs.cs
Normal file
63
tests/Cqrs.Tests/CqrsServicesExtensionsTests/AddSqrs.cs
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
188
tests/Cqrs.Tests/QueryDispatcherImplTests/Dispatch.cs
Normal file
188
tests/Cqrs.Tests/QueryDispatcherImplTests/Dispatch.cs
Normal 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"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
tests/Cqrs.Tests/usings.cs
Normal file
3
tests/Cqrs.Tests/usings.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
global using Shouldly;
|
||||||
|
global using Just.Cqrs;
|
||||||
|
global using Just.Cqrs.Internal;
|
||||||
Reference in New Issue
Block a user