Sessions
Session support enables ordered, grouped message processing - all messages with the same session ID are delivered to the same handler instance in FIFO order.
Azure Service Bus prerequisite
The queue or subscription must be created with RequiresSession = true. This is an immutable property set at entity creation time - you cannot enable sessions on an existing non-session entity. You must recreate the entity.
resource sessionQueue 'Microsoft.ServiceBus/namespaces/queues@2022-10-01-preview' = {
name: 'session-queue'
properties: {
requiresSession: true
}
}If you use the library's topology validation (ValidateTopologyOnStartup = true), it will detect a mismatch between a session handler and a non-session entity at startup.
Session-Enabled Entities
// Session-aware queue handler
messaging.MapQueue<TMessage>("session-queue")
.MapSessionHandler(async (
TMessage message,
SessionContext session,
CancellationToken ct) =>
{
Console.WriteLine($"Session: {session.SessionId}, State: {session.State}");
await session.SetStateAsync(newState, ct);
});
// Session-aware subscription handler
messaging.MapTopic<TMessage>("topic")
.MapSessionSubscription("subscription", handler);INFO
When sending messages to a session-enabled entity, you must set a session ID via SendOptions:
await sender.SendAsync(message, opts => opts.SessionId = "customer-123", ct);Azure Service Bus will reject messages without a session ID on session-enabled entities.
Session Context
SessionContext is injected into session handlers and provides SessionId, LockedUntil, and methods to get/set/clear session state (GetStateAsync, SetStateAsync, ClearStateAsync) and renew the session lock.
Session State Storage
Azure Service Bus provides up to 256 KB of binary session state per session. This state is stored server-side and survives handler restarts. Use it for lightweight checkpointing, not as a general-purpose store.
Sessions with Polymorphic Queues
Sessions work seamlessly with polymorphic queues. A single session-enabled queue can carry multiple message types - the session ID routes to the correct consumer, and the MessageType discriminator routes to the correct handler within that consumer.
// All command types share one session queue - gatewayId is the session key
var commands = messaging.MapPolymorphicQueue("gateway-commands", sessions: true);
commands.MapMessage<PingGatewayRequest>(handler);
commands.MapMessage<SetBatterySetpointRequest>(handler);
commands.MapMessage<RefreshConfigurationRequest>(handler);See Core Concepts - Polymorphic Queues and Patterns - Polymorphic Queue for details.
Competing Consumers and Session Filtering
When multiple instances of the same process connect to a session-enabled queue, the Azure Service Bus broker makes sessions available to whichever processor calls AcceptNextSessionAsync first. Without a session filter, every instance races to acquire any available session - meaning a message targeted at device-A can end up processed by the instance meant for device-B.
WARNING
A ServiceBusSessionProcessor with no SessionIds filter accepts any session from the queue. In a multi-instance deployment where each instance should own exactly its own sessions, this causes non-deterministic mis-routing that is consistent until the session lock expires, then shifts again.
Use WithAcceptedSessionId to restrict a session processor to sessions matching a specific ID:
// Each process knows its own ID and registers a filtered processor
var gatewayId = configuration.GetValue<string>("GatewayId")!;
messaging.MapPolymorphicQueue("gateway-commands", sessions: true)
.WithAcceptedSessionId(gatewayId) // only accept sessions for this instance
.MapMessage<PingGatewayRequest, PingHandler>()
.MapMessage<SetBatterySetpointRequest, SetpointHandler>();WithAcceptedSessionId can be called multiple times if a single process legitimately owns more than one session ID:
// A redundant pair that both handle the same gateway's sessions
commands
.WithAcceptedSessionId("gateway-primary")
.WithAcceptedSessionId("gateway-secondary");INFO
The sender side does not call WithAcceptedSessionId - that is a receiver-only concern. The sender only needs to set opts.SessionId to the target ID when sending.
Why this matters for polymorphic queues
The MessageType application property (set by the sender) routes to the correct handler within a consumer. Session filtering determines which consumer instance receives the message at all. Both layers are necessary for correct multi-instance targeting:
| Layer | Mechanism | Responsibility |
|---|---|---|
| Which consumer instance | SessionIds filter (WithAcceptedSessionId) | Routing to correct process |
| Which handler within the consumer | MessageType application property | Routing to correct handler type |