(body, ConnectorFunctions.JsonOptions);
- }
- catch (JsonException ex)
- {
- this._logger.LogError(ex, "Invalid JSON in request body: '{Message}'.", ex.Message);
-
- var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
- await badRequest
- .WriteAsJsonAsync(new { error = "Request body must contain valid JSON." })
- .ConfigureAwait(continueOnCapturedContext: false);
-
- return badRequest;
- }
-
- if (input == null || string.IsNullOrEmpty(input.TeamId) ||
- string.IsNullOrEmpty(input.ChannelId) || string.IsNullOrEmpty(input.Message))
- {
- var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
- await badRequest
- .WriteAsJsonAsync(new { error = "Fields 'teamId', 'channelId', and 'message' are required." })
- .ConfigureAwait(continueOnCapturedContext: false);
- return badRequest;
- }
-
- // NOTE: PostMessageToConversationAsync uses DynamicPostMessageRequest (dynamic schema).
- // The actual message body properties are determined at runtime by the connector's schema
- // discovery endpoint. With [JsonExtensionData] on AdditionalProperties, arbitrary properties
- // are now serialized correctly. Populate the dictionary with the expected message fields.
- var messageRequest = new Azure.Connectors.Sdk.Teams.Models.DynamicPostMessageRequest();
- messageRequest.AdditionalProperties["recipient"] = JsonSerializer.SerializeToElement(
- new
- {
- groupId = input.TeamId,
- channelId = input.ChannelId,
- });
- messageRequest.AdditionalProperties["messageBody"] = JsonSerializer.SerializeToElement(
- $"{WebUtility.HtmlEncode(input.Message)}
");
-
- var result = await this._teamsClient
- .PostMessageToConversationAsync(
- postAs: ConnectorFunctions.TeamsDefaultPoster,
- postIn: ConnectorFunctions.TeamsDefaultLocation,
- input: messageRequest,
- cancellationToken: cancellationToken)
- .ConfigureAwait(continueOnCapturedContext: false);
-
- var response = request.CreateResponse(HttpStatusCode.OK);
- await response
- .WriteAsJsonAsync(new
- {
- success = true,
- message = "Message posted to Teams channel via generated TeamsClient from SDK.",
- messageId = result?.MessageID,
- messageLink = result?.MessageLink,
- timestamp = DateTime.UtcNow
- })
- .ConfigureAwait(continueOnCapturedContext: false);
-
- return response;
- }
- catch (ConnectorException ex)
- {
- this._logger.LogError(ex, "Teams connector error: '{StatusCode}'.", ex.Status);
-
- var errorResponse = request.CreateResponse(HttpStatusCode.BadGateway);
- await errorResponse
- .WriteAsJsonAsync(new
- {
- success = false,
- error = ex.Message,
- statusCode = ex.Status,
- details = ex.ResponseBody
- })
- .ConfigureAwait(continueOnCapturedContext: false);
-
- return errorResponse;
- }
- catch (Exception ex) when (!ex.IsFatal())
- {
- this._logger.LogError(ex, "Error in PostTeamsMessage.");
-
- var errorResponse = request.CreateResponse(HttpStatusCode.InternalServerError);
- await errorResponse
- .WriteAsJsonAsync(new
- {
- success = false,
- error = ex.Message
- })
- .ConfigureAwait(continueOnCapturedContext: false);
-
- return errorResponse;
- }
- }
-
- ///
- /// Creates a calendar event using the generated .
- ///
- /// The HTTP request containing calendar event details.
- /// The cancellation token.
- [Function("CreateCalendarEvent")]
- public async Task CreateCalendarEventAsync(
- [HttpTrigger(AuthorizationLevel.Function, "post", Route = "calendar/event")] HttpRequestData request,
- CancellationToken cancellationToken)
- {
- this._logger.LogInformation("CreateCalendarEvent: Using generated Office365Client from SDK.");
-
- try
- {
- using var reader = new StreamReader(request.Body);
- var body = await reader
- .ReadToEndAsync(cancellationToken)
- .ConfigureAwait(continueOnCapturedContext: false);
-
- CreateCalendarEventRequest? input;
- try
- {
- input = JsonSerializer.Deserialize(body, ConnectorFunctions.JsonOptions);
- }
- catch (JsonException ex)
- {
- this._logger.LogError(ex, "Invalid JSON in request body: '{Message}'.", ex.Message);
-
- var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
- await badRequest
- .WriteAsJsonAsync(new { error = "Request body must contain valid JSON." })
- .ConfigureAwait(continueOnCapturedContext: false);
-
- return badRequest;
- }
-
- if (input == null || string.IsNullOrEmpty(input.Subject) ||
- string.IsNullOrEmpty(input.StartTime) || string.IsNullOrEmpty(input.EndTime))
- {
- var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
- await badRequest
- .WriteAsJsonAsync(new { error = "Fields 'subject', 'startTime', and 'endTime' are required." })
- .ConfigureAwait(continueOnCapturedContext: false);
- return badRequest;
- }
-
- var calendarEvent = new GraphCalendarEventClient
- {
- Subject = input.Subject,
- Body = input.Body ?? string.Empty,
- StartTime = input.StartTime,
- EndTime = input.EndTime,
- TimeZone = input.TimeZone ?? "UTC",
- RequiredAttendees = input.RequiredAttendees
- };
-
- // NOTE: "Calendar" is the default calendar ID for the signed-in user.
- var calendarId = input.CalendarId ?? "Calendar";
-
- var result = await this._office365Client
- .CalendarPostItemAsync(calendarId, calendarEvent, cancellationToken)
- .ConfigureAwait(continueOnCapturedContext: false);
-
- var response = request.CreateResponse(HttpStatusCode.OK);
- await response
- .WriteAsJsonAsync(new
- {
- success = true,
- message = "Calendar event created via generated Office365Client from SDK.",
- eventId = result?.ICalUId,
- subject = result?.Subject,
- start = result?.StartTime,
- end = result?.EndTime,
- timestamp = DateTime.UtcNow
- })
- .ConfigureAwait(continueOnCapturedContext: false);
-
- return response;
- }
- catch (ConnectorException ex)
- {
- this._logger.LogError(ex, "Connector error: '{StatusCode}'.", ex.Status);
-
- var errorResponse = request.CreateResponse(HttpStatusCode.BadGateway);
- await errorResponse
- .WriteAsJsonAsync(new
- {
- success = false,
- error = ex.Message,
- statusCode = ex.Status,
- details = ex.ResponseBody
- })
- .ConfigureAwait(continueOnCapturedContext: false);
-
- return errorResponse;
- }
- catch (Exception ex) when (!ex.IsFatal())
- {
- this._logger.LogError(ex, "Error in CreateCalendarEvent.");
-
- var errorResponse = request.CreateResponse(HttpStatusCode.InternalServerError);
- await errorResponse
- .WriteAsJsonAsync(new
- {
- success = false,
- error = ex.Message
- })
- .ConfigureAwait(continueOnCapturedContext: false);
-
- return errorResponse;
- }
- }
-}
diff --git a/DirectConnector/Office365Functions.cs b/DirectConnector/Office365Functions.cs
new file mode 100644
index 0000000..b7c9c69
--- /dev/null
+++ b/DirectConnector/Office365Functions.cs
@@ -0,0 +1,591 @@
+//------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//------------------------------------------------------------
+
+using System.Net;
+using System.Text.Json;
+using Azure.Connectors.Sdk;
+using Azure.Connectors.Sdk.Office365;
+using Azure.Connectors.Sdk.Office365.Models;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.Extensions.Logging;
+
+namespace DirectConnector;
+
+///
+/// Azure Functions that use the generated from the Azure Connectors SDK.
+///
+///
+/// Demonstrates email operations, calendar events, trigger callbacks,
+/// and CancellationToken propagation from the Functions host.
+///
+public class Office365Functions
+{
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true
+ };
+
+ ///
+ /// Maximum accepted request body size for trigger callbacks (1 MB).
+ /// Requests exceeding this size are rejected with 200 OK to avoid Connector Gateway retries.
+ ///
+ private const int MaxTriggerCallbackBodySize = 1 * 1024 * 1024;
+
+ private readonly ILogger _logger;
+ private readonly Office365Client _office365Client;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger instance.
+ /// The DI-injected Office365 client (disposed by the host).
+ public Office365Functions(
+ ILogger logger,
+ Office365Client office365Client)
+ {
+ this._logger = logger;
+ this._office365Client = office365Client;
+ }
+
+ ///
+ /// Sends an email using the generated .
+ ///
+ /// The HTTP request containing email details.
+ /// The cancellation token.
+ [Function("SendEmail")]
+ public async Task SendEmailAsync(
+ [HttpTrigger(AuthorizationLevel.Function, "post", Route = "email")] HttpRequestData request,
+ CancellationToken cancellationToken)
+ {
+ this._logger.LogInformation("SendEmail: Using generated Office365Client from SDK.");
+
+ try
+ {
+ using var reader = new StreamReader(request.Body);
+ var body = await reader
+ .ReadToEndAsync(cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ SendEmailRequest? input;
+ try
+ {
+ input = JsonSerializer.Deserialize(body, Office365Functions.JsonOptions);
+ }
+ catch (JsonException ex)
+ {
+ this._logger.LogError(ex, "Invalid JSON in request body: '{Message}'.", ex.Message);
+
+ var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
+ await badRequest
+ .WriteAsJsonAsync(new { error = "Request body must contain valid JSON." })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return badRequest;
+ }
+
+ if (input == null || string.IsNullOrEmpty(input.To))
+ {
+ var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
+ await badRequest
+ .WriteAsJsonAsync(new { error = "Invalid request body - 'to' is required." })
+ .ConfigureAwait(continueOnCapturedContext: false);
+ return badRequest;
+ }
+
+ var emailMessage = new SendEmailInput
+ {
+ To = input.To,
+ Subject = input.Subject ?? "No Subject",
+ Body = input.Body ?? string.Empty
+ };
+
+ await this._office365Client
+ .SendEmailAsync(emailMessage, cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ var response = request.CreateResponse(HttpStatusCode.OK);
+ await response
+ .WriteAsJsonAsync(new
+ {
+ success = true,
+ message = "Email sent via generated Office365Client from SDK.",
+ to = input.To,
+ subject = emailMessage.Subject,
+ timestamp = DateTime.UtcNow
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return response;
+ }
+ catch (ConnectorException ex)
+ {
+ this._logger.LogError(ex, "Connector error: '{StatusCode}'.", ex.Status);
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.BadGateway);
+ await errorResponse
+ .WriteAsJsonAsync(new
+ {
+ success = false,
+ error = ex.Message,
+ statusCode = ex.Status,
+ details = ex.ResponseBody
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ catch (Exception ex) when (!ex.IsFatal())
+ {
+ this._logger.LogError(ex, "Error in SendEmail.");
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.InternalServerError);
+ await errorResponse
+ .WriteAsJsonAsync(new
+ {
+ success = false,
+ error = ex.Message
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ }
+
+ ///
+ /// Gets Outlook categories using the generated .
+ ///
+ /// The HTTP request.
+ /// The cancellation token.
+ [Function("GetCategories")]
+ public async Task GetCategoriesAsync(
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "categories")] HttpRequestData request,
+ CancellationToken cancellationToken)
+ {
+ this._logger.LogInformation("GetCategories: Using generated Office365Client from SDK.");
+
+ try
+ {
+ var categories = await this._office365Client
+ .GetOutlookCategoryNamesAsync(cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ var response = request.CreateResponse(HttpStatusCode.OK);
+ await response
+ .WriteAsJsonAsync(new
+ {
+ success = true,
+ count = categories?.Count ?? 0,
+ categories = categories
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return response;
+ }
+ catch (ConnectorException ex)
+ {
+ this._logger.LogError(ex, "Connector error: '{StatusCode}'.", ex.Status);
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.BadGateway);
+ await errorResponse
+ .WriteAsJsonAsync(new
+ {
+ success = false,
+ error = ex.Message,
+ statusCode = ex.Status
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ catch (Exception ex) when (!ex.IsFatal())
+ {
+ this._logger.LogError(ex, "Error in GetCategories.");
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.InternalServerError);
+ await errorResponse
+ .WriteAsJsonAsync(new
+ {
+ success = false,
+ error = ex.Message
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ }
+
+ ///
+ /// Exports an email message as raw RFC822 (.eml) bytes.
+ ///
+ ///
+ /// Exercises the byte[] response path in .
+ /// This is the Office365 counterpart to for SharePoint —
+ /// both prove that CallConnectorAsync<byte[]> uses ReadAsByteArrayAsync
+ /// instead of JSON deserialization.
+ ///
+ /// The HTTP request containing the message ID.
+ /// The cancellation token.
+ [Function("ExportEmail")]
+ public async Task ExportEmailAsync(
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "email/export")] HttpRequestData request,
+ CancellationToken cancellationToken)
+ {
+ this._logger.LogInformation("ExportEmail: Using generated Office365Client byte[] response path.");
+
+ var messageId = request.Query["messageId"];
+ if (string.IsNullOrEmpty(messageId))
+ {
+ var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
+ await badRequest
+ .WriteAsJsonAsync(new { error = "Query parameter 'messageId' is required." })
+ .ConfigureAwait(continueOnCapturedContext: false);
+ return badRequest;
+ }
+
+ try
+ {
+ // NOTE: This exercises the same byte[] return path as SharePoint's
+ // GetFileContentByPathAsync, proving the pattern works across connectors.
+ var emailBytes = await this._office365Client
+ .ExportEmailAsync(messageId, cancellationToken: cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ var response = request.CreateResponse(HttpStatusCode.OK);
+ response.Headers.Add("Content-Type", "message/rfc822");
+ response.Headers.Add("Content-Disposition", "attachment; filename=\"exported-email.eml\"");
+ await response.Body
+ .WriteAsync(emailBytes, cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ this._logger.LogInformation("Exported email '{MessageId}': '{ByteCount}' bytes.", messageId, emailBytes.Length);
+
+ return response;
+ }
+ catch (ConnectorException ex)
+ {
+ this._logger.LogError(ex, "Connector error: '{StatusCode}'.", ex.Status);
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.BadGateway);
+ await errorResponse
+ .WriteAsJsonAsync(new
+ {
+ success = false,
+ error = ex.Message,
+ statusCode = ex.Status
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ catch (Exception ex) when (!ex.IsFatal())
+ {
+ this._logger.LogError(ex, "Error in ExportEmail.");
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.InternalServerError);
+ await errorResponse
+ .WriteAsJsonAsync(new { success = false, error = ex.Message })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ }
+
+ ///
+ /// Receives Connector Gateway trigger callback with raw triggerBody() JSON.
+ ///
+ ///
+ /// The Connector Gateway provisions a hidden Consumption Logic App that polls for trigger events
+ /// (e.g., OnNewEmail). When fired, it POSTs @triggerBody() to this callback URL
+ /// with a function key via ?code= query parameter.
+ ///
+ /// Unauthenticated requests (missing or invalid function key) are rejected with HTTP 401
+ /// by the Functions runtime before this handler runs.
+ ///
+ /// For authenticated invocations, all exceptions return 200 to prevent Connector Gateway retries.
+ ///
+ /// The HTTP request containing the trigger payload.
+ /// The cancellation token.
+ [Function("TriggerCallback")]
+ [ConnectorTriggerMetadata(
+ ConnectorName = ConnectorNames.Office365,
+ OperationName = Office365TriggerOperations.OnNewEmail,
+ Connection = "Connectors:Office365")]
+ public async Task TriggerCallbackAsync(
+ // NOTE: Function-level key auth. Connector Gateway includes the key via ?code= query parameter
+ // in the callbackUrl configured in the TriggerConfig. Preview uses function key; MI before GA.
+ [HttpTrigger(AuthorizationLevel.Function, "post", Route = "triggerCallback")] HttpRequestData request,
+ CancellationToken cancellationToken)
+ {
+ this._logger.LogInformation("TriggerCallback: Received Connector Gateway trigger callback.");
+
+ try
+ {
+ // NOTE: Check Content-Length header first (works for all streams),
+ // then fall back to Body.Length for seekable streams.
+ long contentLength = -1;
+ if (request.Headers.TryGetValues("Content-Length", out var contentLengthHeaderValues) &&
+ long.TryParse(contentLengthHeaderValues.FirstOrDefault(), out var parsedLength))
+ {
+ contentLength = parsedLength;
+ }
+
+ if (contentLength > Office365Functions.MaxTriggerCallbackBodySize ||
+ (contentLength < 0 && request.Body.CanSeek && request.Body.Length > Office365Functions.MaxTriggerCallbackBodySize))
+ {
+ this._logger.LogWarning("TriggerCallback: Payload too large. Rejecting.");
+
+ var rejectResponse = request.CreateResponse(HttpStatusCode.OK);
+ await rejectResponse
+ .WriteAsJsonAsync(new
+ {
+ success = true,
+ message = "Trigger callback received (payload too large, discarded).",
+ receivedAt = DateTime.UtcNow
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return rejectResponse;
+ }
+
+ // NOTE: Read at most (limit + 1) chars so oversized non-seekable
+ // payloads without Content-Length can still be detected reliably.
+ using var reader = new StreamReader(request.Body);
+ var buffer = new char[Office365Functions.MaxTriggerCallbackBodySize + 1];
+ var charsRead = await reader
+ .ReadBlockAsync(buffer.AsMemory(0, buffer.Length), cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ if (charsRead > Office365Functions.MaxTriggerCallbackBodySize)
+ {
+ this._logger.LogWarning("TriggerCallback: Payload too large. Rejecting.");
+
+ var rejectResponse = request.CreateResponse(HttpStatusCode.OK);
+ await rejectResponse
+ .WriteAsJsonAsync(new
+ {
+ success = true,
+ message = "Trigger callback received (payload too large, discarded).",
+ receivedAt = DateTime.UtcNow
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return rejectResponse;
+ }
+
+ var body = new string(buffer, 0, charsRead);
+
+ // NOTE: Use SDK's per-trigger convenience type for typed deserialization.
+ // Office365OnNewEmailTriggerPayload is a subclass of TriggerCallbackPayload
+ // that provides discoverability — the developer no longer needs to know the inner type.
+ var payload = JsonSerializer.Deserialize(
+ body,
+ Office365Functions.JsonOptions);
+
+ var emails = payload?.Body?.Value;
+ var emailCount = emails?.Count ?? 0;
+
+ this._logger.LogInformation(
+ "TriggerCallback: Deserialized '{EmailCount}' email(s) using Office365OnNewEmailTriggerPayload.",
+ emailCount);
+
+ // NOTE: Cap per-email logging to avoid unbounded log volume on batch triggers.
+ // Log only message IDs (not PII like Subject/From) to reduce accidental exposure.
+ if (emails != null)
+ {
+ foreach (var email in emails.Take(5))
+ {
+ this._logger.LogDebug(
+ "TriggerCallback email: Id='{Id}', ReceivedTime='{ReceivedTime}', HasAttachments='{HasAttachments}', Importance='{Importance}'.",
+ email.MessageId,
+ email.ReceivedTime,
+ email.HasAttachment,
+ email.Importance);
+ }
+
+ if (emailCount > 5)
+ {
+ this._logger.LogDebug("TriggerCallback: '{RemainingCount}' additional email(s) not logged.", emailCount - 5);
+ }
+ }
+
+ var response = request.CreateResponse(HttpStatusCode.OK);
+ await response
+ .WriteAsJsonAsync(new
+ {
+ success = true,
+ message = "Trigger callback received (typed deserialization via SDK).",
+ receivedAt = DateTime.UtcNow,
+ emailCount
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return response;
+ }
+ catch (JsonException ex)
+ {
+ this._logger.LogError(ex, "TriggerCallback: Invalid JSON payload: '{Message}'.", ex.Message);
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.OK);
+ await errorResponse
+ .WriteAsJsonAsync(new
+ {
+ success = true,
+ message = "Trigger callback received (non-JSON payload).",
+ receivedAt = DateTime.UtcNow
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ catch (Exception ex) when (!ex.IsFatal())
+ {
+ this._logger.LogError(ex, "Error in TriggerCallback.");
+
+ // NOTE: Return 200 even on unexpected errors — Connector Gateway treats any 2xx
+ // as "delivered" and we don't want transient failures to cause retries.
+ var errorResponse = request.CreateResponse(HttpStatusCode.OK);
+ await errorResponse
+ .WriteAsJsonAsync(new
+ {
+ success = true,
+ message = "Trigger callback received (processing error).",
+ receivedAt = DateTime.UtcNow
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ }
+
+ ///
+ /// Creates a calendar event using the generated .
+ ///
+ /// The HTTP request containing calendar event details.
+ /// The cancellation token.
+ [Function("CreateCalendarEvent")]
+ public async Task CreateCalendarEventAsync(
+ [HttpTrigger(AuthorizationLevel.Function, "post", Route = "calendar/event")] HttpRequestData request,
+ CancellationToken cancellationToken)
+ {
+ this._logger.LogInformation("CreateCalendarEvent: Using generated Office365Client from SDK.");
+
+ try
+ {
+ using var reader = new StreamReader(request.Body);
+ var body = await reader
+ .ReadToEndAsync(cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ CreateCalendarEventRequest? input;
+ try
+ {
+ input = JsonSerializer.Deserialize(body, Office365Functions.JsonOptions);
+ }
+ catch (JsonException ex)
+ {
+ this._logger.LogError(ex, "Invalid JSON in request body: '{Message}'.", ex.Message);
+
+ var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
+ await badRequest
+ .WriteAsJsonAsync(new { error = "Request body must contain valid JSON." })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return badRequest;
+ }
+
+ if (input == null || string.IsNullOrEmpty(input.Subject) ||
+ string.IsNullOrEmpty(input.StartTime) || string.IsNullOrEmpty(input.EndTime))
+ {
+ var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
+ await badRequest
+ .WriteAsJsonAsync(new { error = "Fields 'subject', 'startTime', and 'endTime' are required." })
+ .ConfigureAwait(continueOnCapturedContext: false);
+ return badRequest;
+ }
+
+ var calendarEvent = new GraphCalendarEventClient
+ {
+ Subject = input.Subject,
+ Body = input.Body ?? string.Empty,
+ StartTime = input.StartTime,
+ EndTime = input.EndTime,
+ TimeZone = input.TimeZone ?? "UTC",
+ RequiredAttendees = input.RequiredAttendees
+ };
+
+ // NOTE: "Calendar" is the default calendar ID for the signed-in user.
+ var calendarId = input.CalendarId ?? "Calendar";
+
+ var result = await this._office365Client
+ .CalendarPostItemAsync(calendarId, calendarEvent, cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ var response = request.CreateResponse(HttpStatusCode.OK);
+ await response
+ .WriteAsJsonAsync(new
+ {
+ success = true,
+ message = "Calendar event created via generated Office365Client from SDK.",
+ eventId = result?.ICalUId,
+ subject = result?.Subject,
+ start = result?.StartTime,
+ end = result?.EndTime,
+ timestamp = DateTime.UtcNow
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return response;
+ }
+ catch (ConnectorException ex)
+ {
+ this._logger.LogError(ex, "Connector error: '{StatusCode}'.", ex.Status);
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.BadGateway);
+ await errorResponse
+ .WriteAsJsonAsync(new
+ {
+ success = false,
+ error = ex.Message,
+ statusCode = ex.Status,
+ details = ex.ResponseBody
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ catch (Exception ex) when (!ex.IsFatal())
+ {
+ this._logger.LogError(ex, "Error in CreateCalendarEvent.");
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.InternalServerError);
+ await errorResponse
+ .WriteAsJsonAsync(new
+ {
+ success = false,
+ error = ex.Message
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ }
+
+ ///
+ /// Request model for sending email.
+ ///
+ private record SendEmailRequest(string? To, string? Subject, string? Body);
+
+ ///
+ /// Request model for creating a calendar event.
+ ///
+ private record CreateCalendarEventRequest(
+ string? CalendarId,
+ string? Subject,
+ string? Body,
+ string? StartTime,
+ string? EndTime,
+ string? TimeZone,
+ string? RequiredAttendees);
+}
diff --git a/DirectConnector/SharePointFunctions.cs b/DirectConnector/SharePointFunctions.cs
new file mode 100644
index 0000000..fb59313
--- /dev/null
+++ b/DirectConnector/SharePointFunctions.cs
@@ -0,0 +1,434 @@
+//------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//------------------------------------------------------------
+
+using System.Net;
+using System.Text;
+using System.Text.Json;
+using Azure.Connectors.Sdk;
+using Azure.Connectors.Sdk.SharePointOnline;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.Extensions.Logging;
+using SharePointBlobMetadata = Azure.Connectors.Sdk.SharePointOnline.Models.BlobMetadata;
+
+namespace DirectConnector;
+
+///
+/// Azure Functions that use the generated from the Azure Connectors SDK.
+///
+///
+/// Demonstrates list browsing, folder listing, file download/upload,
+/// and CancellationToken propagation from the Functions host.
+///
+public class SharePointFunctions
+{
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true
+ };
+
+ private readonly ILogger _logger;
+ private readonly SharePointOnlineClient _sharePointClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger instance.
+ /// The DI-injected SharePoint client (disposed by the host).
+ public SharePointFunctions(
+ ILogger logger,
+ SharePointOnlineClient sharePointClient)
+ {
+ this._logger = logger;
+ this._sharePointClient = sharePointClient;
+ }
+
+ ///
+ /// Gets all SharePoint lists and libraries for a site using the generated .
+ ///
+ /// The HTTP request containing the site address.
+ /// The cancellation token.
+ [Function("GetSharePointLists")]
+ public async Task GetSharePointListsAsync(
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "sharepoint/lists")] HttpRequestData request,
+ CancellationToken cancellationToken)
+ {
+ this._logger.LogInformation("GetSharePointLists: Using generated SharePointOnlineClient from SDK.");
+
+ var siteAddress = request.Query["site"];
+ if (string.IsNullOrEmpty(siteAddress))
+ {
+ var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
+ await badRequest
+ .WriteAsJsonAsync(new { error = "Query parameter 'site' is required." })
+ .ConfigureAwait(continueOnCapturedContext: false);
+ return badRequest;
+ }
+
+ try
+ {
+ var tables = await this._sharePointClient
+ .GetAllTablesAsync(siteAddress, cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ var response = request.CreateResponse(HttpStatusCode.OK);
+ await response
+ .WriteAsJsonAsync(new
+ {
+ success = true,
+ site = siteAddress,
+ count = tables?.Value?.Count ?? 0,
+ lists = tables?.Value?.Select(table => new
+ {
+ name = table.Name,
+ displayName = table.DisplayName
+ })
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return response;
+ }
+ catch (ConnectorException ex)
+ {
+ this._logger.LogError(ex, "SharePoint connector error: '{StatusCode}'.", ex.Status);
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.BadGateway);
+ await errorResponse
+ .WriteAsJsonAsync(new
+ {
+ success = false,
+ error = ex.Message,
+ statusCode = ex.Status,
+ details = ex.ResponseBody
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ catch (Exception ex) when (!ex.IsFatal())
+ {
+ this._logger.LogError(ex, "Error in GetSharePointLists.");
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.InternalServerError);
+ await errorResponse
+ .WriteAsJsonAsync(new
+ {
+ success = false,
+ error = ex.Message
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ }
+
+ ///
+ /// Lists files in a SharePoint folder using the generated .
+ ///
+ ///
+ /// Exercises the model for folder browsing.
+ ///
+ /// The HTTP request containing site and optional folder identifier.
+ /// The cancellation token.
+ [Function("ListFolder")]
+ public async Task ListFolderAsync(
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "sharepoint/files")] HttpRequestData request,
+ CancellationToken cancellationToken)
+ {
+ this._logger.LogInformation("ListFolder: Using generated SharePointOnlineClient from SDK.");
+
+ var siteAddress = request.Query["site"];
+ if (string.IsNullOrEmpty(siteAddress))
+ {
+ var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
+ await badRequest
+ .WriteAsJsonAsync(new { error = "Query parameter 'site' is required." })
+ .ConfigureAwait(continueOnCapturedContext: false);
+ return badRequest;
+ }
+
+ try
+ {
+ var folderId = request.Query["folder"];
+
+ // NOTE: ListRootFolderAsync vs ListFolderAsync demonstrates
+ // two overloads with the same return type but different parameter sets.
+ var files = string.IsNullOrEmpty(folderId)
+ ? await this._sharePointClient
+ .ListRootFolderAsync(siteAddress, cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false)
+ : await this._sharePointClient
+ .ListFolderAsync(siteAddress, folderId, cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ var response = request.CreateResponse(HttpStatusCode.OK);
+ await response
+ .WriteAsJsonAsync(new
+ {
+ success = true,
+ site = siteAddress,
+ folder = string.IsNullOrEmpty(folderId) ? "(root)" : folderId,
+ count = files?.Count ?? 0,
+ files = (files ?? Enumerable.Empty()).Select(file => new
+ {
+ id = file.Id,
+ name = file.Name,
+ displayName = file.DisplayName,
+ path = file.Path,
+ size = file.Size,
+ mediaType = file.MediaType,
+ isFolder = file.IsFolder
+ })
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return response;
+ }
+ catch (ConnectorException ex)
+ {
+ this._logger.LogError(ex, "SharePoint connector error: '{StatusCode}'.", ex.Status);
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.BadGateway);
+ await errorResponse
+ .WriteAsJsonAsync(new
+ {
+ success = false,
+ error = ex.Message,
+ statusCode = ex.Status,
+ details = ex.ResponseBody
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ catch (Exception ex) when (!ex.IsFatal())
+ {
+ this._logger.LogError(ex, "Error in ListFolder.");
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.InternalServerError);
+ await errorResponse
+ .WriteAsJsonAsync(new { success = false, error = ex.Message })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ }
+
+ ///
+ /// Downloads file content from SharePoint as binary bytes.
+ ///
+ ///
+ /// Exercises the byte[] response path in .
+ /// The generated CallConnectorAsync detects byte[] as the response type and uses
+ /// ReadAsByteArrayAsync instead of JSON deserialization.
+ ///
+ /// The HTTP request containing site address and file path.
+ /// The cancellation token.
+ [Function("DownloadFile")]
+ public async Task DownloadFileAsync(
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "sharepoint/download")] HttpRequestData request,
+ CancellationToken cancellationToken)
+ {
+ this._logger.LogInformation("DownloadFile: Using generated SharePointOnlineClient byte[] response path.");
+
+ var siteAddress = request.Query["site"];
+ var filePath = request.Query["path"];
+ if (string.IsNullOrEmpty(siteAddress) || string.IsNullOrEmpty(filePath))
+ {
+ var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
+ await badRequest
+ .WriteAsJsonAsync(new { error = "Query parameters 'site' and 'path' are required." })
+ .ConfigureAwait(continueOnCapturedContext: false);
+ return badRequest;
+ }
+
+ try
+ {
+ // NOTE: This exercises the byte[] return path in the generated client.
+ // CallConnectorAsync uses ReadAsByteArrayAsync instead of JSON deserialization.
+ var fileBytes = await this._sharePointClient
+ .GetFileContentByPathAsync(siteAddress, filePath, cancellationToken: cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ // NOTE: Sanitize the filename to prevent response header injection.
+ // Path.GetFileName strips directory traversal, and we remove CR/LF and quotes
+ // that could corrupt the Content-Disposition header value.
+ var fileName = System.IO.Path.GetFileName(filePath)
+ .Replace("\r", string.Empty)
+ .Replace("\n", string.Empty)
+ .Replace("\"", string.Empty);
+
+ var response = request.CreateResponse(HttpStatusCode.OK);
+ response.Headers.Add("Content-Type", "application/octet-stream");
+ response.Headers.Add("Content-Disposition", $"attachment; filename=\"{fileName}\"");
+ await response.Body
+ .WriteAsync(fileBytes, cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ this._logger.LogInformation("Downloaded '{FileName}': '{ByteCount}' bytes.", fileName, fileBytes.Length);
+
+ return response;
+ }
+ catch (ConnectorException ex)
+ {
+ this._logger.LogError(ex, "SharePoint connector error: '{StatusCode}'.", ex.Status);
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.BadGateway);
+ await errorResponse
+ .WriteAsJsonAsync(new
+ {
+ success = false,
+ error = ex.Message,
+ statusCode = ex.Status,
+ details = ex.ResponseBody
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ catch (Exception ex) when (!ex.IsFatal())
+ {
+ this._logger.LogError(ex, "Error in DownloadFile.");
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.InternalServerError);
+ await errorResponse
+ .WriteAsJsonAsync(new { success = false, error = ex.Message })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ }
+
+ ///
+ /// Uploads a file to a SharePoint document library.
+ ///
+ ///
+ /// Exercises the byte[] input path in .
+ /// Accepts a JSON body with base64-encoded content or plain text, and uploads it to
+ /// the specified SharePoint folder.
+ ///
+ /// The HTTP request containing upload details.
+ /// The cancellation token.
+ [Function("UploadFile")]
+ public async Task UploadFileAsync(
+ [HttpTrigger(AuthorizationLevel.Function, "post", Route = "sharepoint/upload")] HttpRequestData request,
+ CancellationToken cancellationToken)
+ {
+ this._logger.LogInformation("UploadFile: Using generated SharePointOnlineClient byte[] input path.");
+
+ try
+ {
+ using var reader = new StreamReader(request.Body);
+ var body = await reader
+ .ReadToEndAsync(cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ UploadFileRequest? input;
+ try
+ {
+ input = JsonSerializer.Deserialize(body, SharePointFunctions.JsonOptions);
+ }
+ catch (JsonException ex)
+ {
+ this._logger.LogError(ex, "Invalid JSON in request body: '{Message}'.", ex.Message);
+
+ var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
+ await badRequest
+ .WriteAsJsonAsync(new { error = "Request body must contain valid JSON." })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return badRequest;
+ }
+
+ if (input == null ||
+ string.IsNullOrEmpty(input.Site) ||
+ string.IsNullOrEmpty(input.FolderPath) ||
+ string.IsNullOrEmpty(input.FileName))
+ {
+ var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
+ await badRequest
+ .WriteAsJsonAsync(new { error = "Fields 'site', 'folderPath', and 'fileName' are required." })
+ .ConfigureAwait(continueOnCapturedContext: false);
+ return badRequest;
+ }
+
+ // NOTE: Support both base64-encoded binary and plain text content.
+ byte[] fileBytes;
+ try
+ {
+ fileBytes = !string.IsNullOrEmpty(input.ContentBase64)
+ ? Convert.FromBase64String(input.ContentBase64)
+ : Encoding.UTF8.GetBytes(input.Content ?? string.Empty);
+ }
+ catch (FormatException ex)
+ {
+ this._logger.LogError(ex, "Invalid base64 content in 'contentBase64'.");
+
+ var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
+ await badRequest
+ .WriteAsJsonAsync(new { error = "The 'contentBase64' field must contain valid base64-encoded data." })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return badRequest;
+ }
+
+ var metadata = await this._sharePointClient
+ .CreateFileAsync(input.Site, fileBytes, input.FolderPath, input.FileName, cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ var response = request.CreateResponse(HttpStatusCode.OK);
+ await response
+ .WriteAsJsonAsync(new
+ {
+ success = true,
+ message = $"File '{input.FileName}' uploaded to '{input.FolderPath}'.",
+ fileId = metadata?.Id,
+ name = metadata?.Name,
+ path = metadata?.Path,
+ size = fileBytes.Length
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return response;
+ }
+ catch (ConnectorException ex)
+ {
+ this._logger.LogError(ex, "SharePoint connector error: '{StatusCode}'.", ex.Status);
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.BadGateway);
+ await errorResponse
+ .WriteAsJsonAsync(new
+ {
+ success = false,
+ error = ex.Message,
+ statusCode = ex.Status,
+ details = ex.ResponseBody
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ catch (Exception ex) when (!ex.IsFatal())
+ {
+ this._logger.LogError(ex, "Error in UploadFile.");
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.InternalServerError);
+ await errorResponse
+ .WriteAsJsonAsync(new { success = false, error = ex.Message })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ }
+
+ ///
+ /// Request model for uploading a file to SharePoint.
+ ///
+ private record UploadFileRequest(
+ string? Site,
+ string? FolderPath,
+ string? FileName,
+ string? Content,
+ string? ContentBase64);
+}
diff --git a/DirectConnector/TeamsFunctions.cs b/DirectConnector/TeamsFunctions.cs
new file mode 100644
index 0000000..f72a3ce
--- /dev/null
+++ b/DirectConnector/TeamsFunctions.cs
@@ -0,0 +1,406 @@
+//------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//------------------------------------------------------------
+
+using System.Net;
+using System.Text.Json;
+using Azure.Connectors.Sdk;
+using Azure.Connectors.Sdk.Teams;
+using Azure.Connectors.Sdk.Teams.Models;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.Extensions.Logging;
+
+namespace DirectConnector;
+
+///
+/// Azure Functions that use the generated from the Azure Connectors SDK.
+///
+///
+/// Demonstrates listing teams, channels, channel messages with IAsyncEnumerable pagination,
+/// posting messages, and CancellationToken propagation from the Functions host.
+///
+public class TeamsFunctions
+{
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true
+ };
+
+ ///
+ /// Default poster identity for Teams messages posted via the connector.
+ ///
+ private const string TeamsDefaultPoster = "Flow bot";
+
+ ///
+ /// Default message location for Teams channel posts.
+ ///
+ private const string TeamsDefaultLocation = "Channel";
+
+ private readonly ILogger _logger;
+ private readonly TeamsClient _teamsClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger instance.
+ /// The DI-injected Teams client (disposed by the host).
+ public TeamsFunctions(
+ ILogger logger,
+ TeamsClient teamsClient)
+ {
+ this._logger = logger;
+ this._teamsClient = teamsClient;
+ }
+
+ ///
+ /// Lists all Teams the signed-in user is a member of using the generated .
+ ///
+ /// The HTTP request.
+ /// The cancellation token.
+ [Function("GetAllTeams")]
+ public async Task GetAllTeamsAsync(
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "teams/teams")] HttpRequestData request,
+ CancellationToken cancellationToken)
+ {
+ this._logger.LogInformation("GetAllTeams: Using generated TeamsClient from SDK.");
+
+ try
+ {
+ var result = await this._teamsClient
+ .GetAllTeamsAsync(cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ var response = request.CreateResponse(HttpStatusCode.OK);
+ await response
+ .WriteAsJsonAsync(new
+ {
+ success = true,
+ count = result?.TeamsList?.Count ?? 0,
+ teams = result?.TeamsList
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return response;
+ }
+ catch (ConnectorException ex)
+ {
+ this._logger.LogError(ex, "Teams connector error: '{StatusCode}'.", ex.Status);
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.BadGateway);
+ await errorResponse
+ .WriteAsJsonAsync(new
+ {
+ success = false,
+ error = ex.Message,
+ statusCode = ex.Status,
+ details = ex.ResponseBody
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ catch (Exception ex) when (!ex.IsFatal())
+ {
+ this._logger.LogError(ex, "Error in GetAllTeams.");
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.InternalServerError);
+ await errorResponse
+ .WriteAsJsonAsync(new { success = false, error = ex.Message })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ }
+
+ ///
+ /// Lists all channels for a specific team using the generated .
+ ///
+ /// The HTTP request containing the team ID.
+ /// The cancellation token.
+ [Function("GetTeamChannels")]
+ public async Task GetTeamChannelsAsync(
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "teams/channels")] HttpRequestData request,
+ CancellationToken cancellationToken)
+ {
+ this._logger.LogInformation("GetTeamChannels: Using generated TeamsClient from SDK.");
+
+ var teamId = request.Query["teamId"];
+ if (string.IsNullOrEmpty(teamId))
+ {
+ var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
+ await badRequest
+ .WriteAsJsonAsync(new { error = "Query parameter 'teamId' is required." })
+ .ConfigureAwait(continueOnCapturedContext: false);
+ return badRequest;
+ }
+
+ try
+ {
+ var result = await this._teamsClient
+ .GetChannelsForGroupAsync(teamId, cancellationToken: cancellationToken)
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ var response = request.CreateResponse(HttpStatusCode.OK);
+ await response
+ .WriteAsJsonAsync(new
+ {
+ success = true,
+ teamId,
+ count = result?.ChannelList?.Count ?? 0,
+ channels = result?.ChannelList?.Select(channel => new
+ {
+ id = channel.ChannelID,
+ displayName = channel.DisplayName,
+ description = channel.DescriptionOfChannel,
+ membershipType = channel.TheTypeOfTheChannel
+ })
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return response;
+ }
+ catch (ConnectorException ex)
+ {
+ this._logger.LogError(ex, "Teams connector error: '{StatusCode}'.", ex.Status);
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.BadGateway);
+ await errorResponse
+ .WriteAsJsonAsync(new
+ {
+ success = false,
+ error = ex.Message,
+ statusCode = ex.Status,
+ details = ex.ResponseBody
+ })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ catch (Exception ex) when (!ex.IsFatal())
+ {
+ this._logger.LogError(ex, "Error in GetTeamChannels.");
+
+ var errorResponse = request.CreateResponse(HttpStatusCode.InternalServerError);
+ await errorResponse
+ .WriteAsJsonAsync(new { success = false, error = ex.Message })
+ .ConfigureAwait(continueOnCapturedContext: false);
+
+ return errorResponse;
+ }
+ }
+
+ ///
+ /// Gets all messages from a Teams channel, automatically paginating across all pages.
+ ///
+ ///
+ /// Demonstrates pagination: GetMessagesFromChannelAsync
+ /// returns a ConnectorPageable that follows @odata.nextLink automatically.
+ /// The caller uses await foreach and never sees pagination details.
+ ///
+ /// The HTTP request with teamId and channelId query parameters.
+ /// The cancellation token.
+ [Function("GetChannelMessages")]
+ public async Task GetChannelMessagesAsync(
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "teams/messages")] HttpRequestData request,
+ CancellationToken cancellationToken)
+ {
+ this._logger.LogInformation("GetChannelMessages: Demonstrating IAsyncEnumerable pagination.");
+
+ var teamId = request.Query["teamId"];
+ var channelId = request.Query["channelId"];
+
+ if (string.IsNullOrEmpty(teamId) || string.IsNullOrEmpty(channelId))
+ {
+ var badRequest = request.CreateResponse(HttpStatusCode.BadRequest);
+ await badRequest
+ .WriteAsJsonAsync(new { error = "Query parameters 'teamId' and 'channelId' are required." })
+ .ConfigureAwait(continueOnCapturedContext: false);
+ return badRequest;
+ }
+
+ try
+ {
+ // GetMessagesFromChannelAsync returns IAsyncEnumerable that automatically
+ // follows @odata.nextLink pagination across all pages.
+ var messages = new List