3

A brief background first: The Bot framework stores the conversation data within a storage either Azure tables/Cosmos DB (Azure tables in my case). For each conversation there is an entry made into the Azure table with the timestamp, userid, conversation messages and other details.

I am trying to retrieve the conversation details from Azure table storage using custom code in C# as shown below.

Microsoft.WindowsAzure.Storage.CloudStorageAccount storageAccount = Microsoft.WindowsAzure.Storage.CloudStorageAccount.Parse(connectionString);
Microsoft.WindowsAzure.Storage.Table.CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
CloudTable table = tableClient.GetTableReference("botdata");
TableQuery<DynamicTableEntity> projectionQuery = new TableQuery<DynamicTableEntity>().Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, "directline:user"));
var dataRow = table.ExecuteQuery(projectionQuery).Where(q => q.RowKey == "souvik").FirstOrDefault();
var conversation = Encoding.UTF8.GetString(dataRow.Properties["Data"].BinaryValue);
Console.WriteLine(conversation);

I am getting some gibberish text, something like this- \u001f�\b\0\0\0\0\0\u0004\0��\u0005\0C���\u0002\0\0\0 when I retrieve the data stored in Binary format in the Azure table storage. The data looks something like as shown below in the Azure storage explorer-

enter image description here

I have tried and tested for each conversation and I have seen that each time I type and send a data to the bot and get a response, new records are getting added to the Azure table with correct timestamp. Is it the correct way of retrieving the data or am I doing something wrong? Let me know if I can add more details.

Update-1:

I have referred this- How to retrieve Saved Conversation Data in Azure (Tablelogger) but did not get much help my issue.

Update-2

Based on Fei's answer below, I just noticed when I use the PartitionKey as directline:user or directline:conversation I get the deserialized values as {} but if I use directline:private I get somthing like this-

{"ResumptionContext":{"locale":"en-US","isTrustedServiceUrl":true}, "DialogState":"H4sIAAAAAAAEAO1a328iVRRmYGYKbTfbdZtNzLq7GM26DWS2FGi7MVUptUrcVi1VYxrSvcCFjh1mmrl3quyjb74YEx+N/hW+uzFxE/8KX9fsH6HnzAxQ7A8GhHpZoemBDs O953znO+d+99KQFAqF/oIHPuNjNgzm8y29YlvMqnFt3YJfRzeq1E7GP6U20y1zLa2llrQlLZOM5x2DOzZdM6nDbWIk4x85ZUOvfECbu9YhhTtT5Vp6NbtMqunlDE1nFZznF+nM8bWCyaltEoNpm 3oZpvKeHqX29s6+f0MnhlVnJz7nXdkl7DAZH1kMpVIEglAYJ5VDtWaTBmXKl0TnLCLLT8LFJuO0oeUtw6AVDjMx7T1qUluvaA91xs+P5lT0hU0cW8DwRzo4CYhOESAlhv6YIMaJIrdsWjAhJ2aF rjd3m0e06Ni2VSecJj4sfwGp2KE1alN4Gxl+ObPgbwzJEpPRKGBk/HPCkmFUoLqvA4ZM2Wf6YxrdP/aGBaR/DdpfhMVur4TUiUZjqs8i5I8iXwpxsUPLeEfkjs/ST0ydN7sGfN/CWV16T4GRFN /FWBTMFDodkvxL8pOxz4YbZwzNNJgrYRlj7gUOMlTeIJzE3DsQ0NkcY7RRNprbEKcUkqLqDNz0NPBqiPBskgrksikgSphsdXak6kFy6YXzXAHzW1DgPoPFWWzcYoibEutLGwlcLHKD2Icyai JZvpyV9odwH1x4tCQaask9v5t4oSXjDVaxbEMvd0bPaIv403vw8soKyVayy6kH6QxdXH3gpcTtYVfRzGFzxkbmdrRraF7CS1dbPfw6cnFuAmpvUGWbMi6blFZlOTBYO/CZ/yNYiYD4bAOcbcb OIxefw/60n0+j9Jg6JoZD9/dD0bAvRhSk+tNxp3T32HnLNCkubFphizJG6jQHu4lj0Bv/9KJ95xC7/An2B8ZVVPYLhOtAhXIDWf4nFMpz/zBHwqZ+rf3Ws5NvXW/JqJdDfeyWth3DcMtCPNHR 0VDz/axbopJxlK3Y3Zdw0FMqJ3adcrVB+YFVjcg9t3s/nUOUi3BK7B445iHg7HhiddSLzH1/AhCH/sGKtgVbLmoXzJp13hb2JppX0NxCCt3oZ50QlUKC9LPB2fb7ENgmCAgDE/M2mjto4ti2b 7ba9quTJndxK+isB1jWojcuXJNV251K/qaHr55HuRr3vhi5lG3ja1jEtwYgMZJVxsOurpOvWN4gjLmvinrdJOjWdPvV0rQ3JG755/wz8Zxdh6BNziR8hCLRK74jeNNeSX0d50GeuzlX745BztU 3wMmvI604DmxKqrpZ19AP5lrwIOjBqa/K+o+4VIojbPcCn9D6X8gNMNM5zWSQBnSGYwVAEfzqyhhMeQrePJ4nGYbLUTc1C+o9SMQz5eJE9JsJwZrtaL+LGYxCYwLRcPk50pYSiO54gj4t3W4piY WJ1h2azOuIDpRsL5B+PalPfu5PnwgSQVvKJFBHo5hGOaMmu3WD9mKlTb0P8fw4kRjDbeFBkxtQfSxCjr5XJ+pjoj4uQX0I0piCCxUFt54CbYkHP876tkcQ+QOim4mHlnXkXRH2sDSFZglNGsxsBs zax2YOYur4lQrql+kYhoJr8njKisHpcLfbryJpHBlU83DUdiyLe4FnBs1SFs0ymhVUPKmW3l8Vq546khkZJVqNoO6dzkE3rdAqNbncR9LCLfyldEtsvglmxseWNc2KpznXBIxbfQuc+k66QJTET8 RxztrbXrm8F1B5nH7Fe65zOfyvIVI26KnTnAX1bXDrj//erZFi72/Qs62CfWdsm2OntrEOAtYO3BnC+pFWWmWzDmbe93wHKlE/plU3wWr+Xy896kZwz9R33UxcxL6zvBwdDQNvRBbUTdxkhMVxXRSKesU28zeNVmRMBTMAAA=="}

I have a feeling that the DialogState data in the JSON above needs to be decrypted?

For every message I type and the response I receive, there are three records inserted into the Azure table each with a different PartitionKey which are- directline:private, directline:user and directline:conversation.

Souvik Ghosh
  • 4,456
  • 13
  • 56
  • 78
  • 1
    Deleting a question because of heavy downvoting and then immediately asking it again is frowned upon on this site. – Frauke Apr 17 '18 at 11:18
  • @FraukeNonnenmacher I know, I have only received down votes without anyone telling what I have done wrong. See this question, it's the same which I deleted. Do you see anything wrong as per the Stackoverflow guidelines? – Souvik Ghosh Apr 17 '18 at 11:25
  • You do not provide a minimal, complete and verifiable example, and you do not even give your expected result (despite the fact that you were asked for it) – Frauke Apr 17 '18 at 11:30
  • @FraukeNonnenmacher I have edited and updated the question now. Would have done that if someone pointed out at first. Thanks – Souvik Ghosh Apr 17 '18 at 11:46
  • Hmmm - if I recall correctly I **did** point that out to you at first. – Frauke Apr 17 '18 at 11:49
  • 1
    Possible duplicate of [How to retrieve Saved Conversation Data in Azure (Tablelogger)](https://stackoverflow.com/questions/48374471/how-to-retrieve-saved-conversation-data-in-azure-tablelogger) – D4RKCIDE Apr 17 '18 at 18:24

3 Answers3

1

Binary data in Azure Table Storage is stored as Base64 encoded string. What you would need to do is convert this string to bytes first and then get the string from those bytes.

Something like:

var conversation = Encoding.UTF8.GetString(Convert.FromBase64String(dataRow.Properties["Data"].BinaryValue));
Gaurav Mantri
  • 128,066
  • 12
  • 206
  • 241
  • First of all `Convert.FromBase64String` will only accept `string` value. Here I am getting the value as a `Byte` array which is stored in `BinaryValue`. I also tried this- `byte[] data = Convert.FromBase64String("H4sIAAAAAAAEAKuuBQBDv6ajAgAAAA=="); string decodedString = Encoding.UTF8.GetString(data);` but that too didn't work – Souvik Ghosh Apr 17 '18 at 11:28
  • My bad....I did not run the code. Sorry about that! Let me actually save some binary data in a table and retrieve it. – Gaurav Mantri Apr 17 '18 at 11:37
  • So I tried my code above and I was able to get the string back properly. You're correct that EntityProperty.BinaryValue returns a byte array. I tried converting byte array to string and it worked. I also copied base64 encoded string from storage explorer and ran the code above and that also worked. Can you please edit your question and include how the data is inserted in Azure Tables? That might give a clue about why the deserialization is not working in your case. – Gaurav Mantri Apr 17 '18 at 11:48
  • That's the tricky part. I have created the Web App bot on Azure which internally uses bot framework. Looking at the code there is no trace of that I could find, otherwise I could have found the issue. Do you know if there is any way to check that? – Souvik Ghosh Apr 17 '18 at 11:55
  • From the meta data it looks like a JSON property- `[JsonProperty(PropertyName = "data")] public object Data { get; set; }` – Souvik Ghosh Apr 17 '18 at 12:19
  • In my case it was a simply HTML file saved as binary content. I think in your case, the content is binary and that's why you're getting garbled text. – Gaurav Mantri Apr 17 '18 at 12:36
1

If you’d like to use WindowsAzure.Storage client library to retrieve entities from table storage and extract data from Data property, you can refer to the following code.

CloudStorageAccount storageAccount = CloudStorageAccount.Parse("DefaultEndpointsProtocol=https;AccountName={your_account_name};AccountKey={your_account_key};EndpointSuffix=core.windows.net");

CloudTableClient tableClient = storageAccount.CreateCloudTableClient();

CloudTable table = tableClient.GetTableReference("botdata");

TableQuery<MessageEntity> query = new TableQuery<MessageEntity>().Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, "emulator:user"));

foreach (MessageEntity entity in table.ExecuteQuery(query))
{
    string mydata = "";
    using (var msi = new MemoryStream(entity.Data))
    using (var mso = new MemoryStream())
    {
        using (var gs = new GZipStream(msi, CompressionMode.Decompress))
        {
            gs.CopyTo(mso);
        }
        mydata = Encoding.UTF8.GetString(mso.ToArray());
    }

    object data = JsonConvert.DeserializeObject(mydata);

    //.....
}

MessageEntity:

public class MessageEntity : TableEntity
{
    public MessageEntity(string pk, string rk)
    {
        this.PartitionKey = pk;
        this.RowKey = rk;
    }

    public MessageEntity() { }

    public string BotId { get; set; }
    public string ChannelId { get; set; }
    public string ConversationId { get; set; }
    public byte[] Data { get; set; }
    public string UserId { get; set; }
}

Test result:

enter image description here

Note:

  • In source code, you can find the Data property defined as byte[] in the BotDataEntity class.
  • In Jason Sowers's reply, he has shared the information about serialize and deserialize of the Data property.
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Fei Han
  • 26,415
  • 1
  • 30
  • 41
0

I am assuming you are using the botbuilder-azure package

don't do this in a dialog because you can just access the same data in the context object. it just happens to be where I wrote the code

It's actually as simple as this:

private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
    var activity = await result as Activity;

    IBotDataStore<BotData> table = new TableBotDataStore(ConfigurationManager.ConnectionStrings["StorageConnectionString"].ConnectionString);

//use the type of state data you need
    var userData = await table.LoadAsync(Address.FromActivity(activity), BotStoreType.BotUserData, CancellationToken.None );
    var privateConvoData = await table.LoadAsync(Address.FromActivity(activity), BotStoreType.BotPrivateConversationData, CancellationToken.None );
    var convoData = await table.LoadAsync(Address.FromActivity(activity), BotStoreType.BotConversationData, CancellationToken.None);

//in this case I am just replying with the data, but do what you need with it here
    var reply = activity.CreateReply(userData.Data.ToString());
    var reply2 = activity.CreateReply(privateConvoData.Data.ToString());
    var reply3 = activity.CreateReply(convoData.Data.ToString());

    await context.PostAsync(reply);
    await context.PostAsync(reply2);
    await context.PostAsync(reply3);

    context.Wait(MessageReceivedAsync);
}

If you notice in the source for botbuilder-azure there is a serialize and deserialize method. Where this happens:

private 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();
    }
}

So the data you need is compressed into the "jibberish" you were seeing. When accessing the data via the LoadAsync method it is also being decompressed like here:

private 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());
    }
}

deserialize is called inside the LoadAsync method in the return statement return new BotData(entity.ETag, entity.GetData()); It its the GetData() method like below:

internal ObjectT GetData<ObjectT>()
{
    return ((JObject)Deserialize(this.Data)).ToObject<ObjectT>();
}
D4RKCIDE
  • 3,439
  • 1
  • 18
  • 34
  • Thanks Jason for the details. I have used the Microsoft QnA Maker API as the service and created the Web App Bot on Azure without custom coding. I assume the code you have detailed has gone under the hood, abstracted, when I created the bot. Now the data which is getting logged in the table is needed by me in another app. so how do I retrieve the data (conversation) which is already getting stored? – Souvik Ghosh Apr 18 '18 at 04:06
  • Also, I tried to decompress the bytes as you mentioned in the code above. I am getting no data when doing `streamReader.ReadToEnd()`. – Souvik Ghosh Apr 18 '18 at 08:14
  • To use this method your other project would have to contain the `botbuilder-azure` NuGet package, and you would have to pass the `Activity` object to your other app somehow. – D4RKCIDE Apr 18 '18 at 16:16