Search code examples
c#botframework

How to proactively reset a user's IDialogStack on a channel?


I have a situation where I (inadvisedly) upgraded my bot's RootDialog conversation data - specifically, a new member variable - while we have current users.

This means that some users' serialized conversation state does not match the current version of the bot code.

This is fine if the user contacts the bot - they'll get a 'bot is having an issue' message while an exception is thrown on the bot, resetting the stack. Everything should be fine after that.

However, we want to send messages proactively to the bot, and currently all these are failing due to the serialization mismatch - a BadRequest exception.

How can I resolve IBotData and IDialogStack for a given user/channel outside of an IDialogContext? I'm using SQL bot data store (SqlBotDataEntities) on an Azure database.

I'd like to do it say, in a controller method on an admin website, and call stack.Reset(). Is this possible?


Solution

  • Attempting to .LoadAsync the IBotData will cause the stack to be reset automatically. There is enough information in the SqlBotDataEntities table to create an Activity, and use that for the scope. (Since SqlBotDataContext is internal to the Azure Extensions library, you will need to duplicate it in your code.)

    Something like:

    public async Task<HttpResponseMessage> Post()
    {
        using (var context = new SqlBotDataContext(ConfigurationManager.ConnectionStrings["BotDataContextConnectionString"].ConnectionString))
        {
            try
            {
                foreach(var botData in context.BotData)
                {
                    if(botData.BotStoreType == BotStoreType.BotPrivateConversationData)
                    {
                        var message = Activity.CreateMessageActivity();
                        message.ChannelId = botData.ChannelId;
                        message.Timestamp = botData.Timestamp;
                        message.From = new ChannelAccount(id: botData.UserId);
                        message.Conversation = new ConversationAccount(id: botData.ConversationId);
                        message.Recipient = new ChannelAccount(id: botData.BotId);
                        message.ServiceUrl = botData.ServiceUrl;
    
                        using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message))
                        {
                            var scopedData = scope.Resolve<IBotData>();
                            await scopedData.LoadAsync(default(CancellationToken));
                            //resetting the stack is not necessary, since .LoadAsync will fail silently, and reset it
                            //var stack = scope.Resolve<IDialogStack>();
                            //stack.Reset();
                            await scopedData.FlushAsync(default(CancellationToken));
                        }
                    }
                }
            }
            catch (System.Data.SqlClient.SqlException err)
            {
                throw new HttpException((int)HttpStatusCode.InternalServerError, err.Message);
            }
        }
    
        var response = Request.CreateResponse(HttpStatusCode.OK);
        return response;
    } 
    

    With the following copied from https://github.com/Microsoft/BotBuilder-Azure/blob/master/CSharp/Library/Microsoft.Bot.Builder.Azure/SqlBotDataStore.cs#L163

    internal class SqlBotDataContext : System.Data.Entity.DbContext
    {
        public SqlBotDataContext(string connectionString)
            : base(connectionString)
        {
            System.Data.Entity.Database.SetInitializer<SqlBotDataContext>(null);
        }
    
        /// <summary>
        /// Throw if the database or SqlBotDataEntities table have not been created.
        /// </summary>
        static internal void AssertDatabaseReady()
        {
            //var connectionString = Utils.GetAppSetting(AppSettingKeys.SqlServerConnectionString);
            var connectionString = ConfigurationManager.ConnectionStrings["BotDataContextConnectionString"].ConnectionString;
            using (var context = new SqlBotDataContext(connectionString))
            {
                if (!context.Database.Exists())
                    throw new ArgumentException("The sql database defined in the connection has not been created. See https://github.com/Microsoft/BotBuilder-Azure/tree/master/CSharp");
    
                if (context.Database.SqlQuery<int>(@"IF EXISTS (SELECT * FROM sys.tables WHERE name = 'SqlBotDataEntities') 
                                                                    SELECT 1
                                                                ELSE
                                                                    SELECT 0").SingleOrDefault() != 1)
                    throw new ArgumentException("The SqlBotDataEntities table has not been created in the database. See https://github.com/Microsoft/BotBuilder-Azure/tree/master/CSharp");
            }
        }
    
        public DbSet<SqlBotDataEntity> BotData { get; set; }
    }
    public enum BotStoreType
    {
        BotConversationData = 0,
        BotPrivateConversationData = 1,
        BotUserData = 2
    }
    internal class SqlBotDataEntity : IAddress
    {
        private static readonly JsonSerializerSettings serializationSettings = new JsonSerializerSettings()
        {
            Formatting = Formatting.None,
            NullValueHandling = NullValueHandling.Ignore
        };
        internal SqlBotDataEntity() { Timestamp = DateTimeOffset.UtcNow; }
        internal SqlBotDataEntity(BotStoreType botStoreType, string botId, string channelId, string conversationId, string userId, object data)
        {
            this.BotStoreType = botStoreType;
            this.BotId = botId;
            this.ChannelId = channelId;
            this.ConversationId = conversationId;
            this.UserId = userId;
            this.Data = Serialize(data);
            Timestamp = DateTimeOffset.UtcNow;
        }
    
    
        #region Fields
    
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        [Index("idxStoreChannelUser", 1)]
        [Index("idxStoreChannelConversation", 1)]
        [Index("idxStoreChannelConversationUser", 1)]
        public BotStoreType BotStoreType { get; set; }
        public string BotId { get; set; }
        [Index("idxStoreChannelConversation", 2)]
        [Index("idxStoreChannelUser", 2)]
        [Index("idxStoreChannelConversationUser", 2)]
        [MaxLength(200)]
        public string ChannelId { get; set; }
        [Index("idxStoreChannelConversation", 3)]
        [Index("idxStoreChannelConversationUser", 3)]
        [MaxLength(200)]
        public string ConversationId { get; set; }
        [Index("idxStoreChannelUser", 3)]
        [Index("idxStoreChannelConversationUser", 4)]
        [MaxLength(200)]
        public string UserId { get; set; }
        public byte[] Data { get; set; }
        public string ETag { get; set; }
        public string ServiceUrl { get; set; }
        [Required]
        public DateTimeOffset Timestamp { get; set; }
    
        #endregion Fields
    
        #region Methods
    
        private static byte[] Serialize(object data)
        {
            using (var cmpStream = new MemoryStream())
            using (var stream = new GZipStream(cmpStream, CompressionMode.Compress))
            using (var streamWriter = new StreamWriter(stream))
            {
                var serializedJSon = JsonConvert.SerializeObject(data, serializationSettings);
                streamWriter.Write(serializedJSon);
                streamWriter.Close();
                stream.Close();
                return cmpStream.ToArray();
            }
        }
    
        private static object Deserialize(byte[] bytes)
        {
            using (var stream = new MemoryStream(bytes))
            using (var gz = new GZipStream(stream, CompressionMode.Decompress))
            using (var streamReader = new StreamReader(gz))
            {
                return JsonConvert.DeserializeObject(streamReader.ReadToEnd());
            }
        }
    
        internal ObjectT GetData<ObjectT>()
        {
            return ((JObject)Deserialize(this.Data)).ToObject<ObjectT>();
        }
    
        internal object GetData()
        {
            return Deserialize(this.Data);
        }
        internal static async Task<SqlBotDataEntity> GetSqlBotDataEntity(IAddress key, BotStoreType botStoreType, SqlBotDataContext context)
        {
            SqlBotDataEntity entity = null;
            var query = context.BotData.OrderByDescending(d => d.Timestamp);
            switch (botStoreType)
            {
                case BotStoreType.BotConversationData:
                    entity = await query.FirstOrDefaultAsync(d => d.BotStoreType == botStoreType
                                                    && d.ChannelId == key.ChannelId
                                                    && d.ConversationId == key.ConversationId);
                    break;
                case BotStoreType.BotUserData:
                    entity = await query.FirstOrDefaultAsync(d => d.BotStoreType == botStoreType
                                                    && d.ChannelId == key.ChannelId
                                                    && d.UserId == key.UserId);
                    break;
                case BotStoreType.BotPrivateConversationData:
                    entity = await query.FirstOrDefaultAsync(d => d.BotStoreType == botStoreType
                                                    && d.ChannelId == key.ChannelId
                                                    && d.ConversationId == key.ConversationId
                                                    && d.UserId == key.UserId);
                    break;
                default:
                    throw new ArgumentException("Unsupported bot store type!");
            }
    
            return entity;
        }
        #endregion
    }