After using ConversationReset extra dialog is added to the stack (causing problems) - c#

If the user hasn't talked to the bot in a while, I run the following code in the MessageController (before calling Converasation.SendAsync()) to clear the conversation:
// get client from bot Activity
client = activity.GetStateClient();
if (user.LastMessageTime < DateTime.UtcNow.AddHours(-ConversationFreshnessHours))
{
// clear the state
client.BotState.DeleteStateForUser(activity.ChannelId, activity.From.Id);
}
// update last message time stamp
await user.UpdateLastMessageTime();
Note: The user object above is our docdb user that we load based on the channelId and userId. The property LastMessageTime is set after we (might) clear state.
However, after doing this, the bot fails after any call to Wait fails with the InvalidNeedException with message:
invalid need: expected Call, have Wait
I wrote some code to inspect the dialog stack in the private methods and discovered that when this code gets invoked an additional dialog is added to the stack at the base of my normal stack.
Normal Frame 0:
Wait: Wait IMessageActivity for Sage.Bot.Dialogs.RootDialog.Entry.Rest have IMessageActivity Microsoft.Bot.Connector.Activity`
Frame 0 after calling DeleteStateForUser:
Wait: Wait Object for Microsoft.Bot.Builder.Dialogs.Chain+LoopDialog`1[System.Object].ResumeAsync.Rest have Object
For some reason when this dialog is on on the stack, it causes the above exception.
Does anyone know how to clear the conversation without causing this problem?

Please check out how the DeletProfileScorable clears the user state:
https://github.com/Microsoft/BotBuilder/blob/497252e8d9949be20baa2cebaa6ce56de04461cf/CSharp/Library/Microsoft.Bot.Builder/Dialogs/DeleteProfileScorable.cs
this.stack.Reset();
botData.UserData.Clear();
botData.PrivateConversationData.Clear();
await botData.FlushAsync(token);
await botToUser.PostAsync(Resources.UserProfileDeleted);
If you use autofac to load bot data and retrieve the stack, you can duplicate this code in your MessageController:
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, activity))
{
var botData = scope.Resolve<IBotData>();
await botData.LoadAsync(new System.Threading.CancellationToken());
var stack = scope.Resolve<IDialogStack>();
stack.Reset();
botData.UserData.Clear();
botData.PrivateConversationData.Clear();
await botData.FlushAsync(token);
}

Related

WebView2 CallDevToolsProtocolMethodAsync issue with Fetch.continueRequest

I'm using WebView2 in a .net 5 WPF app and have been playing with the devtools protocol as a means of intercepting specific requests for assets. In looking at the Chrome dev docs (https://chromedevtools.github.io/devtools-protocol/), it's possible to intercept requests and then decide whether to continue them, cancel them or satisfy them yourself.
I've been able to successfully intercept the first web request (Eg. https:// www.somedomain.tld), but I've not been able to successfully continue the request (which would presumably trigger any other asset requests made as a result of the parsed html response).
After WebView initialization, I do the following (which works):
// Intercept requests
var receiver = webView.CoreWebView2.GetDevToolsProtocolEventReceiver("Fetch.requestPaused");
receiver.DevToolsProtocolEventReceived += FetchRequestPaused;
await webView.CoreWebView2.CallDevToolsProtocolMethodAsync("Fetch.enable", "{}");
This is my event handler - which doesn't do what I'm expecting it to (although it doesn't deadlock now at least):
private void FetchRequestPaused(object sender, Microsoft.Web.WebView2.Core.CoreWebView2DevToolsProtocolEventReceivedEventArgs e)
{
var doc = JsonDocument.Parse(e.ParameterObjectAsJson);
var id = doc.RootElement.GetProperty("requestId");
var payload = $"{{\"requestId\":\"{id}.0\"}}";
// We can't do this as an async call as it will try to post to the main thread, which is
// busy waiting in this event handler, so we deadlock
//_ = await webView.CoreWebView2.CallDevToolsProtocolMethodAsync("Fetch.continueRequest", payload);
// Exception: Value does not fall within the expected range.
// var result = await webView.CoreWebView2.CallDevToolsProtocolMethodAsync("Fetch.continueRequest", payload).ConfigureAwait(false);
// PROBLEM: This invokes the call on the UI thread OK...
Application.Current.Dispatcher.Invoke(new Action(() =>
{
// ...but it doesn't actually do anything
webView.CoreWebView2.CallDevToolsProtocolMethodAsync("Fetch.continueRequest", payload);
}));
}
Not only does the requested page not finish loading, but the browser is left in an unusual state and so right-clicking on the control and selecting "Refresh" will crash - yielding a COMException:
System.Runtime.InteropServices.COMException: 'The group or resource is not in the correct state to perform the requested operation. (0x8007139F)'
Can anyone see what I'm doing wrong here or am missing??
Thanks!
Additional information
In swapping out the events for the deprecated Network.setRequestInterception / Network.continueInterceptedRequest equivalents, I'm seeing the same behaviour - which at least tells us that it's either a problem with my calling code (most likely) or a bug in WebView2 (possible) rather than Chromium.
Any thoughts?
After some more digging, I realised there were two problems. The first is that my installed version of Edge was slightly behind. The second was that my Action delegate was synchronous. The call should read:
// Somebody forgot some 'async' keywords..!
Application.Current.Dispatcher.Invoke(new Action(async () =>
{
var x = await webView.CoreWebView2.CallDevToolsProtocolMethodAsync("Fetch.continueRequest", payload);
}));

C# WinForms Application exits unexpectedly with no exception, but only when the API piece is not on the same machine

I am developing an application which is to run as a WinForms thick-client, accessing both an API to be running in the cloud (Azure), and a local SQL Server DB for data.
To allow users to log in, the login screen is triggered as a Modal prompt when the application starts up with the following code in the HomeScreen form which is the 'main' page of the application:
using (Form loginScreen = new LoginForm())
{
loginScreen.ShowDialog(this);
}
Once the login screen has been passed, the user can see the home screen, if they cancel it, the application closes. Once they get to the home screen, another API call is run to retrieve data about the user from the API for display on the home screen.
All API calls execute the same code, which is below (this is very early code for a 'working prototype' and I am aware there are probably issues with it that require a refactor, at this point I'm really only interested in understanding what is causing my call to PostAsJsonAsync to fail:
public async Task<ApiResponse> sendApiRequest(RequestDetail reqDet)
{
//create a variable to track if the action was done or we need to retry after a timeout and login
bool actionDone = false;
//instantiate a variable for the ApiResponse so it can be used later outside of the scope of the actionDone loop
ApiResponse res = null;
while (actionDone == false)
{
//populate the main SessionKey of the packet from the GlobalData var (for initial dev, to be refactored out)
reqDet.SessionKey = GlobalData.SessionKey;
//populate the SessionKey in the array underneath the main object (for future use)
reqDet.strParameters["SessionKey"] = GlobalData.SessionKey;
//instantiate a new ApiRequest object to hold the main request body
ApiRequest req = new ApiRequest("ClientRequest", reqDet);
//Create HttpClient class for communication with the server
HttpClient client = new HttpClient();
//Set URL and Headers (URL will be in a config file in future
client.BaseAddress = new Uri("https://removed.the.url.for.se/api/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
//actually call the service, wait for the response, and read it out into the response object
HttpResponseMessage response = await client.PostAsJsonAsync((string)req.requestBody.ApiLocation, req);
res = await response.Content.ReadAsAsync<ApiResponse>();
//check if the response was successful or we need to show an error
if (res.responseType == "Success")
{
//set action done to TRUE so we exit the loop
actionDone = true;
}
else
{
//Use the MessageService to dispaly the error
Error err = res.responseError;
MessagesService ms = new MessagesService();
await ms.displayErrorPrompt(err);
//trigger a login screen and restart the service call if the user's session has expired
if (err.ErrorText.Equals("Session has expired, please log in again"))
{
using (Form login = new LoginForm())
{
login.ShowDialog();
} // Dispose form
}
else
{
// set ActionDone to True if it's not a login error so we don't endlessly call the service
actionDone = true;
}
}
}
//return the final result
return res;
}
When running the entire stack locally, this all works perfectly, I can login and traverse the rest of my application as normal. When running the client locally in VS and the API in Azure, the first call to the Login API succeeds (I can call it multiple times e.g. with a wrong password and it behaves as normal), however the second call to get the user's data to paint on the home screen fails.If I put a breakpoint on the PostAsJsonAsync line, I can see that the line executes once and continues as normal, but immediately after stepping over the line the second time for the user details call, the entire application exits without executing the subsequent code.
What is strange about this is that it exits with a 0x0 return code, does not throw an exception, or in any way behave abnormally other than shutting down after just that line.
I have tried manually calling the APIs on the Azure service in Postman and they all return exactly the same (correct) results I get when running it locally, so I know it is not the deployment to the App Service that is the issue.
Things I have tried to fix it after Googling, reading other SE posts and looking at comments on this question
I have tried enabling first-chance exceptions in Visual Studio for all CLR exceptions. Nothing is caught or thrown that I can see.
Here is a screenshot of my settings in case I've done something wrong
I have tried wrapping just that line in a try-catch block that catches all exceptions. It still immediately stops executing after the PostAsJsonAsync and never reaches the Catch block
Adding the following code to my Program.cs file to catch unhandled exceptions (is never run when I put a breakpoint on it and nothing is written to the console that I can see):
static void Main()
{
AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.UnhandledException += new UnhandledExceptionEventHandler(MyHandler);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new HomeScreen());
}
static void MyHandler(object sender, UnhandledExceptionEventArgs args)
{
Exception e = (Exception)args.ExceptionObject;
Console.WriteLine("MyHandler caught : " + e.Message);
}
Setting a DumpFolder that is writable by all users, and a DumpType of 2 in a key named after my executable at Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\ - I've tried both keys named MyApplication and MyApplication.exe and neither results in a file being produced when the app crashes.
The Windows Event Viewer after the 'crash' (nothing from my application)
Reviewing the request/response in Fiddler - the first 'login' request and response is shown correctly, but the second is not shown at all, so it looks like it's crashing before even sending the request
I'd be extremely grateful for any suggestions you can provide, even if it is only a workaround or 'patch' to resolve the issue. It's extremely strange to me both that it exits the program with no exception and without running the subsequent code, that it only does this when the API piece is running in Azure, not when running locally, and finally that it's only when it gets to the subsequent request after the login.
Update
I have tried commenting out the line that runs the RefreshScreen() function to call the web service again and the application still exits in the same way after the login, but just without hitting my breakpoint a second time. However again only when the application is running against the Azure API and not locally. If I break at the last line of the HomeScreen constructor and keep stepping, it goes back to my Main() method and ends the application. Is there something I'm doing wrong here?
I think the PostAsJsonAsync may have been a red herring so have taken it out of the title.
public HomeScreen()
{
InitializeComponent();
if(GlobalData.SessionKey == null)
{
using (Form loginScreen = new LoginForm())
{
loginScreen.ShowDialog(this);
}
// Dispose form
}
refreshScreen();
}
public async Task refreshScreen()
{
ApiService srv = new ApiService();
ApiResponse res = await srv.sendApiRequest(new Sessions_GetUserDetailsRequest());
if (res.responseType == "Success")
{
foreach (dynamic usrItem in JsonConvert.DeserializeObject(res.responseContent))
{
lblUserName.Text = usrItem.UserGivenName + " " + usrItem.UserSurname;
lblSiteName.Text = usrItem.TenantName;
}
}
}
So after doing some research to answer the helpful comments on this question, I stumbled across the answer.
I have an event in the application that is designed to close the entire application if the user exits the login page without logging in, since otherwise it would return to the 'home screen' form in an invalid state. It contained the following code, designed to close the application if the user didn't have a token (i.e. had cancelled the page):
Because my login process is asynchronous (code above) when I was stepping through the process in VS, I was getting to the "PostAsJsonAsync" step, and it was closing the application without showing me it was running the 'on close' event. However, unknown to me when testing locally, the code had a race condition where it would jump ahead to the 'close form' bit while still awaiting the web service call, and therefore execute the following code:
private void DoOnFormClosing(object sender, FormClosingEventArgs e)
{
if(GlobalData.SessionKey == null || GlobalData.SessionExpiry <= DateTime.Now)
{
Application.Exit();
}
}
The solution was to remove this event as part of the login process, after the login had been validated, meaning this code would never be called if the user had successfully logged in.

Detect when conversation end so I can delete stored conversation state data

I setup a bot that have a normal stack of dialog with each dialog storing some state data in Cosmos DB in Azure. When a dialog end, I use the OnEndDialogAsync to delete the data specific to that dialog.
My question is, how do I detect when the entire conversation end so that I can delete the whole thing? Or does conversation never end?
My current code that delete each dialog data on end:
protected override async Task OnEndDialogAsync(ITurnContext context, DialogInstance instance, DialogReason reason, CancellationToken cancellationToken)
{
DialogStateDictionary dictionary = await Dependencies.StateAccessor.GetAsync(context, () => null);
if (dictionary != null && dictionary.ContainsKey(DialogID) == true)
{
dictionary[DialogID] = null;
}
await Dependencies.StateAccessor.SetAsync(context, dictionary);
}
The code to delete the entire thing would be:
await Dependencies.StateAccessor.SetAsync(context, null);
The concept of a conversation "ending" will be channel-specific. In Web Chat you can have your client respond to the browser leaving the page by letting your bot know. In channels like Teams the conversation is effectively permanent, but you can always arbitrarily define any point in the conversation as the "end" by having your bot reset its state like you're doing. Perhaps you could have a confirm prompt that asks the user "Will that be all?" and if the user says "yes" then the bot could say "Goodbye" or something.
Not sure which channel you are using, recommend to look at endofconversation activity type.

FreshMVVM and resetting VM before popping Modal

FreshMVVM 3.0.0
Xamarin Forms 4.2
A number of our input pages are loaded modally and when the user presses Save we execute a Command like this
var newTemperature = new Temperature()
{
Date = DateTime.Now,
Value = this.TemperatureValue,
CaptureType = CaptureType.Manual,
IsModified = true,
};
await this.Services.DataService.SaveAsync(newTemperature);
// Save completed, now close modal.
await this.CoreMethods.PopPageModel(data, modal, animate);
If you look at the CoreMethods.PopPageModel call in GitHub you can see that it deals with two processes
Raising the PageWasPopped signal
Calling to the Navigation Service to pop the page off of the navigation stack
The FreshMVVM code that handles the page being popped is in FreshPageModel. Among other things the code unhooks from the Appearing and Disappearing events and sets the BindingContext to null. As you can see from the above order that means the BindingContext on the View is set to null before it is popped off the stack.
The problem with this is that for a short period of between 0.5 and 1.5 seconds the user sees a View that looks like the data has all been reset. This could be quite disconcerting if they have just pressed Save.
If I reverse the order of the logic in PopPageModel and pop from the navigation stack before calling RaisePageWasPopped this issue goes away.
Has nobody else seen this problem before?
Any users of FreshMVVM who want to point out the error of my suggested approach?
Just note that the service is being awaited, holding the UI until service completes,
have you tried removing await and popping the page immediately or displaying a loader while the service is busy?
this.InsertReports(metadata.Reports).ConfigureAwait(false);
Our solution to this issue was to implement our own PopPageModel method which essentially switches the order around so that PopPage is called on the Navigation Stack before a call to RaisePageWasPopped
This is what we call when we want to dismiss the Page
public Task DismissAsync(bool modal = true, bool animate = true)
{
return this.DispatcherService.RunOnUiThreadAsync(
async () =>
{
string navServiceName = this.CurrentNavigationServiceName;
if (this.IsModalFirstChild)
{
await this.CoreMethods.PopModalNavigationService(true);
}
else
{
IFreshNavigationService rootNavigation = FreshIOC.Container.Resolve<IFreshNavigationService>(navServiceName);
await rootNavigation.PopPage(modal, animate);
if (modal)
{
this.RaisePageWasPopped();
}
}
});
}

WF4 - resuming from instance store and InstanceNotReadyException

I am using WF4 and the WorkflowApplication to host a workflow. The workflow is very simple (at testing stage). It boils down to a series of logging activities with a delay activity and then more logging before finishing. I am using SqlWorkflowInstanceStore for saving persisting the workflows.
The workflow runs fine until it reaches the delay activity, here I can see that it gets saved into the persist database and then unloaded. I have looked at code examples and use the current code (at bottom) for resuming the workflow after the delay has expired. The code is run in a loop to ensure new (resumable) workflows are loaded. It all seems to work fine, the workflow gets resumed and I can se the expected logging output - however after the workflow completes it seems to try to resume it again. The call
WaitForEvents(_handle, TimeSpan.MaxValue)
contiunes again just like its going to resume a completed workflow. Next the
hasRunnableWorkflows
gets set to true. When the code reaches
wfApp.LoadRunnableInstance();
The exception InstanceNotReadyException (No runnable workflow instances were found in the InstanceStore for this WorkflowApplication to load.) is thrown.
I dont understand why this is happening and how to prevent the exception getting thrown. If I ignore the exception everything seems to work fine but I want to know why this is happening and if I'm doing someting wrong.
Code for resuming the workflow:
public Task ResumePendingFlows()
{
var tcs = new TaskCompletionSource<Guid>();
var store = _workflowInstanceStore?.Store;
if (store != null)
{
bool hasRunnableWorkflows = false;
//wait until a event has occurred
foreach (var currentEvent in store.WaitForEvents(_handle, TimeSpan.MaxValue))
{
if (currentEvent == HasRunnableWorkflowEvent.Value)
{
hasRunnableWorkflows = true;
break;
}
}
if (hasRunnableWorkflows)
{
//create WorkflowApplication with extensions and instance store
var wfApp = CreateWorkflowApplication();
wfApp.LoadRunnableInstance();
Logger?.Debug("Found runnable workflows");
//register completed, unloaded event passing the task completion source
RegisterWorkflowEvents(tcs, wfApp);
wfApp.Run();
}
else
{
Logger?.Debug("Did not find runnable workflows");
tcs.SetResult(Guid.Empty);
}
}
return tcs.Task;
}

Categories

Resources