From 04236ff14fa20085d8b5fdbd02060e9594375ed0 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 15:47:39 -0700 Subject: [PATCH 1/3] Require explicit service URL for activities Make Core ConversationClient activity operations take an explicit serviceUrl instead of deriving routing information from CoreActivity.ServiceUrl. Update app, BotBuilder, streaming, tests, and samples to pass service URLs explicitly without mutating outbound activities for routing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- core/samples/CompatBot/EchoBot.cs | 7 +- .../CompatConversations.cs | 26 ++--- .../TeamsBotAdapter.cs | 20 ++-- .../Api/Clients/ActivityClient.cs | 15 +-- .../Api/Clients/ConversationApiClient.cs | 15 +-- core/src/Microsoft.Teams.Apps/Context.cs | 10 +- .../TeamsBotApplication.cs | 2 +- .../TeamsStreamingWriter.cs | 10 +- .../Microsoft.Teams.Core/BotApplication.cs | 25 ++--- .../ConversationClient.cs | 42 ++++---- .../ConversationClientTests.cs | 10 +- .../CreateConversationTests.cs | 4 +- .../CompatBotAdapterTests.cs | 13 ++- .../CompatConversationsTests.cs | 102 +++++++++++++----- .../OAuthFlowTelemetryTests.cs | 2 +- .../OAuthFlowTests.cs | 2 +- .../PromptPreviewTests.cs | 5 +- .../BotApplicationTests.cs | 4 +- .../ConversationClientTests.cs | 34 +++--- .../Diagnostics/TelemetryTests.cs | 4 +- 20 files changed, 198 insertions(+), 154 deletions(-) diff --git a/core/samples/CompatBot/EchoBot.cs b/core/samples/CompatBot/EchoBot.cs index 042d1ee6..f78ab927 100644 --- a/core/samples/CompatBot/EchoBot.cs +++ b/core/samples/CompatBot/EchoBot.cs @@ -84,11 +84,12 @@ protected override async Task OnMessageActivityAsync(ITurnContext> GetConversationsWi public async Task> ReplyToActivityWithHttpMessagesAsync(string conversationId, string activityId, Activity activity, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl); Dictionary? convertedHeaders = ConvertHeaders(customHeaders); CoreActivity coreActivity = activity.FromBotFrameworkActivity(); - // Default to the ServiceUrl from the adapter if it's not set on the activity, as ConversationClient requires it for sending activities - if (!string.IsNullOrWhiteSpace(ServiceUrl) && coreActivity.ServiceUrl == null) - { - coreActivity.ServiceUrl = new Uri(ServiceUrl); - } - coreActivity.ReplyToId = activityId; coreActivity.Conversation = new Microsoft.Teams.Core.Schema.Conversation(conversationId); - SendActivityResponse? response = await _client.SendActivityAsync(coreActivity, requestContext: RequestContext, customHeaders: convertedHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); + SendActivityResponse? response = await _client.SendActivityAsync(coreActivity, new Uri(ServiceUrl), requestContext: RequestContext, customHeaders: convertedHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); ResourceResponse resourceResponse = new() { @@ -303,19 +298,14 @@ public async Task> SendConversationHisto /// public async Task> SendToConversationWithHttpMessagesAsync(string conversationId, Activity activity, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl); Dictionary? convertedHeaders = ConvertHeaders(customHeaders); CoreActivity coreActivity = activity.FromBotFrameworkActivity(); - // Default to the ServiceUrl from the adapter if it's not set on the activity, as ConversationClient requires it for sending activities - if (!string.IsNullOrWhiteSpace(ServiceUrl) && coreActivity.ServiceUrl == null) - { - coreActivity.ServiceUrl = new Uri(ServiceUrl); - } - coreActivity.Conversation = new Microsoft.Teams.Core.Schema.Conversation(conversationId); - SendActivityResponse? response = await _client.SendActivityAsync(coreActivity, requestContext: RequestContext, customHeaders: convertedHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); + SendActivityResponse? response = await _client.SendActivityAsync(coreActivity, new Uri(ServiceUrl), requestContext: RequestContext, customHeaders: convertedHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); ResourceResponse resourceResponse = new() { @@ -343,20 +333,16 @@ public async Task> SendToConversationWit /// public async Task> UpdateActivityWithHttpMessagesAsync(string conversationId, string activityId, Activity activity, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default) { + ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl); Dictionary? convertedHeaders = ConvertHeaders(customHeaders); CoreActivity coreActivity = activity.FromBotFrameworkActivity(); - // Default to the ServiceUrl from the adapter if it's not set on the activity, as ConversationClient requires it for updating activities - if (!string.IsNullOrWhiteSpace(ServiceUrl) && coreActivity.ServiceUrl == null) - { - coreActivity.ServiceUrl = new Uri(ServiceUrl); - } - UpdateActivityResponse response = await _client.UpdateActivityAsync( conversationId, activityId, coreActivity, + new Uri(ServiceUrl), requestContext: RequestContext, customHeaders: convertedHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); diff --git a/core/src/Microsoft.Teams.Apps.BotBuilder/TeamsBotAdapter.cs b/core/src/Microsoft.Teams.Apps.BotBuilder/TeamsBotAdapter.cs index f8326375..b789a397 100644 --- a/core/src/Microsoft.Teams.Apps.BotBuilder/TeamsBotAdapter.cs +++ b/core/src/Microsoft.Teams.Apps.BotBuilder/TeamsBotAdapter.cs @@ -105,17 +105,21 @@ public override async Task SendActivitiesAsync(ITurnContext CoreActivity coreActivity = activity.FromBotFrameworkActivity(); - // Ensure ServiceUrl is set from turn context if not already present - if (coreActivity.ServiceUrl == null && !string.IsNullOrWhiteSpace(inboundActivity.ServiceUrl)) + string? serviceUrlString = !string.IsNullOrWhiteSpace(activity.ServiceUrl) + ? activity.ServiceUrl + : inboundActivity.ServiceUrl; + if (string.IsNullOrWhiteSpace(serviceUrlString)) { - coreActivity.ServiceUrl = new Uri(inboundActivity.ServiceUrl); + throw new InvalidOperationException("ServiceUrl is required to send activities."); } + Uri serviceUrl = new(serviceUrlString); coreActivity.Conversation ??= new Microsoft.Teams.Core.Schema.Conversation( inboundActivity.Conversation?.Id ?? throw new InvalidOperationException("Conversation ID is required to send activities.")); SendActivityResponse? resp = await botApplication.ConversationClient.SendActivityAsync( coreActivity, + serviceUrl, requestContext, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -143,16 +147,20 @@ public override async Task UpdateActivityAsync(ITurnContext tu CoreActivity coreActivity = activity.FromBotFrameworkActivity(); - // Ensure ServiceUrl is set from turn context if not already present - if (coreActivity.ServiceUrl == null && !string.IsNullOrWhiteSpace(turnContext.Activity.ServiceUrl)) + string? serviceUrlString = !string.IsNullOrWhiteSpace(activity.ServiceUrl) + ? activity.ServiceUrl + : turnContext.Activity.ServiceUrl; + if (string.IsNullOrWhiteSpace(serviceUrlString)) { - coreActivity.ServiceUrl = new Uri(turnContext.Activity.ServiceUrl); + throw new InvalidOperationException("ServiceUrl is required to update activities."); } + Uri serviceUrl = new(serviceUrlString); UpdateActivityResponse res = await botApplication.ConversationClient.UpdateActivityAsync( activity.Conversation.Id, activity.Id, coreActivity, + serviceUrl, requestContext: BotRequestContext.FromInboundActivity(turnContext.Activity?.FromBotFrameworkActivity()), cancellationToken: cancellationToken).ConfigureAwait(false); return new ResourceResponse() { Id = res.Id }; diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs index 1cf236fa..a8ed2769 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ActivityClient.cs @@ -32,9 +32,8 @@ internal ActivityClient(Uri serviceUrl, CoreConversationClient client) public Task CreateAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); - activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, _serviceUrl, customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -43,8 +42,7 @@ internal ActivityClient(Uri serviceUrl, CoreConversationClient client) public Task UpdateAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); - activity.ServiceUrl ??= _serviceUrl; - return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.FromActivity(activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateActivityAsync(conversationId, id, activity, _serviceUrl, requestContext: BotRequestContext.FromActivity(activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -54,9 +52,8 @@ public Task UpdateAsync(string conversationId, string id { ArgumentNullException.ThrowIfNull(activity); activity.ReplyToId = id; - activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, _serviceUrl, customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -75,14 +72,13 @@ public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agent public Task CreateTargetedAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); - activity.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, _serviceUrl, customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -92,8 +88,7 @@ public Task DeleteAsync(string conversationId, string id, AgenticIdentity? agent public Task UpdateTargetedAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); - activity.ServiceUrl ??= _serviceUrl; - return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.FromActivity(activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateTargetedActivityAsync(conversationId, id, activity, _serviceUrl, requestContext: BotRequestContext.FromActivity(activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// diff --git a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs index 26b49bd0..1ae190c2 100644 --- a/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs +++ b/core/src/Microsoft.Teams.Apps/Api/Clients/ConversationApiClient.cs @@ -66,9 +66,8 @@ public Task CreateAsync(ConversationParameters reque public Task CreateActivityAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); - activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, _serviceUrl, customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -77,8 +76,7 @@ public Task CreateAsync(ConversationParameters reque public Task UpdateActivityAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); - activity.ServiceUrl ??= _serviceUrl; - return _client.UpdateActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.FromActivity(activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateActivityAsync(conversationId, id, activity, _serviceUrl, requestContext: BotRequestContext.FromActivity(activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -88,9 +86,8 @@ public Task UpdateActivityAsync(string conversationId, s { ArgumentNullException.ThrowIfNull(activity); activity.ReplyToId = id; - activity.ServiceUrl ??= _serviceUrl; activity.Conversation ??= new Conversation(conversationId); - return _client.SendActivityAsync(activity, customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.SendActivityAsync(activity, _serviceUrl, customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -109,13 +106,12 @@ public Task DeleteActivityAsync(string conversationId, string id, AgenticIdentit public Task CreateTargetedActivityAsync(string conversationId, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); - activity.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, _serviceUrl, customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// @@ -125,8 +121,7 @@ public Task DeleteActivityAsync(string conversationId, string id, AgenticIdentit public Task UpdateTargetedActivityAsync(string conversationId, string id, CoreActivity activity, Dictionary? additionalHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); - activity.ServiceUrl ??= _serviceUrl; - return _client.UpdateTargetedActivityAsync(conversationId, id, activity, requestContext: BotRequestContext.FromActivity(activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); + return _client.UpdateTargetedActivityAsync(conversationId, id, activity, _serviceUrl, requestContext: BotRequestContext.FromActivity(activity), customHeaders: additionalHeaders, cancellationToken: cancellationToken); } /// diff --git a/core/src/Microsoft.Teams.Apps/Context.cs b/core/src/Microsoft.Teams.Apps/Context.cs index 53851f38..88708a4f 100644 --- a/core/src/Microsoft.Teams.Apps/Context.cs +++ b/core/src/Microsoft.Teams.Apps/Context.cs @@ -29,6 +29,9 @@ public class Context(TeamsBotApplication botApplication, TActivity ac /// public TActivity Activity { get; } = activity; + private Uri ServiceUrl => Activity.ServiceUrl + ?? throw new InvalidOperationException("Activity.ServiceUrl is required."); + /// /// Gets the application (client) ID configured for this bot. /// @@ -48,8 +51,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.ForServiceUrl(ServiceUrl); // ==================== Turn State ==================== @@ -260,7 +262,7 @@ internal Context CreateDerivedContext(TNew activity) where TNew : Te TeamsActivity reply = new TeamsActivityBuilder(activity) .WithConversationReference(Activity) .Build(); - return TeamsBotApplication.SendActivityAsync(reply, cancellationToken: cancellationToken); + return TeamsBotApplication.SendActivityAsync(reply, ServiceUrl, cancellationToken: cancellationToken); } /// @@ -274,7 +276,7 @@ internal Context CreateDerivedContext(TNew activity) where TNew : Te .WithType(TeamsActivityTypes.Typing) .WithConversationReference(Activity) .Build(); - return TeamsBotApplication.SendActivityAsync(reply, cancellationToken: cancellationToken); + return TeamsBotApplication.SendActivityAsync(reply, ServiceUrl, cancellationToken: cancellationToken); } // ==================== OAuth Sign-In ==================== diff --git a/core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs b/core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs index f273c8e9..ab5dc28f 100644 --- a/core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs +++ b/core/src/Microsoft.Teams.Apps/TeamsBotApplication.cs @@ -222,7 +222,7 @@ public TeamsBotApplication( TeamsActivity activity = builder.Build(); - return SendActivityAsync(activity, cancellationToken: cancellationToken); + return SendActivityAsync(activity, resolvedUrl, cancellationToken: cancellationToken); } /// diff --git a/core/src/Microsoft.Teams.Apps/TeamsStreamingWriter.cs b/core/src/Microsoft.Teams.Apps/TeamsStreamingWriter.cs index a5b35729..5d6b3611 100644 --- a/core/src/Microsoft.Teams.Apps/TeamsStreamingWriter.cs +++ b/core/src/Microsoft.Teams.Apps/TeamsStreamingWriter.cs @@ -54,6 +54,7 @@ public sealed class TeamsStreamingWriter private readonly ConversationClient _client; private readonly TeamsActivity _reference; private readonly string _conversationId; + private readonly Uri _serviceUrl; private readonly ILogger _logger; // Assigned from the server's 201 response after the first send; null until then. private string? _streamId; @@ -80,6 +81,7 @@ internal TeamsStreamingWriter(ConversationClient client, TeamsActivity reference _client = client; _reference = reference; _conversationId = reference.Conversation?.Id ?? throw new ArgumentException("Activity must have a Conversation with an Id.", nameof(reference)); + _serviceUrl = reference.ServiceUrl ?? throw new ArgumentException("Activity must have a ServiceUrl.", nameof(reference)); _logger = logger ?? NullLogger.Instance; } @@ -218,7 +220,7 @@ public async Task FinalizeResponseAsync(MessageActivity? final = null, Cancellat try { - await _client.SendActivityAsync(activity, cancellationToken: cancellationToken).ConfigureAwait(false); + await _client.SendActivityAsync(activity, _serviceUrl, cancellationToken: cancellationToken).ConfigureAwait(false); } catch (HttpRequestException ex) { @@ -255,7 +257,7 @@ public async Task FinalizeResponseAsync(MessageActivity? final = null, Cancellat { try { - return await _client.SendActivityAsync(activity, cancellationToken: cancellationToken).ConfigureAwait(false); + return await _client.SendActivityAsync(activity, _serviceUrl, cancellationToken: cancellationToken).ConfigureAwait(false); } catch (HttpRequestException ex) { @@ -348,12 +350,12 @@ private async Task SendFinalInPlaceAsync(MessageActivity final, CancellationToke if (_streamId != null) { _logger.LogDebug("Updating original streamed message in place after timeout (streamId '{StreamId}').", _streamId); - await _client.UpdateActivityAsync(_conversationId, _streamId, activity, cancellationToken: cancellationToken).ConfigureAwait(false); + await _client.UpdateActivityAsync(_conversationId, _streamId, activity, _serviceUrl, cancellationToken: cancellationToken).ConfigureAwait(false); } else { // No streamed message exists yet; send the buffered content as a normal message. - await _client.SendActivityAsync(activity, cancellationToken: cancellationToken).ConfigureAwait(false); + await _client.SendActivityAsync(activity, _serviceUrl, cancellationToken: cancellationToken).ConfigureAwait(false); } } diff --git a/core/src/Microsoft.Teams.Core/BotApplication.cs b/core/src/Microsoft.Teams.Core/BotApplication.cs index 1a4d16ef..48bbea34 100644 --- a/core/src/Microsoft.Teams.Core/BotApplication.cs +++ b/core/src/Microsoft.Teams.Core/BotApplication.cs @@ -35,10 +35,10 @@ namespace Microsoft.Teams.Core; /// CoreActivity.CreateBuilder() /// .WithType(ActivityType.Message) /// .WithConversation(activity.Conversation) -/// .WithServiceUrl(activity.ServiceUrl) -/// .WithProperty("text", "Hello!") -/// .Build(), -/// ct); +/// .WithProperty("text", "Hello!") +/// .Build(), +/// activity.ServiceUrl!, +/// ct); /// }; /// /// app.Run(); @@ -64,9 +64,9 @@ namespace Microsoft.Teams.Core; /// CoreActivity.CreateBuilder() /// .WithType(ActivityType.Message) /// .WithConversation(activity.Conversation) -/// .WithServiceUrl(activity.ServiceUrl) /// .WithProperty("text", $"You said: {activity.Properties["text"]}") /// .Build(), +/// activity.ServiceUrl!, /// ct); /// } /// } @@ -153,9 +153,9 @@ public BotApplication(ConversationClient conversationClient, UserTokenClient use /// CoreActivity.CreateBuilder() /// .WithType(ActivityType.Message) /// .WithConversation(activity.Conversation) - /// .WithServiceUrl(activity.ServiceUrl) /// .WithProperty("text", "Received your message!") /// .Build(), + /// activity.ServiceUrl!, /// ct); /// } /// }; @@ -287,31 +287,32 @@ public ITurnMiddleware UseMiddleware(ITurnMiddleware middleware) /// /// /// This is a convenience wrapper around . The activity - /// must have its and properties set. + /// must have its property set. /// /// /// var reply = CoreActivity.CreateBuilder() /// .WithType(ActivityType.Message) /// .WithConversation(incomingActivity.Conversation) - /// .WithServiceUrl(incomingActivity.ServiceUrl) /// .WithProperty("text", "Hello from the bot!") /// .Build(); /// - /// SendActivityResponse? response = await bot.SendActivityAsync(reply, cancellationToken); + /// SendActivityResponse? response = await bot.SendActivityAsync(reply, incomingActivity.ServiceUrl!, cancellationToken); /// string? sentId = response?.Id; /// /// /// - /// The activity to send. Cannot be null. Must have and set. + /// The activity to send. Cannot be null. Must have set. + /// The service URL for the conversation. Cannot be null. /// A cancellation token that can be used to cancel the send operation. /// A task that represents the asynchronous operation. The task result contains a with the ID of the sent activity, or null. /// Thrown if is null or the conversation client has not been initialized. - public async Task SendActivityAsync(CoreActivity activity, CancellationToken cancellationToken = default) + public async Task SendActivityAsync(CoreActivity activity, Uri serviceUrl, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); + ArgumentNullException.ThrowIfNull(serviceUrl); ArgumentNullException.ThrowIfNull(_conversationClient, "ConversationClient not initialized"); - return await _conversationClient.SendActivityAsync(activity, cancellationToken: cancellationToken).ConfigureAwait(false); + return await _conversationClient.SendActivityAsync(activity, serviceUrl, cancellationToken: cancellationToken).ConfigureAwait(false); } /// diff --git a/core/src/Microsoft.Teams.Core/ConversationClient.cs b/core/src/Microsoft.Teams.Core/ConversationClient.cs index 39554551..0ae71218 100644 --- a/core/src/Microsoft.Teams.Core/ConversationClient.cs +++ b/core/src/Microsoft.Teams.Core/ConversationClient.cs @@ -39,20 +39,21 @@ public class ConversationClient(HttpClient httpClient, ILogger /// Sends the specified activity to the conversation endpoint asynchronously. /// - /// The activity to send. Cannot be null. Must contain a valid ServiceUrl and Conversation with an Id. + /// The activity to send. Cannot be null. Must contain a Conversation with an Id. /// The recipient's IsTargeted property determines if this is a targeted activity. + /// The service URL for the conversation. Cannot be null. /// Optional per-request properties (see ) used as a fallback; values derived from the activity take precedence. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the send operation. /// A task that represents the asynchronous operation. The task result contains the response with the ID of the sent activity. /// Thrown if the activity could not be sent successfully. The exception message includes the HTTP status code and /// response content. - public virtual async Task SendActivityAsync(CoreActivity activity, BotRequestContext? requestContext = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) + public virtual async Task SendActivityAsync(CoreActivity activity, Uri serviceUrl, BotRequestContext? requestContext = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); + ArgumentNullException.ThrowIfNull(serviceUrl); string? conversationId = activity.Conversation?.Id; ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); - ArgumentNullException.ThrowIfNull(activity.ServiceUrl); #pragma warning disable ExperimentalTeamsTargeted bool isTargeted = activity.Recipient?.IsTargeted == true; @@ -60,13 +61,13 @@ public class ConversationClient(HttpClient httpClient, ILogger 100 ? conversationId[..100] : conversationId; - url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(convId)}/activities/"; + url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(convId)}/activities/"; } if (isTargeted) @@ -81,7 +82,7 @@ public class ConversationClient(HttpClient httpClient, ILoggerThe ID of the conversation. Cannot be null or whitespace. /// The ID of the activity to update. Cannot be null or whitespace. /// The updated activity data. Cannot be null. + /// The service URL for the conversation. Cannot be null. /// Whether this is a targeted activity visible only to a specific recipient. /// Optional per-request properties (see ) to stamp onto the request's options. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the update operation. /// A task that represents the asynchronous operation. The task result contains the response with the ID of the updated activity. /// Thrown if the activity could not be updated successfully. - public virtual async Task UpdateActivityAsync(string conversationId, string activityId, CoreActivity activity, bool isTargeted = false, BotRequestContext? requestContext = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) + public virtual async Task UpdateActivityAsync(string conversationId, string activityId, CoreActivity activity, Uri serviceUrl, bool isTargeted = false, BotRequestContext? requestContext = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); ArgumentException.ThrowIfNullOrWhiteSpace(activityId); ArgumentNullException.ThrowIfNull(activity); - ArgumentNullException.ThrowIfNull(activity.ServiceUrl); + ArgumentNullException.ThrowIfNull(serviceUrl); - string url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/{Uri.EscapeDataString(activityId)}"; + string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/{Uri.EscapeDataString(activityId)}"; if (isTargeted) { @@ -140,7 +142,7 @@ public virtual async Task UpdateActivityAsync(string con if (span is not null) { span.SetTag(Telemetry.Tags.Operation, Telemetry.Operations.UpdateActivity); - span.SetTag(Telemetry.Tags.ServiceUrl, activity.ServiceUrl.ToString()); + span.SetTag(Telemetry.Tags.ServiceUrl, serviceUrl.ToString()); span.SetTag(Telemetry.Tags.ConversationId, conversationId); span.SetTag(Telemetry.Tags.ActivityId, activityId); span.SetTag(Telemetry.Tags.ActivityType, activity.Type); @@ -171,20 +173,21 @@ public virtual async Task UpdateActivityAsync(string con /// /// The ID of the conversation. Cannot be null or whitespace. /// The ID of the activity to update. Cannot be null or whitespace. - /// The updated activity data. Cannot be null. Must contain a valid ServiceUrl. + /// The updated activity data. Cannot be null. + /// The service URL for the conversation. Cannot be null. /// Optional per-request properties (see ) to stamp onto the request's options. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the update operation. /// A task that represents the asynchronous operation. The task result contains the response with the ID of the updated activity. /// Thrown if the activity could not be updated successfully. - public virtual async Task UpdateTargetedActivityAsync(string conversationId, string activityId, CoreActivity activity, BotRequestContext? requestContext = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) + public virtual async Task UpdateTargetedActivityAsync(string conversationId, string activityId, CoreActivity activity, Uri serviceUrl, BotRequestContext? requestContext = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); ArgumentException.ThrowIfNullOrWhiteSpace(activityId); ArgumentNullException.ThrowIfNull(activity); - ArgumentNullException.ThrowIfNull(activity.ServiceUrl); + ArgumentNullException.ThrowIfNull(serviceUrl); - string url = $"{activity.ServiceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/{Uri.EscapeDataString(activityId)}?isTargetedActivity=true"; + string url = $"{serviceUrl.ToString().TrimEnd('/')}/v3/conversations/{Uri.EscapeDataString(conversationId)}/activities/{Uri.EscapeDataString(activityId)}?isTargetedActivity=true"; string body = activity.ToJson(); @@ -195,7 +198,7 @@ public virtual async Task UpdateTargetedActivityAsync(st if (span is not null) { span.SetTag(Telemetry.Tags.Operation, Telemetry.Operations.UpdateActivity); - span.SetTag(Telemetry.Tags.ServiceUrl, activity.ServiceUrl.ToString()); + span.SetTag(Telemetry.Tags.ServiceUrl, serviceUrl.ToString()); span.SetTag(Telemetry.Tags.ConversationId, conversationId); span.SetTag(Telemetry.Tags.ActivityId, activityId); span.SetTag(Telemetry.Tags.ActivityType, activity.Type); @@ -303,24 +306,25 @@ await _botHttpClient.SendAsync( /// Deletes an existing activity from a conversation using activity context. /// /// The ID of the conversation. - /// The activity to delete. Must contain valid Id and ServiceUrl. Cannot be null. + /// The activity to delete. Must contain a valid Id. Cannot be null. + /// The service URL for the conversation. Cannot be null. /// Whether this is a targeted activity. /// Optional per-request properties (see ) to stamp onto the request's options. /// Optional custom headers to include in the request. /// A cancellation token that can be used to cancel the delete operation. /// A task that represents the asynchronous operation. /// Thrown if the activity could not be deleted successfully. - public virtual async Task DeleteActivityAsync(string conversationId, CoreActivity activity, bool isTargeted = false, BotRequestContext? requestContext = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) + public virtual async Task DeleteActivityAsync(string conversationId, CoreActivity activity, Uri serviceUrl, bool isTargeted = false, BotRequestContext? requestContext = null, CustomHeaders? customHeaders = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(activity); + ArgumentNullException.ThrowIfNull(serviceUrl); ArgumentException.ThrowIfNullOrWhiteSpace(activity.Id); ArgumentException.ThrowIfNullOrWhiteSpace(conversationId); - ArgumentNullException.ThrowIfNull(activity.ServiceUrl); await DeleteActivityAsync( conversationId, activity.Id, - activity.ServiceUrl, + serviceUrl, isTargeted, requestContext, customHeaders, diff --git a/core/test/IntegrationTests/ConversationClientTests.cs b/core/test/IntegrationTests/ConversationClientTests.cs index 35324bcf..1cdb9a59 100644 --- a/core/test/IntegrationTests/ConversationClientTests.cs +++ b/core/test/IntegrationTests/ConversationClientTests.cs @@ -35,7 +35,7 @@ public async Task SendActivity() .WithProperty("text", $"[ConversationClient] SendActivity at `{DateTime.UtcNow:s}`") .Build(); - SendActivityResponse? res = await _f.ConversationClient.SendActivityAsync(activity); + SendActivityResponse? res = await _f.ConversationClient.SendActivityAsync(activity, _f.ServiceUrl); Assert.NotNull(res); Assert.NotNull(res.Id); @@ -54,7 +54,7 @@ public async Task UpdateActivity() .WithProperty("text", $"[ConversationClient] Original at `{DateTime.UtcNow:s}`") .Build(); - SendActivityResponse? sent = await _f.ConversationClient.SendActivityAsync(activity); + SendActivityResponse? sent = await _f.ConversationClient.SendActivityAsync(activity, _f.ServiceUrl); Assert.NotNull(sent?.Id); CoreActivity updated = CoreActivity.CreateBuilder() @@ -66,7 +66,7 @@ public async Task UpdateActivity() .Build(); UpdateActivityResponse res = await _f.ConversationClient.UpdateActivityAsync( - _f.ConversationId, sent.Id, updated, false, BotRequestContext.FromAgenticIdentity(_f.AgenticIdentity)); + _f.ConversationId, sent.Id, updated, _f.ServiceUrl, false, BotRequestContext.FromAgenticIdentity(_f.AgenticIdentity)); Assert.NotNull(res?.Id); _output.WriteLine($"Updated activity: {res.Id}"); @@ -84,7 +84,7 @@ public async Task DeleteActivity() .WithProperty("text", $"[ConversationClient] To delete at `{DateTime.UtcNow:s}`") .Build(); - SendActivityResponse? sent = await _f.ConversationClient.SendActivityAsync(activity); + SendActivityResponse? sent = await _f.ConversationClient.SendActivityAsync(activity, _f.ServiceUrl); Assert.NotNull(sent?.Id); await Task.Delay(2000); @@ -159,7 +159,7 @@ public async Task AddAndDeleteReaction() .WithProperty("text", $"[ConversationClient] Reaction test at `{DateTime.UtcNow:s}`") .Build(); - SendActivityResponse? sent = await _f.ConversationClient.SendActivityAsync(activity); + SendActivityResponse? sent = await _f.ConversationClient.SendActivityAsync(activity, _f.ServiceUrl); Assert.NotNull(sent?.Id); await _f.ConversationClient.AddReactionAsync( diff --git a/core/test/IntegrationTests/CreateConversationTests.cs b/core/test/IntegrationTests/CreateConversationTests.cs index 88dc1bbf..ad2751b9 100644 --- a/core/test/IntegrationTests/CreateConversationTests.cs +++ b/core/test/IntegrationTests/CreateConversationTests.cs @@ -90,7 +90,7 @@ public async Task Core_CreatePersonalChat_AndSendMessage() .WithProperty("text", $"[Core] 1:1 message at `{DateTime.UtcNow:s}`") .Build(); - SendActivityResponse? sent = await _f.ConversationClient.SendActivityAsync(activity); + SendActivityResponse? sent = await _f.ConversationClient.SendActivityAsync(activity, _f.ServiceUrl); Assert.NotNull(sent?.Id); _output.WriteLine($"Created 1:1 conversation {response.Id} and sent activity {sent.Id}"); } @@ -194,7 +194,7 @@ public async Task Core_CreateGroupChat_AndSendMessage() .WithProperty("text", $"[Core] Group message at `{DateTime.UtcNow:s}`") .Build(); - SendActivityResponse? sent = await _f.ConversationClient.SendActivityAsync(activity); + SendActivityResponse? sent = await _f.ConversationClient.SendActivityAsync(activity, _f.ServiceUrl); Assert.NotNull(sent?.Id); _output.WriteLine($"Created group {response.Id} and sent activity {sent.Id}"); } diff --git a/core/test/Microsoft.Teams.Apps.BotBuilder.UnitTests/CompatBotAdapterTests.cs b/core/test/Microsoft.Teams.Apps.BotBuilder.UnitTests/CompatBotAdapterTests.cs index 43781227..2956aeac 100644 --- a/core/test/Microsoft.Teams.Apps.BotBuilder.UnitTests/CompatBotAdapterTests.cs +++ b/core/test/Microsoft.Teams.Apps.BotBuilder.UnitTests/CompatBotAdapterTests.cs @@ -146,12 +146,13 @@ await Assert.ThrowsAsync( } [Fact] - public async Task SendActivitiesAsync_SetsServiceUrlFromTurnContext() + public async Task SendActivitiesAsync_PassesServiceUrlFromTurnContext() { // Arrange Mock mockConversationClient = CreateMockConversationClient(); mockConversationClient.Setup(c => c.SendActivityAsync( It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) @@ -178,7 +179,8 @@ public async Task SendActivitiesAsync_SetsServiceUrlFromTurnContext() mockConversationClient.Verify( c => c.SendActivityAsync( - It.Is(a => a.ServiceUrl != null && a.ServiceUrl.ToString().TrimEnd('/') == "https://turn-context-service-url.com"), + It.Is(a => a.ServiceUrl == null), + It.Is(u => u.ToString().TrimEnd('/') == "https://turn-context-service-url.com"), It.Is(c => c != null && c.BotAppId == "bot-123"), It.IsAny?>(), It.IsAny()), @@ -186,7 +188,7 @@ public async Task SendActivitiesAsync_SetsServiceUrlFromTurnContext() } [Fact] - public async Task UpdateActivityAsync_SetsServiceUrlFromTurnContext() + public async Task UpdateActivityAsync_PassesServiceUrlFromTurnContext() { // Arrange Mock mockConversationClient = CreateMockConversationClient(); @@ -194,6 +196,7 @@ public async Task UpdateActivityAsync_SetsServiceUrlFromTurnContext() It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), @@ -223,7 +226,8 @@ public async Task UpdateActivityAsync_SetsServiceUrlFromTurnContext() c => c.UpdateActivityAsync( "conversation-123", "activity-123", - It.Is(a => a.ServiceUrl != null && a.ServiceUrl.ToString().TrimEnd('/') == "https://turn-context-service-url.com"), + It.Is(a => a.ServiceUrl == null), + It.Is(u => u.ToString().TrimEnd('/') == "https://turn-context-service-url.com"), It.IsAny(), It.IsAny(), It.IsAny?>(), @@ -248,6 +252,7 @@ private static Mock CreateMockConversationClient() mock.Setup(c => c.SendActivityAsync( It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) diff --git a/core/test/Microsoft.Teams.Apps.BotBuilder.UnitTests/CompatConversationsTests.cs b/core/test/Microsoft.Teams.Apps.BotBuilder.UnitTests/CompatConversationsTests.cs index 1abdcadd..53927059 100644 --- a/core/test/Microsoft.Teams.Apps.BotBuilder.UnitTests/CompatConversationsTests.cs +++ b/core/test/Microsoft.Teams.Apps.BotBuilder.UnitTests/CompatConversationsTests.cs @@ -17,7 +17,7 @@ public class CompatConversationsTests private const string TestActivityId = "test-activity-id"; [Fact] - public async Task SendToConversationWithHttpMessagesAsync_SetsServiceUrlFromProperty_WhenActivityServiceUrlIsNull() + public async Task SendToConversationWithHttpMessagesAsync_PassesServiceUrlFromProperty_WhenActivityServiceUrlIsNull() { // Arrange Mock mockConversationClient = CreateMockConversationClient(); @@ -33,9 +33,14 @@ public async Task SendToConversationWithHttpMessagesAsync_SetsServiceUrlFromProp }; CoreActivity? capturedActivity = null; + Uri? capturedServiceUrl = null; mockConversationClient - .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) - .Callback?, CancellationToken>((act, _, _, _) => capturedActivity = act) + .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) + .Callback?, CancellationToken>((act, serviceUrl, _, _, _) => + { + capturedActivity = act; + capturedServiceUrl = serviceUrl; + }) .ReturnsAsync(new SendActivityResponse { Id = TestActivityId }); // Act @@ -43,10 +48,11 @@ public async Task SendToConversationWithHttpMessagesAsync_SetsServiceUrlFromProp // Assert Assert.NotNull(capturedActivity); - Assert.NotNull(capturedActivity.ServiceUrl); - Assert.Equal(TestServiceUrl.TrimEnd('/'), capturedActivity.ServiceUrl.ToString().TrimEnd('/')); + Assert.Null(capturedActivity.ServiceUrl); + Assert.NotNull(capturedServiceUrl); + Assert.Equal(TestServiceUrl.TrimEnd('/'), capturedServiceUrl.ToString().TrimEnd('/')); mockConversationClient.Verify( - c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny()), + c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny()), Times.Once); } @@ -69,9 +75,14 @@ public async Task SendToConversationWithHttpMessagesAsync_DoesNotOverrideService }; CoreActivity? capturedActivity = null; + Uri? capturedServiceUrl = null; mockConversationClient - .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) - .Callback?, CancellationToken>((act, _, _, _) => capturedActivity = act) + .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) + .Callback?, CancellationToken>((act, serviceUrl, _, _, _) => + { + capturedActivity = act; + capturedServiceUrl = serviceUrl; + }) .ReturnsAsync(new SendActivityResponse { Id = TestActivityId }); // Act @@ -81,13 +92,15 @@ public async Task SendToConversationWithHttpMessagesAsync_DoesNotOverrideService Assert.NotNull(capturedActivity); Assert.NotNull(capturedActivity.ServiceUrl); Assert.Equal(activityServiceUrl.TrimEnd('/'), capturedActivity.ServiceUrl.ToString().TrimEnd('/')); + Assert.NotNull(capturedServiceUrl); + Assert.Equal(TestServiceUrl.TrimEnd('/'), capturedServiceUrl.ToString().TrimEnd('/')); mockConversationClient.Verify( - c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny()), + c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny()), Times.Once); } [Fact] - public async Task ReplyToActivityWithHttpMessagesAsync_SetsServiceUrlFromProperty_WhenActivityServiceUrlIsNull() + public async Task ReplyToActivityWithHttpMessagesAsync_PassesServiceUrlFromProperty_WhenActivityServiceUrlIsNull() { // Arrange Mock mockConversationClient = CreateMockConversationClient(); @@ -103,9 +116,14 @@ public async Task ReplyToActivityWithHttpMessagesAsync_SetsServiceUrlFromPropert }; CoreActivity? capturedActivity = null; + Uri? capturedServiceUrl = null; mockConversationClient - .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) - .Callback?, CancellationToken>((act, _, _, _) => capturedActivity = act) + .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) + .Callback?, CancellationToken>((act, serviceUrl, _, _, _) => + { + capturedActivity = act; + capturedServiceUrl = serviceUrl; + }) .ReturnsAsync(new SendActivityResponse { Id = TestActivityId }); // Act @@ -113,11 +131,12 @@ public async Task ReplyToActivityWithHttpMessagesAsync_SetsServiceUrlFromPropert // Assert Assert.NotNull(capturedActivity); - Assert.NotNull(capturedActivity.ServiceUrl); - Assert.Equal(TestServiceUrl.TrimEnd('/'), capturedActivity.ServiceUrl.ToString().TrimEnd('/')); + Assert.Null(capturedActivity.ServiceUrl); + Assert.NotNull(capturedServiceUrl); + Assert.Equal(TestServiceUrl.TrimEnd('/'), capturedServiceUrl.ToString().TrimEnd('/')); Assert.Equal(TestActivityId, capturedActivity.ReplyToId); mockConversationClient.Verify( - c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny()), + c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny()), Times.Once); } @@ -140,9 +159,14 @@ public async Task ReplyToActivityWithHttpMessagesAsync_DoesNotOverrideServiceUrl }; CoreActivity? capturedActivity = null; + Uri? capturedServiceUrl = null; mockConversationClient - .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) - .Callback?, CancellationToken>((act, _, _, _) => capturedActivity = act) + .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) + .Callback?, CancellationToken>((act, serviceUrl, _, _, _) => + { + capturedActivity = act; + capturedServiceUrl = serviceUrl; + }) .ReturnsAsync(new SendActivityResponse { Id = TestActivityId }); // Act @@ -152,13 +176,15 @@ public async Task ReplyToActivityWithHttpMessagesAsync_DoesNotOverrideServiceUrl Assert.NotNull(capturedActivity); Assert.NotNull(capturedActivity.ServiceUrl); Assert.Equal(activityServiceUrl.TrimEnd('/'), capturedActivity.ServiceUrl.ToString().TrimEnd('/')); + Assert.NotNull(capturedServiceUrl); + Assert.Equal(TestServiceUrl.TrimEnd('/'), capturedServiceUrl.ToString().TrimEnd('/')); mockConversationClient.Verify( - c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny()), + c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny()), Times.Once); } [Fact] - public async Task UpdateActivityWithHttpMessagesAsync_SetsServiceUrlFromProperty_WhenActivityServiceUrlIsNull() + public async Task UpdateActivityWithHttpMessagesAsync_PassesServiceUrlFromProperty_WhenActivityServiceUrlIsNull() { // Arrange Mock mockConversationClient = CreateMockConversationClient(); @@ -174,16 +200,22 @@ public async Task UpdateActivityWithHttpMessagesAsync_SetsServiceUrlFromProperty }; CoreActivity? capturedActivity = null; + Uri? capturedServiceUrl = null; mockConversationClient .Setup(c => c.UpdateActivityAsync( It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) - .Callback?, CancellationToken>((_, _, act, _, _, _, _) => capturedActivity = act) + .Callback?, CancellationToken>((_, _, act, serviceUrl, _, _, _, _) => + { + capturedActivity = act; + capturedServiceUrl = serviceUrl; + }) .ReturnsAsync(new UpdateActivityResponse { Id = TestActivityId }); // Act @@ -191,13 +223,15 @@ public async Task UpdateActivityWithHttpMessagesAsync_SetsServiceUrlFromProperty // Assert Assert.NotNull(capturedActivity); - Assert.NotNull(capturedActivity.ServiceUrl); - Assert.Equal(TestServiceUrl.TrimEnd('/'), capturedActivity.ServiceUrl.ToString().TrimEnd('/')); + Assert.Null(capturedActivity.ServiceUrl); + Assert.NotNull(capturedServiceUrl); + Assert.Equal(TestServiceUrl.TrimEnd('/'), capturedServiceUrl.ToString().TrimEnd('/')); mockConversationClient.Verify( c => c.UpdateActivityAsync( TestConversationId, TestActivityId, It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), @@ -230,6 +264,7 @@ public async Task UpdateActivityWithHttpMessagesAsync_PassesRequestContext() TestConversationId, TestActivityId, It.IsAny(), + It.IsAny(), It.IsAny(), It.Is(c => ReferenceEquals(c, requestContext)), It.IsAny?>(), @@ -262,16 +297,22 @@ public async Task UpdateActivityWithHttpMessagesAsync_DoesNotOverrideServiceUrl_ }; CoreActivity? capturedActivity = null; + Uri? capturedServiceUrl = null; mockConversationClient .Setup(c => c.UpdateActivityAsync( It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) - .Callback?, CancellationToken>((_, _, act, _, _, _, _) => capturedActivity = act) + .Callback?, CancellationToken>((_, _, act, serviceUrl, _, _, _, _) => + { + capturedActivity = act; + capturedServiceUrl = serviceUrl; + }) .ReturnsAsync(new UpdateActivityResponse { Id = TestActivityId }); // Act @@ -281,11 +322,14 @@ public async Task UpdateActivityWithHttpMessagesAsync_DoesNotOverrideServiceUrl_ Assert.NotNull(capturedActivity); Assert.NotNull(capturedActivity.ServiceUrl); Assert.Equal(activityServiceUrl.TrimEnd('/'), capturedActivity.ServiceUrl.ToString().TrimEnd('/')); + Assert.NotNull(capturedServiceUrl); + Assert.Equal(TestServiceUrl.TrimEnd('/'), capturedServiceUrl.ToString().TrimEnd('/')); mockConversationClient.Verify( c => c.UpdateActivityAsync( TestConversationId, TestActivityId, It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), @@ -311,8 +355,8 @@ public async Task SendToConversationWithHttpMessagesAsync_EnsuresConversationIdI CoreActivity? capturedActivity = null; mockConversationClient - .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) - .Callback?, CancellationToken>((act, _, _, _) => capturedActivity = act) + .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) + .Callback?, CancellationToken>((act, _, _, _, _) => capturedActivity = act) .ReturnsAsync(new SendActivityResponse { Id = TestActivityId }); // Act @@ -341,8 +385,8 @@ public async Task ReplyToActivityWithHttpMessagesAsync_SetsReplyToIdProperty() CoreActivity? capturedActivity = null; mockConversationClient - .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) - .Callback?, CancellationToken>((act, _, _, _) => capturedActivity = act) + .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) + .Callback?, CancellationToken>((act, _, _, _, _) => capturedActivity = act) .ReturnsAsync(new SendActivityResponse { Id = TestActivityId }); // Act @@ -365,7 +409,7 @@ public async Task SendToConversationWithHttpMessagesAsync_WhenSendActivityReturn // Arrange Mock mockConversationClient = CreateMockConversationClient(); mockConversationClient - .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) + .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) .ReturnsAsync((SendActivityResponse?)null); // Simulate 202 Accepted with no body CompatConversations compatConversations = new(mockConversationClient.Object) @@ -398,7 +442,7 @@ public async Task ReplyToActivityWithHttpMessagesAsync_WhenSendActivityReturnsNu // Arrange Mock mockConversationClient = CreateMockConversationClient(); mockConversationClient - .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) + .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) .ReturnsAsync((SendActivityResponse?)null); // Simulate 202 Accepted with no body CompatConversations compatConversations = new(mockConversationClient.Object) diff --git a/core/test/Microsoft.Teams.Apps.UnitTests/OAuthFlowTelemetryTests.cs b/core/test/Microsoft.Teams.Apps.UnitTests/OAuthFlowTelemetryTests.cs index 64c4932b..aa14992e 100644 --- a/core/test/Microsoft.Teams.Apps.UnitTests/OAuthFlowTelemetryTests.cs +++ b/core/test/Microsoft.Teams.Apps.UnitTests/OAuthFlowTelemetryTests.cs @@ -492,7 +492,7 @@ private static void SetupGetSignInResource(Mock mock) private static void SetupSendActivity(TestHarness harness) { harness.MockConversationClient - .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) + .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync(new SendActivityResponse { Id = "activity-1" }); } diff --git a/core/test/Microsoft.Teams.Apps.UnitTests/OAuthFlowTests.cs b/core/test/Microsoft.Teams.Apps.UnitTests/OAuthFlowTests.cs index 97b526b2..e97ef108 100644 --- a/core/test/Microsoft.Teams.Apps.UnitTests/OAuthFlowTests.cs +++ b/core/test/Microsoft.Teams.Apps.UnitTests/OAuthFlowTests.cs @@ -555,7 +555,7 @@ private static void SetupGetSignInResource(Mock mock) private static void SetupSendActivity(TestHarness harness) { harness.MockConversationClient - .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) + .Setup(c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) .ReturnsAsync(new SendActivityResponse { Id = "activity-1" }); } } diff --git a/core/test/Microsoft.Teams.Apps.UnitTests/PromptPreviewTests.cs b/core/test/Microsoft.Teams.Apps.UnitTests/PromptPreviewTests.cs index 2878f099..c05c5d03 100644 --- a/core/test/Microsoft.Teams.Apps.UnitTests/PromptPreviewTests.cs +++ b/core/test/Microsoft.Teams.Apps.UnitTests/PromptPreviewTests.cs @@ -190,11 +190,12 @@ private static CaptureSlot SetupCapture(TestHarness harness) harness.MockConversationClient .Setup(c => c.SendActivityAsync( It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny())) - .Callback?, CancellationToken>( - (activity, _, _, _) => slot.Value = activity) + .Callback?, CancellationToken>( + (activity, _, _, _, _) => slot.Value = activity) .ReturnsAsync(new SendActivityResponse { Id = "sent-id" }); return slot; } diff --git a/core/test/Microsoft.Teams.Core.UnitTests/BotApplicationTests.cs b/core/test/Microsoft.Teams.Core.UnitTests/BotApplicationTests.cs index 53075958..aff2b641 100644 --- a/core/test/Microsoft.Teams.Core.UnitTests/BotApplicationTests.cs +++ b/core/test/Microsoft.Teams.Core.UnitTests/BotApplicationTests.cs @@ -171,7 +171,7 @@ public async Task SendActivityAsync_WithValidActivity_SendsSuccessfully() ServiceUrl = new Uri("https://test.service.url/"), Conversation = new("conv123") }; - SendActivityResponse? result = await botApp.SendActivityAsync(activity); + SendActivityResponse? result = await botApp.SendActivityAsync(activity, new Uri("https://test.service.url/")); Assert.NotNull(result); Assert.Contains("activity123", result.Id); @@ -183,7 +183,7 @@ public async Task SendActivityAsync_WithNullActivity_ThrowsArgumentNullException BotApplication botApp = CreateBotApplication(); await Assert.ThrowsAsync(() => - botApp.SendActivityAsync(null!)); + botApp.SendActivityAsync(null!, new Uri("https://test.service.url/"))); } [Fact] diff --git a/core/test/Microsoft.Teams.Core.UnitTests/ConversationClientTests.cs b/core/test/Microsoft.Teams.Core.UnitTests/ConversationClientTests.cs index 4b089ca1..71ad3ab9 100644 --- a/core/test/Microsoft.Teams.Core.UnitTests/ConversationClientTests.cs +++ b/core/test/Microsoft.Teams.Core.UnitTests/ConversationClientTests.cs @@ -37,7 +37,7 @@ public async Task SendActivityAsync_WithValidActivity_SendsSuccessfully() Conversation = new("conv123") }; - SendActivityResponse? result = await conversationClient.SendActivityAsync(activity); + SendActivityResponse? result = await conversationClient.SendActivityAsync(activity, new Uri("https://test.service.url/")); Assert.NotNull(result); Assert.Contains("activity123", result.Id); @@ -50,7 +50,7 @@ public async Task SendActivityAsync_WithNullActivity_ThrowsArgumentNullException ConversationClient conversationClient = new(httpClient); await Assert.ThrowsAsync(() => - conversationClient.SendActivityAsync(null!)); + conversationClient.SendActivityAsync(null!, new Uri("https://test.service.url/"))); } [Fact] @@ -66,7 +66,7 @@ public async Task SendActivityAsync_WithNullConversation_ThrowsArgumentNullExcep }; await Assert.ThrowsAsync(() => - conversationClient.SendActivityAsync(activity)); + conversationClient.SendActivityAsync(activity, new Uri("https://test.service.url/"))); } [Fact] @@ -83,11 +83,11 @@ public async Task SendActivityAsync_WithEmptyConversationId_ThrowsArgumentExcept }; await Assert.ThrowsAsync(() => - conversationClient.SendActivityAsync(activity)); + conversationClient.SendActivityAsync(activity, new Uri("https://test.service.url/"))); } [Fact] - public async Task SendActivityAsync_WithNullServiceUrl_ThrowsArgumentNullException() + public async Task SendActivityAsync_WithNullServiceUrlParameter_ThrowsArgumentNullException() { HttpClient httpClient = new(); ConversationClient conversationClient = new(httpClient); @@ -99,7 +99,7 @@ public async Task SendActivityAsync_WithNullServiceUrl_ThrowsArgumentNullExcepti }; await Assert.ThrowsAsync(() => - conversationClient.SendActivityAsync(activity)); + conversationClient.SendActivityAsync(activity, null!)); } [Fact] @@ -129,7 +129,7 @@ public async Task SendActivityAsync_WithHttpError_ThrowsHttpRequestException() }; HttpRequestException exception = await Assert.ThrowsAsync(() => - conversationClient.SendActivityAsync(activity)); + conversationClient.SendActivityAsync(activity, new Uri("https://test.service.url/"))); Assert.Contains("Error sending activity", exception.Message); Assert.Contains("BadRequest", exception.Message); @@ -159,11 +159,11 @@ public async Task SendActivityAsync_ConstructsCorrectUrl() CoreActivity activity = new() { Type = ActivityType.Message, - ServiceUrl = new Uri("https://test.service.url/"), + ServiceUrl = new Uri("https://ignored.service.url/"), Conversation = new("conv123") }; - await conversationClient.SendActivityAsync(activity); + await conversationClient.SendActivityAsync(activity, new Uri("https://test.service.url/")); Assert.NotNull(capturedRequest); Assert.Equal("https://test.service.url/v3/conversations/conv123/activities/", capturedRequest.RequestUri?.ToString()); @@ -199,7 +199,7 @@ public async Task SendActivityAsync_WithIsTargeted_AppendsQueryString() Recipient = new ChannelAccount { IsTargeted = true } }; - await conversationClient.SendActivityAsync(activity); + await conversationClient.SendActivityAsync(activity, new Uri("https://test.service.url/")); Assert.NotNull(capturedRequest); Assert.Contains("isTargetedActivity=true", capturedRequest.RequestUri?.ToString()); @@ -232,7 +232,7 @@ public async Task UpdateActivityAsync_WithIsTargeted_AppendsQueryString() ServiceUrl = new Uri("https://test.service.url/") }; - await conversationClient.UpdateActivityAsync("conv123", "activity123", activity, isTargeted: true); + await conversationClient.UpdateActivityAsync("conv123", "activity123", activity, new Uri("https://test.service.url/"), isTargeted: true); Assert.NotNull(capturedRequest); Assert.Contains("isTargetedActivity=true", capturedRequest.RequestUri?.ToString()); @@ -297,7 +297,7 @@ public async Task DeleteActivityAsync_WithActivity_UsesIsTargetedProperty() ServiceUrl = new Uri("https://test.service.url/") }; - await conversationClient.DeleteActivityAsync("conv123", activity, isTargeted: true); + await conversationClient.DeleteActivityAsync("conv123", activity, new Uri("https://test.service.url/"), isTargeted: true); Assert.NotNull(capturedRequest); Assert.Contains("isTargetedActivity=true", capturedRequest.RequestUri?.ToString()); @@ -336,7 +336,7 @@ public async Task UpdateTargetedActivityAsync_AppendsQueryStringWithoutRecipient ServiceUrl = new Uri("https://test.service.url/"), }; - await conversationClient.UpdateTargetedActivityAsync("conv123", "activity123", activity); + await conversationClient.UpdateTargetedActivityAsync("conv123", "activity123", activity, new Uri("https://test.service.url/")); Assert.NotNull(capturedRequest); Assert.Contains("isTargetedActivity=true", capturedRequest.RequestUri?.ToString()); @@ -405,7 +405,7 @@ public async Task SendActivityAsync_WithAgentsChannel_TruncatesConversationId() Conversation = new(longConversationId) }; - await conversationClient.SendActivityAsync(activity); + await conversationClient.SendActivityAsync(activity, new Uri("https://test.service.url/")); Assert.NotNull(capturedRequest); string expectedTruncatedId = "acf"; @@ -444,7 +444,7 @@ public async Task SendActivityAsync_WithRecipientIsTargeted_DeserializedFromJson """; CoreActivity activity = CoreActivity.FromJsonString(activityJson); - await conversationClient.SendActivityAsync(activity); + await conversationClient.SendActivityAsync(activity, new Uri("https://test.service.url/")); Assert.NotNull(capturedRequest); Assert.Contains("isTargetedActivity=true", capturedRequest.RequestUri?.ToString()); @@ -482,7 +482,7 @@ public async Task SendActivityAsync_WithJsonElementFrom_ExtractsAgenticIdentity( """; CoreActivity activity = CoreActivity.FromJsonString(activityJson); - await conversationClient.SendActivityAsync(activity); + await conversationClient.SendActivityAsync(activity, new Uri("https://test.service.url/")); // Verify the request was made (agenticIdentity is passed to BotHttpClient via request options) Assert.NotNull(capturedRequest); @@ -518,7 +518,7 @@ public async Task SendActivityAsync_WithChannelAccountFrom_ExtractsAgenticIdenti From = from }; - SendActivityResponse? result = await conversationClient.SendActivityAsync(activity); + SendActivityResponse? result = await conversationClient.SendActivityAsync(activity, new Uri("https://test.service.url/")); Assert.NotNull(result); } diff --git a/core/test/Microsoft.Teams.Core.UnitTests/Diagnostics/TelemetryTests.cs b/core/test/Microsoft.Teams.Core.UnitTests/Diagnostics/TelemetryTests.cs index aa9010fb..cd53faa8 100644 --- a/core/test/Microsoft.Teams.Core.UnitTests/Diagnostics/TelemetryTests.cs +++ b/core/test/Microsoft.Teams.Core.UnitTests/Diagnostics/TelemetryTests.cs @@ -114,7 +114,7 @@ public async Task ConversationClient_SendActivityAsync_EmitsConversationClientSp Type = ActivityType.Message, ServiceUrl = new Uri("https://smba.example/"), Conversation = new("conv-1"), - }); + }, new Uri("https://smba.example/")); Assert.NotNull(response); Activity span = Assert.Single(spanCapture.Stopped, a => a.OperationName == "conversation_client"); @@ -144,7 +144,7 @@ await Assert.ThrowsAsync(() => client.SendActivityAsync(ne Type = ActivityType.Message, ServiceUrl = new Uri("https://smba.example/"), Conversation = new("conv-1"), - })); + }, new Uri("https://smba.example/"))); Activity span = Assert.Single(spanCapture.Stopped, a => a.OperationName == "conversation_client"); Assert.Equal(ActivityStatusCode.Error, span.Status); From 9fb54edeb5e1a4a3a523c6a128562d263bac2d1a Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 15:52:07 -0700 Subject: [PATCH 2/3] Remove Core activity service URL builder Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- core/samples/AFBot/Program.cs | 6 ++--- core/samples/CoreBot/Program.cs | 3 +-- core/samples/McpServer/McpTools.cs | 6 ++--- core/samples/Proactive/Worker.cs | 3 +-- .../Schema/TeamsActivityBuilder.cs | 22 ++++++++++++++++++ .../Schema/CoreActivityBuilder.cs | 21 ----------------- .../ConversationClientTests.cs | 5 ---- .../CreateConversationTests.cs | 2 -- .../CoreActivityBuilderTests.cs | 23 ------------------- 9 files changed, 29 insertions(+), 62 deletions(-) diff --git a/core/samples/AFBot/Program.cs b/core/samples/AFBot/Program.cs index fe7204a3..6bbb1deb 100644 --- a/core/samples/AFBot/Program.cs +++ b/core/samples/AFBot/Program.cs @@ -40,12 +40,11 @@ CoreActivity typing = CoreActivity.CreateBuilder() .WithType(ActivityType.Typing) - .WithServiceUrl(activity.ServiceUrl!) .WithChannelId(activity.ChannelId!) .WithConversation(activity.Conversation!) .WithFrom(activity.Recipient) .Build(); - await botApp.SendActivityAsync(typing, cancellationToken: cancellationToken); + await botApp.SendActivityAsync(typing, activity.ServiceUrl!, cancellationToken: cancellationToken); AgentRunResponse agentResponse = await agent.RunAsync(activity.Properties["text"]?.ToString() ?? "OMW", cancellationToken: timer.Token); @@ -53,14 +52,13 @@ Console.WriteLine($"AI:: GOT {agentResponse.Messages.Count} msgs"); CoreActivity replyActivity = CoreActivity.CreateBuilder() .WithType(ActivityType.Message) - .WithServiceUrl(activity.ServiceUrl!) .WithChannelId(activity.ChannelId!) .WithConversation(activity.Conversation!) .WithFrom(activity.Recipient) .WithProperty("text", m1!.Text) .Build(); - SendActivityResponse? res = await botApp.SendActivityAsync(replyActivity, cancellationToken: cancellationToken); + SendActivityResponse? res = await botApp.SendActivityAsync(replyActivity, activity.ServiceUrl!, cancellationToken: cancellationToken); Console.WriteLine("SENT >>> => " + res?.Id); }; diff --git a/core/samples/CoreBot/Program.cs b/core/samples/CoreBot/Program.cs index d893192d..08e7ed7a 100644 --- a/core/samples/CoreBot/Program.cs +++ b/core/samples/CoreBot/Program.cs @@ -19,13 +19,12 @@ CoreActivity replyActivity = CoreActivity.CreateBuilder() .WithType(ActivityType.Message) .WithChannelId(activity.ChannelId) - .WithServiceUrl(activity.ServiceUrl) .WithConversation(activity.Conversation) .WithFrom(activity.Recipient) .WithProperty("text", replyText) .Build(); - await botApp.SendActivityAsync(replyActivity, cancellationToken: cancellationToken); + await botApp.SendActivityAsync(replyActivity, activity.ServiceUrl!, cancellationToken: cancellationToken); }; webApp.Run(); diff --git a/core/samples/McpServer/McpTools.cs b/core/samples/McpServer/McpTools.cs index cac31b59..16a37f4c 100644 --- a/core/samples/McpServer/McpTools.cs +++ b/core/samples/McpServer/McpTools.cs @@ -29,7 +29,7 @@ public async Task Notify( .WithConversation(new Conversation(conversationId)) .WithText(message) .Build(); - await app.SendActivityAsync(notifyActivity, cancellationToken); + await app.SendActivityAsync(notifyActivity, state.ServiceUrl, cancellationToken); return new NotifyResult(Notified: true, UserId: userId); } @@ -53,7 +53,7 @@ public async Task Ask( .Build(); try { - await app.SendActivityAsync(askActivity, cancellationToken); + await app.SendActivityAsync(askActivity, state.ServiceUrl, cancellationToken); } catch { @@ -139,7 +139,7 @@ public async Task RequestApproval( state.Approvals[approvalId] = ApprovalStatus.Pending; try { - await app.SendActivityAsync(activity, cancellationToken); + await app.SendActivityAsync(activity, state.ServiceUrl, cancellationToken); } catch { diff --git a/core/samples/Proactive/Worker.cs b/core/samples/Proactive/Worker.cs index b84a3dca..641d8723 100644 --- a/core/samples/Proactive/Worker.cs +++ b/core/samples/Proactive/Worker.cs @@ -19,12 +19,11 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) if (logger.IsEnabled(LogLevel.Information)) { CoreActivity proactiveMessage = CoreActivity.CreateBuilder() - .WithServiceUrl(new Uri(ServiceUrl)) .WithConversation(new(ConversationId)) .Build(); proactiveMessage.From = new ChannelAccount { Id = FromId }; proactiveMessage.Properties["text"] = $"Proactive hello at {DateTimeOffset.Now}"; - SendActivityResponse? aid = await conversationClient.SendActivityAsync(proactiveMessage, cancellationToken: stoppingToken); + SendActivityResponse? aid = await conversationClient.SendActivityAsync(proactiveMessage, new Uri(ServiceUrl), cancellationToken: stoppingToken); logger.LogInformation("Activity {Aid} sent", aid?.Id ?? "unknown"); } await Task.Delay(1000, stoppingToken); diff --git a/core/src/Microsoft.Teams.Apps/Schema/TeamsActivityBuilder.cs b/core/src/Microsoft.Teams.Apps/Schema/TeamsActivityBuilder.cs index 999e2046..9d51b90e 100644 --- a/core/src/Microsoft.Teams.Apps/Schema/TeamsActivityBuilder.cs +++ b/core/src/Microsoft.Teams.Apps/Schema/TeamsActivityBuilder.cs @@ -27,6 +27,28 @@ internal TeamsActivityBuilder(TeamsActivity activity) : base(activity) { } + /// + /// Sets the service URL. + /// + /// The service URL. + /// The builder instance for chaining. + public TeamsActivityBuilder WithServiceUrl(Uri? serviceUrl) + { + _activity.ServiceUrl = serviceUrl; + return this; + } + + /// + /// Sets the service URL from a string. + /// + /// The service URL as a string. + /// The builder instance for chaining. + public TeamsActivityBuilder WithServiceUrl(string serviceUrlString) + { + _activity.ServiceUrl = new Uri(serviceUrlString); + return this; + } + /// /// Apply Conversation Reference from the specified activity. /// diff --git a/core/src/Microsoft.Teams.Core/Schema/CoreActivityBuilder.cs b/core/src/Microsoft.Teams.Core/Schema/CoreActivityBuilder.cs index e4a7c08c..1ebf5760 100644 --- a/core/src/Microsoft.Teams.Core/Schema/CoreActivityBuilder.cs +++ b/core/src/Microsoft.Teams.Core/Schema/CoreActivityBuilder.cs @@ -40,27 +40,6 @@ public TBuilder WithId(string id) return (TBuilder)this; } - /// - /// Sets the service URL. - /// - /// The service URL. - /// The builder instance for chaining. - public TBuilder WithServiceUrl(Uri? serviceUrl) - { - _activity.ServiceUrl = serviceUrl; - return (TBuilder)this; - } - /// - /// Sets the service URL from a string. - /// - /// The service URL as a string. - /// The builder instance for chaining. - public TBuilder WithServiceUrl(string serviceUrlString) - { - _activity.ServiceUrl = new Uri(serviceUrlString); - return (TBuilder)this; - } - /// /// Sets the channel ID. /// diff --git a/core/test/IntegrationTests/ConversationClientTests.cs b/core/test/IntegrationTests/ConversationClientTests.cs index 1cdb9a59..e738681d 100644 --- a/core/test/IntegrationTests/ConversationClientTests.cs +++ b/core/test/IntegrationTests/ConversationClientTests.cs @@ -30,7 +30,6 @@ public async Task SendActivity() CoreActivity activity = CoreActivity.CreateBuilder() .WithType(ActivityType.Message) .WithFrom(IntegrationTestFixture.GetChannelAccountWithAgenticProperties()) - .WithServiceUrl(_f.ServiceUrl) .WithConversation(new(_f.ConversationId)) .WithProperty("text", $"[ConversationClient] SendActivity at `{DateTime.UtcNow:s}`") .Build(); @@ -49,7 +48,6 @@ public async Task UpdateActivity() CoreActivity activity = CoreActivity.CreateBuilder() .WithType(ActivityType.Message) .WithFrom(IntegrationTestFixture.GetChannelAccountWithAgenticProperties()) - .WithServiceUrl(_f.ServiceUrl) .WithConversation(new(_f.ConversationId)) .WithProperty("text", $"[ConversationClient] Original at `{DateTime.UtcNow:s}`") .Build(); @@ -60,7 +58,6 @@ public async Task UpdateActivity() CoreActivity updated = CoreActivity.CreateBuilder() .WithType(ActivityType.Message) .WithFrom(IntegrationTestFixture.GetChannelAccountWithAgenticProperties()) - .WithServiceUrl(_f.ServiceUrl) .WithConversation(new(_f.ConversationId)) .WithProperty("text", $"[ConversationClient] Updated at `{DateTime.UtcNow:s}`") .Build(); @@ -79,7 +76,6 @@ public async Task DeleteActivity() CoreActivity activity = CoreActivity.CreateBuilder() .WithType(ActivityType.Message) .WithFrom(IntegrationTestFixture.GetChannelAccountWithAgenticProperties()) - .WithServiceUrl(_f.ServiceUrl) .WithConversation(new(_f.ConversationId)) .WithProperty("text", $"[ConversationClient] To delete at `{DateTime.UtcNow:s}`") .Build(); @@ -153,7 +149,6 @@ public async Task AddAndDeleteReaction() CoreActivity activity = CoreActivity.CreateBuilder() .WithType(ActivityType.Message) - .WithServiceUrl(_f.ServiceUrl) .WithFrom(IntegrationTestFixture.GetChannelAccountWithAgenticProperties()) .WithConversation(new(_f.ConversationId)) .WithProperty("text", $"[ConversationClient] Reaction test at `{DateTime.UtcNow:s}`") diff --git a/core/test/IntegrationTests/CreateConversationTests.cs b/core/test/IntegrationTests/CreateConversationTests.cs index ad2751b9..1e155057 100644 --- a/core/test/IntegrationTests/CreateConversationTests.cs +++ b/core/test/IntegrationTests/CreateConversationTests.cs @@ -85,7 +85,6 @@ public async Task Core_CreatePersonalChat_AndSendMessage() CoreActivity activity = CoreActivity.CreateBuilder() .WithType(ActivityType.Message) .WithFrom(IntegrationTestFixture.GetChannelAccountWithAgenticProperties()) - .WithServiceUrl(_f.ServiceUrl) .WithConversation(new(response.Id)) .WithProperty("text", $"[Core] 1:1 message at `{DateTime.UtcNow:s}`") .Build(); @@ -189,7 +188,6 @@ public async Task Core_CreateGroupChat_AndSendMessage() CoreActivity activity = CoreActivity.CreateBuilder() .WithType(ActivityType.Message) .WithFrom(IntegrationTestFixture.GetChannelAccountWithAgenticProperties()) - .WithServiceUrl(_f.ServiceUrl) .WithConversation(new(response.Id)) .WithProperty("text", $"[Core] Group message at `{DateTime.UtcNow:s}`") .Build(); diff --git a/core/test/Microsoft.Teams.Core.UnitTests/CoreActivityBuilderTests.cs b/core/test/Microsoft.Teams.Core.UnitTests/CoreActivityBuilderTests.cs index fa8afa59..cbdbce5e 100644 --- a/core/test/Microsoft.Teams.Core.UnitTests/CoreActivityBuilderTests.cs +++ b/core/test/Microsoft.Teams.Core.UnitTests/CoreActivityBuilderTests.cs @@ -46,18 +46,6 @@ public void WithId_SetsActivityId() Assert.Equal("test-activity-id", activity.Id); } - [Fact] - public void WithServiceUrl_SetsServiceUrl() - { - Uri serviceUrl = new("https://smba.trafficmanager.net/teams/"); - - CoreActivity activity = new CoreActivityBuilder() - .WithServiceUrl(serviceUrl) - .Build(); - - Assert.Equal(serviceUrl, activity.ServiceUrl); - } - [Fact] public void WithChannelId_SetsChannelId() { @@ -96,7 +84,6 @@ public void FluentAPI_CompleteActivity_BuildsCorrectly() .WithId("activity-123") .WithChannelId("msteams") .WithProperty("text", "Test message") - .WithServiceUrl(new Uri("https://smba.trafficmanager.net/teams/")) .Build(); Assert.Equal(ActivityType.Message, activity.Type); @@ -196,14 +183,4 @@ public void Build_AfterModificationThenBuild_ReflectsChanges() Assert.Equal("id-2", activity2.Id); } - [Fact] - public void WithServiceUrl_String_SetsServiceUrl() - { - CoreActivity activity = new CoreActivityBuilder() - .WithServiceUrl("https://smba.trafficmanager.net/teams/") - .Build(); - - Assert.Equal(new Uri("https://smba.trafficmanager.net/teams/"), activity.ServiceUrl); - } - } From 743bd93ab151f359e665c9380f6b7fe4b8fa2c54 Mon Sep 17 00:00:00 2001 From: heyitsaamir Date: Fri, 10 Jul 2026 16:04:33 -0700 Subject: [PATCH 3/3] Respect activity service URLs in compat sends Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- core/samples/A2ABot/A2A/A2AServer.cs | 2 +- core/samples/TabApp/Program.cs | 2 +- .../TeamsBot/WelcomeMessageMiddleware.cs | 2 +- .../CompatConversations.cs | 23 ++++++++++++++----- .../CompatConversationsTests.cs | 12 +++++----- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/core/samples/A2ABot/A2A/A2AServer.cs b/core/samples/A2ABot/A2A/A2AServer.cs index 294c5f43..d80a696d 100644 --- a/core/samples/A2ABot/A2A/A2AServer.cs +++ b/core/samples/A2ABot/A2A/A2AServer.cs @@ -73,7 +73,7 @@ public async Task ExecuteAsync(RequestContext context, AgentEventQueue eventQueu .WithServiceUrl(serviceUrl) .WithConversation(new TeamsConversation { Id = newConvId }) .Build(); - SendActivityResponse? sent = await conversations.SendActivityAsync(proactive, cancellationToken: ct); + SendActivityResponse? sent = await conversations.SendActivityAsync(proactive, serviceUrl, cancellationToken: ct); logger.LogInformation("[{Bot}/A2A] proactive greeting sent (conv={ConvId}, activityId={ActivityId})", config.Name, newConvId, sent?.Id ?? ""); diff --git a/core/samples/TabApp/Program.cs b/core/samples/TabApp/Program.cs index a2d18ee4..52062b50 100644 --- a/core/samples/TabApp/Program.cs +++ b/core/samples/TabApp/Program.cs @@ -84,7 +84,7 @@ .WithServiceUrl(serviceUrl) .WithConversation(new TeamsConversation { Id = conversationId! }) .Build(); - await conversations.SendActivityAsync(activity, cancellationToken: ct); + await conversations.SendActivityAsync(activity, serviceUrl, cancellationToken: ct); return Results.Json(new PostToChatResult(Ok: true)); }).RequireAuthorization(); diff --git a/core/samples/TeamsBot/WelcomeMessageMiddleware.cs b/core/samples/TeamsBot/WelcomeMessageMiddleware.cs index 11fd64da..9514ff3c 100644 --- a/core/samples/TeamsBot/WelcomeMessageMiddleware.cs +++ b/core/samples/TeamsBot/WelcomeMessageMiddleware.cs @@ -45,7 +45,7 @@ public async Task OnTurnAsync(BotApplication botApplication, CoreActivity activi .WithConversationReference(TeamsActivity.FromActivity(activity)) .Build(); - await botApplication.SendActivityAsync(welcomeActivity, cancellationToken: cancellationToken); + await botApplication.SendActivityAsync(welcomeActivity, activity.ServiceUrl!, cancellationToken); _hasSentWelcomeMessage = true; } diff --git a/core/src/Microsoft.Teams.Apps.BotBuilder/CompatConversations.cs b/core/src/Microsoft.Teams.Apps.BotBuilder/CompatConversations.cs index d04afb4d..fee708b6 100644 --- a/core/src/Microsoft.Teams.Apps.BotBuilder/CompatConversations.cs +++ b/core/src/Microsoft.Teams.Apps.BotBuilder/CompatConversations.cs @@ -232,7 +232,6 @@ public async Task> GetConversationsWi public async Task> ReplyToActivityWithHttpMessagesAsync(string conversationId, string activityId, Activity activity, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default) { - ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl); Dictionary? convertedHeaders = ConvertHeaders(customHeaders); CoreActivity coreActivity = activity.FromBotFrameworkActivity(); @@ -240,7 +239,7 @@ public async Task> ReplyToActivityWithHt coreActivity.ReplyToId = activityId; coreActivity.Conversation = new Microsoft.Teams.Core.Schema.Conversation(conversationId); - SendActivityResponse? response = await _client.SendActivityAsync(coreActivity, new Uri(ServiceUrl), requestContext: RequestContext, customHeaders: convertedHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); + SendActivityResponse? response = await _client.SendActivityAsync(coreActivity, ResolveActivityServiceUrl(activity, "reply to activities"), requestContext: RequestContext, customHeaders: convertedHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); ResourceResponse resourceResponse = new() { @@ -298,14 +297,13 @@ public async Task> SendConversationHisto /// public async Task> SendToConversationWithHttpMessagesAsync(string conversationId, Activity activity, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default) { - ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl); Dictionary? convertedHeaders = ConvertHeaders(customHeaders); CoreActivity coreActivity = activity.FromBotFrameworkActivity(); coreActivity.Conversation = new Microsoft.Teams.Core.Schema.Conversation(conversationId); - SendActivityResponse? response = await _client.SendActivityAsync(coreActivity, new Uri(ServiceUrl), requestContext: RequestContext, customHeaders: convertedHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); + SendActivityResponse? response = await _client.SendActivityAsync(coreActivity, ResolveActivityServiceUrl(activity, "send activities"), requestContext: RequestContext, customHeaders: convertedHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); ResourceResponse resourceResponse = new() { @@ -333,7 +331,6 @@ public async Task> SendToConversationWit /// public async Task> UpdateActivityWithHttpMessagesAsync(string conversationId, string activityId, Activity activity, Dictionary>? customHeaders = null, CancellationToken cancellationToken = default) { - ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl); Dictionary? convertedHeaders = ConvertHeaders(customHeaders); CoreActivity coreActivity = activity.FromBotFrameworkActivity(); @@ -342,7 +339,7 @@ public async Task> UpdateActivityWithHtt conversationId, activityId, coreActivity, - new Uri(ServiceUrl), + ResolveActivityServiceUrl(activity, "update activities"), requestContext: RequestContext, customHeaders: convertedHeaders, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -408,6 +405,20 @@ public async Task> UploadAttachmentWithH return convertedHeaders; } + private Uri ResolveActivityServiceUrl(Activity activity, string operation) + { + string? serviceUrl = !string.IsNullOrWhiteSpace(activity.ServiceUrl) + ? activity.ServiceUrl + : ServiceUrl; + + if (string.IsNullOrWhiteSpace(serviceUrl)) + { + throw new InvalidOperationException($"ServiceUrl is required to {operation}."); + } + + return new Uri(serviceUrl); + } + public async Task> GetConversationMemberWithHttpMessagesAsync(string userId, string conversationId, Dictionary> customHeaders = null!, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(ServiceUrl); diff --git a/core/test/Microsoft.Teams.Apps.BotBuilder.UnitTests/CompatConversationsTests.cs b/core/test/Microsoft.Teams.Apps.BotBuilder.UnitTests/CompatConversationsTests.cs index 53927059..5bdbcf86 100644 --- a/core/test/Microsoft.Teams.Apps.BotBuilder.UnitTests/CompatConversationsTests.cs +++ b/core/test/Microsoft.Teams.Apps.BotBuilder.UnitTests/CompatConversationsTests.cs @@ -57,7 +57,7 @@ public async Task SendToConversationWithHttpMessagesAsync_PassesServiceUrlFromPr } [Fact] - public async Task SendToConversationWithHttpMessagesAsync_DoesNotOverrideServiceUrl_WhenActivityServiceUrlIsSet() + public async Task SendToConversationWithHttpMessagesAsync_UsesActivityServiceUrl_WhenActivityServiceUrlIsSet() { // Arrange const string activityServiceUrl = "https://custom.service.url/"; @@ -93,7 +93,7 @@ public async Task SendToConversationWithHttpMessagesAsync_DoesNotOverrideService Assert.NotNull(capturedActivity.ServiceUrl); Assert.Equal(activityServiceUrl.TrimEnd('/'), capturedActivity.ServiceUrl.ToString().TrimEnd('/')); Assert.NotNull(capturedServiceUrl); - Assert.Equal(TestServiceUrl.TrimEnd('/'), capturedServiceUrl.ToString().TrimEnd('/')); + Assert.Equal(activityServiceUrl.TrimEnd('/'), capturedServiceUrl.ToString().TrimEnd('/')); mockConversationClient.Verify( c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny()), Times.Once); @@ -141,7 +141,7 @@ public async Task ReplyToActivityWithHttpMessagesAsync_PassesServiceUrlFromPrope } [Fact] - public async Task ReplyToActivityWithHttpMessagesAsync_DoesNotOverrideServiceUrl_WhenActivityServiceUrlIsSet() + public async Task ReplyToActivityWithHttpMessagesAsync_UsesActivityServiceUrl_WhenActivityServiceUrlIsSet() { // Arrange const string activityServiceUrl = "https://custom.service.url/"; @@ -177,7 +177,7 @@ public async Task ReplyToActivityWithHttpMessagesAsync_DoesNotOverrideServiceUrl Assert.NotNull(capturedActivity.ServiceUrl); Assert.Equal(activityServiceUrl.TrimEnd('/'), capturedActivity.ServiceUrl.ToString().TrimEnd('/')); Assert.NotNull(capturedServiceUrl); - Assert.Equal(TestServiceUrl.TrimEnd('/'), capturedServiceUrl.ToString().TrimEnd('/')); + Assert.Equal(activityServiceUrl.TrimEnd('/'), capturedServiceUrl.ToString().TrimEnd('/')); mockConversationClient.Verify( c => c.SendActivityAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny?>(), It.IsAny()), Times.Once); @@ -279,7 +279,7 @@ public async Task UpdateActivityWithHttpMessagesAsync_PassesRequestContext() } [Fact] - public async Task UpdateActivityWithHttpMessagesAsync_DoesNotOverrideServiceUrl_WhenActivityServiceUrlIsSet() + public async Task UpdateActivityWithHttpMessagesAsync_UsesActivityServiceUrl_WhenActivityServiceUrlIsSet() { // Arrange const string activityServiceUrl = "https://custom.service.url/"; @@ -323,7 +323,7 @@ public async Task UpdateActivityWithHttpMessagesAsync_DoesNotOverrideServiceUrl_ Assert.NotNull(capturedActivity.ServiceUrl); Assert.Equal(activityServiceUrl.TrimEnd('/'), capturedActivity.ServiceUrl.ToString().TrimEnd('/')); Assert.NotNull(capturedServiceUrl); - Assert.Equal(TestServiceUrl.TrimEnd('/'), capturedServiceUrl.ToString().TrimEnd('/')); + Assert.Equal(activityServiceUrl.TrimEnd('/'), capturedServiceUrl.ToString().TrimEnd('/')); mockConversationClient.Verify( c => c.UpdateActivityAsync( TestConversationId,