/
MemoryStorage.cs
175 lines (155 loc) · 7.24 KB
/
MemoryStorage.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Microsoft.Bot.Builder
{
/// <summary>
/// A storage layer that uses an in-memory dictionary.
/// </summary>
public class MemoryStorage : IStorage
{
private static readonly JsonSerializer StateJsonSerializer = new JsonSerializer()
{
TypeNameHandling = TypeNameHandling.All, // CODEQL [cs/unsafe-type-name-handling] we use All so that we get typed roundtrip out of storage, but we don't use validation because we don't know what types are valid
ReferenceLoopHandling = ReferenceLoopHandling.Error,
MaxDepth = null
};
// If a JsonSerializer is not provided during construction, this will be the default static JsonSerializer.
private readonly JsonSerializer _stateJsonSerializer;
private readonly Dictionary<string, JObject> _memory;
private readonly object _syncroot = new object();
private int _eTag = 0;
/// <summary>
/// Initializes a new instance of the <see cref="MemoryStorage"/> class.
/// </summary>
/// <param name="jsonSerializer">If passing in a custom JsonSerializer, we recommend the following settings:
/// <para>jsonSerializer.TypeNameHandling = TypeNameHandling.All.</para>
/// <para>jsonSerializer.NullValueHandling = NullValueHandling.Include.</para>
/// <para>jsonSerializer.ContractResolver = new DefaultContractResolver().</para>
/// </param>
/// <param name="dictionary">A pre-existing dictionary to use; or null to use a new one.</param>
public MemoryStorage(JsonSerializer jsonSerializer, Dictionary<string, JObject> dictionary = null)
{
_stateJsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer));
_memory = dictionary ?? new Dictionary<string, JObject>();
}
/// <summary>
/// Initializes a new instance of the <see cref="MemoryStorage"/> class.
/// </summary>
/// <param name="dictionary">A pre-existing dictionary to use; or null to use a new one.</param>
public MemoryStorage(Dictionary<string, JObject> dictionary = null)
: this(StateJsonSerializer, dictionary)
{
}
/// <summary>
/// Deletes storage items from storage.
/// </summary>
/// <param name="keys">keys of the <see cref="IStoreItem"/> objects to delete.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <seealso cref="ReadAsync(string[], CancellationToken)"/>
/// <seealso cref="WriteAsync(IDictionary{string, object}, CancellationToken)"/>
public Task DeleteAsync(string[] keys, CancellationToken cancellationToken)
{
if (keys == null)
{
throw new ArgumentNullException(nameof(keys));
}
lock (_syncroot)
{
foreach (var key in keys)
{
_memory.Remove(key);
}
}
return Task.CompletedTask;
}
/// <summary>
/// Reads storage items from storage.
/// </summary>
/// <param name="keys">keys of the <see cref="IStoreItem"/> objects to read.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the activities are successfully sent, the task result contains
/// the items read, indexed by key.</remarks>
/// <seealso cref="DeleteAsync(string[], CancellationToken)"/>
/// <seealso cref="WriteAsync(IDictionary{string, object}, CancellationToken)"/>
public Task<IDictionary<string, object>> ReadAsync(string[] keys, CancellationToken cancellationToken)
{
if (keys == null)
{
throw new ArgumentNullException(nameof(keys));
}
var storeItems = new Dictionary<string, object>(keys.Length);
lock (_syncroot)
{
foreach (var key in keys)
{
if (_memory.TryGetValue(key, out var state))
{
if (state != null)
{
storeItems.Add(key, state.ToObject<object>(_stateJsonSerializer));
}
}
}
}
return Task.FromResult<IDictionary<string, object>>(storeItems);
}
/// <summary>
/// Writes storage items to storage.
/// </summary>
/// <param name="changes">The items to write, indexed by key.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <seealso cref="DeleteAsync(string[], CancellationToken)"/>
/// <seealso cref="ReadAsync(string[], CancellationToken)"/>
public Task WriteAsync(IDictionary<string, object> changes, CancellationToken cancellationToken)
{
if (changes == null)
{
throw new ArgumentNullException(nameof(changes));
}
lock (_syncroot)
{
foreach (var change in changes)
{
var newValue = change.Value;
var oldStateETag = default(string);
if (_memory.TryGetValue(change.Key, out var oldState))
{
if (oldState != null && oldState.TryGetValue("eTag", out var etag))
{
oldStateETag = etag.Value<string>();
}
}
var newState = newValue != null ? JObject.FromObject(newValue, _stateJsonSerializer) : null;
// Set ETag if applicable
if (newValue is IStoreItem newStoreItem)
{
if (oldStateETag != null
&&
newStoreItem.ETag != "*"
&&
newStoreItem.ETag != oldStateETag)
{
throw new ArgumentException($"Etag conflict.\r\n\r\nOriginal: {newStoreItem.ETag}\r\nCurrent: {oldStateETag}");
}
newState["eTag"] = (_eTag++).ToString(CultureInfo.InvariantCulture);
}
_memory[change.Key] = newState;
}
}
return Task.CompletedTask;
}
}
}