Add Api Clients with Reactions and Targeted Message support#334
Add Api Clients with Reactions and Targeted Message support#334
Conversation
Introduced a structured API facade (`TeamsBotApplication.Api`) for Teams operations, grouping functionality into Conversations, Users, Teams, Meetings, and Batch sub-APIs. Added new classes for each sub-API, with extensive XML documentation. Updated `TeamsBotApplication` to expose the new facade. Added integration tests for the API hierarchy and error handling. Cleaned up `.csproj` by removing unused RunSettings property. This refactor improves discoverability, organization, and developer experience for Teams API usage.
Added detailed tables to README.md mapping TeamsApi facade methods to their underlying REST endpoints. Documentation covers Conversations, Users (Token), Teams, Meetings, and Batch operations, including HTTP methods, endpoint paths, and relevant base URLs. Added notes on service URL usage and path parameter encoding.
There was a problem hiding this comment.
Pull request overview
This PR introduces a hierarchical Teams API facade surfaced as TeamsBotApplication.Api, organizing Teams operations into sub-APIs (Conversations/Activities/Members, Users/Token, Teams, Meetings, Batch) and adding integration tests to validate the facade structure and delegation.
Changes:
- Added
TeamsApifacade with sub-API classes for common Teams operations (activities, members, users/tokens, teams, meetings, batch). - Updated
TeamsBotApplicationto expose the facade via a newApiproperty. - Added integration tests covering API hierarchy and some error-handling behavior; removed an unused
RunSettingsFilePathproperty from the test project.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| core/test/Microsoft.Teams.Bot.Core.Tests/TeamsApiFacadeTests.cs | Adds integration tests for the new TeamsBotApplication.Api hierarchy and delegations. |
| core/test/Microsoft.Teams.Bot.Core.Tests/Microsoft.Teams.Bot.Core.Tests.csproj | Removes unused RunSettingsFilePath property group. |
| core/src/Microsoft.Teams.Bot.Apps/TeamsBotApplication.cs | Exposes new Api facade via lazy-initialized property. |
| core/src/Microsoft.Teams.Bot.Apps/Api/TeamsApi.cs | Defines top-level hierarchical facade and wires up sub-APIs. |
| core/src/Microsoft.Teams.Bot.Apps/Api/ConversationsApi.cs | Adds container for conversation-scoped APIs (Activities/Members). |
| core/src/Microsoft.Teams.Bot.Apps/Api/ActivitiesApi.cs | Adds activity operations wrapper around ConversationClient. |
| core/src/Microsoft.Teams.Bot.Apps/Api/MembersApi.cs | Adds conversation member operations wrapper around ConversationClient. |
| core/src/Microsoft.Teams.Bot.Apps/Api/UsersApi.cs | Adds container for user-scoped APIs (Token). |
| core/src/Microsoft.Teams.Bot.Apps/Api/UserTokenApi.cs | Adds user token operations wrapper around UserTokenClient. |
| core/src/Microsoft.Teams.Bot.Apps/Api/TeamsOperationsApi.cs | Adds team/channel operations wrapper around TeamsApiClient. |
| core/src/Microsoft.Teams.Bot.Apps/Api/MeetingsApi.cs | Adds meeting operations wrapper around TeamsApiClient. |
| core/src/Microsoft.Teams.Bot.Apps/Api/BatchApi.cs | Adds batch messaging and batch-operation management wrapper around TeamsApiClient. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ArgumentNullException.ThrowIfNull(activity); | ||
| return _client.GetTokenAsync( | ||
| activity.From.Id!, | ||
| connectionName, | ||
| activity.ChannelId!, | ||
| code, | ||
| cancellationToken); |
There was a problem hiding this comment.
The activity-context overload uses null-forgiving operators for required fields (e.g., activity.From.Id and activity.ChannelId) but doesn’t validate them. If ChannelId/From.Id are missing, null can flow into UserTokenClient and result in confusing downstream failures or invalid requests. Add explicit validation (e.g., ThrowIfNullOrWhiteSpace on From.Id and ChannelId) for all activity-context methods in this API to fail fast with clear exceptions.
| ArgumentNullException.ThrowIfNull(activity); | ||
| return _client.FetchParticipantAsync( | ||
| meetingId, | ||
| participantId, | ||
| activity.ChannelData?.Tenant?.Id ?? throw new InvalidOperationException("Tenant ID not available in activity"), | ||
| activity.ServiceUrl!, | ||
| activity.From.GetAgenticIdentity(), | ||
| customHeaders, | ||
| cancellationToken); |
There was a problem hiding this comment.
This throws InvalidOperationException when tenant ID is missing from the provided activity. Since this is invalid caller input (the argument doesn’t contain required data), prefer ArgumentException/ArgumentNullException (or ThrowIfNull/ThrowIfNullOrWhiteSpace) to keep exception semantics consistent with other argument validation across the clients.
| ArgumentNullException.ThrowIfNull(contextActivity); | ||
| return _client.SendMessageToListOfUsersAsync( | ||
| activity, | ||
| teamsMembers, | ||
| contextActivity.ChannelData?.Tenant?.Id ?? throw new InvalidOperationException("Tenant ID not available in activity"), | ||
| contextActivity.ServiceUrl!, | ||
| contextActivity.From.GetAgenticIdentity(), | ||
| customHeaders, | ||
| cancellationToken); |
There was a problem hiding this comment.
This throws InvalidOperationException when tenant ID is missing from the provided activity. Because the tenant ID is required input derived from the argument, use an argument validation exception (ArgumentException/ArgumentNullException) for consistency and clearer caller feedback.
| public TeamsApi Api => _api ??= new TeamsApi( | ||
| ConversationClient, | ||
| UserTokenClient, | ||
| _teamsApiClient); |
There was a problem hiding this comment.
Api is lazily initialized on a singleton TeamsBotApplication without synchronization. Concurrent calls can create multiple TeamsApi instances and fail the “same instance” guarantee under load. Consider initializing in the constructor, using Lazy, or protecting the initialization with a lock/Interlocked to make it thread-safe.
There was a problem hiding this comment.
@rido-min copilot has a good point here, let's initialize this in the constructor?
Introduces ReactionsApi for programmatic message reactions in Teams bots, with AddAsync and DeleteAsync methods. ConversationClient now supports AddReactionAsync and DeleteReactionAsync. Expanded ReactionTypes with more documented types. Demonstrated usage in Program.cs by adding a "cake" reaction to "hello" messages.
| /// <param name="customHeaders">Optional custom headers to include in the request.</param> | ||
| /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> | ||
| /// <returns>A task that represents the asynchronous operation. The task result contains the response with the ID of the updated activity.</returns> | ||
| public Task<UpdateActivityResponse> UpdateAsync( |
There was a problem hiding this comment.
Wondering why we have disparity between send and update in extracting conv id/ activity id from activity itself? maybe we should have an overload for it similar to all other apis?
| /// <param name="customHeaders">Optional custom headers to include in the request.</param> | ||
| /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> | ||
| /// <returns>A task that represents the asynchronous operation.</returns> | ||
| public Task DeleteAsync( |
There was a problem hiding this comment.
Why do we need this overload for teamsactivity ?
There was a problem hiding this comment.
good point, having the overload defeats the benefits of polymorphism
| /// <item><see cref="Members"/> - Member operations (get, delete)</item> | ||
| /// </list> | ||
| /// </remarks> | ||
| public class ConversationsApi |
There was a problem hiding this comment.
Shouldn't there be a method here for create conversation? intentional omission?
| /// <param name="customHeaders">Optional custom headers to include in the request.</param> | ||
| /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> | ||
| /// <returns>A task that represents the asynchronous operation. The task result contains the conversation member.</returns> | ||
| public Task<T> GetByIdAsync<T>( |
There was a problem hiding this comment.
Should we combine GetByIdAsync<T and GetByIdAsync<ConversationAccount to GetByIdAsync <TeamsConversationAccount
| /// <param name="customHeaders">Optional custom headers to include in the request.</param> | ||
| /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> | ||
| /// <returns>A task that represents the asynchronous operation. The task result contains the team details.</returns> | ||
| public Task<TeamDetails> GetByIdAsync( |
There was a problem hiding this comment.
Should teamId also be extracted from activity here ? from activity.channelData.team.id
| /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> | ||
| /// <returns>A task that represents the asynchronous operation. The task result contains the list of channels.</returns> | ||
| public Task<ChannelList> GetChannelsAsync( | ||
| string teamId, |
There was a problem hiding this comment.
^ same
Should teamId also be extracted from activity here ? from activity.channelData.team.id
| /// <param name="customHeaders">Optional custom headers to include in the request.</param> | ||
| /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> | ||
| /// <returns>A task that represents the asynchronous operation.</returns> | ||
| public Task DeleteAsync( |
There was a problem hiding this comment.
good point, having the overload defeats the benefits of polymorphism
| /// <param name="customHeaders">Optional custom headers to include in the request.</param> | ||
| /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> | ||
| /// <returns>A task that represents the asynchronous operation. The task result contains a list of members for the activity.</returns> | ||
| public Task<IList<ConversationAccount>> GetMembersAsync( |
There was a problem hiding this comment.
what does "members of a specific activity" even mean? is it members of the conversation in which this activity was posted?
| public TeamsApi Api => _api ??= new TeamsApi( | ||
| ConversationClient, | ||
| UserTokenClient, | ||
| _teamsApiClient); |
There was a problem hiding this comment.
@rido-min copilot has a good point here, let's initialize this in the constructor?
Major update adding strong-typed handlers and payloads for all Teams invoke and event activity types (adaptive card actions, file consent, task modules, message extensions, meetings, etc.). Introduces new handler registration APIs, extensible InvokeResponse, and builder classes for adaptive card, task module, and message extension responses. Refactors schema for nullability and type-safety, unifies activity/entity types, and improves router logic. Adds new sample bots and documentation. Enhances logging, DI, and test coverage. Removes legacy code and improves maintainability throughout.
Added ArgumentNullException.ThrowIfNull for critical properties in API wrappers to ensure required parameters are not null. Refactored IConfiguration retrieval using pattern matching for clarity. Updated .slnx to exclude certain sample projects from default build. Modernized bearer token extraction and cleaned up unused usings. These changes improve code safety, error reporting, and maintainability.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ArgumentException.ThrowIfNullOrWhiteSpace(reactionType); | ||
| ArgumentNullException.ThrowIfNull(serviceUrl); | ||
|
|
||
| string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{conversationId}/activities/{activityId}/reactions/{reactionType}"; |
There was a problem hiding this comment.
The reaction URL is constructed with unencoded path segments (conversationId, activityId, reactionType). This can break requests (or change meaning) when IDs contain characters that need escaping. Use Uri.EscapeDataString(...) (or an equivalent segment-encoding approach) for each path segment before interpolation.
| activity.Id, | ||
| reactionType, | ||
| activity.ServiceUrl, | ||
| activity.Recipient?.GetAgenticIdentity(), |
There was a problem hiding this comment.
The activity-context overload uses activity.Recipient?.GetAgenticIdentity() as the agentic identity. Other API surfaces in this PR use activity.From?.GetAgenticIdentity() for caller identity; using Recipient (typically the bot) is inconsistent and likely incorrect for OBO/auth context. Consider switching to activity.From?.GetAgenticIdentity() (and make the same change in the Delete overload).
| activity.Recipient?.GetAgenticIdentity(), | |
| activity.From?.GetAgenticIdentity(), |
| ArgumentNullException.ThrowIfNull(activity); | ||
| ArgumentNullException.ThrowIfNull(activity.Id); | ||
| ArgumentNullException.ThrowIfNull(activity.Conversation); | ||
| ArgumentNullException.ThrowIfNull(activity.Conversation.Id); | ||
| ArgumentNullException.ThrowIfNull(activity.ServiceUrl); |
There was a problem hiding this comment.
These guards allow empty/whitespace IDs (activity.Id, activity.Conversation.Id) because they only check for null. Since these values are used as URL path segments, empty/whitespace will produce invalid endpoints. Consider using ArgumentException.ThrowIfNullOrWhiteSpace(...) for string IDs (matching patterns used elsewhere in the PR, e.g., ReactionsApi).
| ArgumentNullException.ThrowIfNull(activity); | ||
| ArgumentNullException.ThrowIfNull(activity.ServiceUrl); | ||
| ArgumentNullException.ThrowIfNull(activity.Conversation); | ||
| ArgumentNullException.ThrowIfNull(activity.Conversation.Id); |
There was a problem hiding this comment.
Similar to ActivitiesApi, this only checks activity.Conversation.Id for null (not whitespace). Since it is used in URL construction, consider switching to ArgumentException.ThrowIfNullOrWhiteSpace(activity.Conversation.Id) in these activity-context overloads for more robust validation.
| ArgumentNullException.ThrowIfNull(activity.Conversation.Id); | |
| ArgumentException.ThrowIfNullOrWhiteSpace(activity.Conversation.Id); |
| CancellationToken cancellationToken = default) | ||
| { | ||
| ArgumentNullException.ThrowIfNull(activity); | ||
| ArgumentNullException.ThrowIfNull(activity); |
There was a problem hiding this comment.
Duplicate ArgumentNullException.ThrowIfNull(activity); call. Remove one to reduce noise and keep guard clauses consistent.
| ArgumentNullException.ThrowIfNull(activity); |
| [Fact] | ||
| public async Task Api_Teams_GetByIdAsync() | ||
| { | ||
| string teamId = Environment.GetEnvironmentVariable("TEST_TEAMID") ?? throw new InvalidOperationException("TEST_TEAMID environment variable not set"); |
There was a problem hiding this comment.
These tests hard-fail when required env vars are missing, which makes them brittle in typical CI/dev runs. Consider skipping tests when env vars aren’t set (e.g., using a skip mechanism or throwing Xunit.SkipException) so the suite can run without requiring external configuration.
| string teamId = Environment.GetEnvironmentVariable("TEST_TEAMID") ?? throw new InvalidOperationException("TEST_TEAMID environment variable not set"); | |
| string? teamId = Environment.GetEnvironmentVariable("TEST_TEAMID"); | |
| if (string.IsNullOrEmpty(teamId)) | |
| { | |
| throw new Xunit.SkipException("Skipping Api_Teams_GetByIdAsync because TEST_TEAMID environment variable is not set."); | |
| } |
core/samples/TeamsBot/Program.cs
Outdated
| { | ||
| await context.SendActivityAsync("Hi there! 👋 You said hello!", cancellationToken); | ||
|
|
||
| await teamsApp.Api.Conversations.Reactions.AddAsync(context.Activity, "cake", cancellationToken: cancellationToken); |
There was a problem hiding this comment.
The sample uses reaction type \"cake\", but the ReactionTypes constants in this repo don’t include it and the service may reject unknown reaction types. Use a known supported value (e.g., one of ReactionTypes.Like/Heart/Laugh/...) or explain in the sample why this custom value is valid.
| await teamsApp.Api.Conversations.Reactions.AddAsync(context.Activity, "cake", cancellationToken: cancellationToken); | |
| await teamsApp.Api.Conversations.Reactions.AddAsync(context.Activity, ReactionTypes.Like, cancellationToken: cancellationToken); |
This pull request introduces support for message reactions in the Teams bot SDK, along with improvements for targeted messaging. The changes add a new `ReactionsApi` for handling reactions, expand the list of supported reaction types, and enhance message delivery to allow targeting specific recipients. The most important updates are grouped below: **Message Reaction Support** * Added a new `ReactionsApi` class, enabling bots to add and remove reactions (like, heart, laugh, etc.) on conversation activities. This includes both direct and context-based methods for reaction management. (`core/src/Microsoft.Teams.Bot.Apps/Api/ReactionsApi.cs`) * Integrated `ReactionsApi` into `ConversationsApi`, exposing it via the `Reactions` property and updating documentation to reflect its availability. (`core/src/Microsoft.Teams.Bot.Apps/Api/ConversationsApi.cs`) [[1]](diffhunk://#diff-51d32bbdd7ca8e036814a550fdcc74432bd293f447a5b105cde5714f6feddde4R16) [[2]](diffhunk://#diff-51d32bbdd7ca8e036814a550fdcc74432bd293f447a5b105cde5714f6feddde4R29) [[3]](diffhunk://#diff-51d32bbdd7ca8e036814a550fdcc74432bd293f447a5b105cde5714f6feddde4R41-R45) * Implemented `AddReactionAsync` and `DeleteReactionAsync` methods in `ConversationClient` to support adding/removing reactions through HTTP calls. (`core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs`) **Expanded Reaction Types** * Extended the `ReactionTypes` class to include new reactions (checkmark, hourglass, pushpin, exclamation) and clarified existing reactions with emoji descriptions. Removed the unused `plusOne` reaction. (`core/src/Microsoft.Teams.Bot.Apps/Schema/MessageActivities/MessageReactionActivity.cs`) **Targeted Messaging Enhancements** * Added an `IsTargeted` property to `CoreActivity` and updated activity cloning to preserve this flag, enabling messages to be directed privately to specific recipients. (`core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivity.cs`) [[1]](diffhunk://#diff-aa65632fe28a87aec3d29025384c7f1e687d5839d848f1220e8ed59e76dcd35bR60-R65) [[2]](diffhunk://#diff-aa65632fe28a87aec3d29025384c7f1e687d5839d848f1220e8ed59e76dcd35bR159) * Modified `SendActivityAsync`, `UpdateActivityAsync`, and `DeleteActivityAsync` methods in `ConversationClient` to handle targeted activities by appending the `isTargetedActivity=true` query parameter when appropriate. (`core/src/Microsoft.Teams.Bot.Core/ConversationClient.cs`) [[1]](diffhunk://#diff-d62a8ce4aac677906758eb430b84c9432f61ff95342701fdcae909310e2aed27R54-R58) [[2]](diffhunk://#diff-d62a8ce4aac677906758eb430b84c9432f61ff95342701fdcae909310e2aed27R91-R96) [[3]](diffhunk://#diff-d62a8ce4aac677906758eb430b84c9432f61ff95342701fdcae909310e2aed27L110-R150) [[4]](diffhunk://#diff-d62a8ce4aac677906758eb430b84c9432f61ff95342701fdcae909310e2aed27R181) * Updated `CoreActivityBuilder` to allow specifying targeted recipients via the `WithRecipient(recipient, isTargeted)` method. (`core/src/Microsoft.Teams.Bot.Core/Schema/CoreActivityBuilder.cs`) **Sample Usage** * Enhanced the Teams bot sample (`core/samples/TeamsBot/Program.cs`) to demonstrate reaction usage (adding a "cake" reaction when "hello" is received) and sending targeted messages to conversation members. These changes collectively make it easier for bot developers to manage reactions and deliver targeted messages within Teams conversations. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Comment out or adjust assertions on Recipient in activity builder tests to reflect updated handling or population of the Recipient property. Update TeamsActivityBuilderTests to check From instead of Recipient where appropriate.
Refactored Conversation to use a primary constructor for id initialization. Updated ConversationClientTest to cache conversation ID, recipient, and agentic identity, and to consistently set the From property. Simplified test code by using the new Conversation(string id) constructor throughout. Added agentic identity support to relevant test calls. Introduced a new test for adding/removing message reactions. Improved error handling and parameter passing for paged member tests. These changes enhance test reliability, reduce duplication, and expand coverage for reaction APIs.
Add MartinCostello.Logging.XUnit to enable structured logging output in test runs. Update ConversationClientTest to accept ITestOutputHelper and configure logging to use xUnit output, with filters to reduce log noise. Pass _agenticIdentity to CreateConversationAsync. Reformat csproj for consistency.
Introduced logic in EchoBot to send a message and programmatically add/remove reactions ("laugh" and "sad") to it in Teams. Switched TeamsActivity to use a hardcoded service URL. Added WithServiceUrl(string) overload to CoreActivityBuilder for convenience. Updated tests to use new Conversation constructor and explicitly set Bot properties for improved clarity. Commented out SendUpdateDeleteActivityAsync in favor of new reaction logic.
Refactored integration test classes to use xUnit output logging via ITestOutputHelper, centralized environment variable handling in constructors, and removed redundant code. Updated all tests to use private fields for IDs and configuration, improved logging filters, and modernized test code for clarity and maintainability. No changes to test logic or assertions.
Introduce _agenticAppBlueprintId, _agenticAppId, and _agenticUserId fields to CompatTeamsInfoTests, initialized from environment variables. Use AzureAd__ClientId for botAppId and populate ChannelAccount properties with agentic identifiers to support new authentication and identity testing scenarios.
Major overhaul of TeamsBotApplication hosting model: - Remove TeamsBotApplicationBuilder; use WebApplicationBuilder/WebApplication directly. - Add AddTeamsBotApplication/UseBotApplication extension methods for registration and endpoint mapping. - Support custom bot app classes and add CustomHosting sample. Middleware pipeline improvements: - Rename ITurnMiddleWare to ITurnMiddleware; change Use() to UseMiddleware(). - Update all usages, tests, and docs for new middleware API. Service registration and API modernization: - Rename AddAuthorization to AddBotAuthorization. - Ensure AddHttpContextAccessor is always registered. - Use explicit types, collection initializers, and modern C# features. - Make ConversationClient/UserTokenClient methods virtual for testability. Update all samples and tests to new hosting and middleware model. Improve code clarity, nullability, and documentation throughout. Add copyright headers and appsettings.json for new sample.
Introduce consistent JsonSerializerOptions for ConversationClient, applying them to all serialization for requests and logs. Improve log detail and consistency. Fix DeleteActivityAsync to properly await internal call. Increase Teams logging to Trace in tests. Add nullable TeamsApi field to TeamsBotApplication.
Introduce agentic identity to Teams API and conversation client tests by reading new environment variables and constructing AgenticIdentity objects. Pass agentic identity to relevant Teams API client and facade methods. Update exception and assertion tests accordingly. Mark or skip tests that are not compatible with agentic identity, and adjust test data where needed. These changes enable testing of agentic identity scenarios in Teams bot integration tests.
Updated message handling to explicitly mark targeted messages by setting the isTargeted property in outgoing activities. The IsTargeted property is now serialized in CoreActivity, ensuring this flag is included in activity JSON payloads for downstream processing. Enhanced reply logic to use TeamsActivity with targeted recipient and mention.
isTargeted is now serialized and deserialized in activity JSON. Updated AdaptiveCardActivity.json and SuggestedActionsActivity.json to include isTargeted. Adjusted related tests to expect and verify the presence of isTargeted in JSON.
Introduced a structured API facade (
TeamsBotApplication.Api) for Teams operations, grouping functionality into Conversations, Users, Teams, Meetings, and Batch sub-APIs. Added new classes for each sub-API, with extensive XML documentation. UpdatedTeamsBotApplicationto expose the new facade. Added integration tests for the API hierarchy and error handling. Cleaned up.csprojby removing unused RunSettings property. This refactor improves discoverability, organization, and developer experience for Teams API usage.This pull request introduces a new
ActivitiesApiclass to the codebase, which provides a unified interface for performing activity-related operations in conversations, such as sending, updating, deleting activities, handling conversation history, and retrieving activity members. The class wraps existing functionality from theConversationClient, making these operations easier to use and more consistent throughout the application.New API for activity operations:
ActivitiesApiclass incore/src/Microsoft.Teams.Bot.Apps/Api/ActivitiesApi.cs, providing methods for sending, updating, and deleting activities, uploading conversation history, and retrieving activity members.Integration and usability improvements:
ConversationClient, ensuring consistency and reusability of existing logic.