├── .github └── workflows │ └── build.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── README.md ├── docs ├── data-model-assumptions.md ├── example-batch-operations.md ├── example-crud-operations.md ├── usage-independent-entities.md ├── usage-many-to-many.md └── usage-one-to-many.md ├── src ├── ConsoleDynamoDbRepository │ ├── ConsoleDynamoDbRepository.csproj │ └── Program.cs ├── DynamoDbRepository │ ├── DynamoDB │ │ ├── DynamoDBClient.cs │ │ ├── DynamoDBConstants.cs │ │ └── DynamoDBItem.cs │ ├── DynamoDbRepository.csproj │ └── Repository │ │ ├── AssociativeEntityRepository.cs │ │ ├── DependentEntityRepository.cs │ │ ├── ISimpleRepository.cs │ │ ├── IndependentEntityRepository.cs │ │ ├── RepositoryBase.cs │ │ └── SimpleRepository.cs └── SampleDynamoDbRepository │ ├── Game │ ├── Game.cs │ ├── GameRepository.cs │ └── IGameRepository.cs │ ├── Person │ ├── Person.cs │ └── PersonRepository.cs │ ├── Project │ ├── IProjectRepository.cs │ ├── Project.cs │ └── ProjectRepository.cs │ ├── SampleDynamoDbRepository.csproj │ ├── User │ ├── IUserRepository.cs │ ├── User.cs │ └── UserRepository.cs │ └── UserProject │ ├── IUserProjectRepository.cs │ ├── UserProject.cs │ └── UserProjectRepository.cs └── test └── DynamoDbRepository.Tests ├── DynamoDBDockerFixture.cs ├── DynamoDbRepository.Tests.csproj ├── RepositoryIntegrationTest.cs └── TestRepositories ├── TestDependentEntityRepo.cs ├── TestEntity.cs └── TestIndependentEntityRepo.cs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET Core 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 3.1.201 20 | - name: Install dependencies 21 | run: dotnet restore 22 | working-directory: src/ConsoleDynamoDbRepository 23 | - name: Build 24 | run: dotnet build --configuration Release --no-restore 25 | working-directory: src/ConsoleDynamoDbRepository 26 | - name: Restore and build test 27 | run: dotnet build 28 | working-directory: test/DynamoDbRepository.Tests 29 | - name: Test 30 | run: > 31 | AWS_ACCESS_KEY_ID=abc AWS_SECRET_ACCESS_KEY=xyz 32 | dotnet test --no-build 33 | /p:CollectCoverage=true /p:CoverletOutput=TestResults/ /p:CoverletOutputFormat=lcov 34 | working-directory: test/DynamoDbRepository.Tests 35 | - name: Publish coverage report to coveralls.io 36 | uses: coverallsapp/github-action@master 37 | with: 38 | github-token: ${{ secrets.GITHUB_TOKEN }} 39 | path-to-lcov: test/DynamoDbRepository.Tests/TestResults/coverage.info 40 | - name: Create Nuget package 41 | run: dotnet pack --configuration Release --no-build -p:PackageVersion=0.0.$GITHUB_RUN_NUMBER 42 | working-directory: src/DynamoDbRepository 43 | - name: Configure Nuget for Github packages 44 | run: > 45 | dotnet nuget add source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json 46 | -n github -u ${{ github.repository_owner }} -p ${{ secrets.GITHUB_TOKEN }} 47 | --store-password-in-clear-text 48 | working-directory: src/DynamoDbRepository 49 | - name: Publish Nuget package - Github packages 50 | run: > 51 | dotnet nuget push **/*.nupkg 52 | --source github 53 | working-directory: src/DynamoDbRepository -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | .vs/ 4 | TestResults/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/src/ConsoleDynamoDbRepository/bin/Debug/netcoreapp3.1/ConsoleDynamoDbRepository.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/src/ConsoleDynamoDbRepository", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/src/ConsoleDynamoDbRepository/ConsoleDynamoDbRepository.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/src/ConsoleDynamoDbRepository/ConsoleDynamoDbRepository.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/src/ConsoleDynamoDbRepository/ConsoleDynamoDbRepository.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Abel Perez Martinez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DynamoDB base repository 2 | 3 | [![.NET Core Github Actions badge](https://github.com/abelperezok/DynamoDB-BaseRepository/workflows/.NET%20Core/badge.svg)](https://github.com/abelperezok/DynamoDB-BaseRepository/actions) 4 | [![Coverage Status](https://coveralls.io/repos/github/abelperezok/DynamoDB-BaseRepository/badge.svg?branch=master)](https://coveralls.io/github/abelperezok/DynamoDB-BaseRepository?branch=master) 5 | 6 | C# .NET Core implementation of the repository pattern using DynamoDB as data store using single table and hierarchical data modelling approach overloading the partition and sort key as well secondary index. 7 | 8 | This implementation aims to solve the most common data persistence use cases ranging from independent entities to more complex data models. 9 | 10 | Key features: 11 | * Ready to use CRUD operations. 12 | * Ready to use batch operations. 13 | * Generic design for flexibility of data types. 14 | * One to many relationship. 15 | * Many to many relationships 16 | 17 | 18 | ## Content 19 | 20 | [Quick usage guide](#quick-usage-guide) 21 | 22 | [Data model assumptions](docs/data-model-assumptions.md) 23 | 24 | [Usage - Independent entities](docs/usage-independent-entities.md) 25 | 26 | [Usage - One to many](docs/usage-one-to-many.md) 27 | 28 | [Usage - Many to many](docs/usage-many-to-many.md) 29 | 30 | [Example: CRUD operations](docs/example-crud-operations.md) 31 | 32 | [Example: Batch operations](docs/example-batch-operations.md) 33 | 34 | ## Quick usage guide 35 | 36 | 1. Define your entity as a POCO, no interface or annotations required. 37 | 38 | ```cs 39 | public class Person 40 | { 41 | public int Id { get; set; } 42 | 43 | public string Name { get; set; } 44 | 45 | public string Email { get; set; } 46 | 47 | public int Age { get; set; } 48 | } 49 | ``` 50 | 51 | 2. Inherit from ```SimpleRepository``` abstract class. 52 | 53 | ```cs 54 | public class PersonRepository : SimpleRepository 55 | { 56 | } 57 | ``` 58 | 59 | 3. Define partition key prefix and sort key prefix in constructor. 60 | 61 | ```cs 62 | public PersonRepository(string tableName, string serviceUrl = null) : base(tableName, serviceUrl) 63 | { 64 | PKPrefix = "PERSON"; 65 | SKPrefix = "METADATA"; 66 | } 67 | ``` 68 | 69 | 4. Override ```TKey GetEntityKey(TEntity item)``` abstract method to return the entity identifier. 70 | 71 | ```cs 72 | protected override int GetEntityKey(Person item) 73 | { 74 | return item.Id; 75 | } 76 | ``` 77 | 78 | 5. Override ```DynamoDBItem ToDynamoDb(TEntity item)``` abstract method to map the entity object to a DynamoDB attribute dictionary. 79 | 80 | ```cs 81 | protected override DynamoDBItem ToDynamoDb(Person item) 82 | { 83 | var dbItem = new DynamoDBItem(); 84 | dbItem.AddNumber("Id", item.Id); 85 | dbItem.AddString("Name", item.Name); 86 | dbItem.AddString("Email", item.Email); 87 | dbItem.AddNumber("Age", item.Age); 88 | return dbItem; 89 | } 90 | ``` 91 | 92 | 6. Override ```TEntity FromDynamoDb(DynamoDBItem item)``` abstract method to map the DynamoDB attribute dictionary to an entity object. 93 | 94 | ```cs 95 | protected override Person FromDynamoDb(DynamoDBItem item) 96 | { 97 | var result = new Person(); 98 | result.Id = item.GetInt32("Id"); 99 | result.Name = item.GetString("Name"); 100 | result.Email = item.GetString("Email"); 101 | result.Age = item.GetInt32("Age"); 102 | return result; 103 | } 104 | ``` 105 | 106 | 7. Use the available methods either directly or through the interface. 107 | 108 | ```cs 109 | public static async Task TestCRUD_PersonRepository() 110 | { 111 | // Create a new PersonRepository 112 | ISimpleRepository repo = new PersonRepository(_tableName); 113 | 114 | // Prepare a Person instance 115 | var p1 = new Person 116 | { 117 | Id = 1, 118 | Name = "personA", 119 | Email = "pa@test.com", 120 | Age = 35 121 | }; 122 | 123 | Console.WriteLine("* Adding Person 1"); 124 | // Add a new person 125 | await repo.Add(p1); 126 | 127 | Console.WriteLine("* Getting the list"); 128 | // Get the full list 129 | var list = await repo.GetList(); 130 | foreach (var item in list) 131 | { 132 | Console.WriteLine(JsonSerializer.Serialize(item)); 133 | } 134 | 135 | Console.ReadKey(); 136 | 137 | Console.WriteLine("* Getting Person 1"); 138 | // Get an individual Person by its Id 139 | var found1 = await repo.Get(p1.Id); 140 | Console.WriteLine(JsonSerializer.Serialize(found1)); 141 | 142 | Console.WriteLine("* Deleting Person 1"); 143 | // Delete an individual Person by its Id 144 | await repo.Delete(p1.Id); 145 | } 146 | ``` 147 | -------------------------------------------------------------------------------- /docs/data-model-assumptions.md: -------------------------------------------------------------------------------- 1 | # Data model assumptions 2 | 3 | When trying to generalize a concept, there has to be some assumptions. In this case, the table structure follows the general ideas about hierarchical data overloading the partition and sort keys as well as the GSI. 4 | 5 | * Generic partition key "PK" string. 6 | * Generic sort key "SK" string. 7 | * Generic attribute "GSI1" string. 8 | * GSI partition key is "GSI1". 9 | * GSI sort key is also "SK". 10 | * GSI projects all attributes. 11 | * Other attributes such as ID, Name, Description, etc. 12 | 13 | ## Table with sample data 14 | 15 | PK (S) | SK (S) | GSI1 | ID | Name | Description 16 | -------|--------|------|----|------|------------ 17 | USER#U1 | METADATA#U1 | USER | U1 | Abel 18 | USER#U2 | METADATA#U2 | USER | U2 | Nestor 19 | PROJECT#P1 | METADATA#P1 | PROJECT | P1 | Project 1 | desc project 1 20 | PROJECT#P2 | METADATA#P2 | PROJECT | P2 | Project 2 | desc project 2 21 | PROJECT#P3 | METADATA#P3 | PROJECT | P3 | Project 3 | desc project 3 22 | USER#U1 | GAME#G1 | | G1 | Game 1 | 23 | USER#U1 | GAME#G2 | | G2 | Game 2 | 24 | 25 | ## GSI with sample data 26 | 27 | PK (S) GSI1 | SK (S) SK | ID | Name | Description 28 | ------------|-----------|----|------|------------ 29 | USER | METADATA#U1 | U1 | Abel | 30 | USER | METADATA#U2 | U2 | Nestor | 31 | PROJECT | METADATA#P1 | P1 | Project 1 | desc project 1 32 | PROJECT | METADATA#P2 | P2 | Project 2 | desc project 2 33 | PROJECT | METADATA#P3 | P3 | Project 3 | desc project 3 34 | 35 | 36 | ## Queries 37 | 38 | * Single item given the ID: Table PK = ENTITY#ID, SK = METADATA#ID 39 | * Get User U1: Table PK = USER#U1, SK = METADATA#U1 40 | * Get Project P1: Table PK = PROJECT#P1, SK = METADATA#P1 41 | * Get Game G1 for User U1: Table PK = USER#U1, SK = GAME#G1 42 | 43 | * Multiple items of one type: GSI PK = ENTITY 44 | * Get all users: GSI PK = USER 45 | * Get all projects: GSI PK = PROJECT 46 | 47 | * Item collection by parent ID: Table PK = PARENT_ENTITY#ID, SK = ENTITY#ID 48 | * Get all games by user U1 : Table PK = USER#U1, SK begins_with GAME 49 | 50 | ## Creating the table - AWS CLI 51 | 52 | Use ```aws dynamodb create-table``` command to create a table. 53 | 54 | ```shell 55 | $ aws dynamodb create-table \ 56 | --table-name dynamodb_test_table \ 57 | --attribute-definitions \ 58 | AttributeName=PK,AttributeType=S \ 59 | AttributeName=SK,AttributeType=S \ 60 | AttributeName=GSI1,AttributeType=S \ 61 | --key-schema \ 62 | AttributeName=PK,KeyType=HASH \ 63 | AttributeName=SK,KeyType=RANGE \ 64 | --provisioned-throughput \ 65 | ReadCapacityUnits=1,WriteCapacityUnits=1 \ 66 | --global-secondary-indexes \ 67 | IndexName=GSI1,KeySchema=["{AttributeName=GSI1,KeyType=HASH},{AttributeName=SK,KeyType=RANGE}"],\ 68 | Projection="{ProjectionType=ALL}",\ 69 | ProvisionedThroughput="{ReadCapacityUnits=1,WriteCapacityUnits=1}" 70 | ``` 71 | 72 | Optionally, wait for the table to be active. 73 | 74 | ```shell 75 | $ aws dynamodb wait table-exists --table-name dynamodb_test_table 76 | ``` 77 | 78 | ## Creating the table - CloudFormation 79 | 80 | ```yaml 81 | # DynamoDb table 82 | DynamoDbTestTable: 83 | Type: AWS::DynamoDB::Table 84 | Properties: 85 | KeySchema: 86 | - AttributeName: PK 87 | KeyType: HASH 88 | - AttributeName: SK 89 | KeyType: RANGE 90 | AttributeDefinitions: 91 | - AttributeName: PK 92 | AttributeType: S 93 | - AttributeName: SK 94 | AttributeType: S 95 | - AttributeName: GSI1 96 | AttributeType: S 97 | ProvisionedThroughput: 98 | ReadCapacityUnits: 1 99 | WriteCapacityUnits: 1 100 | GlobalSecondaryIndexes: 101 | - IndexName: GSI1 102 | KeySchema: 103 | - AttributeName: GSI1 104 | KeyType: HASH 105 | - AttributeName: SK 106 | KeyType: RANGE 107 | Projection: 108 | ProjectionType: ALL 109 | ProvisionedThroughput: 110 | ReadCapacityUnits: 1 111 | WriteCapacityUnits: 1 112 | ``` 113 | -------------------------------------------------------------------------------- /docs/example-batch-operations.md: -------------------------------------------------------------------------------- 1 | # Example: Batch operations 2 | 3 | Batch operations are generally more efficient than then one by one counterpart. Here are some examples illustrating how to use them. 4 | 5 | ## Example of batch insert for an independent entity 6 | 7 | ```cs 8 | private static async Task TestUserRepositoryBatchAddItems() 9 | { 10 | IUserRepository repo = new UserRepository(_tableName); 11 | var itemsToCreate = new List(); 12 | for (int i = 50; i < 60; i++) 13 | { 14 | var uA = new User { Id = "A" + i, Name = "userA" + i, FirstName = "User" + i, LastName = "A" + i, Email = $"a{i}@test.com" }; 15 | itemsToCreate.Add(uA); 16 | Console.WriteLine("* Adding to list user " + uA.Id); 17 | 18 | } 19 | await repo.BatchAddUsers(itemsToCreate); 20 | Console.WriteLine("***** Done adding all users"); 21 | 22 | Console.WriteLine("* Getting all users"); 23 | var users = await repo.GetUserList(); 24 | foreach (var item in users) 25 | { 26 | Console.WriteLine(JsonSerializer.Serialize(item)); 27 | } 28 | } 29 | ``` 30 | 31 | ## Example of batch insert for a dependent entity 32 | 33 | ```cs 34 | private static async Task TestGameRepositoryBatchAddItems() 35 | { 36 | var user = new User { Id = "U1" }; 37 | 38 | IGameRepository repo = new GameRepository(_tableName); 39 | var itemsToCreate = new List(); 40 | for (int i = 50; i < 60; i++) 41 | { 42 | var g = new Game { Id = "G" + i, Name = "Game G" + i }; 43 | itemsToCreate.Add(g); 44 | Console.WriteLine("* Adding to list game " + g.Id); 45 | 46 | } 47 | await repo.BatchAddGames(user.Id, itemsToCreate); 48 | Console.WriteLine("***** Done adding all games"); 49 | 50 | Console.WriteLine("* Getting all games"); 51 | var items = await repo.GetGameList(user.Id); 52 | foreach (var item in items) 53 | { 54 | Console.WriteLine(JsonSerializer.Serialize(item)); 55 | } 56 | } 57 | ``` 58 | 59 | ## Example of batch delete for an independent entity 60 | 61 | ```cs 62 | private static async Task TestUserRepositoryBatchDeleteItems() 63 | { 64 | IUserRepository repo = new UserRepository(_tableName); 65 | var itemsToDelete = new List(); 66 | for (int i = 50; i < 60; i++) 67 | { 68 | var uA = new User { Id = "A" + i }; 69 | itemsToDelete.Add(uA); 70 | Console.WriteLine("* Adding to delete list, user " + uA.Id); 71 | 72 | } 73 | await repo.BatchDeleteUsers(itemsToDelete); 74 | Console.WriteLine("***** Done deleting all users"); 75 | 76 | Console.WriteLine("* Getting all users"); 77 | var users = await repo.GetUserList(); 78 | foreach (var item in users) 79 | { 80 | Console.WriteLine(JsonSerializer.Serialize(item)); 81 | } 82 | } 83 | ``` 84 | 85 | ## Example of batch delete for a dependent entity 86 | 87 | ```cs 88 | private static async Task TestGameRepositoryBatchDeleteItems() 89 | { 90 | var user = new User { Id = "U1" }; 91 | 92 | IGameRepository repo = new GameRepository(_tableName); 93 | var itemsToDelete = new List(); 94 | for (int i = 50; i < 60; i++) 95 | { 96 | var g = new Game { Id = "G" + i }; 97 | itemsToDelete.Add(g); 98 | Console.WriteLine("* Adding to delete list, game " + g.Id); 99 | 100 | } 101 | await repo.BatchDeleteGames(user.Id, itemsToDelete); 102 | Console.WriteLine("***** Done deleting all games"); 103 | 104 | Console.WriteLine("* Getting all games"); 105 | var games = await repo.GetGameList(user.Id); 106 | foreach (var item in games) 107 | { 108 | Console.WriteLine(JsonSerializer.Serialize(item)); 109 | } 110 | } 111 | ``` 112 | -------------------------------------------------------------------------------- /docs/example-crud-operations.md: -------------------------------------------------------------------------------- 1 | # Example: CRUD operations 2 | 3 | Here is an example of performing all basic CRUD operations in a sequence for an independent entity (User). 4 | 5 | ```cs 6 | private static async Task TestCRUD_UserRepository() 7 | { 8 | IUserRepository repo = new UserRepository(_tableName); 9 | 10 | var u1 = new User { Id = "U1", Name = "userU1", FirstName = "User", LastName = "U1", Email = "u1@test.com" }; 11 | Console.WriteLine("* Creating user U1"); 12 | await repo.AddUser(u1); 13 | 14 | var u2 = new User { Id = "U2", Name = "userU2", FirstName = "User", LastName = "U2", Email = "u2@test.com" }; 15 | Console.WriteLine("* Creating user U2"); 16 | await repo.AddUser(u2); 17 | 18 | Console.WriteLine("* Getting all users"); 19 | var users = await repo.GetUserList(); 20 | foreach (var item in users) 21 | { 22 | Console.WriteLine(JsonSerializer.Serialize(item)); 23 | } 24 | 25 | Console.WriteLine("* Getting user U1"); 26 | var found1 = await repo.GetUser(u1.Id); 27 | Console.WriteLine(JsonSerializer.Serialize(found1)); 28 | 29 | Console.WriteLine("* Getting user U2"); 30 | var found2 = await repo.GetUser(u2.Id); 31 | Console.WriteLine(JsonSerializer.Serialize(found2)); 32 | 33 | Console.WriteLine("* Deleting user U1"); 34 | await repo.DeleteUser(u1.Id); 35 | 36 | Console.WriteLine("* Deleting user U2"); 37 | await repo.DeleteUser(u2.Id); 38 | } 39 | ``` 40 | 41 | Here is an example of performing all basic CRUD operations in a sequence for a dependent entity (Game) using generic methods (not interface). 42 | 43 | ```cs 44 | private static async Task TestCRUD_GameRepository() 45 | { 46 | var repo = new GameRepository(_tableName); 47 | var g1 = new Game { Id = "G1", Name = "Game G1" }; 48 | Console.WriteLine("* Creating game G1"); 49 | await repo.AddItemAsync("U1", "G1", g1); 50 | 51 | var g2 = new Game { Id = "G2", Name = "Game G2" }; 52 | Console.WriteLine("* Creating game G2"); 53 | await repo.AddItemAsync("U1", "G2", g2); 54 | 55 | Console.WriteLine("* Getting all users"); 56 | var games = await repo.TableQueryItemsByParentIdAsync("U1"); 57 | foreach (var item in games) 58 | { 59 | Console.WriteLine(JsonSerializer.Serialize(item)); 60 | } 61 | 62 | Console.WriteLine("* Getting game G1"); 63 | var found1 = await repo.GetItemAsync("U1", g1.Id); 64 | Console.WriteLine(JsonSerializer.Serialize(found1)); 65 | 66 | Console.WriteLine("* Getting game G2"); 67 | var found2 = await repo.GetItemAsync("U1", g2.Id); 68 | Console.WriteLine(JsonSerializer.Serialize(found2)); 69 | 70 | Console.WriteLine("* Deleting game G1"); 71 | await repo.DeleteItemAsync("U1", g1.Id); 72 | 73 | Console.WriteLine("* Deleting game G2"); 74 | await repo.DeleteItemAsync("U1", g2.Id); 75 | } 76 | ``` -------------------------------------------------------------------------------- /docs/usage-independent-entities.md: -------------------------------------------------------------------------------- 1 | # Usage - Independent entities 2 | 3 | Independent entities are those not related to any other entity or if the entity is the ```one``` part in a ```one to many``` relationship. 4 | 5 | Define your entity as a POCO, no need to implement any interface or annotate anything. 6 | 7 | ```cs 8 | public class User 9 | { 10 | public string Id { get; set; } 11 | 12 | public string Name { get; set; } 13 | 14 | public string FirstName { get; set; } 15 | 16 | public string LastName { get; set; } 17 | } 18 | ``` 19 | 20 | Inherit from ```IndependentEntityRepository``` abstract class. This class provides methods intended to be used by independent entities and this is reflected in the way the PK and SK value are generated. 21 | 22 | ```cs 23 | public class UserRepository : IndependentEntityRepository 24 | { 25 | } 26 | ``` 27 | 28 | Define partition key prefix and sort key prefix. This is the value that will be used when generating the values for PK, SK and GSI1. 29 | 30 | Set PKPrefix to a value that identifies the entity within the data model i.e ```"USER"```. 31 | 32 | Set SKPrefix to a value that clearly states it's holding entity data and not a relationship i.e ```"METADATA"```. 33 | 34 | ```cs 35 | public UserRepository(string tableName, string serviceUrl = null) : base(tableName, serviceUrl) 36 | { 37 | PKPrefix = "USER"; 38 | SKPrefix = "METADATA"; 39 | } 40 | ``` 41 | 42 | Internally, the ```GSI1``` attribute will be set to the value of PKPrefix property so when the GSI1 Index is queried, these items are found. 43 | 44 | This will enable method ```GSI1QueryAllAsync()``` to retrieve the correct information. 45 | 46 | The typical implementation of the abstract methods will focus only on the data attributes to be mapped to and from DynamoDB. 47 | 48 | ```cs 49 | protected override DynamoDBItem ToDynamoDb(User item) 50 | { 51 | var dbItem = new DynamoDBItem(); 52 | dbItem.AddString("Id", item.Id); 53 | dbItem.AddString("Name", item.Name); 54 | dbItem.AddString("FirstName", item.FirstName); 55 | dbItem.AddString("LastName", item.LastName); 56 | dbItem.AddString("Email", item.Email); 57 | return dbItem; 58 | } 59 | ``` 60 | 61 | ```cs 62 | protected override User FromDynamoDb(DynamoDBItem item) 63 | { 64 | var result = new User(); 65 | result.Id = item.GetString("Id"); 66 | result.Name = item.GetString("Name"); 67 | result.FirstName = item.GetString("FirstName"); 68 | result.LastName = item.GetString("LastName"); 69 | result.Email = item.GetString("Email"); 70 | return result; 71 | } 72 | ``` 73 | 74 | Optionally (and recommended), define your own interface so it exposes methods with the relevant parameter and return values, it's also good practice to separate interface from implementation. 75 | 76 | ```cs 77 | public interface IUserRepository 78 | { 79 | Task AddUser(User user); 80 | 81 | Task DeleteUser(string userId); 82 | 83 | Task UpdateUser(User user); 84 | 85 | Task> GetUserList(); 86 | 87 | Task GetUser(string userId); 88 | 89 | Task BatchAddUsers(IEnumerable items); 90 | 91 | Task BatchDeleteUsers(IEnumerable items); 92 | } 93 | ``` 94 | 95 | Make the repository class to implement the interface. 96 | 97 | ```cs 98 | public class UserRepository : IndependentEntityRepository, IUserRepository 99 | { 100 | } 101 | ``` 102 | 103 | And add code to each method, it should be simple, most of the effort is to adapt to the custom interface. 104 | 105 | ```cs 106 | public async Task AddUser(User user) 107 | { 108 | await AddItemAsync(user.Id, user); 109 | } 110 | 111 | public async Task DeleteUser(string userId) 112 | { 113 | await DeleteItemAsync(userId); 114 | } 115 | 116 | public async Task> GetUserList() 117 | { 118 | return await GSI1QueryAllAsync(); 119 | } 120 | 121 | public async Task UpdateUser(User user) 122 | { 123 | await AddItemAsync(user.Id, user); 124 | } 125 | 126 | public async Task GetUser(string userId) 127 | { 128 | return await GetItemAsync(userId); 129 | } 130 | 131 | public async Task BatchAddUsers(IEnumerable items) 132 | { 133 | await BatchAddItemsAsync(items.Select(x => new KeyValuePair(x.Id, x))); 134 | } 135 | 136 | public async Task BatchDeleteUsers(IEnumerable items) 137 | { 138 | await BatchDeleteItemsAsync(items.Select(x => x.Id)); 139 | } 140 | 141 | ``` 142 | -------------------------------------------------------------------------------- /docs/usage-many-to-many.md: -------------------------------------------------------------------------------- 1 | # Usage - Item collection - Many to many relationship 2 | 3 | This section explains how to handle a typical ```many to many``` relationship following the same pattern. 4 | 5 | Define your relation as a POCO, where normally it should it include both parent entities identifier. Depending on the nature of the model, you can also add other attributes. 6 | 7 | > **Note** - In this example one **User** participates in many **Projects** and one **Project** can have many **Users**. A **User** in a **Project** can be either owner or member. 8 | 9 | ```cs 10 | public class UserProject 11 | { 12 | public string UserId { get; set; } 13 | public string ProjectId { get; set; } 14 | public string Role { get; set; } 15 | } 16 | ``` 17 | 18 | Inherit from ```AssociativeEntityRepository``` abstract class. This class provides methods intended to be used by dependent entities which require both parent entities identifiers and this is reflected in the way the PK and SK value are generated. 19 | 20 | ```cs 21 | public class UserProjectRepository : AssociativeEntityRepository 22 | { 23 | } 24 | ``` 25 | 26 | Define partition key, sort key and GSI1 prefix. This is the value that will be used when generating the values for PK, SK and GSI1. At this point, a choice has to be made regarding which of the parent entities is going to be PK and which is going to be GSI1. There is not right or wrong answer to this. It only determines the way queries are made to retrieve users by project or projects by user. 27 | 28 | Set PKPrefix to a value that identifies the **"main" parent entity** within the data model i.e ```"USER"```. 29 | 30 | Set SKPrefix to a value that identifies the relationship within the data model i.e ```"USER_PROJECT"```. 31 | 32 | Set GSI1Prefix to a value that identifies the **"second" entity in question** within the data model i.e ```"PROJECT"```. 33 | 34 | ```cs 35 | public UserProjectRepository(string tableName, string serviceUrl = null) : base(tableName, serviceUrl) 36 | { 37 | PKPrefix = "USER"; 38 | SKPrefix = "USER_PROJECT"; 39 | GSI1Prefix = "PROJECT"; 40 | } 41 | ``` 42 | 43 | All this will enable the query methods ```TableQueryItemsByParentIdAsync(TKey)``` and ```GSI1QueryItemsByParentIdAsync(TKey)``` to retrieve the correct information. In this case, query the table to retrieve projects by user and query the GSI to retrieve users by project. 44 | 45 | There has to be a way to determine the unique key for the relationship, to solve this, override ```TKey GetRelationKey(TKey parent1Key, TKey parent2Key)``` abstract method. An easy way to make this key unique is just to concatenate both parents' keys. 46 | 47 | ```cs 48 | protected override string GetRelationKey(string parent1Key, string parent2Key) 49 | { 50 | return parent1Key + parent2Key; 51 | } 52 | ``` 53 | 54 | The typical implementation of the other abstract methods will focus only on the data attributes to be mapped to and from DynamoDB. 55 | 56 | ```cs 57 | protected override DynamoDBItem ToDynamoDb(UserProject item) 58 | { 59 | var dbItem = new DynamoDBItem(); 60 | dbItem.AddString("UserId", item.UserId); 61 | dbItem.AddString("ProjectId", item.ProjectId); 62 | dbItem.AddString("Role", item.Role); 63 | return dbItem; 64 | } 65 | ``` 66 | 67 | ```cs 68 | protected override UserProject FromDynamoDb(DynamoDBItem item) 69 | { 70 | var result = new UserProject(); 71 | result.UserId = item.GetString("UserId"); 72 | result.ProjectId = item.GetString("ProjectId"); 73 | result.Role = item.GetString("Role"); 74 | return result; 75 | } 76 | ``` 77 | 78 | Optionally (and recommended), define your own interface so it exposes methods with the relevant parameter and return values, it's also good practice to separate interface from implementation. 79 | 80 | ```cs 81 | public interface IUserProjectRepository 82 | { 83 | Task AddProjectToUser(UserProject userProject); 84 | 85 | Task RemoveProjectFromUser(string userId, string projectId); 86 | 87 | Task> GetProjectsByUserAsync(string userId); 88 | 89 | Task> GetUsersByProjectAsync(string projectId); 90 | } 91 | ``` 92 | 93 | Make the repository class to implement the interface. 94 | 95 | ```cs 96 | public class UserProjectRepository : AssociativeEntityRepository, IUserProjectRepository 97 | { 98 | } 99 | ``` 100 | 101 | And add code to each method, it should be simple, most of the effort is to adapt to the custom interface. 102 | 103 | ```cs 104 | public async Task AddProjectToUser(UserProject userProject) 105 | { 106 | await AddItemAsync(userProject.UserId, userProject.ProjectId, userProject); 107 | } 108 | 109 | public async Task RemoveProjectFromUser(string userId, string projectId) 110 | { 111 | await DeleteItemAsync(userId, projectId); 112 | } 113 | 114 | public async Task> GetProjectsByUserAsync(string userId) 115 | { 116 | return await TableQueryItemsByParentIdAsync(userId); 117 | } 118 | 119 | public async Task> GetUsersByProjectAsync(string projectId) 120 | { 121 | return await GSI1QueryItemsByParentIdAsync(projectId); 122 | } 123 | ``` 124 | -------------------------------------------------------------------------------- /docs/usage-one-to-many.md: -------------------------------------------------------------------------------- 1 | # Usage - Item collection - One to many relationship 2 | 3 | The ```one``` part in a ```one to many``` relationship is treated the same way as single entities with no relationship. This section explains the ```many``` part in a ```one to many``` relationship. 4 | 5 | Define your entity as a POCO, it's entirely optional to include the parent entity identifier, in this example it's not included. It's left to the user's choice in accordance with the rest of the model. 6 | 7 | > **Note** - In this example one **User** has many **Games**. 8 | 9 | ```cs 10 | public class Game 11 | { 12 | public string Id { get; set; } 13 | 14 | public string Name { get; set; } 15 | } 16 | ``` 17 | 18 | Inherit from ```DependentEntityRepository``` abstract class. This class provides methods intended to be used by dependent entities which require the parent entity identifier and this is reflected in the way the PK and SK value are generated. 19 | 20 | ```cs 21 | public class GameRepository : DependentEntityRepository 22 | { 23 | } 24 | ``` 25 | 26 | Define partition key prefix and sort key prefix. This is the value that will be used when generating the values for PK, SK and GSI1. 27 | 28 | Set PKPrefix to a value that identifies the **parent entity** within the data model i.e ```"USER"```. 29 | 30 | Set SKPrefix to a value that identifies the **entity in question** within the data model i.e ```"GAME"```. 31 | 32 | ```cs 33 | public GameRepository(string tableName, string serviceUrl = null) : base(tableName, serviceUrl) 34 | { 35 | PKPrefix = "USER"; 36 | SKPrefix = "GAME"; 37 | } 38 | ``` 39 | 40 | Internally, the ```GSI1``` attribute will be left empty so when the GSI1 Index is queried, these items are not found or confused with another entity. 41 | 42 | All this will enable the method ```TableQueryItemsByParentIdAsync(TKey)``` to retrieve the correct information. 43 | 44 | The typical implementation of the abstract methods will focus only on the data attributes to be mapped to and from DynamoDB. 45 | 46 | ```cs 47 | protected override DynamoDBItem ToDynamoDb(Game item) 48 | { 49 | var dbItem = new DynamoDBItem(); 50 | dbItem.AddString("Id", item.Id); 51 | dbItem.AddString("Name", item.Name); 52 | return dbItem; 53 | } 54 | ``` 55 | 56 | ```cs 57 | protected override Game FromDynamoDb(DynamoDBItem item) 58 | { 59 | var result = new Game(); 60 | result.Id = item.GetString("Id"); 61 | result.Name = item.GetString("Name"); 62 | return result; 63 | } 64 | ``` 65 | 66 | Optionally (and recommended), define your own interface so it exposes methods with the relevant parameter and return values, it's also good practice to separate interface from implementation. 67 | 68 | ```cs 69 | public interface IGameRepository 70 | { 71 | Task AddGame(string userId, Game game); 72 | 73 | Task DeleteGame(string userId, string gameId); 74 | 75 | Task UpdateGame(string userId, Game game); 76 | 77 | Task> GetGameList(string userId); 78 | 79 | Task GetGame(string userId, string gameId); 80 | 81 | Task BatchAddGames(string userId, IEnumerable items); 82 | 83 | Task BatchDeleteGames(string userId, IEnumerable items); 84 | } 85 | ``` 86 | 87 | Make the repository class to implement the interface. 88 | 89 | ```cs 90 | public class GameRepository : DependentEntityRepository, IGameRepository 91 | { 92 | } 93 | ``` 94 | 95 | And add code to each method, it should be simple, most of the effort is to adapt to the custom interface. 96 | 97 | ```cs 98 | public async Task AddGame(string userId, Game game) 99 | { 100 | await AddItemAsync(userId, game.Id, game); 101 | } 102 | 103 | public async Task DeleteGame(string userId, string gameId) 104 | { 105 | await DeleteItemAsync(userId, gameId); 106 | } 107 | 108 | public async Task GetGame(string userId, string gameId) 109 | { 110 | return await GetItemAsync(userId, gameId); 111 | } 112 | 113 | public async Task> GetGameList(string userId) 114 | { 115 | return await TableQueryItemsByParentIdAsync(userId); 116 | } 117 | 118 | public async Task UpdateGame(string userId, Game game) 119 | { 120 | await AddItemAsync(userId, game.Id, game); 121 | } 122 | 123 | public async Task BatchAddGames(string userId, IEnumerable items) 124 | { 125 | await BatchAddItemsAsync(userId, items.Select(x => new KeyValuePair(x.Id, x))); 126 | } 127 | 128 | public async Task BatchDeleteGames(string userId, IEnumerable items) 129 | { 130 | await BatchDeleteItemsAsync(userId, items.Select(x => x.Id)); 131 | } 132 | 133 | ``` 134 | -------------------------------------------------------------------------------- /src/ConsoleDynamoDbRepository/ConsoleDynamoDbRepository.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/ConsoleDynamoDbRepository/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | using System.Threading.Tasks; 5 | using DynamoDbRepository; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using SampleDynamoDbRepository; 8 | 9 | namespace ConsoleDynamoDbRepository 10 | { 11 | class Program 12 | { 13 | private static readonly string _tableName = "dynamodb_test_table"; 14 | static async Task Main(string[] args) 15 | { 16 | // await TestProjectRepositoryCRUD(); 17 | 18 | // await TestUserRepositoryAddItemsOneByOne(); 19 | 20 | // Console.ReadKey(); 21 | 22 | 23 | // await TestUserRepositoryDeleteItemsOneByOne(); 24 | 25 | // await TestUserRepositoryBatchAddItems(); 26 | 27 | // await TestGameRepositoryBatchAddItems(); 28 | 29 | // Console.ReadKey(); 30 | 31 | 32 | // await TestUserRepositoryBatchDeleteItems(); 33 | 34 | // await TestGameRepositoryBatchDeleteItems(); 35 | 36 | // await TestGameRepositorySpecificOperations(); 37 | 38 | // await TestUserProjectRepositoryManyToMany(); 39 | 40 | 41 | // await TestCRUD_UserRepository(); 42 | 43 | // await TestCRUD_GameRepo_GenericMethods(); 44 | 45 | // await TestCRUD_PersonRepository(); 46 | 47 | // DI example 48 | var serviceProvider = new ServiceCollection() 49 | // Add repositories via DI 50 | .AddTransient( 51 | x => new UserRepository(_tableName) // this value should maybe come from configuration 52 | ) 53 | // Build the service provider 54 | .BuildServiceProvider(false); 55 | 56 | using (var scope = serviceProvider.CreateScope()) 57 | { 58 | // Instantiate the Repo 59 | var userRepo = serviceProvider.GetRequiredService(); 60 | 61 | // var users = await userRepo.GetUserList(); 62 | // foreach (var item in users) 63 | // { 64 | // Console.WriteLine(JsonSerializer.Serialize(item)); 65 | // } 66 | 67 | await TestCRUD_UserRepository(userRepo); 68 | } 69 | } 70 | 71 | private static async Task TestCRUD_GameRepository() 72 | { 73 | var repo = new GameRepository(_tableName); 74 | var g1 = new Game { Id = "G1", Name = "Game G1" }; 75 | Console.WriteLine("* Creating game G1"); 76 | await repo.AddItemAsync("U1", "G1", g1); 77 | 78 | var g2 = new Game { Id = "G2", Name = "Game G2" }; 79 | Console.WriteLine("* Creating game G2"); 80 | await repo.AddItemAsync("U1", "G2", g2); 81 | 82 | Console.WriteLine("* Getting all users"); 83 | var games = await repo.TableQueryItemsByParentIdAsync("U1"); 84 | foreach (var item in games) 85 | { 86 | Console.WriteLine(JsonSerializer.Serialize(item)); 87 | } 88 | 89 | Console.WriteLine("* Getting game G1"); 90 | var found1 = await repo.GetItemAsync("U1", g1.Id); 91 | Console.WriteLine(JsonSerializer.Serialize(found1)); 92 | 93 | Console.WriteLine("* Getting game G2"); 94 | var found2 = await repo.GetItemAsync("U1", g2.Id); 95 | Console.WriteLine(JsonSerializer.Serialize(found2)); 96 | 97 | Console.WriteLine("* Deleting game G1"); 98 | await repo.DeleteItemAsync("U1", g1.Id); 99 | 100 | Console.WriteLine("* Deleting game G2"); 101 | await repo.DeleteItemAsync("U1", g2.Id); 102 | } 103 | 104 | private static async Task TestCRUD_UserRepository(IUserRepository repo) 105 | { 106 | // IUserRepository repo = new UserRepository(_tableName); 107 | 108 | var u1 = new User { Id = "U1", Name = "userU1", FirstName = "User", LastName = "U1", Email = "u1@test.com" }; 109 | Console.WriteLine("* Creating user U1"); 110 | await repo.AddUser(u1); 111 | 112 | var u2 = new User { Id = "U2", Name = "userU2", FirstName = "User", LastName = "U2", Email = "u2@test.com" }; 113 | Console.WriteLine("* Creating user U2"); 114 | await repo.AddUser(u2); 115 | 116 | Console.WriteLine("* Getting all users"); 117 | var users = await repo.GetUserList(); 118 | foreach (var item in users) 119 | { 120 | Console.WriteLine(JsonSerializer.Serialize(item)); 121 | } 122 | 123 | Console.WriteLine("* Getting user U1"); 124 | var found1 = await repo.GetUser(u1.Id); 125 | Console.WriteLine(JsonSerializer.Serialize(found1)); 126 | 127 | Console.WriteLine("* Getting user U2"); 128 | var found2 = await repo.GetUser(u2.Id); 129 | Console.WriteLine(JsonSerializer.Serialize(found2)); 130 | 131 | Console.WriteLine("* Deleting user U1"); 132 | await repo.DeleteUser(u1.Id); 133 | 134 | Console.WriteLine("* Deleting user U2"); 135 | await repo.DeleteUser(u2.Id); 136 | } 137 | 138 | public static async Task TestCRUD_PersonRepository() 139 | { 140 | // Create a new PersonRepository 141 | ISimpleRepository repo = new PersonRepository(_tableName); 142 | 143 | // Prepare a Person instance 144 | var p1 = new Person 145 | { 146 | Id = 1, 147 | Name = "personA", 148 | Email = "pa@test.com", 149 | Age = 35 150 | }; 151 | 152 | Console.WriteLine("* Adding Person 1"); 153 | // Add a new person 154 | await repo.Add(p1); 155 | 156 | Console.WriteLine("* Getting the list"); 157 | // Get the full list 158 | var list = await repo.GetList(); 159 | foreach (var item in list) 160 | { 161 | Console.WriteLine(JsonSerializer.Serialize(item)); 162 | } 163 | 164 | Console.ReadKey(); 165 | 166 | Console.WriteLine("* Getting Person 1"); 167 | // Get an individual Person by its Id 168 | var found1 = await repo.Get(p1.Id); 169 | Console.WriteLine(JsonSerializer.Serialize(found1)); 170 | 171 | Console.WriteLine("* Deleting Person 1"); 172 | // Delete an individual Person by its Id 173 | await repo.Delete(p1.Id); 174 | } 175 | 176 | private static async Task TestUserProjectRepositoryManyToMany() 177 | { 178 | IUserRepository userRepo = new UserRepository(_tableName); 179 | 180 | var u1 = new User { Id = "U1", Name = "User 1", FirstName = "User", LastName = "A", Email = "a@test.com" }; 181 | await userRepo.AddUser(u1); 182 | 183 | 184 | IUserProjectRepository repo = new UserProjectRepository(_tableName); 185 | 186 | var u1p1 = new UserProject { UserId = u1.Id, ProjectId = "P1", Role = "owner" }; 187 | await repo.AddProjectToUser(u1p1); 188 | var u1p2 = new UserProject { UserId = u1.Id, ProjectId = "P2", Role = "member" }; 189 | await repo.AddProjectToUser(u1p2); 190 | 191 | Console.WriteLine("Getting projects by user U1"); 192 | var allUPU1 = await repo.GetProjectsByUserAsync(u1.Id); 193 | foreach (var item in allUPU1) 194 | { 195 | Console.WriteLine(JsonSerializer.Serialize(item)); 196 | } 197 | 198 | Console.WriteLine("Getting users by project P1"); 199 | var usersP1 = await repo.GetUsersByProjectAsync("P1"); 200 | foreach (var item in usersP1) 201 | { 202 | Console.WriteLine(JsonSerializer.Serialize(item)); 203 | } 204 | Console.WriteLine("Getting users by project P2"); 205 | var usersP2 = await repo.GetUsersByProjectAsync("P2"); 206 | foreach (var item in usersP2) 207 | { 208 | Console.WriteLine(JsonSerializer.Serialize(item)); 209 | } 210 | 211 | Console.WriteLine("Deleting projects P1 and P2 for user U1"); 212 | await repo.RemoveProjectFromUser(u1p1.UserId, u1p1.ProjectId); 213 | await repo.RemoveProjectFromUser(u1p2.UserId, u1p2.ProjectId); 214 | 215 | Console.WriteLine("Getting projects by user U1 - should be empty"); 216 | var deletedUPU1 = await repo.GetProjectsByUserAsync(u1.Id); 217 | foreach (var item in deletedUPU1) 218 | { 219 | Console.WriteLine(JsonSerializer.Serialize(item)); 220 | } 221 | } 222 | 223 | private static async Task TestGameRepositorySpecificOperations() 224 | { 225 | var userId = "U1"; 226 | IGameRepository repo = new GameRepository(_tableName); 227 | 228 | for (int i = 0; i < 5; i++) 229 | { 230 | var g = new Game { Id = "GA" + i, Name = "Game A" + i }; 231 | Console.WriteLine($"Adding {g.Id}"); 232 | await repo.AddGame(userId, g); 233 | } 234 | 235 | Console.ReadKey(); 236 | 237 | var games = await repo.GetGameList(userId); 238 | foreach (var item in games) 239 | { 240 | Console.WriteLine(JsonSerializer.Serialize(item)); 241 | } 242 | 243 | Console.ReadKey(); 244 | 245 | for (int i = 0; i < 5; i++) 246 | { 247 | var gameId = "GA" + i; 248 | Console.WriteLine($"Deleting {gameId}"); 249 | await repo.DeleteGame(userId, gameId); 250 | } 251 | } 252 | 253 | private static async Task TestUserRepositoryDeleteItemsOneByOne() 254 | { 255 | IUserRepository repo = new UserRepository(_tableName); 256 | for (int i = 0; i < 20; i++) 257 | { 258 | var uA = new User { Id = "A" + i }; 259 | Console.WriteLine("* Deleting user A" + i); 260 | await repo.DeleteUser(uA.Id); 261 | } 262 | } 263 | 264 | private static async Task TestUserRepositoryBatchDeleteItems() 265 | { 266 | IUserRepository repo = new UserRepository(_tableName); 267 | var itemsToDelete = new List(); 268 | for (int i = 50; i < 60; i++) 269 | { 270 | var uA = new User { Id = "A" + i }; 271 | itemsToDelete.Add(uA); 272 | Console.WriteLine("* Adding to delete list, user " + uA.Id); 273 | 274 | } 275 | await repo.BatchDeleteUsers(itemsToDelete); 276 | Console.WriteLine("***** Done deleting all users"); 277 | 278 | Console.WriteLine("* Getting all users"); 279 | var users = await repo.GetUserList(); 280 | foreach (var item in users) 281 | { 282 | Console.WriteLine(JsonSerializer.Serialize(item)); 283 | } 284 | } 285 | 286 | private static async Task TestGameRepositoryBatchDeleteItems() 287 | { 288 | var user = new User { Id = "U1" }; 289 | 290 | IGameRepository repo = new GameRepository(_tableName); 291 | var itemsToDelete = new List(); 292 | for (int i = 50; i < 60; i++) 293 | { 294 | var g = new Game { Id = "G" + i }; 295 | itemsToDelete.Add(g); 296 | Console.WriteLine("* Adding to delete list, game " + g.Id); 297 | 298 | } 299 | await repo.BatchDeleteGames(user.Id, itemsToDelete); 300 | Console.WriteLine("***** Done deleting all games"); 301 | 302 | Console.WriteLine("* Getting all games"); 303 | var games = await repo.GetGameList(user.Id); 304 | foreach (var item in games) 305 | { 306 | Console.WriteLine(JsonSerializer.Serialize(item)); 307 | } 308 | } 309 | 310 | private static async Task TestUserRepositoryAddItemsOneByOne() 311 | { 312 | IUserRepository repo = new UserRepository(_tableName); 313 | for (int i = 0; i < 20; i++) 314 | { 315 | var uA = new User { Id = "A" + i, Name = "userA" + i, FirstName = "User" + i, LastName = "A" + i, Email = $"a{i}@test.com" }; 316 | Console.WriteLine("* Creating user A" + i); 317 | await repo.AddUser(uA); 318 | } 319 | } 320 | 321 | private static async Task TestUserRepositoryBatchAddItems() 322 | { 323 | IUserRepository repo = new UserRepository(_tableName); 324 | var itemsToCreate = new List(); 325 | for (int i = 50; i < 60; i++) 326 | { 327 | var uA = new User { Id = "A" + i, Name = "userA" + i, FirstName = "User" + i, LastName = "A" + i, Email = $"a{i}@test.com" }; 328 | itemsToCreate.Add(uA); 329 | Console.WriteLine("* Adding to list user " + uA.Id); 330 | 331 | } 332 | await repo.BatchAddUsers(itemsToCreate); 333 | Console.WriteLine("***** Done adding all users"); 334 | 335 | Console.WriteLine("* Getting all users"); 336 | var users = await repo.GetUserList(); 337 | foreach (var item in users) 338 | { 339 | Console.WriteLine(JsonSerializer.Serialize(item)); 340 | } 341 | } 342 | 343 | private static async Task TestGameRepositoryBatchAddItems() 344 | { 345 | var user = new User { Id = "U1" }; 346 | 347 | IGameRepository repo = new GameRepository(_tableName); 348 | var itemsToCreate = new List(); 349 | for (int i = 50; i < 60; i++) 350 | { 351 | var g = new Game { Id = "G" + i, Name = "Game G" + i }; 352 | itemsToCreate.Add(g); 353 | Console.WriteLine("* Adding to list game " + g.Id); 354 | 355 | } 356 | await repo.BatchAddGames(user.Id, itemsToCreate); 357 | Console.WriteLine("***** Done adding all games"); 358 | 359 | Console.WriteLine("* Getting all games"); 360 | var items = await repo.GetGameList(user.Id); 361 | foreach (var item in items) 362 | { 363 | Console.WriteLine(JsonSerializer.Serialize(item)); 364 | } 365 | } 366 | 367 | private static async Task TestProjectRepositoryCRUD() 368 | { 369 | IProjectRepository repo = new ProjectRepository(_tableName); 370 | 371 | var pA = new Project { Id = "A", Name = "Project A", Description = "Desc proj A" }; 372 | Console.WriteLine("* Creating project A"); 373 | await repo.AddProject(pA); 374 | 375 | Console.WriteLine("* Retrieving project A"); 376 | var ppA = await repo.GetProject("A"); 377 | if (ppA != null) 378 | Console.WriteLine(JsonSerializer.Serialize(ppA)); 379 | else 380 | Console.WriteLine("not found"); 381 | 382 | Console.ReadKey(); 383 | 384 | 385 | pA.Name = "Project AA"; 386 | pA.Description = "Desc proj AA"; 387 | Console.WriteLine("* Updating project A - renamed to AA"); 388 | await repo.UpdateProject(pA); 389 | 390 | Console.WriteLine("* Retrieving project A after update"); 391 | var pAUpdated = await repo.GetProject("A"); 392 | if (pAUpdated != null) 393 | Console.WriteLine(JsonSerializer.Serialize(pAUpdated)); 394 | else 395 | Console.WriteLine("not found"); 396 | 397 | Console.ReadKey(); 398 | 399 | Console.WriteLine("* Deleting project A"); 400 | await repo.DeleteProject("A"); 401 | 402 | Console.WriteLine("* Retrieving project A after deletion"); 403 | var deletedA = await repo.GetProject("A"); 404 | if (deletedA != null) 405 | Console.WriteLine(JsonSerializer.Serialize(deletedA)); 406 | else 407 | Console.WriteLine("not found"); 408 | } 409 | 410 | 411 | 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /src/DynamoDbRepository/DynamoDB/DynamoDBClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Amazon.DynamoDBv2; 6 | using Amazon.DynamoDBv2.Model; 7 | 8 | namespace DynamoDbRepository 9 | { 10 | public class DynamoDBClient 11 | { 12 | protected readonly IAmazonDynamoDB _dynamoDbClient; 13 | protected readonly string TableName; 14 | 15 | public DynamoDBClient(string tableName, string serviceUrl = null) 16 | { 17 | TableName = tableName; 18 | if (serviceUrl != null) 19 | { 20 | var config = new AmazonDynamoDBConfig { ServiceURL = serviceUrl }; 21 | _dynamoDbClient = new AmazonDynamoDBClient(config); 22 | } 23 | else 24 | { 25 | _dynamoDbClient = new AmazonDynamoDBClient(); 26 | } 27 | } 28 | 29 | private DynamoDBItem DynamoDBKey(string pk, string sk) 30 | { 31 | var dbItem = new DynamoDBItem(); 32 | dbItem.AddPK(pk); 33 | dbItem.AddSK(sk); 34 | return dbItem; 35 | } 36 | 37 | 38 | 39 | public async Task PutItemAsync(string pk, string sk, DynamoDBItem item) 40 | { 41 | if (item == null) 42 | throw new ArgumentNullException(nameof(item)); 43 | 44 | var dbItemKey = DynamoDBKey(pk, sk); 45 | var dbItemData = dbItemKey.MergeData(item); 46 | 47 | var putItemRq = new PutItemRequest 48 | { 49 | TableName = TableName, 50 | Item = dbItemData.ToDictionary() 51 | }; 52 | 53 | var result = await _dynamoDbClient.PutItemAsync(putItemRq); 54 | } 55 | 56 | 57 | public async Task DeleteItemAsync(string pkId, string skId) 58 | { 59 | var dbItemKey = DynamoDBKey(pkId, skId); 60 | var delItemRq = new DeleteItemRequest 61 | { 62 | TableName = TableName, 63 | Key = dbItemKey.ToDictionary() 64 | }; 65 | var result = await _dynamoDbClient.DeleteItemAsync(delItemRq); 66 | } 67 | 68 | public async Task GetItemAsync(string pk, string sk) 69 | { 70 | var dbItemKey = DynamoDBKey(pk, sk); 71 | var getitemRq = new GetItemRequest 72 | { 73 | TableName = TableName, 74 | Key = dbItemKey.ToDictionary() 75 | }; 76 | var getitemResponse = await _dynamoDbClient.GetItemAsync(getitemRq); 77 | return new DynamoDBItem(getitemResponse.Item); 78 | } 79 | 80 | public async Task BatchAddItemsAsync(IEnumerable items) 81 | { 82 | var requests = new List(); 83 | foreach (var item in items) 84 | { 85 | var putRq = new PutRequest(item.ToDictionary()); 86 | requests.Add(new WriteRequest(putRq)); 87 | } 88 | 89 | var batchRq = new Dictionary> { { TableName, requests } }; 90 | await _dynamoDbClient.BatchWriteItemAsync(batchRq); 91 | } 92 | 93 | public async Task BatchDeleteItemsAsync(IEnumerable items) 94 | { 95 | var requests = new List(); 96 | foreach (var item in items) 97 | { 98 | var deleteRq = new DeleteRequest(item.ToDictionary()); 99 | requests.Add(new WriteRequest(deleteRq)); 100 | } 101 | 102 | var batchRq = new Dictionary> { { TableName, requests } }; 103 | var result = await _dynamoDbClient.BatchWriteItemAsync(batchRq); 104 | } 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | // TODO: leaky abstraction here !!! QueryRequest 122 | public async Task> QueryAsync(QueryRequest queryRequest) 123 | { 124 | var queryResponse = await _dynamoDbClient.QueryAsync(queryRequest); 125 | return queryResponse.Items.Select(x => new DynamoDBItem(x)).ToList(); 126 | } 127 | 128 | // TODO: leaky abstraction here !!! QueryRequest 129 | public QueryRequest GetGSI1QueryRequest(string gsi1, string skPrefix) 130 | { 131 | return new QueryRequest 132 | { 133 | TableName = TableName, 134 | IndexName = DynamoDBConstants.GSI1, 135 | KeyConditionExpression = $"{DynamoDBConstants.GSI1} = :gsi1_value and begins_with({DynamoDBConstants.SK}, :sk_prefix)", 136 | ExpressionAttributeValues = new Dictionary 137 | { 138 | { ":gsi1_value", new AttributeValue(gsi1) }, 139 | { ":sk_prefix", new AttributeValue(skPrefix) } 140 | } 141 | }; 142 | } 143 | 144 | // TODO: leaky abstraction here !!! QueryRequest 145 | public QueryRequest GetTableQueryRequest(string pk, string skPrefix) 146 | { 147 | return new QueryRequest 148 | { 149 | TableName = TableName, 150 | KeyConditionExpression = $"{DynamoDBConstants.PK} = :pk_value and begins_with({DynamoDBConstants.SK}, :sk_prefix)", 151 | ExpressionAttributeValues = new Dictionary 152 | { 153 | { ":pk_value", new AttributeValue(pk) }, 154 | { ":sk_prefix", new AttributeValue(skPrefix) } 155 | } 156 | }; 157 | } 158 | } 159 | } -------------------------------------------------------------------------------- /src/DynamoDbRepository/DynamoDB/DynamoDBConstants.cs: -------------------------------------------------------------------------------- 1 | namespace DynamoDbRepository 2 | { 3 | public class DynamoDBConstants 4 | { 5 | public const string PK = "PK"; 6 | public const string SK = "SK"; 7 | public const string GSI1 = "GSI1"; 8 | public const string Separator = "#"; 9 | } 10 | } -------------------------------------------------------------------------------- /src/DynamoDbRepository/DynamoDB/DynamoDBItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Amazon.DynamoDBv2.Model; 5 | 6 | namespace DynamoDbRepository 7 | { 8 | public class DynamoDBItem 9 | { 10 | private Dictionary _data = new Dictionary(); 11 | 12 | public DynamoDBItem() 13 | { 14 | } 15 | 16 | internal DynamoDBItem(Dictionary item) 17 | { 18 | _data = item; 19 | } 20 | 21 | public DynamoDBItem MergeData(DynamoDBItem data) 22 | { 23 | var dataDict = data.ToDictionary(); 24 | var merged = _data.Union(dataDict).ToDictionary(k => k.Key, v => v.Value); 25 | return new DynamoDBItem(merged); 26 | } 27 | 28 | public void AddPK(string value) 29 | { 30 | AddKeyAttrValue(DynamoDBConstants.PK, new AttributeValue(value)); 31 | } 32 | 33 | public void AddSK(string value) 34 | { 35 | AddKeyAttrValue(DynamoDBConstants.SK, new AttributeValue(value)); 36 | } 37 | 38 | public void AddGSI1(string value) 39 | { 40 | AddKeyAttrValue(DynamoDBConstants.GSI1, new AttributeValue(value)); 41 | } 42 | 43 | public void AddString(string key, string value) 44 | { 45 | AddKeyAttrValue(key, new AttributeValue(value)); 46 | } 47 | 48 | public void AddNumber(string key, int value) 49 | { 50 | AddKeyAttrValue(key, BaseNumberAttributeValue(Convert.ToString(value))); 51 | } 52 | 53 | public void AddNumber(string key, double value) 54 | { 55 | AddKeyAttrValue(key, BaseNumberAttributeValue(Convert.ToString(value))); 56 | } 57 | 58 | public bool IsEmpty 59 | { 60 | get { return _data.Count == 0; } 61 | } 62 | 63 | public string GetString(string key) 64 | { 65 | return _data.GetValueOrDefault(key)?.S; 66 | } 67 | 68 | public int GetInt32(string key) 69 | { 70 | return Convert.ToInt32(_data.GetValueOrDefault(key)?.N); 71 | } 72 | 73 | public double GetDouble(string key) 74 | { 75 | return Convert.ToDouble(_data.GetValueOrDefault(key)?.N); 76 | } 77 | 78 | private void AddKeyAttrValue(string key, AttributeValue value) 79 | { 80 | if (!_data.ContainsKey(key)) 81 | _data.Add(key, value); 82 | else 83 | _data[key] = value; 84 | } 85 | 86 | private AttributeValue BaseNumberAttributeValue(string value) 87 | { 88 | var result = new AttributeValue(); 89 | result.N = value; 90 | return result; 91 | } 92 | 93 | internal Dictionary ToDictionary() 94 | { 95 | return _data; 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /src/DynamoDbRepository/DynamoDbRepository.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Abel Perez 13 | DynamoDB; repository; 14 | https://github.com/abelperezok/DynamoDB-BaseRepository 15 | git 16 | git://github.com/abelperezok/DynamoDB-BaseRepository 17 | true 18 | 19 | C# .NET Core implementation of the repository pattern using DynamoDB. 20 | It uses single table and hierarchical data modelling approach overloading the partition and sort key as well secondary index. 21 | 22 | dynamodb; csharp; dotnet-core; dotnet-standard; repository-pattern; single-table-design; partition; sort-key; crud; batch; 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/DynamoDbRepository/Repository/AssociativeEntityRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | 5 | namespace DynamoDbRepository 6 | { 7 | public abstract class AssociativeEntityRepository : RepositoryBase 8 | where TEntity : class 9 | { 10 | public AssociativeEntityRepository(string tableName, string serviceUrl = null) : base(tableName, serviceUrl) 11 | { 12 | } 13 | 14 | protected abstract TKey GetRelationKey(TKey parent1Key, TKey parent2Key); 15 | 16 | public async Task AddItemAsync(TKey parent1Key, TKey parent2Key, TEntity item) 17 | { 18 | var relationKey = GetRelationKey(parent1Key, parent2Key); 19 | var pk = PKValue(parent1Key); 20 | var sk = SKValue(relationKey); 21 | var dbItem = ToDynamoDb(item); 22 | // TODO: try to make the value from dbItem to take precedence 23 | dbItem.AddGSI1(GSI1Value(parent2Key)); 24 | await _dynamoDbClient.PutItemAsync(pk, sk, dbItem); 25 | } 26 | 27 | public async Task DeleteItemAsync(TKey parent1Key, TKey parent2Key) 28 | { 29 | var relationKey = GetRelationKey(parent1Key, parent2Key); 30 | var pk = PKValue(parent1Key); 31 | var sk = SKValue(relationKey); 32 | await _dynamoDbClient.DeleteItemAsync(pk, sk); 33 | } 34 | 35 | 36 | public async Task> GSI1QueryItemsByParentIdAsync(TKey parentKey) 37 | { 38 | var gsi1 = GSI1Value(parentKey); 39 | var queryRq = _dynamoDbClient.GetGSI1QueryRequest(gsi1, SKPrefix); 40 | 41 | var items = await _dynamoDbClient.QueryAsync(queryRq); 42 | return items.Select(FromDynamoDb).ToList(); 43 | } 44 | 45 | public async Task> TableQueryItemsByParentIdAsync(TKey parentKey) 46 | { 47 | var pk = PKValue(parentKey); 48 | var queryRq = _dynamoDbClient.GetTableQueryRequest(pk, SKPrefix); 49 | var result = await _dynamoDbClient.QueryAsync(queryRq); 50 | return result.Select(FromDynamoDb).ToList(); 51 | } 52 | 53 | public async Task BatchAddItemsAsync(TKey parentKey, IEnumerable> items) 54 | { 55 | var pk = PKValue(parentKey); 56 | var dbItems = new List(); 57 | foreach (var item in items) 58 | { 59 | var relationKey = GetRelationKey(parentKey, item.Key); 60 | var sk = SKValue(relationKey); 61 | var dbItem = ToDynamoDb(item.Value); 62 | dbItem.AddPK(pk); 63 | dbItem.AddSK(sk); 64 | dbItem.AddGSI1(GSI1Value(item.Key)); 65 | dbItems.Add(dbItem); 66 | } 67 | 68 | await _dynamoDbClient.BatchAddItemsAsync(dbItems); 69 | } 70 | 71 | public async Task BatchDeleteItemsAsync(TKey parentKey, IEnumerable items) 72 | { 73 | var pk = PKValue(parentKey); 74 | var dbItems = new List(); 75 | foreach (var item in items) 76 | { 77 | var relationKey = GetRelationKey(parentKey, item); 78 | var dbItem = new DynamoDBItem(); 79 | dbItem.AddPK(pk); 80 | dbItem.AddSK(SKValue(relationKey)); 81 | 82 | dbItems.Add(dbItem); 83 | } 84 | 85 | await _dynamoDbClient.BatchDeleteItemsAsync(dbItems); 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /src/DynamoDbRepository/Repository/DependentEntityRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace DynamoDbRepository 7 | { 8 | public abstract class DependentEntityRepository : RepositoryBase 9 | where TEntity : class 10 | { 11 | public DependentEntityRepository(string tableName, string serviceUrl = null) : base(tableName, serviceUrl) 12 | { 13 | } 14 | 15 | public async Task AddItemAsync(TKey parentKey, TKey entityKey, TEntity item) 16 | { 17 | var pk = PKValue(parentKey); 18 | var sk = SKValue(entityKey); 19 | var dbItem = ToDynamoDb(item); 20 | await _dynamoDbClient.PutItemAsync(pk, sk, dbItem); 21 | } 22 | 23 | public async Task> TableQueryItemsByParentIdAsync(TKey parentKey) 24 | { 25 | var pk = PKValue(parentKey); 26 | var queryRq = _dynamoDbClient.GetTableQueryRequest(pk, SKPrefix); 27 | var result = await _dynamoDbClient.QueryAsync(queryRq); 28 | return result.Select(FromDynamoDb).ToList(); 29 | } 30 | 31 | public async Task GetItemAsync(TKey parentKey, TKey entityKey) 32 | { 33 | var pk = PKValue(parentKey); 34 | var sk = SKValue(entityKey); 35 | var item = await _dynamoDbClient.GetItemAsync(pk, sk); 36 | if (item.IsEmpty) 37 | return default(TEntity); 38 | return FromDynamoDb(item); 39 | } 40 | 41 | public async Task DeleteItemAsync(TKey parentKey, TKey entityKey) 42 | { 43 | var pk = PKValue(parentKey); 44 | var sk = SKValue(entityKey); 45 | await _dynamoDbClient.DeleteItemAsync(pk, sk); 46 | } 47 | 48 | public async Task BatchAddItemsAsync(TKey parentKey, IEnumerable> items) 49 | { 50 | var pk = PKValue(parentKey); 51 | var dbItems = new List(); 52 | foreach (var item in items) 53 | { 54 | var sk = SKValue(item.Key); 55 | var dbItem = ToDynamoDb(item.Value); 56 | dbItem.AddPK(pk); 57 | dbItem.AddSK(sk); 58 | 59 | dbItems.Add(dbItem); 60 | } 61 | 62 | await _dynamoDbClient.BatchAddItemsAsync(dbItems); 63 | } 64 | 65 | public async Task BatchDeleteItemsAsync(TKey parentKey, IEnumerable items) 66 | { 67 | var pk = PKValue(parentKey); 68 | var dbItems = new List(); 69 | foreach (var item in items) 70 | { 71 | var dbItem = new DynamoDBItem(); 72 | dbItem.AddPK(pk); 73 | dbItem.AddSK(SKValue(item)); 74 | 75 | dbItems.Add(dbItem); 76 | } 77 | 78 | await _dynamoDbClient.BatchDeleteItemsAsync(dbItems); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /src/DynamoDbRepository/Repository/ISimpleRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace DynamoDbRepository 5 | { 6 | public interface ISimpleRepository where TEntity : class 7 | { 8 | Task Add(TEntity item); 9 | Task Delete(TKey key); 10 | Task Get(TKey key); 11 | Task> GetList(); 12 | Task Update(TEntity item); 13 | } 14 | } -------------------------------------------------------------------------------- /src/DynamoDbRepository/Repository/IndependentEntityRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace DynamoDbRepository 7 | { 8 | public abstract class IndependentEntityRepository : RepositoryBase 9 | where TEntity : class 10 | { 11 | 12 | public IndependentEntityRepository(string tableName, string serviceUrl = null) : base(tableName, serviceUrl) 13 | { 14 | } 15 | 16 | public async Task AddItemAsync(TKey key, TEntity item) 17 | { 18 | var pk = PKValue(key); 19 | var sk = SKValue(key); 20 | var dbItem = ToDynamoDb(item); 21 | // TODO: try to make the value from dbItem to take precedence 22 | dbItem.AddGSI1(PKPrefix); 23 | await _dynamoDbClient.PutItemAsync(pk, sk, dbItem); 24 | } 25 | 26 | public async Task> GSI1QueryAllAsync() 27 | { 28 | var queryRq = _dynamoDbClient.GetGSI1QueryRequest(PKPrefix, SKPrefix); 29 | var result = await _dynamoDbClient.QueryAsync(queryRq); 30 | return result.Select(FromDynamoDb).ToList(); 31 | } 32 | 33 | public async Task GetItemAsync(TKey key) 34 | { 35 | var pk = PKValue(key); 36 | var sk = SKValue(key); 37 | var item = await _dynamoDbClient.GetItemAsync(pk, sk); 38 | if (item.IsEmpty) 39 | return default(TEntity); 40 | return FromDynamoDb(item); 41 | } 42 | 43 | public async Task DeleteItemAsync(TKey key) 44 | { 45 | var pk = PKValue(key); 46 | var sk = SKValue(key); 47 | await _dynamoDbClient.DeleteItemAsync(pk, sk); 48 | } 49 | 50 | public async Task BatchAddItemsAsync(IEnumerable> items) 51 | { 52 | var dbItems = new List(); 53 | foreach (var item in items) 54 | { 55 | var dbItem = ToDynamoDb(item.Value); 56 | dbItem.AddPK(PKValue(item.Key)); 57 | dbItem.AddSK(SKValue(item.Key)); 58 | // TODO: try to make the value from dbItem to take precedence 59 | dbItem.AddGSI1(PKPrefix); 60 | 61 | dbItems.Add(dbItem); 62 | } 63 | 64 | await _dynamoDbClient.BatchAddItemsAsync(dbItems); 65 | } 66 | 67 | public async Task BatchDeleteItemsAsync(IEnumerable items) 68 | { 69 | var dbItems = new List(); 70 | foreach (var item in items) 71 | { 72 | var dbItem = new DynamoDBItem(); 73 | dbItem.AddPK(PKValue(item)); 74 | dbItem.AddSK(SKValue(item)); 75 | 76 | dbItems.Add(dbItem); 77 | } 78 | 79 | await _dynamoDbClient.BatchDeleteItemsAsync(dbItems); 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/DynamoDbRepository/Repository/RepositoryBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DynamoDbRepository 4 | { 5 | public abstract class RepositoryBase where TEntity : class 6 | { 7 | protected string PKPrefix = ""; 8 | protected string SKPrefix = ""; 9 | protected string GSI1Prefix = ""; 10 | 11 | protected string PKPattern { get { return $"{PKPrefix}{DynamoDBConstants.Separator}{{0}}"; } } 12 | protected string SKPattern { get { return $"{SKPrefix}{DynamoDBConstants.Separator}{{0}}"; } } 13 | protected string GSI1Pattern { get { return $"{GSI1Prefix}{DynamoDBConstants.Separator}{{0}}"; } } 14 | 15 | protected DynamoDBClient _dynamoDbClient; 16 | 17 | public RepositoryBase(string tableName, string serviceUrl = null) 18 | { 19 | _dynamoDbClient = new DynamoDBClient(tableName, serviceUrl); 20 | } 21 | 22 | protected abstract DynamoDBItem ToDynamoDb(TEntity item); 23 | protected abstract TEntity FromDynamoDb(DynamoDBItem item); 24 | 25 | protected string PKValue(object id) 26 | { 27 | return string.Format(PKPattern, Convert.ToString(id)); 28 | } 29 | 30 | protected string SKValue(object id) 31 | { 32 | return string.Format(SKPattern, Convert.ToString(id)); 33 | } 34 | 35 | protected string GSI1Value(object id) 36 | { 37 | return string.Format(GSI1Pattern, Convert.ToString(id)); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/DynamoDbRepository/Repository/SimpleRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace DynamoDbRepository 5 | { 6 | public abstract class SimpleRepository : IndependentEntityRepository, ISimpleRepository 7 | where TEntity : class 8 | { 9 | public SimpleRepository(string tableName, string serviceUrl = null) : base(tableName, serviceUrl) 10 | { 11 | } 12 | 13 | protected abstract TKey GetEntityKey(TEntity item); 14 | 15 | public async Task Add(TEntity item) 16 | { 17 | var key = GetEntityKey(item); 18 | await AddItemAsync(key, item); 19 | } 20 | 21 | public async Task Delete(TKey key) 22 | { 23 | await DeleteItemAsync(key); 24 | } 25 | 26 | public async Task> GetList() 27 | { 28 | return await GSI1QueryAllAsync(); 29 | } 30 | 31 | public async Task Update(TEntity item) 32 | { 33 | var key = GetEntityKey(item); 34 | await AddItemAsync(key, item); 35 | } 36 | 37 | public async Task Get(TKey key) 38 | { 39 | return await GetItemAsync(key); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/SampleDynamoDbRepository/Game/Game.cs: -------------------------------------------------------------------------------- 1 | namespace SampleDynamoDbRepository 2 | { 3 | public class Game 4 | { 5 | public string Id { get; set; } 6 | 7 | public string Name { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/SampleDynamoDbRepository/Game/GameRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using DynamoDbRepository; 5 | 6 | namespace SampleDynamoDbRepository 7 | { 8 | 9 | public class GameRepository : DependentEntityRepository, IGameRepository 10 | { 11 | public GameRepository(string tableName, string serviceUrl = null) : base(tableName, serviceUrl) 12 | { 13 | PKPrefix = "USER"; 14 | SKPrefix = "GAME"; 15 | } 16 | 17 | protected override DynamoDBItem ToDynamoDb(Game item) 18 | { 19 | var dbItem = new DynamoDBItem(); 20 | dbItem.AddString("Id", item.Id); 21 | dbItem.AddString("Name", item.Name); 22 | return dbItem; 23 | } 24 | 25 | protected override Game FromDynamoDb(DynamoDBItem item) 26 | { 27 | var result = new Game(); 28 | result.Id = item.GetString("Id"); 29 | result.Name = item.GetString("Name"); 30 | return result; 31 | } 32 | 33 | public async Task AddGame(string userId, Game game) 34 | { 35 | await AddItemAsync(userId, game.Id, game); 36 | } 37 | 38 | public async Task DeleteGame(string userId, string gameId) 39 | { 40 | await DeleteItemAsync(userId, gameId); 41 | } 42 | 43 | public async Task GetGame(string userId, string gameId) 44 | { 45 | return await GetItemAsync(userId, gameId); 46 | } 47 | 48 | public async Task> GetGameList(string userId) 49 | { 50 | return await TableQueryItemsByParentIdAsync(userId); 51 | } 52 | 53 | public async Task UpdateGame(string userId, Game game) 54 | { 55 | await AddItemAsync(userId, game.Id, game); 56 | } 57 | 58 | public async Task BatchAddGames(string userId, IEnumerable items) 59 | { 60 | await BatchAddItemsAsync(userId, items.Select(x => new KeyValuePair(x.Id, x))); 61 | } 62 | 63 | public async Task BatchDeleteGames(string userId, IEnumerable items) 64 | { 65 | await BatchDeleteItemsAsync(userId, items.Select(x => x.Id)); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/SampleDynamoDbRepository/Game/IGameRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace SampleDynamoDbRepository 5 | { 6 | public interface IGameRepository 7 | { 8 | Task AddGame(string userId, Game game); 9 | Task DeleteGame(string userId, string gameId); 10 | Task UpdateGame(string userId, Game game); 11 | Task> GetGameList(string userId); 12 | Task GetGame(string userId, string gameId); 13 | Task BatchAddGames(string userId, IEnumerable items); 14 | Task BatchDeleteGames(string userId, IEnumerable items); 15 | } 16 | } -------------------------------------------------------------------------------- /src/SampleDynamoDbRepository/Person/Person.cs: -------------------------------------------------------------------------------- 1 | namespace SampleDynamoDbRepository 2 | { 3 | public class Person 4 | { 5 | public int Id { get; set; } 6 | 7 | public string Name { get; set; } 8 | 9 | public string Email { get; set; } 10 | 11 | public int Age { get; set; } 12 | 13 | public double Height { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/SampleDynamoDbRepository/Person/PersonRepository.cs: -------------------------------------------------------------------------------- 1 | using DynamoDbRepository; 2 | 3 | namespace SampleDynamoDbRepository 4 | { 5 | public class PersonRepository : SimpleRepository 6 | { 7 | public PersonRepository(string tableName, string serviceUrl = null) : base(tableName, serviceUrl) 8 | { 9 | PKPrefix = "PERSON"; 10 | SKPrefix = "METADATA"; 11 | } 12 | 13 | protected override int GetEntityKey(Person item) 14 | { 15 | return item.Id; 16 | } 17 | 18 | protected override DynamoDBItem ToDynamoDb(Person item) 19 | { 20 | var dbItem = new DynamoDBItem(); 21 | dbItem.AddNumber("Id", item.Id); 22 | dbItem.AddString("Name", item.Name); 23 | dbItem.AddString("Email", item.Email); 24 | dbItem.AddNumber("Age", item.Age); 25 | dbItem.AddNumber("Height", item.Height); 26 | return dbItem; 27 | } 28 | 29 | protected override Person FromDynamoDb(DynamoDBItem item) 30 | { 31 | var result = new Person(); 32 | result.Id = item.GetInt32("Id"); 33 | result.Name = item.GetString("Name"); 34 | result.Email = item.GetString("Email"); 35 | result.Age = item.GetInt32("Age"); 36 | result.Height = item.GetDouble("Height"); 37 | return result; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/SampleDynamoDbRepository/Project/IProjectRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace SampleDynamoDbRepository 5 | { 6 | public interface IProjectRepository 7 | { 8 | Task AddProject(Project project); 9 | Task DeleteProject(string projectId); 10 | Task UpdateProject(Project project); 11 | Task> GetProjectList(); 12 | Task GetProject(string projectId); 13 | } 14 | } -------------------------------------------------------------------------------- /src/SampleDynamoDbRepository/Project/Project.cs: -------------------------------------------------------------------------------- 1 | namespace SampleDynamoDbRepository 2 | { 3 | public class Project 4 | { 5 | public string Id { get; set; } 6 | 7 | public string Name { get; set; } 8 | 9 | public string Description { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/SampleDynamoDbRepository/Project/ProjectRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using DynamoDbRepository; 4 | 5 | namespace SampleDynamoDbRepository 6 | { 7 | public class ProjectRepository : IndependentEntityRepository, IProjectRepository 8 | { 9 | public ProjectRepository(string tableName, string serviceUrl = null) : base(tableName, serviceUrl) 10 | { 11 | PKPrefix = "PROJECT"; 12 | SKPrefix = "METADATA"; 13 | } 14 | 15 | protected override DynamoDBItem ToDynamoDb(Project item) 16 | { 17 | var dbItem = new DynamoDBItem(); 18 | dbItem.AddString("Id", item.Id); 19 | dbItem.AddString("Name", item.Name); 20 | dbItem.AddString("Description", item.Description); 21 | return dbItem; 22 | } 23 | 24 | protected override Project FromDynamoDb(DynamoDBItem item) 25 | { 26 | var result = new Project(); 27 | result.Id = item.GetString("Id"); 28 | result.Name = item.GetString("Name"); 29 | result.Description = item.GetString("Description"); 30 | return result; 31 | } 32 | 33 | public async Task AddProject(Project project) 34 | { 35 | await AddItemAsync(project.Id, project); 36 | } 37 | 38 | public async Task DeleteProject(string projectId) 39 | { 40 | await DeleteItemAsync(projectId); 41 | } 42 | 43 | public async Task UpdateProject(Project project) 44 | { 45 | await AddItemAsync(project.Id, project); 46 | } 47 | 48 | public async Task> GetProjectList() 49 | { 50 | return await GSI1QueryAllAsync(); 51 | } 52 | 53 | public async Task GetProject(string projectId) 54 | { 55 | return await GetItemAsync(projectId); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/SampleDynamoDbRepository/SampleDynamoDbRepository.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/SampleDynamoDbRepository/User/IUserRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace SampleDynamoDbRepository 5 | { 6 | public interface IUserRepository 7 | { 8 | Task AddUser(User user); 9 | Task DeleteUser(string userId); 10 | Task UpdateUser(User user); 11 | Task> GetUserList(); 12 | Task GetUser(string userId); 13 | Task BatchAddUsers(IEnumerable items); 14 | Task BatchDeleteUsers(IEnumerable items); 15 | } 16 | } -------------------------------------------------------------------------------- /src/SampleDynamoDbRepository/User/User.cs: -------------------------------------------------------------------------------- 1 | namespace SampleDynamoDbRepository 2 | { 3 | public class User 4 | { 5 | public string Id { get; set; } 6 | 7 | public string Name { get; set; } 8 | 9 | public string FirstName { get; set; } 10 | 11 | public string LastName { get; set; } 12 | 13 | public string Email { get; set; } 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/SampleDynamoDbRepository/User/UserRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using DynamoDbRepository; 6 | 7 | namespace SampleDynamoDbRepository 8 | { 9 | public class UserRepository : IndependentEntityRepository, IUserRepository 10 | { 11 | public UserRepository(string tableName, string serviceUrl = null) : base(tableName, serviceUrl) 12 | { 13 | PKPrefix = "USER"; 14 | SKPrefix = "METADATA"; 15 | } 16 | 17 | protected override DynamoDBItem ToDynamoDb(User item) 18 | { 19 | var dbItem = new DynamoDBItem(); 20 | dbItem.AddString("Id", item.Id); 21 | dbItem.AddString("Name", item.Name); 22 | dbItem.AddString("FirstName", item.FirstName); 23 | dbItem.AddString("LastName", item.LastName); 24 | dbItem.AddString("Email", item.Email); 25 | return dbItem; 26 | } 27 | 28 | protected override User FromDynamoDb(DynamoDBItem item) 29 | { 30 | var result = new User(); 31 | result.Id = item.GetString("Id"); 32 | result.Name = item.GetString("Name"); 33 | result.FirstName = item.GetString("FirstName"); 34 | result.LastName = item.GetString("LastName"); 35 | result.Email = item.GetString("Email"); 36 | return result; 37 | } 38 | 39 | public async Task AddUser(User user) 40 | { 41 | await AddItemAsync(user.Id, user); 42 | } 43 | 44 | public async Task DeleteUser(string userId) 45 | { 46 | await DeleteItemAsync(userId); 47 | } 48 | 49 | public async Task> GetUserList() 50 | { 51 | return await GSI1QueryAllAsync(); 52 | } 53 | 54 | public async Task UpdateUser(User user) 55 | { 56 | await AddItemAsync(user.Id, user); 57 | } 58 | 59 | public async Task GetUser(string userId) 60 | { 61 | return await GetItemAsync(userId); 62 | } 63 | 64 | public async Task BatchAddUsers(IEnumerable items) 65 | { 66 | await BatchAddItemsAsync(items.Select(x => new KeyValuePair(x.Id, x))); 67 | } 68 | 69 | public async Task BatchDeleteUsers(IEnumerable items) 70 | { 71 | await BatchDeleteItemsAsync(items.Select(x => x.Id)); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/SampleDynamoDbRepository/UserProject/IUserProjectRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace SampleDynamoDbRepository 5 | { 6 | public interface IUserProjectRepository 7 | { 8 | Task AddProjectToUser(UserProject userProject); 9 | 10 | Task RemoveProjectFromUser(string userId, string projectId); 11 | 12 | Task> GetProjectsByUserAsync(string userId); 13 | 14 | Task> GetUsersByProjectAsync(string projectId); 15 | 16 | Task BatchAddProjectsToUser(string userId, IEnumerable userProjects); 17 | 18 | Task BatchRemoveProjectsFromUser(string userId, IEnumerable userProjects); 19 | } 20 | } -------------------------------------------------------------------------------- /src/SampleDynamoDbRepository/UserProject/UserProject.cs: -------------------------------------------------------------------------------- 1 | namespace SampleDynamoDbRepository 2 | { 3 | public class UserProject 4 | { 5 | public string UserId { get; set; } 6 | public string ProjectId { get; set; } 7 | public string Role { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/SampleDynamoDbRepository/UserProject/UserProjectRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using DynamoDbRepository; 5 | 6 | namespace SampleDynamoDbRepository 7 | { 8 | public class UserProjectRepository : AssociativeEntityRepository, IUserProjectRepository 9 | { 10 | 11 | public UserProjectRepository(string tableName, string serviceUrl = null) : base(tableName, serviceUrl) 12 | { 13 | PKPrefix = "USER"; 14 | SKPrefix = "USER_PROJECT"; 15 | GSI1Prefix = "PROJECT"; 16 | } 17 | protected override string GetRelationKey(string parent1Key, string parent2Key) 18 | { 19 | return parent1Key + parent2Key; 20 | } 21 | 22 | protected override DynamoDBItem ToDynamoDb(UserProject item) 23 | { 24 | var dbItem = new DynamoDBItem(); 25 | dbItem.AddString("UserId", item.UserId); 26 | dbItem.AddString("ProjectId", item.ProjectId); 27 | dbItem.AddString("Role", item.Role); 28 | return dbItem; 29 | } 30 | 31 | protected override UserProject FromDynamoDb(DynamoDBItem item) 32 | { 33 | var result = new UserProject(); 34 | result.UserId = item.GetString("UserId"); 35 | result.ProjectId = item.GetString("ProjectId"); 36 | result.Role = item.GetString("Role"); 37 | return result; 38 | } 39 | 40 | 41 | public async Task AddProjectToUser(UserProject userProject) 42 | { 43 | await AddItemAsync(userProject.UserId, userProject.ProjectId, userProject); 44 | } 45 | 46 | public async Task RemoveProjectFromUser(string userId, string projectId) 47 | { 48 | await DeleteItemAsync(userId, projectId); 49 | } 50 | 51 | public async Task> GetProjectsByUserAsync(string userId) 52 | { 53 | return await TableQueryItemsByParentIdAsync(userId); 54 | } 55 | 56 | public async Task> GetUsersByProjectAsync(string projectId) 57 | { 58 | return await GSI1QueryItemsByParentIdAsync(projectId); 59 | } 60 | 61 | public async Task BatchAddProjectsToUser(string userId, IEnumerable userProjects) 62 | { 63 | await BatchAddItemsAsync(userId, userProjects.Select(x => new KeyValuePair(x.ProjectId, x))); 64 | } 65 | 66 | public async Task BatchRemoveProjectsFromUser(string userId, IEnumerable userProjects) 67 | { 68 | await BatchDeleteItemsAsync(userId, userProjects.Select(x => x.ProjectId)); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /test/DynamoDbRepository.Tests/DynamoDBDockerFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.InteropServices; 4 | using System.Threading.Tasks; 5 | using Amazon.DynamoDBv2; 6 | using Amazon.DynamoDBv2.Model; 7 | using Docker.DotNet; 8 | using Docker.DotNet.Models; 9 | using Xunit; 10 | 11 | namespace DynamoDbRepository.Tests 12 | { 13 | public class DynamoDBDockerFixture : IAsyncLifetime 14 | { 15 | private DockerClient _dockerClient; 16 | private string _containerId; 17 | private const string dynamodbLocalImage = "amazon/dynamodb-local"; 18 | public string TableName = "test_table"; 19 | public string ServiceUrl = "http://localhost:8000"; 20 | AmazonDynamoDBClient _dynamoDbClient; 21 | 22 | public DynamoDBDockerFixture() 23 | { 24 | _dynamoDbClient = new AmazonDynamoDBClient(new AmazonDynamoDBConfig { ServiceURL = ServiceUrl }); 25 | _dockerClient = new DockerClientConfiguration(new Uri(DockerApiUri())).CreateClient(); 26 | } 27 | 28 | #region [ IAsyncLifetime interface ] 29 | 30 | public async Task InitializeAsync() 31 | { 32 | await PullImage(); 33 | await StartContainer(); 34 | 35 | var createTableResponse = await CreateTableAsync(TableName); 36 | Console.WriteLine($"{createTableResponse.TableDescription.TableStatus}"); 37 | 38 | await WaitUntilTableIsActive(TableName); 39 | } 40 | public async Task DisposeAsync() 41 | { 42 | if (_containerId != null) 43 | { 44 | await DisposeContainer(); 45 | } 46 | } 47 | 48 | #endregion 49 | 50 | 51 | #region [ Docker container handling ] 52 | 53 | private string DockerApiUri() 54 | { 55 | var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 56 | 57 | if (isWindows) 58 | { 59 | return "npipe://./pipe/docker_engine"; 60 | } 61 | 62 | var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); 63 | 64 | if (isLinux) 65 | { 66 | return "unix:///var/run/docker.sock"; 67 | } 68 | 69 | throw new Exception("Was unable to determine what OS this is running on, does not appear to be Windows or Linux!?"); 70 | } 71 | 72 | private async Task PullImage() 73 | { 74 | var imageCreateParams = new ImagesCreateParameters 75 | { 76 | FromImage = dynamodbLocalImage, 77 | Tag = "latest" 78 | }; 79 | await _dockerClient.Images.CreateImageAsync(imageCreateParams, null, new Progress()); 80 | } 81 | 82 | private async Task StartContainer() 83 | { 84 | var containerParams = new CreateContainerParameters 85 | { 86 | Image = dynamodbLocalImage, 87 | ExposedPorts = new Dictionary 88 | { 89 | { "8000", default(EmptyStruct) } 90 | }, 91 | HostConfig = new HostConfig 92 | { 93 | PortBindings = new Dictionary> 94 | { 95 | { "8000", new List{ new PortBinding { HostPort="8000" } } } 96 | }, 97 | PublishAllPorts = true 98 | }, 99 | }; 100 | var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(containerParams); 101 | _containerId = createContainerResponse.ID; 102 | Console.WriteLine($"Container {createContainerResponse.ID} created"); 103 | foreach (var warning in createContainerResponse.Warnings) 104 | { 105 | Console.WriteLine(warning); 106 | } 107 | var containerStarted = await _dockerClient.Containers.StartContainerAsync(_containerId, null); 108 | Console.WriteLine($"Container {containerStarted} started"); 109 | } 110 | 111 | private async Task DisposeContainer() 112 | { 113 | await _dockerClient.Containers.KillContainerAsync(_containerId, new ContainerKillParameters()); 114 | Console.WriteLine($"Container {_containerId} killed"); 115 | 116 | await _dockerClient.Containers.RemoveContainerAsync(_containerId, new ContainerRemoveParameters()); 117 | Console.WriteLine($"Container {_containerId} removed"); 118 | } 119 | 120 | #endregion 121 | 122 | 123 | #region [ DynamoDB table creation ] 124 | 125 | private async Task CreateTableAsync(string tableName) 126 | { 127 | var createTableReq = new CreateTableRequest 128 | { 129 | TableName = tableName, 130 | KeySchema = new List 131 | { 132 | new KeySchemaElement {AttributeName = "PK", KeyType = KeyType.HASH }, 133 | new KeySchemaElement {AttributeName = "SK", KeyType = KeyType.RANGE } 134 | }, 135 | AttributeDefinitions = new List{ 136 | new AttributeDefinition { AttributeName = "PK", AttributeType = ScalarAttributeType.S }, 137 | new AttributeDefinition { AttributeName = "SK", AttributeType = ScalarAttributeType.S }, 138 | new AttributeDefinition { AttributeName = "GSI1", AttributeType = ScalarAttributeType.S } 139 | }, 140 | ProvisionedThroughput = new ProvisionedThroughput(5, 5), 141 | GlobalSecondaryIndexes = new List 142 | { 143 | new GlobalSecondaryIndex 144 | { 145 | IndexName = "GSI1", 146 | KeySchema = new List 147 | { 148 | new KeySchemaElement {AttributeName = "GSI1", KeyType = KeyType.HASH }, 149 | new KeySchemaElement {AttributeName = "SK", KeyType = KeyType.RANGE } 150 | }, 151 | Projection = new Projection 152 | { 153 | ProjectionType = ProjectionType.ALL 154 | }, 155 | ProvisionedThroughput = new ProvisionedThroughput(5, 5) 156 | } 157 | } 158 | }; 159 | 160 | return await _dynamoDbClient.CreateTableAsync(createTableReq); 161 | } 162 | 163 | private async Task WaitUntilTableIsActive(string tableName) 164 | { 165 | var currentStatus = TableStatus.CREATING; 166 | do 167 | { 168 | Console.WriteLine($"Checking if the Table is ready ... Currently is {currentStatus}"); 169 | var describeTable = await _dynamoDbClient.DescribeTableAsync(tableName); 170 | currentStatus = describeTable.Table.TableStatus; 171 | await Task.Delay(3000); 172 | } 173 | while (currentStatus != TableStatus.ACTIVE); 174 | Console.WriteLine("Table ready !"); 175 | } 176 | 177 | #endregion 178 | 179 | } 180 | } -------------------------------------------------------------------------------- /test/DynamoDbRepository.Tests/DynamoDbRepository.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | false 6 | 7 | 8 | 9 | 10 | runtime; build; native; contentfiles; analyzers; buildtransitive 11 | all 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/DynamoDbRepository.Tests/RepositoryIntegrationTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | using SampleDynamoDbRepository; 5 | using Xunit; 6 | 7 | namespace DynamoDbRepository.Tests 8 | { 9 | 10 | public class RepositoryIntegrationTest : IClassFixture 11 | { 12 | private string _serviceUrl; 13 | private string _tableName; 14 | 15 | public RepositoryIntegrationTest(DynamoDBDockerFixture fixture) 16 | { 17 | _serviceUrl = fixture.ServiceUrl; 18 | _tableName = fixture.TableName; 19 | } 20 | 21 | [Fact] 22 | public async void TestRepo_UserRepository() 23 | { 24 | IUserRepository repo = new UserRepository(_tableName, _serviceUrl); 25 | 26 | var list = await repo.GetUserList(); 27 | Assert.Equal(0, list.Count); 28 | 29 | var item = new User { Id = "User0001", Name = "userA", FirstName = "User", LastName = "A", Email = "a@test.com" }; 30 | await repo.AddUser(item); 31 | 32 | list = await repo.GetUserList(); 33 | Assert.Equal(1, list.Count); 34 | 35 | var item0 = list[0]; 36 | Assert.NotNull(item0); 37 | Assert.Equal("User0001", item0.Id); 38 | Assert.Equal("userA", item0.Name); 39 | Assert.Equal("User", item0.FirstName); 40 | Assert.Equal("A", item0.LastName); 41 | Assert.Equal("a@test.com", item0.Email); 42 | 43 | var found = await repo.GetUser("User0001"); 44 | Assert.NotNull(found); 45 | Assert.Equal("User0001", found.Id); 46 | Assert.Equal("userA", found.Name); 47 | Assert.Equal("User", found.FirstName); 48 | Assert.Equal("A", found.LastName); 49 | Assert.Equal("a@test.com", found.Email); 50 | 51 | 52 | found.Name = "userAA"; 53 | found.Email = "aa-aa@test.com"; 54 | found.FirstName = "UserUser"; 55 | found.LastName = "AA AA"; 56 | await repo.UpdateUser(found); 57 | 58 | var updated = await repo.GetUser("User0001"); 59 | Assert.NotNull(updated); 60 | Assert.Equal("User0001", updated.Id); 61 | Assert.Equal("userAA", updated.Name); 62 | Assert.Equal("UserUser", updated.FirstName); 63 | Assert.Equal("AA AA", updated.LastName); 64 | Assert.Equal("aa-aa@test.com", updated.Email); 65 | 66 | await repo.DeleteUser("User0001"); 67 | var deleted = await repo.GetUser("User0001"); 68 | Assert.Null(deleted); 69 | 70 | var emptyList = await repo.GetUserList(); 71 | Assert.Equal(0, emptyList.Count); 72 | } 73 | 74 | [Fact] 75 | public async void TestRepo_Batch_UserRepository() 76 | { 77 | 78 | IUserRepository repo = new UserRepository(_tableName, _serviceUrl); 79 | 80 | var itemsToCreate = new List(); 81 | for (int i = 50; i < 60; i++) 82 | { 83 | var u = new User { Id = "A" + i, Name = "userA" + i, FirstName = "User" + i, LastName = "A" + i, Email = $"a{i}@test.com" }; 84 | itemsToCreate.Add(u); 85 | } 86 | await repo.BatchAddUsers(itemsToCreate); 87 | 88 | var list = await repo.GetUserList(); 89 | Assert.Equal(10, list.Count); 90 | 91 | for (int i = 0; i < 10; i++) 92 | { 93 | var item = list[i]; 94 | var id = i + 50; 95 | Assert.NotNull(item); 96 | Assert.Equal("A" + id, item.Id); 97 | Assert.Equal("userA" + id, item.Name); 98 | Assert.Equal("User" + id, item.FirstName); 99 | Assert.Equal("A" + id, item.LastName); 100 | Assert.Equal($"a{id}@test.com", item.Email); 101 | } 102 | 103 | var itemsToDelete = new List(); 104 | for (int i = 50; i < 60; i++) 105 | { 106 | var up = new User { Id = "A" + i }; 107 | itemsToDelete.Add(up); 108 | } 109 | await repo.BatchDeleteUsers(itemsToDelete); 110 | 111 | var emptyList = await repo.GetUserList(); 112 | Assert.Empty(emptyList); 113 | } 114 | 115 | [Fact] 116 | public async void TestRepo_ProjectRepository() 117 | { 118 | IProjectRepository repo = new ProjectRepository(_tableName, _serviceUrl); 119 | 120 | var list = await repo.GetProjectList(); 121 | Assert.Equal(0, list.Count); 122 | 123 | var item = new Project { Id = "Project0001", Name = "ProjectA", Description = "Project A" }; 124 | await repo.AddProject(item); 125 | 126 | list = await repo.GetProjectList(); 127 | Assert.Equal(1, list.Count); 128 | 129 | var item0 = list[0]; 130 | Assert.NotNull(item0); 131 | Assert.Equal("Project0001", item0.Id); 132 | Assert.Equal("ProjectA", item0.Name); 133 | Assert.Equal("Project A", item0.Description); 134 | 135 | var found = await repo.GetProject("Project0001"); 136 | Assert.NotNull(found); 137 | Assert.Equal("Project0001", found.Id); 138 | Assert.Equal("ProjectA", found.Name); 139 | Assert.Equal("Project A", found.Description); 140 | 141 | found.Name = "ProjectAA"; 142 | found.Description = "Project AA"; 143 | await repo.UpdateProject(found); 144 | 145 | var updated = await repo.GetProject("Project0001"); 146 | Assert.NotNull(updated); 147 | Assert.Equal("Project0001", updated.Id); 148 | Assert.Equal("ProjectAA", updated.Name); 149 | Assert.Equal("Project AA", updated.Description); 150 | 151 | 152 | await repo.DeleteProject("Project0001"); 153 | var deleted = await repo.GetProject("Project0001"); 154 | Assert.Null(deleted); 155 | 156 | } 157 | 158 | [Fact] 159 | public async void TestRepo_PersonRepository() 160 | { 161 | ISimpleRepository repo = new PersonRepository(_tableName, _serviceUrl); 162 | 163 | var list = await repo.GetList(); 164 | Assert.Equal(0, list.Count); 165 | 166 | var item = new Person { Id = 1, Name = "personA", Email = "pa@test.com", Age = 35, Height = 1.75 }; 167 | await repo.Add(item); 168 | 169 | list = await repo.GetList(); 170 | Assert.Equal(1, list.Count); 171 | 172 | var item0 = list[0]; 173 | Assert.NotNull(item0); 174 | Assert.Equal(1, item0.Id); 175 | Assert.Equal("personA", item0.Name); 176 | Assert.Equal("pa@test.com", item0.Email); 177 | Assert.Equal(35, item0.Age); 178 | Assert.Equal(1.75, item0.Height); 179 | 180 | var found = await repo.Get(1); 181 | Assert.NotNull(found); 182 | Assert.Equal(1, found.Id); 183 | Assert.Equal("personA", found.Name); 184 | Assert.Equal("pa@test.com", found.Email); 185 | Assert.Equal(35, found.Age); 186 | Assert.Equal(1.75, found.Height); 187 | 188 | found.Name = "personAA"; 189 | found.Email = "aa-aa@test.com"; 190 | found.Age = 36; 191 | found.Height = 1.78; 192 | await repo.Update(found); 193 | 194 | var updated = await repo.Get(1); 195 | Assert.NotNull(updated); 196 | Assert.Equal(1, updated.Id); 197 | Assert.Equal("personAA", updated.Name); 198 | Assert.Equal("aa-aa@test.com", updated.Email); 199 | Assert.Equal(36, updated.Age); 200 | Assert.Equal(1.78, updated.Height); 201 | 202 | await repo.Delete(1); 203 | var deleted = await repo.Get(1); 204 | Assert.Null(deleted); 205 | 206 | var emptyList = await repo.GetList(); 207 | Assert.Equal(0, emptyList.Count); 208 | } 209 | 210 | [Fact] 211 | public async void TestRepo_GameRepository() 212 | { 213 | IGameRepository repo = new GameRepository(_tableName, _serviceUrl); 214 | var userId = "U1"; 215 | 216 | var list0 = await repo.GetGameList(userId); 217 | Assert.Equal(0, list0.Count); 218 | 219 | var g = new Game { Id = "GA", Name = "Game A" }; 220 | await repo.AddGame(userId, g); 221 | 222 | var list1 = await repo.GetGameList(userId); 223 | Assert.Equal(1, list1.Count); 224 | 225 | var item0 = list1[0]; 226 | Assert.NotNull(item0); 227 | Assert.Equal("GA", item0.Id); 228 | Assert.Equal("Game A", item0.Name); 229 | 230 | var found = await repo.GetGame(userId, g.Id); 231 | Assert.NotNull(found); 232 | Assert.Equal("GA", found.Id); 233 | Assert.Equal("Game A", found.Name); 234 | 235 | found.Name = "Game AA"; 236 | await repo.UpdateGame(userId, found); 237 | 238 | var updated = await repo.GetGame(userId, g.Id); 239 | Assert.NotNull(found); 240 | Assert.Equal("Game AA", found.Name); 241 | 242 | await repo.DeleteGame(userId, g.Id); 243 | 244 | var deleted = await repo.GetGame(userId, g.Id); 245 | Assert.Null(deleted); 246 | 247 | var emptyList = await repo.GetGameList(userId); 248 | Assert.Equal(0, emptyList.Count); 249 | } 250 | 251 | [Fact] 252 | public async void TestRepo_Batch_GameRepository() 253 | { 254 | var u1 = new User { Id = "U1", Name = "User 1", FirstName = "User", LastName = "A", Email = "a@test.com" }; 255 | 256 | IGameRepository repo = new GameRepository(_tableName, _serviceUrl); 257 | 258 | var itemsToCreate = new List(); 259 | for (int i = 50; i < 60; i++) 260 | { 261 | var g = new Game { Id = "G" + i, Name = "Game " + i }; 262 | itemsToCreate.Add(g); 263 | } 264 | await repo.BatchAddGames(u1.Id, itemsToCreate); 265 | 266 | var list = await repo.GetGameList(u1.Id); 267 | Assert.Equal(10, list.Count); 268 | 269 | for (int i = 0; i < 10; i++) 270 | { 271 | var item = list[i]; 272 | var id = i + 50; 273 | Assert.NotNull(item); 274 | Assert.Equal("G" + id, item.Id); 275 | Assert.Equal("Game " + id, item.Name); 276 | } 277 | 278 | var itemsToDelete = new List(); 279 | for (int i = 50; i < 60; i++) 280 | { 281 | var g = new Game { Id = "G" + i, Name = "Game " + i }; 282 | itemsToDelete.Add(g); 283 | } 284 | await repo.BatchDeleteGames(u1.Id, itemsToDelete); 285 | 286 | var emptyList = await repo.GetGameList(u1.Id); 287 | Assert.Empty(emptyList); 288 | } 289 | 290 | [Fact] 291 | public async void TestRepo_UserProjectRepository() 292 | { 293 | var u1 = new User { Id = "U1", Name = "User 1", FirstName = "User", LastName = "A", Email = "a@test.com" }; 294 | 295 | IUserProjectRepository repo = new UserProjectRepository(_tableName, _serviceUrl); 296 | 297 | var allUPU1 = await repo.GetProjectsByUserAsync(u1.Id); 298 | Assert.Equal(0, allUPU1.Count); 299 | 300 | var u1p1 = new UserProject { UserId = u1.Id, ProjectId = "P1", Role = "owner" }; 301 | await repo.AddProjectToUser(u1p1); 302 | 303 | allUPU1 = await repo.GetProjectsByUserAsync(u1.Id); 304 | Assert.Equal(1, allUPU1.Count); 305 | 306 | var u1p2 = new UserProject { UserId = u1.Id, ProjectId = "P2", Role = "member" }; 307 | await repo.AddProjectToUser(u1p2); 308 | 309 | allUPU1 = await repo.GetProjectsByUserAsync(u1.Id); 310 | Assert.Equal(2, allUPU1.Count); 311 | Assert.Equal(u1.Id, allUPU1[0].UserId); 312 | Assert.Equal("P1", allUPU1[0].ProjectId); 313 | Assert.Equal("owner", allUPU1[0].Role); 314 | Assert.Equal(u1.Id, allUPU1[1].UserId); 315 | Assert.Equal("P2", allUPU1[1].ProjectId); 316 | Assert.Equal("member", allUPU1[1].Role); 317 | 318 | var usersP1 = await repo.GetUsersByProjectAsync("P1"); 319 | Assert.Equal(1, usersP1.Count); 320 | Assert.Equal(u1.Id, usersP1[0].UserId); 321 | Assert.Equal("P1", usersP1[0].ProjectId); 322 | Assert.Equal("owner", usersP1[0].Role); 323 | 324 | var usersP2 = await repo.GetUsersByProjectAsync("P2"); 325 | Assert.Equal(1, usersP2.Count); 326 | Assert.Equal(u1.Id, usersP2[0].UserId); 327 | Assert.Equal("P2", usersP2[0].ProjectId); 328 | Assert.Equal("member", usersP2[0].Role); 329 | 330 | await repo.RemoveProjectFromUser(u1p1.UserId, u1p1.ProjectId); 331 | allUPU1 = await repo.GetProjectsByUserAsync(u1.Id); 332 | Assert.Equal(1, allUPU1.Count); 333 | 334 | await repo.RemoveProjectFromUser(u1p2.UserId, u1p2.ProjectId); 335 | allUPU1 = await repo.GetProjectsByUserAsync(u1.Id); 336 | Assert.Equal(0, allUPU1.Count); 337 | } 338 | 339 | [Fact] 340 | public async void TestRepo_Batch_UserProjectRepository() 341 | { 342 | var u1 = new User { Id = "U1", Name = "User 1", FirstName = "User", LastName = "A", Email = "a@test.com" }; 343 | 344 | IUserProjectRepository repo = new UserProjectRepository(_tableName, _serviceUrl); 345 | 346 | var itemsToCreate = new List(); 347 | for (int i = 50; i < 60; i++) 348 | { 349 | var up = new UserProject { UserId = u1.Id, ProjectId = "P" + i, Role = "member" }; 350 | itemsToCreate.Add(up); 351 | } 352 | await repo.BatchAddProjectsToUser(u1.Id, itemsToCreate); 353 | 354 | var list = await repo.GetProjectsByUserAsync(u1.Id); 355 | Assert.Equal(10, list.Count); 356 | 357 | for (int i = 0; i < 10; i++) 358 | { 359 | var item = list[i]; 360 | var id = i + 50; 361 | Assert.NotNull(item); 362 | Assert.Equal(u1.Id, item.UserId); 363 | Assert.Equal("P" + id, item.ProjectId); 364 | Assert.Equal("member", item.Role); 365 | } 366 | 367 | var itemsToDelete = new List(); 368 | for (int i = 50; i < 60; i++) 369 | { 370 | var up = new UserProject { UserId = u1.Id, ProjectId = "P" + i }; 371 | itemsToDelete.Add(up); 372 | } 373 | await repo.BatchRemoveProjectsFromUser(u1.Id, itemsToDelete); 374 | 375 | var emptyList = await repo.GetProjectsByUserAsync(u1.Id); 376 | Assert.Empty(emptyList); 377 | } 378 | 379 | [Fact] 380 | public async void TestGenericOperations_IndependentEntityRepository() 381 | { 382 | var repo = new TestIndependentEntityRepo(_tableName, _serviceUrl); 383 | 384 | var list = await repo.GSI1QueryAllAsync(); 385 | Assert.Equal(0, list.Count); 386 | 387 | var te1 = new TestEntity { Id = "TE1", Name = "TestEntity TE1" }; 388 | await repo.AddItemAsync("TE1", te1); 389 | 390 | list = await repo.GSI1QueryAllAsync(); 391 | Assert.Equal(1, list.Count); 392 | 393 | var te2 = new TestEntity { Id = "TE2", Name = "TestEntity TE2" }; 394 | await repo.AddItemAsync("TE2", te2); 395 | 396 | list = await repo.GSI1QueryAllAsync(); 397 | Assert.Equal(2, list.Count); 398 | 399 | var found1 = await repo.GetItemAsync(te1.Id); 400 | Assert.NotNull(found1); 401 | Assert.Equal("TE1", found1.Id); 402 | Assert.Equal("TestEntity TE1", found1.Name); 403 | 404 | var found2 = await repo.GetItemAsync(te2.Id); 405 | Assert.NotNull(found2); 406 | Assert.Equal("TE2", found2.Id); 407 | Assert.Equal("TestEntity TE2", found2.Name); 408 | 409 | await repo.DeleteItemAsync(te1.Id); 410 | 411 | list = await repo.GSI1QueryAllAsync(); 412 | Assert.Equal(1, list.Count); 413 | 414 | var deleted1 = await repo.GetItemAsync(te1.Id); 415 | Assert.Null(deleted1); 416 | 417 | await repo.DeleteItemAsync(te2.Id); 418 | list = await repo.GSI1QueryAllAsync(); 419 | Assert.Equal(0, list.Count); 420 | 421 | var deleted2 = await repo.GetItemAsync(te1.Id); 422 | Assert.Null(deleted2); 423 | } 424 | 425 | [Fact] 426 | public async void TestGenericOperations_DependentEntityRepository() 427 | { 428 | var repo = new TestDependentEntityRepo(_tableName, _serviceUrl); 429 | var parentId = "P0001"; 430 | 431 | var list = await repo.TableQueryItemsByParentIdAsync(parentId); 432 | Assert.Equal(0, list.Count); 433 | 434 | var te1 = new TestEntity { Id = "TE1", Name = "TestEntity TE1" }; 435 | await repo.AddItemAsync(parentId, "TE1", te1); 436 | 437 | list = await repo.TableQueryItemsByParentIdAsync(parentId); 438 | Assert.Equal(1, list.Count); 439 | 440 | var te2 = new TestEntity { Id = "TE2", Name = "TestEntity TE2" }; 441 | await repo.AddItemAsync(parentId, "TE2", te2); 442 | 443 | list = await repo.TableQueryItemsByParentIdAsync(parentId); 444 | Assert.Equal(2, list.Count); 445 | 446 | var found1 = await repo.GetItemAsync(parentId, te1.Id); 447 | Assert.NotNull(found1); 448 | Assert.Equal("TE1", found1.Id); 449 | Assert.Equal("TestEntity TE1", found1.Name); 450 | 451 | var found2 = await repo.GetItemAsync(parentId, te2.Id); 452 | Assert.NotNull(found2); 453 | Assert.Equal("TE2", found2.Id); 454 | Assert.Equal("TestEntity TE2", found2.Name); 455 | 456 | await repo.DeleteItemAsync(parentId, te1.Id); 457 | 458 | list = await repo.TableQueryItemsByParentIdAsync(parentId); 459 | Assert.Equal(1, list.Count); 460 | 461 | var deleted1 = await repo.GetItemAsync(parentId, te1.Id); 462 | Assert.Null(deleted1); 463 | 464 | await repo.DeleteItemAsync(parentId, te2.Id); 465 | list = await repo.TableQueryItemsByParentIdAsync(parentId); 466 | Assert.Equal(0, list.Count); 467 | 468 | var deleted2 = await repo.GetItemAsync(parentId, te1.Id); 469 | Assert.Null(deleted2); 470 | } 471 | 472 | [Fact] 473 | public async void TestBatchOperations_IndependentEntityRepository() 474 | { 475 | var repo = new TestIndependentEntityRepo(_tableName, _serviceUrl); 476 | var itemsToCreate = new List>(); 477 | for (int i = 50; i < 60; i++) 478 | { 479 | var te = new TestEntity { Id = "TE" + i, Name = "TestEntity TE" + i }; 480 | itemsToCreate.Add(new KeyValuePair(te.Id, te)); 481 | } 482 | await repo.BatchAddItemsAsync(itemsToCreate); 483 | 484 | var list = await repo.GSI1QueryAllAsync(); 485 | Assert.Equal(10, list.Count); 486 | 487 | for (int i = 0; i < 10; i++) 488 | { 489 | var item = list[i]; 490 | var id = i + 50; 491 | Assert.NotNull(item); 492 | Assert.Equal("TE" + id, item.Id); 493 | Assert.Equal("TestEntity TE" + id, item.Name); 494 | } 495 | 496 | var itemsToDelete = new List(); 497 | for (int i = 50; i < 60; i++) 498 | { 499 | var te = new TestEntity { Id = "TE" + i }; 500 | itemsToDelete.Add(te.Id); 501 | } 502 | await repo.BatchDeleteItemsAsync(itemsToDelete); 503 | 504 | var emptyList = await repo.GSI1QueryAllAsync(); 505 | Assert.Empty(emptyList); 506 | } 507 | 508 | [Fact] 509 | public async void TestBatchOperations_DependentEntityRepository() 510 | { 511 | var parent = new TestEntity { Id = "PTE1" }; 512 | var repo = new TestDependentEntityRepo(_tableName, _serviceUrl); 513 | var itemsToCreate = new List>(); 514 | for (int i = 50; i < 60; i++) 515 | { 516 | var te = new TestEntity { Id = "TE" + i, Name = "TestEntity TE" + i }; 517 | itemsToCreate.Add(new KeyValuePair(te.Id, te)); 518 | } 519 | await repo.BatchAddItemsAsync(parent.Id, itemsToCreate); 520 | 521 | var list = await repo.TableQueryItemsByParentIdAsync(parent.Id); 522 | Assert.Equal(10, list.Count); 523 | 524 | for (int i = 0; i < 10; i++) 525 | { 526 | var item = list[i]; 527 | var id = i + 50; 528 | Assert.NotNull(item); 529 | Assert.Equal("TE" + id, item.Id); 530 | Assert.Equal("TestEntity TE" + id, item.Name); 531 | } 532 | 533 | var itemsToDelete = new List(); 534 | for (int i = 50; i < 60; i++) 535 | { 536 | var te = new TestEntity { Id = "TE" + i }; 537 | itemsToDelete.Add(te.Id); 538 | } 539 | await repo.BatchDeleteItemsAsync(parent.Id, itemsToDelete); 540 | 541 | var emptyList = await repo.TableQueryItemsByParentIdAsync(parent.Id); 542 | Assert.Empty(emptyList); 543 | } 544 | } 545 | } 546 | -------------------------------------------------------------------------------- /test/DynamoDbRepository.Tests/TestRepositories/TestDependentEntityRepo.cs: -------------------------------------------------------------------------------- 1 | namespace DynamoDbRepository.Tests 2 | { 3 | public class TestDependentEntityRepo : DependentEntityRepository 4 | { 5 | public TestDependentEntityRepo(string tableName, string serviceUrl = null) : base(tableName, serviceUrl) 6 | { 7 | PKPrefix = "PARENT_ENTITY"; 8 | SKPrefix = "TEST_ENTITY"; 9 | } 10 | 11 | protected override TestEntity FromDynamoDb(DynamoDBItem item) 12 | { 13 | var result = new TestEntity(); 14 | result.Id = item.GetString("Id"); 15 | result.Name = item.GetString("Name"); 16 | return result; 17 | } 18 | 19 | protected override DynamoDBItem ToDynamoDb(TestEntity item) 20 | { 21 | var dbItem = new DynamoDBItem(); 22 | dbItem.AddString("Id", item.Id); 23 | dbItem.AddString("Name", item.Name); 24 | return dbItem; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/DynamoDbRepository.Tests/TestRepositories/TestEntity.cs: -------------------------------------------------------------------------------- 1 | namespace DynamoDbRepository.Tests 2 | { 3 | public class TestEntity 4 | { 5 | public string Id { get; set; } 6 | 7 | public string Name { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/DynamoDbRepository.Tests/TestRepositories/TestIndependentEntityRepo.cs: -------------------------------------------------------------------------------- 1 | namespace DynamoDbRepository.Tests 2 | { 3 | public class TestIndependentEntityRepo : IndependentEntityRepository 4 | { 5 | public TestIndependentEntityRepo(string tableName, string serviceUrl = null) : base(tableName, serviceUrl) 6 | { 7 | PKPrefix = "TEST_ENTITY"; 8 | SKPrefix = "METADATA"; 9 | } 10 | 11 | protected override TestEntity FromDynamoDb(DynamoDBItem item) 12 | { 13 | var result = new TestEntity(); 14 | result.Id = item.GetString("Id"); 15 | result.Name = item.GetString("Name"); 16 | return result; 17 | } 18 | 19 | protected override DynamoDBItem ToDynamoDb(TestEntity item) 20 | { 21 | var dbItem = new DynamoDBItem(); 22 | dbItem.AddString("Id", item.Id); 23 | dbItem.AddString("Name", item.Name); 24 | return dbItem; 25 | } 26 | } 27 | } 28 | --------------------------------------------------------------------------------