Compare commits
4 Commits
54ea0925dd
...
v1.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
fc1f5ca7d7
|
|||
| 2fded2809f | |||
| 7c3dd84971 | |||
| d437d06c09 |
@@ -25,6 +25,15 @@ tab_width = 4
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
#### .NET diagnostic severity
|
||||
[*.{cs,vb}]
|
||||
|
||||
# CS9124: Parameter is captured into the state of the enclosing type and its value is also used to initialize a field, property, or event.
|
||||
dotnet_diagnostic.CS9124.severity = error
|
||||
|
||||
# CS9107: Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
|
||||
dotnet_diagnostic.CS9107.severity = error
|
||||
|
||||
#### .NET Coding Conventions ####
|
||||
[*.{cs,vb}]
|
||||
|
||||
|
||||
41
.gitea/workflows/publish-nuget.yaml
Normal file
41
.gitea/workflows/publish-nuget.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
name: .NET Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup .NET
|
||||
uses: https://github.com/actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 10.x
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore --nologo
|
||||
|
||||
- name: Test
|
||||
run: dotnet test --nologo --no-restore --configuration Release
|
||||
|
||||
- name: Create the package
|
||||
env:
|
||||
RELEASE_VERSION: ${{ gitea.ref_name }}
|
||||
run: >
|
||||
dotnet pack --no-restore --configuration Release --output nupkgs
|
||||
`echo $RELEASE_VERSION | sed -E 's|^(v([0-9]+(\.[0-9]+){2}))(-([a-z0-9]+)){1}|/p:ReleaseVersion=\2 /p:VersionSuffix=\5|; s|^(v([0-9]+(\.[0-9]+){2}))$|/p:ReleaseVersion=\2|'`
|
||||
|
||||
- name: Publish the package to Gitea
|
||||
run: dotnet nuget push --source ${{ vars.OUTPUT_NUGET_REGISTRY }} --api-key ${{ secrets.LOCAL_NUGET_PACKAGE_TOKEN }} nupkgs/*.nupkg
|
||||
|
||||
- name: Publish the package to NuGet.org
|
||||
run: dotnet nuget push --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_PACKAGE_TOKEN }} nupkgs/*.nupkg
|
||||
@@ -21,34 +21,48 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
name: .NET tests
|
||||
|
||||
env:
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
TEST_PROJECT: ./tests/Cqrs.Tests/Cqrs.Tests.csproj
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup .NET
|
||||
uses: https://github.com/actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
dotnet-version: |
|
||||
8.0.x
|
||||
9.0.x
|
||||
10.0.x
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore --nologo
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --nologo --configuration Release --no-restore
|
||||
- name: Build .NET 10.0
|
||||
run: dotnet build --no-restore --framework net10.0 --configuration Release ${{ env.TEST_PROJECT }}
|
||||
|
||||
- name: Test
|
||||
run: dotnet test --nologo --configuration Release --no-build --verbosity normal --logger trx --collect:"XPlat Code Coverage"
|
||||
- name: Build .NET 9.0
|
||||
run: dotnet build --no-restore --framework net9.0 --configuration Release ${{ env.TEST_PROJECT }}
|
||||
|
||||
- name: Restore local tools
|
||||
run: dotnet tool restore
|
||||
- name: Build .NET 8.0
|
||||
run: dotnet build --no-restore --framework net8.0 --configuration Release ${{ env.TEST_PROJECT }}
|
||||
|
||||
- name: Generate coverage report
|
||||
run: dotnet reportgenerator -reports:**/coverage.cobertura.xml -targetdir:./coverage -reporttypes:MarkdownSummary
|
||||
- name: Test .NET 10.0
|
||||
run: dotnet run --no-build --framework net10.0 --configuration Release --project ${{ env.TEST_PROJECT }} -- -trx TestResults/results-net10.trx
|
||||
|
||||
- name: Test .NET 9.0
|
||||
run: dotnet run --no-build --framework net9.0 --configuration Release --project ${{ env.TEST_PROJECT }} -- -trx TestResults/results-net9.trx
|
||||
|
||||
- name: Test .NET 8.0
|
||||
run: dotnet run --no-build --framework net8.0 --configuration Release --project ${{ env.TEST_PROJECT }} -- -trx TestResults/results-net8.trx
|
||||
|
||||
- name: Upload dotnet test results
|
||||
#uses: actions/upload-artifact@v4
|
||||
uses: christopherhx/gitea-upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage-results
|
||||
path: coverage
|
||||
name: test-results
|
||||
path: TestResults
|
||||
if: ${{ always() }}
|
||||
retention-days: 30
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"dotnet.defaultSolution": "Just.Cqrs.sln",
|
||||
"dotnetAcquisitionExtension.enableTelemetry": false
|
||||
"dotnetAcquisitionExtension.enableTelemetry": false,
|
||||
"dotnet.testWindow.useTestingPlatformProtocol": true
|
||||
}
|
||||
|
||||
@@ -7,11 +7,22 @@
|
||||
<Description>Lightweight, easy-to-use C# library designed to simplify the implementation of the Command Query Responsibility Segregation (CQRS) pattern.</Description>
|
||||
<Authors>JustFixMe</Authors>
|
||||
<Copyright>Copyright (c) 2025 JustFixMe</Copyright>
|
||||
<RepositoryUrl>https://github.com/JustFixMe/Just.Core/</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/JustFixMe/Just.Cqrs/</RepositoryUrl>
|
||||
|
||||
<PackageTags>c#;CQRS</PackageTags>
|
||||
<PackageTags>csharp;cqrs;cqrs-pattern;</PackageTags>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageReadmeFile>readme.md</PackageReadmeFile>
|
||||
|
||||
<ReleaseVersion Condition=" '$(ReleaseVersion)' == '' ">1.0.0</ReleaseVersion>
|
||||
<VersionSuffix Condition=" '$(VersionSuffix)' != '' ">$(VersionSuffix)</VersionSuffix>
|
||||
<VersionPrefix Condition=" '$(VersionSuffix)' != '' ">$(ReleaseVersion)</VersionPrefix>
|
||||
<Version Condition=" '$(VersionSuffix)' == '' ">$(ReleaseVersion)</Version>
|
||||
<AssemblyVersion>$(ReleaseVersion)</AssemblyVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\readme.md" Pack="true" PackagePath=""/>
|
||||
<None Include="..\..\LICENSE" Pack="true" PackagePath=""/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
76
readme.md
76
readme.md
@@ -7,24 +7,90 @@ Inspired by [MediatR](https://github.com/jbogard/MediatR)
|
||||
## Features
|
||||
|
||||
* Separate dispatching of Commands/Queries
|
||||
* Middleware-like behaviours
|
||||
* Middleware-like Behaviors
|
||||
|
||||
## Compatibility
|
||||
|
||||
**Just.Cqrs** is built for .Net Standard 2.1 and .NET 8.0 and 9.0.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Install from NuGet.org
|
||||
|
||||
```
|
||||
```bash
|
||||
# install the package using NuGet
|
||||
dotnet add package Just.Cqrs
|
||||
```
|
||||
|
||||
### Register in DI with ```IServiceCollection```
|
||||
|
||||
```cs
|
||||
```csharp
|
||||
services.AddCqrs(opt => opt
|
||||
.AddQueryHandler<SomeQueryHandler>()
|
||||
.AddCommandHandler<SomeCommandHandler>()
|
||||
.AddBehaviour<SomeBehaviour>()
|
||||
.AddOpenBehaviour(typeof(SomeOpenBehaviour<,>))
|
||||
.AddBehavior<SomeQueryBehavior>()
|
||||
.AddOpenBehavior(typeof(LoggingBehavior<,>))
|
||||
);
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Define a Query and Handler
|
||||
|
||||
```csharp
|
||||
record GetUserByIdQuery(int UserId) : IKnownQuery<User>;
|
||||
|
||||
class GetUserByIdQueryHandler : IQueryHandler<GetUserByIdQuery, User>
|
||||
{
|
||||
public ValueTask<User> Handle(GetUserByIdQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
// Fetch user logic here
|
||||
}
|
||||
}
|
||||
|
||||
// Use Dispatcher to execute the query
|
||||
class GetUserByIdUseCase(IQueryDispatcher dispatcher)
|
||||
{
|
||||
public async Task<IResult> Execute(int userId, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await dispatcher.Dispatch(new GetUserByIdQuery(userId), cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
\* *the same principles apply to commands*
|
||||
|
||||
### Define a Behavior
|
||||
|
||||
```csharp
|
||||
class LoggingBehavior<TRequest, TResult>(ILogger logger) : IDispatchBehavior<TRequest, TResult>
|
||||
where TRequest: notnull
|
||||
{
|
||||
public async ValueTask<TResult> Handle(
|
||||
TRequest request,
|
||||
DispatchFurtherDelegate<TResult> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogInformation("Handling request: {RequestType}", typeof(TRequest).Name);
|
||||
var result = await next();
|
||||
logger.LogInformation("Request handled: {RequestType}", typeof(TRequest).Name);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class SomeQueryBehavior : IDispatchBehavior<SomeQuery, SomeQueryResult>
|
||||
{
|
||||
public async ValueTask<SomeQueryResult> Handle(
|
||||
SomeQuery request,
|
||||
DispatchFurtherDelegate<SomeQueryResult> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// do something
|
||||
return await next();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
**Just.Cqrs** is licensed under the [MIT License](LICENSE).
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Just.Cqrs.Internal;
|
||||
|
||||
namespace Just.Cqrs;
|
||||
|
||||
public interface ICommandHandler<TCommand, TCommandResult> : ICommandHandlerImpl
|
||||
public interface ICommandHandler<TCommand, TCommandResult> : ICommandHandlerImpl, IGenericHandler<TCommand, TCommandResult>
|
||||
where TCommand : notnull
|
||||
{
|
||||
ValueTask<TCommandResult> Handle(TCommand command, CancellationToken cancellation);
|
||||
new ValueTask<TCommandResult> Handle(TCommand command, CancellationToken cancellation);
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
ValueTask<TCommandResult> IGenericHandler<TCommand, TCommandResult>.Handle(
|
||||
TCommand request,
|
||||
CancellationToken cancellationToken) => Handle(request, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public delegate ValueTask<TResponse> DispatchFurtherDelegate<TResponse>();
|
||||
/// <summary>
|
||||
/// Marker interface for static type checking. Should not be used directly.
|
||||
/// </summary>
|
||||
public interface IDispatchBehaviour
|
||||
public interface IDispatchBehavior
|
||||
{
|
||||
Type RequestType { get; }
|
||||
Type ResponseType { get; }
|
||||
@@ -23,7 +23,7 @@ public interface IDispatchBehaviour
|
||||
/// </summary>
|
||||
/// <typeparam name="TRequest">Request type</typeparam>
|
||||
/// <typeparam name="TResponse">Result type of dispatching command/query</typeparam>
|
||||
public interface IDispatchBehaviour<in TRequest, TResponse> : IDispatchBehaviour
|
||||
public interface IDispatchBehavior<in TRequest, TResponse> : IDispatchBehavior
|
||||
where TRequest : notnull
|
||||
{
|
||||
ValueTask<TResponse> Handle(
|
||||
@@ -32,7 +32,7 @@ public interface IDispatchBehaviour<in TRequest, TResponse> : IDispatchBehaviour
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
Type IDispatchBehaviour.RequestType => typeof(TRequest);
|
||||
Type IDispatchBehavior.RequestType => typeof(TRequest);
|
||||
[ExcludeFromCodeCoverage]
|
||||
Type IDispatchBehaviour.ResponseType => typeof(TResponse);
|
||||
Type IDispatchBehavior.ResponseType => typeof(TResponse);
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Just.Cqrs.Internal;
|
||||
|
||||
namespace Just.Cqrs;
|
||||
|
||||
public interface IQueryHandler<TQuery, TQueryResult> : IQueryHandlerImpl
|
||||
public interface IQueryHandler<TQuery, TQueryResult> : IQueryHandlerImpl, IGenericHandler<TQuery, TQueryResult>
|
||||
where TQuery : notnull
|
||||
{
|
||||
ValueTask<TQueryResult> Handle(TQuery query, CancellationToken cancellation);
|
||||
new ValueTask<TQueryResult> Handle(TQuery query, CancellationToken cancellation);
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
ValueTask<TQueryResult> IGenericHandler<TQuery, TQueryResult>.Handle(
|
||||
TQuery request,
|
||||
CancellationToken cancellationToken) => Handle(request, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
namespace Just.Cqrs.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Marker interface for static type checking. Should not be used directly.
|
||||
/// </summary>
|
||||
public interface ICommandHandlerImpl { }
|
||||
|
||||
10
src/Just.Cqrs.Abstractions/Internal/IGenericHandler.cs
Normal file
10
src/Just.Cqrs.Abstractions/Internal/IGenericHandler.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Just.Cqrs.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Marker interface for static type checking. Should not be used directly.
|
||||
/// </summary>
|
||||
public interface IGenericHandler<TRequest, TResponse>
|
||||
where TRequest : notnull
|
||||
{
|
||||
ValueTask<TResponse> Handle(TRequest request, CancellationToken cancellation);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
namespace Just.Cqrs.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Marker interface for static type checking. Should not be used directly.
|
||||
/// </summary>
|
||||
public interface IQueryHandlerImpl { }
|
||||
|
||||
@@ -8,15 +8,19 @@ namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class CqrsServicesExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds all configured Command and Query handlers, behaviors and default implementations of <see cref="ICommandDispatcher"/> and <see cref="IQueryDispatcher"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If called multiple times <see cref="ICommandDispatcher"/> and <see cref="IQueryDispatcher"/> will still be added once
|
||||
/// </remarks>
|
||||
public static IServiceCollection AddCqrs(this IServiceCollection services, Action<CqrsServicesOptions>? configure = null)
|
||||
{
|
||||
var options = new CqrsServicesOptions(services);
|
||||
configure?.Invoke(options);
|
||||
|
||||
services.TryAddKeyedSingleton<IMethodsCache, ConcurrentMethodsCache>(MethodsCacheServiceKey.DispatchCommand);
|
||||
services.TryAddSingleton<IMethodsCache, ConcurrentMethodsCache>();
|
||||
services.TryAddTransient<ICommandDispatcher, CommandDispatcherImpl>();
|
||||
|
||||
services.TryAddKeyedSingleton<IMethodsCache, ConcurrentMethodsCache>(MethodsCacheServiceKey.DispatchQuery);
|
||||
services.TryAddTransient<IQueryDispatcher, QueryDispatcherImpl>();
|
||||
|
||||
foreach (var (service, impl, lifetime) in options.CommandHandlers)
|
||||
@@ -27,7 +31,7 @@ public static class CqrsServicesExtensions
|
||||
{
|
||||
services.TryAdd(new ServiceDescriptor(service, impl, lifetime));
|
||||
}
|
||||
foreach (var (service, impl, lifetime) in options.Behaviours)
|
||||
foreach (var (service, impl, lifetime) in options.Behaviors)
|
||||
{
|
||||
services.Add(new ServiceDescriptor(service, impl, lifetime));
|
||||
}
|
||||
@@ -75,43 +79,43 @@ public static class CqrsServicesExtensions
|
||||
return options;
|
||||
}
|
||||
|
||||
public static CqrsServicesOptions AddOpenBehaviour(this CqrsServicesOptions options, Type behaviour, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
public static CqrsServicesOptions AddOpenBehavior(this CqrsServicesOptions options, Type behavior, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
{
|
||||
var interfaces = behaviour.FindInterfaces(
|
||||
var interfaces = behavior.FindInterfaces(
|
||||
static (x, t) => x.IsGenericType && x.GetGenericTypeDefinition() == (Type)t!,
|
||||
typeof(IDispatchBehaviour<,>));
|
||||
typeof(IDispatchBehavior<,>));
|
||||
|
||||
if (interfaces.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("Supplied type does not implement IDispatchBehaviour<,> interface.", nameof(behaviour));
|
||||
throw new ArgumentException("Supplied type does not implement IDispatchBehavior<,> interface.", nameof(behavior));
|
||||
}
|
||||
|
||||
if (!behaviour.ContainsGenericParameters)
|
||||
if (!behavior.ContainsGenericParameters)
|
||||
{
|
||||
throw new ArgumentException("Supplied type is not sutable for open behaviour.", nameof(behaviour));
|
||||
throw new ArgumentException("Supplied type is not suitable for open Behavior.", nameof(behavior));
|
||||
}
|
||||
|
||||
options.Behaviours.Add((typeof(IDispatchBehaviour<,>), behaviour, lifetime));
|
||||
options.Behaviors.Add((typeof(IDispatchBehavior<,>), behavior, lifetime));
|
||||
return options;
|
||||
}
|
||||
|
||||
public static CqrsServicesOptions AddBehaviour<TBehaviour>(this CqrsServicesOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
where TBehaviour : notnull, IDispatchBehaviour
|
||||
public static CqrsServicesOptions AddBehavior<TBehavior>(this CqrsServicesOptions options, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
where TBehavior : notnull, IDispatchBehavior
|
||||
{
|
||||
var type = typeof(TBehaviour);
|
||||
var type = typeof(TBehavior);
|
||||
|
||||
var interfaces = type.FindInterfaces(
|
||||
static (x, t) => x.IsGenericType && x.GetGenericTypeDefinition() == (Type)t!,
|
||||
typeof(IDispatchBehaviour<,>));
|
||||
typeof(IDispatchBehavior<,>));
|
||||
|
||||
if (interfaces.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Supplied type does not implement IDispatchBehaviour<,> interface.");
|
||||
throw new InvalidOperationException("Supplied type does not implement IDispatchBehavior<,> interface.");
|
||||
}
|
||||
|
||||
foreach (var interfaceType in interfaces)
|
||||
{
|
||||
options.Behaviours.Add((
|
||||
options.Behaviors.Add((
|
||||
interfaceType,
|
||||
type,
|
||||
lifetime));
|
||||
|
||||
@@ -4,7 +4,7 @@ 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)> Behaviors = [];
|
||||
internal readonly List<(Type Service, Type Impl, ServiceLifetime Lifetime)> CommandHandlers = [];
|
||||
internal readonly List<(Type Service, Type Impl, ServiceLifetime Lifetime)> QueryHandlers = [];
|
||||
|
||||
|
||||
@@ -6,27 +6,25 @@ namespace Just.Cqrs.Internal;
|
||||
|
||||
internal sealed class CommandDispatcherImpl(
|
||||
IServiceProvider services,
|
||||
[FromKeyedServices(MethodsCacheServiceKey.DispatchCommand)]IMethodsCache methodsCache
|
||||
) : ICommandDispatcher
|
||||
IMethodsCache methodsCache
|
||||
) : DispatcherBase(methodsCache), ICommandDispatcher
|
||||
{
|
||||
private static readonly Func<(Type RequestType, Type ResponseType), Delegate> CreateDispatchCommandDelegate;
|
||||
static CommandDispatcherImpl()
|
||||
{
|
||||
var dispatcherType = typeof(CommandDispatcherImpl);
|
||||
var genericDispatchImplMethod = dispatcherType
|
||||
.GetMethod(nameof(DispatchCommandImpl), BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
?? throw new InvalidOperationException($"{nameof(DispatchCommandImpl)} method not found.");
|
||||
CreateDispatchCommandDelegate = methodsCacheKey => CreateDispatchDelegate(methodsCacheKey, dispatcherType, genericDispatchImplMethod);
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public ValueTask<TCommandResult> Dispatch<TCommandResult>(object command, CancellationToken cancellationToken)
|
||||
=> DispatchCommand<TCommandResult>(command, cancellationToken);
|
||||
=> DispatchInternal<TCommandResult>(CreateDispatchCommandDelegate, command, cancellationToken);
|
||||
|
||||
public ValueTask<TCommandResult> Dispatch<TCommandResult>(IKnownCommand<TCommandResult> command, CancellationToken cancellationToken)
|
||||
=> 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])!;
|
||||
}
|
||||
=> DispatchInternal<TCommandResult>(CreateDispatchCommandDelegate, command, cancellationToken);
|
||||
|
||||
private ValueTask<TCommandResult> DispatchCommandImpl<TCommand, TCommandResult>(
|
||||
TCommand command,
|
||||
@@ -34,14 +32,8 @@ internal sealed class CommandDispatcherImpl(
|
||||
where TCommand : notnull
|
||||
{
|
||||
var handler = services.GetRequiredService<ICommandHandler<TCommand, TCommandResult>>();
|
||||
var pipeline = services.GetServices<IDispatchBehaviour<TCommand, TCommandResult>>();
|
||||
using var pipelineEnumerator = pipeline.GetEnumerator();
|
||||
var pipeline = services.GetServices<IDispatchBehavior<TCommand, TCommandResult>>();
|
||||
|
||||
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));
|
||||
return DispatchDelegateFactory(command, handler, pipeline, cancellationToken).Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
67
src/Just.Cqrs/Internal/DispatcherBase.cs
Normal file
67
src/Just.Cqrs/Internal/DispatcherBase.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Just.Cqrs.Internal;
|
||||
|
||||
internal abstract class DispatcherBase(IMethodsCache methodsCache)
|
||||
{
|
||||
protected IMethodsCache MethodsCache { get; } = methodsCache;
|
||||
protected ValueTask<TResult> DispatchInternal<TResult>(
|
||||
Func<(Type, Type), Delegate> delegateFactory,
|
||||
object request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = (request.GetType(), typeof(TResult));
|
||||
var dispatchDelegate = (Func<DispatcherBase, object, CancellationToken, ValueTask<TResult>>)
|
||||
MethodsCache.GetOrAdd(cacheKey, delegateFactory);
|
||||
return dispatchDelegate(this, request, cancellationToken);
|
||||
}
|
||||
|
||||
protected DispatchFurtherDelegate<TResponse> DispatchDelegateFactory<TRequest, TResponse, THandler>(
|
||||
TRequest request,
|
||||
THandler handler,
|
||||
IEnumerable<IDispatchBehavior<TRequest, TResponse>> behaviors,
|
||||
CancellationToken cancellationToken)
|
||||
where TRequest : notnull
|
||||
where THandler : IGenericHandler<TRequest, TResponse>
|
||||
{
|
||||
DispatchFurtherDelegate<TResponse> pipeline = behaviors.Reverse()
|
||||
.Aggregate<IDispatchBehavior<TRequest, TResponse>, DispatchFurtherDelegate<TResponse>>(
|
||||
() => handler.Handle(request, cancellationToken),
|
||||
(next, behavior) => () => behavior.Handle(request, next, cancellationToken)
|
||||
);
|
||||
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
internal static Delegate CreateDispatchDelegate((Type RequestType, Type ResponseType) methodsCacheKey, Type dispatcherType, MethodInfo genericDispatchImplMethod)
|
||||
{
|
||||
var dispatcherBaseType = typeof(DispatcherBase);
|
||||
var (requestType, responseType) = methodsCacheKey;
|
||||
var dispatchImplMethod = genericDispatchImplMethod.MakeGenericMethod(requestType, responseType);
|
||||
|
||||
ParameterExpression[] lambdaParameters =
|
||||
[
|
||||
Expression.Parameter(dispatcherBaseType),
|
||||
Expression.Parameter(typeof(object)),
|
||||
Expression.Parameter(typeof(CancellationToken)),
|
||||
];
|
||||
Expression[] callParameters =
|
||||
[
|
||||
Expression.Convert(lambdaParameters[1], requestType),
|
||||
lambdaParameters[2],
|
||||
];
|
||||
var lambdaExpression = Expression.Lambda(
|
||||
typeof(Func<,,,>).MakeGenericType(
|
||||
dispatcherBaseType,
|
||||
typeof(object),
|
||||
typeof(CancellationToken),
|
||||
typeof(ValueTask<>).MakeGenericType(responseType)),
|
||||
Expression.Call(Expression.Convert(lambdaParameters[0], dispatcherType), dispatchImplMethod, callParameters),
|
||||
lambdaParameters
|
||||
);
|
||||
var dispatchQueryDelegate = lambdaExpression.Compile();
|
||||
|
||||
return dispatchQueryDelegate;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,10 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Just.Cqrs.Internal;
|
||||
|
||||
internal interface IMethodsCache
|
||||
{
|
||||
MethodInfo GetOrAdd(Type key, Func<Type, MethodInfo> valueFactory);
|
||||
Delegate GetOrAdd((Type RequestType, Type ResponseType) key, Func<(Type RequestType, Type ResponseType), Delegate> valueFactory);
|
||||
}
|
||||
|
||||
internal static class MethodsCacheServiceKey
|
||||
{
|
||||
internal const string DispatchQuery = "q";
|
||||
internal const string DispatchCommand = "c";
|
||||
}
|
||||
|
||||
internal sealed class ConcurrentMethodsCache : ConcurrentDictionary<Type, MethodInfo>, IMethodsCache;
|
||||
internal sealed class ConcurrentMethodsCache : ConcurrentDictionary<(Type RequestType, Type ResponseType), Delegate>, IMethodsCache;
|
||||
|
||||
@@ -6,27 +6,25 @@ namespace Just.Cqrs.Internal;
|
||||
|
||||
internal sealed class QueryDispatcherImpl(
|
||||
IServiceProvider services,
|
||||
[FromKeyedServices(MethodsCacheServiceKey.DispatchQuery)]IMethodsCache methodsCache
|
||||
) : IQueryDispatcher
|
||||
IMethodsCache methodsCache
|
||||
) : DispatcherBase(methodsCache), IQueryDispatcher
|
||||
{
|
||||
private static readonly Func<(Type RequestType, Type ResponseType), Delegate> CreateDispatchQueryDelegate;
|
||||
static QueryDispatcherImpl()
|
||||
{
|
||||
var dispatcherType = typeof(QueryDispatcherImpl);
|
||||
var genericDispatchImplMethod = dispatcherType
|
||||
.GetMethod(nameof(DispatchQueryImpl), BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
?? throw new InvalidOperationException($"{nameof(DispatchQueryImpl)} method not found.");
|
||||
CreateDispatchQueryDelegate = methodsCacheKey => CreateDispatchDelegate(methodsCacheKey, dispatcherType, genericDispatchImplMethod);
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public ValueTask<TQueryResult> Dispatch<TQueryResult>(object query, CancellationToken cancellationToken)
|
||||
=> DispatchQuery<TQueryResult>(query, cancellationToken);
|
||||
=> DispatchInternal<TQueryResult>(CreateDispatchQueryDelegate, query, cancellationToken);
|
||||
|
||||
public ValueTask<TQueryResult> Dispatch<TQueryResult>(IKnownQuery<TQueryResult> query, CancellationToken cancellationToken)
|
||||
=> 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])!;
|
||||
}
|
||||
=> DispatchInternal<TQueryResult>(CreateDispatchQueryDelegate, query, cancellationToken);
|
||||
|
||||
private ValueTask<TQueryResult> DispatchQueryImpl<TQuery, TQueryResult>(
|
||||
TQuery query,
|
||||
@@ -34,14 +32,8 @@ internal sealed class QueryDispatcherImpl(
|
||||
where TQuery : notnull
|
||||
{
|
||||
var handler = services.GetRequiredService<IQueryHandler<TQuery, TQueryResult>>();
|
||||
var pipeline = services.GetServices<IDispatchBehaviour<TQuery, TQueryResult>>();
|
||||
using var pipelineEnumerator = pipeline.GetEnumerator();
|
||||
var pipeline = services.GetServices<IDispatchBehavior<TQuery, TQueryResult>>();
|
||||
|
||||
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));
|
||||
return DispatchDelegateFactory(query, handler, pipeline, cancellationToken).Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.1;net8.0;net9.0</TargetFrameworks>
|
||||
<TargetFrameworks>netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -13,8 +13,12 @@
|
||||
<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 Condition="$(TargetFramework) == 'net10.0' Or $(TargetFramework) == 'netstandard2.1'">
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="$(TargetFramework) == 'net9.0'">
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="$(TargetFramework) == 'net8.0'">
|
||||
|
||||
@@ -6,6 +6,10 @@ namespace Cqrs.Tests.CommandDispatcherImplTests;
|
||||
|
||||
public class Dispatch
|
||||
{
|
||||
public abstract class TestCommandHandler : ICommandHandler<TestCommand, TestCommandResult>
|
||||
{
|
||||
public abstract ValueTask<TestCommandResult> Handle(TestCommand command, CancellationToken cancellation);
|
||||
}
|
||||
public class TestCommand : IKnownCommand<TestCommandResult> {}
|
||||
public class TestCommandResult {}
|
||||
|
||||
@@ -19,7 +23,7 @@ public class Dispatch
|
||||
var testCommand = new TestCommand();
|
||||
var testCommandResult = new TestCommandResult();
|
||||
|
||||
var commandHandler = Substitute.For<ICommandHandler<TestCommand, TestCommandResult>>();
|
||||
var commandHandler = Substitute.For<TestCommandHandler>();
|
||||
commandHandler.Handle(testCommand, CancellationToken.None).Returns(testCommandResult);
|
||||
|
||||
ServiceCollection serviceCollection =
|
||||
@@ -42,12 +46,12 @@ public class Dispatch
|
||||
await commandHandler.Received(1).Handle(testCommand, CancellationToken.None);
|
||||
}
|
||||
|
||||
public class TestOpenBehaviour<TRequest, TResponse> : IDispatchBehaviour<TRequest, TResponse>
|
||||
public class TestOpenBehavior<TRequest, TResponse> : IDispatchBehavior<TRequest, TResponse>
|
||||
where TRequest : notnull
|
||||
{
|
||||
private readonly Action<TRequest> _callback;
|
||||
|
||||
public TestOpenBehaviour(Action<TRequest> callback)
|
||||
public TestOpenBehavior(Action<TRequest> callback)
|
||||
{
|
||||
_callback = callback;
|
||||
}
|
||||
@@ -60,27 +64,27 @@ public class Dispatch
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WhenPipelineConfigured_ShouldCallAllBehavioursInOrder()
|
||||
public async Task WhenPipelineConfigured_ShouldCallAllBehaviorsInOrder()
|
||||
{
|
||||
// Given
|
||||
var testCommand = new TestCommand();
|
||||
var testCommandResult = new TestCommandResult();
|
||||
List<string> calls = [];
|
||||
|
||||
var commandHandler = Substitute.For<ICommandHandler<TestCommand, TestCommandResult>>();
|
||||
var commandHandler = Substitute.For<TestCommandHandler>();
|
||||
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>())
|
||||
var firstBehavior = Substitute.For<IDispatchBehavior<TestCommand, TestCommandResult>>();
|
||||
firstBehavior.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(args => ((DispatchFurtherDelegate<TestCommandResult>)args[1]).Invoke())
|
||||
.AndDoes(_ => calls.Add("firstBehaviour"));
|
||||
.AndDoes(_ => calls.Add("firstBehavior"));
|
||||
|
||||
var secondBehaviour = Substitute.For<IDispatchBehaviour<TestCommand, TestCommandResult>>();
|
||||
secondBehaviour.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>())
|
||||
var secondBehavior = Substitute.For<IDispatchBehavior<TestCommand, TestCommandResult>>();
|
||||
secondBehavior.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(args => ((DispatchFurtherDelegate<TestCommandResult>)args[1]).Invoke())
|
||||
.AndDoes(_ => calls.Add("secondBehaviour"));
|
||||
.AndDoes(_ => calls.Add("secondBehavior"));
|
||||
|
||||
ServiceCollection serviceCollection =
|
||||
[
|
||||
@@ -90,22 +94,22 @@ public class Dispatch
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
new ServiceDescriptor(
|
||||
typeof(IDispatchBehaviour<TestCommand, TestCommandResult>),
|
||||
(IServiceProvider _) => firstBehaviour,
|
||||
typeof(IDispatchBehavior<TestCommand, TestCommandResult>),
|
||||
(IServiceProvider _) => firstBehavior,
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
new ServiceDescriptor(
|
||||
typeof(IDispatchBehaviour<TestCommand, TestCommandResult>),
|
||||
(IServiceProvider _) => secondBehaviour,
|
||||
typeof(IDispatchBehavior<TestCommand, TestCommandResult>),
|
||||
(IServiceProvider _) => secondBehavior,
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
new ServiceDescriptor(
|
||||
typeof(IDispatchBehaviour<,>),
|
||||
typeof(TestOpenBehaviour<,>),
|
||||
typeof(IDispatchBehavior<,>),
|
||||
typeof(TestOpenBehavior<,>),
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
];
|
||||
serviceCollection.AddTransient<Action<TestCommand>>(_ => (TestCommand _) => calls.Add("thirdBehaviour"));
|
||||
serviceCollection.AddTransient<Action<TestCommand>>(_ => (TestCommand _) => calls.Add("thirdBehavior"));
|
||||
var services = serviceCollection.BuildServiceProvider();
|
||||
|
||||
var sut = new CommandDispatcherImpl(services, new ConcurrentMethodsCache());
|
||||
@@ -115,11 +119,11 @@ public class Dispatch
|
||||
|
||||
// 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 firstBehavior.Received(1).Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>());
|
||||
await secondBehavior.Received(1).Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>());
|
||||
await commandHandler.Received(1).Handle(testCommand, CancellationToken.None);
|
||||
|
||||
calls.ShouldBe(["firstBehaviour", "secondBehaviour", "thirdBehaviour", "commandHandler"]);
|
||||
calls.ShouldBe(["firstBehavior", "secondBehavior", "thirdBehavior", "commandHandler"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -131,20 +135,20 @@ public class Dispatch
|
||||
var testCommandResultAborted = new TestCommandResult();
|
||||
List<string> calls = [];
|
||||
|
||||
var commandHandler = Substitute.For<ICommandHandler<TestCommand, TestCommandResult>>();
|
||||
var commandHandler = Substitute.For<TestCommandHandler>();
|
||||
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>())
|
||||
var firstBehavior = Substitute.For<IDispatchBehavior<TestCommand, TestCommandResult>>();
|
||||
firstBehavior.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(args => ((DispatchFurtherDelegate<TestCommandResult>)args[1]).Invoke())
|
||||
.AndDoes(_ => calls.Add("firstBehaviour"));
|
||||
.AndDoes(_ => calls.Add("firstBehavior"));
|
||||
|
||||
var secondBehaviour = Substitute.For<IDispatchBehaviour<TestCommand, TestCommandResult>>();
|
||||
secondBehaviour.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>())
|
||||
var secondBehavior = Substitute.For<IDispatchBehavior<TestCommand, TestCommandResult>>();
|
||||
secondBehavior.Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(args => ValueTask.FromResult(testCommandResultAborted))
|
||||
.AndDoes(_ => calls.Add("secondBehaviour"));
|
||||
.AndDoes(_ => calls.Add("secondBehavior"));
|
||||
|
||||
ServiceCollection serviceCollection =
|
||||
[
|
||||
@@ -154,22 +158,22 @@ public class Dispatch
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
new ServiceDescriptor(
|
||||
typeof(IDispatchBehaviour<TestCommand, TestCommandResult>),
|
||||
(IServiceProvider _) => firstBehaviour,
|
||||
typeof(IDispatchBehavior<TestCommand, TestCommandResult>),
|
||||
(IServiceProvider _) => firstBehavior,
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
new ServiceDescriptor(
|
||||
typeof(IDispatchBehaviour<TestCommand, TestCommandResult>),
|
||||
(IServiceProvider _) => secondBehaviour,
|
||||
typeof(IDispatchBehavior<TestCommand, TestCommandResult>),
|
||||
(IServiceProvider _) => secondBehavior,
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
new ServiceDescriptor(
|
||||
typeof(IDispatchBehaviour<,>),
|
||||
typeof(TestOpenBehaviour<,>),
|
||||
typeof(IDispatchBehavior<,>),
|
||||
typeof(TestOpenBehavior<,>),
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
];
|
||||
serviceCollection.AddTransient<Action<TestCommand>>(_ => (TestCommand _) => calls.Add("thirdBehaviour"));
|
||||
serviceCollection.AddTransient<Action<TestCommand>>(_ => (TestCommand _) => calls.Add("thirdBehavior"));
|
||||
var services = serviceCollection.BuildServiceProvider();
|
||||
|
||||
var sut = new CommandDispatcherImpl(services, new ConcurrentMethodsCache());
|
||||
@@ -179,10 +183,59 @@ public class Dispatch
|
||||
|
||||
// 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 firstBehavior.Received(1).Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>());
|
||||
await secondBehavior.Received(1).Handle(testCommand, Arg.Any<DispatchFurtherDelegate<TestCommandResult>>(), Arg.Any<CancellationToken>());
|
||||
await commandHandler.Received(0).Handle(testCommand, CancellationToken.None);
|
||||
|
||||
calls.ShouldBe(["firstBehaviour", "secondBehaviour"]);
|
||||
calls.ShouldBe(["firstBehavior", "secondBehavior"]);
|
||||
}
|
||||
|
||||
public abstract class AnotherTestCommandHandler : ICommandHandler<TestCommand, AnotherTestCommandResult>
|
||||
{
|
||||
public abstract ValueTask<AnotherTestCommandResult> Handle(TestCommand command, CancellationToken cancellation);
|
||||
}
|
||||
public class AnotherTestCommandResult {}
|
||||
|
||||
[Fact]
|
||||
public async Task WhenTwoHandlersWithDifferentResultTypesRegisteredForOneCommandType_ShouldCorrectlyDispatchToBoth() // Fix to Cache Key Collision
|
||||
{
|
||||
// Given
|
||||
var testCommand = new TestCommand();
|
||||
var testCommandResult = new TestCommandResult();
|
||||
var anotherTestCommandResult = new AnotherTestCommandResult();
|
||||
|
||||
var commandHandler = Substitute.For<TestCommandHandler>();
|
||||
commandHandler.Handle(testCommand, CancellationToken.None).Returns(testCommandResult);
|
||||
|
||||
var anotherCommandHandler = Substitute.For<AnotherTestCommandHandler>();
|
||||
anotherCommandHandler.Handle(testCommand, CancellationToken.None).Returns(anotherTestCommandResult);
|
||||
|
||||
ServiceCollection serviceCollection =
|
||||
[
|
||||
new ServiceDescriptor(
|
||||
typeof(ICommandHandler<TestCommand, TestCommandResult>),
|
||||
(IServiceProvider _) => commandHandler,
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
new ServiceDescriptor(
|
||||
typeof(ICommandHandler<TestCommand, AnotherTestCommandResult>),
|
||||
(IServiceProvider _) => anotherCommandHandler,
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
];
|
||||
var services = serviceCollection.BuildServiceProvider();
|
||||
|
||||
var sut = new CommandDispatcherImpl(services, new ConcurrentMethodsCache());
|
||||
|
||||
// When
|
||||
var result = await sut.Dispatch(testCommand, CancellationToken.None);
|
||||
var anotherResult = await sut.Dispatch<AnotherTestCommandResult>(testCommand, CancellationToken.None);
|
||||
|
||||
// Then
|
||||
result.ShouldBeSameAs(testCommandResult);
|
||||
await commandHandler.Received(1).Handle(testCommand, CancellationToken.None);
|
||||
|
||||
anotherResult.ShouldBeSameAs(anotherTestCommandResult);
|
||||
await anotherCommandHandler.Received(1).Handle(testCommand, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<OutputType>Exe</OutputType>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</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="xunit.v3" Version="3.2.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="NSubstitute.Analyzers.CSharp" Version="1.0.17">
|
||||
@@ -20,8 +23,12 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="$(TargetFramework) == 'net9.0' Or $(TargetFramework) == 'netstandard2.1'">
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
|
||||
<ItemGroup Condition="$(TargetFramework) == 'net10.0' Or $(TargetFramework) == 'netstandard2.1'">
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="$(TargetFramework) == 'net9.0'">
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="$(TargetFramework) == 'net8.0'">
|
||||
|
||||
@@ -3,12 +3,12 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Cqrs.Tests.CqrsServicesExtensionsTests;
|
||||
|
||||
public class AddBehaviour
|
||||
public class AddBehavior
|
||||
{
|
||||
public class TestCommand {}
|
||||
public class TestCommandResult {}
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class NonGenericTestOpenBehaviour : IDispatchBehaviour<TestCommand, TestCommandResult>
|
||||
public class NonGenericTestOpenBehavior : IDispatchBehavior<TestCommand, TestCommandResult>
|
||||
{
|
||||
public ValueTask<TestCommandResult> Handle(TestCommand request, DispatchFurtherDelegate<TestCommandResult> next, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -20,27 +20,27 @@ public class AddBehaviour
|
||||
[InlineData(ServiceLifetime.Transient)]
|
||||
[InlineData(ServiceLifetime.Scoped)]
|
||||
[InlineData(ServiceLifetime.Singleton)]
|
||||
public void WhenCalled_ShouldRegisterDispatchBehaviour(ServiceLifetime lifetime)
|
||||
public void WhenCalled_ShouldRegisterDispatchBehavior(ServiceLifetime lifetime)
|
||||
{
|
||||
// Given
|
||||
ServiceCollection services = new();
|
||||
|
||||
// When
|
||||
services.AddCqrs(opt => opt
|
||||
.AddBehaviour<NonGenericTestOpenBehaviour>(lifetime));
|
||||
.AddBehavior<NonGenericTestOpenBehavior>(lifetime));
|
||||
|
||||
// Then
|
||||
services.ShouldContain(
|
||||
elementPredicate: descriptor =>
|
||||
descriptor.ServiceType == typeof(IDispatchBehaviour<TestCommand, TestCommandResult>)
|
||||
&& descriptor.ImplementationType == typeof(NonGenericTestOpenBehaviour)
|
||||
descriptor.ServiceType == typeof(IDispatchBehavior<TestCommand, TestCommandResult>)
|
||||
&& descriptor.ImplementationType == typeof(NonGenericTestOpenBehavior)
|
||||
&& descriptor.Lifetime == lifetime,
|
||||
expectedCount: 1
|
||||
);
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class InvalidTestBehaviour : IDispatchBehaviour
|
||||
public class InvalidTestBehavior : IDispatchBehavior
|
||||
{
|
||||
public Type RequestType => throw new NotImplementedException();
|
||||
|
||||
@@ -57,7 +57,7 @@ public class AddBehaviour
|
||||
|
||||
// Then
|
||||
Should.Throw<InvalidOperationException>(() => services.AddCqrs(opt => opt
|
||||
.AddBehaviour<InvalidTestBehaviour>())
|
||||
.AddBehavior<InvalidTestBehavior>())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,10 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Cqrs.Tests.CqrsServicesExtensionsTests;
|
||||
|
||||
public class AddOpenBehaviour
|
||||
public class AddOpenBehavior
|
||||
{
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class TestOpenBehaviour<TRequest, TResponse> : IDispatchBehaviour<TRequest, TResponse>
|
||||
public class TestOpenBehavior<TRequest, TResponse> : IDispatchBehavior<TRequest, TResponse>
|
||||
where TRequest: notnull
|
||||
{
|
||||
public ValueTask<TResponse> Handle(TRequest request, DispatchFurtherDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||
@@ -19,27 +19,27 @@ public class AddOpenBehaviour
|
||||
[InlineData(ServiceLifetime.Transient)]
|
||||
[InlineData(ServiceLifetime.Scoped)]
|
||||
[InlineData(ServiceLifetime.Singleton)]
|
||||
public void WhenCalled_ShouldRegisterOpenDispatchBehaviour(ServiceLifetime lifetime)
|
||||
public void WhenCalled_ShouldRegisterOpenDispatchBehavior(ServiceLifetime lifetime)
|
||||
{
|
||||
// Given
|
||||
ServiceCollection services = new();
|
||||
|
||||
// When
|
||||
services.AddCqrs(opt => opt
|
||||
.AddOpenBehaviour(typeof(TestOpenBehaviour<,>), lifetime));
|
||||
.AddOpenBehavior(typeof(TestOpenBehavior<,>), lifetime));
|
||||
|
||||
// Then
|
||||
services.ShouldContain(
|
||||
elementPredicate: descriptor =>
|
||||
descriptor.ServiceType == typeof(IDispatchBehaviour<,>)
|
||||
&& descriptor.ImplementationType == typeof(TestOpenBehaviour<,>)
|
||||
descriptor.ServiceType == typeof(IDispatchBehavior<,>)
|
||||
&& descriptor.ImplementationType == typeof(TestOpenBehavior<,>)
|
||||
&& descriptor.Lifetime == lifetime,
|
||||
expectedCount: 1
|
||||
);
|
||||
}
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class InvalidOpenBehaviour : IDispatchBehaviour
|
||||
public class InvalidOpenBehavior : IDispatchBehavior
|
||||
{
|
||||
public Type RequestType => throw new NotImplementedException();
|
||||
|
||||
@@ -53,18 +53,18 @@ public class AddOpenBehaviour
|
||||
ServiceCollection services = new();
|
||||
|
||||
// When
|
||||
var invalidOpenDispatchBehaviourType = typeof(InvalidOpenBehaviour);
|
||||
var invalidOpenDispatchBehaviorType = typeof(InvalidOpenBehavior);
|
||||
|
||||
// Then
|
||||
Should.Throw<ArgumentException>(() => services.AddCqrs(opt => opt
|
||||
.AddOpenBehaviour(invalidOpenDispatchBehaviourType))
|
||||
.AddOpenBehavior(invalidOpenDispatchBehaviorType))
|
||||
);
|
||||
}
|
||||
|
||||
public class TestCommand {}
|
||||
public class TestCommandResult {}
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class NonGenericTestOpenBehaviour : IDispatchBehaviour<TestCommand, TestCommandResult>
|
||||
public class NonGenericTestOpenBehavior : IDispatchBehavior<TestCommand, TestCommandResult>
|
||||
{
|
||||
public ValueTask<TestCommandResult> Handle(TestCommand request, DispatchFurtherDelegate<TestCommandResult> next, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -79,11 +79,11 @@ public class AddOpenBehaviour
|
||||
ServiceCollection services = new();
|
||||
|
||||
// When
|
||||
var nonGenericOpenDispatchBehaviourType = typeof(NonGenericTestOpenBehaviour);
|
||||
var nonGenericOpenDispatchBehaviorType = typeof(NonGenericTestOpenBehavior);
|
||||
|
||||
// Then
|
||||
Should.Throw<ArgumentException>(() => services.AddCqrs(opt => opt
|
||||
.AddOpenBehaviour(nonGenericOpenDispatchBehaviourType))
|
||||
.AddOpenBehavior(nonGenericOpenDispatchBehaviorType))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,10 @@ namespace Cqrs.Tests.QueryDispatcherImplTests;
|
||||
|
||||
public class Dispatch
|
||||
{
|
||||
public abstract class TestQueryHandler : IQueryHandler<TestQuery, TestQueryResult>
|
||||
{
|
||||
public abstract ValueTask<TestQueryResult> Handle(TestQuery query, CancellationToken cancellation);
|
||||
}
|
||||
public class TestQuery : IKnownQuery<TestQueryResult> {}
|
||||
public class TestQueryResult {}
|
||||
|
||||
@@ -19,7 +23,7 @@ public class Dispatch
|
||||
var testQuery = new TestQuery();
|
||||
var testQueryResult = new TestQueryResult();
|
||||
|
||||
var queryHandler = Substitute.For<IQueryHandler<TestQuery, TestQueryResult>>();
|
||||
var queryHandler = Substitute.For<TestQueryHandler>();
|
||||
queryHandler.Handle(testQuery, CancellationToken.None).Returns(testQueryResult);
|
||||
|
||||
ServiceCollection serviceCollection =
|
||||
@@ -42,12 +46,12 @@ public class Dispatch
|
||||
await queryHandler.Received(1).Handle(testQuery, CancellationToken.None);
|
||||
}
|
||||
|
||||
public class TestOpenBehaviour<TRequest, TResponse> : IDispatchBehaviour<TRequest, TResponse>
|
||||
public class TestOpenBehavior<TRequest, TResponse> : IDispatchBehavior<TRequest, TResponse>
|
||||
where TRequest : notnull
|
||||
{
|
||||
private readonly Action<TRequest> _callback;
|
||||
|
||||
public TestOpenBehaviour(Action<TRequest> callback)
|
||||
public TestOpenBehavior(Action<TRequest> callback)
|
||||
{
|
||||
_callback = callback;
|
||||
}
|
||||
@@ -60,27 +64,27 @@ public class Dispatch
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WhenPipelineConfigured_ShouldCallAllBehavioursInOrder()
|
||||
public async Task WhenPipelineConfigured_ShouldCallAllBehaviorsInOrder()
|
||||
{
|
||||
// Given
|
||||
var testQuery = new TestQuery();
|
||||
var testQueryResult = new TestQueryResult();
|
||||
List<string> calls = [];
|
||||
|
||||
var queryHandler = Substitute.For<IQueryHandler<TestQuery, TestQueryResult>>();
|
||||
var queryHandler = Substitute.For<TestQueryHandler>();
|
||||
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>())
|
||||
var firstBehavior = Substitute.For<IDispatchBehavior<TestQuery, TestQueryResult>>();
|
||||
firstBehavior.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(args => ((DispatchFurtherDelegate<TestQueryResult>)args[1]).Invoke())
|
||||
.AndDoes(_ => calls.Add("firstBehaviour"));
|
||||
.AndDoes(_ => calls.Add("firstBehavior"));
|
||||
|
||||
var secondBehaviour = Substitute.For<IDispatchBehaviour<TestQuery, TestQueryResult>>();
|
||||
secondBehaviour.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>())
|
||||
var secondBehavior = Substitute.For<IDispatchBehavior<TestQuery, TestQueryResult>>();
|
||||
secondBehavior.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(args => ((DispatchFurtherDelegate<TestQueryResult>)args[1]).Invoke())
|
||||
.AndDoes(_ => calls.Add("secondBehaviour"));
|
||||
.AndDoes(_ => calls.Add("secondBehavior"));
|
||||
|
||||
ServiceCollection serviceCollection =
|
||||
[
|
||||
@@ -90,22 +94,22 @@ public class Dispatch
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
new ServiceDescriptor(
|
||||
typeof(IDispatchBehaviour<TestQuery, TestQueryResult>),
|
||||
(IServiceProvider _) => firstBehaviour,
|
||||
typeof(IDispatchBehavior<TestQuery, TestQueryResult>),
|
||||
(IServiceProvider _) => firstBehavior,
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
new ServiceDescriptor(
|
||||
typeof(IDispatchBehaviour<TestQuery, TestQueryResult>),
|
||||
(IServiceProvider _) => secondBehaviour,
|
||||
typeof(IDispatchBehavior<TestQuery, TestQueryResult>),
|
||||
(IServiceProvider _) => secondBehavior,
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
new ServiceDescriptor(
|
||||
typeof(IDispatchBehaviour<,>),
|
||||
typeof(TestOpenBehaviour<,>),
|
||||
typeof(IDispatchBehavior<,>),
|
||||
typeof(TestOpenBehavior<,>),
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
];
|
||||
serviceCollection.AddTransient<Action<TestQuery>>(_ => (TestQuery _) => calls.Add("thirdBehaviour"));
|
||||
serviceCollection.AddTransient<Action<TestQuery>>(_ => (TestQuery _) => calls.Add("thirdBehavior"));
|
||||
var services = serviceCollection.BuildServiceProvider();
|
||||
|
||||
var sut = new QueryDispatcherImpl(services, new ConcurrentMethodsCache());
|
||||
@@ -115,11 +119,11 @@ public class Dispatch
|
||||
|
||||
// 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 firstBehavior.Received(1).Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>());
|
||||
await secondBehavior.Received(1).Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>());
|
||||
await queryHandler.Received(1).Handle(testQuery, CancellationToken.None);
|
||||
|
||||
calls.ShouldBe(["firstBehaviour", "secondBehaviour", "thirdBehaviour", "queryHandler"]);
|
||||
calls.ShouldBe(["firstBehavior", "secondBehavior", "thirdBehavior", "queryHandler"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -131,20 +135,20 @@ public class Dispatch
|
||||
var testQueryResultAborted = new TestQueryResult();
|
||||
List<string> calls = [];
|
||||
|
||||
var queryHandler = Substitute.For<IQueryHandler<TestQuery, TestQueryResult>>();
|
||||
var queryHandler = Substitute.For<TestQueryHandler>();
|
||||
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>())
|
||||
var firstBehavior = Substitute.For<IDispatchBehavior<TestQuery, TestQueryResult>>();
|
||||
firstBehavior.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(args => ((DispatchFurtherDelegate<TestQueryResult>)args[1]).Invoke())
|
||||
.AndDoes(_ => calls.Add("firstBehaviour"));
|
||||
.AndDoes(_ => calls.Add("firstBehavior"));
|
||||
|
||||
var secondBehaviour = Substitute.For<IDispatchBehaviour<TestQuery, TestQueryResult>>();
|
||||
secondBehaviour.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>())
|
||||
var secondBehavior = Substitute.For<IDispatchBehavior<TestQuery, TestQueryResult>>();
|
||||
secondBehavior.Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(args => ValueTask.FromResult(testQueryResultAborted))
|
||||
.AndDoes(_ => calls.Add("secondBehaviour"));
|
||||
.AndDoes(_ => calls.Add("secondBehavior"));
|
||||
|
||||
ServiceCollection serviceCollection =
|
||||
[
|
||||
@@ -154,22 +158,22 @@ public class Dispatch
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
new ServiceDescriptor(
|
||||
typeof(IDispatchBehaviour<TestQuery, TestQueryResult>),
|
||||
(IServiceProvider _) => firstBehaviour,
|
||||
typeof(IDispatchBehavior<TestQuery, TestQueryResult>),
|
||||
(IServiceProvider _) => firstBehavior,
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
new ServiceDescriptor(
|
||||
typeof(IDispatchBehaviour<TestQuery, TestQueryResult>),
|
||||
(IServiceProvider _) => secondBehaviour,
|
||||
typeof(IDispatchBehavior<TestQuery, TestQueryResult>),
|
||||
(IServiceProvider _) => secondBehavior,
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
new ServiceDescriptor(
|
||||
typeof(IDispatchBehaviour<,>),
|
||||
typeof(TestOpenBehaviour<,>),
|
||||
typeof(IDispatchBehavior<,>),
|
||||
typeof(TestOpenBehavior<,>),
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
];
|
||||
serviceCollection.AddTransient<Action<TestQuery>>(_ => (TestQuery _) => calls.Add("thirdBehaviour"));
|
||||
serviceCollection.AddTransient<Action<TestQuery>>(_ => (TestQuery _) => calls.Add("thirdBehavior"));
|
||||
var services = serviceCollection.BuildServiceProvider();
|
||||
|
||||
var sut = new QueryDispatcherImpl(services, new ConcurrentMethodsCache());
|
||||
@@ -179,10 +183,59 @@ public class Dispatch
|
||||
|
||||
// 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 firstBehavior.Received(1).Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>());
|
||||
await secondBehavior.Received(1).Handle(testQuery, Arg.Any<DispatchFurtherDelegate<TestQueryResult>>(), Arg.Any<CancellationToken>());
|
||||
await queryHandler.Received(0).Handle(testQuery, CancellationToken.None);
|
||||
|
||||
calls.ShouldBe(["firstBehaviour", "secondBehaviour"]);
|
||||
calls.ShouldBe(["firstBehavior", "secondBehavior"]);
|
||||
}
|
||||
|
||||
public abstract class AnotherTestQueryHandler : IQueryHandler<TestQuery, AnotherTestQueryResult>
|
||||
{
|
||||
public abstract ValueTask<AnotherTestQueryResult> Handle(TestQuery query, CancellationToken cancellation);
|
||||
}
|
||||
public class AnotherTestQueryResult {}
|
||||
|
||||
[Fact]
|
||||
public async Task WhenTwoHandlersWithDifferentResultTypesRegisteredForOneQueryType_ShouldCorrectlyDispatchToBoth() // Fix to Cache Key Collision
|
||||
{
|
||||
// Given
|
||||
var testQuery = new TestQuery();
|
||||
var testQueryResult = new TestQueryResult();
|
||||
var anotherTestQueryResult = new AnotherTestQueryResult();
|
||||
|
||||
var queryHandler = Substitute.For<TestQueryHandler>();
|
||||
queryHandler.Handle(testQuery, CancellationToken.None).Returns(testQueryResult);
|
||||
|
||||
var anotherQueryHandler = Substitute.For<AnotherTestQueryHandler>();
|
||||
anotherQueryHandler.Handle(testQuery, CancellationToken.None).Returns(anotherTestQueryResult);
|
||||
|
||||
ServiceCollection serviceCollection =
|
||||
[
|
||||
new ServiceDescriptor(
|
||||
typeof(IQueryHandler<TestQuery, TestQueryResult>),
|
||||
(IServiceProvider _) => queryHandler,
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
new ServiceDescriptor(
|
||||
typeof(IQueryHandler<TestQuery, AnotherTestQueryResult>),
|
||||
(IServiceProvider _) => anotherQueryHandler,
|
||||
ServiceLifetime.Transient
|
||||
),
|
||||
];
|
||||
var services = serviceCollection.BuildServiceProvider();
|
||||
|
||||
var sut = new QueryDispatcherImpl(services, new ConcurrentMethodsCache());
|
||||
|
||||
// When
|
||||
var result = await sut.Dispatch(testQuery, CancellationToken.None);
|
||||
var anotherResult = await sut.Dispatch<AnotherTestQueryResult>(testQuery, CancellationToken.None);
|
||||
|
||||
// Then
|
||||
result.ShouldBeSameAs(testQueryResult);
|
||||
await queryHandler.Received(1).Handle(testQuery, CancellationToken.None);
|
||||
|
||||
anotherResult.ShouldBeSameAs(anotherTestQueryResult);
|
||||
await anotherQueryHandler.Received(1).Handle(testQuery, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user