Skip to content

Pin the NServiceBus SQL Server interop transport to a dedicated database under multi-tenanted storage#3271

Merged
jeremydmiller merged 1 commit into
mainfrom
feat/nsb-sqlserver-dedicated-db
Jun 28, 2026
Merged

Pin the NServiceBus SQL Server interop transport to a dedicated database under multi-tenanted storage#3271
jeremydmiller merged 1 commit into
mainfrom
feat/nsb-sqlserver-dedicated-db

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Scenario

From the wolverine.spike sample: Wolverine's SQL Server message storage is multi-tenanted with a database per tenant (UseMasterTableTenancy), while the NServiceBus-compliant SQL Server interop queues live on one shared database NServiceBus reads from — and must not be replicated into each tenant database.

What I found (reproduced vs. real SQL Server)

The interop transport already only ever provisioned/sent/listened against a single database (Storage.Main) — it never iterated tenant databases (unlike the native SQL Server transport, whose SqlServerQueue deliberately fans out over Parent.Databases.ActiveDatabases()). So the reported "creates a queue per database" symptom does not occur on main.

The real gap: that single database was hardcoded to Storage.Main, with no way to point the interop transport at a dedicated shared database decoupled from the tenanted storage.

Change — opt-in dedicated database

opts.PersistMessagesWithSqlServer(main)
    .UseMasterTableTenancy(t => { t.Register("tenant_one", t1); t.Register("tenant_two", t2); });

// Pin the interop queues to ONE shared database, independent of tenant storage
opts.UseNServiceBusSqlServerInterop(autoProvision: true, connectionString: nsbSharedDb);
  • NServiceBusSqlServerTransport.ConnectionString (optional explicit DB).
  • ConnectAsync builds Settings from the explicit string when set, and no longer requires SQL-Server-backed storage in that case — the interop queues are buffered and never touch Wolverine's durable inbox/outbox.
  • ResolveConnectionString prefers the explicit string, so it resolves before ConnectAsync (Weasel CLI / resource model) and the IDatabase resource also targets the dedicated DB.
  • Non-breaking: connectionString is a trailing optional parameter; when omitted, behavior is unchanged (binds to the Main store).

Tests (vs. real SQL Server)

  • Dedicated opt-in: queue table provisioned only in the dedicated DB (asserted absent from main + both tenant DBs); a send under tenant_one lands a row in the dedicated DB only.
  • Default (no opt-in) under multi-tenancy: binds to Main only, never replicated into a tenant database.
  • Existing NSB smoke test still green — 4/4 NSB transport tests pass locally (net9.0).

Plus a docs subsection in docs/guide/durability/sqlserver.md.

🤖 Generated with Claude Code

… database (multi-tenanted storage)

When Wolverine's SQL Server message storage is multi-tenanted with a database per
tenant, the NServiceBus-compliant SQL Server interop queues must stay on the one
shared database NServiceBus reads from — they must not be replicated per tenant.

The interop transport already only ever targeted a single database (Storage.Main),
but that database was hardcoded and could not be decoupled from the tenanted
storage. Add an opt-in: UseNServiceBusSqlServerInterop(connectionString: ...) pins
the transport's queue tables, send, and listen paths to one explicit database,
independent of storage tenancy. When omitted, behavior is unchanged (binds to the
SQL Server Main message store).

- NServiceBusSqlServerTransport.ConnectionString (explicit, optional)
- ConnectAsync builds Settings from the explicit string when set, and no longer
  requires SQL Server-backed storage in that case (interop queues are buffered and
  never touch Wolverine's durable inbox/outbox)
- ResolveConnectionString prefers the explicit string (resolvable before
  ConnectAsync for the Weasel CLI / resource model, so the IDatabase resource also
  targets the dedicated DB)

Tests (vs real SQL Server): dedicated-DB opt-in provisions the queue table ONLY in
the dedicated database (never main/tenant DBs) and a send under a tenant lands
there; default (no connection string) binds to Main and is never replicated into a
tenant database. Plus docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit b9a22c0 into main Jun 28, 2026
26 checks passed
This was referenced Jun 29, 2026
This was referenced Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant