Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6865db6
Add agentic identity defaults to app API
heyitsaamir Jul 10, 2026
932a337
Hide request context from app API defaults
heyitsaamir Jul 10, 2026
dbf4f55
Simplify activity identity parameters
heyitsaamir Jul 10, 2026
96505f3
Inline app request context resolution
heyitsaamir Jul 10, 2026
cbc4e16
Restore ApiClient doc links
heyitsaamir Jul 10, 2026
81879dd
Add agent XML documentation guidance
heyitsaamir Jul 10, 2026
8acbf2b
Remove Claude instruction file
heyitsaamir Jul 10, 2026
ca8467e
Condense XML doc guidance
heyitsaamir Jul 10, 2026
38c7f36
Clarify effective identity request options
heyitsaamir Jul 10, 2026
5a53f89
Inline Teams request options
heyitsaamir Jul 10, 2026
e0ba479
Apply identity defaults to flattened API
heyitsaamir Jul 10, 2026
792734d
Add inbound activity API scoping
heyitsaamir Jul 10, 2026
0c2611f
Clarify activity update request context
heyitsaamir Jul 10, 2026
c253d96
Clarify agentic tenant documentation
heyitsaamir Jul 10, 2026
521a656
Add API request options for activity calls
heyitsaamir Jul 10, 2026
09bd658
Remove activity additional headers support
heyitsaamir Jul 10, 2026
9815ae0
Rename activity request options
heyitsaamir Jul 10, 2026
55b5fb7
Document breaking change PR guidance
heyitsaamir Jul 10, 2026
1f93eea
Centralize request options conversion
heyitsaamir Jul 10, 2026
ffd8251
Add service URL to request options
heyitsaamir Jul 10, 2026
6f99b07
Remove identity service URL overload
heyitsaamir Jul 10, 2026
2dd0b5d
Simplify request options documentation
heyitsaamir Jul 10, 2026
262c2de
Generalize ApiClient defaults documentation
heyitsaamir Jul 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Agent Instructions

## XML documentation

- Prefer `<see cref="..."/>` for linkable XML doc references, including explicit overloads like `<see cref="ForServiceUrl(Uri)"/>`; use `<c>...</c>` 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.
70 changes: 50 additions & 20 deletions core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,88 +20,117 @@ 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;
}

/// <summary>
/// Create a new activity in a conversation.
/// </summary>
public Task<SendActivityResponse?> CreateAsync(string conversationId, CoreActivity activity, Dictionary<string, string>? additionalHeaders = null, CancellationToken cancellationToken = default)
public Task<SendActivityResponse?> 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);
}

/// <summary>
/// Update an existing activity in a conversation.
/// </summary>
public Task<UpdateActivityResponse> UpdateAsync(string conversationId, string id, CoreActivity activity, Dictionary<string, string>? additionalHeaders = null, CancellationToken cancellationToken = default)
public Task<UpdateActivityResponse> 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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same, add merge to core client

options.ToBotRequestContext(_defaultAgenticIdentity),
BotRequestContext.FromActivity(activity));
return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: requestContext, cancellationToken: cancellationToken);
}

/// <summary>
/// Reply to an existing activity in a conversation.
/// </summary>
public Task<SendActivityResponse?> ReplyAsync(string conversationId, string id, CoreActivity activity, Dictionary<string, string>? additionalHeaders = null, CancellationToken cancellationToken = default)
public Task<SendActivityResponse?> 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);
}

/// <summary>
/// Delete an activity from a conversation.
/// </summary>
public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary<string, string>? 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);

/// <summary>
/// Delete an activity from a conversation.
/// </summary>
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);
}

/// <summary>
/// Get members of a specific activity.
/// </summary>
public async Task<IList<TeamsChannelAccount?>> GetMembersAsync(string conversationId, string id, RequestOptions options = default, CancellationToken cancellationToken = default)
{
IList<ChannelAccount> members = await _client.GetActivityMembersAsync(conversationId, id, options.ServiceUrl ?? _serviceUrl, requestContext: options.ToBotRequestContext(_defaultAgenticIdentity), cancellationToken: cancellationToken).ConfigureAwait(false);
return [.. members.Select(m => TeamsChannelAccount.FromChannelAccount(m))];
}

/// <summary>
/// Create a new targeted activity in a conversation.
/// Targeted activities are only visible to the specified recipient.
/// </summary>
[Experimental("ExperimentalTeamsTargeted")]
public Task<SendActivityResponse?> CreateTargetedAsync(string conversationId, CoreActivity activity, Dictionary<string, string>? additionalHeaders = null, CancellationToken cancellationToken = default)
public Task<SendActivityResponse?> 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);
}

/// <summary>
/// Update an existing targeted activity in a conversation.
/// </summary>
[Experimental("ExperimentalTeamsTargeted")]
public Task<UpdateActivityResponse> UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, Dictionary<string, string>? additionalHeaders = null, CancellationToken cancellationToken = default)
public Task<UpdateActivityResponse> 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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

add this merge to conversation client in core instead for consistency

options.ToBotRequestContext(_defaultAgenticIdentity),
BotRequestContext.FromActivity(activity));
return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: requestContext, cancellationToken: cancellationToken);
}

/// <summary>
/// Delete a targeted activity from a conversation.
/// </summary>
public Task DeleteTargetedAsync(string conversationId, string id, AgenticIdentity? agenticIdentity = null, Dictionary<string, string>? 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);

/// <summary>
/// Delete a targeted activity from a conversation.
/// </summary>
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);
}
}
67 changes: 52 additions & 15 deletions core/src/Microsoft.Teams.Apps/Api/Clients/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,9 +20,9 @@ namespace Microsoft.Teams.Apps.Api.Clients;
/// This client can be constructed in two ways:
/// </para>
/// <list type="bullet">
/// <item><b>DI-friendly (no serviceUrl)</b> — Use <see cref="ApiClient(HttpClient, CoreConversationClient, CoreUserTokenClient, ILogger)"/>
/// and call <see cref="ForServiceUrl"/> per-request to create a scoped instance.</item>
/// <item><b>Fully initialized</b> — Use <see cref="ApiClient(Uri, HttpClient, CoreConversationClient, CoreUserTokenClient, ILogger)"/>
/// <item><b>DI-friendly (no serviceUrl)</b> — Use <c>ApiClient(HttpClient, ConversationClient, UserTokenClient, ILogger)</c>
/// and call <see cref="ForServiceUrl(Uri)"/>, <see cref="ForRequestOptions(RequestOptions)"/>, or <see cref="ForInboundActivity(TeamsActivity)"/> per-request to create a scoped instance.</item>
/// <item><b>Fully initialized</b> — Use <c>ApiClient(Uri, HttpClient, ConversationClient, UserTokenClient, ILogger)</c>
/// when the service URL is known upfront.</item>
/// </list>
/// </remarks>
Expand All @@ -32,10 +34,12 @@ public class ApiClient

internal CoreUserTokenClient UserTokenClient { get; }

internal AgenticIdentity? DefaultAgenticIdentity { get; }

/// <summary>
/// The service URL used by this client.
/// Null when constructed without a service URL (DI-friendly constructor).
/// Call <see cref="ForServiceUrl"/> to create a scoped instance with a service URL.
/// Call <see cref="ForServiceUrl(Uri)"/> or <see cref="ForRequestOptions(RequestOptions)"/> to create a scoped instance with a service URL.
/// </summary>
public virtual Uri ServiceUrl { get; }

Expand Down Expand Up @@ -64,7 +68,7 @@ public class ApiClient

/// <summary>
/// Creates a new <see cref="ApiClient"/> without a service URL (DI-friendly).
/// Use <see cref="ForServiceUrl"/> to create a scoped instance bound to a specific service URL.
/// Use <see cref="ForServiceUrl(Uri)"/> to create a scoped instance bound to a specific service URL.
/// </summary>
/// <param name="httpClient">An <see cref="HttpClient"/> configured with authentication (e.g., via DI with <c>BotAuthenticationHandler</c>).</param>
/// <param name="conversationClient">The core conversation client for conversation/activity/member operations.</param>
Expand All @@ -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!;
Expand All @@ -96,7 +101,8 @@ internal ApiClient(HttpClient httpClient, CoreConversationClient conversationCli
/// <param name="conversationClient">The core conversation client for conversation/activity/member operations.</param>
/// <param name="userTokenClient">The core user token client for sign-in and token operations.</param>
/// <param name="logger">Optional logger.</param>
internal ApiClient(Uri serviceUrl, HttpClient httpClient, CoreConversationClient conversationClient, CoreUserTokenClient userTokenClient, ILogger? logger = null)
/// <param name="defaultAgenticIdentity">Optional default agentic identity for service URL-bound sub-clients.</param>
internal ApiClient(Uri serviceUrl, HttpClient httpClient, CoreConversationClient conversationClient, CoreUserTokenClient userTokenClient, ILogger? logger = null, AgenticIdentity? defaultAgenticIdentity = null)
{
ArgumentNullException.ThrowIfNull(serviceUrl);
ArgumentNullException.ThrowIfNull(httpClient);
Expand All @@ -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);
}

/// <summary>
Expand All @@ -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 });
}

/// <summary>
/// Creates a new <see cref="ApiClient"/> with defaults from the request options.
/// </summary>
/// <param name="options">The request options. <see cref="RequestOptions.ServiceUrl"/> is required.</param>
/// <returns>A new <see cref="ApiClient"/> bound to the request options.</returns>
/// <exception cref="InvalidOperationException">Thrown when <see cref="RequestOptions.ServiceUrl"/> is not provided.</exception>
public virtual ApiClient ForRequestOptions(RequestOptions options)
{
return new ApiClient(_http, ConversationClient, UserTokenClient, options);
}

/// <summary>
/// Creates a new <see cref="ApiClient"/> with defaults from the inbound activity.
/// </summary>
/// <param name="activity">The inbound activity that supplies defaults for this scope.</param>
/// <returns>A new <see cref="ApiClient"/> with defaults from the inbound activity.</returns>
/// <exception cref="InvalidOperationException">Thrown when <paramref name="activity"/> does not include required API defaults.</exception>
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()
});
}
}
Loading
Loading