@@ -40,20 +40,20 @@ using TestDynamo;
4040[Test ]
4141public async Task GetPersonById_WithValidId_ReturnsPerson ()
4242{
43- // arrange
44- using var client = TestDynamoClient .CreateClient <AmazonDynamoDBClient >();
43+ // arrange
44+ using var client = TestDynamoClient .CreateClient <AmazonDynamoDBClient >();
4545
46- // create a table and add some items
47- await client .CreateTableAsync (.. .);
48- await client .BatchWriteItemAsync (.. .);
46+ // create a table and add some items
47+ await client .CreateTableAsync (.. .);
48+ await client .BatchWriteItemAsync (.. .);
4949
50- var testSubject = new MyBeatlesService (client );
50+ var testSubject = new MyBeatlesService (client );
5151
52- // act
53- var beatle = testSubject .GetBeatle (" Ringo" );
52+ // act
53+ var beatle = testSubject .GetBeatle (" Ringo" );
5454
55- // assert
56- Assert .Equal (" Starr" , beatle .SecondName );
55+ // assert
56+ Assert .Equal (" Starr" , beatle .SecondName );
5757}
5858```
5959
@@ -108,8 +108,11 @@ TestDynamo is written in F# and has a lot of F# first constructs
108108``` F#
109109open TestDynamo
110110
111+ let buildBasicClient = TestDynamoClient.createClient<AmazonDynamoDBClient> ValueNone false ValueNone false
111112use db = new Api.FSharp.Database({ regionId = "us-west-1" })
112- use client = ValueSome db |> TestDynamoClient.createClient ValueNone false ValueNone false
113+ use client =
114+ ValueSome db
115+ |> buildBasicClient
113116```
114117
115118In general, functions and extension methods with in ` camelCase ` are targeted at F#, where as those is ` PascalCase ` are targeted at C#
@@ -125,16 +128,38 @@ using var database = new Api.Database(new DatabaseId("us-west-1"));
125128
126129// add a table
127130database
128- .TableBuilder (" Beatles" , (" FirstName" , " S" ))
129- .WithGlobalSecondaryIndex (" SecondNameIndex" , (" SecondName" , " S" ), (" FirstName" , " S" ))
130- .AddTable ();
131+ .TableBuilder (" Beatles" , (" FirstName" , " S" ))
132+ .WithGlobalSecondaryIndex (" SecondNameIndex" , (" SecondName" , " S" ), (" FirstName" , " S" ))
133+ .AddTable ();
131134
132135// add some data
133136database
134- .ItemBuilder (" Beatles" )
135- .Attribute (" FirstName" , " Ringo" )
136- .Attribute (" SecondName" , " Starr" )
137- .AddItem ();
137+ .ItemBuilder (" Beatles" )
138+ .Attribute (" FirstName" , " Ringo" )
139+ .Attribute (" SecondName" , " Starr" )
140+ .AddItem ();
141+ ```
142+
143+ F# databases are supported also
144+
145+ ``` F#
146+ open TestDynamo
147+
148+ use database = new Api.FSharp.Database({ regionId = "us-west-1" });
149+
150+ // add a table
151+ database
152+ |> TableBuilder.create "Beatles" struct ("FirstName", "S") ValueNone
153+ |> TableBuilder.withGlobalSecondaryIndex "SecondNameIndex" struct ("SecondName", "S") (ValueSome struct ("FirstName", "S")) ValueNone false
154+ |> TableBuilder.addTable ValueNone
155+
156+ // add some data
157+ Map.empty
158+ |> Map.add "FirstName" (String "Ringo")
159+ |> Map.add "SecondName" (String "Starr")
160+ |> ItemBuilder.putRequest "Beatles"
161+ |> database.Put ValueNone
162+ |> ignore
138163```
139164
140165### Database Cloning
@@ -150,37 +175,37 @@ private static Api.Database _sharedRootDatabase = BuildDatabase();
150175
151176private static Api .Database BuildDatabase ()
152177{
153- var database = new Api .Database (new DatabaseId (" us-west-1" ));
154-
155- // add a table
156- database
157- .TableBuilder (" Beatles" , (" FirstName" , " S" ))
158- .WithGlobalSecondaryIndex (" SecondNameIndex" , (" SecondName" , " S" ), (" FirstName" , " S" ))
159- .AddTable ();
160-
161- // add some data
162- database
163- .ItemBuilder (" Beatles" )
164- .Attribute (" FirstName" , " Ringo" )
165- .Attribute (" SecondName" , " Starr" )
166- .AddItem ();
167-
168- return database ;
178+ var database = new Api .Database (new DatabaseId (" us-west-1" ));
179+
180+ // add a table
181+ database
182+ .TableBuilder (" Beatles" , (" FirstName" , " S" ))
183+ .WithGlobalSecondaryIndex (" SecondNameIndex" , (" SecondName" , " S" ), (" FirstName" , " S" ))
184+ .AddTable ();
185+
186+ // add some data
187+ database
188+ .ItemBuilder (" Beatles" )
189+ .Attribute (" FirstName" , " Ringo" )
190+ .Attribute (" SecondName" , " Starr" )
191+ .AddItem ();
192+
193+ return database ;
169194}
170195
171196[Test ]
172197public async Task TestSomething ()
173198{
174- // clone the database to get working copy
175- // without altering the original
176- using var database = _sharedRootDatabase .Clone ();
177- using var client = database .CreateClient <AmazonDynamoDBClient >();
199+ // clone the database to get working copy
200+ // without altering the original
201+ using var database = _sharedRootDatabase .Clone ();
202+ using var client = database .CreateClient <AmazonDynamoDBClient >();
178203
179- // act
180- .. .
204+ // act
205+ .. .
181206
182- // assert
183- .. .
207+ // assert
208+ .. .
184209}
185210```
186211
@@ -203,6 +228,22 @@ var ringo = database
203228 .Single (v => v [" FirstName" ].S == " Ringo" );
204229```
205230
231+ Or with F#
232+
233+ ``` F#
234+ use database = GetMeADatabase()
235+
236+ let ringo =
237+ database.GetTable ValueNone "Beatles"
238+ |> LazyDebugTable.getValues ValueNone
239+ |> Seq.filter (
240+ _.InternalItem
241+ >> Item.attributes
242+ >> Map.find "FirstName"
243+ >> (=) (String "Ringo"))
244+ |> Seq.head
245+ ```
246+
206247### Streaming and Subscriptions
207248
208249If streams are enabled on tables they can be used for global table
@@ -219,19 +260,55 @@ using TestDynamo.Lambda;
219260using Amazon .Lambda .DynamoDBEvents ;
220261
221262var subscription = database .AddSubscription <DynamoDBEvent >(
222- " Beatles" ,
223- (dynamoDbStreamsEvent , cancellationToken ) =>
224- {
225- var added = dynamoDbStreamsEvent .Records .FirstOrDefault ()? .Dynamodb .NewImage ? [" FirstName" ]? .S ;
226- if (added != null )
227- Console .WriteLine ($" {added } has joined the Beatles" );
263+ " Beatles" ,
264+ (dynamoDbStreamsEvent , cancellationToken ) =>
265+ {
266+ var added = dynamoDbStreamsEvent .Records .FirstOrDefault ()? .Dynamodb .NewImage ? [" FirstName" ]? .S ;
267+ if (added != null )
268+ Console .WriteLine ($" {added } has joined the Beatles" );
228269
229- var removed = dynamoDbStreamsEvent .Records .FirstOrDefault ()? .Dynamodb .OldImage ? [" FirstName" ]? .S ;
230- if (removed != null )
231- Console .WriteLine ($" {removed } has left the Beatles" );
270+ var removed = dynamoDbStreamsEvent .Records .FirstOrDefault ()? .Dynamodb .OldImage ? [" FirstName" ]? .S ;
271+ if (removed != null )
272+ Console .WriteLine ($" {removed } has left the Beatles" );
232273
233- return default ;
234- });
274+ return default ;
275+ });
276+
277+ // disposing will remove the subscription
278+ subscription .Dispose ();
279+ ```
280+
281+ Or with F#
282+
283+ ``` F#
284+ open TestDynamo
285+ open TestDynamo.Lambda
286+ open Amazon.Lambda.DynamoDBEvents
287+
288+ let subscriber (dynamoDbStreamsEvent: DynamoDBEvent) _ =
289+
290+ let tryFirst f =
291+ dynamoDbStreamsEvent.Records
292+ |> Seq.map f
293+ |> Seq.filter ((<>) null)
294+ |> Seq.map (fun (x: Dictionary<string, AttributeValue>) -> x["FirstName"].S)
295+ |> Seq.tryHead
296+
297+ match tryFirst _.Dynamodb.NewImage with
298+ | Some x -> printf "%s has joined the Beatles" x
299+ | None -> ()
300+
301+ match tryFirst _.Dynamodb.OldImage with
302+ | Some x -> printf "%s has left the Beatles" x
303+ | None -> ()
304+
305+ Unchecked.defaultOf<_>
306+
307+ let subscription =
308+ Subscriptions.addSubscription
309+ (SubscriptionDetails.ofTableName "Beatles")
310+ subscriber
311+ database
235312
236313// disposing will remove the subscription
237314subscription.Dispose();
@@ -241,28 +318,28 @@ Subscribe to raw changes
241318
242319``` C#
243320var subscription = database
244- .SubscribeToStream (" Beatles" , (cdcPacket , cancellationToken ) =>
245- {
246- var added = cdcPacket .data .packet .changeResult .OrderedChanges
247- .Select (x => x .Put )
248- .Where (x => x .IsSome )
249- .Select (x => x .Value [" FirstName" ].S )
250- .FirstOrDefault ();
321+ .SubscribeToStream (" Beatles" , (cdcPacket , cancellationToken ) =>
322+ {
323+ var added = cdcPacket .data .packet .changeResult .OrderedChanges
324+ .Select (x => x .Put )
325+ .Where (x => x .IsSome )
326+ .Select (x => x .Value [" FirstName" ].S )
327+ .FirstOrDefault ();
251328
252- if (added != null )
253- Console .WriteLine ($" {added } has joined the Beatles" );
329+ if (added != null )
330+ Console .WriteLine ($" {added } has joined the Beatles" );
254331
255- var removed = cdcPacket .data .packet .changeResult .OrderedChanges
256- .Select (x => x .Deleted )
257- .Where (x => x .IsSome )
258- .Select (x => x .Value [" FirstName" ].S )
259- .FirstOrDefault ();
332+ var removed = cdcPacket .data .packet .changeResult .OrderedChanges
333+ .Select (x => x .Deleted )
334+ .Where (x => x .IsSome )
335+ .Select (x => x .Value [" FirstName" ].S )
336+ .FirstOrDefault ();
260337
261- if (removed != null )
262- Console .WriteLine ($" {removed } has left the Beatles" );
338+ if (removed != null )
339+ Console .WriteLine ($" {removed } has left the Beatles" );
263340
264- return default ;
265- });
341+ return default ;
342+ });
266343
267344// disposing will remove the subscription
268345subscription .Dispose ();
@@ -298,8 +375,8 @@ using var client = TestDynamoClient.CreateClient<AmazonDynamoDBClient>();
298375using var context = new DynamoDbContext (client )
299376await context .SaveAsync (new Beatle
300377{
301- FirstName = " Ringo" ,
302- SecondName = " Starr"
378+ FirstName = " Ringo" ,
379+ SecondName = " Starr"
303380})
304381```
305382
@@ -429,6 +506,23 @@ using var db2 = DatabaseSerializer.Database.FromFile(@"TestData.json");
429506var json = DatabaseSerializer .GlobalDatabase .ToString (globalDb );
430507```
431508
509+ Or F#
510+
511+ ``` F#
512+ open TestDynamo
513+ open TestDynamo.Serialization
514+
515+ use db1 = new Api.FSharp.Database()
516+ ... populate database
517+
518+ DatabaseSerializer.FSharp.Database.ToFile(db1, @"TestData.json")
519+
520+ use db2 = DatabaseSerializer.FSharp.Database.FromFile(@"TestData.json")
521+
522+ // there are also tools to serialize and deserialze global databases
523+ let json = DatabaseSerializer.FSharp.GlobalDatabase.ToString(globalDb)
524+ ```
525+
432526Serialization is designed to share data between test runs, but ultimately, it scales with the number of items in the database. This means
433527that it may take more time than is ideal for executing fast unit tests. [ Database cloning] ( #database-cloning ) is a better solution for large databases which are shared between multiple tests, as it executes instantly for any sized database or global database
434528
@@ -448,6 +542,22 @@ using var database = await CloudFormationParser.BuildDatabase(new[] { cfnFile1,
448542.. .
449543```
450544
545+ Or with F#
546+
547+ ``` F#
548+ open TestDynamo.Serialization;
549+
550+ async {
551+ use! database =
552+ [ { region = "eu-north-1"
553+ fileJson = File.ReadAllText("myTemplate1.json") }
554+ { region = "us-west-2"
555+ fileJson = File.ReadAllText("myTemplate2.json") } ]
556+ |> CloudFormationParser.buildDatabase { ignoreUnsupportedResources = true } ValueNone
557+ ...
558+ }
559+ ```
560+
451561### Locking and Atomic transactions
452562
453563Test dynamo is more consistant than DynamoDb. In general, all operations on a single database (region) are atomic.
@@ -478,12 +588,12 @@ using var client = TestDynamoClient.CreateClient<AmazonDynamoDBClient>(recordCal
478588await client .CreateTableAsync (.. .);
479589try
480590{
481- // failed request to put an item
482- await client .PutItemAsync (.. .);
591+ // failed request to put an item
592+ await client .PutItemAsync (.. .);
483593}
484594catch
485595{
486- // do nothing
596+ // do nothing
487597 }
488598
489599
@@ -597,20 +707,20 @@ using var database = new Api.Database(new DatabaseId("us-west-1"));
597707var interceptor = new CreateBackupInterceptor (backups );
598708using var client = database .CreateClient <AmazonDynamoDBClient >(interceptor );
599709
600- // execute some requests which are not intercepted. These will not be intercepted
710+ // execute some requests which are not intercepted
601711await client .PutItemAsync (.. .);
602712await client .PutItemAsync (.. .);
603713
604714// create a backup. This will be intercepted
605715var backupResponse = await client .CreateBackupAsync (new CreateBackupRequest
606716{
607- TableName = " Beatles"
717+ TableName = " Beatles"
608718});
609719
610720// restore from backup. This will be intercepted
611721await client .RestoreTableFromBackupAsync (new RestoreTableFromBackupRequest
612722{
613- BackupArn = backupResponse .BackupDetails .BackupArn
723+ BackupArn = backupResponse .BackupDetails .BackupArn
614724});
615725```
616726
0 commit comments