Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Convert Employee Commands to CSharpFunctionalExtensions #141

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
2 changes: 1 addition & 1 deletion .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"login": "KyleMcMaster",
"name": "Kyle McMaster",
"avatar_url": "https://avatars1.githubusercontent.com/u/11415127?v=4",
"profile": "https://github.com/KyleMcMaster",
"profile": "https://www.KyleMcMaster.com",
"contributions": [
"design",
"code",
Expand Down
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:silent

# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:silent
csharp_using_directive_placement = outside_namespace:suggestion

#### C# Formatting Rules ####

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ Sample HRIS application where a list of employees and their payroll information

### Api and Functions

![dotnet core - build & test](https://github.com/KyleMcMaster/payroll-processor/workflows/dotnet%20core%20-%20build%20&%20test/badge.svg)
![dotnet core - build & test](https://github.com/KyleMcMaster/payroll-processor/workflows/.github/workflows/policy-dotnetcore.yml/badge.svg)

### Client

![.github/workflows/npm.yml](https://github.com/KyleMcMaster/payroll-processor/workflows/.github/workflows/npm.yml/badge.svg)
![client - build & test](https://github.com/KyleMcMaster/payroll-processor/workflows/.github/workflows/policy-npm.yml/badge.svg)
[![Styled with Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io)

## Motivation
Expand Down
3 changes: 2 additions & 1 deletion api/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍👍

</PropertyGroup>
</Project>
53 changes: 27 additions & 26 deletions api/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,45 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Ardalis.ApiEndpoints" Version="4.0.1" />
<PackageVersion Include="Ardalis.GuardClauses" Version="4.0.1" />
<PackageVersion Include="AutoFixture" Version="4.17.0" />
<PackageVersion Include="AutoFixture.AutoNSubstitute" Version="4.17.0" />
<PackageVersion Include="AutoFixture.Idioms" Version="4.17.0" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.17.0" />
<PackageVersion Include="AutoMapper" Version="12.0.0" />
<PackageVersion Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
<PackageVersion Include="Azure.Storage.Queues" Version="12.11.1" />
<PackageVersion Include="Ardalis.ApiEndpoints" Version="4.1.0" />
<PackageVersion Include="Ardalis.GuardClauses" Version="4.1.1" />
<PackageVersion Include="AutoFixture" Version="4.18.0" />
<PackageVersion Include="AutoFixture.AutoNSubstitute" Version="4.18.0" />
<PackageVersion Include="AutoFixture.Idioms" Version="4.18.0" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" />
<PackageVersion Include="AutoMapper" Version="12.0.1" />
<PackageVersion Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageVersion Include="Azure.Storage.Queues" Version="12.15.0" />
<PackageVersion Include="Bogus" Version="34.0.2" />
<PackageVersion Include="coverlet.collector" Version="3.1.2">
<PackageVersion Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
<PackageVersion Include="CSharpFunctionalExtensions.FluentAssertions" Version="1.1.0" />
<PackageVersion Include="FluentAssertions" Version="6.8.0" />
<PackageVersion Include="LanguageExt.Core" Version="4.2.9" />
<PackageVersion Include="CSharpFunctionalExtensions" Version="2.40.1" />
<PackageVersion Include="CSharpFunctionalExtensions.FluentAssertions" Version="2.0.0" />
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="LanguageExt.Core" Version="4.4.4" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0" />
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.31.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.1.0" />
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.35.3" />
<PackageVersion Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
<PackageVersion Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="5.0.1" />
<PackageVersion Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="5.2.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
<PackageVersion Include="Microsoft.NET.Sdk.Functions" Version="4.1.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
<PackageVersion Include="Microsoft.NET.Sdk.Functions" Version="4.2.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.10" />
<PackageVersion Include="NewtonSoft.Json" Version="13.0.1" />
<PackageVersion Include="NSubstitute" Version="4.4.0" />
<PackageVersion Include="Scrutor" Version="4.2.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.4.0" />
<PackageVersion Include="xunit" Version="2.4.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5">
<PackageVersion Include="NSubstitute" Version="5.0.0" />
<PackageVersion Include="Scrutor" Version="4.2.2" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
<PackageVersion Include="xunit" Version="2.5.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@
<PackageReference Include="CSharpFunctionalExtensions.FluentAssertions" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit"/>
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="coverlet.collector" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down
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.
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Owner Author

Choose a reason for hiding this comment

The 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
Expand Up @@ -6,6 +6,7 @@

<ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" />
<PackageReference Include="CSharpFunctionalExtensions" />
<PackageReference Include="LanguageExt.Core" />
<PackageReference Include="NewtonSoft.Json" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="coverlet.collector" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="coverlet.collector" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="coverlet.collector" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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));
Expand All @@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you drop the Async suffix most of the time now? I think Microsoft s current guidance is to only use it if there is also an existing sync API of the same name...

But I've seen a lot of modern code go both ways.

{
var command = new EmployeeCreateCommand(
generator.Generate(),
Expand All @@ -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));
}
}

Expand Down
15 changes: 7 additions & 8 deletions payroll-processor.code-workspace
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{
"folders": [
{
"path": ".",
"name": "root"
},
{
"path": "docs",
"name": "docs"
Expand All @@ -15,10 +19,6 @@
{
"path": "vue-client",
"name": "vue"
},
{
"path": ".",
"name": "root"
}
],
"settings": {
Expand All @@ -32,11 +32,11 @@
"api": true,
"client": true,
"docs": true,
"vue-client": true,
"**/bin": true,
"**/obj": true,
"**/dist": true
},

"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
Expand All @@ -52,12 +52,11 @@
*/
"editor.codeActionsOnSave": {}
},

"omnisharp.defaultLaunchSolution": "PayrollProcessor.sln",
"omnisharp.autoStart": true,
"omnisharp.enableEditorConfigSupport": true,
"omnisharp.enableRoslynAnalyzers": true,
"omnisharp.useEditorFormattingSettings": true
"omnisharp.useEditorFormattingSettings": true,
"dotnet.defaultSolution": "PayrollProcessor.sln"
},
"extensions": {
"recommendations": [
Expand Down
Loading