Adds Generic CommandHandlerDecorator
This commit is contained in:
parent
49f9b99ee1
commit
64e696dc5a
21 changed files with 131 additions and 110 deletions
|
|
@ -1,4 +1,3 @@
|
|||
using Akka.Actor;
|
||||
using Microsoft.ApplicationInsights;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
|
|
|
|||
7
Core/CommandQueries/Command.cs
Normal file
7
Core/CommandQueries/Command.cs
Normal 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; }
|
||||
}
|
||||
16
Core/CommandQueries/CommandResponse.cs
Normal file
16
Core/CommandQueries/CommandResponse.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
7
Core/CommandQueries/ICommand.cs
Normal file
7
Core/CommandQueries/ICommand.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace PlanTempus.Core.CommandQueries;
|
||||
|
||||
public interface ICommand
|
||||
{
|
||||
Guid CorrelationId { get; set; }
|
||||
Guid TransactionId { get; set; }
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ public class DatabaseScope : IDisposable
|
|||
Connection = connection;
|
||||
_operation = operation;
|
||||
_operation.Telemetry.Success = true;
|
||||
_operation.Telemetry.Timestamp = DateTimeOffset.UtcNow;
|
||||
_stopwatch = Stopwatch.StartNew();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ namespace PlanTempus.Core.ModuleRegistry
|
|||
var client = new Microsoft.ApplicationInsights.TelemetryClient(configuration);
|
||||
client.Context.GlobalProperties["Application"] = GetType().Namespace?.Split('.')[0];
|
||||
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();
|
||||
|
||||
builder.Register(c => client).InstancePerLifetimeScope();
|
||||
|
|
|
|||
|
|
@ -1,30 +1,28 @@
|
|||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Akka" Version="1.5.32" />
|
||||
<PackageReference Include="Autofac" Version="8.1.1" />
|
||||
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="10.0.0" />
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
<PackageReference Include="Insight.Database" Version="8.0.1" />
|
||||
<PackageReference Include="Insight.Database.Providers.PostgreSQL" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.22.0" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel" Version="2.22.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.1" />
|
||||
<PackageReference Include="npgsql" Version="9.0.2" />
|
||||
<PackageReference Include="Seq.Api" Version="2024.3.0" />
|
||||
<PackageReference Include="Sodium.Core" Version="1.3.5" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" Version="8.1.1"/>
|
||||
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="10.0.0"/>
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0"/>
|
||||
<PackageReference Include="Insight.Database" Version="8.0.1"/>
|
||||
<PackageReference Include="Insight.Database.Providers.PostgreSQL" Version="8.0.1"/>
|
||||
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.22.0"/>
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel" Version="2.22.0"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.3.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.1"/>
|
||||
<PackageReference Include="npgsql" Version="9.0.2"/>
|
||||
<PackageReference Include="Seq.Api" Version="2024.3.0"/>
|
||||
<PackageReference Include="Sodium.Core" Version="1.3.5"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Configurations\AzureAppConfigurationProvider\" />
|
||||
<Folder Include="Configurations\PostgresqlConfigurationBuilder\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Configurations\AzureAppConfigurationProvider\"/>
|
||||
<Folder Include="Configurations\PostgresqlConfigurationBuilder\"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<,>));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using Autofac;
|
|||
using Insight.Database;
|
||||
using PlanTempus.Components;
|
||||
using PlanTempus.Components.Users.Create;
|
||||
using PlanTempus.Core.CommandQueries;
|
||||
using PlanTempus.Core.Database;
|
||||
using PlanTempus.Core.Database.ConnectionFactory;
|
||||
using Shouldly;
|
||||
|
|
@ -24,13 +25,13 @@ public class HandlerTest : TestFixture
|
|||
|
||||
var command = new CreateUserCommand
|
||||
{
|
||||
Email = "lloyd@dumbanddumber.com1", // Lloyd Christmas
|
||||
Email = "lloyd@dumbanddumber.com3", // Lloyd Christmas
|
||||
Password = "1234AceVentura#LOL", // Ace Ventura
|
||||
IsActive = true,
|
||||
CorrelationId = Guid.NewGuid()
|
||||
};
|
||||
|
||||
var result = await commandHandler.Handle<CreateUserCommand, CreateUserResult>(command);
|
||||
var result = await commandHandler.Handle(command);
|
||||
|
||||
result.ShouldNotBeNull();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue