Skip to content

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

@daviburg

Description

@daviburg

Summary

There is no SDK primitive to go from a raw Connector Namespace trigger callback (an HTTP body / Stream / string) to a typed TriggerCallbackPayload<T>. Consumers must hand-write ~100+ lines of boilerplate (bounded body read, UTF-8 decode, JsonDocument parse, manual body-field type discrimination, base64 decode for binary triggers, and finally JsonSerializer.Deserialize<...>), and two subtle correctness traps are easy to hit.

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.

Background / Evidence

Connector Namespace delivers two distinct trigger callback shapes for the same connector:

Trigger Operation Wire body shape
Metadata (properties only) OnNewFilesV2 {"body":{"value":[{...BlobMetadata...}]}} (object)
Binary content OnNewFileV2 {"body":"<base64>"} (string)

Reproduced end-to-end (real files uploaded to OneDrive, real Connector Gateway callbacks observed in App Insights):

  • Binary .txt trigger → {"body":"<base64>"} → deserializing into OneDriveForBusinessOnNewFilesTriggerPayload throws JsonException: Expected StartObject or Null for TriggerCallbackBody<BlobMetadata>, got String. — a cryptic message that does not tell the developer they bound a binary-content trigger to a metadata payload type.
  • .json file via binary trigger → {"body":{...file content...}} → deserializes to a single all-null BlobMetadata (silent data loss).
  • A correctly-shaped metadata envelope whose item fields are camelCase also deserializes to all-null, because the deserialization path does not set PropertyNameCaseInsensitive.

Gaps

  1. No single entry point from a trigger callback body to the typed payload. Every consumer reimplements the same boilerplate (see the OneDriveTriggerCallback sample in Connectors-NET-Samples).
  2. Case sensitivity trap. Deserializing without PropertyNameCaseInsensitive = true yields silently all-null items when the wire uses camelCase. ConnectorJsonSerializer.Options already sets this, but there is nothing that steers trigger-payload consumers to it.
  3. Unactionable error for binary triggers. TriggerCallbackBodyConverter<T> throws "Expected StartObject or Null... got String." when a binary-content (string) body is bound to a metadata payload type. The message should tell the developer this is a binary-content trigger and how to read it.

Proposed change (this repo)

Add a framework-agnostic SDK helper (operates on string/Stream, no Functions Worker dependency) — e.g. ConnectorTriggerPayload:

  • ConnectorTriggerPayload.DefaultSerializerOptions — case-insensitive options reused for all trigger payloads (closes gap Adding Microsoft SECURITY.MD #2).
  • Read<TPayload>(string json) / ReadAsync<TPayload>(Stream body, CancellationToken) — read a metadata trigger callback into its typed TriggerCallbackPayload<T> subclass with the correct options (closes gap This repo is missing a license file #1).
  • TryReadBinaryContent(string json, out byte[] content) / ReadBinaryContentAsync(Stream, CancellationToken) — decode a binary-content trigger callback ({"body":"<base64>"}) into bytes (closes gap This repo is missing a license file #1 for the binary variant).
  • Improve TriggerCallbackBodyConverter<T> to throw an actionable error when it encounters a string body (closes gap This repo is missing important files #3): explain that the callback is a binary-content trigger (e.g. OnNewFileV2) and direct the caller to TryReadBinaryContent.

This reduces a OneDrive trigger handler from ~100 lines of plumbing to roughly:

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

foreach (var file in payload?.Body?.Value ?? [])
{
    // file.Id, file.Name, file.Path, file.Size ...
}

Out of scope (tracked separately)

  • 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, not here.
  • An LSP code action to generate the minimal handler body from [ConnectorTriggerMetadata].

Acceptance criteria

  • New helper with string + Stream overloads, case-insensitive by default.
  • Binary-content read helper for {"body":"<base64>"}.
  • TriggerCallbackBodyConverter<T> emits an actionable message for string bodies.
  • Unit tests covering: metadata (PascalCase + camelCase), binary base64, empty/null bodies, and the actionable error.
  • CHANGELOG entry.

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETPull requests that update .NET codeenhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions