Status: Brainstorming. May close wontfix if no working design emerges.
The 0.9.36 outbox milestone (#138) gave producers a way to enqueue inside the caller's own DbTransaction — the queue write commits or rolls back with the caller's business writes. See docs/outbox-pattern.md and the Outbox Pattern wiki page.
Inbox is the dual on the consumer side. The handler wants to write business data and ack the queue message in one atomic transaction. Right now the dequeue transaction is invisible to the handler, so a business write inside the handler opens its own connection and isn't atomic with the dequeue.
This isn't from scratch. The library already has EnableHoldTransactionUntilMessageCommitted on SqlServer, PostgreSQL, and SQLite. With it on, the per-message connection and transaction stay open across receive, handler dispatch, and RemoveMessage. Handler succeeds -> commit. Handler throws -> rollback. Atomic guarantee already in the machinery. The only missing piece is a seam to hand that connection + transaction to the user's handler.
Where it differs from outbox: outbox-side the caller owns the resource and we borrow it; inbox-side we own it and lend it out. So the outbox lifecycle contract ("we never commit, roll back, or dispose") flips — on inbox we DO commit and roll back, via the existing handler-throws + RemoveMessage paths.
Likely entry points (best guess from a grep)
Source/DotNetWorkQueue.Transport.PostgreSQL/Basic/ConnectionHolder.cs and the SqlServer / SQLite equivalents — where the per-message connection + transaction sit across Receive -> handler dispatch -> RemoveMessage.
Source/DotNetWorkQueue.Transport.RelationalDatabase/ITransportOptions.cs carries the EnableHoldTransactionUntilMessageCommitted flag.
Source/DotNetWorkQueue.Transport.PostgreSQL/Basic/RemoveMessage.cs and the SqlServer equivalent are the commit side.
- The user handler runs on the
Worker / MessageProcessing path. The handoff from ConnectionHolder into the handler context is the seam, and probably not easy. The receive-path threading might not allow a clean exposure.
Open questions for brainstorming
- Surface shape.
DbTransaction alone? A (DbConnection, DbTransaction) pair? An accessor that resolves on demand? Or a property on IReceivedMessage<T> that's non-null only when the transport supports it?
- Opt-in. Outbox uses an
IRelationalProducerQueue<T> capability cast. The receive surface is more complex, so this needs its own design pass.
- Precondition. The feature only works when
EnableHoldTransactionUntilMessageCommitted = true. What happens if the handler tries to use the seam with the option off? Throw at queue start? Throw at first access? Null?
- Cross-DB safety. Less of a concern than outbox (we control the connection), but the user can still reach
transaction.Connection and do whatever. That's the intended use, but worth naming.
- SQLite scope. SQLite has the same option. If the brainstorm says "yes SQLite for inbox", the same milestone should pick up SQLite-outbox so the relational surface stays symmetric. The outbox milestone deferred SQLite-outbox as "extends cleanly if requested later"; this is when. LiteDb isn't a candidate; no ADO.NET transactions in the same shape.
- Sync vs async. Both
SendMessageCommandHandler and SendMessageCommandHandlerAsync exist on every transport; whatever the seam is, it has to work on both.
Why this might not work out
The receive-path threading may make clean exposure infeasible. The handler doesn't run on the same call stack as the connection holder, and injecting the open transaction into the handler context could mean widening abstractions in ways that hurt the non-relational transports more than the inbox helps the relational ones.
If that's where it lands, close wontfix with the writeup on the issue.
Next step: /shipyard:brainstorm against .shipyard/INBOX-PATTERN-IDEA.md after PR #141 merges.
Status: Brainstorming. May close
wontfixif no working design emerges.The 0.9.36 outbox milestone (#138) gave producers a way to enqueue inside the caller's own
DbTransaction— the queue write commits or rolls back with the caller's business writes. See docs/outbox-pattern.md and the Outbox Pattern wiki page.Inbox is the dual on the consumer side. The handler wants to write business data and ack the queue message in one atomic transaction. Right now the dequeue transaction is invisible to the handler, so a business write inside the handler opens its own connection and isn't atomic with the dequeue.
This isn't from scratch. The library already has
EnableHoldTransactionUntilMessageCommittedon SqlServer, PostgreSQL, and SQLite. With it on, the per-message connection and transaction stay open across receive, handler dispatch, andRemoveMessage. Handler succeeds -> commit. Handler throws -> rollback. Atomic guarantee already in the machinery. The only missing piece is a seam to hand that connection + transaction to the user's handler.Where it differs from outbox: outbox-side the caller owns the resource and we borrow it; inbox-side we own it and lend it out. So the outbox lifecycle contract ("we never commit, roll back, or dispose") flips — on inbox we DO commit and roll back, via the existing handler-throws +
RemoveMessagepaths.Likely entry points (best guess from a grep)
Source/DotNetWorkQueue.Transport.PostgreSQL/Basic/ConnectionHolder.csand the SqlServer / SQLite equivalents — where the per-message connection + transaction sit acrossReceive-> handler dispatch ->RemoveMessage.Source/DotNetWorkQueue.Transport.RelationalDatabase/ITransportOptions.cscarries theEnableHoldTransactionUntilMessageCommittedflag.Source/DotNetWorkQueue.Transport.PostgreSQL/Basic/RemoveMessage.csand the SqlServer equivalent are the commit side.Worker/MessageProcessingpath. The handoff fromConnectionHolderinto the handler context is the seam, and probably not easy. The receive-path threading might not allow a clean exposure.Open questions for brainstorming
DbTransactionalone? A(DbConnection, DbTransaction)pair? An accessor that resolves on demand? Or a property onIReceivedMessage<T>that's non-null only when the transport supports it?IRelationalProducerQueue<T>capability cast. The receive surface is more complex, so this needs its own design pass.EnableHoldTransactionUntilMessageCommitted = true. What happens if the handler tries to use the seam with the option off? Throw at queue start? Throw at first access? Null?transaction.Connectionand do whatever. That's the intended use, but worth naming.SendMessageCommandHandlerandSendMessageCommandHandlerAsyncexist on every transport; whatever the seam is, it has to work on both.Why this might not work out
The receive-path threading may make clean exposure infeasible. The handler doesn't run on the same call stack as the connection holder, and injecting the open transaction into the handler context could mean widening abstractions in ways that hurt the non-relational transports more than the inbox helps the relational ones.
If that's where it lands, close
wontfixwith the writeup on the issue.Next step:
/shipyard:brainstormagainst.shipyard/INBOX-PATTERN-IDEA.mdafter PR #141 merges.