/// <summary> /// Sends a set of activities to the sender of the incoming activity. /// </summary> /// <param name="activities">The activities to send.</param> /// <param name="cancellationToken">Cancellation token.</param> /// <returns>A task that represents the work queued to execute.</returns> /// <remarks>If the activities are successfully sent, the task result contains /// an array of <see cref="ResourceResponse"/> objects containing the IDs that /// the receiving channel assigned to the activities.</remarks> public Task <ResourceResponse[]> SendActivitiesAsync(IActivity[] activities, CancellationToken cancellationToken = default(CancellationToken)) { if (activities == null) { throw new ArgumentNullException(nameof(activities)); } if (activities.Length == 0) { throw new ArgumentException("Expecting one or more activities, but the array was empty.", nameof(activities)); } var conversationReference = this.Activity.GetConversationReference(); var bufferedActivities = new List <Activity>(activities.Length); for (var index = 0; index < activities.Length; index++) { // Buffer the incoming activities into a List<T> since we allow the set to be manipulated by the callbacks // Bind the relevant Conversation Reference properties, such as URLs and // ChannelId's, to the activity we're about to send bufferedActivities.Add(activities[index].ApplyConversationReference(conversationReference)); } // If there are no callbacks registered, bypass the overhead of invoking them and send directly to the adapter if (_onSendActivities.Count == 0) { return(SendActivitiesThroughAdapter()); } // Send through the full callback pipeline return(SendActivitiesThroughCallbackPipeline()); Task <ResourceResponse[]> SendActivitiesThroughCallbackPipeline(int nextCallbackIndex = 0) { // If we've executed the last callback, we now send straight to the adapter if (nextCallbackIndex == _onSendActivities.Count) { return(SendActivitiesThroughAdapter()); } return(_onSendActivities[nextCallbackIndex].Invoke(this, bufferedActivities, () => SendActivitiesThroughCallbackPipeline(nextCallbackIndex + 1))); } async Task <ResourceResponse[]> SendActivitiesThroughAdapter() { if (Activity.DeliveryMode == DeliveryModes.ExpectReplies) { var responses = new ResourceResponse[bufferedActivities.Count]; var sentNonTraceActivity = false; for (var index = 0; index < responses.Length; index++) { var activity = bufferedActivities[index]; BufferedReplyActivities.Add(activity); // Ensure the TurnState has the InvokeResponseKey, since this activity // is not being sent through the adapter, where it would be added to TurnState. if (activity.Type == ActivityTypesEx.InvokeResponse) { TurnState.Add(BotFrameworkAdapter.InvokeResponseKey, activity); } responses[index] = new ResourceResponse(); sentNonTraceActivity |= activity.Type != ActivityTypes.Trace; } if (sentNonTraceActivity) { Responded = true; } return(responses); } else { // Send from the list which may have been manipulated via the event handlers. // Note that 'responses' was captured from the root of the call, and will be // returned to the original caller. var responses = await Adapter.SendActivitiesAsync(this, bufferedActivities.ToArray(), cancellationToken).ConfigureAwait(false); var sentNonTraceActivity = false; for (var index = 0; index < responses.Length; index++) { var activity = bufferedActivities[index]; activity.Id = responses[index].Id; sentNonTraceActivity |= activity.Type != ActivityTypes.Trace; } if (sentNonTraceActivity) { Responded = true; } return(responses); } } }