diff --git a/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerTests.cs index 7f08142..2bb01d5 100644 --- a/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerTests.cs +++ b/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerTests.cs @@ -1,101 +1,116 @@ 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"; + public class TestCommandHandler(ILogger logger) : CommandHandler(logger) + { + protected override string CommandName => TestName; + + protected override void Validate(ValidationState validationState, TestCommandRequest commandRequest) + => _testValidate(validationState, commandRequest); - private const string TestProperty = "Test Property"; + protected override Task InternalExecuteAsync(TestCommandRequest commandRequest, CancellationToken cancellationToken) + => _testInternalExecuteAsync(commandRequest, cancellationToken); - private const string TestError = "Test Error"; + public Task TestExecuteAsync(TestCommandRequest commandRequest, CancellationToken cancellationToken) + => ExecuteAsync(commandRequest, cancellationToken); + } - private const string TestExceptionMessage = "Test Exception Message"; + private const string TestName = "Test Command Handler"; - private readonly CancellationToken _testCancellationToken = CancellationToken.None; + private CancellationToken _testCancellationToken; - private readonly TestCommand _testCommand = new(); + private Mock> _loggerMock = null!; - private static Action _validate = (_, _) => {}; + private static Action _testValidate = null!; - private static Func> _internalExecuteAsync = (_, _) => Task.FromResult(new Result()); + private static Func> _testInternalExecuteAsync = null!; - private Mock> _loggerMock = null!; + private static TestCommandRequest _testCommandRequest = null!; - public class TestCommandHandler(ILogger logger) : CommandHandler(logger) + private static TestCommandHandler _testCommandHandler = null!; + + [SetUp] + public void SetUp() { - protected override string CommandName => TestName; + _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()); + + _loggerMock = new Mock>(MockBehavior.Strict); - public Task TestExecuteAsync(TestCommand command, CancellationToken cancellationToken) - => ExecuteAsync(command, cancellationToken); + _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); + } + [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); @@ -105,23 +120,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/CommandHandlerValueTests.cs b/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerValueTests.cs index 36a657e..4a72d6f 100644 --- a/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerValueTests.cs +++ b/DannyGoodacre.Core.Tests/CommandQuery/CommandHandlerValueTests.cs @@ -1,100 +1,120 @@ 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 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 ac1c40a..a17ab40 100644 --- a/DannyGoodacre.Core.Tests/CommandQuery/QueryHandlerTests.cs +++ b/DannyGoodacre.Core.Tests/CommandQuery/QueryHandlerTests.cs @@ -1,100 +1,120 @@ 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 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 new file mode 100644 index 0000000..03a64c1 --- /dev/null +++ b/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerTests.cs @@ -0,0 +1,242 @@ +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 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"; + + private static int _testExpectedChanges; + + private static int _testActualChanges; + + private CancellationToken _testCancellationToken; + + private Mock> _loggerMock = null!; + + private Mock _unitOfWorkMock = null!; + + private Mock _transactionMock = null!; + + private static Func> _internalExecuteAsync = null!; + + private readonly TestCommandRequest _testCommandRequest = new(); + + 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); + } + + [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 '{TestName}' 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_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() + { + // 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 '{TestName}' experienced a transaction failure: {testError}", exception: exception); + + SetupTransaction_DisposeAsync(); + + // Act + var result = await Act(); + + // Assert + AssertInternalError(result, testError); + } + + private Task Act() => _testHandler.TestExecuteAsync(_testCommandRequest, _testCancellationToken); + + private void SetupUnitOfWork_BeginTransactionAsync() + => _unitOfWorkMock + .Setup(x => x.BeginTransactionAsync( + It.Is(y => y == _testCancellationToken))) + .ReturnsAsync(_transactionMock.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_CommitAsync() + => _transactionMock + .Setup(x => x.CommitAsync( + It.Is(y => y == _testCancellationToken))) + .Returns(Task.CompletedTask) + .Verifiable(Times.Once); + + private void SetupTransaction_RollbackAsync() + => _transactionMock + .Setup(x => x.RollbackAsync( + It.Is(y => y == _testCancellationToken))) + .Returns(Task.CompletedTask) + .Verifiable(Times.Once); + + private void SetupTransaction_DisposeAsync() + => _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 new file mode 100644 index 0000000..4bc38f7 --- /dev/null +++ b/DannyGoodacre.Core.Tests/CommandQuery/TransactionCommandHandlerValueTests.cs @@ -0,0 +1,246 @@ +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; + + 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 commandRequest, CancellationToken cancellationToken) + => _internalExecuteAsync(commandRequest, cancellationToken); + + public Task> TestExecuteAsync(TestCommand command, CancellationToken cancellationToken) + => ExecuteAsync(command, cancellationToken); + } + + private const string TestName = "Test Transaction Command Handler"; + + private static int _testResultValue; + + private static int _testExpectedChanges; + + private static int _testActualChanges; + + private CancellationToken _testCancellationToken; + + 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!; + + [SetUp] + public void SetUp() + { + _testResultValue = 123; + + _testExpectedChanges = -1; + + _testCancellationToken = CancellationToken.None; + + _loggerMock = new Mock>(MockBehavior.Strict); + + _unitOfWorkMock = new Mock(MockBehavior.Strict); + + _testTransaction = new Mock(); + + _internalExecuteAsync = (_, _) => Task.FromResult(Result.Success(_testResultValue)); + + _testHandler = new TestTransactionCommandHandler(_loggerMock.Object, _unitOfWorkMock.Object); + } + + [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 '{TestName}' 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, _testResultValue); + } + + [Test] + public async Task ExecuteAsync_WhenSuccessfulAndNotValidatingChanges_ShouldCommitAndReturnSuccess() + { + // Arrange + SetupUnitOfWork_BeginTransactionAsync(); + + SetupUnitOfWork_SaveChangesAsync(); + + SetupTransaction_CommitAsync(); + + SetupTransaction_DisposeAsync(); + + // Act + var result = await Act(); + + // Assert + AssertSuccess(result, _testResultValue); + } + + [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() + { + // 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 '{TestName}' experienced a transaction failure: {testError}", exception: exception); + + 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_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( + 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); +} 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/ICommandRequest.cs b/DannyGoodacre.Core/CommandQuery/Abstractions/ICommandRequest.cs new file mode 100644 index 0000000..95f0706 --- /dev/null +++ b/DannyGoodacre.Core/CommandQuery/Abstractions/ICommandRequest.cs @@ -0,0 +1,6 @@ +namespace DannyGoodacre.Core.CommandQuery.Abstractions; + +/// +/// A command request. +/// +public interface ICommandRequest; diff --git a/DannyGoodacre.Core/CommandQuery/Abstractions/IQueryRequest.cs b/DannyGoodacre.Core/CommandQuery/Abstractions/IQueryRequest.cs new file mode 100644 index 0000000..c32044a --- /dev/null +++ b/DannyGoodacre.Core/CommandQuery/Abstractions/IQueryRequest.cs @@ -0,0 +1,6 @@ +namespace DannyGoodacre.Core.CommandQuery.Abstractions; + +/// +/// A query request. +/// +public interface IQueryRequest; diff --git a/DannyGoodacre.Core/CommandQuery/Abstractions/ITransaction.cs b/DannyGoodacre.Core/CommandQuery/Abstractions/ITransaction.cs new file mode 100644 index 0000000..b2d41bf --- /dev/null +++ b/DannyGoodacre.Core/CommandQuery/Abstractions/ITransaction.cs @@ -0,0 +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 new file mode 100644 index 0000000..8238292 --- /dev/null +++ b/DannyGoodacre.Core/CommandQuery/Abstractions/ITransactionProvider.cs @@ -0,0 +1,14 @@ +namespace DannyGoodacre.Core.CommandQuery.Abstractions; + +/// +/// Provides functionality for initiating an instance. +/// +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 new file mode 100644 index 0000000..ac410c0 --- /dev/null +++ b/DannyGoodacre.Core/CommandQuery/Abstractions/IUnitOfWork.cs @@ -0,0 +1,21 @@ +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 +{ + /// + /// 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..75d87a4 100644 --- a/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs +++ b/DannyGoodacre.Core/CommandQuery/CommandHandler.T.cs @@ -1,42 +1,42 @@ +using DannyGoodacre.Core.CommandQuery.Abstractions; using Microsoft.Extensions.Logging; namespace DannyGoodacre.Core.CommandQuery; -public abstract class CommandHandler(ILogger logger) where TCommand : ICommand +public abstract class CommandHandler(ILogger logger) where TCommandRequest : ICommandRequest { protected abstract string CommandName { get; } - // ReSharper disable once MemberCanBePrivate.Global protected ILogger Logger { get; } = logger; /// /// 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 3148a47..317bff9 100644 --- a/DannyGoodacre.Core/CommandQuery/CommandHandler.cs +++ b/DannyGoodacre.Core/CommandQuery/CommandHandler.cs @@ -1,12 +1,12 @@ +using DannyGoodacre.Core.CommandQuery.Abstractions; using Microsoft.Extensions.Logging; namespace DannyGoodacre.Core.CommandQuery; -public abstract class CommandHandler(ILogger logger) where TCommand : ICommand +public abstract class CommandHandler(ILogger logger) where TCommandRequest : ICommandRequest { protected abstract string CommandName { get; } - // ReSharper disable once MemberCanBePrivate.Global protected ILogger Logger { get; } = logger; /// @@ -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/ICommand.cs b/DannyGoodacre.Core/CommandQuery/ICommand.cs deleted file mode 100644 index b1fd776..0000000 --- a/DannyGoodacre.Core/CommandQuery/ICommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DannyGoodacre.Core.CommandQuery; - -/// -/// A command request. -/// -public interface ICommand; diff --git a/DannyGoodacre.Core/CommandQuery/IQuery.cs b/DannyGoodacre.Core/CommandQuery/IQuery.cs deleted file mode 100644 index 5e1114a..0000000 --- a/DannyGoodacre.Core/CommandQuery/IQuery.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DannyGoodacre.Core.CommandQuery; - -/// -/// A query request. -/// -public interface IQuery; diff --git a/DannyGoodacre.Core/CommandQuery/QueryHandler.cs b/DannyGoodacre.Core/CommandQuery/QueryHandler.cs index e6a9ab1..d8f980c 100644 --- a/DannyGoodacre.Core/CommandQuery/QueryHandler.cs +++ b/DannyGoodacre.Core/CommandQuery/QueryHandler.cs @@ -1,8 +1,9 @@ +using DannyGoodacre.Core.CommandQuery.Abstractions; using Microsoft.Extensions.Logging; namespace DannyGoodacre.Core.CommandQuery; -public abstract class QueryHandler(ILogger logger) where TQuery : IQuery +public abstract class QueryHandler(ILogger logger) where TQueryRequest : IQueryRequest { protected abstract string QueryName { get; } @@ -13,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) { } @@ -24,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. @@ -32,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 new file mode 100644 index 0000000..c930037 --- /dev/null +++ b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.T.cs @@ -0,0 +1,76 @@ +using DannyGoodacre.Core.CommandQuery.Abstractions; +using Microsoft.Extensions.Logging; + +namespace DannyGoodacre.Core.CommandQuery; + +public abstract class TransactionCommandHandler(ILogger logger, IUnitOfWork unitOfWork) + : CommandHandler(logger) where TCommandRequest : ICommandRequest +{ + /// + /// The number of state entries expected to be persisted upon completion. + /// + /// + /// Defaults to -1 to disable validation. + /// + /// + /// This is compared against the result of . + /// + protected virtual int ExpectedChanges => -1; + + /// + /// 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. + /// A to observe while performing the operation. + /// + /// A indicating the outcome of the operation. + /// + protected async override Task> ExecuteAsync(TCommandRequest commandRequest, CancellationToken cancellationToken) + { + await using var transaction = await unitOfWork.BeginTransactionAsync(cancellationToken); + + try + { + var result = await base.ExecuteAsync(commandRequest, cancellationToken); + + if (!result.IsSuccess) + { + await transaction.RollbackAsync(cancellationToken); + + return result; + } + + var actualChanges = await unitOfWork.SaveChangesAsync(cancellationToken); + + 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 (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(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 new file mode 100644 index 0000000..2ee11af --- /dev/null +++ b/DannyGoodacre.Core/CommandQuery/TransactionCommandHandler.cs @@ -0,0 +1,76 @@ +using DannyGoodacre.Core.CommandQuery.Abstractions; +using Microsoft.Extensions.Logging; + +namespace DannyGoodacre.Core.CommandQuery; + +public abstract class TransactionCommandHandler(ILogger logger, IUnitOfWork unitOfWork) + : CommandHandler(logger) where TCommandRequest : ICommandRequest +{ + /// + /// The number of state entries expected to be persisted upon completion. + /// + /// + /// Defaults to -1 to disable validation. + /// + /// + /// This is compared against the result of . + /// + protected virtual int ExpectedChanges => -1; + + /// + /// 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. + /// A to observe while performing the operation. + /// + /// A indicating the outcome of the operation. + /// + protected async override Task ExecuteAsync(TCommandRequest commandRequest, CancellationToken cancellationToken) + { + await using var transaction = await unitOfWork.BeginTransactionAsync(cancellationToken); + + try + { + var result = await base.ExecuteAsync(commandRequest, cancellationToken); + + if (!result.IsSuccess) + { + await transaction.RollbackAsync(cancellationToken); + + return result; + } + + var actualChanges = await unitOfWork.SaveChangesAsync(cancellationToken); + + 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 (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(ex, "Command '{Command}' experienced a transaction failure: {Exception}", CommandName, ex.Message); + + return Result.InternalError(ex.Message); + } + } +} diff --git a/DannyGoodacre.Core/CommandQuery/UnitOfWorkCommandHandler.T.cs b/DannyGoodacre.Core/CommandQuery/UnitOfWorkCommandHandler.T.cs deleted file mode 100644 index ebae580..0000000 --- a/DannyGoodacre.Core/CommandQuery/UnitOfWorkCommandHandler.T.cs +++ /dev/null @@ -1,58 +0,0 @@ -using DannyGoodacre.Core.Data; -using Microsoft.Extensions.Logging; - -namespace DannyGoodacre.Core.CommandQuery; - -public abstract class UnitOfWorkCommandHandler(ILogger logger, IUnitOfWork unitOfWork) - : CommandHandler(logger) where TCommand : ICommand -{ - /// - /// The number of state entries expected to be persisted upon completion. - /// - /// - /// Defaults to -1 to disable validation. - /// - /// - /// This is compared against the result of . - /// - protected virtual int ExpectedChanges => -1; - - /// - /// 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. - /// A to observe while performing the operation. - /// - /// A indicating the outcome of the operation. - /// - protected async override Task> ExecuteAsync(TCommand command, CancellationToken cancellationToken) - { - var result = await base.ExecuteAsync(command, cancellationToken); - - if (!result.IsSuccess) - { - return result; - } - - try - { - var actualChanges = await unitOfWork.SaveChangesAsync(cancellationToken); - - if (ExpectedChanges == -1 || actualChanges == ExpectedChanges) - { - return result; - } - - Logger.LogError("Command '{Command}' made an unexpected number of changes: Expected '{Expected}', Actual '{Actual}'.", CommandName, ExpectedChanges, actualChanges); - - return Result.InternalError("Unexpected number of changes saved."); - } - catch (Exception ex) - { - Logger.LogCritical(ex, "Command '{Command}' failed while saving changes, with exception: {Exception}", CommandName, ex.Message); - - return Result.InternalError(ex.Message); - } - } -} diff --git a/DannyGoodacre.Core/CommandQuery/UnitOfWorkCommandHandler.cs b/DannyGoodacre.Core/CommandQuery/UnitOfWorkCommandHandler.cs deleted file mode 100644 index 3c574a4..0000000 --- a/DannyGoodacre.Core/CommandQuery/UnitOfWorkCommandHandler.cs +++ /dev/null @@ -1,58 +0,0 @@ -using DannyGoodacre.Core.Data; -using Microsoft.Extensions.Logging; - -namespace DannyGoodacre.Core.CommandQuery; - -public abstract class UnitOfWorkCommandHandler(ILogger logger, IUnitOfWork unitOfWork) - : CommandHandler(logger) where TCommand : ICommand -{ - /// - /// The number of state entries expected to be persisted upon completion. - /// - /// - /// Defaults to -1 to disable validation. - /// - /// - /// This is compared against the result of . - /// - protected virtual int ExpectedChanges => -1; - - /// - /// 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. - /// A to observe while performing the operation. - /// - /// A indicating the outcome of the operation. - /// - protected async override Task ExecuteAsync(TCommand command, CancellationToken cancellationToken) - { - var result = await base.ExecuteAsync(command, cancellationToken); - - if (!result.IsSuccess) - { - return result; - } - - try - { - var actualChanges = await unitOfWork.SaveChangesAsync(cancellationToken); - - if (ExpectedChanges == -1 || actualChanges == ExpectedChanges) - { - return result; - } - - Logger.LogError("Command '{Command}' made an unexpected number of changes: Expected '{Expected}', Actual '{Actual}'.", CommandName, ExpectedChanges, actualChanges); - - return Result.InternalError("Unexpected number of changes saved."); - } - catch (Exception ex) - { - Logger.LogCritical(ex, "Command '{Command}' failed while saving changes, with exception: {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..15e83ef 100644 --- a/DannyGoodacre.Core/DannyGoodacre.Core.csproj +++ b/DannyGoodacre.Core/DannyGoodacre.Core.csproj @@ -3,9 +3,9 @@ net10.0 DannyGoodacre.Core - 0.2.0 + 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.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..0504218 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(TransactionCommandHandler<,>)) + { + 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; } + } diff --git a/DannyGoodacre.Tests.Core/DannyGoodacre.Tests.Core.csproj b/DannyGoodacre.Tests.Core/DannyGoodacre.Tests.Core.csproj index 601d47c..b090d20 100644 --- a/DannyGoodacre.Tests.Core/DannyGoodacre.Tests.Core.csproj +++ b/DannyGoodacre.Tests.Core/DannyGoodacre.Tests.Core.csproj @@ -3,9 +3,9 @@ net10.0 DannyGoodacre.Tests.Core - 0.2.0 + 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 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)