-
Notifications
You must be signed in to change notification settings - Fork 23
/
Copy pathDataContext.cs
299 lines (252 loc) · 10.9 KB
/
DataContext.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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using System.Reflection;
using Amazon;
using Amazon.DynamoDBv2.DataModel;
using Amazon.DynamoDBv2.DocumentModel;
using Linq2DynamoDb.DataContext.Caching;
#if AWSSDK_1_5
using IAmazonDynamoDB = Amazon.DynamoDBv2.AmazonDynamoDB;
#else
using Amazon.DynamoDBv2;
#endif
namespace Linq2DynamoDb.DataContext
{
/// <summary>
/// The root object in the whole hierarchy
/// </summary>
public partial class DataContext
{
#region ctors
/// <summary>
/// This ctor takes an AmazonDynamoDBClient, that will be used to load table metadata from DynamoDb
/// </summary>
public DataContext(IAmazonDynamoDB client, string tableNamePrefix)
{
this._client = client;
this._tableNamePrefix = tableNamePrefix;
}
/// <summary>
/// This ctor is supposed to be used with GetTable() method, that takes a pre-loaded Table object
/// </summary>
public DataContext(string tableNamePrefix)
{
this._tableNamePrefix = tableNamePrefix;
}
/// <summary>
/// Initializes a new instance of the Linq2DynamoDb.DataContext.DataContext class. If a table
/// prefix is specified in config, it will be picked up automatically, rather than explicitly
/// defining it in the constructor.
/// </summary>
/// <param name="client"> Returns an AmazonDynamoDb instance passed via ctor. </param>
public DataContext(IAmazonDynamoDB client)
{
this._client = client;
this._tableNamePrefix = AWSConfigsDynamoDB.Context.TableNamePrefix;
}
#endregion
#region Events
/// <summary>
/// Occurs when DataContext wants to log some debugging info
/// </summary>
public event Action<string> OnLog;
#endregion
#region Public Properties
/// <summary>
/// Returns an AmazonDynamoDb instance passed via ctor
/// </summary>
public IAmazonDynamoDB Client { get { return this._client; } }
#endregion
#region Public Methods
/// <summary>
/// Creates an instance of DataTable class for the specified entity.
/// </summary>
public DataTable<TEntity> GetTable<TEntity>()
{
return this.GetTable<TEntity>(null, null);
}
/// <summary>
/// Creates an instance of DataTable class for the specified entity
/// with predefined HashKey value
/// </summary>
public DataTable<TEntity> GetTable<TEntity>(object hashKeyValue)
{
return this.GetTable<TEntity>(hashKeyValue, null);
}
/// <summary>
/// Creates an instance of DataTable class for the specified entity.
/// The table contents and queries will be cached using
/// the provided ITableCache implementation
/// </summary>
public DataTable<TEntity> GetTable<TEntity>(Func<ITableCache> cacheImplementationFactory)
{
return this.GetTable<TEntity>(null, cacheImplementationFactory);
}
/// <summary>
/// Creates an instance of DataTable class for the specified entity
/// with predefined HashKey value. The table contents and queries will be cached using
/// the provided ITableCache implementation
/// </summary>
public DataTable<TEntity> GetTable<TEntity>(object hashKeyValue, Func<ITableCache> cacheImplementationFactory)
{
return this.GetTable<TEntity>(hashKeyValue, cacheImplementationFactory, this.GetTableDefinition(typeof(TEntity)));
}
/// <summary>
/// Creates an instance of DataTable class for the specified entity
/// with predefined HashKey value. The table contents and queries will be cached using
/// the provided ITableCache implementation.
/// The provided AWS SDK's Table object is used for all communications with DynamoDb.
/// </summary>
public DataTable<TEntity> GetTable<TEntity>(object hashKeyValue, Func<ITableCache> cacheImplementationFactory, Table tableDefinition)
{
var entityType = typeof(TEntity);
var tableWrapper = TableWrappers.GetOrAdd
(
new Tuple<Type, object>(entityType, hashKeyValue),
t =>
{
// if cache is not provided, then passing a fake implementation
var cacheImplementation =
cacheImplementationFactory == null
?
FakeCacheImplementation
:
cacheImplementationFactory();
var wrapper = new TableDefinitionWrapper
(
tableDefinition,
entityType,
hashKeyValue,
cacheImplementation,
this.ConsistentRead
);
wrapper.OnLog += this.OnLog;
return wrapper;
}
);
if (tableWrapper.TableDefinition != tableDefinition)
{
throw new InvalidOperationException("You shouldn't pass different Table instances for the same entityType/hashKeyValue pair");
}
// this is to avoid situation, when the same DataContext instance is being used for table creation and then for cached data access.
if
(
(tableWrapper.Cache == FakeCacheImplementation)
&&
(cacheImplementationFactory != null)
)
{
throw new InvalidOperationException("You're trying to get a DataTable<T> instance first without caching and then with cache implementation specified. This is not supported. For creating DynamoDb tables, please, use another separate instance of DataContext");
}
return new DataTable<TEntity>(tableWrapper);
}
/// <summary>
/// Saves all modifications to DynamoDb and to cache, if one is used
/// </summary>
public void SubmitChanges()
{
Task.WaitAll(this.TableWrappers.Values.Select(t => t.SubmitChangesAsync()).ToArray());
}
/// <summary>
/// Asynchronously saves all modifications to DynamoDb and to cache, if one is used
/// </summary>
public Task SubmitChangesAsync()
{
return Task.WhenAll(this.TableWrappers.Values.Select(t => t.SubmitChangesAsync()).ToArray());
}
#endregion
#region Private Properties
/// <summary>
/// It's possible to set this flag in child's constructor. But in most cases it's not necessary!
/// That's because when using caching, the data read from DynamoDb never substitutes data in cache.
/// And data in cache is always consistent.
/// And don't forget, that consistent reads are supported only by Get/Query operations.
/// Scan is always inconsistent, so this flag is skipped when making a scan anyway.
/// </summary>
protected bool ConsistentRead = false;
private readonly IAmazonDynamoDB _client;
private readonly string _tableNamePrefix;
/// <summary>
/// TableDefinitionWrapper instances for each entity type and HashKey value (if specified)
/// </summary>
protected internal readonly ConcurrentDictionary<Tuple<Type, object>, TableDefinitionWrapper> TableWrappers = new ConcurrentDictionary<Tuple<Type, object>, TableDefinitionWrapper>();
/// <summary>
/// A fake cache implementation, which does no caching
/// </summary>
private static readonly ITableCache FakeCacheImplementation = new FakeTableCache();
private class CachedTableDefinitions : ConcurrentDictionary<string, Table>
{
/// <summary>
/// Instead of storing a reference to DynamoDBClient we're storing it's HashCode
/// </summary>
private readonly int _dynamoDbClientHashCode;
public CachedTableDefinitions(IAmazonDynamoDB client)
{
this._dynamoDbClientHashCode = client.GetHashCode();
}
public bool IsAssignedToThisClientInstance(IAmazonDynamoDB client)
{
return this._dynamoDbClientHashCode == client.GetHashCode();
}
}
/// <summary>
/// Table objects are cached here per DynamoDBClient instance.
/// In order not to reload table metadata between DataContext instance creations.
/// </summary>
private static CachedTableDefinitions _cachedTableDefinitions;
#endregion
#region Private Methods
/// <summary>
/// Gets a full table name for a table entity type
/// </summary>
internal string GetTableNameForType(Type entityType)
{
string fullTableName;
var entityAttributes = entityType.GetTypeInfo().GetCustomAttributes(typeof(DynamoDBTableAttribute), true);
if (entityAttributes.Any())
{
fullTableName = this._tableNamePrefix + ((DynamoDBTableAttribute)entityAttributes.First()).TableName;
}
else
{
fullTableName = this._tableNamePrefix + entityType.Name;
}
return fullTableName;
}
internal void Log(string format, params object[] args)
{
var handler = this.OnLog;
if (handler == null)
{
return;
}
handler("DataContext : " + string.Format(format, args));
}
/// <summary>
/// Loads an AWS SDK's Table object, which is used for all get/query/scan/update operations
/// </summary>
private Table GetTableDefinition(Type entityType)
{
if (this._client == null)
{
throw new InvalidOperationException("An instance of AmazonDynamoDbClient was not provided. Use either a ctor, that takes AmazonDynamoDbClient instance or GetTable() method, that takes Table object");
}
var cachedTableDefinitions = _cachedTableDefinitions;
if
(
(cachedTableDefinitions == null)
||
(!cachedTableDefinitions.IsAssignedToThisClientInstance(this._client))
)
{
cachedTableDefinitions = new CachedTableDefinitions(this._client);
_cachedTableDefinitions = cachedTableDefinitions;
}
string tableName = this.GetTableNameForType(entityType);
return cachedTableDefinitions.GetOrAdd(tableName, name => Table.LoadTable(this._client, name));
}
#endregion
}
}