Repository navigation
3.1: in-memory test queues, CloudEvent processor, OTel trace propagation, shared credential, safe options copy - #50
Merged
Merged
Conversation
services.AddInMemoryQueues() makes every registered client (default and named, injected, from the factory, and each processor's queue) an in-memory queue that follows the service's rules: visibility timeouts and dequeue counts on receive, new pop receipts on receive and update, 404 MessageNotFound for an outdated receipt, SendOptions visibility delay and time to live, dead-lettering to the client's poison queue name, and the 1-32 receive limit. Time comes from the registered TimeProvider, so a fake clock drives polling, retries and expiry. No connection details are needed or checked, including by ValidateOnStart; a queue name is still required and poison queue names are still validated. Clients naming the same queue share it, which is how a test reads the poison queue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Event Grid delivering to a storage queue with the CloudEvents schema is the package's original use case, but a handler had to unwrap the envelope itself. ICloudEventHandler<TData> receives the event's data read as TData with the client's serializer, plus the CloudEvent for its metadata. It runs on the existing processor through an internal adapter, so settlement, retries, the poison queue, lease renewal and shutdown are unchanged. Data that cannot be read as TData is treated as an unreadable body: dead-lettered with the event id and type in the reason, or passed to the handler as null with message.Error set when DeadLetterOnDeserializationFailure is off. TData = BinaryData passes the raw bytes through (data_base64 / non-JSON data). QueueProcessor gains two optional constructor parameters (a handler factory and a display name) so the adapter can be built per client and logs name the application's handler rather than the adapter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pt-in trace propagation (#46) - Spans carry the OpenTelemetry messaging attributes (messaging.system "azure.storage.queue", destination, operation type/name; message id, batch count, server address/port and error.type with an error status on failure) alongside the existing tags. Span names unchanged; metrics keep exactly their 3.0 names and tags, so stored series stay continuous. - The meter is created through IMeterFactory when the container has one, so MetricCollector can observe a host's metrics; otherwise a library-owned meter. Hosted processors, including CloudEvent processors, share the same instance. - QueueClientSettings.PropagateTraceContext (off by default) makes SendAsync wrap the body in a JSON envelope carrying W3C traceparent/tracestate. Every client unwraps the strict envelope on receive and peek regardless of the setting, and still reads plain messages. The processing span links to the sender's context and uses it as parent when there is no ambient span. Dead-lettering copies the envelope so poison messages replay with their context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QueueClientSettingsBuilder.UseTokenCredential(TokenCredential) gives every client with an EndpointUri and no TokenCredential of its own a shared credential, so appsettings only needs the endpoint. Applied by a single IPostConfigureOptions that reads the credential from the registry, so it covers clients registered before and after the call, default, named and configuration-bound, runs before ValidateOnStart and first-use validation, and a second call replaces the credential. A client's own credential wins; connection-string clients are untouched. Poison queues inherit it through ForSiblingQueue. No new dependencies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With MessageEncoding = Base64 the library added its MessageDecodingFailed handler to the caller's own QueueClientOptions while building its SDK client. A QueueClient the app built from the same options at that moment captured the handler, which throws outside a library receive. The library now copies the options and attaches its handler only to the copy, for the source client and the poison-queue client. The copy carries every public setting plus what has no public getter: policies added with AddPolicy (at their positions), the caller's own decoding handlers, and Transport only when the caller set one. Those three are read by reflection in one place. If an Azure SDK update removes them, the library logs once (event 45) and falls back to the 3.0 temporary add/remove instead of failing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ata, in-memory parity - TraceContextEnvelope.TryUnwrap never throws: bad body64 (TryGetBytesFromBase64) and invalid UTF-8 strings make a lookalike a plain message instead of failing the whole receive. - CloudEvent data is read by the processor straight after deserialization (an internal after-deserialize step), so unreadable data takes the processor's own path: its options snapshot, outcome dead_lettered and the failure metric. The adapter only hands data over. - queue_message_size_bytes records the serializer's output, before any envelope, as in 3.0. - Wrap skips the whole-payload JSON check for the library's own serializer. - In-memory queues wrap on send with PropagateTraceContext and unwrap on receive/peek, like StorageQueue; they apply the same endpoint/queue-name rules as production. - StorageQueue and InMemoryStorageQueue share one helper (StorageQueueCommon) for limits, envelope encode/decode and deserialization; InMemoryQueue reuses the shared constants. - Tests share one Signal helper; README and XML docs state the envelope's size overhead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…py failure - QueueClientOptionsCopy refuses options whose exact type isn't QueueClientOptions or SpecializedQueueClientOptions: a subclass can hold settings the copy can't see (a raised application id limit broke client construction) and names its own assembly in User-Agent and telemetry. - SdkQueueClientFactory falls back to the 3.0 temporary handler when the copy or building the client from it fails. A genuine configuration error throws again from the fallback, so it still surfaces. Warning 45 is logged once, after the fallback succeeds. - Transport is always copied; the IsCustomTransportSet reflection is gone (the storage SDK never consults it). - An internal constructor takes the copy function so a test can force the fallback; the drift guard now checks every stored field of the option types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jasonshave
marked this pull request as ready for review
October 16, 2026 19:56
jasonshave
enabled auto-merge (squash)
October 16, 2026 19:56
Closed
41 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Folds every open issue (#45–#49) into one PR for 3.1. Everything is additive: no breaking change to 3.0's public API, defaults, telemetry names or wire format.
What's new for users
services.AddInMemoryQueues()swaps every registered client, including processors, for an in-memory queue. A handler can then be tested end to end without Azurite: send, process, then complete, retry or dead-letter, all on a fake clock. No connection string is needed in tests, but the queue-name and poison-queue rules still apply. The README lists where the fake differs from the real service.AddCloudEventProcessor<TData, THandler>()withICloudEventHandler<TData>gives the handler the event's typeddataplus its metadata. Unreadable data takes the processor's normal poison-queue path. The README covers Event Grid's Base64 setting. The Event Grid schema (EventGridEvent) is out of scope.messaging.system=azure.storage.queue,messaging.destination.name,messaging.operation.*,messaging.message.id). Every 3.0 span name, tag and metric is unchanged; metrics keep exactly their 3.0 tags so stored history isn't split.IMeterFactorywhen one exists, so metrics can be tested withMetricCollector.QueueClientSettings.PropagateTraceContextcarries W3C trace context inside a small JSON wrapper, so a single trace runs from sender to handler. Every 3.1 receiver unwraps it whether or not the setting is on; only 3.0 or non-library consumers need care. A message moved back from the poison queue keeps its trace context. The README documents the wire format and the size overhead.queues.UseTokenCredential(new DefaultAzureCredential())applies one credential to every client that has an endpoint and no login of its own, so appsettings only needs endpoints. Decided against building on Microsoft.Extensions.Azure, which would add Azure.Identity to every consumer.QueueClientOptions, instead of briefly adding its handler to the caller's object. Two Azure.Core internals are read by reflection: the policy list and the caller's decoding handlers. If either ever changes, or the options are a subclass, the library falls back to the 3.0 behaviour and logs warning 45 once, so it never fails startup. A guard test flags any Azure SDK change to these types.Review
/code-review high, plus focused reviews of Library briefly attaches its decoding handler to caller-owned QueueClientOptions #49 and of the fix commit. Fixed in this PR:Verification
dotnet build: 0 warnings.Closes #45, closes #46, closes #47, closes #48, closes #49.
🤖 Generated with Claude Code