Add ConnectorTriggerPayload helper for reading trigger callbacks (#190)#191
Add ConnectorTriggerPayload helper for reading trigger callbacks (#190)#191daviburg wants to merge 13 commits into
Conversation
…re#190) Adds a framework-agnostic SDK helper to go from a raw Connector Namespace trigger callback (string or Stream) to a typed TriggerCallbackPayload<T> or decoded binary file bytes, removing ~100 lines of per-function boilerplate. - Read<TPayload> / ReadAsync<TPayload>: metadata triggers (e.g. OnNewFilesV2), case-insensitive so camelCase wire fields bind instead of yielding all-null items. - TryReadBinaryContent / ReadBinaryContentAsync: binary-content triggers (e.g. OnNewFileV2) whose body is a base64 string. - TriggerCallbackBodyConverter<T> now throws an actionable error when a binary-content (string) body is bound to a metadata payload type, pointing to the binary helpers. Adds 11 unit tests; full suite green (862 tests). Updates CHANGELOG and docs/triggers.md. Fixes Azure#190
There was a problem hiding this comment.
Pull request overview
This PR adds an SDK-level helper to simplify reading Connector Namespace trigger callback bodies into either typed metadata payloads (TriggerCallbackPayload<T> subclasses) or decoded binary bytes, addressing common correctness traps (camelCase wire fields and binary trigger bodies bound to metadata payload types).
Changes:
- Introduces
ConnectorTriggerPayloadwithstring/Streamoverloads for metadata callbacks and helpers to decode binary-content callbacks. - Improves
TriggerCallbackBodyConverter<T>to throw a more actionableJsonExceptionwhen a binary-content (string) body is deserialized as metadata. - Updates trigger documentation and adds unit tests + changelog entries for the new helper and improved error.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/Azure.Connectors.Sdk/ConnectorTriggerPayload.cs |
Adds the new trigger-callback parsing/decoding helper API. |
src/Azure.Connectors.Sdk/TriggerCallbackPayload.cs |
Adds an actionable error message for binary-content bodies misbound as metadata. |
tests/Azure.Connectors.Sdk.Tests/ConnectorTriggerPayloadTests.cs |
Adds unit tests covering metadata reads (PascalCase + camelCase) and binary decoding/error behavior. |
docs/triggers.md |
Documents using ConnectorTriggerPayload for metadata and binary-content callbacks. |
CHANGELOG.md |
Adds Added/Changed entries describing the new helper and improved exception. |
- TryReadBinaryContent now returns false (not throws) on malformed JSON (Try* contract). - ReadBinaryContentAsync/ReadAsync read the caller-owned stream without closing it, decode UTF-8 explicitly, and enforce a generous overridable body-size limit (DefaultMaxBodySizeBytes = 100 MB). - Extracted wire property names 'body'/'value' to shared TriggerCallbackPropertyNames constants. - Tests: added ConfigureAwait(continueOnCapturedContext: false) on awaits; new tests for malformed JSON, size-limit enforcement, and stream-not-closed. - Docs/CHANGELOG updated to reflect the size limit and stream non-ownership. Co-authored-by: Dobby <dobby@microsoft.com>
Return the ArrayPool buffer with clearArray:true so residual trigger callback content (including base64 file bytes) cannot be observed by a subsequent renter in the same process. Co-authored-by: Dobby <dobby@microsoft.com>
|
There should be more documentation than just the changelog entry. The helper needs to be discoverable from the repo docs. |
…not-closed test Co-authored-by: Dobby <dobby@microsoft.com>
ReadBoundedAsync now requests at most (remaining allowance + 1) bytes per ReadAsync, so a single read cannot pull far past maxBodySizeBytes before the limit is enforced. Adds a MaxReadTrackingStream-based test asserting no over-read. Co-authored-by: Dobby <dobby@microsoft.com>
daviburg
left a comment
There was a problem hiding this comment.
There should be more documentation than just the changelog entry. The helper needs to be discoverable from the repo docs.
…pe per file - TryReadBinaryContent decodes directly via Convert.FromBase64String (single right-sized array) inside try/catch, halving peak memory for large binary bodies while keeping the Try* contract. - Moved MaxReadTrackingStream test helper to its own file (one top-level type per file). Co-authored-by: Dobby <dobby@microsoft.com>
…uard, dispose inner stream - README: added a Triggers component table and docs/triggers.md link so ConnectorTriggerPayload is discoverable from the repo docs (addresses reviewer's documentation request). - TryReadBinaryContent guards root ValueKind != Object before TryGetProperty, so non-object JSON (null/array/string/number) returns false instead of throwing. - MaxReadTrackingStream now disposes the wrapped inner stream. Co-authored-by: Dobby <dobby@microsoft.com>
|
[Dobby] Added repo-docs discoverability beyond the CHANGELOG:
Let me know if you'd like a dedicated docs page or a sample in Connectors-NET-Samples as well. |
Switch the ConnectorTriggerPayload remarks list from type=bullet to type=definition since its items use term/description pairs. Co-authored-by: Dobby <dobby@microsoft.com>
…hmetic ReadBoundedAsync no longer computes (maxBodySizeBytes - totalBytesRead) + 1 unconditionally; it only adds the +1 overflow probe in the branch where the remaining allowance is smaller than the chunk, so a very large maxBodySizeBytes (e.g. long.MaxValue) cannot overflow and produce a negative request size. Adds ReadAsync_VeryLargeLimit_DoesNotOverflow. Co-authored-by: Dobby <dobby@microsoft.com>
Extract the 81920 read-buffer literal to a named ReadChunkSizeBytes constant (80 KB, matching Stream.CopyTo's default chunk). Co-authored-by: Dobby <dobby@microsoft.com>
…8 bytes ReadBinaryContentAsync now parses JsonDocument directly from the UTF-8 byte buffer instead of decoding to a UTF-16 string first, avoiding a large intermediate allocation for big binary bodies. Extracted the shared base64-body decode into TryDecodeBinaryBody, used by both TryReadBinaryContent (string) and ReadBinaryContentAsync (bytes). Co-authored-by: Dobby <dobby@microsoft.com>
|
[Dobby] The PR description's testing section looks stale: it says 15 |
…re tests Address PR Azure#191 review feedback: 1. ReadAsync<TPayload> now uses a BoundedStream wrapper + JsonSerializer.DeserializeAsync for true streaming deserialization, avoiding the full-body buffer copy for metadata triggers. BoundedStream caps each read and enforces the size guard without buffering. 2. ReadBinaryContentAsync stream tests now cover malformed JSON, invalid base64, non-object root, and the extra-quoted base64 case — matching TryReadBinaryContent string coverage.
| this._maxBytes = maxBytes; | ||
| } | ||
|
|
||
| public override bool CanRead => true; |
| protected override void Dispose(bool disposing) | ||
| { | ||
| // Intentionally do not dispose the inner stream — the caller owns it. | ||
| } |
Description
Customers writing connector trigger handlers (Azure Functions or otherwise) had to hand-write ~100 lines of boilerplate to go from a raw Connector Namespace trigger callback to a typed payload — bounded body read, UTF-8 decode,
JsonDocumentparse, manualbody-field type discrimination (string vs object), base64 decode for binary triggers, then finallyJsonSerializer.Deserialize<…>. Two subtle correctness traps were easy to hit along the way (camelCase wire fields silently deserializing to all-null, and a cryptic exception when a binary-content trigger body was bound to a metadata payload type).This was surfaced by a real customer report on the Connectors for Functions channel (OneDrive for Business "When a file is created" trigger) and reproduced end-to-end against a live Connector Namespace — real files uploaded, real Connector Gateway callbacks observed.
Fixes #190.
Changes
ConnectorTriggerPayloadhelper (Azure.Connectors.Sdk, framework-agnostic —string+Stream, no Functions dependency):Read<TPayload>(string)/ReadAsync<TPayload>(Stream, long maxBodySizeBytes, CancellationToken)— read a metadata trigger callback (e.g. OneDriveOnNewFilesV2) into its typedTriggerCallbackPayload<T>subclass with case-insensitive matching, so camelCase wire fields bind instead of silently yielding all-nullitems.TryReadBinaryContent(string, out byte[])/ReadBinaryContentAsync(Stream, long maxBodySizeBytes, CancellationToken)— decode a binary-content trigger callback (e.g. OneDriveOnNewFileV2), whose body is{"body":"<base64>"}, into file bytes.TryReadBinaryContentreturnsfalse(rather than throwing) on malformed JSON, honouring theTry*contract.Streamoverloads read the caller-owned stream without closing it, decode UTF-8 explicitly, and enforce a generous, overridable body-size limit (DefaultMaxBodySizeBytes= 100 MB) so an oversized/hostile stream fails fast withInvalidOperationException.TriggerCallbackBodyConverter<T>— when a binary-content (string) body is deserialized into a metadata payload type, theJsonExceptionnow explains the cause and points to the binary-content helpers instead of failing with a generic token-mismatch message.body/valueproperty names extracted to an internalTriggerCallbackPropertyNamesclass used by the envelope attributes, the converter, and the readers (single source of truth).docs/triggers.mdhas aReading callbacks with ConnectorTriggerPayloadsection (metadata + binary examples, case-insensitive note, size limit, stream non-ownership);README.mdadds a Triggers component table and linksdocs/triggers.mdso the helper is discoverable from the repo docs.Added+Changedentries.A OneDrive trigger handler drops from ~100 lines of plumbing to:
Out of scope (tracked in #190)
[ConnectorTrigger]POCO binding in the Functions connector extension should consume these SDK primitives (readOperationName, discriminate binary vs metadata, use case-insensitive options). That lives in the Functions connector extension repo.[ConnectorTriggerMetadata].Testing
ConnectorTriggerPayloadTestscovering metadata (PascalCase + camelCase),Streamoverloads, binary base64 decode, empty/non-base64/object/malformed bodies,nullargument, the actionable binary-body error, body-size-limit enforcement, and the caller stream staying open.dotnet test) — 864 passed, 0 failed.Checklist
src/**/Generated/(see CONTRIBUTING.md)release_notes.mdupdated (if shipping a new version, clear previous version notes) — N/A, no version bump in this PReng/build/Version.props(if shipping a new version) — N/A