· 7 years ago · Mar 20, 2018, 05:54 PM
1using Amazon;
2using Amazon.DynamoDBv2;
3using Amazon.DynamoDBv2.Model;
4using Amazon.Runtime;
5using CommonTypes.Behaviours;
6using CommonTypes.Messages;
7using CommonTypes.Settings;
8using Newtonsoft.Json;
9using NoSQL.Behaviours;
10using Polly;
11using System;
12using System.Collections.Generic;
13using System.Linq;
14using System.Threading.Tasks;
15
16namespace NoSQL.DynamoDB
17{
18 public class AWSGameStateStore : IDataStore<GameState>
19 {
20 private AWSSettings _awsSettings;
21 private IAppLogger _appLogger;
22
23 private Policy _retryPolicy;
24 private const string _tableName = "GameState";
25
26 public AWSGameStateStore(IAppLogger appLogger, AWSSettings awsSettings)
27 {
28 _awsSettings = awsSettings;
29 _appLogger = appLogger;
30
31 _retryPolicy = Policy
32 .Handle<Exception>()
33 .WaitAndRetryAsync(3,
34 retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
35 (exception, timeSpan, retryCount, context) =>
36 {
37 var msg = $"AWSGameStateStore Retry - Count:{retryCount}, Exception:{exception.Message}";
38 _appLogger?.LogWarning(msg);
39 }
40 );
41 }
42
43 private AmazonDynamoDBClient GetDynamoClient()
44 {
45 var credentials = new BasicAWSCredentials(
46 accessKey: _awsSettings.NoSQL.AccessKey,
47 secretKey: _awsSettings.NoSQL.SecretKey);
48
49 var config = new AmazonDynamoDBConfig
50 {
51 RegionEndpoint = RegionEndpoint.GetBySystemName(_awsSettings.NoSQL.Region)
52 };
53
54 return new AmazonDynamoDBClient(credentials, config);
55 }
56
57 #region Object Mapping
58
59 private Dictionary<string, AttributeValue> ConvertToDynamoDictionary(GameState item)
60 {
61 return new Dictionary<string, AttributeValue>
62 {
63 {"RecordId", new AttributeValue {S = item.RecordId}},
64 {"PlayerId", new AttributeValue {S = item.PlayerId}},
65 {"CurrentLevel", new AttributeValue {N = item.CurrentLevel.ToString()}},
66 {"Health", new AttributeValue {N = item.Health.ToString()}},
67 {"Inventory", new AttributeValue {M = item.Inventory.ToDictionary(x => x.Key, x => new AttributeValue { S = x.Value })}},
68 {"GameId", new AttributeValue {S = item.GameId}},
69 {"RecordCreatedAt", new AttributeValue {S = item.RecordCreatedAt.ToString()}}
70 };
71 }
72
73 private GameState DynamoToPOCO(Dictionary<string, AttributeValue> item)
74 {
75
76 return new GameState
77 {
78 RecordId = item["RecordId"].S,
79 PlayerId = item["PlayerId"].S,
80 CurrentLevel = Convert.ToInt32(item["CurrentLevel"].N),
81 Health = Convert.ToInt32(item["Health"].N),
82 Inventory = item["Inventory"].M.ToDictionary(x => x.Key, x => x.Value.S),
83 GameId = item["GameId"].S,
84 RecordCreatedAt = DateTime.Parse(item["RecordCreatedAt"].S)
85 };
86 }
87
88 #endregion
89
90 #region Interface Implementation
91 public async Task<GameState> AddEntity(GameState item)
92 {
93 try
94 {
95 await _retryPolicy.ExecuteAsync(async () => {
96 item = await AddGameState(item);
97 });
98 }
99 catch (Exception ex)
100 {
101 _appLogger?.LogError(ex);
102 throw;
103 }
104
105 return item;
106 }
107
108 public async Task<bool> UpdateEntity(GameState item)
109 {
110 bool outcome = false;
111 try
112 {
113 await _retryPolicy.ExecuteAsync(async () => {
114 outcome = await UpdateGameState(item);
115 });
116 }
117 catch (Exception ex)
118 {
119 _appLogger?.LogError(ex);
120 throw;
121 }
122
123 return outcome;
124 }
125
126 public async Task<bool> DeleteEntity(GameState item)
127 {
128 bool outcome = false;
129 try
130 {
131 await _retryPolicy.ExecuteAsync(async () => {
132 outcome = await DeleteGameState(item);
133 });
134 }
135 catch (Exception ex)
136 {
137 _appLogger?.LogError(ex);
138 throw;
139 }
140
141 return outcome;
142 }
143
144 public async Task<GameState> GetEntity(GameState item)
145 {
146 var itemAttributes = await GetGameState(item);
147
148 if (itemAttributes != null)
149 {
150 item = DynamoToPOCO(itemAttributes);
151 }
152
153 return item;
154 }
155
156 public async Task<(List<GameState> list, string nextPageState)> QueryEntity(ICriteria searchCriteria)
157 {
158 var list = new List<GameState>();
159
160 var (collection, nextPageState) = await QueryGameState(searchCriteria);
161
162 foreach (var dict in collection)
163 {
164 list.Add(DynamoToPOCO(dict));
165 }
166
167 return (list, nextPageState);
168 }
169 #endregion
170
171 private async Task<GameState> AddGameState(GameState item)
172 {
173 var client = GetDynamoClient();
174
175 var response = await client.PutItemAsync(
176 tableName: _tableName,
177 item: ConvertToDynamoDictionary(item)
178 );
179
180 return item;
181 }
182
183 private async Task<bool> UpdateGameState(GameState item)
184 {
185 var client = GetDynamoClient();
186
187 var response = await client.UpdateItemAsync(
188 tableName: _tableName,
189 key: new Dictionary<string, AttributeValue>
190 {
191 {"PlayerId", new AttributeValue { S = item.PlayerId.ToString()}},
192 {"RecordCreatedAt", new AttributeValue { S = item.RecordCreatedAt.ToString()}},
193 },
194 attributeUpdates: new Dictionary<string, AttributeValueUpdate>
195 {
196 {"CurrentLevel", new AttributeValueUpdate {Value = new AttributeValue {N = item.CurrentLevel.ToString()} } },
197 {"Health", new AttributeValueUpdate {Value = new AttributeValue {N = item.Health.ToString()} } },
198 {"Inventory", new AttributeValueUpdate {Value = new AttributeValue {M = item.Inventory.ToDictionary(x => x.Key, x => new AttributeValue { S = x.Value })} } }
199 }
200 );
201
202 return true;
203 }
204
205 private async Task<bool> DeleteGameState(GameState item)
206 {
207 var client = GetDynamoClient();
208
209 var response = await client.DeleteItemAsync(
210 tableName: _tableName,
211 key: new Dictionary<string, AttributeValue>
212 {
213 {"PlayerId", new AttributeValue { S = item.PlayerId.ToString()}},
214 {"RecordCreatedAt", new AttributeValue { S = item.RecordCreatedAt.ToString()}},
215 }
216 );
217
218 return true;
219 }
220
221 private async Task<Dictionary<string, AttributeValue>> GetGameState(GameState item)
222 {
223 var client = GetDynamoClient();
224
225 var response = await client.GetItemAsync(
226 tableName: _tableName,
227 key: new Dictionary<string, AttributeValue>
228 {
229 {"PlayerId", new AttributeValue { S = item.PlayerId}},
230 {"RecordCreatedAt", new AttributeValue { S = item.RecordCreatedAt.ToString() }}
231 }
232 );
233
234 return response.Item;
235 }
236
237 private async Task<(List<Dictionary<string, AttributeValue>> Items, string NextPageState)> QueryGameState(ICriteria searchCriteria)
238 {
239 // Create our query
240 var queryRequest = new QueryRequest
241 {
242 Limit = searchCriteria.PageSize,
243 TableName = _tableName,
244 FilterExpression = "CurrentLevel >= :v_CurrentLevel",
245 KeyConditionExpression = "PlayerId = :v_PlayerId",
246 ExpressionAttributeValues = new Dictionary<string, AttributeValue> {
247 {":v_PlayerId", new AttributeValue { S = searchCriteria.SearchFields["PlayerId"] }},
248 {":v_CurrentLevel", new AttributeValue { N = searchCriteria.SearchFields["CurrentLevel"] }}
249 },
250 ProjectionExpression = "RecordId, PlayerId, CurrentLevel, Health, Inventory, GameId, RecordCreatedAt",
251 ConsistentRead = true
252 };
253
254 // Use NextPageState if specified
255 if (!string.IsNullOrEmpty(searchCriteria.NextPageState))
256 {
257 queryRequest.ExclusiveStartKey = JsonConvert.DeserializeObject<Dictionary<string, AttributeValue>>(searchCriteria.NextPageState);
258 }
259
260 // Run the query
261 var client = GetDynamoClient();
262 var response = await client.QueryAsync(queryRequest);
263
264 // Check for next page token
265 var nextPageState = response.Items.Count == searchCriteria.PageSize ? JsonConvert.SerializeObject(response.LastEvaluatedKey) : null;
266
267 // Return the tuple
268 return (response.Items, nextPageState);
269 }
270 }
271}