From 6865db661fa2614ee5738ddbdb013f74cab11584 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Thu, 9 Jul 2026 17:40:13 -0700 Subject: [PATCH 01/23] Add agentic identity defaults to app API Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Api/Clients/ActivityClient.cs | 61 +++- .../Api/Clients/ApiClient.cs | 48 ++- .../Api/Clients/ApiRequestContext.cs | 23 ++ .../Api/Clients/ConversationApiClient.cs | 12 +- .../Api/Clients/MeetingClient.cs | 8 +- .../Api/Clients/MemberClient.cs | 10 +- .../Api/Clients/ReactionClient.cs | 8 +- .../Api/Clients/TeamClient.cs | 8 +- core/src/Microsoft.Teams.Apps/Context.cs | 3 +- .../TeamsBotApplication.HostingExtensions.cs | 6 +- .../TeamsBotApplication.cs | 30 ++ .../TeamsBotApplicationOptions.cs | 5 + .../AgenticIdentityDefaultsTests.cs | 341 ++++++++++++++++++ ...amsBotApplicationHostingExtensionsTests.cs | 1 + 14 files changed, 522 insertions(+), 42 deletions(-) create mode 100644 core/src/Microsoft.Teams.Apps/Api/Clients/ApiRequestContext.cs create mode 100644 core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs index 1cf236fa..e2b1b2c5 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Diagnostics.CodeAnalysis; +using Microsoft.Teams.Apps.Schema; using Microsoft.Teams.Core; using Microsoft.Teams.Core.Http; using Microsoft.Teams.Core.Schema; @@ -19,44 +20,64 @@ public class ActivityClient { private readonly CoreConversationClient _client; private readonly Uri _serviceUrl; + private readonly BotRequestContext? _defaultRequestContext; - internal ActivityClient(Uri serviceUrl, CoreConversationClient client) + internal ActivityClient(Uri serviceUrl, CoreConversationClient client, BotRequestContext? defaultRequestContext = null) { _serviceUrl = serviceUrl; _client = client; + _defaultRequestContext = defaultRequestContext; } /// /// Create a new activity in a conversation. /// public Task CreateAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => CreateAsync(conversationId, activity, agenticIdentity: null, additionalHeaders: additionalHeaders, cancellationToken: cancellationToken); + + /// + /// Create a new activity in a conversation. + /// + public Task CreateAsync(string conversationId, CoreActivity activity, AgenticIdentity? agenticIdentity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// /// Update an existing activity in a conversation. /// public Task UpdateAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => UpdateAsync(conversationId, id, activity, agenticIdentity: null, additionalHeaders: additionalHeaders, cancellationToken: cancellationToken); + + /// + /// Update an existing activity in a conversation. + /// + public Task UpdateAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; - return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.FromActivity(activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: ApiRequestContext.ResolveActivity(_defaultRequestContext, agenticIdentity, activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// /// Reply to an existing activity in a conversation. /// public Task ReplyAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => ReplyAsync(conversationId, id, activity, agenticIdentity: null, additionalHeaders: additionalHeaders, cancellationToken: cancellationToken); + + /// + /// Reply to an existing activity in a conversation. + /// + public Task ReplyAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ReplyToId = id; activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -64,7 +85,16 @@ public Task UpdateAsync(string conversationId, string id /// public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + } + + /// + /// Get members of a specific activity. + /// + public async Task> GetMembersAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + { + IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); + return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; } /// @@ -73,6 +103,14 @@ public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agent /// [Experimental("ExperimentalTeamsTargeted")] public Task CreateTargetedAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => CreateTargetedAsync(conversationId, activity, agenticIdentity: null, additionalHeaders: additionalHeaders, cancellationToken: cancellationToken); + + /// + /// Create a new targeted activity in a conversation. + /// Targeted activities are only visible to the specified recipient. + /// + [Experimental("ExperimentalTeamsTargeted")] + public Task CreateTargetedAsync(string conversationId, CoreActivity activity, AgenticIdentity? agenticIdentity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -82,7 +120,7 @@ public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agent { activity.Recipient.IsTargeted = true; } - return _client.SendActivityAsync(activity, customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -90,10 +128,17 @@ public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agent /// [Experimental("ExperimentalTeamsTargeted")] public Task UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => UpdateTargetedAsync(conversationId, id, activity, agenticIdentity: null, additionalHeaders: additionalHeaders, cancellationToken: cancellationToken); + + /// + /// Update an existing targeted activity in a conversation. + /// + [Experimental("ExperimentalTeamsTargeted")] + public Task UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; - return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.FromActivity(activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: ApiRequestContext.ResolveActivity(_defaultRequestContext, agenticIdentity, activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -101,6 +146,6 @@ public Task UpdateTargetedAsync(string conversationId, s /// public Task DeleteTargetedAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs index 2d17636a..6ef83e08 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Teams.Core.Http; +using Microsoft.Teams.Core.Schema; using CoreConversationClient = Microsoft.Teams.Core.ConversationClient; using CoreUserTokenClient = Microsoft.Teams.Core.UserTokenClient; @@ -18,9 +19,9 @@ namespace Microsoft.Teams.Apps.Api.Clients; /// This client can be constructed in two ways: /// /// -/// DI-friendly (no serviceUrl) — Use -/// and call per-request to create a scoped instance. -/// Fully initialized — Use +/// DI-friendly (no serviceUrl) — Use ApiClient(HttpClient, ConversationClient, UserTokenClient, ILogger) +/// and call ForServiceUrl per-request to create a scoped instance. +/// Fully initialized — Use ApiClient(Uri, HttpClient, ConversationClient, UserTokenClient, ILogger) /// when the service URL is known upfront. /// /// @@ -32,10 +33,12 @@ public class ApiClient internal CoreUserTokenClient UserTokenClient { get; } + internal BotRequestContext? DefaultRequestContext { get; } + /// /// The service URL used by this client. /// Null when constructed without a service URL (DI-friendly constructor). - /// Call to create a scoped instance with a service URL. + /// Call ForServiceUrl to create a scoped instance with a service URL. /// public virtual Uri ServiceUrl { get; } @@ -64,7 +67,7 @@ public class ApiClient /// /// Creates a new without a service URL (DI-friendly). - /// Use to create a scoped instance bound to a specific service URL. + /// Use ForServiceUrl to create a scoped instance bound to a specific service URL. /// /// An configured with authentication (e.g., via DI with BotAuthenticationHandler). /// The core conversation client for conversation/activity/member operations. @@ -80,6 +83,7 @@ internal ApiClient(HttpClient httpClient, CoreConversationClient conversationCli _http = new BotHttpClient(httpClient, logger); ConversationClient = conversationClient; UserTokenClient = userTokenClient; + DefaultRequestContext = null; // ServiceUrl-dependent sub-clients require ForServiceUrl() before use ServiceUrl = null!; @@ -96,7 +100,8 @@ internal ApiClient(HttpClient httpClient, CoreConversationClient conversationCli /// The core conversation client for conversation/activity/member operations. /// The core user token client for sign-in and token operations. /// Optional logger. - internal ApiClient(Uri serviceUrl, HttpClient httpClient, CoreConversationClient conversationClient, CoreUserTokenClient userTokenClient, ILogger? logger = null) + /// Optional default agentic identity for service URL-bound sub-clients. + internal ApiClient(Uri serviceUrl, HttpClient httpClient, CoreConversationClient conversationClient, CoreUserTokenClient userTokenClient, ILogger? logger = null, AgenticIdentity? defaultAgenticIdentity = null) { ArgumentNullException.ThrowIfNull(serviceUrl); ArgumentNullException.ThrowIfNull(httpClient); @@ -106,22 +111,24 @@ internal ApiClient(Uri serviceUrl, HttpClient httpClient, CoreConversationClient _http = new BotHttpClient(httpClient, logger); ConversationClient = conversationClient; UserTokenClient = userTokenClient; + DefaultRequestContext = BotRequestContext.FromAgenticIdentity(defaultAgenticIdentity); ServiceUrl = serviceUrl; - Conversations = new ConversationApiClient(serviceUrl, conversationClient); - Teams = new TeamClient(serviceUrl.ToString(), _http); - Meetings = new MeetingClient(serviceUrl.ToString(), _http); + Conversations = new ConversationApiClient(serviceUrl, conversationClient, DefaultRequestContext); + Teams = new TeamClient(serviceUrl.ToString(), _http, DefaultRequestContext); + Meetings = new MeetingClient(serviceUrl.ToString(), _http, DefaultRequestContext); } // Private constructor for ForServiceUrl — shares BotHttpClient, ConversationClient, and UserTokenClient - private ApiClient(BotHttpClient http, CoreConversationClient conversationClient, CoreUserTokenClient userTokenClient, Uri serviceUrl) + private ApiClient(BotHttpClient http, CoreConversationClient conversationClient, CoreUserTokenClient userTokenClient, Uri serviceUrl, BotRequestContext? defaultRequestContext) { _http = http; ConversationClient = conversationClient; UserTokenClient = userTokenClient; + DefaultRequestContext = defaultRequestContext; ServiceUrl = serviceUrl; - Conversations = new ConversationApiClient(serviceUrl, conversationClient); - Teams = new TeamClient(serviceUrl.ToString(), http); - Meetings = new MeetingClient(serviceUrl.ToString(), http); + Conversations = new ConversationApiClient(serviceUrl, conversationClient, defaultRequestContext); + Teams = new TeamClient(serviceUrl.ToString(), http, defaultRequestContext); + Meetings = new MeetingClient(serviceUrl.ToString(), http, defaultRequestContext); } /// @@ -133,6 +140,19 @@ private ApiClient(BotHttpClient http, CoreConversationClient conversationClient, public virtual ApiClient ForServiceUrl(Uri serviceUrl) { ArgumentNullException.ThrowIfNull(serviceUrl); - return new ApiClient(_http, ConversationClient, UserTokenClient, serviceUrl); + return new ApiClient(_http, ConversationClient, UserTokenClient, serviceUrl, defaultRequestContext: null); + } + + /// + /// Creates a new scoped to the specified service URL with a default agentic identity + /// used when per-call methods do not receive an explicit identity. + /// + /// The Bot Framework service URL for this scope. + /// The default agentic identity for service URL-bound sub-clients. + /// A new bound to the given service URL and default identity. + public virtual ApiClient ForServiceUrl(Uri serviceUrl, AgenticIdentity? defaultAgenticIdentity) + { + ArgumentNullException.ThrowIfNull(serviceUrl); + return new ApiClient(_http, ConversationClient, UserTokenClient, serviceUrl, BotRequestContext.FromAgenticIdentity(defaultAgenticIdentity)); } } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiRequestContext.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiRequestContext.cs new file mode 100644 index 00000000..7f5fcb19 --- /dev/null +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiRequestContext.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Teams.Core; +using Microsoft.Teams.Core.Http; +using Microsoft.Teams.Core.Schema; + +namespace Microsoft.Teams.Apps.Api.Clients; + +internal static class ApiRequestContext +{ + public static BotRequestContext? Resolve(BotRequestContext? defaultRequestContext, AgenticIdentity? agenticIdentity) + => BotRequestContext.Merge(defaultRequestContext, BotRequestContext.FromAgenticIdentity(agenticIdentity)); + + public static BotRequestContext? ResolveActivity(BotRequestContext? defaultRequestContext, AgenticIdentity? agenticIdentity, CoreActivity? activity) + => BotRequestContext.Merge(Resolve(defaultRequestContext, agenticIdentity), BotRequestContext.FromActivity(activity)); + + public static BotRequestOptions? CreateOptions(BotRequestContext? defaultRequestContext, AgenticIdentity? agenticIdentity) + { + BotRequestContext? requestContext = Resolve(defaultRequestContext, agenticIdentity); + return requestContext is null ? null : new BotRequestOptions { RequestContext = requestContext }; + } +} diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs index 26b49bd0..71ad03c5 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs @@ -20,6 +20,7 @@ public class ConversationApiClient { private readonly CoreConversationClient _client; private readonly Uri _serviceUrl; + private readonly BotRequestContext? _defaultRequestContext; /// /// Client for activity operations. @@ -39,14 +40,15 @@ public class ConversationApiClient [Obsolete("Use ConversationApiClient.AddReactionAsync and ConversationApiClient.DeleteReactionAsync instead.")] public ReactionClient Reactions { get; } - internal ConversationApiClient(Uri serviceUrl, CoreConversationClient client) + internal ConversationApiClient(Uri serviceUrl, CoreConversationClient client, BotRequestContext? defaultRequestContext = null) { _serviceUrl = serviceUrl; _client = client; + _defaultRequestContext = defaultRequestContext; #pragma warning disable CS0618 // Suppress obsolete warnings for backward-compatible initialization - Activities = new ActivityClient(serviceUrl, client); - Members = new MemberClient(serviceUrl, client); - Reactions = new ReactionClient(serviceUrl, client); + Activities = new ActivityClient(serviceUrl, client, defaultRequestContext); + Members = new MemberClient(serviceUrl, client, defaultRequestContext); + Reactions = new ReactionClient(serviceUrl, client, defaultRequestContext); #pragma warning restore CS0618 } @@ -55,7 +57,7 @@ internal ConversationApiClient(Uri serviceUrl, CoreConversationClient client) /// public Task CreateAsync(ConversationParameters request, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.CreateConversationAsync(request, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.CreateConversationAsync(request, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } #region Activity Methods diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs index 43485ee3..ee527417 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs @@ -14,11 +14,13 @@ public class MeetingClient { private readonly BotHttpClient _http; private readonly string _serviceUrl; + private readonly BotRequestContext? _defaultRequestContext; - internal MeetingClient(string serviceUrl, BotHttpClient http) + internal MeetingClient(string serviceUrl, BotHttpClient http, BotRequestContext? defaultRequestContext = null) { _serviceUrl = serviceUrl.TrimEnd('/'); _http = http; + _defaultRequestContext = defaultRequestContext; } /// @@ -39,8 +41,8 @@ internal MeetingClient(string serviceUrl, BotHttpClient http) return await _http.SendAsync(HttpMethod.Get, url, body: null, options: CreateRequestOptions(agenticIdentity), cancellationToken).ConfigureAwait(false); } - private static BotRequestOptions? CreateRequestOptions(AgenticIdentity? agenticIdentity) => - agenticIdentity is null ? null : new() { RequestContext = BotRequestContext.FromAgenticIdentity(agenticIdentity) }; + private BotRequestOptions? CreateRequestOptions(AgenticIdentity? agenticIdentity) => + ApiRequestContext.CreateOptions(_defaultRequestContext, agenticIdentity); } /// diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/MemberClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/MemberClient.cs index cfd05b82..a6917b7f 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/MemberClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/MemberClient.cs @@ -20,11 +20,13 @@ public class MemberClient { private readonly CoreConversationClient _client; private readonly Uri _serviceUrl; + private readonly BotRequestContext? _defaultRequestContext; - internal MemberClient(Uri serviceUrl, CoreConversationClient client) + internal MemberClient(Uri serviceUrl, CoreConversationClient client, BotRequestContext? defaultRequestContext = null) { _serviceUrl = serviceUrl; _client = client; + _defaultRequestContext = defaultRequestContext; } /// @@ -33,7 +35,7 @@ internal MemberClient(Uri serviceUrl, CoreConversationClient client) [Obsolete("Use GetPagedAsync instead.")] public async Task> GetAsync(string conversationId, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - IList members = await _client.GetConversationMembersAsync(conversationId, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); + IList members = await _client.GetConversationMembersAsync(conversationId, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; } @@ -53,7 +55,7 @@ public async Task GetPagedAsync( _serviceUrl, pageSize, continuationToken, - requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), + requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); PagedTeamsMembersResult result = new(); @@ -112,7 +114,7 @@ public async IAsyncEnumerable GetAllAsync( /// public Task GetByIdAsync(string conversationId, string memberId, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) where T : ChannelAccount { - return _client.GetConversationMemberAsync(conversationId, memberId, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.GetConversationMemberAsync(conversationId, memberId, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ReactionClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ReactionClient.cs index 88419ec9..8c3c5b78 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ReactionClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ReactionClient.cs @@ -17,11 +17,13 @@ public class ReactionClient { private readonly CoreConversationClient _client; private readonly Uri _serviceUrl; + private readonly BotRequestContext? _defaultRequestContext; - internal ReactionClient(Uri serviceUrl, CoreConversationClient client) + internal ReactionClient(Uri serviceUrl, CoreConversationClient client, BotRequestContext? defaultRequestContext = null) { _serviceUrl = serviceUrl; _client = client; + _defaultRequestContext = defaultRequestContext; } /// @@ -35,7 +37,7 @@ internal ReactionClient(Uri serviceUrl, CoreConversationClient client) /// A to observe while waiting for the task to complete. public Task AddAsync(string conversationId, string activityId, string reactionType, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.AddReactionAsync(conversationId, activityId, reactionType, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.AddReactionAsync(conversationId, activityId, reactionType, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -49,6 +51,6 @@ public Task AddAsync(string conversationId, string activityId, string reactionTy /// A to observe while waiting for the task to complete. public Task DeleteAsync(string conversationId, string activityId, string reactionType, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.DeleteReactionAsync(conversationId, activityId, reactionType, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteReactionAsync(conversationId, activityId, reactionType, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs index 4b3dd905..346a9567 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs @@ -15,11 +15,13 @@ public class TeamClient { private readonly BotHttpClient _http; private readonly string _serviceUrl; + private readonly BotRequestContext? _defaultRequestContext; - internal TeamClient(string serviceUrl, BotHttpClient http) + internal TeamClient(string serviceUrl, BotHttpClient http, BotRequestContext? defaultRequestContext = null) { _serviceUrl = serviceUrl.TrimEnd('/'); _http = http; + _defaultRequestContext = defaultRequestContext; } /// @@ -41,8 +43,8 @@ internal TeamClient(string serviceUrl, BotHttpClient http) return response?.Conversations; } - private static BotRequestOptions? CreateRequestOptions(AgenticIdentity? agenticIdentity) => - agenticIdentity is null ? null : new() { RequestContext = BotRequestContext.FromAgenticIdentity(agenticIdentity) }; + private BotRequestOptions? CreateRequestOptions(AgenticIdentity? agenticIdentity) => + ApiRequestContext.CreateOptions(_defaultRequestContext, agenticIdentity); private sealed class ConversationListResponse { diff --git a/core/src/Microsoft.Teams.Apps/Context.cs b/core/src/Microsoft.Teams.Apps/Context.cs index 53851f38..11d343c9 100644 --- a/core/src/Microsoft.Teams.Apps/Context.cs +++ b/core/src/Microsoft.Teams.Apps/Context.cs @@ -49,7 +49,8 @@ public class Context(TeamsBotApplication botApplication, TActivity ac /// Gets the scoped to the current activity's service URL. /// public ApiClient Api => _api ??= TeamsBotApplication.Api.ForServiceUrl( - Activity.ServiceUrl ?? throw new InvalidOperationException("Activity.ServiceUrl is required to use the Api client.")); + Activity.ServiceUrl ?? throw new InvalidOperationException("Activity.ServiceUrl is required to use the Api client."), + Activity.Recipient?.GetAgenticIdentity()); // ==================== Turn State ==================== diff --git a/core/src/Microsoft.Teams.Apps/TeamsBotApplication.HostingExtensions.cs b/core/src/Microsoft.Teams.Apps/TeamsBotApplication.HostingExtensions.cs index 9f9bc60c..38eaef2b 100644 --- a/core/src/Microsoft.Teams.Apps/TeamsBotApplication.HostingExtensions.cs +++ b/core/src/Microsoft.Teams.Apps/TeamsBotApplication.HostingExtensions.cs @@ -115,7 +115,11 @@ public static IServiceCollection AddTeamsBotApplication(this IServiceColle BotConfig botConfig = BotConfig.Resolve(services, sectionName); // Register TeamsBotApplicationOptions - TeamsBotApplicationOptions teamsOptions = new() { AppId = botConfig.ClientId }; + TeamsBotApplicationOptions teamsOptions = new() + { + AppId = botConfig.ClientId, + TenantId = string.IsNullOrWhiteSpace(botConfig.TenantId) ? null : botConfig.TenantId + }; configure?.Invoke(teamsOptions); services.AddSingleton(teamsOptions); diff --git a/core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs b/core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs index f273c8e9..f71e24af 100644 --- a/core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs +++ b/core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs @@ -21,6 +21,7 @@ namespace Microsoft.Teams.Apps; public class TeamsBotApplication : BotApplication { private readonly TurnStateLoader? _stateLoader; + private readonly string? _tenantId; private Uri? _lastServiceUrl; /// @@ -112,6 +113,7 @@ public TeamsBotApplication( options) { _stateLoader = stateLoader; + _tenantId = string.IsNullOrWhiteSpace(options?.TenantId) ? null : options.TenantId; Api = teamsApiClient; Logger = logger; Router = new Router(logger); @@ -217,6 +219,7 @@ public TeamsBotApplication( AgenticAppId = agenticIdentity.AgenticAppId, AgenticUserId = agenticIdentity.AgenticUserId, AgenticAppBlueprintId = agenticIdentity.AgenticAppBlueprintId, + TenantId = agenticIdentity.TenantId, }); } @@ -241,6 +244,33 @@ public TeamsBotApplication( return SendAsync(threadedConversationId, text, agenticIdentity: agenticIdentity, cancellationToken: cancellationToken); } + /// + /// Creates an agentic identity using this application's defaults. + /// + /// + /// The agentic app blueprint ID defaults to . + /// The tenant ID defaults to the configured when available; + /// otherwise it remains null unless explicitly provided. + /// + /// The agentic application ID. + /// The agentic user ID. + /// Optional tenant ID. Overrides the configured tenant ID when provided. + /// Optional agentic application blueprint ID. Defaults to this application's AppId. + /// An populated with application defaults. + public AgenticIdentity GetAgenticIdentity(string agenticAppId, string agenticUserId, string? tenantId = null, string? agenticAppBlueprintId = null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(agenticAppId); + ArgumentException.ThrowIfNullOrWhiteSpace(agenticUserId); + + return new AgenticIdentity + { + AgenticAppId = agenticAppId, + AgenticUserId = agenticUserId, + TenantId = tenantId ?? _tenantId, + AgenticAppBlueprintId = agenticAppBlueprintId ?? AppId + }; + } + /// [Obsolete("Use SendAsync instead.")] public Task Send(string conversationId, string text, Uri? serviceUrl = null, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default) diff --git a/core/src/Microsoft.Teams.Apps/TeamsBotApplicationOptions.cs b/core/src/Microsoft.Teams.Apps/TeamsBotApplicationOptions.cs index 1bc1ca16..56362971 100644 --- a/core/src/Microsoft.Teams.Apps/TeamsBotApplicationOptions.cs +++ b/core/src/Microsoft.Teams.Apps/TeamsBotApplicationOptions.cs @@ -15,6 +15,11 @@ public sealed class TeamsBotApplicationOptions : BotApplicationOptions { internal List OAuthFlows { get; } = []; + /// + /// Gets or sets the configured tenant ID. Used as the default tenant for agentic identities created by . + /// + public string? TenantId { get; set; } + /// /// Register an OAuth flow with the given connection name and optional configuration. /// diff --git a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs new file mode 100644 index 00000000..804fa373 --- /dev/null +++ b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Teams.Apps.Api.Clients; +using Microsoft.Teams.Apps.Schema; +using Microsoft.Teams.Core; +using Microsoft.Teams.Core.Http; +using Microsoft.Teams.Core.Schema; + +namespace Microsoft.Teams.Apps.UnitTests; + +public class AgenticIdentityDefaultsTests +{ + private static readonly Uri ServiceUrl = new("https://smba.trafficmanager.net/amer/"); + + [Fact] + public async Task ContextApi_UsesRecipientAgenticIdentityAsDefault() + { + CapturingHttpMessageHandler handler = new(); + TeamsBotApplication app = CreateApp(handler); + AgenticIdentity expected = DefaultIdentity(); + Context context = new( + app, + new TeamsActivity + { + Type = TeamsActivityTypes.Message, + ServiceUrl = ServiceUrl, + Recipient = new TeamsChannelAccount + { + AgenticAppId = expected.AgenticAppId, + AgenticUserId = expected.AgenticUserId, + AgenticAppBlueprintId = expected.AgenticAppBlueprintId, + TenantId = expected.TenantId + } + }); + + await context.Api.Conversations.Members.GetPagedAsync("conversation-id"); + + CapturedRequest request = Assert.Single(handler.Requests); + AssertIdentity(expected, request.AgenticIdentity); + } + + [Fact] + public async Task ContextApi_ExplicitIdentityOverridesRecipientDefault() + { + CapturingHttpMessageHandler handler = new(); + TeamsBotApplication app = CreateApp(handler); + AgenticIdentity explicitIdentity = ExplicitIdentity(); + Context context = new( + app, + new TeamsActivity + { + Type = TeamsActivityTypes.Message, + ServiceUrl = ServiceUrl, + Recipient = new TeamsChannelAccount + { + AgenticAppId = "default-agentic-app-id", + AgenticUserId = "default-agentic-user-id", + AgenticAppBlueprintId = "default-agentic-blueprint-id", + TenantId = "default-tenant-id" + } + }); + + await context.Api.Conversations.Members.GetByIdAsync("conversation-id", "member-id", explicitIdentity); + + CapturedRequest request = Assert.Single(handler.Requests); + AssertIdentity(explicitIdentity, request.AgenticIdentity); + } + + [Fact] + public async Task ScopedApi_ServiceUrlBoundClientsUseDefaultIdentity() + { + CapturingHttpMessageHandler handler = new(); + AgenticIdentity expected = DefaultIdentity(); + ApiClient api = CreateApiClient(handler).ForServiceUrl(ServiceUrl, expected); + + await api.Conversations.CreateAsync(new ConversationParameters()); + await api.Conversations.Members.GetByIdAsync("conversation-id", "member-id"); + await api.Conversations.Reactions.AddAsync("conversation-id", "activity-id", "like"); + await api.Teams.GetByIdAsync("team-id"); + await api.Meetings.GetByIdAsync("meeting-id"); + + Assert.Equal(5, handler.Requests.Count); + Assert.All(handler.Requests, request => AssertIdentity(expected, request.AgenticIdentity)); + } + + [Fact] + public async Task ScopedApi_ExplicitIdentityOverridesDefaultIdentity() + { + CapturingHttpMessageHandler handler = new(); + AgenticIdentity explicitIdentity = ExplicitIdentity(); + ApiClient api = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()); + + await api.Meetings.GetParticipantAsync("meeting-id", "participant-id", "tenant-id", explicitIdentity); + + CapturedRequest request = Assert.Single(handler.Requests); + AssertIdentity(explicitIdentity, request.AgenticIdentity); + } + + [Fact] + public async Task ActivityClient_UsesDefaultIdentityWhenExplicitIdentityIsMissing() + { + CapturingHttpMessageHandler handler = new(); + AgenticIdentity expected = DefaultIdentity(); + ActivityClient activities = CreateApiClient(handler).ForServiceUrl(ServiceUrl, expected).Conversations.Activities; + + await activities.CreateAsync("conversation-id", CreateActivity()); + await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity()); + await activities.ReplyAsync("conversation-id", "activity-id", CreateActivity()); + await activities.DeleteAsync("conversation-id", "activity-id"); + await activities.GetMembersAsync("conversation-id", "activity-id"); + await activities.CreateTargetedAsync("conversation-id", CreateTargetedActivity()); + await activities.UpdateTargetedAsync("conversation-id", "activity-id", CreateActivity()); + await activities.DeleteTargetedAsync("conversation-id", "activity-id"); + + Assert.Equal(8, handler.Requests.Count); + Assert.All(handler.Requests, request => AssertIdentity(expected, request.AgenticIdentity)); + } + + [Fact] + public async Task ActivityClient_ExplicitIdentityOverridesDefaultIdentity() + { + CapturingHttpMessageHandler handler = new(); + AgenticIdentity explicitIdentity = ExplicitIdentity(); + ActivityClient activities = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations.Activities; + + await activities.CreateAsync("conversation-id", CreateActivity(), explicitIdentity); + await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity(), explicitIdentity); + await activities.ReplyAsync("conversation-id", "activity-id", CreateActivity(), explicitIdentity); + await activities.DeleteAsync("conversation-id", "activity-id", explicitIdentity); + await activities.GetMembersAsync("conversation-id", "activity-id", explicitIdentity); + await activities.CreateTargetedAsync("conversation-id", CreateTargetedActivity(), explicitIdentity); + await activities.UpdateTargetedAsync("conversation-id", "activity-id", CreateActivity(), explicitIdentity); + await activities.DeleteTargetedAsync("conversation-id", "activity-id", explicitIdentity); + + Assert.Equal(8, handler.Requests.Count); + Assert.All(handler.Requests, request => AssertIdentity(explicitIdentity, request.AgenticIdentity)); + } + + [Fact] + public async Task ActivityClient_ActivityFromIdentityOverridesFallbackIdentity() + { + CapturingHttpMessageHandler handler = new(); + AgenticIdentity fromIdentity = new() + { + AgenticAppId = "from-agentic-app-id", + AgenticUserId = "from-agentic-user-id", + AgenticAppBlueprintId = "from-agentic-blueprint-id", + TenantId = "from-tenant-id" + }; + ActivityClient activities = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations.Activities; + + await activities.CreateAsync("conversation-id", CreateActivity(fromIdentity), ExplicitIdentity()); + await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity(fromIdentity), ExplicitIdentity()); + + Assert.Equal(2, handler.Requests.Count); + Assert.All(handler.Requests, request => + { + AssertIdentity(fromIdentity, request.AgenticIdentity); + Assert.Equal("from-bot-id", request.BotAppId); + }); + } + + [Fact] + public void GetAgenticIdentity_DefaultsBlueprintAndTenant() + { + TeamsBotApplication app = CreateApp(options: new TeamsBotApplicationOptions + { + AppId = "app-id", + TenantId = "tenant-id" + }); + + AgenticIdentity identity = app.GetAgenticIdentity("agentic-app-id", "agentic-user-id"); + + Assert.Equal("agentic-app-id", identity.AgenticAppId); + Assert.Equal("agentic-user-id", identity.AgenticUserId); + Assert.Equal("app-id", identity.AgenticAppBlueprintId); + Assert.Equal("tenant-id", identity.TenantId); + } + + [Fact] + public void GetAgenticIdentity_ExplicitValuesOverrideDefaults() + { + TeamsBotApplication app = CreateApp(options: new TeamsBotApplicationOptions + { + AppId = "app-id", + TenantId = "tenant-id" + }); + + AgenticIdentity identity = app.GetAgenticIdentity( + "agentic-app-id", + "agentic-user-id", + tenantId: "explicit-tenant-id", + agenticAppBlueprintId: "explicit-blueprint-id"); + + Assert.Equal("explicit-blueprint-id", identity.AgenticAppBlueprintId); + Assert.Equal("explicit-tenant-id", identity.TenantId); + } + + [Fact] + public void GetAgenticIdentity_LeavesTenantNullWhenUnconfigured() + { + TeamsBotApplication app = CreateApp(options: new TeamsBotApplicationOptions { AppId = "app-id" }); + + AgenticIdentity identity = app.GetAgenticIdentity("agentic-app-id", "agentic-user-id"); + + Assert.Equal("app-id", identity.AgenticAppBlueprintId); + Assert.Null(identity.TenantId); + } + + private static TeamsBotApplication CreateApp(CapturingHttpMessageHandler? handler = null, TeamsBotApplicationOptions? options = null) + => new( + CreateApiClient(handler ?? new CapturingHttpMessageHandler()), + new HttpContextAccessor(), + NullLogger.Instance, + options ?? new TeamsBotApplicationOptions { AppId = "app-id" }); + + private static ApiClient CreateApiClient(CapturingHttpMessageHandler handler) + { + HttpClient httpClient = new(handler); + ConversationClient conversationClient = new(httpClient, NullLogger.Instance); + UserTokenClient userTokenClient = new(new HttpClient(), new ConfigurationBuilder().Build(), NullLogger.Instance); + return new ApiClient(httpClient, conversationClient, userTokenClient); + } + + private static CoreActivity CreateActivity(AgenticIdentity? fromIdentity = null) + { + CoreActivity activity = new() + { + Type = ActivityType.Message + }; + + if (fromIdentity is not null) + { + activity.From = new ChannelAccount + { + Id = "28:from-bot-id", + AgenticAppId = fromIdentity.AgenticAppId, + AgenticUserId = fromIdentity.AgenticUserId, + AgenticAppBlueprintId = fromIdentity.AgenticAppBlueprintId, + TenantId = fromIdentity.TenantId + }; + } + + return activity; + } + + private static CoreActivity CreateTargetedActivity() + => new() + { + Type = ActivityType.Message, + Recipient = new ChannelAccount { Id = "recipient-id" } + }; + + private static AgenticIdentity DefaultIdentity() + => new() + { + AgenticAppId = "default-agentic-app-id", + AgenticUserId = "default-agentic-user-id", + AgenticAppBlueprintId = "default-agentic-blueprint-id", + TenantId = "default-tenant-id" + }; + + private static AgenticIdentity ExplicitIdentity() + => new() + { + AgenticAppId = "explicit-agentic-app-id", + AgenticUserId = "explicit-agentic-user-id", + AgenticAppBlueprintId = "explicit-agentic-blueprint-id", + TenantId = "explicit-tenant-id" + }; + + private static void AssertIdentity(AgenticIdentity expected, AgenticIdentity? actual) + { + Assert.NotNull(actual); + Assert.Equal(expected.AgenticAppId, actual!.AgenticAppId); + Assert.Equal(expected.AgenticUserId, actual.AgenticUserId); + Assert.Equal(expected.AgenticAppBlueprintId, actual.AgenticAppBlueprintId); + Assert.Equal(expected.TenantId, actual.TenantId); + } + + private sealed class CapturingHttpMessageHandler : HttpMessageHandler + { + private static readonly HttpRequestOptionsKey AgenticIdentityKey = new(BotRequestContext.AgenticIdentityKey); + private static readonly HttpRequestOptionsKey BotAppIdKey = new(BotRequestContext.BotAppIdKey); + + public List Requests { get; } = []; + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + request.Options.TryGetValue(AgenticIdentityKey, out object? agenticIdentity); + request.Options.TryGetValue(BotAppIdKey, out object? botAppId); + Requests.Add(new CapturedRequest( + request.Method, + request.RequestUri?.ToString() ?? string.Empty, + agenticIdentity as AgenticIdentity, + botAppId as string)); + + return Task.FromResult(CreateResponse(request)); + } + + private static HttpResponseMessage CreateResponse(HttpRequestMessage request) + { + string path = request.RequestUri?.AbsolutePath ?? string.Empty; + string json = path.Contains("/pagedmembers", StringComparison.Ordinal) + ? "{\"members\":[{\"id\":\"member-id\"}]}" + : path.EndsWith("/members", StringComparison.Ordinal) + ? "[{\"id\":\"member-id\"}]" + : path.Contains("/members/", StringComparison.Ordinal) + ? "{\"id\":\"member-id\"}" + : path.EndsWith("/conversations", StringComparison.Ordinal) && request.Method == HttpMethod.Post + ? "{\"id\":\"conversation-id\"}" + : path.Contains("/activities", StringComparison.Ordinal) && request.Method != HttpMethod.Delete + ? "{\"id\":\"activity-id\"}" + : path.Contains("/teams/", StringComparison.Ordinal) && path.EndsWith("/conversations", StringComparison.Ordinal) + ? "{\"conversations\":[]}" + : path.Contains("/teams/", StringComparison.Ordinal) + ? "{\"id\":\"team-id\"}" + : path.Contains("/meetings/", StringComparison.Ordinal) && path.Contains("/participants/", StringComparison.Ordinal) + ? "{\"user\":{\"id\":\"participant-id\"}}" + : path.Contains("/meetings/", StringComparison.Ordinal) + ? "{\"id\":\"meeting-id\"}" + : string.Empty; + + HttpResponseMessage response = new(HttpStatusCode.OK); + if (json.Length > 0) + { + response.Content = new StringContent(json, Encoding.UTF8, "application/json"); + } + + return response; + } + } + + private sealed record CapturedRequest(HttpMethod Method, string Url, AgenticIdentity? AgenticIdentity, string? BotAppId); +} diff --git a/core/test/Microsoft.Teams.Apps.UnitTests/TeamsBotApplicationHostingExtensionsTests.cs b/core/test/Microsoft.Teams.Apps.UnitTests/TeamsBotApplicationHostingExtensionsTests.cs index 7acabc0d..9369c813 100644 --- a/core/test/Microsoft.Teams.Apps.UnitTests/TeamsBotApplicationHostingExtensionsTests.cs +++ b/core/test/Microsoft.Teams.Apps.UnitTests/TeamsBotApplicationHostingExtensionsTests.cs @@ -42,6 +42,7 @@ public void AddTeamsBotApplication_RegistersAllRequiredServices() Assert.NotNull(serviceProvider.GetRequiredService()); TeamsBotApplicationOptions options = serviceProvider.GetRequiredService(); Assert.Equal("teams-bundle-client-id", options.AppId); + Assert.Equal("teams-bundle-tenant-id", options.TenantId); BotApplicationOptions botOptions = serviceProvider.GetRequiredService(); Assert.Equal("teams-bundle-client-id", botOptions.AppId); From 932a33764bed38a8581a7d2fb22448c3895e8468 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Thu, 9 Jul 2026 18:25:44 -0700 Subject: [PATCH 02/23] Hide request context from app API defaults Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Api/Clients/ActivityClient.cs | 23 ++++++++-------- .../Api/Clients/ApiClient.cs | 26 +++++++++---------- .../Api/Clients/ApiRequestContext.cs | 12 ++++----- .../Api/Clients/ConversationApiClient.cs | 15 +++++------ .../Api/Clients/MeetingClient.cs | 8 +++--- .../Api/Clients/MemberClient.cs | 13 +++++----- .../Api/Clients/ReactionClient.cs | 11 ++++---- .../Api/Clients/TeamClient.cs | 8 +++--- 8 files changed, 56 insertions(+), 60 deletions(-) diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs index e2b1b2c5..c2e785dd 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Teams.Apps.Schema; using Microsoft.Teams.Core; -using Microsoft.Teams.Core.Http; using Microsoft.Teams.Core.Schema; using CoreConversationClient = Microsoft.Teams.Core.ConversationClient; @@ -20,13 +19,13 @@ public class ActivityClient { private readonly CoreConversationClient _client; private readonly Uri _serviceUrl; - private readonly BotRequestContext? _defaultRequestContext; + private readonly AgenticIdentity? _defaultAgenticIdentity; - internal ActivityClient(Uri serviceUrl, CoreConversationClient client, BotRequestContext? defaultRequestContext = null) + internal ActivityClient(Uri serviceUrl, CoreConversationClient client, AgenticIdentity? defaultAgenticIdentity = null) { _serviceUrl = serviceUrl; _client = client; - _defaultRequestContext = defaultRequestContext; + _defaultAgenticIdentity = defaultAgenticIdentity; } /// @@ -43,7 +42,7 @@ internal ActivityClient(Uri serviceUrl, CoreConversationClient client, BotReques ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -59,7 +58,7 @@ public Task UpdateAsync(string conversationId, string id { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; - return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: ApiRequestContext.ResolveActivity(_defaultRequestContext, agenticIdentity, activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: ApiRequestContext.ResolveActivity(_defaultAgenticIdentity, agenticIdentity, activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -77,7 +76,7 @@ public Task UpdateAsync(string conversationId, string id activity.ReplyToId = id; activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -85,7 +84,7 @@ public Task UpdateAsync(string conversationId, string id /// public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -93,7 +92,7 @@ public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agent /// public async Task> GetMembersAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); + IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; } @@ -120,7 +119,7 @@ public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agent { activity.Recipient.IsTargeted = true; } - return _client.SendActivityAsync(activity, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -138,7 +137,7 @@ public Task UpdateTargetedAsync(string conversationId, s { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; - return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: ApiRequestContext.ResolveActivity(_defaultRequestContext, agenticIdentity, activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: ApiRequestContext.ResolveActivity(_defaultAgenticIdentity, agenticIdentity, activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -146,6 +145,6 @@ public Task UpdateTargetedAsync(string conversationId, s /// public Task DeleteTargetedAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs index 6ef83e08..e2df0f86 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs @@ -33,7 +33,7 @@ public class ApiClient internal CoreUserTokenClient UserTokenClient { get; } - internal BotRequestContext? DefaultRequestContext { get; } + internal AgenticIdentity? DefaultAgenticIdentity { get; } /// /// The service URL used by this client. @@ -83,7 +83,7 @@ internal ApiClient(HttpClient httpClient, CoreConversationClient conversationCli _http = new BotHttpClient(httpClient, logger); ConversationClient = conversationClient; UserTokenClient = userTokenClient; - DefaultRequestContext = null; + DefaultAgenticIdentity = null; // ServiceUrl-dependent sub-clients require ForServiceUrl() before use ServiceUrl = null!; @@ -111,24 +111,24 @@ internal ApiClient(Uri serviceUrl, HttpClient httpClient, CoreConversationClient _http = new BotHttpClient(httpClient, logger); ConversationClient = conversationClient; UserTokenClient = userTokenClient; - DefaultRequestContext = BotRequestContext.FromAgenticIdentity(defaultAgenticIdentity); + DefaultAgenticIdentity = defaultAgenticIdentity; ServiceUrl = serviceUrl; - Conversations = new ConversationApiClient(serviceUrl, conversationClient, DefaultRequestContext); - Teams = new TeamClient(serviceUrl.ToString(), _http, DefaultRequestContext); - Meetings = new MeetingClient(serviceUrl.ToString(), _http, DefaultRequestContext); + Conversations = new ConversationApiClient(serviceUrl, conversationClient, DefaultAgenticIdentity); + Teams = new TeamClient(serviceUrl.ToString(), _http, DefaultAgenticIdentity); + Meetings = new MeetingClient(serviceUrl.ToString(), _http, DefaultAgenticIdentity); } // Private constructor for ForServiceUrl — shares BotHttpClient, ConversationClient, and UserTokenClient - private ApiClient(BotHttpClient http, CoreConversationClient conversationClient, CoreUserTokenClient userTokenClient, Uri serviceUrl, BotRequestContext? defaultRequestContext) + private ApiClient(BotHttpClient http, CoreConversationClient conversationClient, CoreUserTokenClient userTokenClient, Uri serviceUrl, AgenticIdentity? defaultAgenticIdentity) { _http = http; ConversationClient = conversationClient; UserTokenClient = userTokenClient; - DefaultRequestContext = defaultRequestContext; + DefaultAgenticIdentity = defaultAgenticIdentity; ServiceUrl = serviceUrl; - Conversations = new ConversationApiClient(serviceUrl, conversationClient, defaultRequestContext); - Teams = new TeamClient(serviceUrl.ToString(), http, defaultRequestContext); - Meetings = new MeetingClient(serviceUrl.ToString(), http, defaultRequestContext); + Conversations = new ConversationApiClient(serviceUrl, conversationClient, defaultAgenticIdentity); + Teams = new TeamClient(serviceUrl.ToString(), http, defaultAgenticIdentity); + Meetings = new MeetingClient(serviceUrl.ToString(), http, defaultAgenticIdentity); } /// @@ -140,7 +140,7 @@ private ApiClient(BotHttpClient http, CoreConversationClient conversationClient, public virtual ApiClient ForServiceUrl(Uri serviceUrl) { ArgumentNullException.ThrowIfNull(serviceUrl); - return new ApiClient(_http, ConversationClient, UserTokenClient, serviceUrl, defaultRequestContext: null); + return new ApiClient(_http, ConversationClient, UserTokenClient, serviceUrl, defaultAgenticIdentity: null); } /// @@ -153,6 +153,6 @@ public virtual ApiClient ForServiceUrl(Uri serviceUrl) public virtual ApiClient ForServiceUrl(Uri serviceUrl, AgenticIdentity? defaultAgenticIdentity) { ArgumentNullException.ThrowIfNull(serviceUrl); - return new ApiClient(_http, ConversationClient, UserTokenClient, serviceUrl, BotRequestContext.FromAgenticIdentity(defaultAgenticIdentity)); + return new ApiClient(_http, ConversationClient, UserTokenClient, serviceUrl, defaultAgenticIdentity); } } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiRequestContext.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiRequestContext.cs index 7f5fcb19..72c9a19b 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiRequestContext.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiRequestContext.cs @@ -9,15 +9,15 @@ namespace Microsoft.Teams.Apps.Api.Clients; internal static class ApiRequestContext { - public static BotRequestContext? Resolve(BotRequestContext? defaultRequestContext, AgenticIdentity? agenticIdentity) - => BotRequestContext.Merge(defaultRequestContext, BotRequestContext.FromAgenticIdentity(agenticIdentity)); + public static BotRequestContext? Resolve(AgenticIdentity? defaultAgenticIdentity, AgenticIdentity? agenticIdentity) + => BotRequestContext.FromAgenticIdentity(agenticIdentity ?? defaultAgenticIdentity); - public static BotRequestContext? ResolveActivity(BotRequestContext? defaultRequestContext, AgenticIdentity? agenticIdentity, CoreActivity? activity) - => BotRequestContext.Merge(Resolve(defaultRequestContext, agenticIdentity), BotRequestContext.FromActivity(activity)); + public static BotRequestContext? ResolveActivity(AgenticIdentity? defaultAgenticIdentity, AgenticIdentity? agenticIdentity, CoreActivity? activity) + => BotRequestContext.Merge(Resolve(defaultAgenticIdentity, agenticIdentity), BotRequestContext.FromActivity(activity)); - public static BotRequestOptions? CreateOptions(BotRequestContext? defaultRequestContext, AgenticIdentity? agenticIdentity) + public static BotRequestOptions? CreateOptions(AgenticIdentity? defaultAgenticIdentity, AgenticIdentity? agenticIdentity) { - BotRequestContext? requestContext = Resolve(defaultRequestContext, agenticIdentity); + BotRequestContext? requestContext = Resolve(defaultAgenticIdentity, agenticIdentity); return requestContext is null ? null : new BotRequestOptions { RequestContext = requestContext }; } } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs index 71ad03c5..ea841649 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs @@ -5,7 +5,6 @@ using System.Runtime.CompilerServices; using Microsoft.Teams.Apps.Schema; using Microsoft.Teams.Core; -using Microsoft.Teams.Core.Http; using Microsoft.Teams.Core.Schema; using CoreConversationClient = Microsoft.Teams.Core.ConversationClient; @@ -20,7 +19,7 @@ public class ConversationApiClient { private readonly CoreConversationClient _client; private readonly Uri _serviceUrl; - private readonly BotRequestContext? _defaultRequestContext; + private readonly AgenticIdentity? _defaultAgenticIdentity; /// /// Client for activity operations. @@ -40,15 +39,15 @@ public class ConversationApiClient [Obsolete("Use ConversationApiClient.AddReactionAsync and ConversationApiClient.DeleteReactionAsync instead.")] public ReactionClient Reactions { get; } - internal ConversationApiClient(Uri serviceUrl, CoreConversationClient client, BotRequestContext? defaultRequestContext = null) + internal ConversationApiClient(Uri serviceUrl, CoreConversationClient client, AgenticIdentity? defaultAgenticIdentity = null) { _serviceUrl = serviceUrl; _client = client; - _defaultRequestContext = defaultRequestContext; + _defaultAgenticIdentity = defaultAgenticIdentity; #pragma warning disable CS0618 // Suppress obsolete warnings for backward-compatible initialization - Activities = new ActivityClient(serviceUrl, client, defaultRequestContext); - Members = new MemberClient(serviceUrl, client, defaultRequestContext); - Reactions = new ReactionClient(serviceUrl, client, defaultRequestContext); + Activities = new ActivityClient(serviceUrl, client, defaultAgenticIdentity); + Members = new MemberClient(serviceUrl, client, defaultAgenticIdentity); + Reactions = new ReactionClient(serviceUrl, client, defaultAgenticIdentity); #pragma warning restore CS0618 } @@ -57,7 +56,7 @@ internal ConversationApiClient(Uri serviceUrl, CoreConversationClient client, Bo /// public Task CreateAsync(ConversationParameters request, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.CreateConversationAsync(request, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.CreateConversationAsync(request, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } #region Activity Methods diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs index ee527417..6f21206a 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs @@ -14,13 +14,13 @@ public class MeetingClient { private readonly BotHttpClient _http; private readonly string _serviceUrl; - private readonly BotRequestContext? _defaultRequestContext; + private readonly AgenticIdentity? _defaultAgenticIdentity; - internal MeetingClient(string serviceUrl, BotHttpClient http, BotRequestContext? defaultRequestContext = null) + internal MeetingClient(string serviceUrl, BotHttpClient http, AgenticIdentity? defaultAgenticIdentity = null) { _serviceUrl = serviceUrl.TrimEnd('/'); _http = http; - _defaultRequestContext = defaultRequestContext; + _defaultAgenticIdentity = defaultAgenticIdentity; } /// @@ -42,7 +42,7 @@ internal MeetingClient(string serviceUrl, BotHttpClient http, BotRequestContext? } private BotRequestOptions? CreateRequestOptions(AgenticIdentity? agenticIdentity) => - ApiRequestContext.CreateOptions(_defaultRequestContext, agenticIdentity); + ApiRequestContext.CreateOptions(_defaultAgenticIdentity, agenticIdentity); } /// diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/MemberClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/MemberClient.cs index a6917b7f..95451e66 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/MemberClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/MemberClient.cs @@ -4,7 +4,6 @@ using System.Runtime.CompilerServices; using Microsoft.Teams.Apps.Schema; using Microsoft.Teams.Core; -using Microsoft.Teams.Core.Http; using Microsoft.Teams.Core.Schema; using CoreConversationClient = Microsoft.Teams.Core.ConversationClient; @@ -20,13 +19,13 @@ public class MemberClient { private readonly CoreConversationClient _client; private readonly Uri _serviceUrl; - private readonly BotRequestContext? _defaultRequestContext; + private readonly AgenticIdentity? _defaultAgenticIdentity; - internal MemberClient(Uri serviceUrl, CoreConversationClient client, BotRequestContext? defaultRequestContext = null) + internal MemberClient(Uri serviceUrl, CoreConversationClient client, AgenticIdentity? defaultAgenticIdentity = null) { _serviceUrl = serviceUrl; _client = client; - _defaultRequestContext = defaultRequestContext; + _defaultAgenticIdentity = defaultAgenticIdentity; } /// @@ -35,7 +34,7 @@ internal MemberClient(Uri serviceUrl, CoreConversationClient client, BotRequestC [Obsolete("Use GetPagedAsync instead.")] public async Task> GetAsync(string conversationId, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - IList members = await _client.GetConversationMembersAsync(conversationId, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); + IList members = await _client.GetConversationMembersAsync(conversationId, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; } @@ -55,7 +54,7 @@ public async Task GetPagedAsync( _serviceUrl, pageSize, continuationToken, - requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), + requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); PagedTeamsMembersResult result = new(); @@ -114,7 +113,7 @@ public async IAsyncEnumerable GetAllAsync( /// public Task GetByIdAsync(string conversationId, string memberId, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) where T : ChannelAccount { - return _client.GetConversationMemberAsync(conversationId, memberId, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.GetConversationMemberAsync(conversationId, memberId, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ReactionClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ReactionClient.cs index 8c3c5b78..c454d158 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ReactionClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ReactionClient.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.Teams.Core.Http; using Microsoft.Teams.Core.Schema; using CoreConversationClient = Microsoft.Teams.Core.ConversationClient; @@ -17,13 +16,13 @@ public class ReactionClient { private readonly CoreConversationClient _client; private readonly Uri _serviceUrl; - private readonly BotRequestContext? _defaultRequestContext; + private readonly AgenticIdentity? _defaultAgenticIdentity; - internal ReactionClient(Uri serviceUrl, CoreConversationClient client, BotRequestContext? defaultRequestContext = null) + internal ReactionClient(Uri serviceUrl, CoreConversationClient client, AgenticIdentity? defaultAgenticIdentity = null) { _serviceUrl = serviceUrl; _client = client; - _defaultRequestContext = defaultRequestContext; + _defaultAgenticIdentity = defaultAgenticIdentity; } /// @@ -37,7 +36,7 @@ internal ReactionClient(Uri serviceUrl, CoreConversationClient client, BotReques /// A to observe while waiting for the task to complete. public Task AddAsync(string conversationId, string activityId, string reactionType, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.AddReactionAsync(conversationId, activityId, reactionType, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.AddReactionAsync(conversationId, activityId, reactionType, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -51,6 +50,6 @@ public Task AddAsync(string conversationId, string activityId, string reactionTy /// A to observe while waiting for the task to complete. public Task DeleteAsync(string conversationId, string activityId, string reactionType, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.DeleteReactionAsync(conversationId, activityId, reactionType, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultRequestContext, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteReactionAsync(conversationId, activityId, reactionType, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs index 346a9567..ed06c0d4 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs @@ -15,13 +15,13 @@ public class TeamClient { private readonly BotHttpClient _http; private readonly string _serviceUrl; - private readonly BotRequestContext? _defaultRequestContext; + private readonly AgenticIdentity? _defaultAgenticIdentity; - internal TeamClient(string serviceUrl, BotHttpClient http, BotRequestContext? defaultRequestContext = null) + internal TeamClient(string serviceUrl, BotHttpClient http, AgenticIdentity? defaultAgenticIdentity = null) { _serviceUrl = serviceUrl.TrimEnd('/'); _http = http; - _defaultRequestContext = defaultRequestContext; + _defaultAgenticIdentity = defaultAgenticIdentity; } /// @@ -44,7 +44,7 @@ internal TeamClient(string serviceUrl, BotHttpClient http, BotRequestContext? de } private BotRequestOptions? CreateRequestOptions(AgenticIdentity? agenticIdentity) => - ApiRequestContext.CreateOptions(_defaultRequestContext, agenticIdentity); + ApiRequestContext.CreateOptions(_defaultAgenticIdentity, agenticIdentity); private sealed class ConversationListResponse { From dbf4f55eb057c801b4467627a2497b270109373c Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Thu, 9 Jul 2026 18:29:42 -0700 Subject: [PATCH 03/23] Simplify activity identity parameters Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Api/Clients/ActivityClient.cs | 43 +++---------------- .../AgenticIdentityDefaultsTests.cs | 14 +++--- 2 files changed, 12 insertions(+), 45 deletions(-) diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs index c2e785dd..aae7361e 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs @@ -31,13 +31,7 @@ internal ActivityClient(Uri serviceUrl, CoreConversationClient client, AgenticId /// /// Create a new activity in a conversation. /// - public Task CreateAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => CreateAsync(conversationId, activity, agenticIdentity: null, additionalHeaders: additionalHeaders, cancellationToken: cancellationToken); - - /// - /// Create a new activity in a conversation. - /// - public Task CreateAsync(string conversationId, CoreActivity activity, AgenticIdentity? agenticIdentity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task CreateAsync(string conversationId, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -48,13 +42,7 @@ internal ActivityClient(Uri serviceUrl, CoreConversationClient client, AgenticId /// /// Update an existing activity in a conversation. /// - public Task UpdateAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => UpdateAsync(conversationId, id, activity, agenticIdentity: null, additionalHeaders: additionalHeaders, cancellationToken: cancellationToken); - - /// - /// Update an existing activity in a conversation. - /// - public Task UpdateAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task UpdateAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -64,13 +52,7 @@ public Task UpdateAsync(string conversationId, string id /// /// Reply to an existing activity in a conversation. /// - public Task ReplyAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => ReplyAsync(conversationId, id, activity, agenticIdentity: null, additionalHeaders: additionalHeaders, cancellationToken: cancellationToken); - - /// - /// Reply to an existing activity in a conversation. - /// - public Task ReplyAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task ReplyAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ReplyToId = id; @@ -101,15 +83,7 @@ public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agent /// Targeted activities are only visible to the specified recipient. /// [Experimental("ExperimentalTeamsTargeted")] - public Task CreateTargetedAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => CreateTargetedAsync(conversationId, activity, agenticIdentity: null, additionalHeaders: additionalHeaders, cancellationToken: cancellationToken); - - /// - /// Create a new targeted activity in a conversation. - /// Targeted activities are only visible to the specified recipient. - /// - [Experimental("ExperimentalTeamsTargeted")] - public Task CreateTargetedAsync(string conversationId, CoreActivity activity, AgenticIdentity? agenticIdentity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task CreateTargetedAsync(string conversationId, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -126,14 +100,7 @@ public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agent /// Update an existing targeted activity in a conversation. /// [Experimental("ExperimentalTeamsTargeted")] - public Task UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => UpdateTargetedAsync(conversationId, id, activity, agenticIdentity: null, additionalHeaders: additionalHeaders, cancellationToken: cancellationToken); - - /// - /// Update an existing targeted activity in a conversation. - /// - [Experimental("ExperimentalTeamsTargeted")] - public Task UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; diff --git a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs index 804fa373..f7834a48 100644 --- a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs +++ b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs @@ -129,13 +129,13 @@ public async Task ActivityClient_ExplicitIdentityOverridesDefaultIdentity() AgenticIdentity explicitIdentity = ExplicitIdentity(); ActivityClient activities = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations.Activities; - await activities.CreateAsync("conversation-id", CreateActivity(), explicitIdentity); - await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity(), explicitIdentity); - await activities.ReplyAsync("conversation-id", "activity-id", CreateActivity(), explicitIdentity); + await activities.CreateAsync("conversation-id", CreateActivity(), agenticIdentity: explicitIdentity); + await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity(), agenticIdentity: explicitIdentity); + await activities.ReplyAsync("conversation-id", "activity-id", CreateActivity(), agenticIdentity: explicitIdentity); await activities.DeleteAsync("conversation-id", "activity-id", explicitIdentity); await activities.GetMembersAsync("conversation-id", "activity-id", explicitIdentity); - await activities.CreateTargetedAsync("conversation-id", CreateTargetedActivity(), explicitIdentity); - await activities.UpdateTargetedAsync("conversation-id", "activity-id", CreateActivity(), explicitIdentity); + await activities.CreateTargetedAsync("conversation-id", CreateTargetedActivity(), agenticIdentity: explicitIdentity); + await activities.UpdateTargetedAsync("conversation-id", "activity-id", CreateActivity(), agenticIdentity: explicitIdentity); await activities.DeleteTargetedAsync("conversation-id", "activity-id", explicitIdentity); Assert.Equal(8, handler.Requests.Count); @@ -155,8 +155,8 @@ public async Task ActivityClient_ActivityFromIdentityOverridesFallbackIdentity() }; ActivityClient activities = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations.Activities; - await activities.CreateAsync("conversation-id", CreateActivity(fromIdentity), ExplicitIdentity()); - await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity(fromIdentity), ExplicitIdentity()); + await activities.CreateAsync("conversation-id", CreateActivity(fromIdentity), agenticIdentity: ExplicitIdentity()); + await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity(fromIdentity), agenticIdentity: ExplicitIdentity()); Assert.Equal(2, handler.Requests.Count); Assert.All(handler.Requests, request => From 96505f3885c9cc79ad5c13a9faf615f032af23fe Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 11:16:48 -0700 Subject: [PATCH 04/23] Inline app request context resolution Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Api/Clients/ActivityClient.cs | 17 +++++++------- .../Api/Clients/ApiRequestContext.cs | 23 ------------------- .../Api/Clients/ConversationApiClient.cs | 3 ++- .../Api/Clients/MeetingClient.cs | 4 +++- .../Api/Clients/MemberClient.cs | 7 +++--- .../Api/Clients/ReactionClient.cs | 5 ++-- .../Api/Clients/TeamClient.cs | 4 +++- 7 files changed, 24 insertions(+), 39 deletions(-) delete mode 100644 core/src/Microsoft.Teams.Apps/Api/Clients/ApiRequestContext.cs diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs index aae7361e..1961d443 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Teams.Apps.Schema; using Microsoft.Teams.Core; +using Microsoft.Teams.Core.Http; using Microsoft.Teams.Core.Schema; using CoreConversationClient = Microsoft.Teams.Core.ConversationClient; @@ -36,7 +37,7 @@ internal ActivityClient(Uri serviceUrl, CoreConversationClient client, AgenticId ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -46,7 +47,7 @@ public Task UpdateAsync(string conversationId, string id { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; - return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: ApiRequestContext.ResolveActivity(_defaultAgenticIdentity, agenticIdentity, activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.Merge(BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), BotRequestContext.FromActivity(activity)), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -58,7 +59,7 @@ public Task UpdateAsync(string conversationId, string id activity.ReplyToId = id; activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -66,7 +67,7 @@ public Task UpdateAsync(string conversationId, string id /// public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -74,7 +75,7 @@ public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agent /// public async Task> GetMembersAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); + IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; } @@ -93,7 +94,7 @@ public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agent { activity.Recipient.IsTargeted = true; } - return _client.SendActivityAsync(activity, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -104,7 +105,7 @@ public Task UpdateTargetedAsync(string conversationId, s { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; - return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: ApiRequestContext.ResolveActivity(_defaultAgenticIdentity, agenticIdentity, activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.Merge(BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), BotRequestContext.FromActivity(activity)), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -112,6 +113,6 @@ public Task UpdateTargetedAsync(string conversationId, s /// public Task DeleteTargetedAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiRequestContext.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiRequestContext.cs deleted file mode 100644 index 72c9a19b..00000000 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiRequestContext.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.Teams.Core; -using Microsoft.Teams.Core.Http; -using Microsoft.Teams.Core.Schema; - -namespace Microsoft.Teams.Apps.Api.Clients; - -internal static class ApiRequestContext -{ - public static BotRequestContext? Resolve(AgenticIdentity? defaultAgenticIdentity, AgenticIdentity? agenticIdentity) - => BotRequestContext.FromAgenticIdentity(agenticIdentity ?? defaultAgenticIdentity); - - public static BotRequestContext? ResolveActivity(AgenticIdentity? defaultAgenticIdentity, AgenticIdentity? agenticIdentity, CoreActivity? activity) - => BotRequestContext.Merge(Resolve(defaultAgenticIdentity, agenticIdentity), BotRequestContext.FromActivity(activity)); - - public static BotRequestOptions? CreateOptions(AgenticIdentity? defaultAgenticIdentity, AgenticIdentity? agenticIdentity) - { - BotRequestContext? requestContext = Resolve(defaultAgenticIdentity, agenticIdentity); - return requestContext is null ? null : new BotRequestOptions { RequestContext = requestContext }; - } -} diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs index ea841649..39ca0572 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using Microsoft.Teams.Apps.Schema; using Microsoft.Teams.Core; +using Microsoft.Teams.Core.Http; using Microsoft.Teams.Core.Schema; using CoreConversationClient = Microsoft.Teams.Core.ConversationClient; @@ -56,7 +57,7 @@ internal ConversationApiClient(Uri serviceUrl, CoreConversationClient client, Ag /// public Task CreateAsync(ConversationParameters request, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.CreateConversationAsync(request, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.CreateConversationAsync(request, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } #region Activity Methods diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs index 6f21206a..50275287 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs @@ -42,7 +42,9 @@ internal MeetingClient(string serviceUrl, BotHttpClient http, AgenticIdentity? d } private BotRequestOptions? CreateRequestOptions(AgenticIdentity? agenticIdentity) => - ApiRequestContext.CreateOptions(_defaultAgenticIdentity, agenticIdentity); + BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity) is { } requestContext + ? new BotRequestOptions { RequestContext = requestContext } + : null; } /// diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/MemberClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/MemberClient.cs index 95451e66..bb8d23fd 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/MemberClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/MemberClient.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using Microsoft.Teams.Apps.Schema; using Microsoft.Teams.Core; +using Microsoft.Teams.Core.Http; using Microsoft.Teams.Core.Schema; using CoreConversationClient = Microsoft.Teams.Core.ConversationClient; @@ -34,7 +35,7 @@ internal MemberClient(Uri serviceUrl, CoreConversationClient client, AgenticIden [Obsolete("Use GetPagedAsync instead.")] public async Task> GetAsync(string conversationId, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - IList members = await _client.GetConversationMembersAsync(conversationId, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); + IList members = await _client.GetConversationMembersAsync(conversationId, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; } @@ -54,7 +55,7 @@ public async Task GetPagedAsync( _serviceUrl, pageSize, continuationToken, - requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), + requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); PagedTeamsMembersResult result = new(); @@ -113,7 +114,7 @@ public async IAsyncEnumerable GetAllAsync( /// public Task GetByIdAsync(string conversationId, string memberId, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) where T : ChannelAccount { - return _client.GetConversationMemberAsync(conversationId, memberId, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.GetConversationMemberAsync(conversationId, memberId, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ReactionClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ReactionClient.cs index c454d158..3b6e64ed 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ReactionClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ReactionClient.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Teams.Core.Http; using Microsoft.Teams.Core.Schema; using CoreConversationClient = Microsoft.Teams.Core.ConversationClient; @@ -36,7 +37,7 @@ internal ReactionClient(Uri serviceUrl, CoreConversationClient client, AgenticId /// A to observe while waiting for the task to complete. public Task AddAsync(string conversationId, string activityId, string reactionType, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.AddReactionAsync(conversationId, activityId, reactionType, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.AddReactionAsync(conversationId, activityId, reactionType, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -50,6 +51,6 @@ public Task AddAsync(string conversationId, string activityId, string reactionTy /// A to observe while waiting for the task to complete. public Task DeleteAsync(string conversationId, string activityId, string reactionType, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.DeleteReactionAsync(conversationId, activityId, reactionType, _serviceUrl, requestContext: ApiRequestContext.Resolve(_defaultAgenticIdentity, agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteReactionAsync(conversationId, activityId, reactionType, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs index ed06c0d4..9deb67bf 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs @@ -44,7 +44,9 @@ internal TeamClient(string serviceUrl, BotHttpClient http, AgenticIdentity? defa } private BotRequestOptions? CreateRequestOptions(AgenticIdentity? agenticIdentity) => - ApiRequestContext.CreateOptions(_defaultAgenticIdentity, agenticIdentity); + BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity) is { } requestContext + ? new BotRequestOptions { RequestContext = requestContext } + : null; private sealed class ConversationListResponse { From cbc4e164a4b02daf7b297f2ec3f569fbd68a5fcb Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 11:25:47 -0700 Subject: [PATCH 05/23] Restore ApiClient doc links Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs index e2df0f86..552709b9 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs @@ -20,7 +20,7 @@ namespace Microsoft.Teams.Apps.Api.Clients; /// /// /// DI-friendly (no serviceUrl) — Use ApiClient(HttpClient, ConversationClient, UserTokenClient, ILogger) -/// and call ForServiceUrl per-request to create a scoped instance. +/// and call per-request to create a scoped instance. /// Fully initialized — Use ApiClient(Uri, HttpClient, ConversationClient, UserTokenClient, ILogger) /// when the service URL is known upfront. /// @@ -38,7 +38,7 @@ public class ApiClient /// /// The service URL used by this client. /// Null when constructed without a service URL (DI-friendly constructor). - /// Call ForServiceUrl to create a scoped instance with a service URL. + /// Call to create a scoped instance with a service URL. /// public virtual Uri ServiceUrl { get; } @@ -67,7 +67,7 @@ public class ApiClient /// /// Creates a new without a service URL (DI-friendly). - /// Use ForServiceUrl to create a scoped instance bound to a specific service URL. + /// Use to create a scoped instance bound to a specific service URL. /// /// An configured with authentication (e.g., via DI with BotAuthenticationHandler). /// The core conversation client for conversation/activity/member operations. From 81879dd15769fa9c087e6329fd712a4d4082036c Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 11:26:40 -0700 Subject: [PATCH 06/23] Add agent XML documentation guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 7 +++++++ CLAUDE.md | 3 +++ 2 files changed, 10 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..3790685f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,7 @@ +# Agent Instructions + +## XML documentation + +- Use `` when referencing types or members so generated docs retain links. +- Use explicit overload references, such as ``, when a member has multiple overloads. +- Use `...` only for inline code formatting when a doc link is not intended. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..9d7c663c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ +# Claude Instructions + +Follow the shared repository agent guidance in `AGENTS.md`. From 8acbf2bf9211b14a5e6658d6cb0eee85ee92479b Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 11:30:44 -0700 Subject: [PATCH 07/23] Remove Claude instruction file Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CLAUDE.md | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 9d7c663c..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,3 +0,0 @@ -# Claude Instructions - -Follow the shared repository agent guidance in `AGENTS.md`. From ca8467e3498a369afbc8d0de9b8e9ec558cdc206 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 11:31:20 -0700 Subject: [PATCH 08/23] Condense XML doc guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3790685f..246c1b52 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,4 @@ ## XML documentation -- Use `` when referencing types or members so generated docs retain links. -- Use explicit overload references, such as ``, when a member has multiple overloads. -- Use `...` only for inline code formatting when a doc link is not intended. +- Prefer `` for linkable XML doc references, including explicit overloads like ``; use `...` only for inline code formatting. From 38c7f36fe2ff47c0c0053ab6eb2ffd9e50c5080c Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 11:38:12 -0700 Subject: [PATCH 09/23] Clarify effective identity request options Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Api/Clients/MeetingClient.cs | 14 ++++++++++---- .../Microsoft.Teams.Apps/Api/Clients/TeamClient.cs | 14 ++++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs index 50275287..0a52d3da 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs @@ -41,10 +41,16 @@ internal MeetingClient(string serviceUrl, BotHttpClient http, AgenticIdentity? d return await _http.SendAsync(HttpMethod.Get, url, body: null, options: CreateRequestOptions(agenticIdentity), cancellationToken).ConfigureAwait(false); } - private BotRequestOptions? CreateRequestOptions(AgenticIdentity? agenticIdentity) => - BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity) is { } requestContext - ? new BotRequestOptions { RequestContext = requestContext } - : null; + private BotRequestOptions? CreateRequestOptions(AgenticIdentity? agenticIdentity) + { + AgenticIdentity? effectiveIdentity = agenticIdentity ?? _defaultAgenticIdentity; + return effectiveIdentity is null + ? null + : new BotRequestOptions + { + RequestContext = BotRequestContext.FromAgenticIdentity(effectiveIdentity) + }; + } } /// diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs index 9deb67bf..e404b991 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs @@ -43,10 +43,16 @@ internal TeamClient(string serviceUrl, BotHttpClient http, AgenticIdentity? defa return response?.Conversations; } - private BotRequestOptions? CreateRequestOptions(AgenticIdentity? agenticIdentity) => - BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity) is { } requestContext - ? new BotRequestOptions { RequestContext = requestContext } - : null; + private BotRequestOptions? CreateRequestOptions(AgenticIdentity? agenticIdentity) + { + AgenticIdentity? effectiveIdentity = agenticIdentity ?? _defaultAgenticIdentity; + return effectiveIdentity is null + ? null + : new BotRequestOptions + { + RequestContext = BotRequestContext.FromAgenticIdentity(effectiveIdentity) + }; + } private sealed class ConversationListResponse { From 5a53f89fb265a71675d2eedc7ccb75926a1ed956 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 11:41:26 -0700 Subject: [PATCH 10/23] Inline Teams request options Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Api/Clients/MeetingClient.cs | 15 ++------------- .../Api/Clients/TeamClient.cs | 15 ++------------- 2 files changed, 4 insertions(+), 26 deletions(-) diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs index 0a52d3da..c8bdd4d5 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs @@ -29,7 +29,7 @@ internal MeetingClient(string serviceUrl, BotHttpClient http, AgenticIdentity? d public async Task GetByIdAsync(string id, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default) { string url = $"{_serviceUrl}/v1/meetings/{Uri.EscapeDataString(id)}"; - return await _http.SendAsync(HttpMethod.Get, url, body: null, options: CreateRequestOptions(agenticIdentity), cancellationToken).ConfigureAwait(false); + return await _http.SendAsync(HttpMethod.Get, url, body: null, options: new BotRequestOptions { RequestContext = BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity) }, cancellationToken).ConfigureAwait(false); } /// @@ -38,18 +38,7 @@ internal MeetingClient(string serviceUrl, BotHttpClient http, AgenticIdentity? d public async Task GetParticipantAsync(string meetingId, string id, string tenantId, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default) { string url = $"{_serviceUrl}/v1/meetings/{Uri.EscapeDataString(meetingId)}/participants/{Uri.EscapeDataString(id)}?tenantId={Uri.EscapeDataString(tenantId)}"; - return await _http.SendAsync(HttpMethod.Get, url, body: null, options: CreateRequestOptions(agenticIdentity), cancellationToken).ConfigureAwait(false); - } - - private BotRequestOptions? CreateRequestOptions(AgenticIdentity? agenticIdentity) - { - AgenticIdentity? effectiveIdentity = agenticIdentity ?? _defaultAgenticIdentity; - return effectiveIdentity is null - ? null - : new BotRequestOptions - { - RequestContext = BotRequestContext.FromAgenticIdentity(effectiveIdentity) - }; + return await _http.SendAsync(HttpMethod.Get, url, body: null, options: new BotRequestOptions { RequestContext = BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity) }, cancellationToken).ConfigureAwait(false); } } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs index e404b991..5e9de7ed 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs @@ -30,7 +30,7 @@ internal TeamClient(string serviceUrl, BotHttpClient http, AgenticIdentity? defa public async Task GetByIdAsync(string id, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default) { string url = $"{_serviceUrl}/v3/teams/{Uri.EscapeDataString(id)}"; - return await _http.SendAsync(HttpMethod.Get, url, body: null, options: CreateRequestOptions(agenticIdentity), cancellationToken).ConfigureAwait(false); + return await _http.SendAsync(HttpMethod.Get, url, body: null, options: new BotRequestOptions { RequestContext = BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity) }, cancellationToken).ConfigureAwait(false); } /// @@ -39,21 +39,10 @@ internal TeamClient(string serviceUrl, BotHttpClient http, AgenticIdentity? defa public async Task?> GetConversationsAsync(string id, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default) { string url = $"{_serviceUrl}/v3/teams/{Uri.EscapeDataString(id)}/conversations"; - ConversationListResponse? response = await _http.SendAsync(HttpMethod.Get, url, body: null, options: CreateRequestOptions(agenticIdentity), cancellationToken).ConfigureAwait(false); + ConversationListResponse? response = await _http.SendAsync(HttpMethod.Get, url, body: null, options: new BotRequestOptions { RequestContext = BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity) }, cancellationToken).ConfigureAwait(false); return response?.Conversations; } - private BotRequestOptions? CreateRequestOptions(AgenticIdentity? agenticIdentity) - { - AgenticIdentity? effectiveIdentity = agenticIdentity ?? _defaultAgenticIdentity; - return effectiveIdentity is null - ? null - : new BotRequestOptions - { - RequestContext = BotRequestContext.FromAgenticIdentity(effectiveIdentity) - }; - } - private sealed class ConversationListResponse { [JsonPropertyName("conversations")] From e0ba4791a464d3b4d871c0e0b7da1f47e56863d9 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 11:50:36 -0700 Subject: [PATCH 11/23] Apply identity defaults to flattened API Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Api/Clients/ConversationApiClient.cs | 43 ++++++---- .../AgenticIdentityDefaultsTests.cs | 78 ++++++++++++++++++- 2 files changed, 100 insertions(+), 21 deletions(-) diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs index 39ca0572..3e791383 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs @@ -65,34 +65,34 @@ public Task CreateAsync(ConversationParameters reque /// /// Create a new activity in a conversation. /// - public Task CreateActivityAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task CreateActivityAsync(string conversationId, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// /// Update an existing activity in a conversation. /// - public Task UpdateActivityAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task UpdateActivityAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; - return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.FromActivity(activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.Merge(BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), BotRequestContext.FromActivity(activity)), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// /// Reply to an existing activity in a conversation. /// - public Task ReplyToActivityAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task ReplyToActivityAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ReplyToId = id; activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -100,7 +100,16 @@ public Task UpdateActivityAsync(string conversationId, s /// public Task DeleteActivityAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + } + + /// + /// Get members of a specific activity. + /// + public async Task> GetActivityMembersAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + { + IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); + return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; } /// @@ -108,7 +117,7 @@ public Task DeleteActivityAsync(string conversationId, string id, AgenticIdentit /// Targeted activities are only visible to the specified recipient. /// [Experimental("ExperimentalTeamsTargeted")] - public Task CreateTargetedActivityAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task CreateTargetedActivityAsync(string conversationId, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -117,18 +126,18 @@ public Task DeleteActivityAsync(string conversationId, string id, AgenticIdentit { activity.Recipient.IsTargeted = true; } - return _client.SendActivityAsync(activity, customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// /// Update an existing targeted activity in a conversation. /// [Experimental("ExperimentalTeamsTargeted")] - public Task UpdateTargetedActivityAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task UpdateTargetedActivityAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; - return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.FromActivity(activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.Merge(BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), BotRequestContext.FromActivity(activity)), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -136,7 +145,7 @@ public Task UpdateTargetedActivityAsync(string conversat /// public Task DeleteTargetedActivityAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } #endregion @@ -149,7 +158,7 @@ public Task DeleteTargetedActivityAsync(string conversationId, string id, Agenti [Obsolete("Use GetMembersPagedAsync instead.")] public async Task> GetMembersAsync(string conversationId, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - IList members = await _client.GetConversationMembersAsync(conversationId, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); + IList members = await _client.GetConversationMembersAsync(conversationId, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; } @@ -169,7 +178,7 @@ public async Task GetMembersPagedAsync( _serviceUrl, pageSize, continuationToken, - requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), + requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); PagedTeamsMembersResult result = new(); @@ -228,7 +237,7 @@ public async IAsyncEnumerable GetAllMembersAsync( /// public Task GetMemberByIdAsync(string conversationId, string memberId, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) where T : ChannelAccount { - return _client.GetConversationMemberAsync(conversationId, memberId, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.GetConversationMemberAsync(conversationId, memberId, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -255,7 +264,7 @@ public Task GetMemberByIdAsync(string conversationId, string memberId, Age /// A to observe while waiting for the task to complete. public Task AddReactionAsync(string conversationId, string activityId, string reactionType, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.AddReactionAsync(conversationId, activityId, reactionType, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.AddReactionAsync(conversationId, activityId, reactionType, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -269,7 +278,7 @@ public Task AddReactionAsync(string conversationId, string activityId, string re /// A to observe while waiting for the task to complete. public Task DeleteReactionAsync(string conversationId, string activityId, string reactionType, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { - return _client.DeleteReactionAsync(conversationId, activityId, reactionType, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteReactionAsync(conversationId, activityId, reactionType, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } #endregion diff --git a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs index f7834a48..f56ded4e 100644 --- a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs +++ b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs @@ -39,7 +39,7 @@ public async Task ContextApi_UsesRecipientAgenticIdentityAsDefault() } }); - await context.Api.Conversations.Members.GetPagedAsync("conversation-id"); + await context.Api.Conversations.GetMembersPagedAsync("conversation-id"); CapturedRequest request = Assert.Single(handler.Requests); AssertIdentity(expected, request.AgenticIdentity); @@ -66,7 +66,7 @@ public async Task ContextApi_ExplicitIdentityOverridesRecipientDefault() } }); - await context.Api.Conversations.Members.GetByIdAsync("conversation-id", "member-id", explicitIdentity); + await context.Api.Conversations.GetMemberByIdAsync("conversation-id", "member-id", explicitIdentity); CapturedRequest request = Assert.Single(handler.Requests); AssertIdentity(explicitIdentity, request.AgenticIdentity); @@ -80,8 +80,8 @@ public async Task ScopedApi_ServiceUrlBoundClientsUseDefaultIdentity() ApiClient api = CreateApiClient(handler).ForServiceUrl(ServiceUrl, expected); await api.Conversations.CreateAsync(new ConversationParameters()); - await api.Conversations.Members.GetByIdAsync("conversation-id", "member-id"); - await api.Conversations.Reactions.AddAsync("conversation-id", "activity-id", "like"); + await api.Conversations.GetMemberByIdAsync("conversation-id", "member-id"); + await api.Conversations.AddReactionAsync("conversation-id", "activity-id", "like"); await api.Teams.GetByIdAsync("team-id"); await api.Meetings.GetByIdAsync("meeting-id"); @@ -102,12 +102,78 @@ public async Task ScopedApi_ExplicitIdentityOverridesDefaultIdentity() AssertIdentity(explicitIdentity, request.AgenticIdentity); } + [Fact] + public async Task ConversationApi_ActivityMethodsUseDefaultIdentityWhenExplicitIdentityIsMissing() + { + CapturingHttpMessageHandler handler = new(); + AgenticIdentity expected = DefaultIdentity(); + ConversationApiClient conversations = CreateApiClient(handler).ForServiceUrl(ServiceUrl, expected).Conversations; + + await conversations.CreateActivityAsync("conversation-id", CreateActivity()); + await conversations.UpdateActivityAsync("conversation-id", "activity-id", CreateActivity()); + await conversations.ReplyToActivityAsync("conversation-id", "activity-id", CreateActivity()); + await conversations.DeleteActivityAsync("conversation-id", "activity-id"); + await conversations.GetActivityMembersAsync("conversation-id", "activity-id"); + await conversations.CreateTargetedActivityAsync("conversation-id", CreateTargetedActivity()); + await conversations.UpdateTargetedActivityAsync("conversation-id", "activity-id", CreateActivity()); + await conversations.DeleteTargetedActivityAsync("conversation-id", "activity-id"); + + Assert.Equal(8, handler.Requests.Count); + Assert.All(handler.Requests, request => AssertIdentity(expected, request.AgenticIdentity)); + } + + [Fact] + public async Task ConversationApi_ActivityMethodsExplicitIdentityOverridesDefaultIdentity() + { + CapturingHttpMessageHandler handler = new(); + AgenticIdentity explicitIdentity = ExplicitIdentity(); + ConversationApiClient conversations = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations; + + await conversations.CreateActivityAsync("conversation-id", CreateActivity(), agenticIdentity: explicitIdentity); + await conversations.UpdateActivityAsync("conversation-id", "activity-id", CreateActivity(), agenticIdentity: explicitIdentity); + await conversations.ReplyToActivityAsync("conversation-id", "activity-id", CreateActivity(), agenticIdentity: explicitIdentity); + await conversations.DeleteActivityAsync("conversation-id", "activity-id", explicitIdentity); + await conversations.GetActivityMembersAsync("conversation-id", "activity-id", explicitIdentity); + await conversations.CreateTargetedActivityAsync("conversation-id", CreateTargetedActivity(), agenticIdentity: explicitIdentity); + await conversations.UpdateTargetedActivityAsync("conversation-id", "activity-id", CreateActivity(), agenticIdentity: explicitIdentity); + await conversations.DeleteTargetedActivityAsync("conversation-id", "activity-id", explicitIdentity); + + Assert.Equal(8, handler.Requests.Count); + Assert.All(handler.Requests, request => AssertIdentity(explicitIdentity, request.AgenticIdentity)); + } + + [Fact] + public async Task ConversationApi_ActivityFromIdentityOverridesFallbackIdentity() + { + CapturingHttpMessageHandler handler = new(); + AgenticIdentity fromIdentity = new() + { + AgenticAppId = "from-agentic-app-id", + AgenticUserId = "from-agentic-user-id", + AgenticAppBlueprintId = "from-agentic-blueprint-id", + TenantId = "from-tenant-id" + }; + ConversationApiClient conversations = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations; + + await conversations.CreateActivityAsync("conversation-id", CreateActivity(fromIdentity), agenticIdentity: ExplicitIdentity()); + await conversations.UpdateActivityAsync("conversation-id", "activity-id", CreateActivity(fromIdentity), agenticIdentity: ExplicitIdentity()); + + Assert.Equal(2, handler.Requests.Count); + Assert.All(handler.Requests, request => + { + AssertIdentity(fromIdentity, request.AgenticIdentity); + Assert.Equal("from-bot-id", request.BotAppId); + }); + } + [Fact] public async Task ActivityClient_UsesDefaultIdentityWhenExplicitIdentityIsMissing() { CapturingHttpMessageHandler handler = new(); AgenticIdentity expected = DefaultIdentity(); +#pragma warning disable CS0618 // Verifies backward-compatible obsolete wrapper behavior. ActivityClient activities = CreateApiClient(handler).ForServiceUrl(ServiceUrl, expected).Conversations.Activities; +#pragma warning restore CS0618 await activities.CreateAsync("conversation-id", CreateActivity()); await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity()); @@ -127,7 +193,9 @@ public async Task ActivityClient_ExplicitIdentityOverridesDefaultIdentity() { CapturingHttpMessageHandler handler = new(); AgenticIdentity explicitIdentity = ExplicitIdentity(); +#pragma warning disable CS0618 // Verifies backward-compatible obsolete wrapper behavior. ActivityClient activities = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations.Activities; +#pragma warning restore CS0618 await activities.CreateAsync("conversation-id", CreateActivity(), agenticIdentity: explicitIdentity); await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity(), agenticIdentity: explicitIdentity); @@ -153,7 +221,9 @@ public async Task ActivityClient_ActivityFromIdentityOverridesFallbackIdentity() AgenticAppBlueprintId = "from-agentic-blueprint-id", TenantId = "from-tenant-id" }; +#pragma warning disable CS0618 // Verifies backward-compatible obsolete wrapper behavior. ActivityClient activities = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations.Activities; +#pragma warning restore CS0618 await activities.CreateAsync("conversation-id", CreateActivity(fromIdentity), agenticIdentity: ExplicitIdentity()); await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity(fromIdentity), agenticIdentity: ExplicitIdentity()); From 792734da9791e30cccefdc3ccf16408a083b2715 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 11:52:02 -0700 Subject: [PATCH 12/23] Add inbound activity API scoping Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Api/Clients/ApiClient.cs | 17 +++++++++- core/src/Microsoft.Teams.Apps/Context.cs | 4 +-- .../AgenticIdentityDefaultsTests.cs | 34 +++++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs index 552709b9..fd53c4ec 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Teams.Apps.Schema; using Microsoft.Teams.Core.Http; using Microsoft.Teams.Core.Schema; @@ -20,7 +21,7 @@ namespace Microsoft.Teams.Apps.Api.Clients; /// /// /// DI-friendly (no serviceUrl) — Use ApiClient(HttpClient, ConversationClient, UserTokenClient, ILogger) -/// and call per-request to create a scoped instance. +/// and call or per-request to create a scoped instance. /// Fully initialized — Use ApiClient(Uri, HttpClient, ConversationClient, UserTokenClient, ILogger) /// when the service URL is known upfront. /// @@ -155,4 +156,18 @@ public virtual ApiClient ForServiceUrl(Uri serviceUrl, AgenticIdentity? defaultA ArgumentNullException.ThrowIfNull(serviceUrl); return new ApiClient(_http, ConversationClient, UserTokenClient, serviceUrl, defaultAgenticIdentity); } + + /// + /// Creates a new scoped to an inbound activity's service URL and recipient agentic identity. + /// + /// The inbound activity for this scope. + /// A new bound to the inbound activity's service URL and default identity. + /// Thrown when does not include a service URL. + public virtual ApiClient ForInboundActivity(TeamsActivity activity) + { + ArgumentNullException.ThrowIfNull(activity); + return ForServiceUrl( + activity.ServiceUrl ?? throw new InvalidOperationException("Activity.ServiceUrl is required to use the Api client."), + activity.Recipient?.GetAgenticIdentity()); + } } diff --git a/core/src/Microsoft.Teams.Apps/Context.cs b/core/src/Microsoft.Teams.Apps/Context.cs index 11d343c9..a4bc2b45 100644 --- a/core/src/Microsoft.Teams.Apps/Context.cs +++ b/core/src/Microsoft.Teams.Apps/Context.cs @@ -48,9 +48,7 @@ public class Context(TeamsBotApplication botApplication, TActivity ac /// /// Gets the scoped to the current activity's service URL. /// - public ApiClient Api => _api ??= TeamsBotApplication.Api.ForServiceUrl( - Activity.ServiceUrl ?? throw new InvalidOperationException("Activity.ServiceUrl is required to use the Api client."), - Activity.Recipient?.GetAgenticIdentity()); + public ApiClient Api => _api ??= TeamsBotApplication.Api.ForInboundActivity(Activity); // ==================== Turn State ==================== diff --git a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs index f56ded4e..7b5079bb 100644 --- a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs +++ b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs @@ -102,6 +102,40 @@ public async Task ScopedApi_ExplicitIdentityOverridesDefaultIdentity() AssertIdentity(explicitIdentity, request.AgenticIdentity); } + [Fact] + public async Task ForInboundActivity_UsesServiceUrlAndRecipientAgenticIdentity() + { + CapturingHttpMessageHandler handler = new(); + AgenticIdentity expected = DefaultIdentity(); + ApiClient api = CreateApiClient(handler).ForInboundActivity(new TeamsActivity + { + ServiceUrl = ServiceUrl, + Recipient = new TeamsChannelAccount + { + AgenticAppId = expected.AgenticAppId, + AgenticUserId = expected.AgenticUserId, + AgenticAppBlueprintId = expected.AgenticAppBlueprintId, + TenantId = expected.TenantId + } + }); + + await api.Conversations.GetMembersPagedAsync("conversation-id"); + + Assert.Equal(ServiceUrl, api.ServiceUrl); + CapturedRequest request = Assert.Single(handler.Requests); + AssertIdentity(expected, request.AgenticIdentity); + } + + [Fact] + public void ForInboundActivity_ThrowsWhenServiceUrlIsMissing() + { + ApiClient api = CreateApiClient(new CapturingHttpMessageHandler()); + + InvalidOperationException exception = Assert.Throws(() => api.ForInboundActivity(new TeamsActivity())); + + Assert.Equal("Activity.ServiceUrl is required to use the Api client.", exception.Message); + } + [Fact] public async Task ConversationApi_ActivityMethodsUseDefaultIdentityWhenExplicitIdentityIsMissing() { From 0c2611f98d6f1282569f77cfa67924fd25beba2e Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 11:54:10 -0700 Subject: [PATCH 13/23] Clarify activity update request context Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs index 1961d443..c9cafbdc 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs @@ -47,7 +47,10 @@ public Task UpdateAsync(string conversationId, string id { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; - return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.Merge(BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), BotRequestContext.FromActivity(activity)), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + BotRequestContext? requestContext = BotRequestContext.Merge( + BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), + BotRequestContext.FromActivity(activity)); + return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: requestContext, customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -105,7 +108,10 @@ public Task UpdateTargetedAsync(string conversationId, s { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; - return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.Merge(BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), BotRequestContext.FromActivity(activity)), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + BotRequestContext? requestContext = BotRequestContext.Merge( + BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), + BotRequestContext.FromActivity(activity)); + return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: requestContext, customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// From c253d96e2ea587a9c0e1f8e10befa61e724c65dc Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 11:58:17 -0700 Subject: [PATCH 14/23] Clarify agentic tenant documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs b/core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs index f71e24af..9116b424 100644 --- a/core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs +++ b/core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs @@ -254,7 +254,7 @@ public TeamsBotApplication( /// /// The agentic application ID. /// The agentic user ID. - /// Optional tenant ID. Overrides the configured tenant ID when provided. + /// Optional tenant ID of the agent instance for multi-tenant applications. Overrides the configured tenant ID when provided. /// Optional agentic application blueprint ID. Defaults to this application's AppId. /// An populated with application defaults. public AgenticIdentity GetAgenticIdentity(string agenticAppId, string agenticUserId, string? tenantId = null, string? agenticAppBlueprintId = null) From 521a6560cf143a41e7c914788cc83ca590653f64 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 14:54:11 -0700 Subject: [PATCH 15/23] Add API request options for activity calls Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Api/Clients/APIRequestOptions.cs | 22 +++++ .../Api/Clients/ActivityClient.cs | 83 ++++++++++++++---- .../Api/Clients/ConversationApiClient.cs | 85 ++++++++++++++++--- .../AgenticIdentityDefaultsTests.cs | 72 +++++++++++----- 4 files changed, 212 insertions(+), 50 deletions(-) create mode 100644 core/src/Microsoft.Teams.Apps/Api/Clients/APIRequestOptions.cs diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/APIRequestOptions.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/APIRequestOptions.cs new file mode 100644 index 00000000..689ea699 --- /dev/null +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/APIRequestOptions.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Teams.Core.Schema; + +namespace Microsoft.Teams.Apps.Api.Clients; + +/// +/// Options for Apps API requests that need per-call settings beyond the required method parameters. +/// +public readonly record struct APIRequestOptions +{ + /// + /// Gets the agentic identity to authenticate as for this request. + /// + public AgenticIdentity? AgenticIdentity { get; init; } + + /// + /// Gets additional headers to include in this request. + /// + public Dictionary? AdditionalHeaders { get; init; } +} diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs index c9cafbdc..3fd207a2 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs @@ -32,53 +32,83 @@ internal ActivityClient(Uri serviceUrl, CoreConversationClient client, AgenticId /// /// Create a new activity in a conversation. /// - public Task CreateAsync(string conversationId, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task CreateAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => CreateAsync(conversationId, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); + + /// + /// Create a new activity in a conversation. + /// + public Task CreateAsync(string conversationId, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); } /// /// Update an existing activity in a conversation. /// - public Task UpdateAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task UpdateAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => UpdateAsync(conversationId, id, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); + + /// + /// Update an existing activity in a conversation. + /// + public Task UpdateAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; BotRequestContext? requestContext = BotRequestContext.Merge( - BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), + BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), BotRequestContext.FromActivity(activity)); - return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: requestContext, customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: requestContext, customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); } /// /// Reply to an existing activity in a conversation. /// - public Task ReplyAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task ReplyAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => ReplyAsync(conversationId, id, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); + + /// + /// Reply to an existing activity in a conversation. + /// + public Task ReplyAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ReplyToId = id; activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); } /// /// Delete an activity from a conversation. /// public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => DeleteAsync(conversationId, id, new APIRequestOptions { AgenticIdentity = agenticIdentity, AdditionalHeaders = additionalHeaders }, cancellationToken); + + /// + /// Delete an activity from a conversation. + /// + public Task DeleteAsync(string conversationId, string id, APIRequestOptions options, CancellationToken cancellationToken = default) { - return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); } /// /// Get members of a specific activity. /// - public async Task> GetMembersAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task> GetMembersAsync(string conversationId, string id, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => GetMembersAsync(conversationId, id, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); + + /// + /// Get members of a specific activity. + /// + public async Task> GetMembersAsync(string conversationId, string id, APIRequestOptions options, CancellationToken cancellationToken = default) { - IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); + IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; } @@ -87,7 +117,15 @@ public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agent /// Targeted activities are only visible to the specified recipient. /// [Experimental("ExperimentalTeamsTargeted")] - public Task CreateTargetedAsync(string conversationId, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task CreateTargetedAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => CreateTargetedAsync(conversationId, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); + + /// + /// Create a new targeted activity in a conversation. + /// Targeted activities are only visible to the specified recipient. + /// + [Experimental("ExperimentalTeamsTargeted")] + public Task CreateTargetedAsync(string conversationId, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -97,28 +135,41 @@ public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agent { activity.Recipient.IsTargeted = true; } - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); } /// /// Update an existing targeted activity in a conversation. /// [Experimental("ExperimentalTeamsTargeted")] - public Task UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => UpdateTargetedAsync(conversationId, id, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); + + /// + /// Update an existing targeted activity in a conversation. + /// + [Experimental("ExperimentalTeamsTargeted")] + public Task UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; BotRequestContext? requestContext = BotRequestContext.Merge( - BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), + BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), BotRequestContext.FromActivity(activity)); - return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: requestContext, customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: requestContext, customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); } /// /// Delete a targeted activity from a conversation. /// public Task DeleteTargetedAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => DeleteTargetedAsync(conversationId, id, new APIRequestOptions { AgenticIdentity = agenticIdentity, AdditionalHeaders = additionalHeaders }, cancellationToken); + + /// + /// Delete a targeted activity from a conversation. + /// + public Task DeleteTargetedAsync(string conversationId, string id, APIRequestOptions options, CancellationToken cancellationToken = default) { - return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); } } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs index 3e791383..c8ce3fb5 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs @@ -65,50 +65,83 @@ public Task CreateAsync(ConversationParameters reque /// /// Create a new activity in a conversation. /// - public Task CreateActivityAsync(string conversationId, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task CreateActivityAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => CreateActivityAsync(conversationId, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); + + /// + /// Create a new activity in a conversation. + /// + public Task CreateActivityAsync(string conversationId, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); } /// /// Update an existing activity in a conversation. /// - public Task UpdateActivityAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task UpdateActivityAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => UpdateActivityAsync(conversationId, id, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); + + /// + /// Update an existing activity in a conversation. + /// + public Task UpdateActivityAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; - return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.Merge(BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), BotRequestContext.FromActivity(activity)), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + BotRequestContext? requestContext = BotRequestContext.Merge( + BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), + BotRequestContext.FromActivity(activity)); + return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: requestContext, customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); } /// /// Reply to an existing activity in a conversation. /// - public Task ReplyToActivityAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task ReplyToActivityAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => ReplyToActivityAsync(conversationId, id, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); + + /// + /// Reply to an existing activity in a conversation. + /// + public Task ReplyToActivityAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ReplyToId = id; activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); } /// /// Delete an activity from a conversation. /// public Task DeleteActivityAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => DeleteActivityAsync(conversationId, id, new APIRequestOptions { AgenticIdentity = agenticIdentity, AdditionalHeaders = additionalHeaders }, cancellationToken); + + /// + /// Delete an activity from a conversation. + /// + public Task DeleteActivityAsync(string conversationId, string id, APIRequestOptions options, CancellationToken cancellationToken = default) { - return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); } /// /// Get members of a specific activity. /// - public async Task> GetActivityMembersAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task> GetActivityMembersAsync(string conversationId, string id, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => GetActivityMembersAsync(conversationId, id, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); + + /// + /// Get members of a specific activity. + /// + public async Task> GetActivityMembersAsync(string conversationId, string id, APIRequestOptions options, CancellationToken cancellationToken = default) { - IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); + IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; } @@ -117,7 +150,15 @@ public Task DeleteActivityAsync(string conversationId, string id, AgenticIdentit /// Targeted activities are only visible to the specified recipient. /// [Experimental("ExperimentalTeamsTargeted")] - public Task CreateTargetedActivityAsync(string conversationId, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task CreateTargetedActivityAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => CreateTargetedActivityAsync(conversationId, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); + + /// + /// Create a new targeted activity in a conversation. + /// Targeted activities are only visible to the specified recipient. + /// + [Experimental("ExperimentalTeamsTargeted")] + public Task CreateTargetedActivityAsync(string conversationId, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -126,26 +167,42 @@ public Task DeleteActivityAsync(string conversationId, string id, AgenticIdentit { activity.Recipient.IsTargeted = true; } - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); } /// /// Update an existing targeted activity in a conversation. /// [Experimental("ExperimentalTeamsTargeted")] - public Task UpdateTargetedActivityAsync(string conversationId, string id, CoreActivity activity, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + public Task UpdateTargetedActivityAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => UpdateTargetedActivityAsync(conversationId, id, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); + + /// + /// Update an existing targeted activity in a conversation. + /// + [Experimental("ExperimentalTeamsTargeted")] + public Task UpdateTargetedActivityAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; - return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.Merge(BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), BotRequestContext.FromActivity(activity)), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + BotRequestContext? requestContext = BotRequestContext.Merge( + BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), + BotRequestContext.FromActivity(activity)); + return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: requestContext, customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); } /// /// Delete a targeted activity from a conversation. /// public Task DeleteTargetedActivityAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) + => DeleteTargetedActivityAsync(conversationId, id, new APIRequestOptions { AgenticIdentity = agenticIdentity, AdditionalHeaders = additionalHeaders }, cancellationToken); + + /// + /// Delete a targeted activity from a conversation. + /// + public Task DeleteTargetedActivityAsync(string conversationId, string id, APIRequestOptions options, CancellationToken cancellationToken = default) { - return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); } #endregion diff --git a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs index 7b5079bb..3053b65c 100644 --- a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs +++ b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs @@ -162,15 +162,16 @@ public async Task ConversationApi_ActivityMethodsExplicitIdentityOverridesDefaul CapturingHttpMessageHandler handler = new(); AgenticIdentity explicitIdentity = ExplicitIdentity(); ConversationApiClient conversations = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations; + APIRequestOptions options = RequestOptions(explicitIdentity); - await conversations.CreateActivityAsync("conversation-id", CreateActivity(), agenticIdentity: explicitIdentity); - await conversations.UpdateActivityAsync("conversation-id", "activity-id", CreateActivity(), agenticIdentity: explicitIdentity); - await conversations.ReplyToActivityAsync("conversation-id", "activity-id", CreateActivity(), agenticIdentity: explicitIdentity); - await conversations.DeleteActivityAsync("conversation-id", "activity-id", explicitIdentity); - await conversations.GetActivityMembersAsync("conversation-id", "activity-id", explicitIdentity); - await conversations.CreateTargetedActivityAsync("conversation-id", CreateTargetedActivity(), agenticIdentity: explicitIdentity); - await conversations.UpdateTargetedActivityAsync("conversation-id", "activity-id", CreateActivity(), agenticIdentity: explicitIdentity); - await conversations.DeleteTargetedActivityAsync("conversation-id", "activity-id", explicitIdentity); + await conversations.CreateActivityAsync("conversation-id", CreateActivity(), options); + await conversations.UpdateActivityAsync("conversation-id", "activity-id", CreateActivity(), options); + await conversations.ReplyToActivityAsync("conversation-id", "activity-id", CreateActivity(), options); + await conversations.DeleteActivityAsync("conversation-id", "activity-id", options); + await conversations.GetActivityMembersAsync("conversation-id", "activity-id", options); + await conversations.CreateTargetedActivityAsync("conversation-id", CreateTargetedActivity(), options); + await conversations.UpdateTargetedActivityAsync("conversation-id", "activity-id", CreateActivity(), options); + await conversations.DeleteTargetedActivityAsync("conversation-id", "activity-id", options); Assert.Equal(8, handler.Requests.Count); Assert.All(handler.Requests, request => AssertIdentity(explicitIdentity, request.AgenticIdentity)); @@ -188,9 +189,10 @@ public async Task ConversationApi_ActivityFromIdentityOverridesFallbackIdentity( TenantId = "from-tenant-id" }; ConversationApiClient conversations = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations; + APIRequestOptions options = RequestOptions(ExplicitIdentity()); - await conversations.CreateActivityAsync("conversation-id", CreateActivity(fromIdentity), agenticIdentity: ExplicitIdentity()); - await conversations.UpdateActivityAsync("conversation-id", "activity-id", CreateActivity(fromIdentity), agenticIdentity: ExplicitIdentity()); + await conversations.CreateActivityAsync("conversation-id", CreateActivity(fromIdentity), options); + await conversations.UpdateActivityAsync("conversation-id", "activity-id", CreateActivity(fromIdentity), options); Assert.Equal(2, handler.Requests.Count); Assert.All(handler.Requests, request => @@ -230,15 +232,16 @@ public async Task ActivityClient_ExplicitIdentityOverridesDefaultIdentity() #pragma warning disable CS0618 // Verifies backward-compatible obsolete wrapper behavior. ActivityClient activities = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations.Activities; #pragma warning restore CS0618 + APIRequestOptions options = RequestOptions(explicitIdentity); - await activities.CreateAsync("conversation-id", CreateActivity(), agenticIdentity: explicitIdentity); - await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity(), agenticIdentity: explicitIdentity); - await activities.ReplyAsync("conversation-id", "activity-id", CreateActivity(), agenticIdentity: explicitIdentity); - await activities.DeleteAsync("conversation-id", "activity-id", explicitIdentity); - await activities.GetMembersAsync("conversation-id", "activity-id", explicitIdentity); - await activities.CreateTargetedAsync("conversation-id", CreateTargetedActivity(), agenticIdentity: explicitIdentity); - await activities.UpdateTargetedAsync("conversation-id", "activity-id", CreateActivity(), agenticIdentity: explicitIdentity); - await activities.DeleteTargetedAsync("conversation-id", "activity-id", explicitIdentity); + await activities.CreateAsync("conversation-id", CreateActivity(), options); + await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity(), options); + await activities.ReplyAsync("conversation-id", "activity-id", CreateActivity(), options); + await activities.DeleteAsync("conversation-id", "activity-id", options); + await activities.GetMembersAsync("conversation-id", "activity-id", options); + await activities.CreateTargetedAsync("conversation-id", CreateTargetedActivity(), options); + await activities.UpdateTargetedAsync("conversation-id", "activity-id", CreateActivity(), options); + await activities.DeleteTargetedAsync("conversation-id", "activity-id", options); Assert.Equal(8, handler.Requests.Count); Assert.All(handler.Requests, request => AssertIdentity(explicitIdentity, request.AgenticIdentity)); @@ -258,9 +261,10 @@ public async Task ActivityClient_ActivityFromIdentityOverridesFallbackIdentity() #pragma warning disable CS0618 // Verifies backward-compatible obsolete wrapper behavior. ActivityClient activities = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations.Activities; #pragma warning restore CS0618 + APIRequestOptions options = RequestOptions(ExplicitIdentity()); - await activities.CreateAsync("conversation-id", CreateActivity(fromIdentity), agenticIdentity: ExplicitIdentity()); - await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity(fromIdentity), agenticIdentity: ExplicitIdentity()); + await activities.CreateAsync("conversation-id", CreateActivity(fromIdentity), options); + await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity(fromIdentity), options); Assert.Equal(2, handler.Requests.Count); Assert.All(handler.Requests, request => @@ -270,6 +274,31 @@ public async Task ActivityClient_ActivityFromIdentityOverridesFallbackIdentity() }); } + [Fact] + public async Task ActivityMethods_AcceptPositionalNullAdditionalHeaders() + { + CapturingHttpMessageHandler handler = new(); + AgenticIdentity expected = DefaultIdentity(); + ConversationApiClient conversations = CreateApiClient(handler).ForServiceUrl(ServiceUrl, expected).Conversations; +#pragma warning disable CS0618 // Verifies backward-compatible obsolete wrapper behavior. + ActivityClient activities = conversations.Activities; +#pragma warning restore CS0618 + + await conversations.CreateActivityAsync("conversation-id", CreateActivity(), null); + await conversations.UpdateActivityAsync("conversation-id", "activity-id", CreateActivity(), null); + await conversations.ReplyToActivityAsync("conversation-id", "activity-id", CreateActivity(), null); + await conversations.CreateTargetedActivityAsync("conversation-id", CreateTargetedActivity(), null); + await conversations.UpdateTargetedActivityAsync("conversation-id", "activity-id", CreateActivity(), null); + await activities.CreateAsync("conversation-id", CreateActivity(), null); + await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity(), null); + await activities.ReplyAsync("conversation-id", "activity-id", CreateActivity(), null); + await activities.CreateTargetedAsync("conversation-id", CreateTargetedActivity(), null); + await activities.UpdateTargetedAsync("conversation-id", "activity-id", CreateActivity(), null); + + Assert.Equal(10, handler.Requests.Count); + Assert.All(handler.Requests, request => AssertIdentity(expected, request.AgenticIdentity)); + } + [Fact] public void GetAgenticIdentity_DefaultsBlueprintAndTenant() { @@ -379,6 +408,9 @@ private static AgenticIdentity ExplicitIdentity() TenantId = "explicit-tenant-id" }; + private static APIRequestOptions RequestOptions(AgenticIdentity agenticIdentity) + => new() { AgenticIdentity = agenticIdentity }; + private static void AssertIdentity(AgenticIdentity expected, AgenticIdentity? actual) { Assert.NotNull(actual); From 09bd6584e03476cce12a03cb746745af81484580 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 14:56:37 -0700 Subject: [PATCH 16/23] Remove activity additional headers support Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Api/Clients/APIRequestOptions.cs | 4 - .../Api/Clients/ActivityClient.cs | 75 +++++-------------- .../Api/Clients/ConversationApiClient.cs | 75 +++++-------------- .../AgenticIdentityDefaultsTests.cs | 25 ------- 4 files changed, 36 insertions(+), 143 deletions(-) diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/APIRequestOptions.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/APIRequestOptions.cs index 689ea699..bd72ff45 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/APIRequestOptions.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/APIRequestOptions.cs @@ -15,8 +15,4 @@ public readonly record struct APIRequestOptions /// public AgenticIdentity? AgenticIdentity { get; init; } - /// - /// Gets additional headers to include in this request. - /// - public Dictionary? AdditionalHeaders { get; init; } } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs index 3fd207a2..36ae0be4 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs @@ -32,83 +32,59 @@ internal ActivityClient(Uri serviceUrl, CoreConversationClient client, AgenticId /// /// Create a new activity in a conversation. /// - public Task CreateAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => CreateAsync(conversationId, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); - - /// - /// Create a new activity in a conversation. - /// - public Task CreateAsync(string conversationId, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) + public Task CreateAsync(string conversationId, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); } /// /// Update an existing activity in a conversation. /// - public Task UpdateAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => UpdateAsync(conversationId, id, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); - - /// - /// Update an existing activity in a conversation. - /// - public Task UpdateAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) + public Task UpdateAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; BotRequestContext? requestContext = BotRequestContext.Merge( BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), BotRequestContext.FromActivity(activity)); - return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: requestContext, customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: requestContext, cancellationToken: cancellationToken); } /// /// Reply to an existing activity in a conversation. /// - public Task ReplyAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => ReplyAsync(conversationId, id, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); - - /// - /// Reply to an existing activity in a conversation. - /// - public Task ReplyAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) + public Task ReplyAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ReplyToId = id; activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); } /// /// Delete an activity from a conversation. /// - public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => DeleteAsync(conversationId, id, new APIRequestOptions { AgenticIdentity = agenticIdentity, AdditionalHeaders = additionalHeaders }, cancellationToken); + public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default) + => DeleteAsync(conversationId, id, new APIRequestOptions { AgenticIdentity = agenticIdentity }, cancellationToken); /// /// Delete an activity from a conversation. /// public Task DeleteAsync(string conversationId, string id, APIRequestOptions options, CancellationToken cancellationToken = default) { - return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); } /// /// Get members of a specific activity. /// - public Task> GetMembersAsync(string conversationId, string id, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => GetMembersAsync(conversationId, id, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); - - /// - /// Get members of a specific activity. - /// - public async Task> GetMembersAsync(string conversationId, string id, APIRequestOptions options, CancellationToken cancellationToken = default) + public async Task> GetMembersAsync(string conversationId, string id, APIRequestOptions options = default, CancellationToken cancellationToken = default) { - IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); + IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken).ConfigureAwait(false); return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; } @@ -117,15 +93,7 @@ public Task DeleteAsync(string conversationId, string id, APIRequestOptions opti /// Targeted activities are only visible to the specified recipient. /// [Experimental("ExperimentalTeamsTargeted")] - public Task CreateTargetedAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => CreateTargetedAsync(conversationId, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); - - /// - /// Create a new targeted activity in a conversation. - /// Targeted activities are only visible to the specified recipient. - /// - [Experimental("ExperimentalTeamsTargeted")] - public Task CreateTargetedAsync(string conversationId, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) + public Task CreateTargetedAsync(string conversationId, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -135,41 +103,34 @@ public Task DeleteAsync(string conversationId, string id, APIRequestOptions opti { activity.Recipient.IsTargeted = true; } - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); } /// /// Update an existing targeted activity in a conversation. /// [Experimental("ExperimentalTeamsTargeted")] - public Task UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => UpdateTargetedAsync(conversationId, id, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); - - /// - /// Update an existing targeted activity in a conversation. - /// - [Experimental("ExperimentalTeamsTargeted")] - public Task UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) + public Task UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; BotRequestContext? requestContext = BotRequestContext.Merge( BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), BotRequestContext.FromActivity(activity)); - return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: requestContext, customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: requestContext, cancellationToken: cancellationToken); } /// /// Delete a targeted activity from a conversation. /// - public Task DeleteTargetedAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => DeleteTargetedAsync(conversationId, id, new APIRequestOptions { AgenticIdentity = agenticIdentity, AdditionalHeaders = additionalHeaders }, cancellationToken); + public Task DeleteTargetedAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default) + => DeleteTargetedAsync(conversationId, id, new APIRequestOptions { AgenticIdentity = agenticIdentity }, cancellationToken); /// /// Delete a targeted activity from a conversation. /// public Task DeleteTargetedAsync(string conversationId, string id, APIRequestOptions options, CancellationToken cancellationToken = default) { - return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); } } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs index c8ce3fb5..821f90c5 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs @@ -65,83 +65,59 @@ public Task CreateAsync(ConversationParameters reque /// /// Create a new activity in a conversation. /// - public Task CreateActivityAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => CreateActivityAsync(conversationId, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); - - /// - /// Create a new activity in a conversation. - /// - public Task CreateActivityAsync(string conversationId, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) + public Task CreateActivityAsync(string conversationId, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); } /// /// Update an existing activity in a conversation. /// - public Task UpdateActivityAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => UpdateActivityAsync(conversationId, id, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); - - /// - /// Update an existing activity in a conversation. - /// - public Task UpdateActivityAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) + public Task UpdateActivityAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; BotRequestContext? requestContext = BotRequestContext.Merge( BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), BotRequestContext.FromActivity(activity)); - return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: requestContext, customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: requestContext, cancellationToken: cancellationToken); } /// /// Reply to an existing activity in a conversation. /// - public Task ReplyToActivityAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => ReplyToActivityAsync(conversationId, id, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); - - /// - /// Reply to an existing activity in a conversation. - /// - public Task ReplyToActivityAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) + public Task ReplyToActivityAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ReplyToId = id; activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); } /// /// Delete an activity from a conversation. /// - public Task DeleteActivityAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => DeleteActivityAsync(conversationId, id, new APIRequestOptions { AgenticIdentity = agenticIdentity, AdditionalHeaders = additionalHeaders }, cancellationToken); + public Task DeleteActivityAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default) + => DeleteActivityAsync(conversationId, id, new APIRequestOptions { AgenticIdentity = agenticIdentity }, cancellationToken); /// /// Delete an activity from a conversation. /// public Task DeleteActivityAsync(string conversationId, string id, APIRequestOptions options, CancellationToken cancellationToken = default) { - return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); } /// /// Get members of a specific activity. /// - public Task> GetActivityMembersAsync(string conversationId, string id, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => GetActivityMembersAsync(conversationId, id, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); - - /// - /// Get members of a specific activity. - /// - public async Task> GetActivityMembersAsync(string conversationId, string id, APIRequestOptions options, CancellationToken cancellationToken = default) + public async Task> GetActivityMembersAsync(string conversationId, string id, APIRequestOptions options = default, CancellationToken cancellationToken = default) { - IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); + IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken).ConfigureAwait(false); return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; } @@ -150,15 +126,7 @@ public Task DeleteActivityAsync(string conversationId, string id, APIRequestOpti /// Targeted activities are only visible to the specified recipient. /// [Experimental("ExperimentalTeamsTargeted")] - public Task CreateTargetedActivityAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => CreateTargetedActivityAsync(conversationId, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); - - /// - /// Create a new targeted activity in a conversation. - /// Targeted activities are only visible to the specified recipient. - /// - [Experimental("ExperimentalTeamsTargeted")] - public Task CreateTargetedActivityAsync(string conversationId, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) + public Task CreateTargetedActivityAsync(string conversationId, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -167,42 +135,35 @@ public Task DeleteActivityAsync(string conversationId, string id, APIRequestOpti { activity.Recipient.IsTargeted = true; } - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); } /// /// Update an existing targeted activity in a conversation. /// [Experimental("ExperimentalTeamsTargeted")] - public Task UpdateTargetedActivityAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => UpdateTargetedActivityAsync(conversationId, id, activity, new APIRequestOptions { AdditionalHeaders = additionalHeaders }, cancellationToken); - - /// - /// Update an existing targeted activity in a conversation. - /// - [Experimental("ExperimentalTeamsTargeted")] - public Task UpdateTargetedActivityAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options, CancellationToken cancellationToken = default) + public Task UpdateTargetedActivityAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; BotRequestContext? requestContext = BotRequestContext.Merge( BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), BotRequestContext.FromActivity(activity)); - return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: requestContext, customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: requestContext, cancellationToken: cancellationToken); } /// /// Delete a targeted activity from a conversation. /// - public Task DeleteTargetedActivityAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) - => DeleteTargetedActivityAsync(conversationId, id, new APIRequestOptions { AgenticIdentity = agenticIdentity, AdditionalHeaders = additionalHeaders }, cancellationToken); + public Task DeleteTargetedActivityAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default) + => DeleteTargetedActivityAsync(conversationId, id, new APIRequestOptions { AgenticIdentity = agenticIdentity }, cancellationToken); /// /// Delete a targeted activity from a conversation. /// public Task DeleteTargetedActivityAsync(string conversationId, string id, APIRequestOptions options, CancellationToken cancellationToken = default) { - return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), customHeaders: options.AdditionalHeaders, cancellationToken: cancellationToken); + return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); } #endregion diff --git a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs index 3053b65c..3d3b0630 100644 --- a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs +++ b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs @@ -274,31 +274,6 @@ public async Task ActivityClient_ActivityFromIdentityOverridesFallbackIdentity() }); } - [Fact] - public async Task ActivityMethods_AcceptPositionalNullAdditionalHeaders() - { - CapturingHttpMessageHandler handler = new(); - AgenticIdentity expected = DefaultIdentity(); - ConversationApiClient conversations = CreateApiClient(handler).ForServiceUrl(ServiceUrl, expected).Conversations; -#pragma warning disable CS0618 // Verifies backward-compatible obsolete wrapper behavior. - ActivityClient activities = conversations.Activities; -#pragma warning restore CS0618 - - await conversations.CreateActivityAsync("conversation-id", CreateActivity(), null); - await conversations.UpdateActivityAsync("conversation-id", "activity-id", CreateActivity(), null); - await conversations.ReplyToActivityAsync("conversation-id", "activity-id", CreateActivity(), null); - await conversations.CreateTargetedActivityAsync("conversation-id", CreateTargetedActivity(), null); - await conversations.UpdateTargetedActivityAsync("conversation-id", "activity-id", CreateActivity(), null); - await activities.CreateAsync("conversation-id", CreateActivity(), null); - await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity(), null); - await activities.ReplyAsync("conversation-id", "activity-id", CreateActivity(), null); - await activities.CreateTargetedAsync("conversation-id", CreateTargetedActivity(), null); - await activities.UpdateTargetedAsync("conversation-id", "activity-id", CreateActivity(), null); - - Assert.Equal(10, handler.Requests.Count); - Assert.All(handler.Requests, request => AssertIdentity(expected, request.AgenticIdentity)); - } - [Fact] public void GetAgenticIdentity_DefaultsBlueprintAndTenant() { From 9815ae01d6ba75519a6e4162a886ba246037b734 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 14:57:49 -0700 Subject: [PATCH 17/23] Rename activity request options Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Api/Clients/ActivityClient.cs | 20 +++++++++---------- .../Api/Clients/ConversationApiClient.cs | 20 +++++++++---------- ...APIRequestOptions.cs => RequestOptions.cs} | 3 +-- .../AgenticIdentityDefaultsTests.cs | 10 +++++----- 4 files changed, 26 insertions(+), 27 deletions(-) rename core/src/Microsoft.Teams.Apps/Api/Clients/{APIRequestOptions.cs => RequestOptions.cs} (90%) diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs index 36ae0be4..64802fa5 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs @@ -32,7 +32,7 @@ internal ActivityClient(Uri serviceUrl, CoreConversationClient client, AgenticId /// /// Create a new activity in a conversation. /// - public Task CreateAsync(string conversationId, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) + public Task CreateAsync(string conversationId, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -43,7 +43,7 @@ internal ActivityClient(Uri serviceUrl, CoreConversationClient client, AgenticId /// /// Update an existing activity in a conversation. /// - public Task UpdateAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) + public Task UpdateAsync(string conversationId, string id, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -56,7 +56,7 @@ public Task UpdateAsync(string conversationId, string id /// /// Reply to an existing activity in a conversation. /// - public Task ReplyAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) + public Task ReplyAsync(string conversationId, string id, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ReplyToId = id; @@ -69,12 +69,12 @@ public Task UpdateAsync(string conversationId, string id /// Delete an activity from a conversation. /// public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default) - => DeleteAsync(conversationId, id, new APIRequestOptions { AgenticIdentity = agenticIdentity }, cancellationToken); + => DeleteAsync(conversationId, id, new RequestOptions { AgenticIdentity = agenticIdentity }, cancellationToken); /// /// Delete an activity from a conversation. /// - public Task DeleteAsync(string conversationId, string id, APIRequestOptions options, CancellationToken cancellationToken = default) + public Task DeleteAsync(string conversationId, string id, RequestOptions options, CancellationToken cancellationToken = default) { return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); } @@ -82,7 +82,7 @@ public Task DeleteAsync(string conversationId, string id, APIRequestOptions opti /// /// Get members of a specific activity. /// - public async Task> GetMembersAsync(string conversationId, string id, APIRequestOptions options = default, CancellationToken cancellationToken = default) + public async Task> GetMembersAsync(string conversationId, string id, RequestOptions options = default, CancellationToken cancellationToken = default) { IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken).ConfigureAwait(false); return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; @@ -93,7 +93,7 @@ public Task DeleteAsync(string conversationId, string id, APIRequestOptions opti /// Targeted activities are only visible to the specified recipient. /// [Experimental("ExperimentalTeamsTargeted")] - public Task CreateTargetedAsync(string conversationId, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) + public Task CreateTargetedAsync(string conversationId, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -110,7 +110,7 @@ public Task DeleteAsync(string conversationId, string id, APIRequestOptions opti /// Update an existing targeted activity in a conversation. /// [Experimental("ExperimentalTeamsTargeted")] - public Task UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) + public Task UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -124,12 +124,12 @@ public Task UpdateTargetedAsync(string conversationId, s /// Delete a targeted activity from a conversation. /// public Task DeleteTargetedAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default) - => DeleteTargetedAsync(conversationId, id, new APIRequestOptions { AgenticIdentity = agenticIdentity }, cancellationToken); + => DeleteTargetedAsync(conversationId, id, new RequestOptions { AgenticIdentity = agenticIdentity }, cancellationToken); /// /// Delete a targeted activity from a conversation. /// - public Task DeleteTargetedAsync(string conversationId, string id, APIRequestOptions options, CancellationToken cancellationToken = default) + public Task DeleteTargetedAsync(string conversationId, string id, RequestOptions options, CancellationToken cancellationToken = default) { return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs index 821f90c5..4c141c73 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs @@ -65,7 +65,7 @@ public Task CreateAsync(ConversationParameters reque /// /// Create a new activity in a conversation. /// - public Task CreateActivityAsync(string conversationId, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) + public Task CreateActivityAsync(string conversationId, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -76,7 +76,7 @@ public Task CreateAsync(ConversationParameters reque /// /// Update an existing activity in a conversation. /// - public Task UpdateActivityAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) + public Task UpdateActivityAsync(string conversationId, string id, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -89,7 +89,7 @@ public Task UpdateActivityAsync(string conversationId, s /// /// Reply to an existing activity in a conversation. /// - public Task ReplyToActivityAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) + public Task ReplyToActivityAsync(string conversationId, string id, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ReplyToId = id; @@ -102,12 +102,12 @@ public Task UpdateActivityAsync(string conversationId, s /// Delete an activity from a conversation. /// public Task DeleteActivityAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default) - => DeleteActivityAsync(conversationId, id, new APIRequestOptions { AgenticIdentity = agenticIdentity }, cancellationToken); + => DeleteActivityAsync(conversationId, id, new RequestOptions { AgenticIdentity = agenticIdentity }, cancellationToken); /// /// Delete an activity from a conversation. /// - public Task DeleteActivityAsync(string conversationId, string id, APIRequestOptions options, CancellationToken cancellationToken = default) + public Task DeleteActivityAsync(string conversationId, string id, RequestOptions options, CancellationToken cancellationToken = default) { return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); } @@ -115,7 +115,7 @@ public Task DeleteActivityAsync(string conversationId, string id, APIRequestOpti /// /// Get members of a specific activity. /// - public async Task> GetActivityMembersAsync(string conversationId, string id, APIRequestOptions options = default, CancellationToken cancellationToken = default) + public async Task> GetActivityMembersAsync(string conversationId, string id, RequestOptions options = default, CancellationToken cancellationToken = default) { IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken).ConfigureAwait(false); return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; @@ -126,7 +126,7 @@ public Task DeleteActivityAsync(string conversationId, string id, APIRequestOpti /// Targeted activities are only visible to the specified recipient. /// [Experimental("ExperimentalTeamsTargeted")] - public Task CreateTargetedActivityAsync(string conversationId, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) + public Task CreateTargetedActivityAsync(string conversationId, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -142,7 +142,7 @@ public Task DeleteActivityAsync(string conversationId, string id, APIRequestOpti /// Update an existing targeted activity in a conversation. /// [Experimental("ExperimentalTeamsTargeted")] - public Task UpdateTargetedActivityAsync(string conversationId, string id, CoreActivity activity, APIRequestOptions options = default, CancellationToken cancellationToken = default) + public Task UpdateTargetedActivityAsync(string conversationId, string id, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; @@ -156,12 +156,12 @@ public Task UpdateTargetedActivityAsync(string conversat /// Delete a targeted activity from a conversation. /// public Task DeleteTargetedActivityAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default) - => DeleteTargetedActivityAsync(conversationId, id, new APIRequestOptions { AgenticIdentity = agenticIdentity }, cancellationToken); + => DeleteTargetedActivityAsync(conversationId, id, new RequestOptions { AgenticIdentity = agenticIdentity }, cancellationToken); /// /// Delete a targeted activity from a conversation. /// - public Task DeleteTargetedActivityAsync(string conversationId, string id, APIRequestOptions options, CancellationToken cancellationToken = default) + public Task DeleteTargetedActivityAsync(string conversationId, string id, RequestOptions options, CancellationToken cancellationToken = default) { return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/APIRequestOptions.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/RequestOptions.cs similarity index 90% rename from core/src/Microsoft.Teams.Apps/Api/Clients/APIRequestOptions.cs rename to core/src/Microsoft.Teams.Apps/Api/Clients/RequestOptions.cs index bd72ff45..1c1939d3 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/APIRequestOptions.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/RequestOptions.cs @@ -8,11 +8,10 @@ namespace Microsoft.Teams.Apps.Api.Clients; /// /// Options for Apps API requests that need per-call settings beyond the required method parameters. /// -public readonly record struct APIRequestOptions +public readonly record struct RequestOptions { /// /// Gets the agentic identity to authenticate as for this request. /// public AgenticIdentity? AgenticIdentity { get; init; } - } diff --git a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs index 3d3b0630..ae7c6f27 100644 --- a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs +++ b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs @@ -162,7 +162,7 @@ public async Task ConversationApi_ActivityMethodsExplicitIdentityOverridesDefaul CapturingHttpMessageHandler handler = new(); AgenticIdentity explicitIdentity = ExplicitIdentity(); ConversationApiClient conversations = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations; - APIRequestOptions options = RequestOptions(explicitIdentity); + RequestOptions options = CreateRequestOptions(explicitIdentity); await conversations.CreateActivityAsync("conversation-id", CreateActivity(), options); await conversations.UpdateActivityAsync("conversation-id", "activity-id", CreateActivity(), options); @@ -189,7 +189,7 @@ public async Task ConversationApi_ActivityFromIdentityOverridesFallbackIdentity( TenantId = "from-tenant-id" }; ConversationApiClient conversations = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations; - APIRequestOptions options = RequestOptions(ExplicitIdentity()); + RequestOptions options = CreateRequestOptions(ExplicitIdentity()); await conversations.CreateActivityAsync("conversation-id", CreateActivity(fromIdentity), options); await conversations.UpdateActivityAsync("conversation-id", "activity-id", CreateActivity(fromIdentity), options); @@ -232,7 +232,7 @@ public async Task ActivityClient_ExplicitIdentityOverridesDefaultIdentity() #pragma warning disable CS0618 // Verifies backward-compatible obsolete wrapper behavior. ActivityClient activities = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations.Activities; #pragma warning restore CS0618 - APIRequestOptions options = RequestOptions(explicitIdentity); + RequestOptions options = CreateRequestOptions(explicitIdentity); await activities.CreateAsync("conversation-id", CreateActivity(), options); await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity(), options); @@ -261,7 +261,7 @@ public async Task ActivityClient_ActivityFromIdentityOverridesFallbackIdentity() #pragma warning disable CS0618 // Verifies backward-compatible obsolete wrapper behavior. ActivityClient activities = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations.Activities; #pragma warning restore CS0618 - APIRequestOptions options = RequestOptions(ExplicitIdentity()); + RequestOptions options = CreateRequestOptions(ExplicitIdentity()); await activities.CreateAsync("conversation-id", CreateActivity(fromIdentity), options); await activities.UpdateAsync("conversation-id", "activity-id", CreateActivity(fromIdentity), options); @@ -383,7 +383,7 @@ private static AgenticIdentity ExplicitIdentity() TenantId = "explicit-tenant-id" }; - private static APIRequestOptions RequestOptions(AgenticIdentity agenticIdentity) + private static RequestOptions CreateRequestOptions(AgenticIdentity agenticIdentity) => new() { AgenticIdentity = agenticIdentity }; private static void AssertIdentity(AgenticIdentity expected, AgenticIdentity? actual) From 55b5fb79206b326cb0430e59af86754a7941f733 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 14:58:24 -0700 Subject: [PATCH 18/23] Document breaking change PR guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 246c1b52..f17c774a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,3 +3,7 @@ ## XML documentation - Prefer `` for linkable XML doc references, including explicit overloads like ``; use `...` only for inline code formatting. + +## Pull requests + +- If a change breaks a public-facing API or behavior, call it out explicitly in the PR/review description. From 1f93eeac6f1e94dd2e8dc5f99a75e892f9972327 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 14:59:58 -0700 Subject: [PATCH 19/23] Centralize request options conversion Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Api/Clients/ActivityClient.cs | 16 ++++++++-------- .../Api/Clients/ConversationApiClient.cs | 16 ++++++++-------- .../Api/Clients/RequestOptions.cs | 9 +++++++++ 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs index 64802fa5..d4ef4a92 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs @@ -37,7 +37,7 @@ internal ActivityClient(Uri serviceUrl, CoreConversationClient client, AgenticId ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } /// @@ -48,7 +48,7 @@ public Task UpdateAsync(string conversationId, string id ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; BotRequestContext? requestContext = BotRequestContext.Merge( - BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), + options.ToBotRequestContext(_defaultAgenticIdentity), BotRequestContext.FromActivity(activity)); return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: requestContext, cancellationToken: cancellationToken); } @@ -62,7 +62,7 @@ public Task UpdateAsync(string conversationId, string id activity.ReplyToId = id; activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } /// @@ -76,7 +76,7 @@ public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agent /// public Task DeleteAsync(string conversationId, string id, RequestOptions options, CancellationToken cancellationToken = default) { - return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); + return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } /// @@ -84,7 +84,7 @@ public Task DeleteAsync(string conversationId, string id, RequestOptions options /// public async Task> GetMembersAsync(string conversationId, string id, RequestOptions options = default, CancellationToken cancellationToken = default) { - IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken).ConfigureAwait(false); + IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken).ConfigureAwait(false); return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; } @@ -103,7 +103,7 @@ public Task DeleteAsync(string conversationId, string id, RequestOptions options { activity.Recipient.IsTargeted = true; } - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } /// @@ -115,7 +115,7 @@ public Task UpdateTargetedAsync(string conversationId, s ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; BotRequestContext? requestContext = BotRequestContext.Merge( - BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), + options.ToBotRequestContext(_defaultAgenticIdentity), BotRequestContext.FromActivity(activity)); return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: requestContext, cancellationToken: cancellationToken); } @@ -131,6 +131,6 @@ public Task DeleteTargetedAsync(string conversationId, string id, AgenticIdentit /// public Task DeleteTargetedAsync(string conversationId, string id, RequestOptions options, CancellationToken cancellationToken = default) { - return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); + return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs index 4c141c73..5aba54de 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs @@ -70,7 +70,7 @@ public Task CreateAsync(ConversationParameters reque ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } /// @@ -81,7 +81,7 @@ public Task UpdateActivityAsync(string conversationId, s ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; BotRequestContext? requestContext = BotRequestContext.Merge( - BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), + options.ToBotRequestContext(_defaultAgenticIdentity), BotRequestContext.FromActivity(activity)); return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: requestContext, cancellationToken: cancellationToken); } @@ -95,7 +95,7 @@ public Task UpdateActivityAsync(string conversationId, s activity.ReplyToId = id; activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } /// @@ -109,7 +109,7 @@ public Task DeleteActivityAsync(string conversationId, string id, AgenticIdentit /// public Task DeleteActivityAsync(string conversationId, string id, RequestOptions options, CancellationToken cancellationToken = default) { - return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); + return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } /// @@ -117,7 +117,7 @@ public Task DeleteActivityAsync(string conversationId, string id, RequestOptions /// public async Task> GetActivityMembersAsync(string conversationId, string id, RequestOptions options = default, CancellationToken cancellationToken = default) { - IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken).ConfigureAwait(false); + IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken).ConfigureAwait(false); return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; } @@ -135,7 +135,7 @@ public Task DeleteActivityAsync(string conversationId, string id, RequestOptions { activity.Recipient.IsTargeted = true; } - return _client.SendActivityAsync(activity, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } /// @@ -147,7 +147,7 @@ public Task UpdateTargetedActivityAsync(string conversat ArgumentNullException.ThrowIfNull(activity); activity.ServiceUrl ??= _serviceUrl; BotRequestContext? requestContext = BotRequestContext.Merge( - BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), + options.ToBotRequestContext(_defaultAgenticIdentity), BotRequestContext.FromActivity(activity)); return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: requestContext, cancellationToken: cancellationToken); } @@ -163,7 +163,7 @@ public Task DeleteTargetedActivityAsync(string conversationId, string id, Agenti /// public Task DeleteTargetedActivityAsync(string conversationId, string id, RequestOptions options, CancellationToken cancellationToken = default) { - return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(options.AgenticIdentity ?? _defaultAgenticIdentity), cancellationToken: cancellationToken); + return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } #endregion diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/RequestOptions.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/RequestOptions.cs index 1c1939d3..a08286e3 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/RequestOptions.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/RequestOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Teams.Core.Http; using Microsoft.Teams.Core.Schema; namespace Microsoft.Teams.Apps.Api.Clients; @@ -14,4 +15,12 @@ public readonly record struct RequestOptions /// Gets the agentic identity to authenticate as for this request. /// public AgenticIdentity? AgenticIdentity { get; init; } + + internal BotRequestContext? ToBotRequestContext(AgenticIdentity? defaultAgenticIdentity = null) + => BotRequestContext.FromAgenticIdentity(AgenticIdentity ?? defaultAgenticIdentity); + + internal BotRequestOptions? ToBotRequestOptions(AgenticIdentity? defaultAgenticIdentity = null) + => ToBotRequestContext(defaultAgenticIdentity) is { } requestContext + ? new BotRequestOptions { RequestContext = requestContext } + : null; } From ffd8251bb849728109c762a29e30ccae9240f353 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 15:04:49 -0700 Subject: [PATCH 20/23] Add service URL to request options Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Api/Clients/ActivityClient.cs | 16 ++++---- .../Api/Clients/ApiClient.cs | 41 ++++++++++++++----- .../Api/Clients/ConversationApiClient.cs | 16 ++++---- .../Api/Clients/RequestOptions.cs | 5 +++ .../AgenticIdentityDefaultsTests.cs | 41 +++++++++++++++++++ 5 files changed, 92 insertions(+), 27 deletions(-) diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs index d4ef4a92..8a1601fe 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs @@ -35,7 +35,7 @@ internal ActivityClient(Uri serviceUrl, CoreConversationClient client, AgenticId public Task CreateAsync(string conversationId, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); - activity.ServiceUrl ??= _serviceUrl; + activity.ServiceUrl ??= options.ServiceUrl ?? _serviceUrl; activity.Conversation ??= new Conversation(conversationId); return _client.SendActivityAsync(activity, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } @@ -46,7 +46,7 @@ internal ActivityClient(Uri serviceUrl, CoreConversationClient client, AgenticId public Task UpdateAsync(string conversationId, string id, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); - activity.ServiceUrl ??= _serviceUrl; + activity.ServiceUrl ??= options.ServiceUrl ?? _serviceUrl; BotRequestContext? requestContext = BotRequestContext.Merge( options.ToBotRequestContext(_defaultAgenticIdentity), BotRequestContext.FromActivity(activity)); @@ -60,7 +60,7 @@ public Task UpdateAsync(string conversationId, string id { ArgumentNullException.ThrowIfNull(activity); activity.ReplyToId = id; - activity.ServiceUrl ??= _serviceUrl; + activity.ServiceUrl ??= options.ServiceUrl ?? _serviceUrl; activity.Conversation ??= new Conversation(conversationId); return _client.SendActivityAsync(activity, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } @@ -76,7 +76,7 @@ public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agent /// public Task DeleteAsync(string conversationId, string id, RequestOptions options, CancellationToken cancellationToken = default) { - return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); + return _client.DeleteActivityAsync(conversationId, id, options.ServiceUrl ?? _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } /// @@ -84,7 +84,7 @@ public Task DeleteAsync(string conversationId, string id, RequestOptions options /// public async Task> GetMembersAsync(string conversationId, string id, RequestOptions options = default, CancellationToken cancellationToken = default) { - IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken).ConfigureAwait(false); + IList members = await _client.GetActivityMembersAsync(conversationId, id, options.ServiceUrl ?? _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken).ConfigureAwait(false); return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; } @@ -96,7 +96,7 @@ public Task DeleteAsync(string conversationId, string id, RequestOptions options public Task CreateTargetedAsync(string conversationId, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); - activity.ServiceUrl ??= _serviceUrl; + activity.ServiceUrl ??= options.ServiceUrl ?? _serviceUrl; activity.Conversation ??= new Conversation(conversationId); // Ensure recipient is marked as targeted if (activity.Recipient is not null) @@ -113,7 +113,7 @@ public Task DeleteAsync(string conversationId, string id, RequestOptions options public Task UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); - activity.ServiceUrl ??= _serviceUrl; + activity.ServiceUrl ??= options.ServiceUrl ?? _serviceUrl; BotRequestContext? requestContext = BotRequestContext.Merge( options.ToBotRequestContext(_defaultAgenticIdentity), BotRequestContext.FromActivity(activity)); @@ -131,6 +131,6 @@ public Task DeleteTargetedAsync(string conversationId, string id, AgenticIdentit /// public Task DeleteTargetedAsync(string conversationId, string id, RequestOptions options, CancellationToken cancellationToken = default) { - return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); + return _client.DeleteTargetedActivityAsync(conversationId, id, options.ServiceUrl ?? _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs index fd53c4ec..da8f0964 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs @@ -119,17 +119,19 @@ internal ApiClient(Uri serviceUrl, HttpClient httpClient, CoreConversationClient Meetings = new MeetingClient(serviceUrl.ToString(), _http, DefaultAgenticIdentity); } - // Private constructor for ForServiceUrl — shares BotHttpClient, ConversationClient, and UserTokenClient - private ApiClient(BotHttpClient http, CoreConversationClient conversationClient, CoreUserTokenClient userTokenClient, Uri serviceUrl, AgenticIdentity? defaultAgenticIdentity) + // Private constructor for scoped clients — shares BotHttpClient, ConversationClient, and UserTokenClient + private ApiClient(BotHttpClient http, CoreConversationClient conversationClient, CoreUserTokenClient userTokenClient, RequestOptions options) { + Uri serviceUrl = options.ServiceUrl ?? throw new InvalidOperationException("RequestOptions.ServiceUrl is required to scope the Api client."); + _http = http; ConversationClient = conversationClient; UserTokenClient = userTokenClient; - DefaultAgenticIdentity = defaultAgenticIdentity; + DefaultAgenticIdentity = options.AgenticIdentity; ServiceUrl = serviceUrl; - Conversations = new ConversationApiClient(serviceUrl, conversationClient, defaultAgenticIdentity); - Teams = new TeamClient(serviceUrl.ToString(), http, defaultAgenticIdentity); - Meetings = new MeetingClient(serviceUrl.ToString(), http, defaultAgenticIdentity); + Conversations = new ConversationApiClient(serviceUrl, conversationClient, DefaultAgenticIdentity); + Teams = new TeamClient(serviceUrl.ToString(), http, DefaultAgenticIdentity); + Meetings = new MeetingClient(serviceUrl.ToString(), http, DefaultAgenticIdentity); } /// @@ -141,7 +143,7 @@ private ApiClient(BotHttpClient http, CoreConversationClient conversationClient, public virtual ApiClient ForServiceUrl(Uri serviceUrl) { ArgumentNullException.ThrowIfNull(serviceUrl); - return new ApiClient(_http, ConversationClient, UserTokenClient, serviceUrl, defaultAgenticIdentity: null); + return ForRequestOptions(new RequestOptions { ServiceUrl = serviceUrl }); } /// @@ -154,7 +156,22 @@ public virtual ApiClient ForServiceUrl(Uri serviceUrl) public virtual ApiClient ForServiceUrl(Uri serviceUrl, AgenticIdentity? defaultAgenticIdentity) { ArgumentNullException.ThrowIfNull(serviceUrl); - return new ApiClient(_http, ConversationClient, UserTokenClient, serviceUrl, defaultAgenticIdentity); + return ForRequestOptions(new RequestOptions + { + ServiceUrl = serviceUrl, + AgenticIdentity = defaultAgenticIdentity + }); + } + + /// + /// Creates a new scoped to the service URL and default agentic identity in the request options. + /// + /// The request options. is required. + /// A new bound to the request options. + /// Thrown when is not provided. + public virtual ApiClient ForRequestOptions(RequestOptions options) + { + return new ApiClient(_http, ConversationClient, UserTokenClient, options); } /// @@ -166,8 +183,10 @@ public virtual ApiClient ForServiceUrl(Uri serviceUrl, AgenticIdentity? defaultA public virtual ApiClient ForInboundActivity(TeamsActivity activity) { ArgumentNullException.ThrowIfNull(activity); - return ForServiceUrl( - activity.ServiceUrl ?? throw new InvalidOperationException("Activity.ServiceUrl is required to use the Api client."), - activity.Recipient?.GetAgenticIdentity()); + return ForRequestOptions(new RequestOptions + { + ServiceUrl = activity.ServiceUrl ?? throw new InvalidOperationException("Activity.ServiceUrl is required to use the Api client."), + AgenticIdentity = activity.Recipient?.GetAgenticIdentity() + }); } } diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs index 5aba54de..df6f2471 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs @@ -68,7 +68,7 @@ public Task CreateAsync(ConversationParameters reque public Task CreateActivityAsync(string conversationId, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); - activity.ServiceUrl ??= _serviceUrl; + activity.ServiceUrl ??= options.ServiceUrl ?? _serviceUrl; activity.Conversation ??= new Conversation(conversationId); return _client.SendActivityAsync(activity, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } @@ -79,7 +79,7 @@ public Task CreateAsync(ConversationParameters reque public Task UpdateActivityAsync(string conversationId, string id, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); - activity.ServiceUrl ??= _serviceUrl; + activity.ServiceUrl ??= options.ServiceUrl ?? _serviceUrl; BotRequestContext? requestContext = BotRequestContext.Merge( options.ToBotRequestContext(_defaultAgenticIdentity), BotRequestContext.FromActivity(activity)); @@ -93,7 +93,7 @@ public Task UpdateActivityAsync(string conversationId, s { ArgumentNullException.ThrowIfNull(activity); activity.ReplyToId = id; - activity.ServiceUrl ??= _serviceUrl; + activity.ServiceUrl ??= options.ServiceUrl ?? _serviceUrl; activity.Conversation ??= new Conversation(conversationId); return _client.SendActivityAsync(activity, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } @@ -109,7 +109,7 @@ public Task DeleteActivityAsync(string conversationId, string id, AgenticIdentit /// public Task DeleteActivityAsync(string conversationId, string id, RequestOptions options, CancellationToken cancellationToken = default) { - return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); + return _client.DeleteActivityAsync(conversationId, id, options.ServiceUrl ?? _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } /// @@ -117,7 +117,7 @@ public Task DeleteActivityAsync(string conversationId, string id, RequestOptions /// public async Task> GetActivityMembersAsync(string conversationId, string id, RequestOptions options = default, CancellationToken cancellationToken = default) { - IList members = await _client.GetActivityMembersAsync(conversationId, id, _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken).ConfigureAwait(false); + IList members = await _client.GetActivityMembersAsync(conversationId, id, options.ServiceUrl ?? _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken).ConfigureAwait(false); return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))]; } @@ -129,7 +129,7 @@ public Task DeleteActivityAsync(string conversationId, string id, RequestOptions public Task CreateTargetedActivityAsync(string conversationId, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); - activity.ServiceUrl ??= _serviceUrl; + activity.ServiceUrl ??= options.ServiceUrl ?? _serviceUrl; activity.Conversation ??= new Conversation(conversationId); if (activity.Recipient is not null) { @@ -145,7 +145,7 @@ public Task DeleteActivityAsync(string conversationId, string id, RequestOptions public Task UpdateTargetedActivityAsync(string conversationId, string id, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); - activity.ServiceUrl ??= _serviceUrl; + activity.ServiceUrl ??= options.ServiceUrl ?? _serviceUrl; BotRequestContext? requestContext = BotRequestContext.Merge( options.ToBotRequestContext(_defaultAgenticIdentity), BotRequestContext.FromActivity(activity)); @@ -163,7 +163,7 @@ public Task DeleteTargetedActivityAsync(string conversationId, string id, Agenti /// public Task DeleteTargetedActivityAsync(string conversationId, string id, RequestOptions options, CancellationToken cancellationToken = default) { - return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); + return _client.DeleteTargetedActivityAsync(conversationId, id, options.ServiceUrl ?? _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken); } #endregion diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/RequestOptions.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/RequestOptions.cs index a08286e3..1a3c5e73 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/RequestOptions.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/RequestOptions.cs @@ -11,6 +11,11 @@ namespace Microsoft.Teams.Apps.Api.Clients; /// public readonly record struct RequestOptions { + /// + /// Gets the service URL for this request. + /// + public Uri? ServiceUrl { get; init; } + /// /// Gets the agentic identity to authenticate as for this request. /// diff --git a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs index ae7c6f27..36e3f222 100644 --- a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs +++ b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs @@ -126,6 +126,24 @@ public async Task ForInboundActivity_UsesServiceUrlAndRecipientAgenticIdentity() AssertIdentity(expected, request.AgenticIdentity); } + [Fact] + public async Task ForRequestOptions_UsesServiceUrlAndAgenticIdentity() + { + CapturingHttpMessageHandler handler = new(); + AgenticIdentity expected = DefaultIdentity(); + ApiClient api = CreateApiClient(handler).ForRequestOptions(new RequestOptions + { + ServiceUrl = ServiceUrl, + AgenticIdentity = expected + }); + + await api.Conversations.GetMembersPagedAsync("conversation-id"); + + Assert.Equal(ServiceUrl, api.ServiceUrl); + CapturedRequest request = Assert.Single(handler.Requests); + AssertIdentity(expected, request.AgenticIdentity); + } + [Fact] public void ForInboundActivity_ThrowsWhenServiceUrlIsMissing() { @@ -136,6 +154,16 @@ public void ForInboundActivity_ThrowsWhenServiceUrlIsMissing() Assert.Equal("Activity.ServiceUrl is required to use the Api client.", exception.Message); } + [Fact] + public void ForRequestOptions_ThrowsWhenServiceUrlIsMissing() + { + ApiClient api = CreateApiClient(new CapturingHttpMessageHandler()); + + InvalidOperationException exception = Assert.Throws(() => api.ForRequestOptions(new RequestOptions())); + + Assert.Equal("RequestOptions.ServiceUrl is required to scope the Api client.", exception.Message); + } + [Fact] public async Task ConversationApi_ActivityMethodsUseDefaultIdentityWhenExplicitIdentityIsMissing() { @@ -202,6 +230,19 @@ public async Task ConversationApi_ActivityFromIdentityOverridesFallbackIdentity( }); } + [Fact] + public async Task ConversationApi_ActivityRequestOptionsServiceUrlOverridesScopedServiceUrl() + { + CapturingHttpMessageHandler handler = new(); + Uri serviceUrl = new("https://override.example.com/"); + ConversationApiClient conversations = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations; + + await conversations.CreateActivityAsync("conversation-id", CreateActivity(), new RequestOptions { ServiceUrl = serviceUrl }); + + CapturedRequest request = Assert.Single(handler.Requests); + Assert.StartsWith(serviceUrl.ToString(), request.Url, StringComparison.Ordinal); + } + [Fact] public async Task ActivityClient_UsesDefaultIdentityWhenExplicitIdentityIsMissing() { From 6f99b077fa84fd9397da8a58e6ca23cc52c67198 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 15:06:06 -0700 Subject: [PATCH 21/23] Remove identity service URL overload Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Api/Clients/ApiClient.cs | 21 +------- .../AgenticIdentityDefaultsTests.cs | 54 +++++++++++++++---- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs index da8f0964..eff84b81 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs @@ -21,7 +21,7 @@ namespace Microsoft.Teams.Apps.Api.Clients; /// /// /// DI-friendly (no serviceUrl) — Use ApiClient(HttpClient, ConversationClient, UserTokenClient, ILogger) -/// and call or per-request to create a scoped instance. +/// and call , , or per-request to create a scoped instance. /// Fully initialized — Use ApiClient(Uri, HttpClient, ConversationClient, UserTokenClient, ILogger) /// when the service URL is known upfront. /// @@ -39,7 +39,7 @@ public class ApiClient /// /// The service URL used by this client. /// Null when constructed without a service URL (DI-friendly constructor). - /// Call to create a scoped instance with a service URL. + /// Call or to create a scoped instance with a service URL. /// public virtual Uri ServiceUrl { get; } @@ -146,23 +146,6 @@ public virtual ApiClient ForServiceUrl(Uri serviceUrl) return ForRequestOptions(new RequestOptions { ServiceUrl = serviceUrl }); } - /// - /// Creates a new scoped to the specified service URL with a default agentic identity - /// used when per-call methods do not receive an explicit identity. - /// - /// The Bot Framework service URL for this scope. - /// The default agentic identity for service URL-bound sub-clients. - /// A new bound to the given service URL and default identity. - public virtual ApiClient ForServiceUrl(Uri serviceUrl, AgenticIdentity? defaultAgenticIdentity) - { - ArgumentNullException.ThrowIfNull(serviceUrl); - return ForRequestOptions(new RequestOptions - { - ServiceUrl = serviceUrl, - AgenticIdentity = defaultAgenticIdentity - }); - } - /// /// Creates a new scoped to the service URL and default agentic identity in the request options. /// diff --git a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs index 36e3f222..c27f6668 100644 --- a/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs +++ b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs @@ -77,7 +77,11 @@ public async Task ScopedApi_ServiceUrlBoundClientsUseDefaultIdentity() { CapturingHttpMessageHandler handler = new(); AgenticIdentity expected = DefaultIdentity(); - ApiClient api = CreateApiClient(handler).ForServiceUrl(ServiceUrl, expected); + ApiClient api = CreateApiClient(handler).ForRequestOptions(new RequestOptions + { + ServiceUrl = ServiceUrl, + AgenticIdentity = expected + }); await api.Conversations.CreateAsync(new ConversationParameters()); await api.Conversations.GetMemberByIdAsync("conversation-id", "member-id"); @@ -94,7 +98,11 @@ public async Task ScopedApi_ExplicitIdentityOverridesDefaultIdentity() { CapturingHttpMessageHandler handler = new(); AgenticIdentity explicitIdentity = ExplicitIdentity(); - ApiClient api = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()); + ApiClient api = CreateApiClient(handler).ForRequestOptions(new RequestOptions + { + ServiceUrl = ServiceUrl, + AgenticIdentity = DefaultIdentity() + }); await api.Meetings.GetParticipantAsync("meeting-id", "participant-id", "tenant-id", explicitIdentity); @@ -169,7 +177,11 @@ public async Task ConversationApi_ActivityMethodsUseDefaultIdentityWhenExplicitI { CapturingHttpMessageHandler handler = new(); AgenticIdentity expected = DefaultIdentity(); - ConversationApiClient conversations = CreateApiClient(handler).ForServiceUrl(ServiceUrl, expected).Conversations; + ConversationApiClient conversations = CreateApiClient(handler).ForRequestOptions(new RequestOptions + { + ServiceUrl = ServiceUrl, + AgenticIdentity = expected + }).Conversations; await conversations.CreateActivityAsync("conversation-id", CreateActivity()); await conversations.UpdateActivityAsync("conversation-id", "activity-id", CreateActivity()); @@ -189,7 +201,11 @@ public async Task ConversationApi_ActivityMethodsExplicitIdentityOverridesDefaul { CapturingHttpMessageHandler handler = new(); AgenticIdentity explicitIdentity = ExplicitIdentity(); - ConversationApiClient conversations = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations; + ConversationApiClient conversations = CreateApiClient(handler).ForRequestOptions(new RequestOptions + { + ServiceUrl = ServiceUrl, + AgenticIdentity = DefaultIdentity() + }).Conversations; RequestOptions options = CreateRequestOptions(explicitIdentity); await conversations.CreateActivityAsync("conversation-id", CreateActivity(), options); @@ -216,7 +232,11 @@ public async Task ConversationApi_ActivityFromIdentityOverridesFallbackIdentity( AgenticAppBlueprintId = "from-agentic-blueprint-id", TenantId = "from-tenant-id" }; - ConversationApiClient conversations = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations; + ConversationApiClient conversations = CreateApiClient(handler).ForRequestOptions(new RequestOptions + { + ServiceUrl = ServiceUrl, + AgenticIdentity = DefaultIdentity() + }).Conversations; RequestOptions options = CreateRequestOptions(ExplicitIdentity()); await conversations.CreateActivityAsync("conversation-id", CreateActivity(fromIdentity), options); @@ -235,7 +255,11 @@ public async Task ConversationApi_ActivityRequestOptionsServiceUrlOverridesScope { CapturingHttpMessageHandler handler = new(); Uri serviceUrl = new("https://override.example.com/"); - ConversationApiClient conversations = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations; + ConversationApiClient conversations = CreateApiClient(handler).ForRequestOptions(new RequestOptions + { + ServiceUrl = ServiceUrl, + AgenticIdentity = DefaultIdentity() + }).Conversations; await conversations.CreateActivityAsync("conversation-id", CreateActivity(), new RequestOptions { ServiceUrl = serviceUrl }); @@ -249,7 +273,11 @@ public async Task ActivityClient_UsesDefaultIdentityWhenExplicitIdentityIsMissin CapturingHttpMessageHandler handler = new(); AgenticIdentity expected = DefaultIdentity(); #pragma warning disable CS0618 // Verifies backward-compatible obsolete wrapper behavior. - ActivityClient activities = CreateApiClient(handler).ForServiceUrl(ServiceUrl, expected).Conversations.Activities; + ActivityClient activities = CreateApiClient(handler).ForRequestOptions(new RequestOptions + { + ServiceUrl = ServiceUrl, + AgenticIdentity = expected + }).Conversations.Activities; #pragma warning restore CS0618 await activities.CreateAsync("conversation-id", CreateActivity()); @@ -271,7 +299,11 @@ public async Task ActivityClient_ExplicitIdentityOverridesDefaultIdentity() CapturingHttpMessageHandler handler = new(); AgenticIdentity explicitIdentity = ExplicitIdentity(); #pragma warning disable CS0618 // Verifies backward-compatible obsolete wrapper behavior. - ActivityClient activities = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations.Activities; + ActivityClient activities = CreateApiClient(handler).ForRequestOptions(new RequestOptions + { + ServiceUrl = ServiceUrl, + AgenticIdentity = DefaultIdentity() + }).Conversations.Activities; #pragma warning restore CS0618 RequestOptions options = CreateRequestOptions(explicitIdentity); @@ -300,7 +332,11 @@ public async Task ActivityClient_ActivityFromIdentityOverridesFallbackIdentity() TenantId = "from-tenant-id" }; #pragma warning disable CS0618 // Verifies backward-compatible obsolete wrapper behavior. - ActivityClient activities = CreateApiClient(handler).ForServiceUrl(ServiceUrl, DefaultIdentity()).Conversations.Activities; + ActivityClient activities = CreateApiClient(handler).ForRequestOptions(new RequestOptions + { + ServiceUrl = ServiceUrl, + AgenticIdentity = DefaultIdentity() + }).Conversations.Activities; #pragma warning restore CS0618 RequestOptions options = CreateRequestOptions(ExplicitIdentity()); From 2dd0b5d7e54dc36ff0a30218f2ce41df6042d4a1 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 15:12:51 -0700 Subject: [PATCH 22/23] Simplify request options documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs index eff84b81..cb4f478a 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs @@ -147,7 +147,7 @@ public virtual ApiClient ForServiceUrl(Uri serviceUrl) } /// - /// Creates a new scoped to the service URL and default agentic identity in the request options. + /// Creates a new with defaults from the request options. /// /// The request options. is required. /// A new bound to the request options. From 262c2de3e3759ec69a29a4515c795d14758d0517 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 15:13:27 -0700 Subject: [PATCH 23/23] Generalize ApiClient defaults documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs index cb4f478a..933a6524 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs @@ -158,11 +158,11 @@ public virtual ApiClient ForRequestOptions(RequestOptions options) } /// - /// Creates a new scoped to an inbound activity's service URL and recipient agentic identity. + /// Creates a new with defaults from the inbound activity. /// - /// The inbound activity for this scope. - /// A new bound to the inbound activity's service URL and default identity. - /// Thrown when does not include a service URL. + /// The inbound activity that supplies defaults for this scope. + /// A new with defaults from the inbound activity. + /// Thrown when does not include required API defaults. public virtual ApiClient ForInboundActivity(TeamsActivity activity) { ArgumentNullException.ThrowIfNull(activity);