Skip to content

Commit

Permalink
Ensure that ObjectExecutorFactory resolves the handler that was use…
Browse files Browse the repository at this point in the history
…d to generate metadata
  • Loading branch information
jakubfijalkowski committed Feb 6, 2024
1 parent 2b28316 commit 628e4c3
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 49 deletions.
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());
}
}

0 comments on commit 628e4c3

Please sign in to comment.