Consider the following code:
private readonly IEmailSender _emailSender;
public async Task Consume(ConsumeContext<UserCreated> context) // UserCreated is an event from a separate system
{
await _dbContext.User.AddAsync(new UserAggregate { UserName = "kiddo" });
EmailTemplate[] emailTemplates = CreateEmailTemplatePerUser();
await _dbContext.SaveChangesAsync();
await _emailSender.Send(emailTemplates);
}
To ensure we don't send any duplicate mails, we'd like to refactor this code and use an outbox. Resulting in the following code (simply just replacing IEmailSender
with ISendEndpointProvider
):
private readonly ISendEndpointProvider _sendEndpointProvider;
public async Task Consume(ConsumeContext<UserCreated> context) // UserCreated is an event from a separate system
{
await _dbContext.User.AddAsync(new UserAggregate { UserName = "kiddo" });
EmailCommands[] emailCommands = CreateEmailCommandPerUser();
await _sendEndpointProvider.Send(emailCommands);
await _dbContext.SaveChangesAsync();
}
-- Redacted Inaccurate Statement about designed behavior --
How would one go about designing this system instead? It would've been super nice to use the outbox to create multiple commands that deal with sending 1 mail individually. (our actual use case is to batch them per 100 because of 3rd party API limits but I left that out of the example for clarity)
Creating a new scope theoretically could work (since you'd no longer be in that scoped ConsumeContext
) but that feels hacky and has nasty side effects when working with scoped filters.
When working with the transactional (mongo) outbox, we expect that no matter what context you're in, your messages sould be saved in the outbox.messages collection instead of instantly being sent when in a ConsumeContext
.