From 1dd9d30e2648675811dce6f94e2f7d139a1b62ca Mon Sep 17 00:00:00 2001 From: Wojciech Klusek Date: Wed, 18 Oct 2023 13:53:26 +0200 Subject: [PATCH] Add why avoid commiting transactions in handlers section --- docs/cqrs/command/index.md | 2 +- ...void_commiting_transactions_in_handlers.md | 132 ++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 docs/cqrs/pipeline/avoid_commiting_transactions_in_handlers.md diff --git a/docs/cqrs/command/index.md b/docs/cqrs/command/index.md index 92b3de812..2fbdd25f0 100644 --- a/docs/cqrs/command/index.md +++ b/docs/cqrs/command/index.md @@ -67,7 +67,7 @@ As you can see, the command handler is really simple - it just finds project wit 4. If the business process requires to modify multiple aggregates, try to use [events] (but don't over-engineer). 5. If that does not help, modify/add/delete multiple aggregates. 6. Do not throw exceptions from inside commands. The client will receive generic error (`500 Internal Server Error`). Do it only as a last resort. -7. Database transaction will be commited at the end of the [pipeline] (assuming [CommitTransaction] pipeline element was added), so it's not recommended to commit it inside query handler as it may make serialized [events] inconsistent with the entity. +7. Database transaction will be commited at the end of the [pipeline] (assuming [CommitTransaction] pipeline element was added), so it's not recommended to commit it inside query handler as it may make [events] inconsistent with the entity. To read more why it's not recommended to commit transactions in handlers visit [here](../pipeline/avoid_commiting_transactions_in_handlers.md). ## Naming conventions diff --git a/docs/cqrs/pipeline/avoid_commiting_transactions_in_handlers.md b/docs/cqrs/pipeline/avoid_commiting_transactions_in_handlers.md new file mode 100644 index 000000000..1fea32e23 --- /dev/null +++ b/docs/cqrs/pipeline/avoid_commiting_transactions_in_handlers.md @@ -0,0 +1,132 @@ +# Avoid committing transactions in handlers + +> **Tip:** Before delving into this section, it's highly recommended to explore the [CQRS], [Domain] and [MassTransit] sections. + +Directly commiting transactions in [command]/[operation] handlers poses a challenge, potentially causing inconsistencies between [events] and the associated entities. Let's examine the pipeline configuration below: + +```csharp + protected override void ConfigureApp(IApplicationBuilder app) + { + . . . + app.UseEndpoints(endpoints => + { + endpoints.MapRemoteCqrs( + "/api", + cqrs => + { + cqrs.Commands = c => + c.CQRSTrace() + .Secure() + .Validate() + .CommitTransaction() + .PublishEvents(); + + cqrs.Queries = c => + c.CQRSTrace() + .Secure(); + + cqrs.Operations = c => + c.CQRSTrace() + .Secure() + .CommitTransaction() + .PublishEvents(); + } + ); + }); + } +``` + +Consider the `CommitDatabaseTransactionMiddleware` middleware, which is invoked by the `CommitTransaction()` method. This middleware executes subsequent middlewares and then calls the `SaveChangesAsync` method on `CoreDbContext` to commit all changes within a single transaction. + +```csharp +public class CommitDatabaseTransactionMiddleware + where TDbContext : DbContext +{ + private readonly RequestDelegate next; + + public CommitDatabaseTransactionMiddleware(RequestDelegate next) + { + this.next = next; + } + + public async Task InvokeAsync(HttpContext httpContext, TDbContext dbContext) + { + await next(httpContext); + await dbContext.SaveChangesAsync(httpContext.RequestAborted); + } +} +``` + +Take, for instance, an example where we wish to send an email to an employee upon the occurrence of the `EmployeeAssignedToAssignment` event: + +```csharp +public class Project : IAggregateRoot +{ + . . . + + public void AssignEmployeeToAssignment( + AssignmentId assignmentId, + EmployeeId employeeId) + { + assignments.Single(t => t.Id == assignmentId) + .AssignEmployee(employeeId); + + DomainEvents.Raise(new EmployeeAssignedToAssignment( + AssignmentId assignmentId, + EmployeeId employeeId)); + } + + . . . +} +``` + +Now, let's imagine using this method in a [command] handler: + +```csharp +public class AssignEmployeeToAssignmentCH + : ICommandHandler +{ + private readonly IRepository projects; + private readonly CoreDbContext dbContext; + + public AssignEmployeeToAssignmentCH( + IRepository projects, + CoreDbContext dbContext) + { + this.projects = projects; + this.dbContext = dbContext; + } + + public Task ExecuteAsync(HttpContext context, UpdateProjectName command) + { + var project = await projects.FindAndEnsureExistsAsync( + ProjectId.Parse(command.ProjectId), + context.RequestAborted); + + project.AssignEmployeeToAssignment( + AssignmentId.Parse(command.AssignmentId), + EmployeeId.Parse(command.EmployeeId)); + + projects.Update(project); + + // Directly committing the transaction in the command handler, + // which should be avoided + await dbContext.SaveChangesAsync(context.RequestAborted) + } +} +``` + +In our pipeline configuration, [events] are published after the [command] handler is executed. This implies that changes on `CoreDbContext` will be committed before events are published. In LeanCode Corelibrary, we utilize [MassTransit] for message broker interaction, employing the [transactional outbox](https://masstransit.io/documentation/patterns/transactional-outbox) concept. This ensures that messages are committed to the database before being available to message brokers after publication and the invocation of the `SaveChangesAsync` method on CoreDbContext. + +In our scenario, if the database fails after successfully saving changes to the project, the `EmployeeAssignedToAssignment` message won't be saved, leading to it being unavailable to message brokers and not sent. + +Conversely, removing `SaveChangesAsync` from the [command] handler would result in both messages and project changes being committed in a single transaction. In the event of a database failure, neither project changes nor messages would be saved or sent, providing clients with information about the request failure without unintended side effects. This is why it's not recommended to not commit transactions directly in [command]/[operation] handlers. + +> **Tip:** To read more about LeanCode Corelibrary MassTransit integtation visit [here](../../external_integrations/messaging_masstransit/index.md). + +[CQRS]: ../index.md +[Domain]: ../../domain/index.md +[MassTransit]: ../../external_integrations/messaging_masstransit/index.md +[events]: ../../domain/domain_event/index.md +[command]: ../command/index.md +[operation]: ../operation/index.md diff --git a/mkdocs.yml b/mkdocs.yml index 39cb18597..d571a27fe 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,6 +23,7 @@ nav: - Pipeline: - ./cqrs/pipeline/index.md - Adding custom middlewares: ./cqrs/pipeline/adding_custom_middlewares.md + - Avoid committing transactions in handlers: ./cqrs/pipeline/avoid_commiting_transactions_in_handlers.md - Command: - ./cqrs/command/index.md - Query: