diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..675f5ba --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,43 @@ +name: .NET Build, Test, and Publish Nuget Package + +on: + push: + branches: + - "**" + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + pull_request: + branches: + - "**" +env: + VERSION: 0.0.0 + +defaults: + run: + working-directory: ./ + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set Version Variable + if: ${{ github.ref_type == 'tag' }} + env: + TAG: ${{ github.ref_name }} + run: echo "VERSION=${TAG#v}" >> $GITHUB_ENV + - name: Setup .NET + uses: actions/setup-dotnet@v2 + with: + dotnet-version: 6.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore /p:Version=$VERSION + - name: Test + run: dotnet test --no-build --verbosity normal + - name: pack nuget packages + run: dotnet pack --output nupkgs --no-restore --no-build /p:PackageVersion=$VERSION + - name: upload nuget package + if: github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v') + run: dotnet nuget push nupkgs/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json diff --git a/Dynatello.sln b/Dynatello.sln index 9c42018..06b899d 100644 --- a/Dynatello.sln +++ b/Dynatello.sln @@ -11,6 +11,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F0497192 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dynatello.Tests", "tests\Dynatello.Tests\Dynatello.Tests.csproj", "{D0078AB2-F320-42BD-89C9-ED5491A35DE1}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{10579316-5B7C-49A6-8128-54F95D2BBE5C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Repository", "samples\Repository\Repository.csproj", "{41B5DF1A-1542-4CBF-BEB0-CE684D054363}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -28,9 +32,14 @@ Global {D0078AB2-F320-42BD-89C9-ED5491A35DE1}.Debug|Any CPU.Build.0 = Debug|Any CPU {D0078AB2-F320-42BD-89C9-ED5491A35DE1}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0078AB2-F320-42BD-89C9-ED5491A35DE1}.Release|Any CPU.Build.0 = Release|Any CPU + {41B5DF1A-1542-4CBF-BEB0-CE684D054363}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41B5DF1A-1542-4CBF-BEB0-CE684D054363}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41B5DF1A-1542-4CBF-BEB0-CE684D054363}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41B5DF1A-1542-4CBF-BEB0-CE684D054363}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {892C771D-B39C-4221-B04D-FF59A061995C} = {EDABF10C-5499-442F-8BE1-9205EEE7E016} {D0078AB2-F320-42BD-89C9-ED5491A35DE1} = {F0497192-D217-4A90-9C1C-E1A6DEDC4526} + {41B5DF1A-1542-4CBF-BEB0-CE684D054363} = {10579316-5B7C-49A6-8128-54F95D2BBE5C} EndGlobalSection EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 0000000..c240aad --- /dev/null +++ b/README.md @@ -0,0 +1,137 @@ +# Dynatello + +## What does is do? +A DynamoDB source generator that does the heavy lifting when it comes to using the low-level client in DynamoDB. + +### Features + +* Builder patterns to create request builders through the source generated code. + +## Installation + +Add [![DynamoDBGenerator][1]][2] + + + +[1]: https://img.shields.io/nuget/v/Dynatello.svg?label=Dynatello +[2]: https://www.nuget.org/packages/Dynatello + +## Example + +```csharp +using System.Net; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.DataModel; +using Amazon.DynamoDBv2.Model; +using DynamoDBGenerator.Attributes; +using Dynatello; +using Dynatello.Builders; +using Dynatello.Builders.Types; + +ProductRepository productRepository = new ProductRepository("MY_TABLE", new AmazonDynamoDBClient()); + +public class ProductRepository +{ + private readonly IAmazonDynamoDB _amazonDynamoDb; + private readonly GetRequestBuilder _getProductByTable; + private readonly UpdateRequestBuilder<(string Id, decimal NewPrice, DateTime TimeStamp)> _updatePrice; + private readonly PutRequestBuilder _createProduct; + private readonly QueryRequestBuilder _queryByPrice; + + public ProductRepository(string tableName, IAmazonDynamoDB amazonDynamoDb) + { + _amazonDynamoDb = amazonDynamoDb; + + _getProductByTable = Product.GetById + .OnTable(tableName) + .ToGetRequestBuilder(arg => arg); // Since the ArgumentType is set to string, we don't need to select a property. + + _updatePrice = Product.UpdatePrice + .OnTable(tableName) + .WithUpdateExpression((db, arg) => $"SET {db.Price} = {arg.NewPrice}, {db.Metadata.ModifiedAt} = {arg.TimeStamp}") // Specify the update operation + .ToUpdateItemRequestBuilder((marshaller, arg) => marshaller.PartitionKey(arg.Id)); + + _createProduct = Product.Put + .OnTable(tableName) + .WithConditionExpression((db, arg) => $"{db.Id} <> {arg.Id}") // Ensure we don't have an existing Product in DynamoDB + .ToPutRequestBuilder(); + + _queryByPrice = Product.QueryByPrice + .OnTable(tableName) + .WithKeyConditionExpression((db, arg) => $"{db.Price} = {arg}") + .ToQueryRequestBuilder() + with + { + IndexName = Product.PriceIndex + }; + } + + public async Task> SearchByPrice(decimal price) + { + QueryRequest request = _queryByPrice.Build(price); + QueryResponse? response = await _amazonDynamoDb.QueryAsync(request); + + if (response.HttpStatusCode is not HttpStatusCode.OK) + throw new Exception("..."); + + return response.Items + .Select(x => Product.QueryByPrice.Unmarshall(x)) + .ToArray(); + } + + public async Task Create(Product product) + { + PutItemRequest request = _createProduct.Build(product); + PutItemResponse response = await _amazonDynamoDb.PutItemAsync(request); + + if (response.HttpStatusCode is not HttpStatusCode.OK) + throw new Exception("..."); + } + + public async Task GetById(string id) + { + GetItemRequest request = _getProductByTable.Build(id); + GetItemResponse response = await _amazonDynamoDb.GetItemAsync(request); + + if (response.HttpStatusCode is HttpStatusCode.NotFound) + return null; + + if (response.HttpStatusCode is not HttpStatusCode.OK) + throw new Exception("..."); + + Product product = Product.GetById.Unmarshall(response.Item); + + return product; + } + + public async Task UpdatePrice(string id, decimal price) + { + UpdateItemRequest request = _updatePrice.Build((id, price, DateTime.UtcNow)); + UpdateItemResponse response = await _amazonDynamoDb.UpdateItemAsync(request); + + if (response.HttpStatusCode is not HttpStatusCode.OK) + return null; + + Product product = Product.UpdatePrice.Unmarshall(response.Attributes); + + return product; + } +} + +// These attributes is what makes the source generator kick in. Make sure to have the class 'partial' as well. +[DynamoDBMarshaller(typeof(Product), PropertyName = "Put")] +[DynamoDBMarshaller(typeof(Product), ArgumentType = typeof(string), PropertyName = "GetById")] +[DynamoDBMarshaller(typeof(Product), ArgumentType = typeof((string Id, decimal NewPrice, DateTime TimeStamp)), PropertyName = "UpdatePrice")] +[DynamoDBMarshaller(typeof(Product), ArgumentType = typeof(decimal), PropertyName = "QueryByPrice")] +public partial record Product( + [property: DynamoDBHashKey, DynamoDBGlobalSecondaryIndexRangeKey(Product.PriceIndex)] string Id, + [property: DynamoDBGlobalSecondaryIndexHashKey(Product.PriceIndex)] decimal Price, + string Description, + Product.MetadataEntity Metadata +) +{ + public const string PriceIndex = "PriceIndex"; + + public record MetadataEntity(DateTime CreatedAt, DateTime ModifiedAt); +} +``` diff --git a/samples/Repository/Program.cs b/samples/Repository/Program.cs new file mode 100644 index 0000000..af42634 --- /dev/null +++ b/samples/Repository/Program.cs @@ -0,0 +1,115 @@ +using System.Net; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.DataModel; +using Amazon.DynamoDBv2.Model; +using DynamoDBGenerator.Attributes; +using Dynatello; +using Dynatello.Builders; +using Dynatello.Builders.Types; + +ProductRepository productRepository = new ProductRepository("MY_TABLE", new AmazonDynamoDBClient()); + +public class ProductRepository +{ + private readonly IAmazonDynamoDB _amazonDynamoDb; + private readonly GetRequestBuilder _getProductByTable; + private readonly UpdateRequestBuilder<(string Id, decimal NewPrice, DateTime TimeStamp)> _updatePrice; + private readonly PutRequestBuilder _createProduct; + private readonly QueryRequestBuilder _queryByPrice; + + public ProductRepository(string tableName, IAmazonDynamoDB amazonDynamoDb) + { + _amazonDynamoDb = amazonDynamoDb; + + _getProductByTable = Product.GetById + .OnTable(tableName) + .ToGetRequestBuilder(arg => arg); // Since the ArgumentType is set to string, we don't need to select a property. + + _updatePrice = Product.UpdatePrice + .OnTable(tableName) + .WithUpdateExpression((db, arg) => $"SET {db.Price} = {arg.NewPrice}, {db.Metadata.ModifiedAt} = {arg.TimeStamp}") // Specify the update operation + .ToUpdateItemRequestBuilder((marshaller, arg) => marshaller.PartitionKey(arg.Id)); + + _createProduct = Product.Put + .OnTable(tableName) + .WithConditionExpression((db, arg) => $"{db.Id} <> {arg.Id}") // Ensure we don't have an existing Product in DynamoDB + .ToPutRequestBuilder(); + + _queryByPrice = Product.QueryByPrice + .OnTable(tableName) + .WithKeyConditionExpression((db, arg) => $"{db.Price} = {arg}") + .ToQueryRequestBuilder() + with + { + IndexName = Product.PriceIndex + }; + } + + public async Task> SearchByPrice(decimal price) + { + QueryRequest request = _queryByPrice.Build(price); + QueryResponse? response = await _amazonDynamoDb.QueryAsync(request); + + if (response.HttpStatusCode is not HttpStatusCode.OK) + throw new Exception("..."); + + return response.Items + .Select(x => Product.QueryByPrice.Unmarshall(x)) + .ToArray(); + } + + public async Task Create(Product product) + { + PutItemRequest request = _createProduct.Build(product); + PutItemResponse response = await _amazonDynamoDb.PutItemAsync(request); + + if (response.HttpStatusCode is not HttpStatusCode.OK) + throw new Exception("..."); + } + + public async Task GetById(string id) + { + GetItemRequest request = _getProductByTable.Build(id); + GetItemResponse response = await _amazonDynamoDb.GetItemAsync(request); + + if (response.HttpStatusCode is HttpStatusCode.NotFound) + return null; + + if (response.HttpStatusCode is not HttpStatusCode.OK) + throw new Exception("..."); + + Product product = Product.GetById.Unmarshall(response.Item); + + return product; + } + + public async Task UpdatePrice(string id, decimal price) + { + UpdateItemRequest request = _updatePrice.Build((id, price, DateTime.UtcNow)); + UpdateItemResponse response = await _amazonDynamoDb.UpdateItemAsync(request); + + if (response.HttpStatusCode is not HttpStatusCode.OK) + return null; + + Product product = Product.UpdatePrice.Unmarshall(response.Attributes); + + return product; + } +} + +// These attributes is what makes the source generator kick in. Make sure to have the class 'partial' as well. +[DynamoDBMarshaller(typeof(Product), PropertyName = "Put")] +[DynamoDBMarshaller(typeof(Product), ArgumentType = typeof(string), PropertyName = "GetById")] +[DynamoDBMarshaller(typeof(Product), ArgumentType = typeof((string Id, decimal NewPrice, DateTime TimeStamp)), PropertyName = "UpdatePrice")] +[DynamoDBMarshaller(typeof(Product), ArgumentType = typeof(decimal), PropertyName = "QueryByPrice")] +public partial record Product( + [property: DynamoDBHashKey, DynamoDBGlobalSecondaryIndexRangeKey(Product.PriceIndex)] string Id, + [property: DynamoDBGlobalSecondaryIndexHashKey(Product.PriceIndex)] decimal Price, + string Description, + Product.MetadataEntity Metadata +) +{ + public const string PriceIndex = "PriceIndex"; + + public record MetadataEntity(DateTime CreatedAt, DateTime ModifiedAt); +} diff --git a/samples/Repository/Repository.csproj b/samples/Repository/Repository.csproj new file mode 100644 index 0000000..ed9cca5 --- /dev/null +++ b/samples/Repository/Repository.csproj @@ -0,0 +1,16 @@ + + + + Exe + net6.0 + enable + enable + false + + + + + + + +