diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..f17c774a
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,9 @@
+# Agent Instructions
+
+## 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.
diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs
index 1cf236fa..8a1601fe 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,52 +20,72 @@ public class ActivityClient
{
private readonly CoreConversationClient _client;
private readonly Uri _serviceUrl;
+ private readonly AgenticIdentity? _defaultAgenticIdentity;
- internal ActivityClient(Uri serviceUrl, CoreConversationClient client)
+ internal ActivityClient(Uri serviceUrl, CoreConversationClient client, AgenticIdentity? defaultAgenticIdentity = null)
{
_serviceUrl = serviceUrl;
_client = client;
+ _defaultAgenticIdentity = defaultAgenticIdentity;
}
///
/// Create a new activity in a conversation.
///
- public Task CreateAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default)
+ 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, customHeaders: additionalHeaders, cancellationToken: cancellationToken);
+ return _client.SendActivityAsync(activity, requestContext: options.ToBotRequestContext(_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)
+ public Task UpdateAsync(string conversationId, string id, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(activity);
- activity.ServiceUrl ??= _serviceUrl;
- return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.FromActivity(activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken);
+ activity.ServiceUrl ??= options.ServiceUrl ?? _serviceUrl;
+ BotRequestContext? requestContext = BotRequestContext.Merge(
+ options.ToBotRequestContext(_defaultAgenticIdentity),
+ BotRequestContext.FromActivity(activity));
+ 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)
+ public Task ReplyAsync(string conversationId, string id, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(activity);
activity.ReplyToId = id;
- activity.ServiceUrl ??= _serviceUrl;
+ activity.ServiceUrl ??= options.ServiceUrl ?? _serviceUrl;
activity.Conversation ??= new Conversation(conversationId);
- return _client.SendActivityAsync(activity, customHeaders: additionalHeaders, cancellationToken: cancellationToken);
+ return _client.SendActivityAsync(activity, requestContext: options.ToBotRequestContext(_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)
+ public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default)
+ => DeleteAsync(conversationId, id, new RequestOptions { AgenticIdentity = agenticIdentity }, cancellationToken);
+
+ ///
+ /// Delete an activity from a conversation.
+ ///
+ public Task DeleteAsync(string conversationId, string id, RequestOptions options, CancellationToken cancellationToken = default)
{
- return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken);
+ return _client.DeleteActivityAsync(conversationId, id, options.ServiceUrl ?? _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken);
+ }
+
+ ///
+ /// Get members of a specific activity.
+ ///
+ public async Task> GetMembersAsync(string conversationId, string id, RequestOptions options = default, CancellationToken cancellationToken = default)
+ {
+ 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))];
}
///
@@ -72,35 +93,44 @@ 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)
+ 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)
{
activity.Recipient.IsTargeted = true;
}
- return _client.SendActivityAsync(activity, customHeaders: additionalHeaders, cancellationToken: cancellationToken);
+ return _client.SendActivityAsync(activity, requestContext: options.ToBotRequestContext(_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)
+ public Task UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(activity);
- activity.ServiceUrl ??= _serviceUrl;
- return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.FromActivity(activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken);
+ activity.ServiceUrl ??= options.ServiceUrl ?? _serviceUrl;
+ BotRequestContext? requestContext = BotRequestContext.Merge(
+ options.ToBotRequestContext(_defaultAgenticIdentity),
+ BotRequestContext.FromActivity(activity));
+ 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)
+ public Task DeleteTargetedAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default)
+ => DeleteTargetedAsync(conversationId, id, new RequestOptions { AgenticIdentity = agenticIdentity }, cancellationToken);
+
+ ///
+ /// Delete a targeted activity from a conversation.
+ ///
+ public Task DeleteTargetedAsync(string conversationId, string id, RequestOptions options, CancellationToken cancellationToken = default)
{
- return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, 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 2d17636a..933a6524 100644
--- a/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs
+++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs
@@ -3,7 +3,9 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Microsoft.Teams.Apps.Schema;
using Microsoft.Teams.Core.Http;
+using Microsoft.Teams.Core.Schema;
using CoreConversationClient = Microsoft.Teams.Core.ConversationClient;
using CoreUserTokenClient = Microsoft.Teams.Core.UserTokenClient;
@@ -18,9 +20,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 , , or 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 +34,12 @@ public class ApiClient
internal CoreUserTokenClient UserTokenClient { get; }
+ internal AgenticIdentity? DefaultAgenticIdentity { 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 or to create a scoped instance with a service URL.
///
public virtual Uri ServiceUrl { get; }
@@ -64,7 +68,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 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 +84,7 @@ internal ApiClient(HttpClient httpClient, CoreConversationClient conversationCli
_http = new BotHttpClient(httpClient, logger);
ConversationClient = conversationClient;
UserTokenClient = userTokenClient;
+ DefaultAgenticIdentity = null;
// ServiceUrl-dependent sub-clients require ForServiceUrl() before use
ServiceUrl = null!;
@@ -96,7 +101,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 +112,26 @@ internal ApiClient(Uri serviceUrl, HttpClient httpClient, CoreConversationClient
_http = new BotHttpClient(httpClient, logger);
ConversationClient = conversationClient;
UserTokenClient = userTokenClient;
+ DefaultAgenticIdentity = 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, 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)
+ // 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 = options.AgenticIdentity;
ServiceUrl = serviceUrl;
- Conversations = new ConversationApiClient(serviceUrl, conversationClient);
- Teams = new TeamClient(serviceUrl.ToString(), http);
- Meetings = new MeetingClient(serviceUrl.ToString(), http);
+ Conversations = new ConversationApiClient(serviceUrl, conversationClient, DefaultAgenticIdentity);
+ Teams = new TeamClient(serviceUrl.ToString(), http, DefaultAgenticIdentity);
+ Meetings = new MeetingClient(serviceUrl.ToString(), http, DefaultAgenticIdentity);
}
///
@@ -133,6 +143,33 @@ private ApiClient(BotHttpClient http, CoreConversationClient conversationClient,
public virtual ApiClient ForServiceUrl(Uri serviceUrl)
{
ArgumentNullException.ThrowIfNull(serviceUrl);
- return new ApiClient(_http, ConversationClient, UserTokenClient, serviceUrl);
+ return ForRequestOptions(new RequestOptions { ServiceUrl = serviceUrl });
+ }
+
+ ///
+ /// Creates a new with defaults from 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);
+ }
+
+ ///
+ /// Creates a new with defaults from the inbound activity.
+ ///
+ /// 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);
+ 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 26b49bd0..df6f2471 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 AgenticIdentity? _defaultAgenticIdentity;
///
/// 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, AgenticIdentity? defaultAgenticIdentity = null)
{
_serviceUrl = serviceUrl;
_client = client;
+ _defaultAgenticIdentity = defaultAgenticIdentity;
#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, defaultAgenticIdentity);
+ Members = new MemberClient(serviceUrl, client, defaultAgenticIdentity);
+ Reactions = new ReactionClient(serviceUrl, client, defaultAgenticIdentity);
#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: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken);
}
#region Activity Methods
@@ -63,42 +65,60 @@ 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, 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, customHeaders: additionalHeaders, cancellationToken: cancellationToken);
+ return _client.SendActivityAsync(activity, requestContext: options.ToBotRequestContext(_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)
+ public Task UpdateActivityAsync(string conversationId, string id, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(activity);
- activity.ServiceUrl ??= _serviceUrl;
- return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.FromActivity(activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken);
+ activity.ServiceUrl ??= options.ServiceUrl ?? _serviceUrl;
+ BotRequestContext? requestContext = BotRequestContext.Merge(
+ options.ToBotRequestContext(_defaultAgenticIdentity),
+ BotRequestContext.FromActivity(activity));
+ 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)
+ public Task ReplyToActivityAsync(string conversationId, string id, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(activity);
activity.ReplyToId = id;
- activity.ServiceUrl ??= _serviceUrl;
+ activity.ServiceUrl ??= options.ServiceUrl ?? _serviceUrl;
activity.Conversation ??= new Conversation(conversationId);
- return _client.SendActivityAsync(activity, customHeaders: additionalHeaders, cancellationToken: cancellationToken);
+ return _client.SendActivityAsync(activity, requestContext: options.ToBotRequestContext(_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)
+ public Task DeleteActivityAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default)
+ => DeleteActivityAsync(conversationId, id, new RequestOptions { AgenticIdentity = agenticIdentity }, cancellationToken);
+
+ ///
+ /// Delete an activity from a conversation.
+ ///
+ public Task DeleteActivityAsync(string conversationId, string id, RequestOptions options, CancellationToken cancellationToken = default)
+ {
+ return _client.DeleteActivityAsync(conversationId, id, options.ServiceUrl ?? _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken);
+ }
+
+ ///
+ /// Get members of a specific activity.
+ ///
+ public async Task> GetActivityMembersAsync(string conversationId, string id, RequestOptions options = default, CancellationToken cancellationToken = default)
{
- return _client.DeleteActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken);
+ 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))];
}
///
@@ -106,35 +126,44 @@ 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, 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)
{
activity.Recipient.IsTargeted = true;
}
- return _client.SendActivityAsync(activity, customHeaders: additionalHeaders, cancellationToken: cancellationToken);
+ return _client.SendActivityAsync(activity, requestContext: options.ToBotRequestContext(_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)
+ public Task UpdateTargetedActivityAsync(string conversationId, string id, CoreActivity activity, RequestOptions options = default, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(activity);
- activity.ServiceUrl ??= _serviceUrl;
- return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.FromActivity(activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken);
+ activity.ServiceUrl ??= options.ServiceUrl ?? _serviceUrl;
+ BotRequestContext? requestContext = BotRequestContext.Merge(
+ options.ToBotRequestContext(_defaultAgenticIdentity),
+ BotRequestContext.FromActivity(activity));
+ 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)
+ public Task DeleteTargetedActivityAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, CancellationToken cancellationToken = default)
+ => DeleteTargetedActivityAsync(conversationId, id, new RequestOptions { AgenticIdentity = agenticIdentity }, cancellationToken);
+
+ ///
+ /// Delete a targeted activity from a conversation.
+ ///
+ public Task DeleteTargetedActivityAsync(string conversationId, string id, RequestOptions options, CancellationToken cancellationToken = default)
{
- return _client.DeleteTargetedActivityAsync(conversationId, id, _serviceUrl, requestContext: BotRequestContext.FromAgenticIdentity(agenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken);
+ return _client.DeleteTargetedActivityAsync(conversationId, id, options.ServiceUrl ?? _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken);
}
#endregion
@@ -147,7 +176,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))];
}
@@ -167,7 +196,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();
@@ -226,7 +255,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);
}
///
@@ -253,7 +282,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);
}
///
@@ -267,7 +296,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/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/MeetingClient.cs
index 43485ee3..c8bdd4d5 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 AgenticIdentity? _defaultAgenticIdentity;
- internal MeetingClient(string serviceUrl, BotHttpClient http)
+ internal MeetingClient(string serviceUrl, BotHttpClient http, AgenticIdentity? defaultAgenticIdentity = null)
{
_serviceUrl = serviceUrl.TrimEnd('/');
_http = http;
+ _defaultAgenticIdentity = defaultAgenticIdentity;
}
///
@@ -27,7 +29,7 @@ internal MeetingClient(string serviceUrl, BotHttpClient http)
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);
}
///
@@ -36,11 +38,8 @@ internal MeetingClient(string serviceUrl, BotHttpClient http)
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);
+ return await _http.SendAsync(HttpMethod.Get, url, body: null, options: new BotRequestOptions { RequestContext = BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity) }, cancellationToken).ConfigureAwait(false);
}
-
- private static BotRequestOptions? CreateRequestOptions(AgenticIdentity? agenticIdentity) =>
- agenticIdentity is null ? null : new() { RequestContext = BotRequestContext.FromAgenticIdentity(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..bb8d23fd 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 AgenticIdentity? _defaultAgenticIdentity;
- internal MemberClient(Uri serviceUrl, CoreConversationClient client)
+ internal MemberClient(Uri serviceUrl, CoreConversationClient client, AgenticIdentity? defaultAgenticIdentity = null)
{
_serviceUrl = serviceUrl;
_client = client;
+ _defaultAgenticIdentity = defaultAgenticIdentity;
}
///
@@ -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: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), 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: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity),
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: 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 88419ec9..3b6e64ed 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 AgenticIdentity? _defaultAgenticIdentity;
- internal ReactionClient(Uri serviceUrl, CoreConversationClient client)
+ internal ReactionClient(Uri serviceUrl, CoreConversationClient client, AgenticIdentity? defaultAgenticIdentity = null)
{
_serviceUrl = serviceUrl;
_client = client;
+ _defaultAgenticIdentity = defaultAgenticIdentity;
}
///
@@ -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: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), 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: BotRequestContext.FromAgenticIdentity(agenticIdentity ?? _defaultAgenticIdentity), customHeaders: additionalHeaders, cancellationToken: cancellationToken);
}
}
diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/RequestOptions.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/RequestOptions.cs
new file mode 100644
index 00000000..1a3c5e73
--- /dev/null
+++ b/core/src/Microsoft.Teams.Apps/Api/Clients/RequestOptions.cs
@@ -0,0 +1,31 @@
+// 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;
+
+///
+/// Options for Apps API requests that need per-call settings beyond the required method parameters.
+///
+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.
+ ///
+ 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;
+}
diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/TeamClient.cs
index 4b3dd905..5e9de7ed 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 AgenticIdentity? _defaultAgenticIdentity;
- internal TeamClient(string serviceUrl, BotHttpClient http)
+ internal TeamClient(string serviceUrl, BotHttpClient http, AgenticIdentity? defaultAgenticIdentity = null)
{
_serviceUrl = serviceUrl.TrimEnd('/');
_http = http;
+ _defaultAgenticIdentity = defaultAgenticIdentity;
}
///
@@ -28,7 +30,7 @@ internal TeamClient(string serviceUrl, BotHttpClient http)
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);
}
///
@@ -37,13 +39,10 @@ internal TeamClient(string serviceUrl, BotHttpClient http)
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 static BotRequestOptions? CreateRequestOptions(AgenticIdentity? agenticIdentity) =>
- agenticIdentity is null ? null : new() { RequestContext = BotRequestContext.FromAgenticIdentity(agenticIdentity) };
-
private sealed class ConversationListResponse
{
[JsonPropertyName("conversations")]
diff --git a/core/src/Microsoft.Teams.Apps/Context.cs b/core/src/Microsoft.Teams.Apps/Context.cs
index 53851f38..a4bc2b45 100644
--- a/core/src/Microsoft.Teams.Apps/Context.cs
+++ b/core/src/Microsoft.Teams.Apps/Context.cs
@@ -48,8 +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."));
+ public ApiClient Api => _api ??= TeamsBotApplication.Api.ForInboundActivity(Activity);
// ==================== 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..9116b424 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 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)
+ {
+ 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..c27f6668
--- /dev/null
+++ b/core/test/Microsoft.Teams.Apps.UnitTests/AgenticIdentityDefaultsTests.cs
@@ -0,0 +1,529 @@
+// 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.GetMembersPagedAsync("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.GetMemberByIdAsync("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).ForRequestOptions(new RequestOptions
+ {
+ ServiceUrl = ServiceUrl,
+ AgenticIdentity = expected
+ });
+
+ await api.Conversations.CreateAsync(new ConversationParameters());
+ 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");
+
+ 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).ForRequestOptions(new RequestOptions
+ {
+ ServiceUrl = ServiceUrl,
+ AgenticIdentity = 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 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 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()
+ {
+ 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 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()
+ {
+ CapturingHttpMessageHandler handler = new();
+ AgenticIdentity expected = DefaultIdentity();
+ 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());
+ 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).ForRequestOptions(new RequestOptions
+ {
+ ServiceUrl = ServiceUrl,
+ AgenticIdentity = DefaultIdentity()
+ }).Conversations;
+ RequestOptions options = CreateRequestOptions(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));
+ }
+
+ [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).ForRequestOptions(new RequestOptions
+ {
+ ServiceUrl = ServiceUrl,
+ AgenticIdentity = DefaultIdentity()
+ }).Conversations;
+ RequestOptions options = CreateRequestOptions(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 =>
+ {
+ AssertIdentity(fromIdentity, request.AgenticIdentity);
+ Assert.Equal("from-bot-id", request.BotAppId);
+ });
+ }
+
+ [Fact]
+ public async Task ConversationApi_ActivityRequestOptionsServiceUrlOverridesScopedServiceUrl()
+ {
+ CapturingHttpMessageHandler handler = new();
+ Uri serviceUrl = new("https://override.example.com/");
+ ConversationApiClient conversations = CreateApiClient(handler).ForRequestOptions(new RequestOptions
+ {
+ ServiceUrl = ServiceUrl,
+ AgenticIdentity = 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()
+ {
+ CapturingHttpMessageHandler handler = new();
+ AgenticIdentity expected = DefaultIdentity();
+#pragma warning disable CS0618 // Verifies backward-compatible obsolete wrapper behavior.
+ ActivityClient activities = CreateApiClient(handler).ForRequestOptions(new RequestOptions
+ {
+ ServiceUrl = ServiceUrl,
+ AgenticIdentity = expected
+ }).Conversations.Activities;
+#pragma warning restore CS0618
+
+ 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();
+#pragma warning disable CS0618 // Verifies backward-compatible obsolete wrapper behavior.
+ ActivityClient activities = CreateApiClient(handler).ForRequestOptions(new RequestOptions
+ {
+ ServiceUrl = ServiceUrl,
+ AgenticIdentity = DefaultIdentity()
+ }).Conversations.Activities;
+#pragma warning restore CS0618
+ RequestOptions options = CreateRequestOptions(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));
+ }
+
+ [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"
+ };
+#pragma warning disable CS0618 // Verifies backward-compatible obsolete wrapper behavior.
+ ActivityClient activities = CreateApiClient(handler).ForRequestOptions(new RequestOptions
+ {
+ ServiceUrl = ServiceUrl,
+ AgenticIdentity = DefaultIdentity()
+ }).Conversations.Activities;
+#pragma warning restore CS0618
+ RequestOptions options = CreateRequestOptions(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 =>
+ {
+ 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 RequestOptions CreateRequestOptions(AgenticIdentity agenticIdentity)
+ => new() { AgenticIdentity = agenticIdentity };
+
+ 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