TableBotDataStore.cs
1 //
2 // Copyright (c) Microsoft. All rights reserved.
3 // Licensed under the MIT license.
4 //
5 // Microsoft Bot Framework: http://botframework.com
6 //
7 // Bot Builder SDK Github:
8 // https://github.com/Microsoft/BotBuilder
9 //
10 // Copyright (c) Microsoft Corporation
11 // All rights reserved.
12 //
13 // MIT License:
14 // Permission is hereby granted, free of charge, to any person obtaining
15 // a copy of this software and associated documentation files (the
16 // "Software"), to deal in the Software without restriction, including
17 // without limitation the rights to use, copy, modify, merge, publish,
18 // distribute, sublicense, and/or sell copies of the Software, and to
19 // permit persons to whom the Software is furnished to do so, subject to
20 // the following conditions:
21 //
22 // The above copyright notice and this permission notice shall be
23 // included in all copies or substantial portions of the Software.
24 //
25 // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
26 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 //
33 
34 using System;
35 using System.Collections.Generic;
36 using System.Threading.Tasks;
37 using Microsoft.Bot.Connector;
38 using Newtonsoft.Json;
39 using System.Net;
40 using System.Threading;
41 using System.Web;
44 using Microsoft.WindowsAzure.Storage.Table;
45 using Microsoft.WindowsAzure.Storage;
46 using System.IO;
47 using System.IO.Compression;
48 using Newtonsoft.Json.Linq;
49 
50 namespace Microsoft.Bot.Builder.Azure
51 {
52 
56  public class TableBotDataStore : IBotDataStore<BotData>
57  {
58  private static HashSet<string> checkedTables = new HashSet<string>();
59 
65  public TableBotDataStore(string connectionString, string tableName = "botdata")
66  : this(CloudStorageAccount.Parse(connectionString), tableName)
67  {
68  }
69 
75  public TableBotDataStore(CloudStorageAccount storageAccount, string tableName = "botdata")
76  {
77  var tableClient = storageAccount.CreateCloudTableClient();
78  this.Table = tableClient.GetTableReference(tableName);
79 
80  lock (checkedTables)
81  {
82  if (!checkedTables.Contains(tableName))
83  {
84  this.Table.CreateIfNotExists();
85  checkedTables.Add(tableName);
86  }
87  }
88  }
89 
94  public TableBotDataStore(CloudTable table)
95  {
96  this.Table = table;
97  }
98 
102  public CloudTable Table { get; private set; }
103 
104  async Task<BotData> IBotDataStore<BotData>.LoadAsync(IAddress key, BotStoreType botStoreType, CancellationToken cancellationToken)
105  {
106  var entityKey = BotDataEntity.GetEntityKey(key, botStoreType);
107  try
108  {
109  var result = await this.Table.ExecuteAsync(TableOperation.Retrieve<BotDataEntity>(entityKey.PartitionKey, entityKey.RowKey));
110  BotDataEntity entity = (BotDataEntity)result.Result;
111  if (entity == null)
112  // empty record ready to be saved
113  return new BotData(eTag: String.Empty, data: null);
114 
115  // return botdata
116  return new BotData(entity.ETag, entity.GetData());
117  }
118  catch (StorageException err)
119  {
120  throw new HttpException(err.RequestInformation.HttpStatusCode, err.RequestInformation.HttpStatusMessage);
121  }
122  }
123 
124  async Task IBotDataStore<BotData>.SaveAsync(IAddress key, BotStoreType botStoreType, BotData botData, CancellationToken cancellationToken)
125  {
126  var entityKey = BotDataEntity.GetEntityKey(key, botStoreType);
127  BotDataEntity entity = new BotDataEntity(key.BotId, key.ChannelId, key.ConversationId, key.UserId, botData.Data)
128  {
129  ETag = botData.ETag
130  };
131  entity.PartitionKey = entityKey.PartitionKey;
132  entity.RowKey = entityKey.RowKey;
133 
134  try
135  {
136  if (String.IsNullOrEmpty(entity.ETag))
137  await this.Table.ExecuteAsync(TableOperation.Insert(entity));
138  else if (entity.ETag == "*")
139  {
140  if (botData.Data != null)
141  await this.Table.ExecuteAsync(TableOperation.InsertOrReplace(entity));
142  else
143  await this.Table.ExecuteAsync(TableOperation.Delete(entity));
144  }
145  else
146  {
147  if (botData.Data != null)
148  await this.Table.ExecuteAsync(TableOperation.Replace(entity));
149  else
150  await this.Table.ExecuteAsync(TableOperation.Delete(entity));
151  }
152  }
153  catch (StorageException err)
154  {
155  if ((HttpStatusCode)err.RequestInformation.HttpStatusCode == HttpStatusCode.Conflict)
156  throw new HttpException((int)HttpStatusCode.PreconditionFailed, err.RequestInformation.HttpStatusMessage);
157 
158  throw new HttpException(err.RequestInformation.HttpStatusCode, err.RequestInformation.HttpStatusMessage);
159  }
160  }
161 
162  Task<bool> IBotDataStore<BotData>.FlushAsync(IAddress key, CancellationToken cancellationToken)
163  {
164  // Everything is saved. Flush is no-op
165  return Task.FromResult(true);
166  }
167 
168  }
169 
170  internal class EntityKey
171  {
172  public EntityKey(string partition, string row)
173  {
174  PartitionKey = partition;
175  RowKey = row;
176  }
177 
178  public string PartitionKey { get; private set; }
179  public string RowKey { get; private set; }
180 
181  }
182 
183  internal class BotDataEntity : TableEntity
184  {
185  private static readonly JsonSerializerSettings serializationSettings = new JsonSerializerSettings()
186  {
187  Formatting = Formatting.None,
188  NullValueHandling = NullValueHandling.Ignore
189  };
190 
191  public BotDataEntity()
192  {
193  }
194 
195  internal BotDataEntity(string botId, string channelId, string conversationId, string userId, object data)
196  {
197  this.BotId = botId;
198  this.ChannelId = channelId;
199  this.ConversationId = conversationId;
200  this.UserId = userId;
201  this.Data = Serialize(data);
202  }
203 
204  private byte[] Serialize(object data)
205  {
206  using (var cmpStream = new MemoryStream())
207  using (var stream = new GZipStream(cmpStream, CompressionMode.Compress))
208  using (var streamWriter = new StreamWriter(stream))
209  {
210  var serializedJSon = JsonConvert.SerializeObject(data, serializationSettings);
211  streamWriter.Write(serializedJSon);
212  streamWriter.Close();
213  stream.Close();
214  return cmpStream.ToArray();
215  }
216  }
217 
218  private object Deserialize(byte[] bytes)
219  {
220  using (var stream = new MemoryStream(bytes))
221  using (var gz = new GZipStream(stream, CompressionMode.Decompress))
222  using (var streamReader = new StreamReader(gz))
223  {
224  return JsonConvert.DeserializeObject(streamReader.ReadToEnd());
225  }
226  }
227 
228 
229  internal static EntityKey GetEntityKey(IAddress key, BotStoreType botStoreType)
230  {
231  switch (botStoreType)
232  {
233  case BotStoreType.BotConversationData:
234  return new EntityKey($"{key.ChannelId}:conversation", key.ConversationId.SanitizeForAzureKeys());
235 
236  case BotStoreType.BotUserData:
237  return new EntityKey($"{key.ChannelId}:user", key.UserId.SanitizeForAzureKeys());
238 
239  case BotStoreType.BotPrivateConversationData:
240  return new EntityKey($"{key.ChannelId}:private", $"{key.ConversationId.SanitizeForAzureKeys()}:{key.UserId.SanitizeForAzureKeys()}");
241 
242  default:
243  throw new ArgumentException("Unsupported bot store type!");
244  }
245  }
246 
247  internal ObjectT GetData<ObjectT>()
248  {
249  return ((JObject)Deserialize(this.Data)).ToObject<ObjectT>();
250  }
251 
252  internal object GetData()
253  {
254  return Deserialize(this.Data);
255  }
256 
257  public string BotId { get; set; }
258 
259  public string ChannelId { get; set; }
260 
261  public string ConversationId { get; set; }
262 
263  public string UserId { get; set; }
264 
265  public byte[] Data { get; set; }
266  }
267 
268 }
Task SaveAsync(IAddress key, BotStoreType botStoreType, T data, CancellationToken cancellationToken)
Save a BotData using the ETag. Etag consistency checks If ETag is null or empty, this will set the va...
Root namespace for the Microsoft Bot Connector SDK.
Definition: ActionTypes.cs:7
IBotDataStore<T> Implementation using Azure Storage Table
TableBotDataStore(string connectionString, string tableName="botdata")
Creates an instance of the IBotDataStore<T> that uses the azure table storage.
TableBotDataStore(CloudStorageAccount storageAccount, string tableName="botdata")
Creates an instance of the IBotDataStore<T> that uses the azure table storage.
The key that minimally and completely identifies a bot&#39;s conversation with a user on a channel...
Definition: Address.cs:45
Task< bool > FlushAsync(IAddress key, CancellationToken cancellationToken)
TableBotDataStore(CloudTable table)
Creates an instance of the IBotDataStore<T> that uses the azure table storage.
Namespace for internal Dialogs machinery that is not useful for most developers.
Task< T > LoadAsync(IAddress key, BotStoreType botStoreType, CancellationToken cancellationToken)
Return BotData with Data pointing to a JObject or an empty BotData() record with ETag:"" ...
Core namespace for Dialogs and associated infrastructure.
Definition: Address.cs:40
Root namespace for the Microsoft Bot Builder SDK.