Azure Functions v4 (isolated, .NET 8) that reads session-enabled messages from NO_SESSION/STATE_SUB, reorders them by sessionId + order, and forwards the ordered stream to ORDERED_TOPIC/SESS_SUB (session-enabled). Ordering state is stored as session state on the same input subscription NO_SESSION/STATE_SUB. Supports emulator, Standard, and Premium namespaces.
- Background worker (not
ServiceBusTrigger) accepts sessions viaAcceptNextSessionAsynconNO_SESSION/STATE_SUB; this first client call anchors the send-via entity. - One Service Bus client;
EnableCrossEntityTransactionsis driven byServiceBus:UseTransactions/ServiceBusUseTransactions(ignored for emulator). - TransactionScope covers clone→send→complete and session-state save; state is stored on
NO_SESSION/STATE_SUB, so the whole workflow is in one transaction when enabled. - Rationale (SDK behavior): “Enable cross entity transaction... the first entity that an operation occurs on becomes the entity through which all subsequent sends will be routed through ('send-via' entity)...” (source: ServiceBusClientBuilder).
- Function code:
src/ - Tests:
tests/ - Emulator config:
emulator/config.json, compose:emulator/docker-compose.sbus.yml
flowchart LR
subgraph InputTopic["NO_SESSION"]
INSTATE["STATE_SUB (sessions)"]
end
subgraph Worker["ReorderWorker (AcceptNextSession)"]
RECV["Receive & lock session"]
ORDER["Reorder & manage state"]
STATE["Session state save"]
end
subgraph OrderedTopic["ORDERED_TOPIC"]
OUT["SESS_SUB (sessions)"]
end
INSTATE -- receive (first op = via anchor) --> RECV
RECV --> ORDER
ORDER --> |"send (txn)"| OUT
ORDER --> |"complete (txn)"| INSTATE
ORDER --> |"set state (txn)"| STATE
- Set
ServiceBusConnectionbefore running tests or the worker; otherwise tests are skipped (emulator is optional). - Optional:
ServiceBusUseTransactions=trueto enable TransactionScope on Premium.src/local.settings.jsonexample:
{
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"ServiceBusConnection": "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=<emulator-key>;UseDevelopmentEmulator=true;",
"FUNCTIONS_WORKER_PROCESS_COUNT": "1",
"ServiceBus:UseTransactions": "false"
}
}If running inside a container and localhost is not reachable, use host.docker.internal.
cd emulator/
docker compose -f docker-compose.sbus.yml up -d
docker compose -f docker-compose.sbus.yml ps
# stop/clean:
docker compose -f docker-compose.sbus.yml down -vIf the emulator SQL container fails to start, ensure env vars are provided (defaults are baked into compose): SQL_PASSWORD=LocalEmulatorSql123!, ACCEPT_EULA=Y.
The emulator web port publishes to 5300 (internal 5300). Prefer another? Adjust EMULATOR_HTTP_PORT and the port mapping in emulator/docker-compose.sbus.yml.
Devcontainer no longer depends on the emulator network; access the emulator from inside the container via host.docker.internal (added in runArgs).
Connection strings (emulator):
- Host OS:
Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=LocalEmulatorKey123!;UseDevelopmentEmulator=true; - From devcontainer:
Endpoint=sb://host.docker.internal;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=LocalEmulatorKey123!;UseDevelopmentEmulator=true;
cd src/
PATH=$HOME/.dotnet:$PATH func startIf storage is needed, start Azurite:
docker run -d --name azurite -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azuriteProject: tests/SbReorder.Tests.
Provide ServiceBusConnection; if absent, tests are skipped. ServiceBusUseTransactions enables tx on Premium.
- Emulator (transactions off):
PATH=$HOME/.dotnet:$PATH ServiceBusUseTransactions=false \
ServiceBusConnection="$YOUR_CONNECTION" \
dotnet test tests/SbReorder.Tests- Standard namespace:
PATH=$HOME/.dotnet:$PATH ServiceBusUseTransactions=false \
ServiceBusConnection="$YOUR_CONNECTION" \
dotnet test tests/SbReorder.Tests- Premium namespace (transactions on):
PATH=$HOME/.dotnet:$PATH ServiceBusUseTransactions=true \
ServiceBusConnection="$YOUR_CONNECTION" \
dotnet test tests/SbReorder.TestsLatest results:
- Emulator (UseTransactions=false): PASS
- Standard (UseTransactions=false): PASS
- Premium (
UseTransactions=true): PASS (transaction wraps only send+complete; session state is committed separately)
- order < expected → DLQ input message (in
NO_SESSION/STATE_SUBDLQ). - order > expected → defer input and record in session state on
NO_SESSION/STATE_SUB; state save is outside the transaction. - order == expected → send to
ORDERED_TOPIC/SESS_SUB, complete input, then drain deferred in order.
flowchart TD
START[Receive msg] --> PARSE[Read sessionId/order]
PARSE --> LOAD[Load session state]
LOAD --> FIRST{state.LastSeen == 0?}
FIRST -->|yes| INIT[Send+complete; state=order]
FIRST -->|no| EXPECT[expected = lastSeen+1]
EXPECT --> MATCH{order == expected?}
MATCH -->|yes| INORDER[Send+complete; drain deferred; lastSeen=order; save state]
MATCH -->|no| SMALL{order < expected?}
SMALL -->|yes| DLQ[Dead-letter]
SMALL -->|no| DEFER[Defer msg; add to deferred; save state]
INIT --> END
INORDER --> END
DLQ --> END
DEFER --> END
- .NET SDK 8.0.417 (
global.json) - Azure Functions Core Tools v4 (LTS)
- git, pwsh