{"id":24268271,"url":"https://github.com/fsprojects/testdynamo","last_synced_at":"2026-04-21T13:04:38.953Z","repository":{"id":272472830,"uuid":"916704907","full_name":"fsprojects/TestDynamo","owner":"fsprojects","description":"An in-memory dynamodb client for automated testing","archived":false,"fork":false,"pushed_at":"2025-03-17T17:00:29.000Z","size":1437,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-12-06T14:24:04.086Z","etag":null,"topics":["database","dynamodb","fsharp","testing-tools"],"latest_commit_sha":null,"homepage":"","language":"F#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fsprojects.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-01-14T16:01:56.000Z","updated_at":"2025-04-27T20:14:26.000Z","dependencies_parsed_at":"2025-01-14T18:13:58.016Z","dependency_job_id":"c77614f2-2d06-41c9-b2c4-58e2c4cded73","html_url":"https://github.com/fsprojects/TestDynamo","commit_stats":null,"previous_names":["fsprojects/testdynamo"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/fsprojects/TestDynamo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fsprojects%2FTestDynamo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fsprojects%2FTestDynamo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fsprojects%2FTestDynamo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fsprojects%2FTestDynamo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fsprojects","download_url":"https://codeload.github.com/fsprojects/TestDynamo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fsprojects%2FTestDynamo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32093157,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-21T11:25:29.218Z","status":"ssl_error","status_checked_at":"2026-04-21T11:25:28.499Z","response_time":128,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["database","dynamodb","fsharp","testing-tools"],"created_at":"2025-01-15T13:34:37.251Z","updated_at":"2026-04-21T13:04:38.897Z","avatar_url":"https://github.com/fsprojects.png","language":"F#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TestDynamo\n\nAn in-memory dynamodb client for automated testing\n\n[![Made with F#](https://img.shields.io/badge/Made%20with-FSharp-rgb(184,69,252).svg)](https://fsharp.org/)\n[![Build Status](https://github.com/fsprojects/TestDynamo/actions/workflows/dotnet2.yml/badge.svg)](https://github.com/fsprojects/TestDynamo/actions)\n[![Code coverage](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffsprojects%2FTestDynamo%2Frefs%2Fheads%2Fmain%2FautomatedBuildResults.json\u0026query=%24.projects.TestDynamo.coverage\u0026label=Code%20Coverage\u0026color=green \"Code coverage\")](https://github.com/fsprojects/TestDynamo/blob/main/tests/coverage.ps1)\n[![Github](https://img.shields.io/badge/Github-TestDynamo-black \"Github\")](https://github.com/fsprojects/TestDynamo)\n[![NuGet](https://img.shields.io/badge/NuGet-TestDynamo-blue \"NuGet\")](https://www.nuget.org/packages/TestDynamo)\n[![NuGet](https://img.shields.io/badge/NuGet-TestDynamo.Serialization-blue \"NuGet\")](https://www.nuget.org/packages/TestDynamo.Serialization)\n[![NuGet](https://img.shields.io/badge/NuGet-TestDynamo.Lambda-blue \"NuGet\")](https://www.nuget.org/packages/TestDynamo.Lambda)\n\nTestDynamo is a rewrite of dynamodb in dotnet designed for testing and debugging. \nIt implements a partial feature set of `IAmazonDynamoDb` to manage schemas and read and write items.\n\n * [Core features](https://github.com/fsprojects/TestDynamo/blob/main/Features.md)\n    * Table management (Create/Update/Delete table)\n    * Index management (Create/Update/Delete index)\n    * Item operations (Put/Delete/Update etc)\n    * Queries and Scans\n    * Batching and Transactional writes\n * Document model and `DynamoDBContext`\n * Multi region setups\n * Global tables and replication\n * Streams and stream subscribers\n * Efficient cloning and deep copying of databases for isolation\n * Full database serialization and deserialization for data driven testing\n * Basic cloudformation template support for creating Tables and GlobalTables\n * Mocking utils\n\n## Installation\n\n * Core functionality: `dotnet add package TestDynamo`\n * Add lambda support: `dotnet add package TestDynamo.Lambda`\n * Add serialization or cloud formation support: `dotnet add package TestDynamo.Serialization`\n\n## The basics\n\n```C#\nusing TestDynamo;\n\n[Test]\npublic async Task GetPersonById_WithValidId_ReturnsPerson()\n{\n    // arrange\n    using var client = TestDynamoClient.CreateClient\u003cAmazonDynamoDBClient\u003e();\n\n    // create a table and add some items\n    await client.CreateTableAsync(...);\n    await client.BatchWriteItemAsync(...);\n\n    var testSubject = new MyBeatlesService(client);\n\n    // act\n    var beatle = testSubject.GetBeatle(\"Ringo\");\n\n    // assert\n    Assert.Equal(\"Starr\", beatle.SecondName);\n}\n```\n\n## The details\n\nTestDynamo has a suite of features and components to model a dynamodb environment and simplify the process of writing tests.\n\n * Support for [DynamoDb versions](https://www.nuget.org/packages/AWSSDK.DynamoDBv2#versions-body-tab) \u003e= 3.5.0\n * [`Api.Database`](#database) contains tables from a single region.\n    * [F# Support](#f-database) out of the box\n    * [Full expression engine](#using-expressions) so you can test your queries, scans, projections and conditions\n    * [Schema and item change](#schema-and-item-change) tools make creating and populating test databases easier\n    * [Database cloning](#database-cloning) allows you to make copies of entire AWS regions which can be safely used in other tests.\n    * [Debug properties](#debug-properties) are optimized for reading in a debugger.\n    * [Test query tools](#test-tools) to get data in and out of the database with as little code as possible\n    * [Streaming and Subscriptions](#streaming-and-subscriptions) can model lambdas subscribed to dynamodb streams\n * [`DynamoDBContext`](#dynamodbcontext) works out of the box\n * [`Api.GlobalDatabase`](#global-database) models an AWS account spread over multiple regions. It contains `Api.Database`s.\n    * [Global databases](#global-database-cloning) can be cloned too\n * [`TestDynamoClient`](#testdynamoclient) is the entry point for linking a database to a `AmazonDynamoDBClient`.\n    * Check the [features](https://github.com/fsprojects/TestDynamo/blob/main/Features.md) for a full list of endpoints, request args and responses that are supported.\n * [`DatabaseSerializer`](#database-serializers) is a json serializer/deserializer for entire databases and global databases.\n * [Cloud Formation Templates](#cloud-formation-templates) can be consumed to to initialize databases and global databases\n * [Locking and atomic transactions](#locking-and-atomic-transactions)\n * [Transact write ClientRequestToken](#transact-write-clientrequesttoken)\n * [Recorders](#recorders) can be used to record the activity on a client which can be asserted on later\n * [Interceptors](#interceptors) can be used to modify the functionality of the database, either to add more traditional mocking or to polyfill unsupported features\n * [Logging](#logging) can be configured at the database level or the `AmazonDynamoDBClient` level\n\n### Using Expressions\n\nAll [dynamodb expression types](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.html) are supported\n\n### Database\n\nThe database is the core of TestDynamo and it models a single region and it's tables. The database \nis both fast and lightweight, built from simple data structures.\n\nDatabases can be injected into an `AmazonDynamoDBClient` to then be passed into a test\n\n```C#\nusing TestDynamo;\n\nusing var db = new Api.Database(new DatabaseId(\"us-west-1\"));\nusing var client = db.CreateClient\u003cAmazonDynamoDBClient\u003e();\n```\n\n### F# Database\n\nTestDynamo is written in F# and has a lot of F# first constructs\n\n```F#\nopen TestDynamo\n\nlet buildBasicClient = TestDynamoClient.createClient\u003cAmazonDynamoDBClient\u003e ValueNone false ValueNone false\nuse db = new Api.FSharp.Database({ regionId = \"us-west-1\" })\nuse client = \n    ValueSome db \n    |\u003e buildBasicClient\n```\n\nIn general, functions and extension methods in `camelCase` are targeted at F#, where as those is `PascalCase` are targeted at C#\n\n### Schema and Item Change\n\nDatabases have some convenience methods to make adding tables and items easier\n\n```C#\nusing TestDynamo;\n\nusing var database = new Api.Database(new DatabaseId(\"us-west-1\"));\n\n// add a table\ndatabase\n    .TableBuilder(\"Beatles\", (\"FirstName\", \"S\"))\n    .WithGlobalSecondaryIndex(\"SecondNameIndex\", (\"SecondName\", \"S\"), (\"FirstName\", \"S\"))\n    .AddTable();\n\n// add some data\ndatabase\n    .ItemBuilder(\"Beatles\")\n    .Attribute(\"FirstName\", \"Ringo\")\n    .Attribute(\"SecondName\", \"Starr\")\n    .AddItem();\n```\n\nF# databases are supported also\n\n```F#\nopen TestDynamo\n\nuse database = new Api.FSharp.Database({ regionId = \"us-west-1\" });\n\n// add a table\ndatabase\n|\u003e TableBuilder.create \"Beatles\" struct (\"FirstName\", \"S\") ValueNone\n|\u003e TableBuilder.withGlobalSecondaryIndex \"SecondNameIndex\" struct (\"SecondName\", \"S\") (ValueSome struct (\"FirstName\", \"S\")) ValueNone false\n|\u003e TableBuilder.addTable ValueNone\n\n// add some data\nMap.empty\n|\u003e Map.add \"FirstName\" (String \"Ringo\")\n|\u003e Map.add \"SecondName\" (String \"Starr\")\n|\u003e ItemBuilder.putRequest \"Beatles\"\n|\u003e database.Put ValueNone\n|\u003e ignore\n```\n\n### Database Cloning\n\nDatabases are built for cloning. This allows you to create and populate a database once, and then clone it to be used \nin any test with full isolation. TestDynamo uses immutable data structures for most things, which means that nothing is \nactually copied during a clone so cloning has almost no overhead.\n\n```C#\nusing TestDynamo;\n\nprivate static Api.Database _sharedRootDatabase = BuildDatabase();\n\nprivate static Api.Database BuildDatabase()\n{\n    var database = new Api.Database(new DatabaseId(\"us-west-1\"));\n\n    // add a table\n    database\n        .TableBuilder(\"Beatles\", (\"FirstName\", \"S\"))\n        .WithGlobalSecondaryIndex(\"SecondNameIndex\", (\"SecondName\", \"S\"), (\"FirstName\", \"S\"))\n        .AddTable();\n\n    // add some data\n    database\n        .ItemBuilder(\"Beatles\")\n        .Attribute(\"FirstName\", \"Ringo\")\n        .Attribute(\"SecondName\", \"Starr\")\n        .AddItem();\n\n    return database;\n}\n\n[Test]\npublic async Task TestSomething()\n{\n    // clone the database to get working copy\n    // without altering the original\n    using var database = _sharedRootDatabase.Clone();\n    using var client = database.CreateClient\u003cAmazonDynamoDBClient\u003e();\n\n    // act\n    ...\n\n    // assert\n    ...\n}\n```\n\n### Debug Properties\n\nUse the handy debug properties on `Api.Database` and `Api.GlobalDatabase` in your debugger of choice\n\n![https://raw.githubusercontent.com/fsprojects/TestDynamo/refs/heads/main/Docs/DbDebugger.png](./Docs/DbDebugger.png \"Debugger\")\n\n### Test Tools\n\nUse query tools to find items in a table\n\n```C#\nusing var database = GetMeADatabase();\n\nvar ringo = database\n    .GetTable(\"Beatles\")\n    .GetValues()\n    .Single(v =\u003e v[\"FirstName\"].S == \"Ringo\");\n```\n\nOr with F#\n\n```F#\nuse database = GetMeADatabase()\n\nlet ringo = \n    database.GetTable ValueNone \"Beatles\"\n    |\u003e LazyDebugTable.getValues ValueNone\n    |\u003e Seq.filter (\n        _.InternalItem \n        \u003e\u003e Item.attributes \n        \u003e\u003e Map.find \"FirstName\" \n        \u003e\u003e (=) (String \"Ringo\"))\n    |\u003e Seq.head\n```\n\n### Streaming and Subscriptions\n\nIf streams are enabled on tables they can be used for global table \nreplication and custom subscribers. TestDynamo differs from dynamodb as follows\n\n * There is no limit to the number of subscribers that you can have on a stream\n * Strem settings (e.g. `NEW_AND_OLD_IMAGES`) are configured per subscriber. If these values are set on a stream they will be ignored\n\nTo subscribe to changes with a lambda stream subscription syntax you can import the `TestDynamo.Lambda` package from nuget\n\n```C#\nusing TestDynamo;\nusing TestDynamo.Lambda;\nusing Amazon.Lambda.DynamoDBEvents;\n\nvar subscription = database.AddSubscription\u003cDynamoDBEvent\u003e(\n    \"Beatles\",\n    (dynamoDbStreamsEvent, cancellationToken) =\u003e\n    {\n        var added = dynamoDbStreamsEvent.Records.FirstOrDefault()?.Dynamodb.NewImage?[\"FirstName\"]?.S;\n        if (added != null)\n            Console.WriteLine($\"{added} has joined the Beatles\");\n\n        var removed = dynamoDbStreamsEvent.Records.FirstOrDefault()?.Dynamodb.OldImage?[\"FirstName\"]?.S;\n        if (removed != null)\n            Console.WriteLine($\"{removed} has left the Beatles\");\n\n        return default;\n    });\n\n// disposing will remove the subscription\nsubscription.Dispose();\n```\n\nOr with F#\n\n```F#\nopen TestDynamo\nopen TestDynamo.Lambda\nopen Amazon.Lambda.DynamoDBEvents\n\nlet subscriber (dynamoDbStreamsEvent: DynamoDBEvent) _ =\n\n    let tryFirst f =\n        dynamoDbStreamsEvent.Records\n        |\u003e Seq.map f\n        |\u003e Seq.filter ((\u003c\u003e) null)\n        |\u003e Seq.map (fun (x: Dictionary\u003cstring, AttributeValue\u003e) -\u003e x[\"FirstName\"].S)\n        |\u003e Seq.tryHead\n\n    match tryFirst _.Dynamodb.NewImage with \n    | Some x -\u003e printf \"%s has joined the Beatles\" x\n    | None -\u003e ()\n\n    match tryFirst _.Dynamodb.OldImage with \n    | Some x -\u003e printf \"%s has left the Beatles\" x\n    | None -\u003e ()\n\n    Unchecked.defaultOf\u003c_\u003e\n\nlet subscription = \n    Subscriptions.addSubscription\n        (SubscriptionDetails.ofTableName \"Beatles\")\n        subscriber\n        database\n\n// disposing will remove the subscription\nsubscription.Dispose();\n```\n\nSubscribe to raw changes\n\n```C#\nvar subscription = database\n    .SubscribeToStream(\"Beatles\", (cdcPacket, cancellationToken) =\u003e\n    {\n        var added = cdcPacket.data.packet.changeResult.OrderedChanges\n            .Select(x =\u003e x.Put)\n            .Where(x =\u003e x.IsSome)\n            .Select(x =\u003e x.Value[\"FirstName\"].S)\n            .FirstOrDefault();\n\n        if (added != null)\n            Console.WriteLine($\"{added} has joined the Beatles\");\n\n        var removed = cdcPacket.data.packet.changeResult.OrderedChanges\n            .Select(x =\u003e x.Deleted)\n            .Where(x =\u003e x.IsSome)\n            .Select(x =\u003e x.Value[\"FirstName\"].S)\n            .FirstOrDefault();\n\n        if (removed != null)\n            Console.WriteLine($\"{removed} has left the Beatles\");\n\n        return default;\n    });\n\n// disposing will remove the subscription\nsubscription.Dispose();\n```\n\nSubscriptions synchonicity and error handling can be customized with `SubscriberBehaviour` through the \n`SubscribeToStream` and `AddSubscription` methods. \n\nSubscriptions can be executed synchonously or asynchonously. For example, if a subscription \nis configured to execute synchronously, and a PUT request is executed, the `AmazonDynamoDBClient` will not return\na PUT response until the subscriber has completed it's work. If the subscription is asynchronous, then subscriber\nexecution is disconnected from the event trigger.\n\nIf a subscriber is synchronous, its errors can be propagated back to the trigger method, allowing for more direct\ntest results. Otherwise, errors are cached and can be retrieved in the form of Exceptions when an `AwaitAllSubscribers`\nmethod is called\n\n#### AwaitAllSubscribers\n\nThe `Api.Database`, `Api.GlobalDatabase` and `AmazonDynamoDBClient` have `AwaitAllSubscribers` methods to pause test execution\nuntil all subscribers have executed. This method will throw any exceptions that were experienced within subscribers and were not\npropagated synchronously. For `AmazonDynamoDBClient`, the `AwaitAllSubscribers` method is static and available on the `TestDynamoClient` class.\n\n### DynamoDBContext\n\n```C#\nusing TestDynamo;\n\nusing var client = TestDynamoClient.CreateClient\u003cAmazonDynamoDBClient\u003e();\n\n... // initialize the database schema\n\nusing var context = new DynamoDbContext(client)\nawait context.SaveAsync(new Beatle\n{\n    FirstName = \"Ringo\",\n    SecondName = \"Starr\"\n})\n```\n\n### Global Database\n\nThe global database models an AWS account with a collection of regions. Each region is an [`Api.Database`](#database). It is used to test global table functionality \n\nCreating global tables is a synchonous operation. The global table will \nbe ready to use as soon as the `AmazonDynamoDBClient` client returns a response.\n\n```C#\nusing TestDynamo;\n\nusing var globalDatabase = new GlobalDatabase();\n\n// create a global table from ap-south-2\nusing var apSouth2Client = globalDatabase.CreateClient\u003cAmazonDynamoDBClient\u003e(new DatabaseId(\"ap-south-2\"));\nawait apSouth2Client.CreateGlobalTableAsync(...);\n\n// create a local table in cn-north-1\nusing var cnNorthClient = globalDatabase.CreateClient\u003cAmazonDynamoDBClient\u003e(new DatabaseId(\"cn-north-1\"));\nawait cnNorthClient.CreateTableAsync(...);\n```\n\n### Global Database Cloning\n\n```C#\nusing TestDynamo;\n\nusing var globalDatabase = new GlobalDatabase();\nusing var db2 = globalDatabase.Clone();\n```\n\n#### AwaitAllSubscribers\n\nThe `Api.GlobalDatabase` and `AmazonDynamoDBClient` have `AwaitAllSubscribers` methods to pause test execution\nuntil all data has been replicated between databases. For `AmazonDynamoDBClient`, the `AwaitAllSubscribers` method is static and available on the `TestDynamoClient` class.\n\n### TestDynamoClient\n\n`TestDynamoClient` links an `AmazonDynamoDBClient` to an `Api.Database` or an `Api.GlobalDatabase`. It has several useful extension methods\n\n#### Create methods\n\n```C#\nusing TestDynamo;\n\n// create a client with an empty database\nusing var client1 = TestDynamoClient.CreateClient\u003cAmazonDynamoDBClient\u003e();\n\n// create a client from an existing database\nusing var db1 = new Api.Database();\nusing var client21 = db1.CreateClient\u003cAmazonDynamoDBClient\u003e();\n\nusing var db2 = new Api.GlobalDatabase();\nusing var client22 = db2.CreateClient\u003cAmazonDynamoDBClient\u003e();\n\n// attach a database to an existing client\nusing var db3 = new Api.Database();\nusing var client3 = new AmazonDynamoDBClient();\ndb3.Attach(client3);\n```\n\n#### Get methods\n\n```C#\nusing TestDynamo;\n\nusing var client1 = TestDynamoClient.CreateClient\u003cAmazonDynamoDBClient\u003e();\n\n// get the underlying database from a client\nvar db1 = TestDynamoClient.GetDatabase(client1);\n\n// get a debug table from a client\nvar beatles = TestDynamoClient.GetTable(client1, \"Beatles\");\n```\n\nOr in F#\n\n```F#\nopen TestDynamo\n\nuse client1 = TestDynamoClient.createClient\u003cAmazonDynamoDBClient\u003e ValueNone false ValueNone false ValueNone\n\n// get the underlying database from a client\nlet db1 = TestDynamoClient.getDatabase client1\n\n// get a debug table from a client\nlet beatles = TestDynamoClient.getTable \"Beatles\" client1\n```\n\n#### Set methods\n\n```C#\nusing TestDynamo;\n\nusing var client = TestDynamoClient.CreateClient\u003cAmazonDynamoDBClient\u003e();\n\n// set an artificial processing delay\nTestDynamoClient.SetProcessingDelay(client, TimeSpan.FromSeconds(0.1));\n\n// set paging settings for database\nTestDynamoClient.SetScanLimits(client, ...);\n\n// set the AWS account id for the client\nTestDynamoClient.SetAwsAccountId(client, \"12345678\");\n```\n\n### Database Serializers\n\nDatabase serializers are available from the `TestDynamo.Serialization` nuget package.\n\nDatabase serializers can serialize or deserialize an entire database or global database to facilitate data driven testing.\n\n```C#\nusing TestDynamo;\nusing TestDynamo.Serialization;\n\nusing var db1 = new Api.Database();\n... populate database\n\nDatabaseSerializer.Database.ToFile(db1, @\"TestData.json\");\n\nusing var db2 = DatabaseSerializer.Database.FromFile(@\"TestData.json\");\n\n// there are also tools to serialize and deserialze global databases\nvar json = DatabaseSerializer.GlobalDatabase.ToString(globalDb);\n```\n\nOr F#\n\n```F#\nopen TestDynamo\nopen TestDynamo.Serialization\n\nuse db1 = new Api.FSharp.Database()\n... populate database\n\nDatabaseSerializer.FSharp.Database.ToFile(db1, @\"TestData.json\")\n\nuse db2 = DatabaseSerializer.FSharp.Database.FromFile(@\"TestData.json\")\n\n// there are also tools to serialize and deserialze global databases\nlet json = DatabaseSerializer.FSharp.GlobalDatabase.ToString(globalDb)\n```\n\nSerialization is designed to share data between test runs, but ultimately, it scales with the number of items in the database. This means\nthat 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\n\n### Cloud Formation Templates\n\nStatic [Cloud Formation templates](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_DynamoDB.html) \ncan be consumed to create databases and global databases. Dynamic templates with functions are not supported.\n\nImport the `dotnet add package TestDynamo.Serialization` package to use cloudformation templates\n\n```C#\nusing TestDynamo.Serialization;\n\nvar cfnFile1 = new CloudFormationFile(await File.ReadAllTextAsync(\"myTemplate1.json\"), \"eu-north-1\");\nvar cfnFile2 = new CloudFormationFile(await File.ReadAllTextAsync(\"myTemplate2.json\"), \"us-west-2\");\nusing var database = await CloudFormationParser.BuildDatabase(new[] { cfnFile1, cfnFile2 }, new CloudFormationSettings(true));\n...\n```\n\nOr with F#\n\n```F#\nopen TestDynamo.Serialization;\n\nasync {\n    use! database = \n        [ { region = \"eu-north-1\"\n            fileJson = File.ReadAllText(\"myTemplate1.json\") }\n          { region = \"us-west-2\"\n            fileJson = File.ReadAllText(\"myTemplate2.json\") } ]\n        |\u003e CloudFormationParser.buildDatabase { ignoreUnsupportedResources = true } ValueNone\n    ...\n}\n```\n\n### Locking and Atomic transactions\n\nTest dynamo is more consistant than DynamoDb. In general, all operations on a single database (region) are atomic. \nWithin the `AmazonDynamoDBClient` client, BatchRead and BatchWrite operations are executed as several independant operations in order\nto simulate non consistency.\n\nThe biggest differences you will see are\n\n * Reads are always atomic\n * Writes from tables to global secondary indexes are always atomic\n\n### Transact write ClientRequestToken\n\n[Client request tokens](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html#API_TransactWriteItems_RequestSyntax) are used in transact write operations as an idempotency key. If 2 requests have the\nsame client request token, the second one will not be executed. By default AWS keeps client request tokens for 10 minutes. TestDynamo\nkeeps client request tokens for 10 seconds. This cache time can be updated in `Settings`.\n\n### Recorders\n\nRecorders can be added to record all inputs and outputs of a database\n\n```C#\nusing TestDynamo;\n\nusing var client = TestDynamoClient.CreateClient\u003cAmazonDynamoDBClient\u003e(recordCalls: true);\n\n// successful request to create a table\nawait client.CreateTableAsync(...);\ntry\n{\n    // failed request to put an item\n    await client.PutItemAsync(...);\n}\ncatch\n{\n    // do nothing\n}\n\n\nvar recordings = TestDynamoClient\n    .GetRecordings(client)\n    .ToList();\n\nAssert.True(recordings[0].request is CreateTableRequest);\nAssert.True(recordings[0].IsSuccess);\nAssert.True(recordings[0].SuccessResponse is CreateTableResponse);\n\nAssert.True(recordings[1].request is PutItemRequest);\nAssert.False(recordings[1].IsSuccess);\nAssert.NotNull(recordings[1].Exception);\n```\n\n### Interceptors\n\nInterceptors can be added to intercept and override certain database functionality.\n\nFor example, this sample implements create and restore backup functionality\n\n#### Implement backups functionality\n\n```C#\nusing TestDynamo;\nusing TestDynamo.Api.FSharp;\nusing TestDynamo.Client;\nusing TestDynamo.Model;\n\n/// \u003csummary\u003e\n/// An interceptor which implements the CreateBackup and RestoreTableFromBackup operations\n/// \u003c/summary\u003e\npublic class CreateBackupInterceptor(Dictionary\u003cstring, DatabaseCloneData\u003e backupStore) : IRequestInterceptor\n{\n    public async ValueTask\u003cobject?\u003e InterceptRequest(Api.FSharp.Database database, object request, CancellationToken c)\n    {\n        if (request is CreateBackupRequest create)\n            return CreateBackup(database, create.TableName);\n\n        if (request is RestoreTableFromBackupRequest restore)\n            return await RestoreBackup(database, restore.BackupArn);\n\n        // return null to allow the client to process other request types as normal\n        return null;\n    }\n\n    private CreateBackupResponse CreateBackup(Api.FSharp.Database database, string tableName)\n    {\n        // wrap the database in something that is more C# friendly\n        using var csDatabase = new Api.Database(database);\n\n        // clone the required database and remove all other tables\n        var cloneData = csDatabase.BuildCloneData();\n        cloneData = new DatabaseCloneData(\n            cloneData.data.ExtractTables(new [] { tableName }),\n            cloneData.databaseId);\n\n        // create a fake arn and store a cloned DB as a backup\n        var arn = $\"{database.Id.regionId}/{tableName}\";\n        backupStore.Add(arn, cloneData);\n\n        return new CreateBackupResponse\n        {\n            BackupDetails = new BackupDetails\n            {\n                BackupArn = arn,\n                BackupStatus = BackupStatus.AVAILABLE\n            }\n        };\n    }\n\n    private async ValueTask\u003cRestoreTableFromBackupResponse\u003e RestoreBackup(Api.FSharp.Database database, string arn)\n    {\n        // parse fake ARN created in the CreateBackup method\n        var arnParts = arn.Split(\"/\");\n        if (arnParts.Length != 2)\n            throw new AmazonDynamoDBException(\"Invalid backup arn\");\n\n        var tableName = arnParts[1];\n        if (!backupStore.TryGetValue(arn, out var backup))\n            throw new AmazonDynamoDBException(\"Invalid backup arn\");\n\n        // wrap the database in something that is more C# friendly\n        using var csDatabase = new Api.Database(database);\n\n        // delete any existing data to make room for restore data\n        if (csDatabase.TryDescribeTable(tableName).IsSome)\n            await csDatabase.DeleteTable(tableName);\n\n        csDatabase.Import(backup.data);\n        return new RestoreTableFromBackupResponse\n        {\n            TableDescription = new TableDescription\n            {\n                TableName = tableName\n            }\n        };\n    }\n\n    // no need to intercept responses\n    public ValueTask\u003cobject?\u003e InterceptResponse(Api.FSharp.Database database, object request, object response, CancellationToken c) =\u003e default;\n}\n\n// create an in memory store for backups\nvar backups = new Dictionary\u003cstring, DatabaseCloneData\u003e();\n\nusing var database = new Api.Database(new DatabaseId(\"us-west-1\"));\n\n// create an interceptor and use in a client\nvar interceptor = new CreateBackupInterceptor(backups);\nusing var client = database.CreateClient\u003cAmazonDynamoDBClient\u003e(interceptor);\n\n// execute some requests which are not intercepted\nawait client.PutItemAsync(...);\nawait client.PutItemAsync(...);\n\n// create a backup. This will be intercepted\nvar backupResponse = await client.CreateBackupAsync(new CreateBackupRequest\n{\n    TableName = \"Beatles\"\n});\n\n// restore from backup. This will be intercepted\nawait client.RestoreTableFromBackupAsync(new RestoreTableFromBackupRequest\n{\n    BackupArn = backupResponse.BackupDetails.BackupArn\n});\n```\n\n#### Implement BillingMode functionality\n\nHere is another example of how to implement some out of scope [BillingMode](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BillingModeSummary.html) functionality with an interceptor\n\n```C#\n/// \u003csummary\u003e\n/// An interceptor which implements BillingMode functionality for CreateTableAsync\n/// \u003c/summary\u003e\npublic class BillingModeInterceptor : IRequestInterceptor\n{\n    // request interception is not requred\n    public ValueTask\u003cobject?\u003e InterceptRequest(Api.FSharp.Database database, object request, CancellationToken c) =\u003e default;\n\n    public ValueTask\u003cobject?\u003e InterceptResponse(Api.FSharp.Database database, object request, object response, CancellationToken c)\n    {\n        if (request is not CreateTableRequest req || response is not CreateTableResponse resp)\n            return default;\n\n        // modify the output\n        resp.TableDescription.BillingModeSummary = new BillingModeSummary\n        {\n            BillingMode = req.BillingMode ?? BillingMode.PAY_PER_REQUEST,\n            LastUpdateToPayPerRequestDateTime = DateTime.UtcNow\n        };\n\n        // Return default so that the response will be passed on after modification\n        // If a non null item is returned here, it will be passed on instead \n        return default;\n    }\n}\n```\n\n### Logging\n\nLogging is implemented by `Microsoft.Extensions.Logging.ILogger`. Databases can be created with loggers. Clients can also be created with loggers, which will override the datase logging","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffsprojects%2Ftestdynamo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffsprojects%2Ftestdynamo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffsprojects%2Ftestdynamo/lists"}