Skip to content

Add ConnectorTriggerPayload helper for reading trigger callbacks (#190)#191

Open
daviburg wants to merge 13 commits into
Azure:mainfrom
daviburg:feature/trigger-payload-reader
Open

Add ConnectorTriggerPayload helper for reading trigger callbacks (#190)#191
daviburg wants to merge 13 commits into
Azure:mainfrom
daviburg:feature/trigger-payload-reader

Conversation

@daviburg

@daviburg daviburg commented Jun 9, 2026

Copy link
Copy Markdown
Member

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, JsonDocument parse, manual body-field type discrimination (string vs object), base64 decode for binary triggers, then finally JsonSerializer.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

  • New ConnectorTriggerPayload helper (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. OneDrive OnNewFilesV2) into its typed TriggerCallbackPayload<T> subclass with case-insensitive matching, so camelCase wire fields bind instead of silently yielding all-null items.
    • TryReadBinaryContent(string, out byte[]) / ReadBinaryContentAsync(Stream, long maxBodySizeBytes, CancellationToken) — decode a binary-content trigger callback (e.g. OneDrive OnNewFileV2), whose body is {"body":"<base64>"}, into file bytes. TryReadBinaryContent returns false (rather than throwing) on malformed JSON, honouring the Try* contract.
    • The Stream overloads 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 with InvalidOperationException.
  • Actionable error in TriggerCallbackBodyConverter<T> — when a binary-content (string) body is deserialized into a metadata payload type, the JsonException now explains the cause and points to the binary-content helpers instead of failing with a generic token-mismatch message.
  • Shared wire-name constantsbody/value property names extracted to an internal TriggerCallbackPropertyNames class used by the envelope attributes, the converter, and the readers (single source of truth).
  • Docsdocs/triggers.md has a Reading callbacks with ConnectorTriggerPayload section (metadata + binary examples, case-insensitive note, size limit, stream non-ownership); README.md adds a Triggers component table and links docs/triggers.md so the helper is discoverable from the repo docs.
  • CHANGELOGAdded + Changed entries.

A OneDrive trigger handler drops from ~100 lines of plumbing to:

var payload = await ConnectorTriggerPayload
    .ReadAsync<OneDriveForBusinessOnNewFilesTriggerPayload>(request.Body, cancellationToken: cancellationToken)
    .ConfigureAwait(continueOnCapturedContext: false);

foreach (var file in payload?.Body?.Value ?? Array.Empty<BlobMetadata>())
{
    // file.Id, file.Name, file.Path, file.Size ...
}

Out of scope (tracked in #190)

  • The [ConnectorTrigger] POCO binding in the Functions connector extension should consume these SDK primitives (read OperationName, discriminate binary vs metadata, use case-insensitive options). That lives in the Functions connector extension repo.
  • An LSP code action to generate the minimal handler body from [ConnectorTriggerMetadata].

Testing

  • Unit tests added/updated — 15 tests in ConnectorTriggerPayloadTests covering metadata (PascalCase + camelCase), Stream overloads, binary base64 decode, empty/non-base64/object/malformed bodies, null argument, the actionable binary-body error, body-size-limit enforcement, and the caller stream staying open.
  • All existing tests pass (dotnet test) — 864 passed, 0 failed.
  • Manual testing — reproduced end-to-end against a live Connector Namespace (real OneDrive file uploads → real Connector Gateway callbacks); confirmed metadata and binary callbacks parse correctly.

Checklist

  • Code follows the project's coding conventions
  • No modifications to files under src/**/Generated/ (see CONTRIBUTING.md)
  • Documentation updated (if behavior changed)
  • CHANGELOG.md updated (if user-facing change)
  • release_notes.md updated (if shipping a new version, clear previous version notes) — N/A, no version bump in this PR
  • Version updated in eng/build/Version.props (if shipping a new version) — N/A

…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
@daviburg daviburg requested a review from a team as a code owner June 9, 2026 04:32
Copilot AI review requested due to automatic review settings June 9, 2026 04:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ConnectorTriggerPayload with string/Stream overloads for metadata callbacks and helpers to decode binary-content callbacks.
  • Improves TriggerCallbackBodyConverter<T> to throw a more actionable JsonException when 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.

Comment thread src/Azure.Connectors.Sdk/ConnectorTriggerPayload.cs Outdated
Comment thread src/Azure.Connectors.Sdk/ConnectorTriggerPayload.cs Outdated
Comment thread docs/triggers.md
Comment thread tests/Azure.Connectors.Sdk.Tests/ConnectorTriggerPayloadTests.cs
Comment thread tests/Azure.Connectors.Sdk.Tests/ConnectorTriggerPayloadTests.cs Outdated
Comment thread tests/Azure.Connectors.Sdk.Tests/ConnectorTriggerPayloadTests.cs Outdated
Comment thread src/Azure.Connectors.Sdk/ConnectorTriggerPayload.cs Outdated
- 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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread src/Azure.Connectors.Sdk/ConnectorTriggerPayload.cs
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>
@daviburg

daviburg commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

There should be more documentation than just the changelog entry. The helper needs to be discoverable from the repo docs.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread tests/Azure.Connectors.Sdk.Tests/ConnectorTriggerPayloadTests.cs
…not-closed test

Co-authored-by: Dobby <dobby@microsoft.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread src/Azure.Connectors.Sdk/ConnectorTriggerPayload.cs
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 daviburg left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be more documentation than just the changelog entry. The helper needs to be discoverable from the repo docs.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread src/Azure.Connectors.Sdk/ConnectorTriggerPayload.cs Outdated
Comment thread tests/Azure.Connectors.Sdk.Tests/ConnectorTriggerPayloadTests.cs Outdated
…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comment thread tests/Azure.Connectors.Sdk.Tests/MaxReadTrackingStream.cs
Comment thread src/Azure.Connectors.Sdk/ConnectorTriggerPayload.cs Outdated
…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>
@daviburg

Copy link
Copy Markdown
Member Author

[Dobby] Added repo-docs discoverability beyond the CHANGELOG:

  • README.md: new Triggers row under SDK Components describing TriggerCallbackPayload<T> and ConnectorTriggerPayload (Read/ReadAsync + TryReadBinaryContent/ReadBinaryContentAsync), plus a link to docs/triggers.md.
  • README.md Documentation list now links docs/triggers.md.
  • docs/triggers.md already has a Reading callbacks with ConnectorTriggerPayload section with metadata + binary examples, the case-insensitive note, the bounded-read/size-limit note, and stream non-ownership.

Let me know if you'd like a dedicated docs page or a sample in Connectors-NET-Samples as well.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comment thread src/Azure.Connectors.Sdk/ConnectorTriggerPayload.cs Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comment thread src/Azure.Connectors.Sdk/ConnectorTriggerPayload.cs
…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comment thread src/Azure.Connectors.Sdk/ConnectorTriggerPayload.cs Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comment thread src/Azure.Connectors.Sdk/ConnectorTriggerPayload.cs Outdated
…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@daviburg daviburg self-assigned this Jun 10, 2026
Comment thread src/Azure.Connectors.Sdk/ConnectorTriggerPayload.cs Outdated
Comment thread tests/Azure.Connectors.Sdk.Tests/ConnectorTriggerPayloadTests.cs
@daviburg

Copy link
Copy Markdown
Member Author

[Dobby] The PR description's testing section looks stale: it says 15 ConnectorTriggerPayloadTests and 864 total tests, but the current head has 19 targeted tests and my local dotnet test Azure.Connectors.Sdk.sln run passed 870 tests. Please update the PR description so reviewers/release readers have accurate validation evidence.

daviburg and others added 2 commits June 11, 2026 13:46
…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.
Copilot AI review requested due to automatic review settings June 11, 2026 23:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

this._maxBytes = maxBytes;
}

public override bool CanRead => true;
Comment on lines +427 to +430
protected override void Dispose(bool disposing)
{
// Intentionally do not dispose the inner stream — the caller owns it.
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add SDK helper to read trigger callback payloads (Stream/string → TriggerCallbackPayload<T>) and fix binary-trigger/case-insensitive traps

2 participants