Dialogs/BotData.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 
35 using Microsoft.Bot.Connector;
36 using Newtonsoft.Json;
37 using Newtonsoft.Json.Linq;
38 using System;
39 using System.Collections.Concurrent;
40 using System.Collections.Generic;
41 using System.Configuration;
42 using System.IO;
43 using System.IO.Compression;
44 using System.Linq;
45 using System.Net;
46 using System.Threading;
47 using System.Threading.Tasks;
48 using System.Web;
49 
50 namespace Microsoft.Bot.Builder.Dialogs.Internals
51 {
52  public enum BotStoreType
53  {
57  }
58 
59  public interface IBotDataStore<T>
60  {
68  Task<T> LoadAsync(IAddress key, BotStoreType botStoreType, CancellationToken cancellationToken);
69 
83  Task SaveAsync(IAddress key, BotStoreType botStoreType, T data, CancellationToken cancellationToken);
84  Task<bool> FlushAsync(IAddress key, CancellationToken cancellationToken);
85  }
86 
90  public class InMemoryDataStore : IBotDataStore<BotData>
91  {
92  internal readonly ConcurrentDictionary<string, string> store = new ConcurrentDictionary<string, string>();
93  private readonly Dictionary<BotStoreType, object> locks = new Dictionary<BotStoreType, object>()
94  {
95  { BotStoreType.BotConversationData, new object() },
96  { BotStoreType.BotPrivateConversationData, new object() },
97  { BotStoreType.BotUserData, new object() }
98  };
99 
100  async Task<BotData> IBotDataStore<BotData>.LoadAsync(IAddress key, BotStoreType botStoreType, CancellationToken cancellationToken)
101  {
102  string serializedData;
103  if (store.TryGetValue(GetKey(key, botStoreType), out serializedData))
104  return Deserialize(serializedData);
105  return new BotData(eTag: String.Empty);
106  }
107 
108  async Task IBotDataStore<BotData>.SaveAsync(IAddress key, BotStoreType botStoreType, BotData botData, CancellationToken cancellationToken)
109  {
110  lock (locks[botStoreType])
111  {
112  if (botData.Data != null)
113  {
114  store.AddOrUpdate(GetKey(key, botStoreType), (dictionaryKey) =>
115  {
116  botData.ETag = Guid.NewGuid().ToString("n");
117  return Serialize(botData);
118  }, (dictionaryKey, value) =>
119  {
120  ValidateETag(botData, value);
121  botData.ETag = Guid.NewGuid().ToString("n");
122  return Serialize(botData);
123  });
124  }
125  else
126  {
127  // remove record on null
128  string value;
129  if (store.TryGetValue(GetKey(key, botStoreType), out value))
130  {
131  ValidateETag(botData, value);
132  store.TryRemove(GetKey(key, botStoreType), out value);
133  return;
134  }
135  }
136  }
137  }
138 
139  private static void ValidateETag(BotData botData, string value)
140  {
141  if (botData.ETag != "*" && Deserialize(value).ETag != botData.ETag)
142  {
143  throw new HttpException((int)HttpStatusCode.PreconditionFailed, "Inconsistent SaveAsync based on ETag!");
144  }
145  }
146 
147  Task<bool> IBotDataStore<BotData>.FlushAsync(IAddress key, CancellationToken cancellationToken)
148  {
149  // Everything is saved. Flush is no-op
150  return Task.FromResult(true);
151  }
152 
153  private static string GetKey(IAddress key, BotStoreType botStoreType)
154  {
155  switch (botStoreType)
156  {
157  case BotStoreType.BotConversationData:
158  return $"conversation:{key.BotId}:{key.ChannelId}:{key.ConversationId}";
159  case BotStoreType.BotUserData:
160  return $"user:{key.BotId}:{key.ChannelId}:{key.UserId}";
161  case BotStoreType.BotPrivateConversationData:
162  return $"privateConversation:{key.BotId}:{key.ChannelId}:{key.UserId}:{key.ConversationId}";
163  default:
164  throw new ArgumentException("Unsupported bot store type!");
165  }
166  }
167 
168  private static string Serialize(BotData data)
169  {
170  using (var cmpStream = new MemoryStream())
171  using (var stream = new GZipStream(cmpStream, CompressionMode.Compress))
172  using (var streamWriter = new StreamWriter(stream))
173  {
174  var serializedJSon = JsonConvert.SerializeObject(data);
175  streamWriter.Write(serializedJSon);
176  streamWriter.Close();
177  stream.Close();
178  return Convert.ToBase64String(cmpStream.ToArray());
179  }
180  }
181 
182  private static BotData Deserialize(string str)
183  {
184  byte[] bytes = Convert.FromBase64String(str);
185  using (var stream = new MemoryStream(bytes))
186  using (var gz = new GZipStream(stream, CompressionMode.Decompress))
187  using (var streamReader = new StreamReader(gz))
188  {
189  return JsonConvert.DeserializeObject<BotData>(streamReader.ReadToEnd());
190  }
191  }
192  }
193 
197  public class ConnectorStore : IBotDataStore<BotData>
198  {
199  private readonly IStateClient stateClient;
200  public ConnectorStore(IStateClient stateClient)
201  {
202  SetField.NotNull(out this.stateClient, nameof(stateClient), stateClient);
203  }
204 
205  async Task<BotData> IBotDataStore<BotData>.LoadAsync(IAddress key, BotStoreType botStoreType, CancellationToken cancellationToken)
206  {
207  BotData botData;
208  switch (botStoreType)
209  {
210  case BotStoreType.BotConversationData:
211  botData = await stateClient.BotState.GetConversationDataAsync(key.ChannelId, key.ConversationId, cancellationToken);
212  break;
213  case BotStoreType.BotUserData:
214  botData = await stateClient.BotState.GetUserDataAsync(key.ChannelId, key.UserId, cancellationToken);
215  break;
216  case BotStoreType.BotPrivateConversationData:
217  botData = await stateClient.BotState.GetPrivateConversationDataAsync(key.ChannelId, key.ConversationId, key.UserId, cancellationToken);
218  break;
219  default:
220  throw new ArgumentException($"{botStoreType} is not a valid store type!");
221  }
222  return botData;
223  }
224 
225  async Task IBotDataStore<BotData>.SaveAsync(IAddress key, BotStoreType botStoreType, BotData botData, CancellationToken cancellationToken)
226  {
227  switch (botStoreType)
228  {
229  case BotStoreType.BotConversationData:
230  await stateClient.BotState.SetConversationDataAsync(key.ChannelId, key.ConversationId, botData, cancellationToken);
231  break;
232  case BotStoreType.BotUserData:
233  await stateClient.BotState.SetUserDataAsync(key.ChannelId, key.UserId, botData, cancellationToken);
234  break;
235  case BotStoreType.BotPrivateConversationData:
236  await stateClient.BotState.SetPrivateConversationDataAsync(key.ChannelId, key.ConversationId, key.UserId, botData, cancellationToken);
237  break;
238  default:
239  throw new ArgumentException($"{botStoreType} is not a valid store type!");
240  }
241  }
242 
243  Task<bool> IBotDataStore<BotData>.FlushAsync(IAddress key, CancellationToken cancellationToken)
244  {
245  // Everything is saved. Flush is no-op
246  return Task.FromResult(true);
247  }
248  }
249 
254  {
266  }
267 
271  public class CachingBotDataStore : IBotDataStore<BotData>
272  {
273  private readonly IBotDataStore<BotData> inner;
274  internal readonly Dictionary<IAddress, CacheEntry> cache = new Dictionary<IAddress, CacheEntry>();
275  private readonly CachingBotDataStoreConsistencyPolicy dataConsistencyPolicy;
276 
278  {
279  SetField.NotNull(out this.inner, nameof(inner), inner);
280  this.dataConsistencyPolicy = dataConsistencyPolicy;
281  }
282 
283  internal class CacheEntry
284  {
285  public BotData BotConversationData { set; get; }
286  public BotData BotPrivateConversationData { set; get; }
287  public BotData BotUserData { set; get; }
288  }
289 
290  async Task<bool> IBotDataStore<BotData>.FlushAsync(IAddress key, CancellationToken cancellationToken)
291  {
292  CacheEntry entry;
293  if (cache.TryGetValue(key, out entry))
294  {
295  // Removing the cached entry to make sure that we are not leaking
296  // flushed entries when CachingBotDataStore is registered as a singleton object.
297  // Also since this store is not updating ETags on LoadAsync(...), there
298  // will be a conflict if we reuse the cached entries after flush.
299  cache.Remove(key);
300  await this.Save(key, entry, cancellationToken);
301  return true;
302  }
303  else
304  {
305  return false;
306  }
307  }
308 
309  async Task<BotData> IBotDataStore<BotData>.LoadAsync(IAddress key, BotStoreType botStoreType, CancellationToken cancellationToken)
310  {
311  CacheEntry cacheEntry;
312  BotData value = null;
313  if (!cache.TryGetValue(key, out cacheEntry))
314  {
315  cacheEntry = new CacheEntry();
316  cache.Add(key, cacheEntry);
317  value = await LoadFromInnerAndCache(cacheEntry, botStoreType, key, cancellationToken);
318  }
319  else
320  {
321  switch (botStoreType)
322  {
323  case BotStoreType.BotConversationData:
324  if (cacheEntry.BotConversationData != null)
325  {
326  value = cacheEntry.BotConversationData;
327  }
328  break;
329  case BotStoreType.BotPrivateConversationData:
330  if (cacheEntry.BotPrivateConversationData != null)
331  {
332  value = cacheEntry.BotPrivateConversationData;
333  }
334  break;
335  case BotStoreType.BotUserData:
336  if (cacheEntry.BotUserData != null)
337  {
338  value = cacheEntry.BotUserData;
339  }
340  break;
341  default:
342  throw new NotImplementedException();
343  }
344 
345  if (value == null)
346  {
347  value = await LoadFromInnerAndCache(cacheEntry, botStoreType, key, cancellationToken);
348  }
349  }
350 
351  return value;
352  }
353 
354  async Task IBotDataStore<BotData>.SaveAsync(IAddress key, BotStoreType botStoreType, BotData value, CancellationToken cancellationToken)
355  {
356  CacheEntry entry;
357  if (!cache.TryGetValue(key, out entry))
358  {
359  entry = new CacheEntry();
360  cache.Add(key, entry);
361  }
362 
363  SetCachedValue(entry, botStoreType, value);
364  }
365 
366  private async Task<BotData> LoadFromInnerAndCache(CacheEntry cacheEntry, BotStoreType botStoreType, IAddress key, CancellationToken token)
367  {
368  var value = await inner.LoadAsync(key, botStoreType, token);
369 
370  if (value != null)
371  {
372  SetCachedValue(cacheEntry, botStoreType, value);
373  }
374  else
375  {
376  // inner store returned null, we create a new instance of BotData with ETag = "*"
377  value = new BotData() { ETag = "*" };
378  SetCachedValue(cacheEntry, botStoreType, value);
379  }
380  return value;
381  }
382 
383  private void SetCachedValue(CacheEntry entry, BotStoreType botStoreType, BotData value)
384  {
385  switch (botStoreType)
386  {
387  case BotStoreType.BotConversationData:
388  entry.BotConversationData = value;
389  break;
390  case BotStoreType.BotPrivateConversationData:
391  entry.BotPrivateConversationData = value;
392  break;
393  case BotStoreType.BotUserData:
394  entry.BotUserData = value;
395  break;
396  default:
397  throw new NotImplementedException();
398  }
399  }
400 
401  private async Task Save(IAddress key, CacheEntry entry, CancellationToken cancellationToken)
402  {
403  switch (this.dataConsistencyPolicy)
404  {
405  case CachingBotDataStoreConsistencyPolicy.LastWriteWins:
406  if (entry?.BotConversationData != null)
407  {
408  entry.BotConversationData.ETag = "*";
409  }
410 
411  if (entry?.BotUserData != null)
412  {
413  entry.BotUserData.ETag = "*";
414  }
415 
416  if (entry?.BotPrivateConversationData != null)
417  {
418  entry.BotPrivateConversationData.ETag = "*";
419  }
420  break;
421  case CachingBotDataStoreConsistencyPolicy.ETagBasedConsistency:
422  // no action needed, store relies on the ETags returned by inner store
423  break;
424  default:
425  throw new ArgumentException($"{this.dataConsistencyPolicy} is not a valid consistency policy!");
426  }
427 
428  var tasks = new List<Task>(capacity: 3);
429 
430  if (entry?.BotConversationData != null)
431  {
432  tasks.Add(inner.SaveAsync(key, BotStoreType.BotConversationData, entry.BotConversationData, cancellationToken));
433  }
434 
435  if (entry?.BotUserData != null)
436  {
437  tasks.Add(inner.SaveAsync(key, BotStoreType.BotUserData, entry.BotUserData, cancellationToken));
438  }
439 
440  if (entry?.BotPrivateConversationData != null)
441  {
442  tasks.Add(inner.SaveAsync(key, BotStoreType.BotPrivateConversationData, entry.BotPrivateConversationData, cancellationToken));
443  }
444  await Task.WhenAll(tasks);
445  }
446  }
447 
449  {
450  private readonly IBotData inner;
451  private readonly IDialogTaskManager dialogTaskManager;
452 
454  {
455  SetField.NotNull(out this.inner, nameof(inner), inner);
456  SetField.NotNull(out this.dialogTaskManager, nameof(dialogTaskManager), dialogTaskManager);
457  }
458 
459  public IBotDataBag UserData { get { return inner.UserData; } }
460  public IBotDataBag ConversationData { get { return inner.ConversationData; } }
461  public IBotDataBag PrivateConversationData { get { return inner.PrivateConversationData; } }
462  public async Task LoadAsync(CancellationToken token)
463  {
464  await this.inner.LoadAsync(token);
465  await this.dialogTaskManager.LoadDialogTasks(token);
466  }
467 
468  public async Task FlushAsync(CancellationToken token)
469  {
470  await this.dialogTaskManager.FlushDialogTasks(token);
471  await this.inner.FlushAsync(token);
472  }
473  }
474 
475  public abstract class BotDataBase<T> : IBotData
476  {
478  protected readonly IAddress botDataKey;
479  private IBotDataBag conversationData;
480  private IBotDataBag privateConversationData;
481  private IBotDataBag userData;
482 
483  public BotDataBase(IAddress botDataKey, IBotDataStore<BotData> botDataStore)
484  {
485  SetField.NotNull(out this.botDataStore, nameof(botDataStore), botDataStore);
486  SetField.NotNull(out this.botDataKey, nameof(botDataKey), botDataKey);
487  }
488 
489  protected abstract T MakeData();
490  protected abstract IBotDataBag WrapData(T data);
491 
492  public async Task LoadAsync(CancellationToken cancellationToken)
493  {
494  var conversationTask = LoadData(BotStoreType.BotConversationData, cancellationToken);
495  var privateConversationTask = LoadData(BotStoreType.BotPrivateConversationData, cancellationToken);
496  var userTask = LoadData(BotStoreType.BotUserData, cancellationToken);
497 
498  this.conversationData = await conversationTask;
499  this.privateConversationData = await privateConversationTask;
500  this.userData = await userTask;
501  }
502 
503  public async Task FlushAsync(CancellationToken cancellationToken)
504  {
505  await this.botDataStore.FlushAsync(botDataKey, cancellationToken);
506  }
507 
509  {
510  get
511  {
512  CheckNull(nameof(conversationData), conversationData);
513  return this.conversationData;
514  }
515  }
516 
518  {
519  get
520  {
521  CheckNull(nameof(privateConversationData), privateConversationData);
522  return this.privateConversationData;
523  }
524  }
525 
527  {
528  get
529  {
530  CheckNull(nameof(userData), userData);
531  return this.userData;
532  }
533  }
534 
535  private async Task<IBotDataBag> LoadData(BotStoreType botStoreType, CancellationToken cancellationToken)
536  {
537  var botData = await this.botDataStore.LoadAsync(botDataKey, botStoreType, cancellationToken);
538  if (botData?.Data == null)
539  {
540  botData.Data = this.MakeData();
541  await this.botDataStore.SaveAsync(botDataKey, botStoreType, botData, cancellationToken);
542  }
543  return this.WrapData((T)botData.Data);
544  }
545 
546  private void CheckNull(string name, IBotDataBag value)
547  {
548  if (value == null)
549  {
550  throw new InvalidOperationException($"{name} cannot be null! probably forgot to call LoadAsync() first!");
551  }
552  }
553  }
554 
555  public sealed class DictionaryBotData : BotDataBase<Dictionary<string, object>>
556  {
557  public DictionaryBotData(IAddress botDataKey, IBotDataStore<BotData> botDataStore)
558  : base(botDataKey, botDataStore)
559  {
560  }
561 
562  protected override Dictionary<string, object> MakeData()
563  {
564  return new Dictionary<string, object>();
565  }
566 
567  private sealed class Bag : IBotDataBag
568  {
569  private readonly Dictionary<string, object> bag;
570  public Bag(Dictionary<string, object> bag)
571  {
572  SetField.NotNull(out this.bag, nameof(bag), bag);
573  }
574 
575  int IBotDataBag.Count { get { return this.bag.Count; } }
576 
577  void IBotDataBag.SetValue<T>(string key, T value)
578  {
579  this.bag[key] = value;
580  }
581 
582  bool IBotDataBag.ContainsKey(string key)
583  {
584  return this.bag.ContainsKey(key);
585  }
586 
587  bool IBotDataBag.TryGetValue<T>(string key, out T value)
588  {
589  object boxed;
590  bool found = this.bag.TryGetValue(key, out boxed);
591  if (found)
592  {
593  if (boxed is T)
594  {
595  value = (T)boxed;
596  return true;
597  }
598  }
599 
600  value = default(T);
601  return false;
602  }
603 
604  bool IBotDataBag.RemoveValue(string key)
605  {
606  return this.bag.Remove(key);
607  }
608 
609  void IBotDataBag.Clear()
610  {
611  this.bag.Clear();
612  }
613  }
614 
615  protected override IBotDataBag WrapData(Dictionary<string, object> data)
616  {
617  return new Bag(data);
618  }
619  }
620 
621  public sealed class JObjectBotData : BotDataBase<JObject>
622  {
623  public JObjectBotData(IAddress botDataKey, IBotDataStore<BotData> botDataStore)
624  : base(botDataKey, botDataStore)
625  {
626  }
627 
628  protected override JObject MakeData()
629  {
630  return new JObject();
631  }
632  private sealed class Bag : IBotDataBag
633  {
634  private readonly JObject bag;
635  public Bag(JObject bag)
636  {
637  SetField.NotNull(out this.bag, nameof(bag), bag);
638  }
639 
640  int IBotDataBag.Count { get { return this.bag.Count; } }
641 
642  void IBotDataBag.SetValue<T>(string key, T value)
643  {
644  var token = JToken.FromObject(value);
645 #if DEBUG
646  var copy = token.ToObject<T>();
647 #endif
648  this.bag[key] = token;
649  }
650 
651  bool IBotDataBag.ContainsKey(string key)
652  {
653  return this.bag[key] != null;
654  }
655 
656  bool IBotDataBag.TryGetValue<T>(string key, out T value)
657  {
658  JToken token;
659  bool found = this.bag.TryGetValue(key, out token);
660  if (found)
661  {
662  value = token.ToObject<T>();
663  return true;
664  }
665 
666  value = default(T);
667  return false;
668  }
669 
670  bool IBotDataBag.RemoveValue(string key)
671  {
672  return this.bag.Remove(key);
673  }
674 
675  void IBotDataBag.Clear()
676  {
677  this.bag.RemoveAll();
678  }
679 
680  }
681 
682  protected override IBotDataBag WrapData(JObject data)
683  {
684  return new Bag(data);
685  }
686  }
687 
688  public sealed class BotDataBagStream : MemoryStream
689  {
690  private readonly IBotDataBag bag;
691  private readonly string key;
692  public BotDataBagStream(IBotDataBag bag, string key)
693  {
694  SetField.NotNull(out this.bag, nameof(bag), bag);
695  SetField.NotNull(out this.key, nameof(key), key);
696 
697  byte[] blob;
698  if (this.bag.TryGetValue(key, out blob))
699  {
700  this.Write(blob, 0, blob.Length);
701  this.Position = 0;
702  }
703  }
704 
705  public override void Flush()
706  {
707  base.Flush();
708 
709  var blob = this.ToArray();
710  this.bag.SetValue(this.key, blob);
711  }
712 
713  public override void Close()
714  {
715  this.Flush();
716  base.Close();
717  }
718  }
719 }
The Bot State REST API allows your bot to store and retrieve state associated with conversations cond...
Definition: IStateClient.cs:50
async Task FlushAsync(CancellationToken cancellationToken)
Flushes the bot data to IBotDataStore<T>
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...
bool ContainsKey(string key)
Checks if data bag contains a value with specified key
Root namespace for the Microsoft Bot Connector SDK.
Definition: ActionTypes.cs:7
JObjectBotData(IAddress botDataKey, IBotDataStore< BotData > botDataStore)
Causes CachingBotDataStore to write data with the same BotData.ETag returned by CachingBotDataStore.inner. As a result IBotDataStore<T>.FlushAsync(IAddress, CancellationToken) might fail because of ETag inconsistencies.
void Clear()
Removes all of the values from data bag.
bool RemoveValue(string key)
Removes the specified key from the bot data bag.
Caches data for BotDataBase<T> and wraps the data in BotData to be stored in CachingBotDataStore.inner
Volitile in-memory implementation of IBotDataStore<BotData>
CachingBotDataStoreConsistencyPolicy
The data consistency policy for CachingBotDataStore
IBotDataBag UserData
Private bot data associated with a user (across all channels and conversations).
Definition: IBotData.cs:47
async Task LoadAsync(CancellationToken token)
Loads the bot data from IBotDataStore<T>
DictionaryBotData(IAddress botDataKey, IBotDataStore< BotData > botDataStore)
Namespace for internal machinery that is not useful for most developers.
async Task FlushAsync(CancellationToken token)
Flushes the bot data to IBotDataStore<T>
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)
A property bag of bot data.
Definition: IBotDataBag.cs:41
IBotDataBag ConversationData
Private bot data associated with a conversation.
Definition: IBotData.cs:52
int Count
Gets the number of key/value pairs contained in the IBotDataBag.
Definition: IBotDataBag.cs:46
CachingBotDataStore(IBotDataStore< BotData > inner, CachingBotDataStoreConsistencyPolicy dataConsistencyPolicy)
DialogTaskManagerBotDataLoader(IBotData inner, IDialogTaskManager dialogTaskManager)
async Task LoadAsync(CancellationToken cancellationToken)
Loads the bot data from IBotDataStore<T>
BotDataBase(IAddress botDataKey, IBotDataStore< BotData > botDataStore)
Task< T > LoadAsync(IAddress key, BotStoreType botStoreType, CancellationToken cancellationToken)
Return BotData with Data pointing to a JObject or an empty BotData() record with ETag:"" ...
Causes CachingBotDataStore to set BotData.ETag to "*" when it flushes the data to storage...
override IBotDataBag WrapData(Dictionary< string, object > data)
override Dictionary< string, object > MakeData()
readonly IBotDataStore< BotData > botDataStore
Namespace for the internal fibers machinery that is not useful for most developers.
Definition: Awaitable.cs:36
implementation of IBotDatStore which uses the State REST API on state.botframework.com to store data
Root namespace for the Microsoft Bot Builder SDK.
IBotDataBag PrivateConversationData
Private bot data associated with a user in a conversation.
Definition: IBotData.cs:57