-
-
Notifications
You must be signed in to change notification settings - Fork 22
Convert Employee Commands to CSharpFunctionalExtensions #141
base: main
Are you sure you want to change the base?
Changes from all commits
e707711
33917cb
0d40a88
12b1074
6dcf8ec
4612474
43625c7
eb9c6d6
aeda797
32da87f
a4d0ba0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
using System.Threading; | ||
using CSharpFunctionalExtensions; | ||
|
||
namespace PayrollProcessor.Core.Domain.Intrastructure.Operations.Commands; | ||
public interface IStranglerCommandDispatcher | ||
{ | ||
Result Dispatch(ICommand command, CancellationToken token = default); | ||
|
||
Result<TResponse> Dispatch<TResponse>(ICommand<TResponse> command, CancellationToken token = default); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
using System.Threading; | ||
using CSharpFunctionalExtensions; | ||
|
||
namespace PayrollProcessor.Core.Domain.Intrastructure.Operations.Commands; | ||
public interface IStranglerCommandHandler<TCommand> where TCommand : ICommand | ||
{ | ||
Result Execute(TCommand command, CancellationToken token); | ||
} | ||
|
||
public interface IStranglerCommandHandler<TCommand, TResponse> where TCommand : ICommand<TResponse> | ||
{ | ||
Result<TResponse> Execute(TCommand command, CancellationToken token); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Threading; | ||
using Ardalis.GuardClauses; | ||
using CSharpFunctionalExtensions; | ||
using PayrollProcessor.Core.Domain.Intrastructure.Operations.Factories; | ||
|
||
namespace PayrollProcessor.Core.Domain.Intrastructure.Operations.Commands; | ||
|
||
/// <summary> | ||
/// TODO: Temporarily named after the Strangler Fig Pattern as this serves as an implementation of <see cref="ICommandDispatcher"/> migrating to CSharpFunctionalExtensions from LanguageExt. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you done this kind of naming in a project before? I haven't but I like the idea of calling this type out explicitly. That way, you know it should eventually be renamed when it takes over. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't done this before but the approach was mentioned in Code That Fits In Your Head and I couldn't think of a better name so I figured calling it out explicitly would be best while the code is in migration state. |
||
/// </summary> | ||
public class StranglerCommandDispatcher : IStranglerCommandDispatcher | ||
{ | ||
private readonly ServiceProviderDelegate serviceProvider; | ||
private static readonly ConcurrentDictionary<Type, object> commandHandlers = new ConcurrentDictionary<Type, object>(); | ||
|
||
public StranglerCommandDispatcher(ServiceProviderDelegate serviceProvider) | ||
{ | ||
Guard.Against.Null(serviceProvider, nameof(serviceProvider)); | ||
|
||
this.serviceProvider = serviceProvider; | ||
} | ||
|
||
public Result Dispatch(ICommand command, CancellationToken token = default) | ||
{ | ||
Guard.Against.Null(command, nameof(command)); | ||
|
||
var commandType = command.GetType(); | ||
|
||
var handler = (StranglerCommandHandlerWrapper)commandHandlers | ||
.GetOrAdd( | ||
commandType, | ||
#pragma warning disable CS8603 // Possible null reference return. | ||
t => Activator | ||
.CreateInstance(typeof(StranglerCommandHandlerWrapperImpl<>) | ||
.MakeGenericType(commandType))); | ||
#pragma warning restore CS8603 // Possible null reference return. | ||
|
||
return handler.Dispatch(command, serviceProvider, token); | ||
} | ||
|
||
public Result<TResponse> Dispatch<TResponse>(ICommand<TResponse> command, CancellationToken token = default) | ||
{ | ||
Guard.Against.Null(command, nameof(command)); | ||
|
||
var commandType = command.GetType(); | ||
|
||
var handler = (StranglerCommandHandlerWrapper<TResponse>)commandHandlers | ||
.GetOrAdd( | ||
commandType, | ||
#pragma warning disable CS8603 // Possible null reference return. | ||
t => Activator | ||
.CreateInstance(typeof(StranglerCreateCommandHandlerWrapperImpl<,>) | ||
.MakeGenericType(commandType, typeof(TResponse)))); | ||
#pragma warning restore CS8603 // Possible null reference return. | ||
|
||
return handler.Dispatch(command, serviceProvider, token); | ||
} | ||
} | ||
|
||
internal abstract class StranglerCommandHandlerWrapper : HandlerBase | ||
{ | ||
public abstract Result Dispatch(ICommand command, ServiceProviderDelegate serviceProvider, CancellationToken token); | ||
} | ||
|
||
internal class StranglerCommandHandlerWrapperImpl<TCommand> : StranglerCommandHandlerWrapper | ||
where TCommand : ICommand | ||
{ | ||
public override Result Dispatch(ICommand command, ServiceProviderDelegate serviceProvider, CancellationToken token) => | ||
GetHandler<IStranglerCommandHandler<TCommand>>(serviceProvider).Execute((TCommand)command, token); | ||
} | ||
|
||
internal abstract class StranglerCommandHandlerWrapper<TResponse> : HandlerBase | ||
{ | ||
public abstract Result<TResponse> Dispatch(ICommand<TResponse> command, ServiceProviderDelegate serviceProvider, CancellationToken token); | ||
} | ||
|
||
internal class StranglerCreateCommandHandlerWrapperImpl<TCommand, TResponse> : StranglerCommandHandlerWrapper<TResponse> | ||
where TCommand : ICommand<TResponse> | ||
{ | ||
public override Result<TResponse> Dispatch(ICommand<TResponse> command, ServiceProviderDelegate serviceProvider, CancellationToken token) => | ||
GetHandler<IStranglerCommandHandler<TCommand, TResponse>>(serviceProvider).Execute((TCommand)command, token); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
using System.Threading.Tasks; | ||
using Ardalis.ApiEndpoints; | ||
using Ardalis.GuardClauses; | ||
using CSharpFunctionalExtensions; | ||
using Microsoft.AspNetCore.Mvc; | ||
using PayrollProcessor.Core.Domain.Features.Employees; | ||
using PayrollProcessor.Core.Domain.Intrastructure.Identifiers; | ||
|
@@ -11,14 +12,14 @@ | |
|
||
namespace PayrollProcessor.Web.Api.Features.Employees; | ||
|
||
public class EmployeeCreate : EndpointBaseAsync | ||
public class EmployeeCreate : EndpointBaseSync | ||
.WithRequest<EmployeeCreateRequest> | ||
.WithActionResult<Employee> | ||
{ | ||
private readonly ICommandDispatcher dispatcher; | ||
private readonly IStranglerCommandDispatcher dispatcher; | ||
private readonly IEntityIdGenerator generator; | ||
|
||
public EmployeeCreate(ICommandDispatcher dispatcher, IEntityIdGenerator generator) | ||
public EmployeeCreate(IStranglerCommandDispatcher dispatcher, IEntityIdGenerator generator) | ||
{ | ||
Guard.Against.Null(dispatcher, nameof(dispatcher)); | ||
Guard.Against.Null(generator, nameof(generator)); | ||
|
@@ -34,7 +35,7 @@ public EmployeeCreate(ICommandDispatcher dispatcher, IEntityIdGenerator generato | |
OperationId = "Employees.Create", | ||
Tags = new[] { "Employees" }) | ||
] | ||
public override Task<ActionResult<Employee>> HandleAsync([FromBody] EmployeeCreateRequest request, CancellationToken token) | ||
public override ActionResult<Employee> Handle([FromBody] EmployeeCreateRequest request) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you drop the But I've seen a lot of modern code go both ways. |
||
{ | ||
var command = new EmployeeCreateCommand( | ||
generator.Generate(), | ||
|
@@ -52,9 +53,9 @@ public override Task<ActionResult<Employee>> HandleAsync([FromBody] EmployeeCrea | |
|
||
return dispatcher | ||
.Dispatch(command) | ||
.Match<Employee, ActionResult<Employee>>( | ||
employee => employee, | ||
ex => new APIErrorResult(ex.Message)); | ||
.Match<ActionResult<Employee>, Employee>( | ||
onSuccess: employee => Ok(employee), | ||
onFailure: ex => new APIErrorResult(ex)); | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍👍