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,13 +1,14 @@
using Autofac;
using PlanTempus.Components.Users.Create;
using PlanTempus.Core.CommandQueries;
namespace PlanTempus.Components;
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);
}
}

View file

@ -1,13 +1,15 @@
namespace PlanTempus.Components;
using PlanTempus.Core.CommandQueries;
namespace PlanTempus.Components;
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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,34 +1,31 @@
using Microsoft.AspNetCore.Mvc;
using PlanTempus.Core.CommandQueries;
namespace PlanTempus.Components.Users.Create
{
[ApiController]
[Route("api/users")]
public class CreateUserController(CreateUserHandler handler, CreateUserValidator validator) : ControllerBase
{
[HttpPost]
public async Task<ActionResult<CreateUserResult>> Create([FromBody] CreateUserCommand command)
{
try
{
var validationResult = await validator.ValidateAsync(command);
if (!validationResult.IsValid)
{
return BadRequest(validationResult.Errors);
}
[ApiController]
[Route("api/users")]
public class CreateUserController(CreateUserHandler handler, CreateUserValidator validator) : ControllerBase
{
[HttpPost]
public async Task<ActionResult<CommandResponse>> Create([FromBody] CreateUserCommand command)
{
try
{
var validationResult = await validator.ValidateAsync(command);
if (!validationResult.IsValid)
{
return BadRequest(validationResult.Errors);
}
var result = await handler.Handle(command);
return CreatedAtAction(
nameof(CreateUserCommand),
"GetUser",
new { id = result.Id },
result);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
var result = await handler.Handle(command);
return Accepted($"/api/requests/{result.RequestId}", result);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
}

View file

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