Botframework v4: How to simplify this waterfall dialog? - c#

I have this code but but i think its over-complicated and can be simplified.
Also is there a way to go back to a spefici waterfall step if ever the user type "back" without restarting the whole dialog? I am new to this and it's hard to find a guide or online course on botframework v4 since it is new. Any help would be appreciated thanks!
public GetNameAndAgeDialog(string dialogId, IEnumerable<WaterfallStep> steps = null) : base(dialogId, steps)
{
var name = "";
var age = "";
AddStep(async (stepContext, cancellationToken) =>
{
return await stepContext.PromptAsync("textPrompt",
new PromptOptions
{
Prompt = stepContext.Context.Activity.CreateReply("What's your name?")
});
});
AddStep(async (stepContext, cancellationToken) =>
{
name = stepContext.Result.ToString();
return await stepContext.PromptAsync("numberPrompt",
new PromptOptions
{
Prompt = stepContext.Context.Activity.CreateReply($"Hi {name}, How old are you ?")
});
});
AddStep(async (stepContext, cancellationToken) =>
{
age= stepContext.Result.ToString();
return await stepContext.PromptAsync("confirmPrompt",
new PromptOptions
{
Prompt = stepContext.Context.Activity.CreateReply($"Got it you're {name}, age {age}. {Environment.NewLine}Is this correct?"),
Choices = new[] {new Choice {Value = "Yes"},
new Choice {Value = "No"},
}.ToList()
});
});
AddStep(async (stepContext, cancellationToken) =>
{
var result = (stepContext.Result as FoundChoice).Value;
if(result == "Yes" || result == "yes" || result == "Yeah" || result == "Correct" || result == "correct")
{
var state = await (stepContext.Context.TurnState["FPBotAccessors"] as FPBotAccessors).FPBotStateAccessor.GetAsync(stepContext.Context);
state.Name = name;
state.Age = int.Parse(age);
return await stepContext.BeginDialogAsync(MainDialog.Id, cancellationToken);
}
else
{
//restart the dialog
return await stepContext.ReplaceDialogAsync(GetNameAndAgeDialog.Id);
}
});
}
public static string Id => "GetNameAndAgeDialog";
public static GetNameAndAgeDialog Instance { get; } = new GetNameAndAgeDialog(Id);
}
And this is my accessors code:
public class FPBotAccessors
{
public FPBotAccessors(ConversationState conversationState)
{
ConversationState = conversationState ?? throw new ArgumentNullException(nameof(conversationState));
}
public static string FPBotAccessorName { get; } = $"{nameof(FPBotAccessors)}.FPBotState";
public IStatePropertyAccessor<FPBotState> FPBotStateAccessor { get; internal set; }
public static string DialogStateAccessorName { get; } = $"{nameof(FPBotAccessors)}.DialogState";
public IStatePropertyAccessor<DialogState> DialogStateAccessor { get; internal set; }
public ConversationState ConversationState { get; }
//
public static string ConversationFlowName { get; } = "ConversationFlow";
public IStatePropertyAccessor<ConversationFlow> ConversationFlowAccessor { get; set; }
}

So, there are a couple issues with your code and things you can do to make it better.
State within the dialog
First, let's start with the fact that you're closing over local variables in your constructor and accessing those from the closures that represent your steps. This "works" right now but is ultimately flawed. The initial flaw is different depending on the approach you've taking with instancing your GetNameAndAgeDialog dialog.
If you're using it as a singleton, that means all active conversations between users and your bot would be going through that one instance and you would have a concurrency issue where two users talking to the bot at the same time would be storing their values into the same memory (those variables) and stepping on each other's data.
It's also possible, depending on which samples you're following, that you're instead instantiating your GetNameAndAgeDialog on every turn. This would mean that those variables are then initialized to an empty string on every turn of the conversation and you'd lose track of the original values.
Ultimately, regardless of the instancing used, the approach ends up being flawed no matter what when it comes to scale out because at best your state would be pegged to a single server instance and if one turn of the conversation took place on ServerA and the next turn of the conversation took place on ServerM then ServerM would not have the values from the previous turn.
Alright, so clearly you need to store them with some kind of proper state management mechanism. You're clearly somewhat familiar with using BotState (be it conversation or user scope) already being that you're already using the state property accessors, but it's probably premature to store values you're collecting throughout a multi-turn prompt into someplace more permanent until you're at the end of the collection process. Luckily, dialogs themselves are stored into state, which you may have figured out when you set up a state property accessor for DialogState, and therefore offer a temporarily persistence mechanism that is tied to each dialog's lifetime on the dialog stack. Using this state is not obvious or documented well (yet), but WaterfallDialog actually goes a step further and exposes a first class Values collection via its WaterfallStepContext companion class which is fed into each step. This means that each step of your waterfall flow can add values into the Values collection and access values that previous steps may have put into there. There is a pretty good sample of this in the documentation page titled Create advanced conversation flow using branches and loops.
Not Making The Best Use of Prompts
You're using a TextPrompt for name which is perfect and you'll get a string from it and be all set. Though you might want to consider throwing a validator on there to make sure you get something that looks like a name instead of just allowing any old value.
You appear to be using a NumberPrompt<T> for the age (judging by the name "numberPrompt" at least), but then you .ToString() the step.Result and ultimately do an int.Parse in the final step. Using a NumberPrompt<int> would guarantee you get an int and you can/should just use that value as is rather than turning it back into a string and then parsing it yourself again later.
You've got a prompt named "confirmPrompt", but it does not appear to be an actual ConfirmPrompt because you're doing all the Choice work and positive value detection (e.g. checking for variations of "Yes") yourself. If you actually use a ConfirmPrompt it will do this all of this for you and its result will be a bool which you can then just easily test in your logic.
Minor stuff
Currently you're using stepContext.Context.Activity.CreateReply to create activities. This is fine, but long winded and unecessary. I would highly recommend just using the MessageFactory APIs.
I would always make sure to pass the CancellationToken through to all the XXXAsync APIs that take it... it's just good practice.
Your final step either restarts the GetNameAndAgeDialog if they don't confirm the details or starts the MainDialog if they do confirm the details. Restarting with ReplaceDialogAsync is awesome, that's the right way to do it! I just wanted to point out that by using BeginDialogAsync to start the MainDialog means that you're effectively leaving the GetNameAndAgeDialog at the bottom of the stack for the remainder of the conversation's lifetime. It's not a huge deal, but considering you'll likely never pop the stack back to there I would instead suggest using ReplaceDialogAsync for starting up the MainDialog as well.
Refactored Code
Here is the code rewritten using all of the advice above:
public GetNameAndAgeDialog(string dialogId, IEnumerable<WaterfallStep> steps = null) : base(dialogId, steps)
{
AddStep(async (stepContext, cancellationToken) =>
{
return await stepContext.PromptAsync("textPrompt",
new PromptOptions
{
Prompt = MessageFactory.Text("What's your name?"),
},
cancellationToken: cancellationToken);
});
AddStep(async (stepContext, cancellationToken) =>
{
var name = (string)stepContext.Result;
stepContext.Values["name"] = name;
return await stepContext.PromptAsync("numberPrompt",
new PromptOptions
{
Prompt = MessageFactory.Text($"Hi {name}, How old are you ?"),
},
cancellationToken: cancellationToken);
});
AddStep(async (stepContext, cancellationToken) =>
{
var age = (int)stepContext.Result;
stepContext.Values["age"] = age;
return await stepContext.PromptAsync("confirmPrompt",
new PromptOptions
{
Prompt = MessageFactory.Text($"Got it you're {name}, age {age}.{Environment.NewLine}Is this correct?"),
},
cancellationToken: cancellationToken);
});
AddStep(async (stepContext, cancellationToken) =>
{
var result = (bool)stepContext.Result;
if(result)
{
var state = await (stepContext.Context.TurnState["FPBotAccessors"] as FPBotAccessors).FPBotStateAccessor.GetAsync(stepContext.Context);
state.Name = stepContext.Values["name"] as string;
state.Age = stepContext.Values["age"] as int;
return await stepContext.ReplaceDialogAsync(MainDialog.Id, cancellationToken: cancellationToken);
}
else
{
//restart the dialog
return await stepContext.ReplaceDialogAsync(GetNameAndAgeDialog.Id, cancellationToken: cancellationToken);
}
});
}
Also is there a way to go back to a spefici waterfall step if ever the user type "back" without restarting the whole dialog?
No, there is not a way to do this today. The topic has come up in internal discussions with the team, but nothing has been decided yet. If you think this is a feature that would be useful, please submit an issue over on GitHub and we can see if it gains enough momentum to get the feature added.

Related

How to create and run Dialog from Middleware with Microsoft BotFramework

Question
How can I conditionally create and run a Dialog from
middleware without breaking the bot?
Context
I'm using the dotnetcore/13.core-bot sample.
I have a setup to run a custom spellchecking Middleware. I am trying to create a dialog from Middleware so that after the user types some misspelled input and ONLY when two or more spellcheck suggestions are found, the user gets the possible sentence interpretations and chooses from a HeroCard or similar.
From my middleware SpellcheckMiddleware.cs, myDialog.RunAsync(...) runs a dialog, however, after the middleware exits onTurnAsync(), I get a fatal error: "An item with the same key has already been added". That error occurs when the bot tries to continue MainDialog from MainDialog.cs which is the dialog that was setup in Startup.cs.
Bot emulator visual
Error capture within Visual Studio
("An item with the same key has already been added")
---
My code
The only thing I have changed from the sample code is creating these two files, one defines the dialog that resolves a spellcheck with multiple suggestions, and one that is middleware that should run the spellcheck dialog.
SpellcheckMiddleware.cs:
public class SpellCheckMiddleware : IMiddleware
{
private readonly ConversationState conversationState;
public SpellCheckMiddleware(
IConfiguration configuration,
ConversationState conversationState)
{
this.conversationState = conversationState;
}
public async Task OnTurnAsync(
ITurnContext turnContext,
NextDelegate next,
CancellationToken cancellationToken = new CancellationToken())
{
# Fake suggestions
List<List<String>> suggestions = new List<List<String>>{
new List<String>{'Info', 'Olympics'},
new List<String>{'Info', 'Olympia'},
};
SpellcheckSuggestionsDialog myDialog = new SpellcheckSuggestionsDialog(suggestions);
await myDialog.RunAsync(
turnContext,
conversationState.CreateProperty<DialogState>(nameof(DialogState)),
cancellationToken);
await next(cancellationToken);
}
}
SpellcheckSuggestionsDialog.cs:
class SpellcheckSuggestionsDialog : ComponentDialog
{
// Create a prompt that uses the default choice recognizer which allows exact matching, or number matching.
public ChoicePrompt SpellcheckPrompt { get; set; }
public WaterfallDialog WaterfallDialog { get; set; }
List<string> Choices { get; set; }
internal SpellcheckSuggestionsDialog(
IEnumerable<IEnumerable<string>> correctedSentenceParts)
{
SpellcheckPrompt = new ChoicePrompt(
nameof(ChoicePrompt),
validator: null,
defaultLocale: null);
WaterfallDialog = new WaterfallDialog(
nameof(WaterfallDialog),
new WaterfallStep[]{
SpellingSuggestionsCartesianChoiceAsync,
EndSpellingDialogAsync
});
AddDialog(SpellcheckPrompt);
AddDialog(WaterfallDialog);
InitialDialogId = nameof(WaterfallDialog);
// Get all possible combinations of the elements in the list of list. Works as expected.
var possibleUtterances = correctedSentenceParts.CartesianProduct();
// Generate a choices array with the flattened list
Choices = new();
foreach (var item in possibleUtterances) {
System.Console.WriteLine(item.JoinStrings(" "));
Choices.Add(item.JoinStrings(" "));
}
}
private async Task<DialogTurnResult> SpellingSuggestionsCartesianChoiceAsync(
WaterfallStepContext stepContext,
CancellationToken cancellationToken)
{
return await stepContext.PromptAsync(
SpellcheckPrompt.Id,
new PromptOptions()
{
Choices = ChoiceFactory.ToChoices(Choices),
RetryPrompt = MessageFactory.Text("Did you mean...?"),
Prompt = MessageFactory.Text("Did you mean...?"),
Style = ListStyle.HeroCard
});
}
private async Task<DialogTurnResult> EndSpellingDialogAsync(
WaterfallStepContext stepContext,
CancellationToken cancellationToken)
{
// Overriding text sent using the choosen correction.
stepContext.Context.TurnState.Add("CorrectionChoice", stepContext.Result);
var choosen_correction = stepContext.Context.TurnState.Get<string>("CorrectionChoice");
stepContext.Context.Activity.Text = choosen_correction;
return await stepContext.EndDialogAsync(null, cancellationToken);
}
}
As of 2022, there is no sample from Microsoft showing dialogs being spawned from middleware, so that is likely not the intended way to use the framework. It may be possible, but then you're in a sense going against the framework, which is never advisable.
In order to have a dialog that provides spellcheck suggestions when the user types with a typo, I suggest you make that dialog part of logic specified in the WaterFall Dialog steps in MainDialog.cs.
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
{
IntroStepAsync,
SpellcheckStep, //new step to force user to choose a spellcheck suggestions
ActStepAsync,
FinalStepAsync,
}));
This comes with the drawback that if you need to spellcheck multiple user inputs in the conversation, then you will need to add multiple spellcheck steps, each customized to handle the input expected at the matching point in the conversation steps.

Block Controller Method while already running

I have a controller which returns a large json object. If this object does not exist, it will generate and return it afterwards. The generation takes about 5 seconds, and if the client sent the request multiple times, the object gets generated with x-times the children. So my question is: Is there a way to block the second request, until the first one finished, independent who sent the request?
Normally I would do it with a Singleton, but because I am having scoped services, singleton does not work here
Warning: this is very oppinionated and maybe not suitable for Stack Overflow, but here it is anyway
Although I'll provide no code... when things take a while to generate, you don't usually spend that time directly in controller code, but do something like "start a background task to generate the result, and provide a "task id", which can be queried on another different call).
So, my preferred course of action for this would be having two different controller actions:
Generate, which creates the background job, assigns it some id, and returns the id
GetResult, to which you pass the task id, and returns either different error codes for "job id doesn't exist", "job id isn't finished", or a 200 with the result.
This way, your clients will need to call both, however, in Generate, you can check if the job is already being created and return an existing job id.
This of course moves the need to "retry and check" to your client: in exchange, you don't leave the connection to the server opened during those 5 seconds (which could potentially be multiplied by a number of clients) and return fast.
Otherwise, if you don't care about having your clients wait for a response during those 5 seconds, you could do a simple:
if(resultDoesntExist) {
resultDoesntExist = false; // You can use locks for the boolean setters or Interlocked instead of just setting a member
resultIsBeingGenerated = true;
generateResult(); // <-- this is what takes 5 seconds
resultIsBeingGenerated = false;
}
while(resultIsBeingGenerated) { await Task.Delay(10); } // <-- other clients will wait here
var result = getResult(); // <-- this should be fast once the result is already created
return result;
note: those booleans and the actual loop could be on the controller, or on the service, or wherever you see fit: just be wary of making them thread-safe in however method you see appropriate
So you basically make other clients wait till the first one generates the result, with "almost" no CPU load on the server... however with a connection open and a thread from the threadpool used, so I just DO NOT recommend this :-)
PS: #Leaky solution above is also good, but it also shifts the responsability to retry to the client, and if you are going to do that, I'd probably go directly with a "background job id", instead of having the first (the one that generates the result) one take 5 seconds. IMO, if it can be avoided, no API action should ever take 5 seconds to return :-)
Do you have an example for Interlocked.CompareExchange?
Sure. I'm definitely not the most knowledgeable person when it comes to multi-threading stuff, but this is quite simple (as you might know, Interlocked has no support for bool, so it's customary to represent it with an integral type):
public class QueryStatus
{
private static int _flag;
// Returns false if the query has already started.
public bool TrySetStarted()
=> Interlocked.CompareExchange(ref _flag, 1, 0) == 0;
public void SetFinished()
=> Interlocked.Exchange(ref _flag, 0);
}
I think it's the safest if you use it like this, with a 'Try' method, which tries to set the value and tells you if it was already set, in an atomic way.
Besides simply adding this (I mean just the field and the methods) to your existing component, you can also use it as a separate component, injected from the IOC container as scoped. Or even injected as a singleton, and then you don't have to use a static field.
Storing state like this should be good for as long as the application is running, but if the hosted application is recycled due to inactivity, it's obviously lost. Though, that won't happen while a request is still processing, and definitely won't happen in 5 seconds.
(And if you wanted to synchronize between app service instances, you could 'quickly' save a flag to the database, in a transaction with proper isolation level set. Or use e.g. Azure Redis Cache.)
Example solution
As Kit noted, rightly so, I didn't provide a full solution above.
So, a crude implementation could go like this:
public class SomeQueryService : ISomeQueryService
{
private static int _hasStartedFlag;
private static bool TrySetStarted()
=> Interlocked.CompareExchange(ref _hasStartedFlag, 1, 0) == 0;
private static void SetFinished()
=> Interlocked.Exchange(ref _hasStartedFlag, 0);
public async Task<(bool couldExecute, object result)> TryExecute()
{
if (!TrySetStarted())
return (couldExecute: false, result: null);
// Safely execute long query.
SetFinished();
return (couldExecute: true, result: result);
}
}
// In the controller, obviously
[HttpGet()]
public async Task<IActionResult> DoLongQuery([FromServices] ISomeQueryService someQueryService)
{
var (couldExecute, result) = await someQueryService.TryExecute();
if (!couldExecute)
{
return new ObjectResult(new ProblemDetails
{
Status = StatusCodes.Status503ServiceUnavailable,
Title = "Another request has already started. Try again later.",
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.4"
})
{ StatusCode = StatusCodes.Status503ServiceUnavailable };
}
return Ok(result);
}
Of course possibly you'd want to extract the 'blocking' logic from the controller action into somewhere else, for example an action filter. In that case the flag should also go into a separate component that could be shared between the query service and the filter.
General use action filter
I felt bad about my inelegant solution above, and I realized that this problem can be generalized into basically a connection number limiter on an endpoint.
I wrote this small action filter that can be applied to any endpoint (multiple endpoints), and it accepts the number of allowed connections:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ConcurrencyLimiterAttribute : ActionFilterAttribute
{
private readonly int _allowedConnections;
private static readonly ConcurrentDictionary<string, int> _connections = new ConcurrentDictionary<string, int>();
public ConcurrencyLimiterAttribute(int allowedConnections = 1)
=> _allowedConnections = allowedConnections;
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var key = context.HttpContext.Request.Path;
if (_connections.AddOrUpdate(key, 1, (k, v) => ++v) > _allowedConnections)
{
Close(withError: true);
return;
}
try
{
await next();
}
finally
{
Close();
}
void Close(bool withError = false)
{
if (withError)
{
context.Result = new ObjectResult(new ProblemDetails
{
Status = StatusCodes.Status503ServiceUnavailable,
Title = $"Maximum {_allowedConnections} simultaneous connections are allowed. Try again later.",
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.4"
})
{ StatusCode = StatusCodes.Status503ServiceUnavailable };
}
_connections.AddOrUpdate(key, 0, (k, v) => --v);
}
}
}

My Discord Bot won't accept Users as parameters for commands

I'm currently working on a Discord bot to learn how to code one. I thought I had it down, but when I try to use the following command, it does nothing:
[Command("ping")]
public async Task Ping(IUser user)
{
await Context.Channel.SendMessageAsync(user.ToString());
}
It's part of a public class, and if I use any other parameter type (e.g. IChannel, bool, int) it works. It's just this one parameter type. It also doesn't log any errors or exceptions. Any ideas?
[Command("ping")]
public async Task Ping(IUser user)
{
await Context.Channel.SendMessageAsync(user.ToString());
}
Your code id perfect. But think about this, the user is of the type IUser and your conversion to sting makes it vague. Instead try this:
[Command("ping")]
public async Task Ping(SocketGuildUser user)
{
await Context.Channel.SendMessageAsync(user.Username);
}
If you want to ping the user try user.Mention.
Also when I started learning I made a bot as well. Here is the source code. Its very very very basic. It definitely will help.
You could try using this workaround for your bot:
public async Task SampleCommand(string user="", [Remainder]string message="")
{
IUser subject = null;
if (user != "")
{
var guilds = (await Context.Client.GetGuildsAsync(Discord.CacheMode.AllowDownload));
var users = new List<IUser>();
foreach (var g in guilds)
users.AddRange(await g.GetUsersAsync(CacheMode.AllowDownload));
users = users.GroupBy(o => o.Id).Select(o => o.First()).ToList();
var search = users.Where(o => o.Username.ToLower().Contains(user.ToLower()) || Context.Message.MentionedUserIds.Contains(o.Id) || o.ToString().ToLower().Contains(user.ToLower())).ToArray();
if (search.Length == 0)
{
await ReplyAsync("***Error!*** *Couldn't find that user.*");
return;
}
else if (search.Length > 1)
{
await ReplyAsync("***Error!*** *Found more than one matching users.*");
return;
}
subject = search.First();
}
// ...
// execute command
Or you could wrap that in a method for easier access and reusability.
Basically, what it does is it looks for available users that match the given string (in nickname, username or mentions. You could also make it check for IDs if you so desire).
Edit: In my case I'm allowing people to mention anyone who shares the server with the bot, but in your case it might be more benefitial to just use the Context.Guild instead and cancel the command in case of DMs.
I ended up taking Reynevan's advice, and wrote a method for converting a mention into an IUser. Just call CustomUserTypereader.GetUser(mention_parameter, Context.Guild);
using System.Threading.Tasks;
using Discord;
public class CustomUserTypereader
{
public static async Task<IUser> GetUserFromString(string s, IGuild server)
{
if (s.IndexOf('#') == -1 || s.Replace("<", "").Replace(">", "").Length != s.Length - 2)
throw new System.Exception("Not a valid user mention.");
string idStr = s.Replace("<", "").Replace(">", "").Replace("#", "");
try
{
ulong id = ulong.Parse(idStr);
return await server.GetUserAsync(id);
}
catch
{
throw new System.Exception("Could not parse User ID. Are you sure the user is still on the server?");
}
}
}

Branching dialogs/forms based on response in MS Bot Framework

We're experimenting with the MS Bot Framework and haven't quite worked out how to do this scenario:
We have a LUIS Dialog (type <object>), which is working correctly and is trained properly. To use the common sandwich example, the basics of what LUIS intent is looking for is the user asking for the status of an order. If the order number was provided in the question ("What is the status of order 1234?"), then the LUIS dialog does the lookup and reports the status directly (which is all currently working).
However, if the user just triggers the intent without providing the order number ("I'd like to look up the status of an order."), I'd like to launch another dialog/form to ask the user if they'd like to look up the order by address or order number, and then do the appropriate DB lookup based on how they answer.
I'm just not sure how to configure the Form/Dialog (or even which is best in this case) to do a different lookup based on if they choose address or number lookup.
Here's the intent so far:
private readonly BuildFormDelegate<OrderStatusDialog> OrderStatusDelegate;
[LuisIntent(nameof(LuisIntents.OrderStatus))]
public async Task OrderStatus(IDialogContext context, LuisResult result)
{
// Order number(s) were provided
if (result.Entities.Any(Entity => Entity.Type == nameof(LuisEntityTypes.OrderNumber)))
{
// Loop in case they asked about multiple orders
foreach (var entity in result.Entities.Where(Entity => Entity.Type == nameof(LuisEntityTypes.OrderNumber)))
{
var orderNum = entity.Entity;
// Call webservice to check status
var request = new RestRequest(Properties.Settings.Default.GetOrderByNum, Method.GET);
request.AddUrlSegment("num", orderNum);
var response = await RestHelper.SendRestRequestAsync(request);
var parsedResponse = JObject.Parse(response);
if ((bool)parsedResponse["errored"])
{
await context.PostAsync((string)parsedResponse["errMsg"]);
continue;
}
// Grab status from returned JSON
var status = parsedResponse["orderStatus"].ToString();
await context.PostAsync($"The status of order {orderNum} is {status}");
}
context.Wait(MessageReceived);
}
// Order number was not provided
else
{
var orderStatusForm = new FormDialog<OrderStatusDialog>(new OrderStatusDialog(), OrderStatusDelegate,
FormOptions.PromptInStart);
context.Call<OrderStatusDialog>(orderStatusForm, CallBack);
}
}
private async Task CallBack(IDialogContext context, IAwaitable<object> result)
{
context.Wait(MessageReceived);
}
And the form:
public enum OrderStatusLookupOptions
{
Address,
OrderNumber
}
[Serializable]
public class OrderStatusDialog
{
public OrderStatusLookupOptions? LookupOption;
public static IForm<OrderStatusDialog> BuildForm()
{
return new FormBuilder<OrderStatusDialog>()
.Message("In order to look up the status of a order, we will first need either the order number or your delivery address.")
.Build();
}
}
The FormFlow route is a valid option. What is missing in your form flow is asking for the address/order number after the lookup option is selected.
What you can do in that case is adding two more fields to the OrderStatusDialog class: OrderNumber and DeliveryAddress.
Then you need to use the selected OrderStatusLookupOptions to activate/deactivate the next field.
The code, from the top of my head, would be something like:
[Serializable]
public class OrderStatusDialog
{
public OrderStatusLookupOptions? LookupOption;
public int OrderNumber;
public string DeliveryAddress
public static IForm<OrderStatusDialog> BuildForm()
{
return new FormBuilder<OrderStatusDialog>()
.Message("In order to look up the status of a order, we will first need either the order number or your delivery address.")
.Field(nameof(OrderStatusDialog.LookupOption))
.Field(new FieldReflector<OrderStatusDialog>(nameof(OrderStatusDialog.OrderNumber))
.SetActive(state => state.LookupOption == OrderStatusLookupOptions.OrderNumber))
.Field(new FieldReflector<OrderStatusDialog>(nameof(OrderStatusDialog.DeliveryAddress))
.SetActive(state => state.LookupOption == OrderStatusLookupOptions.Address))
.Build();
}
}
Then on your Callback method you will receive the form filled and you can do the DB lookup.
Alternatively, you can just use PromptDialogs and guide the user through the same experience. Take a look to the MultiDialogs sample to see the different alternatives.
I added a working sample on this here.

What Should I be using here? Threading? Async?

I am not sure what to use in this scenario.
I have an asp.net web api method that basically does this
Finds points of interests from foursquare near user.
Uses the foursquare locations to do queries in my database to find unique data about point of interest near user.
However since I need to store some of the foursquare information to link to my unique data to that location I decided to store all the information in my database and have my database act as my caching system.
This means anything new point of interest that comes in I have to insert into my database, check if it exists and if so then skip it or if it exists check the last refresh date(foursquare policy states all data must be refreshed after 30 day) and if it out past the refresh date I have to update the data.
I want to slow the user down and have to wait for the above to happen. I want my code to do step 1 and then do what I just mentioned while at the same time doing step 2.
Once step 2 finishes I want to return the data and let the user get on their way. If my caching system is not finished then it should keep going but not bog down the user.
I won't use any of these new results in step 2 as if I am inserting it then I won't have any data on that location at this time.
Not sure if I need to make a thread or use the async/await to achieve this.
Edit
Here is what I am trying to do
public HttpResponseMessage Get()
{
// this will do a foursquare lookup to find all stores near the user
// I want to insert them into my database and link it to my unquie data.
// stores pulled from foursquare will
// a) Be new and not in my database
// b) exist in my database but have been refreshed lately
// c) have not been refreshed in timeframe of foursquare policy
// THIS SHOULD WORK IN THE BACKGROUND
storeService.PointsOfInterestNearUser(80, -130); //As you can see it is
//void. Not sure where to put the async/await stuff
// find this product. Should be happening at the same time as above line.
var product = productService.FindProduct("Noodles");
//This will get returned to the user.
// the new stores taht are being added in StoreNearUser
//won't effect this search as I will have not data on this new store
// if existing store is being refreshed it is possible old
//address might be picked up...
//I can live with that as I doubt the address will change much.
// this should happen after product
var allStores = storeService.FindStoresThatHaveItem(product);
// this should be returned as soon as above line is finished.
//If StoreNearUser is not done, it should keep going but not hold up user.
return allStores;
}
public void StoresNearUser(double latitude, double longitude)
{
// get all categories I can about in foursquare.
//First time from db otherwise cached.
List<StoreCategory> storeCategories = GetStoreCategories();
// do a request and get everything in near the user
//(provided it is also in a category I care about)
var request = CreateFoursquareStoreRequest
(latitude, longitude, storeCategories);
// do the actual call.
var response = client.Execute<VenueSearch>(request);
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
// start going through the results, add or update or skip of entry will happen
AddUpdateStores(storeCategories, response);
}
else
{
ErrorSignal.FromCurrentContext().Raise(response.ErrorException);
}
}
Edit 2
public async Task StoresNearUser(double latitude, double longitude)
{
// get all categories I can about in foursquare. First time from db otherwise cached.
List<StoreCategory> storeCategories = GetStoreCategories();
// do a request and get everything in near the user(provided it is also in a category I care about)
var request = CreateFoursquareStoreRequest(latitude, longitude, storeCategories);
await client.ExecuteAsync<VenueSearch>
( request
, response =>
{
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
AddUpdateStores(storeCategories, response);
}
else
{
ErrorSignal.FromCurrentContext()
.Raise(response.ErrorException);
}
}
);
}
gives me this error
Cannot await 'RestSharp.RestRequestAsyncHandle'
I also don't get the difference between Task and void. From what I read if you just use Task it means you are sending nothing back of meaning, then why not just use void?
Edit 2
I found this post to show me how to make the wrapper for Restsharp. It is not 100% what I want but that is a separate issue.
public async Task StoresNearUser(double latitude, double longitude)
{
List<StoreCategory> storeCategories = GetStoreCategories();
var request = CreateFoursquareStoreRequest
(latitude, longitude, maxRadius, returnLimit, storeCategories);
var response = await client.GetResponseAsync(request);
if (response.StatusCode == HttpStatusCode.OK)
{
// had to use json.net right now as the wrapper does not expose restsharps deserilizer
var venue = JsonConvert
.DeserializeObject<VenueSearch>(response.Content);
AddUpdateStores(storeCategories, venue);
}
else
{
ErrorSignal.FromCurrentContext()
.Raise(response.ErrorException);
}
}
public async Task<HttpResponseMessage>Get()
{
await storeService.PointsOfInterestNearUser(80, -130);
var product = productService.FindProduct("Noodles");
var allStores = storeService.FindStoresThatHaveItem(product);
return allStores;
}
When I watch from the debugger it looks like it is still all going in order. I think product and allStores need to be since I need the product before I can find the stores but PointsOfInterestNearUser should be going at the same time as FindProduct.
Edit 3
Here is my FindProduct Method. Not sure what to make async to me it looks like everything needs to wait.
public ResponseResult<Product> FindProduct(string barcode)
{
ResponseResult<Product> responseResult = new ResponseResult<Product>();
Product product = null;
try
{
var findBarCode = context.Barcodes.Where(x => x.Code == barcode).Select(x => x.Product).FirstOrDefault();
responseResult.Response = product;
if (product == null)
{
responseResult.Status.Code = HttpStatusCode.NotFound;
}
else
{
responseResult.Status.Code = HttpStatusCode.OK;
}
}
catch (SqlException ex)
{
ErrorSignal.FromCurrentContext().Raise(ex);
responseResult.Status.Code = HttpStatusCode.InternalServerError;
responseResult.Status.Message = GenericErrors.InternalError;
}
return responseResult;
}
Edit 4
Still not sure how to do the Task.WhenAll()
public async Task<HttpResponseMessage>Get()
{
Task[] tasks = new Task[2];
tasks[0] = storeService.PointsOfInterestNearUser(80, -130);
tasks[1] = productService.FindProduct("Noodles");
await Task.WhenAll(tasks);
// not sure how to get product back out. I looked in the debugger and saw a "Result" that has it but when I do tasks[1].Result inetllisene cannot find .Result
var allStores = storeService.FindStoresThatHaveItem(product);
return allStores;
}
I would recommend using async/await for this. Updating a cache is one of the rare situations where returning early from an ASP.NET request is acceptable. You can see my blog post on the subject for some helpful code.
So, something like this (simplified to just look up one "interesting place" per location):
public async Task<PlaceWithData> FindPlaceAsync(Location myLocation)
{
Place place = await GetPlaceFromFoursquareAsync(myLocation);
PlaceWithData ret = await GetExtraDataFromDatabaseAsync(place);
if (ret.NeedsRefresh)
BackgroundTaskManager.Run(() => UpdateDatabaseAsync(place, ret));
return ret;
}
You may also want to consider extending the ASP.NET caching system rather than doing a "roll your own" cache.

Categories

Resources