• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

microsoft / botbuilder-dotnet / 387980

18 Apr 2024 02:30PM UTC coverage: 78.177% (+0.005%) from 78.172%
387980

Pull #6776

CI-PR build

web-flow
Merge e3a39ab5e into ed06db290
Pull Request #6776: Add counter to onError trigger to break possible loops

26198 of 33511 relevant lines covered (78.18%)

0.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.29
/libraries/Microsoft.Bot.Builder.Dialogs/DialogExtensions.cs
1
// Copyright (c) Microsoft Corporation. All rights reserved.
2
// Licensed under the MIT License.
3

4
using System;
5
using System.Collections.Generic;
6
using System.Linq;
7
using System.Security.Claims;
8
using System.Security.Principal;
9
using System.Threading;
10
using System.Threading.Tasks;
11
using Microsoft.Bot.Builder.Dialogs.Memory;
12
using Microsoft.Bot.Builder.Skills;
13
using Microsoft.Bot.Connector.Authentication;
14
using Microsoft.Bot.Schema;
15
using Newtonsoft.Json.Linq;
16

17
namespace Microsoft.Bot.Builder.Dialogs
18
{
19
    /// <summary>
20
    /// Provides extension methods for <see cref="Dialog"/> and derived classes.
21
    /// </summary>
22
    public static class DialogExtensions
23
    {
24
        /// <summary>
25
        /// Creates a dialog stack and starts a dialog, pushing it onto the stack.
26
        /// </summary>
27
        /// <param name="dialog">The dialog to start.</param>
28
        /// <param name="turnContext">The context for the current turn of the conversation.</param>
29
        /// <param name="accessor">The <see cref="IStatePropertyAccessor{DialogState}"/> accessor
30
        /// with which to manage the state of the dialog stack.</param>
31
        /// <param name="cancellationToken">A cancellation token that can be used by other objects
32
        /// or threads to receive notice of cancellation.</param>
33
        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
34
        public static async Task RunAsync(this Dialog dialog, ITurnContext turnContext, IStatePropertyAccessor<DialogState> accessor, CancellationToken cancellationToken)
35
        {
36
            var dialogSet = new DialogSet(accessor);
1✔
37

38
            // look for the IBotTelemetryClient on the TurnState, if not there take it from the Dialog, if not there fall back to the "null" default
39
            dialogSet.TelemetryClient = turnContext.TurnState.Get<IBotTelemetryClient>() ?? dialog.TelemetryClient ?? NullBotTelemetryClient.Instance;
×
40

41
            dialogSet.Add(dialog);
1✔
42

43
            var dialogContext = await dialogSet.CreateContextAsync(turnContext, cancellationToken).ConfigureAwait(false);
1✔
44

45
            await InternalRunAsync(turnContext, dialog.Id, dialogContext, null, cancellationToken).ConfigureAwait(false);
1✔
46
        }
1✔
47

48
        internal static async Task<DialogTurnResult> InternalRunAsync(ITurnContext turnContext, string dialogId, DialogContext dialogContext, DialogStateManagerConfiguration stateConfiguration, CancellationToken cancellationToken)
49
        {
50
            // map TurnState into root dialog context.services
51
            foreach (var service in turnContext.TurnState)
1✔
52
            {
53
                dialogContext.Services[service.Key] = service.Value;
1✔
54
            }
55

56
            var dialogStateManager = new DialogStateManager(dialogContext, stateConfiguration);
1✔
57
            await dialogStateManager.LoadAllScopesAsync(cancellationToken).ConfigureAwait(false);
1✔
58
            dialogContext.Context.TurnState.Add(dialogStateManager);
1✔
59

60
            DialogTurnResult dialogTurnResult = null;
1✔
61

62
            // Loop as long as we are getting valid OnError handled we should continue executing the actions for the turn.
63
            //
64
            // NOTE: We loop around this block because each pass through we either complete the turn and break out of the loop
65
            // or we have had an exception AND there was an OnError action which captured the error.  We need to continue the 
66
            // turn based on the actions the OnError handler introduced.
67
            var endOfTurn = false;
1✔
68
            var errorHandlerCalled = 0;
1✔
69

70
            while (!endOfTurn)
1✔
71
            {
72
                try
73
                {
74
                    dialogTurnResult = await InnerRunAsync(turnContext, dialogId, dialogContext, cancellationToken).ConfigureAwait(false);
1✔
75

76
                    // turn successfully completed, break the loop
77
                    endOfTurn = true;
1✔
78
                }
1✔
79
                catch (Exception err)
1✔
80
                {
81
                    var handled = false;
1✔
82
                    var innerExceptions = new List<Exception>();
1✔
83
                    try
84
                    {
85
                        errorHandlerCalled++;
1✔
86

87
                        // fire error event, bubbling from the leaf.
88
                        handled = await dialogContext.EmitEventAsync(DialogEvents.Error, err, bubble: true, fromLeaf: true, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
89

90
                        var turnObject = (JObject)turnContext.TurnState["turn"];
1✔
91
                        
92
                        var executionLimit = 0;
1✔
93

94
                        foreach (var childToken in turnObject.Children<JProperty>())
1✔
95
                        {
96
                            if (childToken.Name == "executionLimit")
1✔
97
                            {
98
                                executionLimit = (int)childToken.Value;
1✔
99
                            }
100
                        }
101

102
                        if (executionLimit > 0 && errorHandlerCalled > executionLimit)
1✔
103
                        {
104
                            // if the errorHandler has being called multiple times, there's an error inside the onError.
105
                            // We should throw the exception and break the loop.
106
                            handled = false;
1✔
107
                        }
108
                    }
1✔
109
#pragma warning disable CA1031 // Do not catch general exception types (capture the error in case it's not handled properly)
110
                    catch (Exception emitErr)
1✔
111
#pragma warning restore CA1031 // Do not catch general exception types
112
                    {
113
                        innerExceptions.Add(emitErr);
1✔
114
                    }
1✔
115

116
                    if (innerExceptions.Any())
1✔
117
                    {
118
                        innerExceptions.Add(err);
1✔
119
                        throw new AggregateException("Unable to emit the error as a DialogEvent.", innerExceptions);
1✔
120
                    }
121

122
                    if (!handled)
1✔
123
                    {
124
                        // error was NOT handled, throw the exception and end the turn. (This will trigger the Adapter.OnError handler and end the entire dialog stack)
125
                        throw;
×
126
                    }
127
                }
1✔
128
            }
129

130
            // save all state scopes to their respective botState locations.
131
            await dialogStateManager.SaveAllChangesAsync(cancellationToken).ConfigureAwait(false);
1✔
132

133
            // return the redundant result because the DialogManager contract expects it
134
            return dialogTurnResult;
1✔
135
        }
1✔
136

137
        private static async Task<DialogTurnResult> InnerRunAsync(ITurnContext turnContext, string dialogId, DialogContext dialogContext, CancellationToken cancellationToken)
138
        {
139
            // Handle EoC and Reprompt event from a parent bot (can be root bot to skill or skill to skill)
140
            if (IsFromParentToSkill(turnContext))
1✔
141
            {
142
                // Handle remote cancellation request from parent.
143
                if (turnContext.Activity.Type == ActivityTypes.EndOfConversation)
1✔
144
                {
145
                    if (!dialogContext.Stack.Any())
1✔
146
                    {
147
                        // No dialogs to cancel, just return.
148
                        return new DialogTurnResult(DialogTurnStatus.Empty);
×
149
                    }
150

151
                    var activeDialogContext = GetActiveDialogContext(dialogContext);
1✔
152

153
                    // Send cancellation message to the top dialog in the stack to ensure all the parents are canceled in the right order. 
154
                    return await activeDialogContext.CancelAllDialogsAsync(true, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
155
                }
156

157
                // Handle a reprompt event sent from the parent.
158
                if (turnContext.Activity.Type == ActivityTypes.Event && turnContext.Activity.Name == DialogEvents.RepromptDialog)
1✔
159
                {
160
                    if (!dialogContext.Stack.Any())
1✔
161
                    {
162
                        // No dialogs to reprompt, just return.
163
                        return new DialogTurnResult(DialogTurnStatus.Empty);
1✔
164
                    }
165

166
                    await dialogContext.RepromptDialogAsync(cancellationToken).ConfigureAwait(false);
1✔
167
                    return new DialogTurnResult(DialogTurnStatus.Waiting);
1✔
168
                }
169
            }
170

171
            // Continue or start the dialog.
172
            var result = await dialogContext.ContinueDialogAsync(cancellationToken).ConfigureAwait(false);
1✔
173
            if (result.Status == DialogTurnStatus.Empty)
1✔
174
            {
175
                result = await dialogContext.BeginDialogAsync(dialogId, null, cancellationToken).ConfigureAwait(false);
1✔
176
            }
177

178
            await SendStateSnapshotTraceAsync(dialogContext, cancellationToken).ConfigureAwait(false);
1✔
179

180
            // Skills should send EoC when the dialog completes.
181
            if (result.Status == DialogTurnStatus.Complete || result.Status == DialogTurnStatus.Cancelled)
1✔
182
            {
183
                if (SendEoCToParent(turnContext))
1✔
184
                {
185
                    // Send End of conversation at the end.
186
                    var code = result.Status == DialogTurnStatus.Complete ? EndOfConversationCodes.CompletedSuccessfully : EndOfConversationCodes.UserCancelled;
×
187
                    var activity = new Activity(ActivityTypes.EndOfConversation) { Value = result.Result, Locale = turnContext.Activity.Locale, Code = code };
1✔
188
                    await turnContext.SendActivityAsync(activity, cancellationToken).ConfigureAwait(false);
1✔
189
                }
190
            }
191

192
            return result;
1✔
193
        }
1✔
194

195
        /// <summary>
196
        /// Helper to send a trace activity with a memory snapshot of the active dialog DC. 
197
        /// </summary>
198
        private static async Task SendStateSnapshotTraceAsync(DialogContext dialogContext, CancellationToken cancellationToken)
199
        {
200
            var traceLabel = dialogContext.Context.TurnState.Get<IIdentity>(BotAdapter.BotIdentityKey) is ClaimsIdentity claimIdentity && SkillValidation.IsSkillClaim(claimIdentity.Claims)
1✔
201
                ? "Skill State"
1✔
202
                : "Bot State";
1✔
203

204
            // send trace of memory
205
            var snapshot = GetActiveDialogContext(dialogContext).State.GetMemorySnapshot();
1✔
206
            var traceActivity = (Activity)Activity.CreateTraceActivity("BotState", "https://www.botframework.com/schemas/botState", snapshot, traceLabel);
1✔
207
            await dialogContext.Context.SendActivityAsync(traceActivity, cancellationToken).ConfigureAwait(false);
1✔
208
        }
1✔
209

210
        /// <summary>
211
        /// Helper to determine if we should send an EoC to the parent or not.
212
        /// </summary>
213
        private static bool SendEoCToParent(ITurnContext turnContext)
214
        {
215
            if (turnContext.TurnState.Get<IIdentity>(BotAdapter.BotIdentityKey) is ClaimsIdentity claimIdentity && SkillValidation.IsSkillClaim(claimIdentity.Claims))
1✔
216
            {
217
                // EoC Activities returned by skills are bounced back to the bot by SkillHandler.
218
                // In those cases we will have a SkillConversationReference instance in state.
219
                var skillConversationReference = turnContext.TurnState.Get<SkillConversationReference>(SkillHandler.SkillConversationReferenceKey);
1✔
220
                if (skillConversationReference != null)
1✔
221
                {
222
                    // If the skillConversationReference.OAuthScope is for one of the supported channels, we are at the root and we should not send an EoC.
223
                    return skillConversationReference.OAuthScope != AuthenticationConstants.ToChannelFromBotOAuthScope && skillConversationReference.OAuthScope != GovernmentAuthenticationConstants.ToChannelFromBotOAuthScope;
1✔
224
                }
225

226
                return true;
1✔
227
            }
228

229
            return false;
1✔
230
        }
231

232
        private static bool IsFromParentToSkill(ITurnContext turnContext)
233
        {
234
            if (turnContext.TurnState.Get<SkillConversationReference>(SkillHandler.SkillConversationReferenceKey) != null)
1✔
235
            {
236
                return false;
1✔
237
            }
238

239
            return turnContext.TurnState.Get<IIdentity>(BotAdapter.BotIdentityKey) is ClaimsIdentity claimIdentity && SkillValidation.IsSkillClaim(claimIdentity.Claims);
1✔
240
        }
241

242
        // Recursively walk up the DC stack to find the active DC.
243
        private static DialogContext GetActiveDialogContext(DialogContext dialogContext)
244
        {
245
            var child = dialogContext.Child;
1✔
246
            if (child == null)
1✔
247
            {
248
                return dialogContext;
1✔
249
            }
250

251
            return GetActiveDialogContext(child);
1✔
252
        }
253
    }
254
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc