user6269864
user6269864

Reputation:

Cannot serialize IDialogContext; need to get user information from inside a Dialog object in MS Bot Framework

I am using MS bot framework and trying to make a bot send a scheduled message. To do that, I'm using the Hangfire framework.

I'm trying to use this code for scheduling, where context is the IDialogContext object passed from my Dialog, and the SendScheduledMessageToUser method just called context.PostAsync() to send a message to the user:

            BackgroundJob.Schedule(() => CurrencyDialog.SendScheduledMessageToUser(context), new DateTimeOffset(when));

The problem is that context turns out to be null. I am seeing a serialization exception in the console where this is called. I am assuming you can't serialize the context object because of circular references inside of it.

So in order to send a scheduled message to the user, my best idea is to obtain user information (ID, conversation ID, channel, service URL etc.) and then to pass this simple data to the scheduled method, so that it can send a message to the user. However, incredibly there seems to be no way to get user data from inside an implementation of IDialog. IDialogContext has that data but it is marked as private or internal so I cannot get it. And I cannot pass the Activity object to the Dialog when the dialog is first started, because there is no constructor.

Any ideas on getting user info from an implementation of IDialog or otherwise getting some data that can be serialized in order to send a scheduled message to the user?

Upvotes: 1

Views: 1353

Answers (3)

Pravin Chandankhede
Pravin Chandankhede

Reputation: 116

There is another way of getting the user data. In your Controllers Post method do this -

case ActivityTypes.Message:

    StateClient stateClient = activity.GetStateClient();        
    BotData userData = await stateClient.BotState.GetUserDataAsync(activity.ChannelId, activity.From.Id);
    userData.SetProperty<string>("name", activity.From.Name);
    //other properties you want to set.
    await stateClient.BotState.SetUserDataAsync(activity.ChannelId, activity.From.Id, userData);

and then in your intent method, access them like this -

[LuisIntent("Greetings")]
public async Task Greetings(IDialogContext context, LuisResult result)
{
var username = context.UserData.Get<string>("name");
await context.PostAsync($"hi {username}");
    context.Wait(MessageReceived);
}

This is much elegant then creating your activities manually.

Upvotes: 3

user6269864
user6269864

Reputation:

OK, so the solution I've ended up using is very very lame, but works.

Since Hangfire throws an exception when serializing either the IDialogContext or Activity, I save a few string parameters in the Dialog object and in the parameters of the method called by Hangfire, and then when necessary, recreate the activity.

In my dialog class:

public class CurrencyDialog : IDialog<object>
{
    public string serviceUrl;
    public string from;
    public string recipient;
    public string conversation;
    public string channelId;
    public string activityId;

When the dialog starts, save the strings in the object:

    public async Task StartAsync(IDialogContext context)
    {
        //await context.PostAsync("What rate would you like to check today?");
        context.Wait(BeginCurrencyDialog);
    }

    public async Task BeginCurrencyDialog(IDialogContext context, IAwaitable<IMessageActivity> argument)
    {
        //this needs to be saved because neither Activity nor IDialogContext are serializable, but Hangfire needs it
        IMessageActivity activity = await argument;
        serviceUrl = activity.ServiceUrl;
        from = activity.From.Id;
        recipient = activity.Recipient.Id;
        conversation = activity.Conversation.Id;
        channelId = activity.ChannelId;
        activityId = activity.Id;

Pass these to your method used in Hangfire when you want to schedule a job:

Finally, in the method called by Hangfire when the time comes, use this method to recreate the activity, which you can then use to send an answer:

    //creates an activity that can be used to send rich messages in response; cannot use original activity because it is not serializable
    public Activity CreateActivity()
    {
        if (activityId == null) { return null; }

        ChannelAccount fromAccount = new ChannelAccount(id: from);
        ChannelAccount recipientAccount = new ChannelAccount(id: recipient);
        ConversationAccount conversationAccount = new ConversationAccount(id: conversation);

        return new Activity()
        {
            Type = ActivityTypes.Message,
            ServiceUrl = serviceUrl,
            From = fromAccount,
            Recipient = recipientAccount,
            Conversation = conversationAccount,
            ChannelId = channelId,
            Id = activityId
        };
    }

After that, you can use activity.CreateReply() to create a reply and context.PostAsync to send it to the user.

Upvotes: 0

Kien Chu
Kien Chu

Reputation: 4895

You can pass in the original activity to background job

My example is using Quartz.NET, but it should be similar to Hangfire

public class ReminderJob : IJob
{
    public void Execute(IJobExecutionContext context)
    {
        var dataMap = context.Trigger.JobDataMap;
        var originalActivity = dataMap["originalActivity"] as Activity;
        var message = dataMap["reply"] as string;

        if (originalActivity == null) return;

        var connector = new ConnectorClient(new Uri(originalActivity.ServiceUrl));
        var reply = originalActivity.CreateReply(message);
        connector.Conversations.ReplyToActivity(reply);
    }
}

public class JobScheduler
{
    public static void StartReminderJob(ITrigger trigger)
    {
        var scheduler = StdSchedulerFactory.GetDefaultScheduler();
        scheduler.Start();

        var job = JobBuilder.Create<ReminderJob>().Build();
        scheduler.ScheduleJob(job, trigger);
    }
}

This is how I use it

// how to use
var jobDetail = TriggerBuilder
            .Create()
            .StartAt(new DateTimeOffset(result.Start.Value.ToUniversalTime()))
            .Build();

jobDetail.JobDataMap["originalActivity"] = originalMessage;
jobDetail.JobDataMap["reply"] = $"test";

JobScheduler.StartReminderJob(jobDetail);

Upvotes: 1

Related Questions