From e2aa1cde537526745edc776de9a6d4f205b92a52 Mon Sep 17 00:00:00 2001 From: Danny Goodacre Date: Sat, 10 Jan 2026 12:52:04 +0000 Subject: [PATCH 01/10] WIP --- .../{ => Abstractions}/ICommand.cs | 2 +- .../CommandQuery/{ => Abstractions}/IQuery.cs | 2 +- .../CommandQuery/Abstractions/ITransaction.cs | 8 +++ .../Abstractions/ITransactionProvider.cs | 6 ++ .../CommandQuery/Abstractions/IUnitOfWork.cs | 18 ++++++ .../CommandQuery/CommandHandler.T.cs | 1 + .../CommandQuery/CommandHandler.cs | 1 + .../CommandQuery/QueryHandler.cs | 1 + ...er.T.cs => TransactionCommandHandler.T.cs} | 36 +++++++---- ...andler.cs => TransactionCommandHandler.cs} | 36 +++++++---- DannyGoodacre.Core/Data/IUnitOfWork.cs | 11 ---- .../Extensions/ServiceCollectionExtensions.cs | 52 +++++++++------- .../Extensions/TypeExtensions.cs | 62 ++++++++++--------- 13 files changed, 144 insertions(+), 92 deletions(-) rename DannyGoodacre.Core/CommandQuery/{ => Abstractions}/ICommand.cs (58%) rename DannyGoodacre.Core/CommandQuery/{ => Abstractions}/IQuery.cs (57%) create mode 100644 DannyGoodacre.Core/CommandQuery/Abstractions/ITransaction.cs create mode 100644 DannyGoodacre.Core/CommandQuery/Abstractions/ITransactionProvider.cs create mode 100644 DannyGoodacre.Core/CommandQuery/Abstractions/IUnitOfWork.cs rename DannyGoodacre.Core/CommandQuery/{UnitOfWorkCommandHandler.T.cs => TransactionCommandHandler.T.cs} (57%) rename DannyGoodacre.Core/CommandQuery/{UnitOfWorkCommandHandler.cs => TransactionCommandHandler.cs} (56%) delete mode 100644 DannyGoodacre.Core/Data/IUnitOfWork.cs diff --git a/DannyGoodacre.Core/CommandQuery/ICommand.cs b/DannyGoodacre.Core/CommandQuery/Abstractions/ICommand.cs similarity index 58% rename from DannyGoodacre.Core/CommandQuery/ICommand.cs rename to DannyGoodacre.Core/CommandQuery/Abstractions/ICommand.cs index b1fd776..f0fb032 100644 --- a/DannyGoodacre.Core/CommandQuery/ICommand.cs +++ b/DannyGoodacre.Core/CommandQuery/Abstractions/ICommand.cs @@ -1,4 +1,4 @@ -namespace DannyGoodacre.Core.CommandQuery; +namespace DannyGoodacre.Core.CommandQuery.Abstractions; /// /// A command request. diff --git a/DannyGoodacre.Core/CommandQuery/IQuery.cs b/DannyGoodacre.Core/CommandQuery/Abstractions/IQuery.cs similarity index 57% rename from DannyGoodacre.Core/CommandQuery/IQuery.cs rename to DannyGoodacre.Core/CommandQuery/Abstractions/IQuery.cs index 5e1114a..6b0c5a6 100644 --- a/DannyGoodacre.Core/CommandQuery/IQuery.cs +++ b/DannyGoodacre.Core/CommandQuery/Abstractions/IQuery.cs @@ -1,4 +1,4 @@ -namespace DannyGoodacre.Core.CommandQuery; +namespace DannyGoodacre.Core.CommandQuery.Abstractions; /// /// A query request. diff --git a/DannyGoodacre.Core/CommandQuery/Abstractions/ITransaction.cs b/DannyGoodacre.Core/CommandQuery/Abstractions/ITransaction.cs new file mode 100644 index 0000000..7b49430 --- /dev/null +++ b/DannyGoodacre.Core/CommandQuery/Abstractions/ITransaction.cs @@ -0,0 +1,8 @@ +namespace DannyGoodacre.Core.CommandQuery.Abstractions; + +public interface ITransaction : IAsyncDisposable +{ + Task CommitAsync(CancellationToken cancellationToken = default); + + Task RollbackAsync(CancellationToken cancellationToken = default); +} diff --git a/DannyGoodacre.Core/CommandQuery/Abstractions/ITransactionProvider.cs b/DannyGoodacre.Core/CommandQuery/Abstractions/ITransactionProvider.cs new file mode 100644 index 0000000..f666794 --- /dev/null +++ b/DannyGoodacre.Core/CommandQuery/Abstractions/ITransactionProvider.cs @@ -0,0 +1,6 @@ +namespace DannyGoodacre.Core.CommandQuery.Abstractions; + +public interface ITransactionProvider +{ + Task BeginTransactionAsync(CancellationToken cancellationToken = default); +} diff --git a/DannyGoodacre.Core/CommandQuery/Abstractions/IUnitOfWork.cs b/DannyGoodacre.Core/CommandQuery/Abstractions/IUnitOfWork.cs new file mode 100644 index 0000000..210bd4b --- /dev/null +++ b/DannyGoodacre.Core/CommandQuery/Abstractions/IUnitOfWork.cs @@ -0,0 +1,18 @@ +namespace DannyGoodacre.Core.CommandQuery.Abstractions; + +public interface IUnitOfWork +{ + /// + /// Persist all changes made during this operation to the underlying data store. + /// + /// A to observe while performing the operation. + /// The number of state entries written to the store. + Task SaveChangesAsync(CancellationToken cancellationToken = default); + + /// + /// Start a new transaction to ensure multiple operations succeed or fail as a single unit. + /// + /// A to observe while performing the operation. + /// An instance. + Task BeginTransactionAsync(CancellationToken cancellationToken = default); +} diff --git a/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs b/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs index 8690d7f..9430825 100644 --- a/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs +++ b/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs @@ -1,3 +1,4 @@ +using DannyGoodacre.Core.CommandQuery.Abstractions; using Microsoft.Extensions.Logging; namespace DannyGoodacre.Core.CommandQuery; diff --git a/DannyGoodacre.Core/CommandQuery/CommandHandler.cs b/DannyGoodacre.Core/CommandQuery/CommandHandler.cs index 3148a47..64f9eb6 100644 --- a/DannyGoodacre.Core/CommandQuery/CommandHandler.cs +++ b/DannyGoodacre.Core/CommandQuery/CommandHandler.cs @@ -1,3 +1,4 @@ +using DannyGoodacre.Core.CommandQuery.Abstractions; using Microsoft.Extensions.Logging; namespace DannyGoodacre.Core.CommandQuery; diff --git a/DannyGoodacre.Core/CommandQuery/QueryHandler.cs b/DannyGoodacre.Core/CommandQuery/QueryHandler.cs index e6a9ab1..966dda4 100644 --- a/DannyGoodacre.Core/CommandQuery/QueryHandler.cs +++ b/DannyGoodacre.Core/CommandQuery/QueryHandler.cs @@ -1,3 +1,4 @@ +using DannyGoodacre.Core.CommandQuery.Abstractions; using Microsoft.Extensions.Logging; namespace DannyGoodacre.Core.CommandQuery; diff --git a/DannyGoodacre.Core/CommandQuery/UnitOfWorkCommandHandler.T.cs b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.T.cs similarity index 57% rename from DannyGoodacre.Core/CommandQuery/UnitOfWorkCommandHandler.T.cs rename to DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.T.cs index ebae580..936ded6 100644 --- a/DannyGoodacre.Core/CommandQuery/UnitOfWorkCommandHandler.T.cs +++ b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.T.cs @@ -1,9 +1,9 @@ -using DannyGoodacre.Core.Data; +using DannyGoodacre.Core.CommandQuery.Abstractions; using Microsoft.Extensions.Logging; namespace DannyGoodacre.Core.CommandQuery; -public abstract class UnitOfWorkCommandHandler(ILogger logger, IUnitOfWork unitOfWork) +public abstract class TransactionalCommandHandler(ILogger logger, IUnitOfWork unitOfWork) : CommandHandler(logger) where TCommand : ICommand { /// @@ -28,29 +28,39 @@ public abstract class UnitOfWorkCommandHandler(ILogger logger /// protected async override Task> ExecuteAsync(TCommand command, CancellationToken cancellationToken) { - var result = await base.ExecuteAsync(command, cancellationToken); - - if (!result.IsSuccess) - { - return result; - } + await using var transaction = await unitOfWork.BeginTransactionAsync(cancellationToken); try { - var actualChanges = await unitOfWork.SaveChangesAsync(cancellationToken); + var result = await base.ExecuteAsync(command, cancellationToken); - if (ExpectedChanges == -1 || actualChanges == ExpectedChanges) + if (!result.IsSuccess) { + await transaction.RollbackAsync(cancellationToken); + return result; } - Logger.LogError("Command '{Command}' made an unexpected number of changes: Expected '{Expected}', Actual '{Actual}'.", CommandName, ExpectedChanges, actualChanges); + var actualChanges = await unitOfWork.SaveChangesAsync(cancellationToken); - return Result.InternalError("Unexpected number of changes saved."); + if (ExpectedChanges != -1 && actualChanges != ExpectedChanges) + { + await transaction.RollbackAsync(cancellationToken); + + Logger.LogError("Command '{Command}' attempted to persist an unexpected number of changes: Expected '{Expected}', Actual '{Actual}'.", CommandName, ExpectedChanges, actualChanges); + + return Result.InternalError("Database integrity check failed."); + } + + await transaction.CommitAsync(cancellationToken); + + return result; } catch (Exception ex) { - Logger.LogCritical(ex, "Command '{Command}' failed while saving changes, with exception: {Exception}", CommandName, ex.Message); + await transaction.RollbackAsync(cancellationToken); + + Logger.LogCritical("Command '{Command}' experienced a transaction failure: {Exception}", CommandName, ex.Message); return Result.InternalError(ex.Message); } diff --git a/DannyGoodacre.Core/CommandQuery/UnitOfWorkCommandHandler.cs b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.cs similarity index 56% rename from DannyGoodacre.Core/CommandQuery/UnitOfWorkCommandHandler.cs rename to DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.cs index 3c574a4..7123fcf 100644 --- a/DannyGoodacre.Core/CommandQuery/UnitOfWorkCommandHandler.cs +++ b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.cs @@ -1,9 +1,9 @@ -using DannyGoodacre.Core.Data; +using DannyGoodacre.Core.CommandQuery.Abstractions; using Microsoft.Extensions.Logging; namespace DannyGoodacre.Core.CommandQuery; -public abstract class UnitOfWorkCommandHandler(ILogger logger, IUnitOfWork unitOfWork) +public abstract class TransactionCommandHandler(ILogger logger, IUnitOfWork unitOfWork) : CommandHandler(logger) where TCommand : ICommand { /// @@ -28,29 +28,39 @@ public abstract class UnitOfWorkCommandHandler(ILogger logger, IUnitOf /// protected async override Task ExecuteAsync(TCommand command, CancellationToken cancellationToken) { - var result = await base.ExecuteAsync(command, cancellationToken); - - if (!result.IsSuccess) - { - return result; - } + await using var transaction = await unitOfWork.BeginTransactionAsync(cancellationToken); try { - var actualChanges = await unitOfWork.SaveChangesAsync(cancellationToken); + var result = await base.ExecuteAsync(command, cancellationToken); - if (ExpectedChanges == -1 || actualChanges == ExpectedChanges) + if (!result.IsSuccess) { + await transaction.RollbackAsync(cancellationToken); + return result; } - Logger.LogError("Command '{Command}' made an unexpected number of changes: Expected '{Expected}', Actual '{Actual}'.", CommandName, ExpectedChanges, actualChanges); + var actualChanges = await unitOfWork.SaveChangesAsync(cancellationToken); - return Result.InternalError("Unexpected number of changes saved."); + if (ExpectedChanges != -1 && actualChanges != ExpectedChanges) + { + await transaction.RollbackAsync(cancellationToken); + + Logger.LogError("Command '{Command}' attempted to persist an unexpected number of changes: Expected '{Expected}', Actual '{Actual}'.", CommandName, ExpectedChanges, actualChanges); + + return Result.InternalError("Database integrity check failed."); + } + + await transaction.CommitAsync(cancellationToken); + + return result; } catch (Exception ex) { - Logger.LogCritical(ex, "Command '{Command}' failed while saving changes, with exception: {Exception}", CommandName, ex.Message); + await transaction.RollbackAsync(cancellationToken); + + Logger.LogCritical("Command '{Command}' experienced a transaction failure: {Exception}", CommandName, ex.Message); return Result.InternalError(ex.Message); } diff --git a/DannyGoodacre.Core/Data/IUnitOfWork.cs b/DannyGoodacre.Core/Data/IUnitOfWork.cs deleted file mode 100644 index 8b430e0..0000000 --- a/DannyGoodacre.Core/Data/IUnitOfWork.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace DannyGoodacre.Core.Data; - -public interface IUnitOfWork -{ - /// - /// Persist all changes made during this operation to the underlying data store. - /// - /// A to observe while performing the operation. - /// The number of state entries written to the store. - Task SaveChangesAsync(CancellationToken cancellationToken = default); -} diff --git a/DannyGoodacre.Core/Extensions/ServiceCollectionExtensions.cs b/DannyGoodacre.Core/Extensions/ServiceCollectionExtensions.cs index 5785065..fc1e0e1 100644 --- a/DannyGoodacre.Core/Extensions/ServiceCollectionExtensions.cs +++ b/DannyGoodacre.Core/Extensions/ServiceCollectionExtensions.cs @@ -7,41 +7,45 @@ namespace SystemMonitor.Core; public static class ServiceCollectionExtensions { - public static IServiceCollection AddCommandHandlers(this IServiceCollection services, params Assembly[] assemblies) + extension(IServiceCollection services) { - var handlerTypes = assemblies - .SelectMany(x => x.GetTypes()) - .Where(x => x is { IsAbstract: false, IsClass: true } && x.IsCommandHandler()); - - foreach (var handlerType in handlerTypes) + public IServiceCollection AddCommandHandlers(params Assembly[] assemblies) { - var interfaces = handlerType.GetInterfaces(); + var handlerTypes = assemblies + .SelectMany(x => x.GetTypes()) + .Where(x => x is { IsAbstract: false, IsClass: true } && x.IsCommandHandler()); - foreach (var serviceType in interfaces) + foreach (var handlerType in handlerTypes) { - services.AddScoped(serviceType, handlerType); - } - } + var interfaces = handlerType.GetInterfaces(); - return services; - } + foreach (var serviceType in interfaces) + { + services.AddScoped(serviceType, handlerType); + } + } - public static IServiceCollection AddQueryHandlers(this IServiceCollection services, params Assembly[] assemblies) - { - var handlerTypes = assemblies - .SelectMany(x => x.GetTypes()) - .Where(x => x is { IsAbstract: false, IsClass: true } && x.IsQueryHandler()); + return services; + } - foreach (var handlerType in handlerTypes) + public IServiceCollection AddQueryHandlers(params Assembly[] assemblies) { - var interfaces = handlerType.GetInterfaces(); + var handlerTypes = assemblies + .SelectMany(x => x.GetTypes()) + .Where(x => x is { IsAbstract: false, IsClass: true } && x.IsQueryHandler()); - foreach (var serviceType in interfaces) + foreach (var handlerType in handlerTypes) { - services.AddScoped(serviceType, handlerType); + var interfaces = handlerType.GetInterfaces(); + + foreach (var serviceType in interfaces) + { + services.AddScoped(serviceType, handlerType); + } } - } - return services; + return services; + } } + } diff --git a/DannyGoodacre.Core/Extensions/TypeExtensions.cs b/DannyGoodacre.Core/Extensions/TypeExtensions.cs index b82bfe5..24db959 100644 --- a/DannyGoodacre.Core/Extensions/TypeExtensions.cs +++ b/DannyGoodacre.Core/Extensions/TypeExtensions.cs @@ -4,50 +4,54 @@ namespace DannyGoodacre.Core.Extensions; internal static class TypeExtensions { - public static bool IsCommandHandler(this Type type) + extension(Type type) { - var baseType = type.BaseType; - - while (baseType is not null) + public bool IsCommandHandler() { - if (baseType.IsGenericType) - { - var definition = baseType.GetGenericTypeDefinition(); + var baseType = type.BaseType; - if (definition == typeof(CommandHandler<>) - || definition == typeof(CommandHandler<,>) - || definition == typeof(UnitOfWorkCommandHandler<>) - || definition == typeof(UnitOfWorkCommandHandler<,>)) + while (baseType is not null) + { + if (baseType.IsGenericType) { - return true; + var definition = baseType.GetGenericTypeDefinition(); + + if (definition == typeof(CommandHandler<>) + || definition == typeof(CommandHandler<,>) + || definition == typeof(TransactionCommandHandler<>) + || definition == typeof(TransactionalCommandHandler<,>)) + { + return true; + } } + + baseType = baseType.BaseType; } - baseType = baseType.BaseType; + return false; } - return false; - } - - public static bool IsQueryHandler(this Type type) - { - var baseType = type.BaseType; - - while (baseType is not null) + public bool IsQueryHandler() { - if (baseType.IsGenericType) - { - var definition = baseType.GetGenericTypeDefinition(); + var baseType = type.BaseType; - if (definition == typeof(QueryHandler<,>)) + while (baseType is not null) + { + if (baseType.IsGenericType) { - return true; + var definition = baseType.GetGenericTypeDefinition(); + + if (definition == typeof(QueryHandler<,>)) + { + return true; + } } + + baseType = baseType.BaseType; } - baseType = baseType.BaseType; + return false; } - - return false; } + } From e29286f163f25664b7acd2610d402ca016601f19 Mon Sep 17 00:00:00 2001 From: Danny Goodacre Date: Sat, 10 Jan 2026 15:38:32 +0000 Subject: [PATCH 02/10] WIP --- .../CommandQuery/CommandHandlerTests.cs | 32 +-- .../CommandQuery/CommandHandlerValueTests.cs | 4 +- .../CommandQuery/QueryHandlerTests.cs | 4 +- .../TransactionCommandHandlerTests.cs | 216 +++++++++++++++++ .../TransactionCommandHandlerValueTests.cs | 220 ++++++++++++++++++ .../UnitOfWorkCommandHandlerTests.cs | 158 ------------- .../UnitOfWorkCommandHandlerValueTests.cs | 160 ------------- .../DannyGoodacre.Core.Tests.csproj | 1 + .../ServiceCollectionExtensionsTests.cs | 34 +-- .../Extensions/TypeExtensionsTests.cs | 17 +- .../{ICommand.cs => ICommandRequest.cs} | 2 +- .../{IQuery.cs => IQueryRequest.cs} | 2 +- .../CommandQuery/CommandHandler.T.cs | 2 +- .../CommandQuery/CommandHandler.cs | 2 +- .../CommandQuery/QueryHandler.cs | 2 +- .../TransactionCommandHandler.T.cs | 4 +- .../CommandQuery/TransactionCommandHandler.cs | 4 +- .../Extensions/TypeExtensions.cs | 2 +- 18 files changed, 494 insertions(+), 372 deletions(-) create mode 100644 DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerTests.cs create mode 100644 DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs delete mode 100644 DannyGoodacre.Core.Tests/CommandQuery/UnitOfWorkCommandHandlerTests.cs delete mode 100644 DannyGoodacre.Core.Tests/CommandQuery/UnitOfWorkCommandHandlerValueTests.cs rename DannyGoodacre.Core/CommandQuery/Abstractions/{ICommand.cs => ICommandRequest.cs} (76%) rename DannyGoodacre.Core/CommandQuery/Abstractions/{IQuery.cs => IQueryRequest.cs} (76%) diff --git a/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerTests.cs index 7f08142..8bb6a2f 100644 --- a/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerTests.cs +++ b/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerTests.cs @@ -1,13 +1,13 @@ using DannyGoodacre.Core.CommandQuery; +using DannyGoodacre.Core.CommandQuery.Abstractions; using Microsoft.Extensions.Logging; -using Moq; namespace DannyGoodacre.Core.Tests.CommandQuery; [TestFixture] public class CommandHandlerTests : TestBase { - public class TestCommand : ICommand; + public class TestCommandRequest : ICommandRequest; private const string TestName = "Test Command Handler"; @@ -19,26 +19,26 @@ public class TestCommand : ICommand; private readonly CancellationToken _testCancellationToken = CancellationToken.None; - private readonly TestCommand _testCommand = new(); + private readonly TestCommandRequest _testCommandRequest = new(); - private static Action _validate = (_, _) => {}; + private static Action _validate = (_, _) => {}; - private static Func> _internalExecuteAsync = (_, _) => Task.FromResult(new Result()); + private static Func> _internalExecuteAsync = (_, _) => Task.FromResult(new Result()); private Mock> _loggerMock = null!; - public class TestCommandHandler(ILogger logger) : CommandHandler(logger) + public class TestCommandHandler(ILogger logger) : CommandHandler(logger) { protected override string CommandName => TestName; - protected override void Validate(ValidationState validationState, TestCommand command) - => _validate(validationState, command); + protected override void Validate(ValidationState validationState, TestCommandRequest commandRequest) + => _validate(validationState, commandRequest); - protected override Task InternalExecuteAsync(TestCommand command, CancellationToken cancellationToken) - => _internalExecuteAsync(command, cancellationToken); + protected override Task InternalExecuteAsync(TestCommandRequest commandRequest, CancellationToken cancellationToken) + => _internalExecuteAsync(commandRequest, cancellationToken); - public Task TestExecuteAsync(TestCommand command, CancellationToken cancellationToken) - => ExecuteAsync(command, cancellationToken); + public Task TestExecuteAsync(TestCommandRequest commandRequest, CancellationToken cancellationToken) + => ExecuteAsync(commandRequest, cancellationToken); } [Test] @@ -55,7 +55,7 @@ public async Task ExecuteAsync_WhenValidationFails_ShouldReturnInvalid() var handler = new TestCommandHandler(_loggerMock.Object); // Act - var result = await handler.TestExecuteAsync(_testCommand, _testCancellationToken); + var result = await handler.TestExecuteAsync(_testCommandRequest, _testCancellationToken); // Assert AssertInvalid(result); @@ -76,7 +76,7 @@ public async Task ExecuteAsync_WhenCancelled_ShouldReturnCancelled() await cancellationTokenSource.CancelAsync(); // Act - var result = await handler.TestExecuteAsync(_testCommand, cancellationTokenSource.Token); + var result = await handler.TestExecuteAsync(_testCommandRequest, cancellationTokenSource.Token); // Assert AssertCancelled(result); @@ -95,7 +95,7 @@ public async Task ExecuteAsync_WhenCancelledDuring_ShouldReturnCancelled() var handler = new TestCommandHandler(_loggerMock.Object); // Act - var result = await handler.TestExecuteAsync(_testCommand, _testCancellationToken); + var result = await handler.TestExecuteAsync(_testCommandRequest, _testCancellationToken); // Assert AssertCancelled(result); @@ -119,7 +119,7 @@ public async Task ExecuteAsync_WhenExceptionOccurs_ShouldReturnInternalError() var handler = new TestCommandHandler(_loggerMock.Object); // Act - var result = await handler.TestExecuteAsync(_testCommand, _testCancellationToken); + var result = await handler.TestExecuteAsync(_testCommandRequest, _testCancellationToken); // Assert AssertInternalError(result, TestExceptionMessage); diff --git a/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerValueTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerValueTests.cs index 36a657e..50d4c01 100644 --- a/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerValueTests.cs +++ b/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerValueTests.cs @@ -1,13 +1,13 @@ using DannyGoodacre.Core.CommandQuery; +using DannyGoodacre.Core.CommandQuery.Abstractions; using Microsoft.Extensions.Logging; -using Moq; namespace DannyGoodacre.Core.Tests.CommandQuery; [TestFixture] public class CommandHandlerValueTests : TestBase { - public class TestCommand : ICommand; + public class TestCommand : ICommandRequest; private const string TestName = "Test Command Handler"; diff --git a/DannyGoodacre.Core.Tests/CommandQuery/QueryHandlerTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/QueryHandlerTests.cs index ac1c40a..1730eb7 100644 --- a/DannyGoodacre.Core.Tests/CommandQuery/QueryHandlerTests.cs +++ b/DannyGoodacre.Core.Tests/CommandQuery/QueryHandlerTests.cs @@ -1,13 +1,13 @@ using DannyGoodacre.Core.CommandQuery; +using DannyGoodacre.Core.CommandQuery.Abstractions; using Microsoft.Extensions.Logging; -using Moq; namespace DannyGoodacre.Core.Tests.CommandQuery; [TestFixture] public class QueryHandlerTests : TestBase { - public class TestQuery : IQuery; + public class TestQuery : IQueryRequest; private const string TestName = "Test Query Handler"; diff --git a/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerTests.cs new file mode 100644 index 0000000..c0bbeed --- /dev/null +++ b/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerTests.cs @@ -0,0 +1,216 @@ +using DannyGoodacre.Core.CommandQuery; +using DannyGoodacre.Core.CommandQuery.Abstractions; +using Microsoft.Extensions.Logging; + +namespace DannyGoodacre.Core.Tests.CommandQuery; + +[TestFixture] +public class TransactionCommandHandlerTests : TestBase +{ + public class TestCommand : ICommandRequest; + + private const string TestCommandName = "Test Transaction Command Handler"; + + private static int _testExpectedChanges; + + private static int _testActualChanges; + + private readonly CancellationToken _testCancellationToken = CancellationToken.None; + + private readonly TestCommand _testCommand = new(); + + private Mock> _loggerMock = null!; + + private Mock _unitOfWorkMock = null!; + + private Mock _testTransaction = null!; + + private static Func> _internalExecuteAsync = null!; + + private static TestTransactionCommandHandler _testHandler = null!; + + public class TestTransactionCommandHandler(ILogger logger, IUnitOfWork unitOfWork) + : TransactionCommandHandler(logger, unitOfWork) + { + protected override string CommandName => TestCommandName; + + protected override int ExpectedChanges => _testExpectedChanges; + + protected override Task InternalExecuteAsync(TestCommand command, CancellationToken cancellationToken) + => _internalExecuteAsync(command, cancellationToken); + + public Task TestExecuteAsync(TestCommand command, CancellationToken cancellationToken) + => ExecuteAsync(command, cancellationToken); + } + + [SetUp] + public void SetUp() + { + _loggerMock = new Mock>(MockBehavior.Strict); + + _unitOfWorkMock = new Mock(MockBehavior.Strict); + + _internalExecuteAsync = (_, _) => Task.FromResult(Result.Success()); + + _testHandler = new TestTransactionCommandHandler(_loggerMock.Object, _unitOfWorkMock.Object); + + _testExpectedChanges = -1; + + _testTransaction = new Mock(); + } + + [Test] + public async Task ExecuteAsync_WhenNotSuccessful_ShouldRollbackAndReturnResult() + { + // Arrange + SetupUnitOfWork_BeginTransactionAsync(); + + const string testError = "Test Internal Error"; + + _internalExecuteAsync = (_, _) => Task.FromResult(Result.InternalError(testError)); + + SetupTransaction_RollbackAsync(); + + SetupTransaction_DisposeAsync(); + + // Act + var result = await Act(); + + // Assert + AssertInternalError(result, testError); + } + + [Test] + public async Task ExecuteAsync_WhenSuccessfulAndInvalidNumberOfChanges_ShouldRollbackAndReturnInternalError() + { + // Arrange + SetupUnitOfWork_BeginTransactionAsync(); + + _testExpectedChanges = 123; + + _testActualChanges = 456; + + SetupUnitOfWork_SaveChangesAsync(); + + SetupTransaction_RollbackAsync(); + + _loggerMock.Setup(LogLevel.Error, $"Command '{TestCommandName}' attempted to persist an unexpected number of changes: Expected '{_testExpectedChanges}', Actual '{_testActualChanges}'."); + + SetupTransaction_DisposeAsync(); + + // Act + var result = await Act(); + + // Assert + AssertInternalError(result, "Database integrity check failed."); + + } + + [Test] + public async Task ExecuteAsync_WhenSuccessfulAndValidNumberOfChanges_ShouldCommitAndReturnSuccess() + { + // Arrange + SetupUnitOfWork_BeginTransactionAsync(); + + _testExpectedChanges = 123; + + _testActualChanges = 123; + + SetupUnitOfWork_SaveChangesAsync(); + + SetupTransaction_CommitAsync(); + + SetupTransaction_DisposeAsync(); + + // Act + var result = await Act(); + + // Assert + AssertSuccess(result); + } + + [Test] + public async Task ExecuteAsync_WhenSuccessfulAndNotValidatingChanges_ShouldCommitAndReturnSuccess() + { + // Arrange + SetupUnitOfWork_BeginTransactionAsync(); + + SetupUnitOfWork_SaveChangesAsync(); + + SetupTransaction_CommitAsync(); + + SetupTransaction_DisposeAsync(); + + // Act + var result = await Act(); + + // Assert + AssertSuccess(result); + } + + [Test] + public async Task ExecuteAsync_WhenSuccessfulAndExceptionOccurs_ShouldRollbackAndReturnInternalError() + { + // Arrange + SetupUnitOfWork_BeginTransactionAsync(); + + const string testError = "Test Internal Error"; + + var exception = new Exception(testError); + + _unitOfWorkMock + .Setup(x => x.SaveChangesAsync( + It.Is(y => y == _testCancellationToken))) + .ThrowsAsync(exception) + .Verifiable(Times.Once); + + SetupTransaction_RollbackAsync(); + + _loggerMock.Setup(LogLevel.Critical, $"Command '{TestCommandName}' experienced a transaction failure: {testError}"); + + SetupTransaction_DisposeAsync(); + + // Act + var result = await Act(); + + // Assert + AssertInternalError(result, testError); + } + + private Task Act() + => _testHandler.TestExecuteAsync(_testCommand, _testCancellationToken); + + private void SetupUnitOfWork_BeginTransactionAsync() + => _unitOfWorkMock + .Setup(x => x.BeginTransactionAsync( + It.Is(y => y == _testCancellationToken))) + .ReturnsAsync(_testTransaction.Object) + .Verifiable(Times.Once); + + private void SetupUnitOfWork_SaveChangesAsync() + => _unitOfWorkMock + .Setup(x => x.SaveChangesAsync( + It.Is(y => y == _testCancellationToken))) + .ReturnsAsync(_testActualChanges) + .Verifiable(Times.Once); + + private void SetupTransaction_RollbackAsync() + => _testTransaction + .Setup(x => x.RollbackAsync( + It.Is(y => y == _testCancellationToken))) + .Returns(Task.CompletedTask) + .Verifiable(Times.Once); + + private void SetupTransaction_DisposeAsync() + => _testTransaction + .Setup(x => x.DisposeAsync()) + .Returns(ValueTask.CompletedTask) + .Verifiable(Times.Once); + + private void SetupTransaction_CommitAsync() + => _testTransaction + .Setup(x => x.CommitAsync( + It.Is(y => y == _testCancellationToken))) + .Returns(Task.CompletedTask) + .Verifiable(Times.Once); +} diff --git a/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs new file mode 100644 index 0000000..080b6e1 --- /dev/null +++ b/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs @@ -0,0 +1,220 @@ +using DannyGoodacre.Core.CommandQuery; +using DannyGoodacre.Core.CommandQuery.Abstractions; +using Microsoft.Extensions.Logging; + +namespace DannyGoodacre.Core.Tests.CommandQuery; + +[TestFixture] +public class TransactionCommandWithValueHandlerTests : TestBase +{ + public class TestCommand : ICommandRequest; + + private const string TestCommandName = "Test Transaction Command Handler"; + + private static int _testResultValue; + + private static int _testExpectedChanges; + + private static int _testActualChanges; + + private readonly CancellationToken _testCancellationToken = CancellationToken.None; + + private readonly TestCommand _testCommand = new(); + + private Mock> _loggerMock = null!; + + private Mock _unitOfWorkMock = null!; + + private Mock _testTransaction = null!; + + private static Func>> _internalExecuteAsync = null!; + + private static TestTransactionCommandWithValueHandler _testHandler = null!; + + public class TestTransactionCommandWithValueHandler(ILogger logger, IUnitOfWork unitOfWork) + : TransactionCommandHandler(logger, unitOfWork) + { + protected override string CommandName => TestCommandName; + + protected override int ExpectedChanges => _testExpectedChanges; + + protected override Task> InternalExecuteAsync(TestCommand command, CancellationToken cancellationToken) + => _internalExecuteAsync(command, cancellationToken); + + public Task> TestExecuteAsync(TestCommand command, CancellationToken cancellationToken) + => ExecuteAsync(command, cancellationToken); + } + + [SetUp] + public void SetUp() + { + _loggerMock = new Mock>(MockBehavior.Strict); + + _unitOfWorkMock = new Mock(MockBehavior.Strict); + + _testResultValue = 123; + + _internalExecuteAsync = (_, _) => Task.FromResult(Result.Success(_testResultValue)); + + _testHandler = new TestTransactionCommandWithValueHandler(_loggerMock.Object, _unitOfWorkMock.Object); + + _testExpectedChanges = -1; + + _testTransaction = new Mock(); + } + + [Test] + public async Task ExecuteAsync_WhenNotSuccessful_ShouldRollbackAndReturnResult() + { + // Arrange + SetupUnitOfWork_BeginTransactionAsync(); + + const string testError = "Test Internal Error"; + + _internalExecuteAsync = (_, _) => Task.FromResult(Result.InternalError(testError)); + + SetupTransaction_RollbackAsync(); + + SetupTransaction_DisposeAsync(); + + // Act + var result = await Act(); + + // Assert + AssertInternalError(result, testError); + } + + [Test] + public async Task ExecuteAsync_WhenSuccessfulAndInvalidNumberOfChanges_ShouldRollbackAndReturnInternalError() + { + // Arrange + SetupUnitOfWork_BeginTransactionAsync(); + + _testExpectedChanges = 123; + + _testActualChanges = 456; + + SetupUnitOfWork_SaveChangesAsync(); + + SetupTransaction_RollbackAsync(); + + _loggerMock.Setup(LogLevel.Error, $"Command '{TestCommandName}' attempted to persist an unexpected number of changes: Expected '{_testExpectedChanges}', Actual '{_testActualChanges}'."); + + SetupTransaction_DisposeAsync(); + + // Act + var result = await Act(); + + // Assert + AssertInternalError(result, "Database integrity check failed."); + + } + + [Test] + public async Task ExecuteAsync_WhenSuccessfulAndValidNumberOfChanges_ShouldCommitAndReturnSuccess() + { + // Arrange + SetupUnitOfWork_BeginTransactionAsync(); + + _testExpectedChanges = 123; + + _testActualChanges = 123; + + SetupUnitOfWork_SaveChangesAsync(); + + SetupTransaction_CommitAsync(); + + SetupTransaction_DisposeAsync(); + + // Act + var result = await Act(); + + // Assert + AssertSuccess(result); + } + + [Test] + public async Task ExecuteAsync_WhenSuccessfulAndNotValidatingChanges_ShouldCommitAndReturnSuccess() + { + // Arrange + SetupUnitOfWork_BeginTransactionAsync(); + + SetupUnitOfWork_SaveChangesAsync(); + + SetupTransaction_CommitAsync(); + + SetupTransaction_DisposeAsync(); + + // Act + var result = await Act(); + + // Assert + AssertSuccess(result); + } + + [Test] + public async Task ExecuteAsync_WhenSuccessfulAndExceptionOccurs_ShouldRollbackAndReturnInternalError() + { + // Arrange + SetupUnitOfWork_BeginTransactionAsync(); + + const string testError = "Test Internal Error"; + + var exception = new Exception(testError); + + _unitOfWorkMock + .Setup(x => x.SaveChangesAsync( + It.Is(y => y == _testCancellationToken))) + .ThrowsAsync(exception) + .Verifiable(Times.Once); + + SetupTransaction_RollbackAsync(); + + _loggerMock.Setup(LogLevel.Critical, $"Command '{TestCommandName}' experienced a transaction failure: {testError}"); + + SetupTransaction_DisposeAsync(); + + // Act + var result = await Act(); + + // Assert + AssertInternalError(result, testError); + } + + private Task> Act() + => _testHandler.TestExecuteAsync(_testCommand, _testCancellationToken); + + private void SetupUnitOfWork_BeginTransactionAsync() + => _unitOfWorkMock + .Setup(x => x.BeginTransactionAsync( + It.Is(y => y == _testCancellationToken))) + .ReturnsAsync(_testTransaction.Object) + .Verifiable(Times.Once); + + private void SetupUnitOfWork_SaveChangesAsync() + => _unitOfWorkMock + .Setup(x => x.SaveChangesAsync( + It.Is(y => y == _testCancellationToken))) + .ReturnsAsync(_testActualChanges) + .Verifiable(Times.Once); + + private void SetupTransaction_RollbackAsync() + => _testTransaction + .Setup(x => x.RollbackAsync( + It.Is(y => y == _testCancellationToken))) + .Returns(Task.CompletedTask) + .Verifiable(Times.Once); + + private void SetupTransaction_DisposeAsync() + => _testTransaction + .Setup(x => x.DisposeAsync()) + .Returns(ValueTask.CompletedTask) + .Verifiable(Times.Once); + + private void SetupTransaction_CommitAsync() + => _testTransaction + .Setup(x => x.CommitAsync( + It.Is(y => y == _testCancellationToken))) + .Returns(Task.CompletedTask) + .Verifiable(Times.Once); +} diff --git a/DannyGoodacre.Core.Tests/CommandQuery/UnitOfWorkCommandHandlerTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/UnitOfWorkCommandHandlerTests.cs deleted file mode 100644 index 38d6cdd..0000000 --- a/DannyGoodacre.Core.Tests/CommandQuery/UnitOfWorkCommandHandlerTests.cs +++ /dev/null @@ -1,158 +0,0 @@ -using DannyGoodacre.Core.CommandQuery; -using DannyGoodacre.Core.Data; -using Microsoft.Extensions.Logging; -using Moq; - -namespace DannyGoodacre.Core.Tests.CommandQuery; - -[TestFixture] -public class UnitOfWorkCommandHandlerTests : TestBase -{ - public class TestCommand : ICommand; - - private const string TestCommandName = "Test Unit Of Work Command Handler"; - - private static int _testExpectedChanges; - - private static int _testActualChanges; - - private readonly CancellationToken _testCancellationToken = CancellationToken.None; - - private Mock> _loggerMock = null!; - - private Mock _unitOfWorkMock = null!; - - private readonly TestCommand _testCommand = new(); - - private static Func> _internalExecuteAsync = null!; - - private static TestUnitOfWorkCommandHandler _testHandler = null!; - - public class TestUnitOfWorkCommandHandler(ILogger logger, IUnitOfWork unitOfWork) - : UnitOfWorkCommandHandler(logger, unitOfWork) - { - protected override string CommandName => TestCommandName; - - protected override int ExpectedChanges => _testExpectedChanges; - - protected override Task InternalExecuteAsync(TestCommand command, CancellationToken cancellationToken) - => _internalExecuteAsync(command, cancellationToken); - - public Task TestExecuteAsync(TestCommand command, CancellationToken cancellationToken) - => ExecuteAsync(command, cancellationToken); - } - - [SetUp] - public void SetUp() - { - _loggerMock = new Mock>(MockBehavior.Strict); - - _unitOfWorkMock = new Mock(MockBehavior.Strict); - - _internalExecuteAsync = (_, _) => Task.FromResult(Result.Success()); - - _testExpectedChanges = -1; - - _testHandler = new TestUnitOfWorkCommandHandler(_loggerMock.Object, _unitOfWorkMock.Object); - } - - [Test] - public async Task ExecuteAsync_WhenNotSuccessful_ShouldReturnResult() - { - // Arrange - const string testError = "Test Internal Error"; - - _internalExecuteAsync = (_, _) => Task.FromResult(Result.InternalError(testError)); - - // Act - var result = await Act(); - - // Assert - AssertInternalError(result, testError); - } - - [Test] - public async Task ExecuteAsync_WhenSuccessfulAndNotValidatingChanges_ShouldReturnSuccess() - { - // Arrange - _testActualChanges = 456; - - SetupUnitOfWork_SaveChangesAsync(); - - // Act - var result = await Act(); - - // Assert - AssertSuccess(result); - } - - [Test] - public async Task ExecuteAsync_WhenSuccessfulAndChangesValid_ShouldReturnSuccess() - { - // Arrange - _testExpectedChanges = 123; - - _testActualChanges = 123; - - SetupUnitOfWork_SaveChangesAsync(); - - // Act - var result = await Act(); - - // Assert - AssertSuccess(result); - } - - [Test] - public async Task ExecuteAsync_WhenSuccessfulAndInvalidChanges_ShouldReturnInternalError() - { - // Arrange - _testExpectedChanges = 123; - - _testActualChanges = 456; - - SetupUnitOfWork_SaveChangesAsync(); - - _loggerMock.Setup(LogLevel.Error, $"Command '{TestCommandName}' made an unexpected number of changes: Expected '{_testExpectedChanges}', Actual '{_testActualChanges}'."); - - // Act - var result = await Act(); - - // Assert - AssertInternalError(result, "Unexpected number of changes saved."); - } - - [Test] - public async Task ExecuteAsync_WhenSuccessfulAndExceptionOccursDuringSaving_ShouldReturnInternalError() - { - // Arrange - _testActualChanges = 456; - - const string testError = "Test Internal Error"; - - var exception = new Exception(testError); - - _unitOfWorkMock - .Setup(x => x.SaveChangesAsync( - It.Is(y => y == _testCancellationToken))) - .ThrowsAsync(exception) - .Verifiable(Times.Once); - - _loggerMock.Setup(LogLevel.Critical, $"Command '{TestCommandName}' failed while saving changes, with exception: {testError}", exception: exception); - - // Act - var result = await Act(); - - // Assert - AssertInternalError(result, testError); - } - - private Task Act() => _testHandler.TestExecuteAsync(_testCommand, _testCancellationToken); - - private void SetupUnitOfWork_SaveChangesAsync() - => _unitOfWorkMock - .Setup(x => x.SaveChangesAsync( - It.Is(y => y == _testCancellationToken))) - .ReturnsAsync(_testActualChanges) - .Verifiable(Times.Once); -} diff --git a/DannyGoodacre.Core.Tests/CommandQuery/UnitOfWorkCommandHandlerValueTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/UnitOfWorkCommandHandlerValueTests.cs deleted file mode 100644 index 683dca4..0000000 --- a/DannyGoodacre.Core.Tests/CommandQuery/UnitOfWorkCommandHandlerValueTests.cs +++ /dev/null @@ -1,160 +0,0 @@ -using DannyGoodacre.Core.CommandQuery; -using DannyGoodacre.Core.Data; -using Microsoft.Extensions.Logging; -using Moq; - -namespace DannyGoodacre.Core.Tests.CommandQuery; - -[TestFixture] -public class UnitOfWorkCommandHandlerValueTests : TestBase -{ - public class TestCommand : ICommand; - - private const string TestCommandName = "Test Unit Of Work Command With Value Handler"; - - private static int _testExpectedChanges; - - private static int _testActualChanges; - - private const int TestResultValue = 789; - - private readonly CancellationToken _testCancellationToken = CancellationToken.None; - - private Mock> _loggerMock = null!; - - private Mock _unitOfWorkMock = null!; - - private readonly TestCommand _testCommand = new(); - - private static Func>> _internalExecuteAsync = null!; - - private static TestUnitOfWorkCommandWithValueHandler _testHandler = null!; - - public class TestUnitOfWorkCommandWithValueHandler(ILogger logger, IUnitOfWork unitOfWork) - : UnitOfWorkCommandHandler(logger, unitOfWork) - { - protected override string CommandName => TestCommandName; - - protected override int ExpectedChanges => _testExpectedChanges; - - protected override Task> InternalExecuteAsync(TestCommand command, CancellationToken cancellationToken) - => _internalExecuteAsync(command, cancellationToken); - - public Task> TestExecuteAsync(TestCommand command, CancellationToken cancellationToken) - => ExecuteAsync(command, cancellationToken); - } - - [SetUp] - public void SetUp() - { - _loggerMock = new Mock>(MockBehavior.Strict); - - _unitOfWorkMock = new Mock(MockBehavior.Strict); - - _internalExecuteAsync = (_, _) => Task.FromResult(Result.Success(TestResultValue)); - - _testExpectedChanges = -1; - - _testHandler = new TestUnitOfWorkCommandWithValueHandler(_loggerMock.Object, _unitOfWorkMock.Object); - } - - [Test] - public async Task ExecuteAsync_WhenNotSuccessful_ShouldReturnResult() - { - // Arrange - const string testError = "Test Internal Error"; - - _internalExecuteAsync = (_, _) => Task.FromResult(Result.InternalError(testError)); - - // Act - var result = await Act(); - - // Assert - AssertInternalError(result, testError); - } - - [Test] - public async Task ExecuteAsync_WhenSuccessfulAndNotValidatingChanges_ShouldReturnSuccess() - { - // Arrange - _testActualChanges = 456; - - SetupUnitOfWork_SaveChangesAsync(); - - // Act - var result = await Act(); - - // Assert - AssertSuccess(result); - } - - [Test] - public async Task ExecuteAsync_WhenSuccessfulAndChangesValid_ShouldReturnSuccess() - { - // Arrange - _testExpectedChanges = 123; - - _testActualChanges = 123; - - SetupUnitOfWork_SaveChangesAsync(); - - // Act - var result = await Act(); - - // Assert - AssertSuccess(result); - } - - [Test] - public async Task ExecuteAsync_WhenSuccessfulAndInvalidChanges_ShouldReturnInternalError() - { - // Arrange - _testExpectedChanges = 123; - - _testActualChanges = 456; - - SetupUnitOfWork_SaveChangesAsync(); - - _loggerMock.Setup(LogLevel.Error, $"Command '{TestCommandName}' made an unexpected number of changes: Expected '{_testExpectedChanges}', Actual '{_testActualChanges}'."); - - // Act - var result = await Act(); - - // Assert - AssertInternalError(result, "Unexpected number of changes saved."); - } - - [Test] - public async Task ExecuteAsync_WhenSuccessfulAndExceptionOccursDuringSaving_ShouldReturnInternalError() - { - // Arrange - _testActualChanges = 456; - - const string testError = "Test Internal Error"; - - var exception = new Exception(testError); - - _unitOfWorkMock - .Setup(x => x.SaveChangesAsync( - It.Is(y => y == _testCancellationToken))) - .ThrowsAsync(exception) - .Verifiable(Times.Once); - - _loggerMock.Setup(LogLevel.Critical, $"Command '{TestCommandName}' failed while saving changes, with exception: {testError}", exception: exception); - - // Act - var result = await Act(); - - // Assert - AssertInternalError(result, testError); - } - - private Task> Act() => _testHandler.TestExecuteAsync(_testCommand, _testCancellationToken); - - private void SetupUnitOfWork_SaveChangesAsync() - => _unitOfWorkMock - .Setup(x => x.SaveChangesAsync( - It.Is(y => y == _testCancellationToken))) - .ReturnsAsync(_testActualChanges) - .Verifiable(Times.Once); -} diff --git a/DannyGoodacre.Core.Tests/DannyGoodacre.Core.Tests.csproj b/DannyGoodacre.Core.Tests/DannyGoodacre.Core.Tests.csproj index 779b4a6..7beaf67 100644 --- a/DannyGoodacre.Core.Tests/DannyGoodacre.Core.Tests.csproj +++ b/DannyGoodacre.Core.Tests/DannyGoodacre.Core.Tests.csproj @@ -24,6 +24,7 @@ + diff --git a/DannyGoodacre.Core.Tests/Extensions/ServiceCollectionExtensionsTests.cs b/DannyGoodacre.Core.Tests/Extensions/ServiceCollectionExtensionsTests.cs index 06269d0..fb7b68e 100644 --- a/DannyGoodacre.Core.Tests/Extensions/ServiceCollectionExtensionsTests.cs +++ b/DannyGoodacre.Core.Tests/Extensions/ServiceCollectionExtensionsTests.cs @@ -1,9 +1,8 @@ using System.Reflection; using DannyGoodacre.Core.CommandQuery; -using DannyGoodacre.Core.Data; +using DannyGoodacre.Core.CommandQuery.Abstractions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Moq; using SystemMonitor.Core; namespace DannyGoodacre.Core.Tests.Extensions; @@ -11,27 +10,27 @@ namespace DannyGoodacre.Core.Tests.Extensions; [TestFixture] public class ServiceCollectionExtensionsTests { - private class MyCommand : ICommand; + private class MyCommandRequest : ICommandRequest; private interface ITestCommand; private class TestCommandHandler(ILogger logger) - : CommandHandler(logger), ITestCommand + : CommandHandler(logger), ITestCommand { protected override string CommandName => "Test Command"; - protected override Task InternalExecuteAsync(MyCommand command, CancellationToken cancellationToken) + protected override Task InternalExecuteAsync(MyCommandRequest commandRequest, CancellationToken cancellationToken) => Task.FromResult(Result.Success()); } private interface ITestCommandWithValue; private class TestCommandWithValueHandler(ILogger logger) - : CommandHandler(logger), ITestCommandWithValue + : CommandHandler(logger), ITestCommandWithValue { protected override string CommandName => "Test Command With Value"; - protected override Task> InternalExecuteAsync(MyCommand command, CancellationToken cancellationToken) + protected override Task> InternalExecuteAsync(MyCommandRequest commandRequest, CancellationToken cancellationToken) => Task.FromResult(Result.Success(123)); } @@ -39,39 +38,42 @@ private class UnitOfWork : IUnitOfWork { public Task SaveChangesAsync(CancellationToken cancellationToken = default) => Task.FromResult(1); + + public Task BeginTransactionAsync(CancellationToken cancellationToken = default) + => throw new NotImplementedException(); } private interface ITestUnitOfWorkCommand; - private class TestUnitOfWorkCommandHandler(ILogger logger) - : UnitOfWorkCommandHandler(logger, new UnitOfWork()), ITestUnitOfWorkCommand + private class TestTransactionCommandHandler(ILogger logger) + : TransactionCommandHandler(logger, new UnitOfWork()), ITestUnitOfWorkCommand { protected override string CommandName => "Test Unit Of Work Command"; - protected override Task InternalExecuteAsync(MyCommand command, CancellationToken cancellationToken) + protected override Task InternalExecuteAsync(MyCommandRequest commandRequest, CancellationToken cancellationToken) => Task.FromResult(Result.Success()); } private interface ITestUnitOfWorkCommandWithValue; - private class TestUnitOfWorkCommandWithValueHandler(ILogger logger) - : UnitOfWorkCommandHandler(logger, new UnitOfWork()), ITestUnitOfWorkCommandWithValue + private class TransactionCommandWithValueHandler(ILogger logger) + : TransactionCommandHandler(logger, new UnitOfWork()), ITestUnitOfWorkCommandWithValue { protected override string CommandName => "Test Unit Of Work Command With Value"; - protected override Task> InternalExecuteAsync(MyCommand command, CancellationToken cancellationToken) + protected override Task> InternalExecuteAsync(MyCommandRequest commandRequest, CancellationToken cancellationToken) => Task.FromResult(Result.Success(123)); } - private class MyQuery : IQuery; + private class MyQueryRequest : IQueryRequest; private interface ITestQuery; - private class TestQueryHandler(ILogger logger) : QueryHandler(logger), ITestQuery + private class TestQueryHandler(ILogger logger) : QueryHandler(logger), ITestQuery { protected override string QueryName => "Test Query"; - protected override Task> InternalExecuteAsync(MyQuery query, CancellationToken cancellationToken) + protected override Task> InternalExecuteAsync(MyQueryRequest queryRequest, CancellationToken cancellationToken) => Task.FromResult(Result.Success(123)); } diff --git a/DannyGoodacre.Core.Tests/Extensions/TypeExtensionsTests.cs b/DannyGoodacre.Core.Tests/Extensions/TypeExtensionsTests.cs index c6a417d..a76daaf 100644 --- a/DannyGoodacre.Core.Tests/Extensions/TypeExtensionsTests.cs +++ b/DannyGoodacre.Core.Tests/Extensions/TypeExtensionsTests.cs @@ -1,4 +1,5 @@ using DannyGoodacre.Core.CommandQuery; +using DannyGoodacre.Core.CommandQuery.Abstractions; using DannyGoodacre.Core.Extensions; using Microsoft.Extensions.Logging; @@ -7,33 +8,33 @@ namespace DannyGoodacre.Core.Tests.Extensions; [TestFixture] public class TypeExtensionsTests { - private class Command : ICommand; + private class CommandRequest : ICommandRequest; - private class Query : IQuery; + private class QueryRequest : IQueryRequest; - private class SimpleCommandHandler(ILogger logger) : CommandHandler(logger) + private class SimpleCommandHandler(ILogger logger) : CommandHandler(logger) { protected override string CommandName => "Simple Command"; - protected override Task InternalExecuteAsync(Command command, CancellationToken cancellationToken) + protected override Task InternalExecuteAsync(CommandRequest commandRequest, CancellationToken cancellationToken) => Task.FromResult(Result.Success()); } - private class CommandWithValueHandler(ILogger logger) : CommandHandler(logger) + private class CommandWithValueHandler(ILogger logger) : CommandHandler(logger) { protected override string CommandName => "Result Command"; - protected override Task> InternalExecuteAsync(Command command, CancellationToken cancellationToken) + protected override Task> InternalExecuteAsync(CommandRequest commandRequest, CancellationToken cancellationToken) => Task.FromResult(Result.Success(123)); } private class DeepCommandHandler(ILogger logger) : SimpleCommandHandler(logger); - private class SimpleQueryHandler(ILogger logger) : QueryHandler(logger) + private class SimpleQueryHandler(ILogger logger) : QueryHandler(logger) { protected override string QueryName => "Simple Query"; - protected override Task> InternalExecuteAsync(Query query, CancellationToken cancellationToken) + protected override Task> InternalExecuteAsync(QueryRequest queryRequest, CancellationToken cancellationToken) => Task.FromResult(Result.Success(123)); } diff --git a/DannyGoodacre.Core/CommandQuery/Abstractions/ICommand.cs b/DannyGoodacre.Core/CommandQuery/Abstractions/ICommandRequest.cs similarity index 76% rename from DannyGoodacre.Core/CommandQuery/Abstractions/ICommand.cs rename to DannyGoodacre.Core/CommandQuery/Abstractions/ICommandRequest.cs index f0fb032..95f0706 100644 --- a/DannyGoodacre.Core/CommandQuery/Abstractions/ICommand.cs +++ b/DannyGoodacre.Core/CommandQuery/Abstractions/ICommandRequest.cs @@ -3,4 +3,4 @@ namespace DannyGoodacre.Core.CommandQuery.Abstractions; /// /// A command request. /// -public interface ICommand; +public interface ICommandRequest; diff --git a/DannyGoodacre.Core/CommandQuery/Abstractions/IQuery.cs b/DannyGoodacre.Core/CommandQuery/Abstractions/IQueryRequest.cs similarity index 76% rename from DannyGoodacre.Core/CommandQuery/Abstractions/IQuery.cs rename to DannyGoodacre.Core/CommandQuery/Abstractions/IQueryRequest.cs index 6b0c5a6..c32044a 100644 --- a/DannyGoodacre.Core/CommandQuery/Abstractions/IQuery.cs +++ b/DannyGoodacre.Core/CommandQuery/Abstractions/IQueryRequest.cs @@ -3,4 +3,4 @@ namespace DannyGoodacre.Core.CommandQuery.Abstractions; /// /// A query request. /// -public interface IQuery; +public interface IQueryRequest; diff --git a/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs b/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs index 9430825..e634fce 100644 --- a/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs +++ b/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs @@ -3,7 +3,7 @@ namespace DannyGoodacre.Core.CommandQuery; -public abstract class CommandHandler(ILogger logger) where TCommand : ICommand +public abstract class CommandHandler(ILogger logger) where TCommand : ICommandRequest { protected abstract string CommandName { get; } diff --git a/DannyGoodacre.Core/CommandQuery/CommandHandler.cs b/DannyGoodacre.Core/CommandQuery/CommandHandler.cs index 64f9eb6..cb05408 100644 --- a/DannyGoodacre.Core/CommandQuery/CommandHandler.cs +++ b/DannyGoodacre.Core/CommandQuery/CommandHandler.cs @@ -3,7 +3,7 @@ namespace DannyGoodacre.Core.CommandQuery; -public abstract class CommandHandler(ILogger logger) where TCommand : ICommand +public abstract class CommandHandler(ILogger logger) where TCommand : ICommandRequest { protected abstract string CommandName { get; } diff --git a/DannyGoodacre.Core/CommandQuery/QueryHandler.cs b/DannyGoodacre.Core/CommandQuery/QueryHandler.cs index 966dda4..33bcccb 100644 --- a/DannyGoodacre.Core/CommandQuery/QueryHandler.cs +++ b/DannyGoodacre.Core/CommandQuery/QueryHandler.cs @@ -3,7 +3,7 @@ namespace DannyGoodacre.Core.CommandQuery; -public abstract class QueryHandler(ILogger logger) where TQuery : IQuery +public abstract class QueryHandler(ILogger logger) where TQuery : IQueryRequest { protected abstract string QueryName { get; } diff --git a/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.T.cs b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.T.cs index 936ded6..26b491a 100644 --- a/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.T.cs +++ b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.T.cs @@ -3,8 +3,8 @@ namespace DannyGoodacre.Core.CommandQuery; -public abstract class TransactionalCommandHandler(ILogger logger, IUnitOfWork unitOfWork) - : CommandHandler(logger) where TCommand : ICommand +public abstract class TransactionCommandHandler(ILogger logger, IUnitOfWork unitOfWork) + : CommandHandler(logger) where TCommand : ICommandRequest { /// /// The number of state entries expected to be persisted upon completion. diff --git a/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.cs b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.cs index 7123fcf..f498333 100644 --- a/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.cs +++ b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.cs @@ -4,7 +4,7 @@ namespace DannyGoodacre.Core.CommandQuery; public abstract class TransactionCommandHandler(ILogger logger, IUnitOfWork unitOfWork) - : CommandHandler(logger) where TCommand : ICommand + : CommandHandler(logger) where TCommand : ICommandRequest { /// /// The number of state entries expected to be persisted upon completion. @@ -52,7 +52,7 @@ protected async override Task ExecuteAsync(TCommand command, Cancellatio return Result.InternalError("Database integrity check failed."); } - await transaction.CommitAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); return result; } diff --git a/DannyGoodacre.Core/Extensions/TypeExtensions.cs b/DannyGoodacre.Core/Extensions/TypeExtensions.cs index 24db959..0504218 100644 --- a/DannyGoodacre.Core/Extensions/TypeExtensions.cs +++ b/DannyGoodacre.Core/Extensions/TypeExtensions.cs @@ -19,7 +19,7 @@ public bool IsCommandHandler() if (definition == typeof(CommandHandler<>) || definition == typeof(CommandHandler<,>) || definition == typeof(TransactionCommandHandler<>) - || definition == typeof(TransactionalCommandHandler<,>)) + || definition == typeof(TransactionCommandHandler<,>)) { return true; } From a4061b653f5ceb6444f06e3dcf171010d92c325f Mon Sep 17 00:00:00 2001 From: Danny Goodacre Date: Sat, 10 Jan 2026 16:03:29 +0000 Subject: [PATCH 03/10] WIP --- .../Extensions/ServiceCollectionExtensionsTests.cs | 4 ++-- .../CommandQuery/Abstractions/ITransaction.cs | 11 +++++++++++ .../CommandQuery/Abstractions/ITransactionProvider.cs | 8 ++++++++ .../CommandQuery/Abstractions/IUnitOfWork.cs | 3 +++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/DannyGoodacre.Core.Tests/Extensions/ServiceCollectionExtensionsTests.cs b/DannyGoodacre.Core.Tests/Extensions/ServiceCollectionExtensionsTests.cs index fb7b68e..56f934f 100644 --- a/DannyGoodacre.Core.Tests/Extensions/ServiceCollectionExtensionsTests.cs +++ b/DannyGoodacre.Core.Tests/Extensions/ServiceCollectionExtensionsTests.cs @@ -85,7 +85,7 @@ public void AddCommandHandlers() services.AddSingleton(Mock.Of()); - var assembly = Assembly.GetExecutingAssembly(); + var assembly = Assembly.GetExecutingAssembly()!; // Act services.AddCommandHandlers(assembly); @@ -140,7 +140,7 @@ public void AddQueryHandlers() services.AddSingleton(Mock.Of()); - var assembly = Assembly.GetExecutingAssembly(); + var assembly = Assembly.GetExecutingAssembly()!; // Act services.AddQueryHandlers(assembly); diff --git a/DannyGoodacre.Core/CommandQuery/Abstractions/ITransaction.cs b/DannyGoodacre.Core/CommandQuery/Abstractions/ITransaction.cs index 7b49430..b2d41bf 100644 --- a/DannyGoodacre.Core/CommandQuery/Abstractions/ITransaction.cs +++ b/DannyGoodacre.Core/CommandQuery/Abstractions/ITransaction.cs @@ -1,8 +1,19 @@ namespace DannyGoodacre.Core.CommandQuery.Abstractions; +/// +/// Provides a mechanism for managing an atomic unit of work. +/// public interface ITransaction : IAsyncDisposable { + /// + /// Commit all changes made during this transaction to the underlying data store. + /// + /// A to observe while performing the operation. Task CommitAsync(CancellationToken cancellationToken = default); + /// + /// Discard all changes made during this transaction. + /// + /// A to observe while performing the operation. Task RollbackAsync(CancellationToken cancellationToken = default); } diff --git a/DannyGoodacre.Core/CommandQuery/Abstractions/ITransactionProvider.cs b/DannyGoodacre.Core/CommandQuery/Abstractions/ITransactionProvider.cs index f666794..37ed2a0 100644 --- a/DannyGoodacre.Core/CommandQuery/Abstractions/ITransactionProvider.cs +++ b/DannyGoodacre.Core/CommandQuery/Abstractions/ITransactionProvider.cs @@ -1,6 +1,14 @@ namespace DannyGoodacre.Core.CommandQuery.Abstractions; +/// +/// Provides functionality for initiating an . +/// public interface ITransactionProvider { + /// + /// Start a new transaction. + /// + /// A to observe while performing the operation. + /// An instance. Task BeginTransactionAsync(CancellationToken cancellationToken = default); } diff --git a/DannyGoodacre.Core/CommandQuery/Abstractions/IUnitOfWork.cs b/DannyGoodacre.Core/CommandQuery/Abstractions/IUnitOfWork.cs index 210bd4b..ac410c0 100644 --- a/DannyGoodacre.Core/CommandQuery/Abstractions/IUnitOfWork.cs +++ b/DannyGoodacre.Core/CommandQuery/Abstractions/IUnitOfWork.cs @@ -1,5 +1,8 @@ namespace DannyGoodacre.Core.CommandQuery.Abstractions; +/// +/// Provides functionality for coordinating and persisting changes to an underlying data store as a single atomic unit. +/// public interface IUnitOfWork { /// From b84d03fe4886462337331810b7e8885f5fece744 Mon Sep 17 00:00:00 2001 From: Danny Goodacre Date: Sat, 10 Jan 2026 16:07:51 +0000 Subject: [PATCH 04/10] WIP --- .../Extensions/ServiceCollectionExtensionsTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DannyGoodacre.Core.Tests/Extensions/ServiceCollectionExtensionsTests.cs b/DannyGoodacre.Core.Tests/Extensions/ServiceCollectionExtensionsTests.cs index 56f934f..fb7b68e 100644 --- a/DannyGoodacre.Core.Tests/Extensions/ServiceCollectionExtensionsTests.cs +++ b/DannyGoodacre.Core.Tests/Extensions/ServiceCollectionExtensionsTests.cs @@ -85,7 +85,7 @@ public void AddCommandHandlers() services.AddSingleton(Mock.Of()); - var assembly = Assembly.GetExecutingAssembly()!; + var assembly = Assembly.GetExecutingAssembly(); // Act services.AddCommandHandlers(assembly); @@ -140,7 +140,7 @@ public void AddQueryHandlers() services.AddSingleton(Mock.Of()); - var assembly = Assembly.GetExecutingAssembly()!; + var assembly = Assembly.GetExecutingAssembly(); // Act services.AddQueryHandlers(assembly); From 3dcaedd4fd82134211210aa26062fb5f44dda5d1 Mon Sep 17 00:00:00 2001 From: Danny Goodacre Date: Sat, 10 Jan 2026 17:34:08 +0000 Subject: [PATCH 05/10] WIP --- .../TransactionCommandHandlerTests.cs | 47 ++++++++++++++----- .../TransactionCommandHandlerValueTests.cs | 47 ++++++++++++++----- .../TransactionCommandHandler.T.cs | 10 +++- .../CommandQuery/TransactionCommandHandler.cs | 10 +++- DannyGoodacre.Core/DannyGoodacre.Core.csproj | 2 +- 5 files changed, 91 insertions(+), 25 deletions(-) diff --git a/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerTests.cs index c0bbeed..050b830 100644 --- a/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerTests.cs +++ b/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerTests.cs @@ -9,7 +9,7 @@ public class TransactionCommandHandlerTests : TestBase { public class TestCommand : ICommandRequest; - private const string TestCommandName = "Test Transaction Command Handler"; + private const string TestName = "Test Transaction Command Handler"; private static int _testExpectedChanges; @@ -32,7 +32,7 @@ public class TestCommand : ICommandRequest; public class TestTransactionCommandHandler(ILogger logger, IUnitOfWork unitOfWork) : TransactionCommandHandler(logger, unitOfWork) { - protected override string CommandName => TestCommandName; + protected override string CommandName => TestName; protected override int ExpectedChanges => _testExpectedChanges; @@ -94,7 +94,7 @@ public async Task ExecuteAsync_WhenSuccessfulAndInvalidNumberOfChanges_ShouldRol SetupTransaction_RollbackAsync(); - _loggerMock.Setup(LogLevel.Error, $"Command '{TestCommandName}' attempted to persist an unexpected number of changes: Expected '{_testExpectedChanges}', Actual '{_testActualChanges}'."); + _loggerMock.Setup(LogLevel.Error, $"Command '{TestName}' attempted to persist an unexpected number of changes: Expected '{_testExpectedChanges}', Actual '{_testActualChanges}'."); SetupTransaction_DisposeAsync(); @@ -148,6 +148,31 @@ public async Task ExecuteAsync_WhenSuccessfulAndNotValidatingChanges_ShouldCommi AssertSuccess(result); } + [Test] + public async Task ExecuteAsync_WhenSuccessfulAndCancelled_ShouldRollbackAndReturnCancelled() + { + // Arrange + SetupUnitOfWork_BeginTransactionAsync(); + + _unitOfWorkMock + .Setup(x => x.SaveChangesAsync( + It.Is(y => y == _testCancellationToken))) + .ThrowsAsync(new OperationCanceledException()) + .Verifiable(Times.Once); + + SetupTransaction_RollbackAsync(); + + _loggerMock.Setup(LogLevel.Information, $"Command '{TestName}' was cancelled while persisting changes."); + + SetupTransaction_DisposeAsync(); + + // Act + var result = await Act(); + + // Assert + AssertCancelled(result); + } + [Test] public async Task ExecuteAsync_WhenSuccessfulAndExceptionOccurs_ShouldRollbackAndReturnInternalError() { @@ -166,7 +191,7 @@ public async Task ExecuteAsync_WhenSuccessfulAndExceptionOccurs_ShouldRollbackAn SetupTransaction_RollbackAsync(); - _loggerMock.Setup(LogLevel.Critical, $"Command '{TestCommandName}' experienced a transaction failure: {testError}"); + _loggerMock.Setup(LogLevel.Critical, $"Command '{TestName}' experienced a transaction failure: {testError}", exception: exception); SetupTransaction_DisposeAsync(); @@ -194,6 +219,13 @@ private void SetupUnitOfWork_SaveChangesAsync() .ReturnsAsync(_testActualChanges) .Verifiable(Times.Once); + private void SetupTransaction_CommitAsync() + => _testTransaction + .Setup(x => x.CommitAsync( + It.Is(y => y == _testCancellationToken))) + .Returns(Task.CompletedTask) + .Verifiable(Times.Once); + private void SetupTransaction_RollbackAsync() => _testTransaction .Setup(x => x.RollbackAsync( @@ -206,11 +238,4 @@ private void SetupTransaction_DisposeAsync() .Setup(x => x.DisposeAsync()) .Returns(ValueTask.CompletedTask) .Verifiable(Times.Once); - - private void SetupTransaction_CommitAsync() - => _testTransaction - .Setup(x => x.CommitAsync( - It.Is(y => y == _testCancellationToken))) - .Returns(Task.CompletedTask) - .Verifiable(Times.Once); } diff --git a/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs index 080b6e1..81a037d 100644 --- a/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs +++ b/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs @@ -9,7 +9,7 @@ public class TransactionCommandWithValueHandlerTests : TestBase { public class TestCommand : ICommandRequest; - private const string TestCommandName = "Test Transaction Command Handler"; + private const string TestName = "Test Transaction Command Handler"; private static int _testResultValue; @@ -34,7 +34,7 @@ public class TestCommand : ICommandRequest; public class TestTransactionCommandWithValueHandler(ILogger logger, IUnitOfWork unitOfWork) : TransactionCommandHandler(logger, unitOfWork) { - protected override string CommandName => TestCommandName; + protected override string CommandName => TestName; protected override int ExpectedChanges => _testExpectedChanges; @@ -98,7 +98,7 @@ public async Task ExecuteAsync_WhenSuccessfulAndInvalidNumberOfChanges_ShouldRol SetupTransaction_RollbackAsync(); - _loggerMock.Setup(LogLevel.Error, $"Command '{TestCommandName}' attempted to persist an unexpected number of changes: Expected '{_testExpectedChanges}', Actual '{_testActualChanges}'."); + _loggerMock.Setup(LogLevel.Error, $"Command '{TestName}' attempted to persist an unexpected number of changes: Expected '{_testExpectedChanges}', Actual '{_testActualChanges}'."); SetupTransaction_DisposeAsync(); @@ -152,6 +152,31 @@ public async Task ExecuteAsync_WhenSuccessfulAndNotValidatingChanges_ShouldCommi AssertSuccess(result); } + [Test] + public async Task ExecuteAsync_WhenCancelled_ShouldRollbackAndReturnCancelled() + { + // Arrange + SetupUnitOfWork_BeginTransactionAsync(); + + _unitOfWorkMock + .Setup(x => x.SaveChangesAsync( + It.Is(y => y == _testCancellationToken))) + .ThrowsAsync(new OperationCanceledException()) + .Verifiable(Times.Once); + + SetupTransaction_RollbackAsync(); + + _loggerMock.Setup(LogLevel.Information, $"Command '{TestName}' was cancelled while persisting changes."); + + SetupTransaction_DisposeAsync(); + + // Act + var result = await Act(); + + // Assert + AssertCancelled(result); + } + [Test] public async Task ExecuteAsync_WhenSuccessfulAndExceptionOccurs_ShouldRollbackAndReturnInternalError() { @@ -170,7 +195,7 @@ public async Task ExecuteAsync_WhenSuccessfulAndExceptionOccurs_ShouldRollbackAn SetupTransaction_RollbackAsync(); - _loggerMock.Setup(LogLevel.Critical, $"Command '{TestCommandName}' experienced a transaction failure: {testError}"); + _loggerMock.Setup(LogLevel.Critical, $"Command '{TestName}' experienced a transaction failure: {testError}", exception: exception); SetupTransaction_DisposeAsync(); @@ -198,6 +223,13 @@ private void SetupUnitOfWork_SaveChangesAsync() .ReturnsAsync(_testActualChanges) .Verifiable(Times.Once); + private void SetupTransaction_CommitAsync() + => _testTransaction + .Setup(x => x.CommitAsync( + It.Is(y => y == _testCancellationToken))) + .Returns(Task.CompletedTask) + .Verifiable(Times.Once); + private void SetupTransaction_RollbackAsync() => _testTransaction .Setup(x => x.RollbackAsync( @@ -210,11 +242,4 @@ private void SetupTransaction_DisposeAsync() .Setup(x => x.DisposeAsync()) .Returns(ValueTask.CompletedTask) .Verifiable(Times.Once); - - private void SetupTransaction_CommitAsync() - => _testTransaction - .Setup(x => x.CommitAsync( - It.Is(y => y == _testCancellationToken))) - .Returns(Task.CompletedTask) - .Verifiable(Times.Once); } diff --git a/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.T.cs b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.T.cs index 26b491a..86d0eca 100644 --- a/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.T.cs +++ b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.T.cs @@ -56,11 +56,19 @@ protected async override Task> ExecuteAsync(TCommand command, Ca return result; } + catch (OperationCanceledException) + { + await transaction.RollbackAsync(cancellationToken); + + Logger.LogInformation("Command '{Command}' was cancelled while persisting changes.", CommandName); + + return Result.Cancelled(); + } catch (Exception ex) { await transaction.RollbackAsync(cancellationToken); - Logger.LogCritical("Command '{Command}' experienced a transaction failure: {Exception}", CommandName, ex.Message); + Logger.LogCritical(ex, "Command '{Command}' experienced a transaction failure: {Exception}", CommandName, ex.Message); return Result.InternalError(ex.Message); } diff --git a/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.cs b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.cs index f498333..d00391a 100644 --- a/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.cs +++ b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.cs @@ -56,11 +56,19 @@ protected async override Task ExecuteAsync(TCommand command, Cancellatio return result; } + catch (OperationCanceledException) + { + await transaction.RollbackAsync(cancellationToken); + + Logger.LogInformation("Command '{Command}' was cancelled while persisting changes.", CommandName); + + return Result.Cancelled(); + } catch (Exception ex) { await transaction.RollbackAsync(cancellationToken); - Logger.LogCritical("Command '{Command}' experienced a transaction failure: {Exception}", CommandName, ex.Message); + Logger.LogCritical(ex, "Command '{Command}' experienced a transaction failure: {Exception}", CommandName, ex.Message); return Result.InternalError(ex.Message); } diff --git a/DannyGoodacre.Core/DannyGoodacre.Core.csproj b/DannyGoodacre.Core/DannyGoodacre.Core.csproj index ee32e77..94998f8 100644 --- a/DannyGoodacre.Core/DannyGoodacre.Core.csproj +++ b/DannyGoodacre.Core/DannyGoodacre.Core.csproj @@ -3,7 +3,7 @@ net10.0 DannyGoodacre.Core - 0.2.0 + 0.3.0 Danny Goodacre Common logic for my .NET projects. https://github.com/dannygoodacre/DannyGoodacre.Core From 4448f2ee0d6b418c532a80d75d7adcf77932097e Mon Sep 17 00:00:00 2001 From: Danny Goodacre Date: Sat, 10 Jan 2026 20:29:29 +0000 Subject: [PATCH 06/10] WIP --- .../CommandQuery/CommandHandlerTests.cs | 102 +++++++++-------- .../CommandQuery/CommandHandlerValueTests.cs | 107 ++++++++++-------- .../CommandQuery/QueryHandlerTests.cs | 107 ++++++++++-------- .../TransactionCommandHandlerTests.cs | 61 +++++----- .../TransactionCommandHandlerValueTests.cs | 55 ++++----- .../CommandQuery/CommandHandler.T.cs | 1 - .../CommandQuery/CommandHandler.cs | 1 - DannyGoodacre.Tests.Core/TestBase.cs | 9 ++ 8 files changed, 249 insertions(+), 194 deletions(-) diff --git a/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerTests.cs index 8bb6a2f..2bb01d5 100644 --- a/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerTests.cs +++ b/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerTests.cs @@ -9,93 +9,108 @@ public class CommandHandlerTests : TestBase { public class TestCommandRequest : ICommandRequest; - private const string TestName = "Test Command Handler"; + public class TestCommandHandler(ILogger logger) : CommandHandler(logger) + { + protected override string CommandName => TestName; + + protected override void Validate(ValidationState validationState, TestCommandRequest commandRequest) + => _testValidate(validationState, commandRequest); + + protected override Task InternalExecuteAsync(TestCommandRequest commandRequest, CancellationToken cancellationToken) + => _testInternalExecuteAsync(commandRequest, cancellationToken); - private const string TestProperty = "Test Property"; + public Task TestExecuteAsync(TestCommandRequest commandRequest, CancellationToken cancellationToken) + => ExecuteAsync(commandRequest, cancellationToken); + } - private const string TestError = "Test Error"; + private const string TestName = "Test Command Handler"; - private const string TestExceptionMessage = "Test Exception Message"; + private CancellationToken _testCancellationToken; - private readonly CancellationToken _testCancellationToken = CancellationToken.None; + private Mock> _loggerMock = null!; - private readonly TestCommandRequest _testCommandRequest = new(); + private static Action _testValidate = null!; - private static Action _validate = (_, _) => {}; + private static Func> _testInternalExecuteAsync = null!; - private static Func> _internalExecuteAsync = (_, _) => Task.FromResult(new Result()); + private static TestCommandRequest _testCommandRequest = null!; - private Mock> _loggerMock = null!; + private static TestCommandHandler _testCommandHandler = null!; - public class TestCommandHandler(ILogger logger) : CommandHandler(logger) + [SetUp] + public void SetUp() { - protected override string CommandName => TestName; + _testCancellationToken = CancellationToken.None; - protected override void Validate(ValidationState validationState, TestCommandRequest commandRequest) - => _validate(validationState, commandRequest); + _testValidate = (_, _) => {}; - protected override Task InternalExecuteAsync(TestCommandRequest commandRequest, CancellationToken cancellationToken) - => _internalExecuteAsync(commandRequest, cancellationToken); + _testInternalExecuteAsync = (_, _) => Task.FromResult(Result.Success()); - public Task TestExecuteAsync(TestCommandRequest commandRequest, CancellationToken cancellationToken) - => ExecuteAsync(commandRequest, cancellationToken); + _loggerMock = new Mock>(MockBehavior.Strict); + + _testCommandRequest = new TestCommandRequest(); + + _testCommandHandler = new TestCommandHandler(_loggerMock.Object); } [Test] public async Task ExecuteAsync_WhenValidationFails_ShouldReturnInvalid() { // Arrange - _loggerMock = new Mock>(MockBehavior.Strict); + const string testProperty = "Test Property"; - _loggerMock.Setup(LogLevel.Error, $"Command '{TestName}' failed validation: {TestProperty}:{Environment.NewLine} - {TestError}"); + const string testError = "Test Error"; - _validate = (validationState, _) - => validationState.AddError(TestProperty, TestError); + _loggerMock.Setup(LogLevel.Error, $"Command '{TestName}' failed validation: {testProperty}:{Environment.NewLine} - {testError}"); - var handler = new TestCommandHandler(_loggerMock.Object); + _testValidate = (validationState, _) => validationState.AddError(testProperty, testError); // Act - var result = await handler.TestExecuteAsync(_testCommandRequest, _testCancellationToken); + var result = await Act(); // Assert AssertInvalid(result); } [Test] - public async Task ExecuteAsync_WhenCancelled_ShouldReturnCancelled() + public async Task ExecuteAsync_WhenCancelledBefore_ShouldReturnCancelled() { // Arrange var cancellationTokenSource = new CancellationTokenSource(); - _loggerMock = new Mock>(MockBehavior.Strict); + _testCancellationToken = cancellationTokenSource.Token; _loggerMock.Setup(LogLevel.Information, $"Command '{TestName}' was cancelled before execution."); - var handler = new TestCommandHandler(_loggerMock.Object); - await cancellationTokenSource.CancelAsync(); // Act - var result = await handler.TestExecuteAsync(_testCommandRequest, cancellationTokenSource.Token); + var result = await Act(); // Assert AssertCancelled(result); } + [Test] + public async Task ExecuteAsync_WhenSuccessful_ShouldReturnSuccess() + { + // Act + var result = await Act(); + + // Assert + AssertSuccess(result); + } + [Test] public async Task ExecuteAsync_WhenCancelledDuring_ShouldReturnCancelled() { // Arrange - _loggerMock = new Mock>(MockBehavior.Strict); - _loggerMock.Setup(LogLevel.Information, $"Command '{TestName}' was cancelled during execution."); - _internalExecuteAsync = (_, _) => throw new OperationCanceledException(); - - var handler = new TestCommandHandler(_loggerMock.Object); + _testInternalExecuteAsync = (_, _) => throw new OperationCanceledException(); // Act - var result = await handler.TestExecuteAsync(_testCommandRequest, _testCancellationToken); + var result = await Act(); // Assert AssertCancelled(result); @@ -105,23 +120,20 @@ public async Task ExecuteAsync_WhenCancelledDuring_ShouldReturnCancelled() public async Task ExecuteAsync_WhenExceptionOccurs_ShouldReturnInternalError() { // Arrange - var exception = new ApplicationException(TestExceptionMessage); - - _loggerMock = new Mock>(MockBehavior.Strict); + const string testExceptionMessage = "Test Exception Message"; - _loggerMock.Setup( - LogLevel.Critical, - $"Command '{TestName}' failed with exception: {TestExceptionMessage}", - exception: exception); + var exception = new Exception(testExceptionMessage); - _internalExecuteAsync = (_, _) => throw exception; + _loggerMock.Setup(LogLevel.Critical, $"Command '{TestName}' failed with exception: {testExceptionMessage}", exception: exception); - var handler = new TestCommandHandler(_loggerMock.Object); + _testInternalExecuteAsync = (_, _) => throw exception; // Act - var result = await handler.TestExecuteAsync(_testCommandRequest, _testCancellationToken); + var result = await Act(); // Assert - AssertInternalError(result, TestExceptionMessage); + AssertInternalError(result, testExceptionMessage); } + + private Task Act() => _testCommandHandler.TestExecuteAsync(_testCommandRequest, _testCancellationToken); } diff --git a/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerValueTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerValueTests.cs index 50d4c01..4a72d6f 100644 --- a/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerValueTests.cs +++ b/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerValueTests.cs @@ -7,94 +7,114 @@ namespace DannyGoodacre.Core.Tests.CommandQuery; [TestFixture] public class CommandHandlerValueTests : TestBase { - public class TestCommand : ICommandRequest; + public class TestCommandRequest : ICommandRequest; - private const string TestName = "Test Command Handler"; - - private const string TestProperty = "Test Property"; + public class TestCommandHandler(ILogger logger) : CommandHandler(logger) + { + protected override string CommandName => TestName; - private const string TestError = "Test Error"; + protected override void Validate(ValidationState validationState, TestCommandRequest commandRequest) + => _testValidate(validationState, commandRequest); - private const string TestExceptionMessage = "Test Exception Message"; + protected override Task> InternalExecuteAsync(TestCommandRequest commandRequest, CancellationToken cancellationToken) + => _testInternalExecuteAsync(commandRequest, cancellationToken); - private readonly CancellationToken _testCancellationToken = CancellationToken.None; + public Task> TestExecuteAsync(TestCommandRequest commandRequest, CancellationToken cancellationToken) + => ExecuteAsync(commandRequest, cancellationToken); + } - private readonly TestCommand _testCommand = new(); + private const string TestName = "Test Command Handler"; - private static Action _validate = (_, _) => {}; + private int _testResultValue; - private static Func>> _internalExecuteAsync = (_, _) => Task.FromResult(Result.Success("Test result value")); + private CancellationToken _testCancellationToken; private Mock> _loggerMock = null!; - public class TestCommandHandler(ILogger logger) : CommandHandler(logger) + private static Action _testValidate = null!; + + private static Func>> _testInternalExecuteAsync = null!; + + private static TestCommandRequest _testCommandRequest = null!; + + private static TestCommandHandler _testCommandHandler = null!; + + [SetUp] + public void SetUp() { - protected override string CommandName => TestName; + _testResultValue = 123; + + _testCancellationToken = CancellationToken.None; - protected override void Validate(ValidationState validationState, TestCommand command) - => _validate(validationState, command); + _testValidate = (_, _) => {}; - protected override Task> InternalExecuteAsync(TestCommand command, CancellationToken cancellationToken) - => _internalExecuteAsync(command, cancellationToken); + _testInternalExecuteAsync = (_, _) => Task.FromResult(Result.Success(_testResultValue)); - public Task> TestExecuteAsync(TestCommand command, CancellationToken cancellationToken) => ExecuteAsync(command, cancellationToken); + _loggerMock = new Mock>(MockBehavior.Strict); + + _testCommandRequest = new TestCommandRequest(); + + _testCommandHandler = new TestCommandHandler(_loggerMock.Object); } [Test] public async Task ExecuteAsync_WhenValidationFails_ShouldReturnInvalid() { // Arrange - _loggerMock = new Mock>(MockBehavior.Strict); + const string testProperty = "Test Property"; - _loggerMock.Setup(LogLevel.Error, $"Command '{TestName}' failed validation: {TestProperty}:{Environment.NewLine} - {TestError}"); + const string testError = "Test Error"; - _validate = (validationState, _) - => validationState.AddError(TestProperty, TestError); + _loggerMock.Setup(LogLevel.Error, $"Command '{TestName}' failed validation: {testProperty}:{Environment.NewLine} - {testError}"); - var handler = new TestCommandHandler(_loggerMock.Object); + _testValidate = (validationState, _) => validationState.AddError(testProperty, testError); // Act - var result = await handler.TestExecuteAsync(_testCommand, _testCancellationToken); + var result = await Act(); // Assert AssertInvalid(result); } [Test] - public async Task ExecuteAsync_WhenCancelled_ShouldReturnCancelled() + public async Task ExecuteAsync_WhenCancelledBefore_ShouldReturnCancelled() { // Arrange var cancellationTokenSource = new CancellationTokenSource(); - _loggerMock = new Mock>(MockBehavior.Strict); + _testCancellationToken = cancellationTokenSource.Token; _loggerMock.Setup(LogLevel.Information, $"Command '{TestName}' was cancelled before execution."); - var handler = new TestCommandHandler(_loggerMock.Object); - await cancellationTokenSource.CancelAsync(); // Act - var result = await handler.TestExecuteAsync(_testCommand, cancellationTokenSource.Token); + var result = await Act(); // Assert AssertCancelled(result); } + [Test] + public async Task ExecuteAsync_WhenSuccessful_ShouldReturnSuccess() + { + // Act + var result = await Act(); + + // Assert + AssertSuccess(result, _testResultValue); + } + [Test] public async Task ExecuteAsync_WhenCancelledDuring_ShouldReturnCancelled() { // Arrange - _loggerMock = new Mock>(MockBehavior.Strict); - _loggerMock.Setup(LogLevel.Information, $"Command '{TestName}' was cancelled during execution."); - _internalExecuteAsync = (_, _) => throw new OperationCanceledException(); - - var handler = new TestCommandHandler(_loggerMock.Object); + _testInternalExecuteAsync = (_, _) => throw new OperationCanceledException(); // Act - var result = await handler.TestExecuteAsync(_testCommand, _testCancellationToken); + var result = await Act(); // Assert AssertCancelled(result); @@ -104,23 +124,20 @@ public async Task ExecuteAsync_WhenCancelledDuring_ShouldReturnCancelled() public async Task ExecuteAsync_WhenExceptionOccurs_ShouldReturnInternalError() { // Arrange - var exception = new ApplicationException(TestExceptionMessage); + const string testExceptionMessage = "Test Exception Message"; - _loggerMock = new Mock>(MockBehavior.Strict); - - _loggerMock.Setup( - LogLevel.Critical, - $"Command '{TestName}' failed with exception: {TestExceptionMessage}", - exception: exception); + var exception = new Exception(testExceptionMessage); - _internalExecuteAsync = (_, _) => throw exception; + _loggerMock.Setup(LogLevel.Critical, $"Command '{TestName}' failed with exception: {testExceptionMessage}", exception: exception); - var handler = new TestCommandHandler(_loggerMock.Object); + _testInternalExecuteAsync = (_, _) => throw exception; // Act - var result = await handler.TestExecuteAsync(_testCommand, _testCancellationToken); + var result = await Act(); // Assert - AssertInternalError(result, TestExceptionMessage); + AssertInternalError(result, testExceptionMessage); } + + private Task> Act() => _testCommandHandler.TestExecuteAsync(_testCommandRequest, _testCancellationToken); } diff --git a/DannyGoodacre.Core.Tests/CommandQuery/QueryHandlerTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/QueryHandlerTests.cs index 1730eb7..a17ab40 100644 --- a/DannyGoodacre.Core.Tests/CommandQuery/QueryHandlerTests.cs +++ b/DannyGoodacre.Core.Tests/CommandQuery/QueryHandlerTests.cs @@ -7,94 +7,114 @@ namespace DannyGoodacre.Core.Tests.CommandQuery; [TestFixture] public class QueryHandlerTests : TestBase { - public class TestQuery : IQueryRequest; + public class TestQueryRequest : IQueryRequest; - private const string TestName = "Test Query Handler"; - - private const string TestProperty = "Test Property"; + public class TestQueryHandler(ILogger logger) : QueryHandler(logger) + { + protected override string QueryName => TestName; - private const string TestError = "Test Error"; + protected override void Validate(ValidationState validationState, TestQueryRequest queryRequest) + => _testValidate(validationState, queryRequest); - private const string TestExceptionMessage = "Test Exception Message"; + protected override Task> InternalExecuteAsync(TestQueryRequest queryRequest, CancellationToken cancellationToken) + => _testInternalExecuteAsync(queryRequest, cancellationToken); - private readonly CancellationToken _testCancellationToken = CancellationToken.None; + public Task> TestExecuteAsync(TestQueryRequest command, CancellationToken cancellationToken) + => ExecuteAsync(command, cancellationToken); + } - private readonly TestQuery _testQuery = new(); + private const string TestName = "Test Query Handler"; - private static Action _validate = (_, _) => {}; + private int _testResultValue; - private static Func>> _internalExecuteAsync = (_, _) => Task.FromResult(new Result()); + private CancellationToken _testCancellationToken; private Mock> _loggerMock = null!; - public class TestQueryHandler(ILogger logger) : QueryHandler(logger) + private static Action _testValidate = null!; + + private static Func>> _testInternalExecuteAsync = null!; + + private static TestQueryRequest _testQueryRequest = null!; + + private static TestQueryHandler _testCommandHandler = null!; + + [SetUp] + public void SetUp() { - protected override string QueryName => TestName; + _testResultValue = 123; + + _testCancellationToken = CancellationToken.None; - protected override void Validate(ValidationState validationState, TestQuery query) - => _validate(validationState, query); + _testValidate = (_, _) => {}; - protected override Task> InternalExecuteAsync(TestQuery query, CancellationToken cancellationToken) - => _internalExecuteAsync(query, cancellationToken); + _testInternalExecuteAsync = (_, _) => Task.FromResult(Result.Success(_testResultValue)); - public Task> TestExecuteAsync(TestQuery command, CancellationToken cancellationToken) => ExecuteAsync(command, cancellationToken); + _loggerMock = new Mock>(MockBehavior.Strict); + + _testQueryRequest = new TestQueryRequest(); + + _testCommandHandler = new TestQueryHandler(_loggerMock.Object); } [Test] public async Task ExecuteAsync_WhenValidationFails_ShouldReturnInvalid() { // Arrange - _loggerMock = new Mock>(MockBehavior.Strict); + const string testProperty = "Test Property"; - _loggerMock.Setup(LogLevel.Error, $"Query '{TestName}' failed validation: {TestProperty}:{Environment.NewLine} - {TestError}"); + const string testError = "Test Error"; - _validate = (validationState, _) - => validationState.AddError(TestProperty, TestError); + _loggerMock.Setup(LogLevel.Error, $"Query '{TestName}' failed validation: {testProperty}:{Environment.NewLine} - {testError}"); - var handler = new TestQueryHandler(_loggerMock.Object); + _testValidate = (validationState, _) => validationState.AddError(testProperty, testError); // Act - var result = await handler.TestExecuteAsync(_testQuery, _testCancellationToken); + var result = await Act(); // Assert AssertInvalid(result); } [Test] - public async Task ExecuteAsync_WhenCancelled_ShouldReturnCancelled() + public async Task ExecuteAsync_WhenCancelledBefore_ShouldReturnCancelled() { // Arrange var cancellationTokenSource = new CancellationTokenSource(); - _loggerMock = new Mock>(MockBehavior.Strict); + _testCancellationToken = cancellationTokenSource.Token; _loggerMock.Setup(LogLevel.Information, $"Query '{TestName}' was cancelled before execution."); - var handler = new TestQueryHandler(_loggerMock.Object); - await cancellationTokenSource.CancelAsync(); // Act - var result = await handler.TestExecuteAsync(_testQuery, cancellationTokenSource.Token); + var result = await Act(); // Assert AssertCancelled(result); } + [Test] + public async Task ExecuteAsync_WhenSuccessful_ShouldReturnSuccess() + { + // Act + var result = await Act(); + + // Assert + AssertSuccess(result, _testResultValue); + } + [Test] public async Task ExecuteAsync_WhenCancelledDuring_ShouldReturnCancelled() { // Arrange - _loggerMock = new Mock>(MockBehavior.Strict); - _loggerMock.Setup(LogLevel.Information, $"Query '{TestName}' was cancelled during execution."); - _internalExecuteAsync = (_, _) => throw new OperationCanceledException(); - - var handler = new TestQueryHandler(_loggerMock.Object); + _testInternalExecuteAsync = (_, _) => throw new OperationCanceledException(); // Act - var result = await handler.TestExecuteAsync(_testQuery, _testCancellationToken); + var result = await Act(); // Assert AssertCancelled(result); @@ -104,23 +124,20 @@ public async Task ExecuteAsync_WhenCancelledDuring_ShouldReturnCancelled() public async Task ExecuteAsync_WhenExceptionOccurs_ShouldReturnInternalError() { // Arrange - var exception = new ApplicationException(TestExceptionMessage); + const string testExceptionMessage = "Test Exception Message"; - _loggerMock = new Mock>(MockBehavior.Strict); - - _loggerMock.Setup( - LogLevel.Critical, - $"Query '{TestName}' failed with exception: {TestExceptionMessage}", - exception: exception); + var exception = new Exception(testExceptionMessage); - _internalExecuteAsync = (_, _) => throw exception; + _loggerMock.Setup(LogLevel.Critical, $"Query '{TestName}' failed with exception: {testExceptionMessage}", exception: exception); - var handler = new TestQueryHandler(_loggerMock.Object); + _testInternalExecuteAsync = (_, _) => throw exception; // Act - var result = await handler.TestExecuteAsync(_testQuery, _testCancellationToken); + var result = await Act(); // Assert - AssertInternalError(result, TestExceptionMessage); + AssertInternalError(result, testExceptionMessage); } + + private Task> Act() => _testCommandHandler.TestExecuteAsync(_testQueryRequest, _testCancellationToken); } diff --git a/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerTests.cs index 050b830..03a64c1 100644 --- a/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerTests.cs +++ b/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerTests.cs @@ -7,7 +7,21 @@ namespace DannyGoodacre.Core.Tests.CommandQuery; [TestFixture] public class TransactionCommandHandlerTests : TestBase { - public class TestCommand : ICommandRequest; + public class TestCommandRequest : ICommandRequest; + + public class TestTransactionCommandHandler(ILogger logger, IUnitOfWork unitOfWork) + : TransactionCommandHandler(logger, unitOfWork) + { + protected override string CommandName => TestName; + + protected override int ExpectedChanges => _testExpectedChanges; + + protected override Task InternalExecuteAsync(TestCommandRequest commandRequest, CancellationToken cancellationToken) + => _internalExecuteAsync(commandRequest, cancellationToken); + + public Task TestExecuteAsync(TestCommandRequest commandRequest, CancellationToken cancellationToken) + => ExecuteAsync(commandRequest, cancellationToken); + } private const string TestName = "Test Transaction Command Handler"; @@ -15,48 +29,36 @@ public class TestCommand : ICommandRequest; private static int _testActualChanges; - private readonly CancellationToken _testCancellationToken = CancellationToken.None; - - private readonly TestCommand _testCommand = new(); + private CancellationToken _testCancellationToken; private Mock> _loggerMock = null!; private Mock _unitOfWorkMock = null!; - private Mock _testTransaction = null!; + private Mock _transactionMock = null!; - private static Func> _internalExecuteAsync = null!; + private static Func> _internalExecuteAsync = null!; - private static TestTransactionCommandHandler _testHandler = null!; + private readonly TestCommandRequest _testCommandRequest = new(); - public class TestTransactionCommandHandler(ILogger logger, IUnitOfWork unitOfWork) - : TransactionCommandHandler(logger, unitOfWork) - { - protected override string CommandName => TestName; - - protected override int ExpectedChanges => _testExpectedChanges; - - protected override Task InternalExecuteAsync(TestCommand command, CancellationToken cancellationToken) - => _internalExecuteAsync(command, cancellationToken); - - public Task TestExecuteAsync(TestCommand command, CancellationToken cancellationToken) - => ExecuteAsync(command, cancellationToken); - } + private static TestTransactionCommandHandler _testHandler = null!; [SetUp] public void SetUp() { + _testExpectedChanges = -1; + + _testCancellationToken = CancellationToken.None; + _loggerMock = new Mock>(MockBehavior.Strict); _unitOfWorkMock = new Mock(MockBehavior.Strict); + _transactionMock = new Mock(); + _internalExecuteAsync = (_, _) => Task.FromResult(Result.Success()); _testHandler = new TestTransactionCommandHandler(_loggerMock.Object, _unitOfWorkMock.Object); - - _testExpectedChanges = -1; - - _testTransaction = new Mock(); } [Test] @@ -202,14 +204,13 @@ public async Task ExecuteAsync_WhenSuccessfulAndExceptionOccurs_ShouldRollbackAn AssertInternalError(result, testError); } - private Task Act() - => _testHandler.TestExecuteAsync(_testCommand, _testCancellationToken); + private Task Act() => _testHandler.TestExecuteAsync(_testCommandRequest, _testCancellationToken); private void SetupUnitOfWork_BeginTransactionAsync() => _unitOfWorkMock .Setup(x => x.BeginTransactionAsync( It.Is(y => y == _testCancellationToken))) - .ReturnsAsync(_testTransaction.Object) + .ReturnsAsync(_transactionMock.Object) .Verifiable(Times.Once); private void SetupUnitOfWork_SaveChangesAsync() @@ -220,21 +221,21 @@ private void SetupUnitOfWork_SaveChangesAsync() .Verifiable(Times.Once); private void SetupTransaction_CommitAsync() - => _testTransaction + => _transactionMock .Setup(x => x.CommitAsync( It.Is(y => y == _testCancellationToken))) .Returns(Task.CompletedTask) .Verifiable(Times.Once); private void SetupTransaction_RollbackAsync() - => _testTransaction + => _transactionMock .Setup(x => x.RollbackAsync( It.Is(y => y == _testCancellationToken))) .Returns(Task.CompletedTask) .Verifiable(Times.Once); private void SetupTransaction_DisposeAsync() - => _testTransaction + => _transactionMock .Setup(x => x.DisposeAsync()) .Returns(ValueTask.CompletedTask) .Verifiable(Times.Once); diff --git a/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs index 81a037d..91366fa 100644 --- a/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs +++ b/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs @@ -9,6 +9,20 @@ public class TransactionCommandWithValueHandlerTests : TestBase { public class TestCommand : ICommandRequest; + public class TestTransactionCommandHandler(ILogger logger, IUnitOfWork unitOfWork) + : TransactionCommandHandler(logger, unitOfWork) + { + protected override string CommandName => TestName; + + protected override int ExpectedChanges => _testExpectedChanges; + + protected override Task> InternalExecuteAsync(TestCommand command, CancellationToken cancellationToken) + => _internalExecuteAsync(command, cancellationToken); + + public Task> TestExecuteAsync(TestCommand command, CancellationToken cancellationToken) + => ExecuteAsync(command, cancellationToken); + } + private const string TestName = "Test Transaction Command Handler"; private static int _testResultValue; @@ -17,11 +31,11 @@ public class TestCommand : ICommandRequest; private static int _testActualChanges; - private readonly CancellationToken _testCancellationToken = CancellationToken.None; + private CancellationToken _testCancellationToken; private readonly TestCommand _testCommand = new(); - private Mock> _loggerMock = null!; + private Mock> _loggerMock = null!; private Mock _unitOfWorkMock = null!; @@ -29,38 +43,26 @@ public class TestCommand : ICommandRequest; private static Func>> _internalExecuteAsync = null!; - private static TestTransactionCommandWithValueHandler _testHandler = null!; + private static TestTransactionCommandHandler _testHandler = null!; - public class TestTransactionCommandWithValueHandler(ILogger logger, IUnitOfWork unitOfWork) - : TransactionCommandHandler(logger, unitOfWork) + [SetUp] + public void SetUp() { - protected override string CommandName => TestName; - - protected override int ExpectedChanges => _testExpectedChanges; + _testResultValue = 123; - protected override Task> InternalExecuteAsync(TestCommand command, CancellationToken cancellationToken) - => _internalExecuteAsync(command, cancellationToken); + _testExpectedChanges = -1; - public Task> TestExecuteAsync(TestCommand command, CancellationToken cancellationToken) - => ExecuteAsync(command, cancellationToken); - } + _testCancellationToken = CancellationToken.None; - [SetUp] - public void SetUp() - { - _loggerMock = new Mock>(MockBehavior.Strict); + _loggerMock = new Mock>(MockBehavior.Strict); _unitOfWorkMock = new Mock(MockBehavior.Strict); - _testResultValue = 123; + _testTransaction = new Mock(); _internalExecuteAsync = (_, _) => Task.FromResult(Result.Success(_testResultValue)); - _testHandler = new TestTransactionCommandWithValueHandler(_loggerMock.Object, _unitOfWorkMock.Object); - - _testExpectedChanges = -1; - - _testTransaction = new Mock(); + _testHandler = new TestTransactionCommandHandler(_loggerMock.Object, _unitOfWorkMock.Object); } [Test] @@ -130,7 +132,7 @@ public async Task ExecuteAsync_WhenSuccessfulAndValidNumberOfChanges_ShouldCommi var result = await Act(); // Assert - AssertSuccess(result); + AssertSuccess(result, _testResultValue); } [Test] @@ -149,7 +151,7 @@ public async Task ExecuteAsync_WhenSuccessfulAndNotValidatingChanges_ShouldCommi var result = await Act(); // Assert - AssertSuccess(result); + AssertSuccess(result, _testResultValue); } [Test] @@ -206,8 +208,7 @@ public async Task ExecuteAsync_WhenSuccessfulAndExceptionOccurs_ShouldRollbackAn AssertInternalError(result, testError); } - private Task> Act() - => _testHandler.TestExecuteAsync(_testCommand, _testCancellationToken); + private Task> Act() => _testHandler.TestExecuteAsync(_testCommand, _testCancellationToken); private void SetupUnitOfWork_BeginTransactionAsync() => _unitOfWorkMock diff --git a/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs b/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs index e634fce..4350916 100644 --- a/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs +++ b/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs @@ -7,7 +7,6 @@ public abstract class CommandHandler(ILogger logger) where TC { protected abstract string CommandName { get; } - // ReSharper disable once MemberCanBePrivate.Global protected ILogger Logger { get; } = logger; /// diff --git a/DannyGoodacre.Core/CommandQuery/CommandHandler.cs b/DannyGoodacre.Core/CommandQuery/CommandHandler.cs index cb05408..993f181 100644 --- a/DannyGoodacre.Core/CommandQuery/CommandHandler.cs +++ b/DannyGoodacre.Core/CommandQuery/CommandHandler.cs @@ -7,7 +7,6 @@ public abstract class CommandHandler(ILogger logger) where TCommand : { protected abstract string CommandName { get; } - // ReSharper disable once MemberCanBePrivate.Global protected ILogger Logger { get; } = logger; /// diff --git a/DannyGoodacre.Tests.Core/TestBase.cs b/DannyGoodacre.Tests.Core/TestBase.cs index e775368..9bdf23e 100644 --- a/DannyGoodacre.Tests.Core/TestBase.cs +++ b/DannyGoodacre.Tests.Core/TestBase.cs @@ -29,7 +29,16 @@ protected static void AssertSuccess(Result result) Assert.That(result.IsSuccess, Is.True); Assert.That(result.Status, Is.EqualTo(Status.Success)); } + } + protected static void AssertSuccess(Result result, T expectedValue) + { + using (Assert.EnterMultipleScope()) + { + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.Status, Is.EqualTo(Status.Success)); + Assert.That(result.Value, Is.EqualTo(expectedValue)); + } } protected static void AssertInvalid(Result result) From 781aa0deee54ca44bef749a64f3ee1895df13423 Mon Sep 17 00:00:00 2001 From: Danny Goodacre Date: Sat, 10 Jan 2026 21:07:14 +0000 Subject: [PATCH 07/10] WIP --- DannyGoodacre.Tests.Core/DannyGoodacre.Tests.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DannyGoodacre.Tests.Core/DannyGoodacre.Tests.Core.csproj b/DannyGoodacre.Tests.Core/DannyGoodacre.Tests.Core.csproj index 601d47c..7f4bda6 100644 --- a/DannyGoodacre.Tests.Core/DannyGoodacre.Tests.Core.csproj +++ b/DannyGoodacre.Tests.Core/DannyGoodacre.Tests.Core.csproj @@ -3,7 +3,7 @@ net10.0 DannyGoodacre.Tests.Core - 0.2.0 + 0.2.1 Danny Goodacre Common logic for my .NET test projects. https://github.com/dannygoodacre/DannyGoodacre.Core From a2480d9f44d64f6caf1eea0eb7eba1b50f7c008f Mon Sep 17 00:00:00 2001 From: Danny Goodacre Date: Sat, 10 Jan 2026 21:39:26 +0000 Subject: [PATCH 08/10] WIP --- .../CommandQuery/Abstractions/ITransactionProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DannyGoodacre.Core/CommandQuery/Abstractions/ITransactionProvider.cs b/DannyGoodacre.Core/CommandQuery/Abstractions/ITransactionProvider.cs index 37ed2a0..8238292 100644 --- a/DannyGoodacre.Core/CommandQuery/Abstractions/ITransactionProvider.cs +++ b/DannyGoodacre.Core/CommandQuery/Abstractions/ITransactionProvider.cs @@ -1,7 +1,7 @@ namespace DannyGoodacre.Core.CommandQuery.Abstractions; /// -/// Provides functionality for initiating an . +/// Provides functionality for initiating an instance. /// public interface ITransactionProvider { From 02987b70dcdfae26bc299a574241cf07176331df Mon Sep 17 00:00:00 2001 From: Danny Goodacre Date: Sat, 10 Jan 2026 21:43:46 +0000 Subject: [PATCH 09/10] WIP --- .../TransactionCommandHandlerValueTests.cs | 4 ++-- .../CommandQuery/CommandHandler.T.cs | 18 +++++++++--------- .../CommandQuery/CommandHandler.cs | 14 +++++++------- .../CommandQuery/QueryHandler.cs | 10 +++++----- .../TransactionCommandHandler.T.cs | 10 +++++----- .../CommandQuery/TransactionCommandHandler.cs | 10 +++++----- 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs index 91366fa..4bc38f7 100644 --- a/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs +++ b/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs @@ -16,8 +16,8 @@ public class TestTransactionCommandHandler(ILogger logger, IUnitOfWork unitOfWor protected override int ExpectedChanges => _testExpectedChanges; - protected override Task> InternalExecuteAsync(TestCommand command, CancellationToken cancellationToken) - => _internalExecuteAsync(command, cancellationToken); + protected override Task> InternalExecuteAsync(TestCommand commandRequest, CancellationToken cancellationToken) + => _internalExecuteAsync(commandRequest, cancellationToken); public Task> TestExecuteAsync(TestCommand command, CancellationToken cancellationToken) => ExecuteAsync(command, cancellationToken); diff --git a/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs b/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs index 4350916..75d87a4 100644 --- a/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs +++ b/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs @@ -3,7 +3,7 @@ namespace DannyGoodacre.Core.CommandQuery; -public abstract class CommandHandler(ILogger logger) where TCommand : ICommandRequest +public abstract class CommandHandler(ILogger logger) where TCommandRequest : ICommandRequest { protected abstract string CommandName { get; } @@ -13,30 +13,30 @@ public abstract class CommandHandler(ILogger logger) where TC /// Validate the command before execution. /// /// A to populate with the operation's outcome. - /// The command request to validate. - protected virtual void Validate(ValidationState validationState, TCommand command) + /// The command request to validate. + protected virtual void Validate(ValidationState validationState, TCommandRequest commandRequest) { } /// /// The internal command logic. /// - /// The valid command request to process. + /// The valid command request to process. /// A to observe while performing the operation. /// A indicating the outcome of the operation. - protected abstract Task> InternalExecuteAsync(TCommand command, CancellationToken cancellationToken); + protected abstract Task> InternalExecuteAsync(TCommandRequest commandRequest, CancellationToken cancellationToken); /// /// Run the command by validating first and, if successful, execute the internal logic. /// - /// The command request to validate and process. + /// The command request to validate and process. /// A to observe while performing the operation. /// A indicating the outcome of the operation. - protected async virtual Task> ExecuteAsync(TCommand command, CancellationToken cancellationToken) + protected async virtual Task> ExecuteAsync(TCommandRequest commandRequest, CancellationToken cancellationToken) { var validationState = new ValidationState(); - Validate(validationState, command); + Validate(validationState, commandRequest); if (validationState.HasErrors) { @@ -54,7 +54,7 @@ protected async virtual Task> ExecuteAsync(TCommand command, Can try { - return await InternalExecuteAsync(command, cancellationToken); + return await InternalExecuteAsync(commandRequest, cancellationToken); } catch (OperationCanceledException) { diff --git a/DannyGoodacre.Core/CommandQuery/CommandHandler.cs b/DannyGoodacre.Core/CommandQuery/CommandHandler.cs index 993f181..317bff9 100644 --- a/DannyGoodacre.Core/CommandQuery/CommandHandler.cs +++ b/DannyGoodacre.Core/CommandQuery/CommandHandler.cs @@ -3,7 +3,7 @@ namespace DannyGoodacre.Core.CommandQuery; -public abstract class CommandHandler(ILogger logger) where TCommand : ICommandRequest +public abstract class CommandHandler(ILogger logger) where TCommandRequest : ICommandRequest { protected abstract string CommandName { get; } @@ -14,7 +14,7 @@ public abstract class CommandHandler(ILogger logger) where TCommand : /// /// A to populate with the operation's outcome. /// The command request to validate. - protected virtual void Validate(ValidationState validationState, TCommand command) + protected virtual void Validate(ValidationState validationState, TCommandRequest command) { } @@ -24,19 +24,19 @@ protected virtual void Validate(ValidationState validationState, TCommand comman /// The valid command request to process. /// A to observe while performing the operation. /// A indicating the outcome of the operation. - protected abstract Task InternalExecuteAsync(TCommand command, CancellationToken cancellationToken); + protected abstract Task InternalExecuteAsync(TCommandRequest command, CancellationToken cancellationToken); /// /// Run the command by validating first and, if valid, execute the internal logic. /// - /// The command request to validate and process. + /// The command request to validate and process. /// A to observe while performing the operation. /// A indicating the outcome of the operation. - protected async virtual Task ExecuteAsync(TCommand command, CancellationToken cancellationToken) + protected async virtual Task ExecuteAsync(TCommandRequest commandRequest, CancellationToken cancellationToken) { var validationState = new ValidationState(); - Validate(validationState, command); + Validate(validationState, commandRequest); if (validationState.HasErrors) { @@ -54,7 +54,7 @@ protected async virtual Task ExecuteAsync(TCommand command, Cancellation try { - return await InternalExecuteAsync(command, cancellationToken); + return await InternalExecuteAsync(commandRequest, cancellationToken); } catch (OperationCanceledException) { diff --git a/DannyGoodacre.Core/CommandQuery/QueryHandler.cs b/DannyGoodacre.Core/CommandQuery/QueryHandler.cs index 33bcccb..d8f980c 100644 --- a/DannyGoodacre.Core/CommandQuery/QueryHandler.cs +++ b/DannyGoodacre.Core/CommandQuery/QueryHandler.cs @@ -3,7 +3,7 @@ namespace DannyGoodacre.Core.CommandQuery; -public abstract class QueryHandler(ILogger logger) where TQuery : IQueryRequest +public abstract class QueryHandler(ILogger logger) where TQueryRequest : IQueryRequest { protected abstract string QueryName { get; } @@ -14,8 +14,8 @@ public abstract class QueryHandler(ILogger logger) where TQuery /// Validate the query before execution. /// /// A to populate with the operation's outcome. - /// The query request to validate. - protected virtual void Validate(ValidationState validationState, TQuery query) + /// The query request to validate. + protected virtual void Validate(ValidationState validationState, TQueryRequest queryRequest) { } @@ -25,7 +25,7 @@ protected virtual void Validate(ValidationState validationState, TQuery query) /// The valid query request to process. /// A to observe while performing the operation. /// A indicating the outcome of the operation. - protected abstract Task> InternalExecuteAsync(TQuery query, CancellationToken cancellationToken); + protected abstract Task> InternalExecuteAsync(TQueryRequest query, CancellationToken cancellationToken); /// /// Run the query by validating first and, if successful, execute the internal logic. @@ -33,7 +33,7 @@ protected virtual void Validate(ValidationState validationState, TQuery query) /// The query request to validate and process. /// A to observe while performing the operation. /// A indicating the outcome of the operation. - protected async Task> ExecuteAsync(TQuery query, CancellationToken cancellationToken) + protected async Task> ExecuteAsync(TQueryRequest query, CancellationToken cancellationToken) { var validationState = new ValidationState(); diff --git a/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.T.cs b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.T.cs index 86d0eca..c930037 100644 --- a/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.T.cs +++ b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.T.cs @@ -3,8 +3,8 @@ namespace DannyGoodacre.Core.CommandQuery; -public abstract class TransactionCommandHandler(ILogger logger, IUnitOfWork unitOfWork) - : CommandHandler(logger) where TCommand : ICommandRequest +public abstract class TransactionCommandHandler(ILogger logger, IUnitOfWork unitOfWork) + : CommandHandler(logger) where TCommandRequest : ICommandRequest { /// /// The number of state entries expected to be persisted upon completion. @@ -21,18 +21,18 @@ public abstract class TransactionCommandHandler(ILogger logge /// Run the command by validating first and, if valid, execute the internal logic. /// If the command executes successfully, save the changes to the database. /// - /// The command request to validate and process. + /// The command request to validate and process. /// A to observe while performing the operation. /// /// A indicating the outcome of the operation. /// - protected async override Task> ExecuteAsync(TCommand command, CancellationToken cancellationToken) + protected async override Task> ExecuteAsync(TCommandRequest commandRequest, CancellationToken cancellationToken) { await using var transaction = await unitOfWork.BeginTransactionAsync(cancellationToken); try { - var result = await base.ExecuteAsync(command, cancellationToken); + var result = await base.ExecuteAsync(commandRequest, cancellationToken); if (!result.IsSuccess) { diff --git a/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.cs b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.cs index d00391a..2ee11af 100644 --- a/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.cs +++ b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.cs @@ -3,8 +3,8 @@ namespace DannyGoodacre.Core.CommandQuery; -public abstract class TransactionCommandHandler(ILogger logger, IUnitOfWork unitOfWork) - : CommandHandler(logger) where TCommand : ICommandRequest +public abstract class TransactionCommandHandler(ILogger logger, IUnitOfWork unitOfWork) + : CommandHandler(logger) where TCommandRequest : ICommandRequest { /// /// The number of state entries expected to be persisted upon completion. @@ -21,18 +21,18 @@ public abstract class TransactionCommandHandler(ILogger logger, IUnitO /// Run the command by validating first and, if valid, execute the internal logic. /// If the command executes successfully, save the changes to the database. /// - /// The command request to validate and process. + /// The command request to validate and process. /// A to observe while performing the operation. /// /// A indicating the outcome of the operation. /// - protected async override Task ExecuteAsync(TCommand command, CancellationToken cancellationToken) + protected async override Task ExecuteAsync(TCommandRequest commandRequest, CancellationToken cancellationToken) { await using var transaction = await unitOfWork.BeginTransactionAsync(cancellationToken); try { - var result = await base.ExecuteAsync(command, cancellationToken); + var result = await base.ExecuteAsync(commandRequest, cancellationToken); if (!result.IsSuccess) { From 1840fd4252a96cf15840ee504449ae7c10704b1b Mon Sep 17 00:00:00 2001 From: Danny Goodacre Date: Sat, 10 Jan 2026 23:02:45 +0000 Subject: [PATCH 10/10] WIP --- DannyGoodacre.Core/DannyGoodacre.Core.csproj | 4 ++-- DannyGoodacre.Tests.Core/DannyGoodacre.Tests.Core.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DannyGoodacre.Core/DannyGoodacre.Core.csproj b/DannyGoodacre.Core/DannyGoodacre.Core.csproj index 94998f8..15e83ef 100644 --- a/DannyGoodacre.Core/DannyGoodacre.Core.csproj +++ b/DannyGoodacre.Core/DannyGoodacre.Core.csproj @@ -5,7 +5,7 @@ DannyGoodacre.Core 0.3.0 Danny Goodacre - Common logic for my .NET projects. + A lightweight CQRS and clean architecture foundation library, including a result pattern and transaction management. https://github.com/dannygoodacre/DannyGoodacre.Core MIT enable @@ -13,7 +13,7 @@ - + diff --git a/DannyGoodacre.Tests.Core/DannyGoodacre.Tests.Core.csproj b/DannyGoodacre.Tests.Core/DannyGoodacre.Tests.Core.csproj index 7f4bda6..b090d20 100644 --- a/DannyGoodacre.Tests.Core/DannyGoodacre.Tests.Core.csproj +++ b/DannyGoodacre.Tests.Core/DannyGoodacre.Tests.Core.csproj @@ -5,7 +5,7 @@ DannyGoodacre.Tests.Core 0.2.1 Danny Goodacre - Common logic for my .NET test projects. + A lightweight testing foundation library. https://github.com/dannygoodacre/DannyGoodacre.Core MIT enable