georgink
georgink

Reputation: 53

Is it possible to use MassTransit transactional outbox with multiple db contexts?

I am trying to build a modular monolith application where each module is defined in its own class library project. I would like to utilize a single SQL Server 2019 database and create a separate db schema for each of the individual modules. I would like to also utilize the built-in MassTransit outbox pattern implementation. The thing is that I want to have the 3 auto-generated outbox tables in each database schema:

cards.InboxState cards.OutboxState cards.OutboxMessage

transactions.InboxState transactions.OutboxState transactions.OutboxMessage

This is my current MassTransit configuration:

builder.Services.AddMassTransit(x =>
{
    var hostUri = builder.Configuration.GetValueOrThrow<string>("RABBITMQ_URI");
    var username = builder.Configuration.GetValueOrThrow<string>("RABBITMQ_USERNAME");
    var password = builder.Configuration.GetValueOrThrow<string>("RABBITMQ_PASSWORD");

    x.AddConsumer<CreditCardCreatedByIssuerEventConsumer>();
    x.AddConsumer<CreditCardRequestedEventConsumer>();

    x.SetKebabCaseEndpointNameFormatter();

    x.AddEntityFrameworkOutbox<CardsDbContext>(o =>
    {
        o.QueryDelay = TimeSpan.FromSeconds(3);
        o.UseSqlServer();
        o.UseBusOutbox();
    });

    x.AddEntityFrameworkOutbox<TransactionsDbContext>(o =>
    {
        o.QueryDelay = TimeSpan.FromSeconds(3);
        o.UseSqlServer();
        o.UseBusOutbox();
    });

    x.UsingRabbitMq((ctx, cfg) =>
    {
        cfg.Host(new Uri(hostUri), h =>
        {
            h.Username(username);
            h.Password(password);
        });

        cfg.AutoStart = true;
        cfg.ConfigureEndpoints(ctx);
    });
});

I am currently getting the following error:

2024-03-15 18:59:17 fail: MassTransit.EntityFrameworkCoreIntegration.BusOutboxDeliveryService[0]
2024-03-15 18:59:17       ProcessMessageBatch faulted
2024-03-15 18:59:17       System.NullReferenceException: Object reference not set to an instance of an object.
2024-03-15 18:59:17          at MassTransit.Middleware.Outbox.BusOutboxNotification.WaitForDelivery(CancellationToken cancellationToken) in /_/src/MassTransit/Middleware/Outbox/BusOutboxNotification.cs:line 38
2024-03-15 18:59:17          at MassTransit.EntityFrameworkCoreIntegration.BusOutboxDeliveryService`1.ExecuteAsync(CancellationToken stoppingToken) in /_/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/BusOutboxDeliveryService.cs:line 72

The thing is that everything works as expected once I remove one of the db context registrations:

builder.Services.AddMassTransit(x =>
{
    var hostUri = builder.Configuration.GetValueOrThrow<string>("RABBITMQ_URI");
    var username = builder.Configuration.GetValueOrThrow<string>("RABBITMQ_USERNAME");
    var password = builder.Configuration.GetValueOrThrow<string>("RABBITMQ_PASSWORD");

    x.AddConsumer<CreditCardCreatedByIssuerEventConsumer>();
    x.AddConsumer<CreditCardRequestedEventConsumer>();

    x.SetKebabCaseEndpointNameFormatter();

    x.AddEntityFrameworkOutbox<CardsDbContext>(o =>
    {
        o.QueryDelay = TimeSpan.FromSeconds(3);
        o.UseSqlServer();
        o.UseBusOutbox();
    });

    x.UsingRabbitMq((ctx, cfg) =>
    {
        cfg.Host(new Uri(hostUri), h =>
        {
            h.Username(username);
            h.Password(password);
        });

        cfg.AutoStart = true;
        cfg.ConfigureEndpoints(ctx);
    });
});

So the logical question is if this is even possible?

And if it isn't could you suggest some sort of a workaround to this?

Any help would be greatly appreciated! Thanks!

Upvotes: 1

Views: 637

Answers (2)

Eduardo Tolino
Eduardo Tolino

Reputation: 305

@Chris Patterson

At this point, I am not thinking of anything elegant. But what if there were a method that allowed us to pass the correct DbContext to the utilized endpoint? Wouldn't that work?

I noticed that currently, the Outbox doesn't record entries in the table because the instantiated DbContext is incorrect. MassTransit only uses the last configured DbContext, and therefore, SaveChanges() will not save updates from another context.

Example:

x.AddEntityFrameworkOutbox<ReportingDbContext>(o =>
{
    o.UseSqlServer(false);
    o.UseBusOutbox();
});

x.AddEntityFrameworkOutbox<OrderingDbContext>(o =>
{
    o.UseSqlServer(false);
    o.UseBusOutbox();
});

enter image description here

In this case, only the OrderingDbContext is within the MassTransit context because it was the last one configured.

So if I am working in the Reporting context, the Outbox tables are not recorded by EF because MassTransit is using another DbContext.

However, if I make changes in the Ordering context, since I am using the same DbContext that MassTransit is using (it is the same instance / scoped), SaveChanges() will work, and the Outbox will function correctly, recording the event and subsequently calling the consumers.

Wouldn't passing the correct DbContext to MassTransit resolve this issue and allow us to work with multiple DbContexts?

I always code with multiple DbContexts, one for each ClassLibrary within the same Host (when modular monolith).

It would be excellent to use MassTransit with multiple DbContexts.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // Ensure a different Schema for each DbContext.
    modelBuilder.HasDefaultSchema(Constants.Database.Schema);
    modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);

    base.OnModelCreating(modelBuilder);

    modelBuilder.AddInboxStateEntity();
    modelBuilder.AddOutboxMessageEntity();
    modelBuilder.AddOutboxStateEntity();
}

Upvotes: 2

Chris Patterson
Chris Patterson

Reputation: 33457

No, it isn't. The transactional outbox only works with the default bus on a single DbContext.

Upvotes: 2

Related Questions