Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure that ObjectExecutorFactory resolves the handler that was used to generate metadata #639

Merged
merged 3 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions src/CQRS/LeanCode.CQRS.AspNetCore/ObjectExecutorFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@ public ObjectExecutor CreateExecutorFor(CQRSObjectMetadata @object)
return @object.ObjectKind switch
{
CQRSObjectKind.Command
=> ExecuteCommandMethod.MakeGenericMethod(@object.ObjectType).CreateDelegate<ObjectExecutor>(),
=> ExecuteCommandMethod
.MakeGenericMethod(@object.ObjectType, @object.HandlerType)
.CreateDelegate<ObjectExecutor>(),
CQRSObjectKind.Query
=> ExecuteQueryMethod
.MakeGenericMethod(@object.ObjectType, GetResultType(typeof(IQuery<>)))
.MakeGenericMethod(@object.ObjectType, GetResultType(typeof(IQuery<>)), @object.HandlerType)
.CreateDelegate<ObjectExecutor>(),
CQRSObjectKind.Operation
=> ExecuteOperationMethod
.MakeGenericMethod(@object.ObjectType, GetResultType(typeof(IOperation<>)))
.MakeGenericMethod(@object.ObjectType, GetResultType(typeof(IOperation<>)), @object.HandlerType)
.CreateDelegate<ObjectExecutor>(),
_ => throw new InvalidOperationException($"Unexpected object kind: {@object.ObjectKind}")
};
Expand All @@ -54,44 +56,47 @@ Type GetResultType(Type interfaceType) =>
.First();
}

private static async Task<object?> ExecuteOperationAsync<TOperation, TResult>(
private static async Task<object?> ExecuteOperationAsync<TOperation, TResult, THandler>(
HttpContext httpContext,
CQRSRequestPayload payload
)
where TOperation : IOperation<TResult>
where THandler : IOperationHandler<TOperation, TResult>
{
var operation = (TOperation)payload.Payload;

var handler =
httpContext.RequestServices.GetService<IOperationHandler<TOperation, TResult>>()
httpContext.RequestServices.GetService<THandler>()
?? throw new OperationHandlerNotFoundException(typeof(TOperation));
return await handler.ExecuteAsync(httpContext, operation);
}

private static async Task<object?> ExecuteQueryAsync<TQuery, TResult>(
private static async Task<object?> ExecuteQueryAsync<TQuery, TResult, THandler>(
HttpContext httpContext,
CQRSRequestPayload payload
)
where TQuery : IQuery<TResult>
where THandler : IQueryHandler<TQuery, TResult>
{
var query = (TQuery)payload.Payload;

var handler =
httpContext.RequestServices.GetService<IQueryHandler<TQuery, TResult>>()
httpContext.RequestServices.GetService<THandler>()
?? throw new QueryHandlerNotFoundException(typeof(TQuery));
return await handler.ExecuteAsync(httpContext, query);
}

private static async Task<object?> ExecuteCommandAsync<TCommand>(
private static async Task<object?> ExecuteCommandAsync<TCommand, THandler>(
HttpContext httpContext,
CQRSRequestPayload payload
)
where TCommand : ICommand
where THandler : ICommandHandler<TCommand>
{
var command = (TCommand)payload.Payload;

var handler =
httpContext.RequestServices.GetService<ICommandHandler<TCommand>>()
httpContext.RequestServices.GetService<THandler>()
?? throw new CommandHandlerNotFoundException(typeof(TCommand));
await handler.ExecuteAsync(httpContext, command);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ IEnumerable<CQRSObjectMetadata> cqrsObjects

public static void AddCQRSHandler(this IServiceCollection serviceCollection, CQRSObjectMetadata obj)
{
serviceCollection.Add(
new ServiceDescriptor(MakeHandlerInterfaceType(obj), obj.HandlerType, ServiceLifetime.Scoped)
);
serviceCollection.Add(new(MakeHandlerInterfaceType(obj), obj.HandlerType, ServiceLifetime.Scoped));
serviceCollection.Add(new(obj.HandlerType, obj.HandlerType, ServiceLifetime.Scoped));

Type MakeHandlerInterfaceType(CQRSObjectMetadata obj)
{
Expand Down
127 changes: 90 additions & 37 deletions test/CQRS/LeanCode.CQRS.AspNetCore.Tests/ObjectExecutorFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using FluentAssertions;
using LeanCode.Contracts;
using LeanCode.CQRS.Execution;
using Microsoft.AspNetCore.Http;
Expand All @@ -10,32 +11,27 @@ namespace LeanCode.CQRS.AspNetCore.Tests;
[SuppressMessage(category: "?", "CA1034", Justification = "Nesting public types for better tests separation")]
public class ObjectExecutorFactoryTests
{
private readonly ObjectExecutorFactory executorFactory;
private readonly IServiceProvider serviceProvider;
private readonly ICommandHandler<Command> commandHandler;
private readonly IQueryHandler<Query, QueryResult> queryHandler;
private readonly IOperationHandler<Operation, OperationResult> operationHandler;
private readonly ObjectExecutorFactory executorFactory = new ObjectExecutorFactory();
private readonly IServiceProvider serviceProvider = Substitute.For<IServiceProvider>();

private readonly CommandHandler commandHandler = Substitute.For<CommandHandler>();
private readonly QueryHandler queryHandler = Substitute.For<QueryHandler>();
private readonly OperationHandler operationHandler = Substitute.For<OperationHandler>();

private readonly CQRSObjectMetadata queryMetadata =
new(CQRSObjectKind.Query, typeof(Query), typeof(QueryResult), typeof(IgnoreType));
new(CQRSObjectKind.Query, typeof(Query), typeof(QueryResult), typeof(QueryHandler));

private readonly CQRSObjectMetadata commandMetadata =
new(CQRSObjectKind.Command, typeof(Command), typeof(CommandResult), typeof(IgnoreType));
new(CQRSObjectKind.Command, typeof(Command), typeof(CommandResult), typeof(CommandHandler));

private readonly CQRSObjectMetadata operationMetadata =
new(CQRSObjectKind.Operation, typeof(Operation), typeof(OperationResult), typeof(IgnoreType));
new(CQRSObjectKind.Operation, typeof(Operation), typeof(OperationResult), typeof(OperationHandler));

public ObjectExecutorFactoryTests()
{
executorFactory = new ObjectExecutorFactory();
serviceProvider = Substitute.For<IServiceProvider>();
commandHandler = Substitute.For<ICommandHandler<Command>>();
queryHandler = Substitute.For<IQueryHandler<Query, QueryResult>>();
operationHandler = Substitute.For<IOperationHandler<Operation, OperationResult>>();

serviceProvider.GetService(typeof(ICommandHandler<Command>)).Returns(commandHandler);
serviceProvider.GetService(typeof(IQueryHandler<Query, QueryResult>)).Returns(queryHandler);
serviceProvider.GetService(typeof(IOperationHandler<Operation, OperationResult>)).Returns(operationHandler);
serviceProvider.GetService(typeof(CommandHandler)).Returns(commandHandler);
serviceProvider.GetService(typeof(QueryHandler)).Returns(queryHandler);
serviceProvider.GetService(typeof(OperationHandler)).Returns(operationHandler);
}

[Fact]
Expand All @@ -48,8 +44,23 @@ public async Task Creates_executor_for_command_which_passes_payload_to_command_h
var result = await executorMethod(ctx, new CQRSRequestPayload(cmd));

await commandHandler.Received().ExecuteAsync(ctx, cmd);
var commandResult = Assert.IsType<CommandResult>(result);
Assert.True(commandResult.WasSuccessful);
result.Should().BeOfType<CommandResult>().Which.WasSuccessful.Should().BeTrue();
}

[Fact]
public async Task Resolves_the_exact_command_handler_from_DI()
{
var cmd = new Command();
var ctx = MockHttpContext();

serviceProvider
.GetService(typeof(ICommandHandler<Command>))
.Returns(Substitute.For<ICommandHandler<Command>>());

var executorMethod = executorFactory.CreateExecutorFor(commandMetadata);
await executorMethod(ctx, new CQRSRequestPayload(cmd));

await commandHandler.Received().ExecuteAsync(ctx, cmd);
}

[Fact]
Expand All @@ -65,8 +76,23 @@ public async Task Creates_query_handler_which_passes_payload_to_query_handler()
var result = await executorMethod(ctx, new CQRSRequestPayload(query));

await queryHandler.Received().ExecuteAsync(ctx, query);
var queryResult = Assert.IsType<QueryResult>(result);
Assert.Same(returnedResult, queryResult);
result.Should().BeOfType<QueryResult>().Which.Should().BeSameAs(returnedResult);
}

[Fact]
public async Task Resolves_the_exact_query_handler_from_DI()
{
var query = new Query();
var ctx = MockHttpContext();

serviceProvider
.GetService(typeof(IQueryHandler<Query, QueryResult>))
.Returns(Substitute.For<IQueryHandler<Query, QueryResult>>());

var executorMethod = executorFactory.CreateExecutorFor(queryMetadata);
await executorMethod(ctx, new CQRSRequestPayload(query));

await queryHandler.Received().ExecuteAsync(ctx, Arg.Any<Query>());
}

[Fact]
Expand All @@ -81,8 +107,23 @@ public async Task Creates_operation_handler_which_passes_payload_to_operation_ha
var result = await executorMethod(ctx, new CQRSRequestPayload(operation));

await operationHandler.Received().ExecuteAsync(ctx, operation);
var operationResult = Assert.IsType<OperationResult>(result);
Assert.Same(returnedResult, operationResult);
result.Should().BeOfType<OperationResult>().Which.Should().BeSameAs(returnedResult);
}

[Fact]
public async Task Resolves_the_exact_operation_handler_from_DI()
{
var operation = new Operation();
var ctx = MockHttpContext();

serviceProvider
.GetService(typeof(IOperationHandler<Operation, OperationResult>))
.Returns(Substitute.For<IOperationHandler<Operation, OperationResult>>());

var executorMethod = executorFactory.CreateExecutorFor(operationMetadata);
await executorMethod(ctx, new CQRSRequestPayload(operation));

await operationHandler.Received().ExecuteAsync(ctx, Arg.Any<Operation>());
}

[Fact]
Expand All @@ -91,12 +132,11 @@ public async Task Throws_command_handler_not_found_if_cannot_get_it_from_DI()
var cmd = new Command();
var ctx = MockHttpContext();

serviceProvider.GetService(typeof(ICommandHandler<Command>)).Returns(null);
serviceProvider.GetService(typeof(CommandHandler)).Returns(null);
var executorMethod = executorFactory.CreateExecutorFor(commandMetadata);

await Assert.ThrowsAsync<CommandHandlerNotFoundException>(
() => executorMethod(ctx, new CQRSRequestPayload(cmd))
);
var act = () => executorMethod(ctx, new CQRSRequestPayload(cmd));
await act.Should().ThrowAsync<CommandHandlerNotFoundException>();
}

[Fact]
Expand All @@ -105,12 +145,11 @@ public async Task Throws_query_handler_not_found_if_cannot_get_it_from_DI()
var query = new Query();
var ctx = MockHttpContext();

serviceProvider.GetService(typeof(IQueryHandler<Query, QueryResult>)).Returns(null);
serviceProvider.GetService(typeof(QueryHandler)).Returns(null);
var executorMethod = executorFactory.CreateExecutorFor(queryMetadata);

await Assert.ThrowsAsync<QueryHandlerNotFoundException>(
() => executorMethod(ctx, new CQRSRequestPayload(query))
);
var act = () => executorMethod(ctx, new CQRSRequestPayload(query));
await act.Should().ThrowAsync<QueryHandlerNotFoundException>();
}

[Fact]
Expand All @@ -119,12 +158,11 @@ public async Task Throws_operation_handler_not_found_if_cannot_get_it_from_DI()
var operation = new Operation();
var ctx = MockHttpContext();

serviceProvider.GetService(typeof(IOperationHandler<Operation, OperationResult>)).Returns(null);
serviceProvider.GetService(typeof(OperationHandler)).Returns(null);
var executorMethod = executorFactory.CreateExecutorFor(operationMetadata);

await Assert.ThrowsAsync<OperationHandlerNotFoundException>(
() => executorMethod(ctx, new CQRSRequestPayload(operation))
);
var act = () => executorMethod(ctx, new CQRSRequestPayload(operation));
await act.Should().ThrowAsync<OperationHandlerNotFoundException>();
}

private HttpContext MockHttpContext()
Expand All @@ -136,13 +174,28 @@ private HttpContext MockHttpContext()

public class Command : ICommand { }

public class Query : IQuery<QueryResult> { }
public class CommandHandler : ICommandHandler<Command>
{
public Task ExecuteAsync(HttpContext context, Command command) => Task.CompletedTask;
}

public class QueryResult { }

public class Query : IQuery<QueryResult> { }

public class QueryHandler : IQueryHandler<Query, QueryResult>
{
public virtual Task<QueryResult> ExecuteAsync(HttpContext context, Query query) =>
Task.FromResult(new QueryResult());
}

public class Operation : IOperation<OperationResult> { }

public class OperationResult { }

public class IgnoreType { }
public class OperationHandler : IOperationHandler<Operation, OperationResult>
{
public virtual Task<OperationResult> ExecuteAsync(HttpContext context, Operation operation) =>
Task.FromResult(new OperationResult());
}
}
Loading