Skip to content

Commit

Permalink
Add why avoid commiting transactions in handlers section
Browse files Browse the repository at this point in the history
  • Loading branch information
Wojciech Klusek committed Jan 5, 2024
1 parent 7702896 commit 1dd9d30
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 1 deletion.
2 changes: 1 addition & 1 deletion docs/cqrs/command/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
132 changes: 132 additions & 0 deletions docs/cqrs/pipeline/avoid_commiting_transactions_in_handlers.md
Original file line number Diff line number Diff line change
@@ -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<CoreDbContext>()
.PublishEvents();

cqrs.Queries = c =>
c.CQRSTrace()
.Secure();

cqrs.Operations = c =>
c.CQRSTrace()
.Secure()
.CommitTransaction<CoreDbContext>()
.PublishEvents();
}
);
});
}
```

Consider the `CommitDatabaseTransactionMiddleware<TDbContext>` middleware, which is invoked by the `CommitTransaction<CoreDbContext>()` 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<TDbContext>
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<ProjectId>
{
. . .

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<AssignEmployeeToAssignment>
{
private readonly IRepository<Project, ProjectId> projects;
private readonly CoreDbContext dbContext;

public AssignEmployeeToAssignmentCH(
IRepository<Project, ProjectId> 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
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 1dd9d30

Please sign in to comment.