Adds Generic CommandHandlerDecorator

This commit is contained in:
Janus C. H. Knudsen 2025-03-12 00:13:53 +01:00
parent 49f9b99ee1
commit 64e696dc5a
21 changed files with 131 additions and 110 deletions

View file

@ -1,4 +1,3 @@
using Akka.Actor;
using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>

View file

@ -0,0 +1,7 @@
namespace PlanTempus.Core.CommandQueries;
public abstract class Command : ICommand
{
public required Guid CorrelationId { get; set; }
public Guid TransactionId { get; set; }
}

View file

@ -0,0 +1,16 @@
namespace PlanTempus.Core.CommandQueries;
public class CommandResponse
{
public Guid RequestId { get; }
public Guid CorrelationId { get; }
public Guid? TransactionId { get; }
public DateTime CreatedAt { get; }
public CommandResponse(Guid correlationId)
{
CorrelationId = correlationId;
RequestId = Guid.CreateVersion7();
CreatedAt = DateTime.Now;
}
}

View file

@ -0,0 +1,7 @@
namespace PlanTempus.Core.CommandQueries;
public interface ICommand
{
Guid CorrelationId { get; set; }
Guid TransactionId { get; set; }
}

View file

@ -15,6 +15,7 @@ public class DatabaseScope : IDisposable
Connection = connection; Connection = connection;
_operation = operation; _operation = operation;
_operation.Telemetry.Success = true; _operation.Telemetry.Success = true;
_operation.Telemetry.Timestamp = DateTimeOffset.UtcNow;
_stopwatch = Stopwatch.StartNew(); _stopwatch = Stopwatch.StartNew();
} }

View file

@ -17,7 +17,7 @@ namespace PlanTempus.Core.ModuleRegistry
var client = new Microsoft.ApplicationInsights.TelemetryClient(configuration); var client = new Microsoft.ApplicationInsights.TelemetryClient(configuration);
client.Context.GlobalProperties["Application"] = GetType().Namespace?.Split('.')[0]; client.Context.GlobalProperties["Application"] = GetType().Namespace?.Split('.')[0];
client.Context.GlobalProperties["MachineName"] = Environment.MachineName; client.Context.GlobalProperties["MachineName"] = Environment.MachineName;
client.Context.GlobalProperties["Version"] = Environment.Version.ToString(); client.Context.GlobalProperties["CLRVersion"] = Environment.Version.ToString();
client.Context.GlobalProperties["ProcessorCount"] = Environment.ProcessorCount.ToString(); client.Context.GlobalProperties["ProcessorCount"] = Environment.ProcessorCount.ToString();
builder.Register(c => client).InstancePerLifetimeScope(); builder.Register(c => client).InstancePerLifetimeScope();

View file

@ -1,13 +1,11 @@
 <Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Akka" Version="1.5.32" />
<PackageReference Include="Autofac" Version="8.1.1"/> <PackageReference Include="Autofac" Version="8.1.1"/>
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="10.0.0"/> <PackageReference Include="Autofac.Extensions.DependencyInjection" Version="10.0.0"/>
<PackageReference Include="FluentValidation" Version="11.11.0"/> <PackageReference Include="FluentValidation" Version="11.11.0"/>

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>

View file

@ -1,13 +1,14 @@
using Autofac; using Autofac;
using PlanTempus.Components.Users.Create; using PlanTempus.Components.Users.Create;
using PlanTempus.Core.CommandQueries;
namespace PlanTempus.Components; namespace PlanTempus.Components;
public class CommandHandler(IComponentContext context) : ICommandHandler public class CommandHandler(IComponentContext context) : ICommandHandler
{ {
public async Task<TCommandResult> Handle<TCommand, TCommandResult>(TCommand command) public async Task<CommandResponse> Handle<TCommand>(TCommand command) where TCommand : ICommand
{ {
var handler = context.Resolve<ICommandHandler<TCommand, TCommandResult>>(); var handler = context.Resolve<ICommandHandler<TCommand>>();
return await handler.Handle(command); return await handler.Handle(command);
} }
} }

View file

@ -1,13 +1,15 @@
namespace PlanTempus.Components; using PlanTempus.Core.CommandQueries;
namespace PlanTempus.Components;
public interface ICommandHandler public interface ICommandHandler
{ {
Task<TCommandResult> Handle<TCommand, TCommandResult>(TCommand command); Task<CommandResponse> Handle<TCommand>(TCommand command) where TCommand : ICommand;
} }
public interface ICommandHandler<in T, TResult> public interface ICommandHandler<TCommand> where TCommand : ICommand
{ {
Task<TResult> Handle(T input); Task<CommandResponse> Handle(TCommand command);
} }
public interface ICommandHandlerDecorator public interface ICommandHandlerDecorator

View file

@ -1,5 +1,6 @@
using Autofac; using Autofac;
using PlanTempus.Components.Users.Create; using PlanTempus.Components.Users.Create;
using PlanTempus.Core.CommandQueries;
using PlanTempus.Core.SeqLogging; using PlanTempus.Core.SeqLogging;
namespace PlanTempus.Components.ModuleRegistry namespace PlanTempus.Components.ModuleRegistry
@ -10,35 +11,27 @@ namespace PlanTempus.Components.ModuleRegistry
protected override void Load(ContainerBuilder builder) protected override void Load(ContainerBuilder builder)
{ {
builder.RegisterType<PlanTempus.Components.CommandHandler>() builder.RegisterType<PlanTempus.Components.CommandHandler>()
.As<PlanTempus.Components.ICommandHandler>() .As<PlanTempus.Components.ICommandHandler>()
.InstancePerLifetimeScope(); .InstancePerLifetimeScope();
builder.RegisterAssemblyTypes(ThisAssembly) builder.RegisterAssemblyTypes(ThisAssembly)
.Where(t => !typeof(ICommandHandlerDecorator).IsAssignableFrom(t)) .Where(t => !typeof(ICommandHandlerDecorator).IsAssignableFrom(t))
.AsClosedTypesOf(typeof(ICommandHandler<,>)) .AsClosedTypesOf(typeof(ICommandHandler<>))
.InstancePerLifetimeScope(); .InstancePerLifetimeScope();
// Registrer alle handlers
// builder.RegisterAssemblyTypes(ThisAssembly)
// .AsClosedTypesOf(typeof(ICommandHandler<,>))
// .InstancePerLifetimeScope();
builder.RegisterAssemblyTypes(ThisAssembly) builder.RegisterAssemblyTypes(ThisAssembly)
.As<ICommandHandlerDecorator>(); .As<ICommandHandlerDecorator>();
builder.RegisterDecorator( builder.RegisterGenericDecorator(
typeof(CreateUserHandlerDecorator), typeof(CommandHandlerDecorator<>),
typeof(ICommandHandler<CreateUserCommand, CreateUserResult>)); typeof(ICommandHandler<>));
// //
// Registrer en decorator for alle ICommandHandler<TCommand> // Registrer en decorator for alle ICommandHandler<TCommand>
// builder.RegisterGenericDecorator( // builder.RegisterGenericDecorator(
// typeof(CommandHandlerDecorator<,>), // typeof(CommandHandlerDecorator<,>),
// typeof(ICommandHandler<,>)); // typeof(ICommandHandler<,>));
} }
} }
} }

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>

View file

@ -1,27 +1,30 @@
using System.Diagnostics; using System.Diagnostics;
using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.DataContracts;
using PlanTempus.Core.CommandQueries;
namespace PlanTempus.Components.Users.Create; namespace PlanTempus.Components.Users.Create;
public class CreateUserHandlerDecorator( public class CommandHandlerDecorator<TCommand>(
ICommandHandler<CreateUserCommand, CreateUserResult> decoratedHandler, ICommandHandler<TCommand> decoratedHandler,
TelemetryClient telemetryClient) TelemetryClient telemetryClient) : ICommandHandler<TCommand>, ICommandHandlerDecorator where TCommand : ICommand
: ICommandHandler<CreateUserCommand, CreateUserResult>, ICommandHandlerDecorator
{ {
public async Task<CreateUserResult> Handle(CreateUserCommand command) public async Task<CommandResponse> Handle(TCommand command)
{ {
// var correlationId = Activity.Current?.RootId ?? command.CorrelationId; // var correlationId = Activity.Current?.RootId ?? command.CorrelationId;
using (var operation = using (var operation =
telemetryClient.StartOperation<RequestTelemetry>($"Handle {nameof(CreateUserCommand)}", telemetryClient.StartOperation<RequestTelemetry>($"Handle {decoratedHandler.GetType().FullName}",
command.CorrelationId.ToString())) command.CorrelationId.ToString()))
{ {
try try
{ {
operation.Telemetry.Properties["CorrelationId"] = command.CorrelationId.ToString(); operation.Telemetry.Properties["CorrelationId"] = command.CorrelationId.ToString();
operation.Telemetry.Properties["TransactionId"] = command.TransactionId.ToString();
var result = await decoratedHandler.Handle(command); var result = await decoratedHandler.Handle(command);
operation.Telemetry.Properties["RequestId"] = result.RequestId.ToString();
operation.Telemetry.Success = true; operation.Telemetry.Success = true;
return result; return result;
@ -32,7 +35,9 @@ public class CreateUserHandlerDecorator(
telemetryClient.TrackException(ex, new Dictionary<string, string> telemetryClient.TrackException(ex, new Dictionary<string, string>
{ {
["CommandType"] = nameof(CreateUserCommand) ["CorrelationId"] = command.CorrelationId.ToString(),
["Command"] = command.GetType().Name,
["CommandHandler"] = decoratedHandler.GetType().FullName
}); });
throw; throw;
} }

View file

@ -1,15 +1,11 @@
using PlanTempus.Core.CommandQueries;
namespace PlanTempus.Components.Users.Create namespace PlanTempus.Components.Users.Create
{ {
public interface ICommand public class CreateUserCommand : Command
{
Guid CorrelationId { get; set; }
}
public class CreateUserCommand : ICommand
{ {
public required string Email { get; set; } public required string Email { get; set; }
public required string Password { get; set; } public required string Password { get; set; }
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;
public required Guid CorrelationId { get; set; }
} }
} }

View file

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using PlanTempus.Core.CommandQueries;
namespace PlanTempus.Components.Users.Create namespace PlanTempus.Components.Users.Create
{ {
@ -7,7 +8,7 @@ namespace PlanTempus.Components.Users.Create
public class CreateUserController(CreateUserHandler handler, CreateUserValidator validator) : ControllerBase public class CreateUserController(CreateUserHandler handler, CreateUserValidator validator) : ControllerBase
{ {
[HttpPost] [HttpPost]
public async Task<ActionResult<CreateUserResult>> Create([FromBody] CreateUserCommand command) public async Task<ActionResult<CommandResponse>> Create([FromBody] CreateUserCommand command)
{ {
try try
{ {
@ -19,11 +20,7 @@ namespace PlanTempus.Components.Users.Create
var result = await handler.Handle(command); var result = await handler.Handle(command);
return CreatedAtAction( return Accepted($"/api/requests/{result.RequestId}", result);
nameof(CreateUserCommand),
"GetUser",
new { id = result.Id },
result);
} }
catch (Exception ex) catch (Exception ex)
{ {

View file

@ -3,17 +3,16 @@ using Microsoft.ApplicationInsights;
using Npgsql; using Npgsql;
using PlanTempus.Components.Users.Exceptions; using PlanTempus.Components.Users.Exceptions;
using PlanTempus.Core; using PlanTempus.Core;
using PlanTempus.Core.CommandQueries;
using PlanTempus.Core.Database; using PlanTempus.Core.Database;
using PlanTempus.Core.Telemetry;
namespace PlanTempus.Components.Users.Create namespace PlanTempus.Components.Users.Create
{ {
public class CreateUserHandler( public class CreateUserHandler(
TelemetryClient telemetryClient,
IDatabaseOperations databaseOperations, IDatabaseOperations databaseOperations,
ISecureTokenizer secureTokenizer) : ICommandHandler<CreateUserCommand, CreateUserResult> ISecureTokenizer secureTokenizer) : ICommandHandler<CreateUserCommand>
{ {
public async Task<CreateUserResult> Handle(CreateUserCommand command) public async Task<CommandResponse> Handle(CreateUserCommand command)
{ {
using var db = databaseOperations.CreateScope(nameof(CreateUserHandler)); using var db = databaseOperations.CreateScope(nameof(CreateUserHandler));
try try
@ -28,20 +27,19 @@ namespace PlanTempus.Components.Users.Create
var data = await db.Connection.QuerySqlAsync<CreateUserResult>(sql, new var data = await db.Connection.QuerySqlAsync<CreateUserResult>(sql, new
{ {
Email = command.Email, command.Email,
PasswordHash = secureTokenizer.TokenizeText(command.Password), PasswordHash = secureTokenizer.TokenizeText(command.Password),
SecurityStamp = Guid.NewGuid().ToString("N"), SecurityStamp = Guid.NewGuid().ToString("N"),
EmailConfirmed = false, EmailConfirmed = false,
AccessFailedCount = 0, AccessFailedCount = 0,
LockoutEnabled = false, LockoutEnabled = false,
IsActive = command.IsActive, command.IsActive,
}); });
var result = data.First(); var result = data.First();
return new CommandResponse(command.CorrelationId);
return result;
} }
catch (PostgresException ex) when (ex.SqlState == "23505") catch (PostgresException ex) when (ex.SqlState == "23505")
{ {

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>

View file

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>

View file

@ -2,6 +2,7 @@ using Autofac;
using Insight.Database; using Insight.Database;
using PlanTempus.Components; using PlanTempus.Components;
using PlanTempus.Components.Users.Create; using PlanTempus.Components.Users.Create;
using PlanTempus.Core.CommandQueries;
using PlanTempus.Core.Database; using PlanTempus.Core.Database;
using PlanTempus.Core.Database.ConnectionFactory; using PlanTempus.Core.Database.ConnectionFactory;
using Shouldly; using Shouldly;
@ -24,13 +25,13 @@ public class HandlerTest : TestFixture
var command = new CreateUserCommand var command = new CreateUserCommand
{ {
Email = "lloyd@dumbanddumber.com1", // Lloyd Christmas Email = "lloyd@dumbanddumber.com3", // Lloyd Christmas
Password = "1234AceVentura#LOL", // Ace Ventura Password = "1234AceVentura#LOL", // Ace Ventura
IsActive = true, IsActive = true,
CorrelationId = Guid.NewGuid() CorrelationId = Guid.NewGuid()
}; };
var result = await commandHandler.Handle<CreateUserCommand, CreateUserResult>(command); var result = await commandHandler.Handle(command);
result.ShouldNotBeNull(); result.ShouldNotBeNull();
} }

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>