Compare commits
1 Commits
7c3dd84971
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fded2809f |
@@ -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}]
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -75,23 +79,23 @@ public static class CqrsServicesExtensions
|
||||
return options;
|
||||
}
|
||||
|
||||
public static CqrsServicesOptions AddOpenBehavior(this CqrsServicesOptions options, Type Behavior, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
public static CqrsServicesOptions AddOpenBehavior(this CqrsServicesOptions options, Type behavior, ServiceLifetime lifetime = ServiceLifetime.Singleton)
|
||||
{
|
||||
var interfaces = Behavior.FindInterfaces(
|
||||
var interfaces = behavior.FindInterfaces(
|
||||
static (x, t) => x.IsGenericType && x.GetGenericTypeDefinition() == (Type)t!,
|
||||
typeof(IDispatchBehavior<,>));
|
||||
|
||||
if (interfaces.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("Supplied type does not implement IDispatchBehavior<,> interface.", nameof(Behavior));
|
||||
throw new ArgumentException("Supplied type does not implement IDispatchBehavior<,> interface.", nameof(behavior));
|
||||
}
|
||||
|
||||
if (!Behavior.ContainsGenericParameters)
|
||||
if (!behavior.ContainsGenericParameters)
|
||||
{
|
||||
throw new ArgumentException("Supplied type is not sutable for open Behavior.", nameof(Behavior));
|
||||
throw new ArgumentException("Supplied type is not suitable for open Behavior.", nameof(behavior));
|
||||
}
|
||||
|
||||
options.Behaviors.Add((typeof(IDispatchBehavior<,>), Behavior, lifetime));
|
||||
options.Behaviors.Add((typeof(IDispatchBehavior<,>), behavior, lifetime));
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -35,13 +33,7 @@ internal sealed class CommandDispatcherImpl(
|
||||
{
|
||||
var handler = services.GetRequiredService<ICommandHandler<TCommand, TCommandResult>>();
|
||||
var pipeline = services.GetServices<IDispatchBehavior<TCommand, TCommandResult>>();
|
||||
using var pipelineEnumerator = pipeline.GetEnumerator();
|
||||
|
||||
return DispatchDelegateFactory(pipelineEnumerator).Invoke();
|
||||
|
||||
DispatchFurtherDelegate<TCommandResult> DispatchDelegateFactory(IEnumerator<IDispatchBehavior<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,
|
||||
@@ -35,13 +33,7 @@ internal sealed class QueryDispatcherImpl(
|
||||
{
|
||||
var handler = services.GetRequiredService<IQueryHandler<TQuery, TQueryResult>>();
|
||||
var pipeline = services.GetServices<IDispatchBehavior<TQuery, TQueryResult>>();
|
||||
using var pipelineEnumerator = pipeline.GetEnumerator();
|
||||
|
||||
return DispatchDelegateFactory(pipelineEnumerator).Invoke();
|
||||
|
||||
DispatchFurtherDelegate<TQueryResult> DispatchDelegateFactory(IEnumerator<IDispatchBehavior<TQuery, TQueryResult>> enumerator) =>
|
||||
enumerator.MoveNext()
|
||||
? (() => enumerator.Current.Handle(query, DispatchDelegateFactory(enumerator), cancellationToken))
|
||||
: (() => handler.Handle(query, cancellationToken));
|
||||
return DispatchDelegateFactory(query, handler, pipeline, cancellationToken).Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
@@ -67,7 +71,7 @@ public class Dispatch
|
||||
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"));
|
||||
@@ -131,7 +135,7 @@ 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"));
|
||||
@@ -185,4 +189,53 @@ public class Dispatch
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
@@ -67,7 +71,7 @@ public class Dispatch
|
||||
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"));
|
||||
@@ -131,7 +135,7 @@ 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"));
|
||||
@@ -185,4 +189,53 @@ public class Dispatch
|
||||
|
||||
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