From 21d41b3791e19a35aae35e7b1104a78590d672a7 Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Sun, 22 Sep 2024 15:40:15 +0200 Subject: [PATCH] Add more samples & examples --- Dynatello.sln | 14 ++ README.md | 161 ++++++++++++------ samples/ExtendDto/ExtendDto.csproj | 14 ++ samples/ExtendDto/Program.cs | 22 +++ samples/Repository/Program.cs | 78 ++++----- samples/Repository/Repository.csproj | 1 - samples/RequestPipeline/Program.cs | 41 +++++ .../RequestPipeline/RequestPipeline.csproj | 13 ++ 8 files changed, 246 insertions(+), 98 deletions(-) create mode 100644 samples/ExtendDto/ExtendDto.csproj create mode 100644 samples/ExtendDto/Program.cs create mode 100644 samples/RequestPipeline/Program.cs create mode 100644 samples/RequestPipeline/RequestPipeline.csproj diff --git a/Dynatello.sln b/Dynatello.sln index 06b899d..e228623 100644 --- a/Dynatello.sln +++ b/Dynatello.sln @@ -15,6 +15,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{1057 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Repository", "samples\Repository\Repository.csproj", "{41B5DF1A-1542-4CBF-BEB0-CE684D054363}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExtendDto", "samples\ExtendDto\ExtendDto.csproj", "{30A5C236-1D22-47E4-9BEA-1F4ED3167830}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RequestPipeline", "samples\RequestPipeline\RequestPipeline.csproj", "{FF35AE29-0089-4D12-A6A8-F3ADC5E19F25}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,10 +40,20 @@ Global {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 + {30A5C236-1D22-47E4-9BEA-1F4ED3167830}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30A5C236-1D22-47E4-9BEA-1F4ED3167830}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30A5C236-1D22-47E4-9BEA-1F4ED3167830}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30A5C236-1D22-47E4-9BEA-1F4ED3167830}.Release|Any CPU.Build.0 = Release|Any CPU + {FF35AE29-0089-4D12-A6A8-F3ADC5E19F25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF35AE29-0089-4D12-A6A8-F3ADC5E19F25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF35AE29-0089-4D12-A6A8-F3ADC5E19F25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF35AE29-0089-4D12-A6A8-F3ADC5E19F25}.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} + {30A5C236-1D22-47E4-9BEA-1F4ED3167830} = {10579316-5B7C-49A6-8128-54F95D2BBE5C} + {FF35AE29-0089-4D12-A6A8-F3ADC5E19F25} = {10579316-5B7C-49A6-8128-54F95D2BBE5C} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index e95213c..677f492 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Dynatello -## What does it do? -A reflection free & generic typed solution to send requests towards dynamodb without having to manually maintain the low-level parts. +## What does the library do? + +Offers a unified API ontop of the low-level API in DynamoDB by utilizing [DynamoDB.SourceGenerator](https://github.com/inputfalken/DynamoDB.SourceGenerator). ## Features @@ -19,22 +20,116 @@ Add the following NuGet package as a dependency to you project. [2]: https://www.nuget.org/packages/Dynatello ## Example +The examples are taken from [samples] (samples/) directory. + +### Extend DTO's +How you could extend a DTO to contain the mashaller functionality. + +```csharp +using DynamoDBGenerator.Attributes; +using Dynatello; +using Dynatello.Builders; +using Dynatello.Handlers; + +IRequestHandler getById = Cat + .FromId.OnTable("TABLE") + .ToGetRequestHandler(x => x.ToGetRequestBuilder()); + +if (args.Length == 1) +{ + var response = await getById.Send(args[0], CancellationToken.None); + + Console.WriteLine(response); +} +else +{ + throw new NotImplementedException(); +} + +[DynamoDBMarshaller(AccessName = "FromId", ArgumentType = typeof(string))] +public partial record Cat(string Id, string Name, double Cuteness); +``` + + +### Middlware through `IRequestPipeline`. + +```csharp +using Amazon.Runtime; +using DynamoDBGenerator.Attributes; +using Dynatello; +using Dynatello.Builders; +using Dynatello.Handlers; +using Dynatello.Pipelines; + +IRequestHandler getById = Cat + .FromId.OnTable("TABLE") + .ToGetRequestHandler( + x => x.ToGetRequestBuilder(), + x => + { + x.RequestsPipelines.Add(new RequestConsoleLogger()); + } + ); + +if (args.Length == 1) +{ + var response = await getById.Send(args[0], CancellationToken.None); + + Console.WriteLine(response); +} +else +{ + throw new NotImplementedException(); +} + +public class RequestConsoleLogger : IRequestPipeLine +{ + public Task Invoke( + RequestContext requestContext, + Func> continuation + ) + { + throw new NotImplementedException(); + } +} + +[DynamoDBMarshaller(AccessName = "FromId", ArgumentType = typeof(string))] +public partial record Cat(string Id, string Name, double Cuteness); +``` + + +### Repository +How you could isolate all DynamoDB access code to a repository class. ```csharp -using System.Diagnostics; using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.DataModel; -using Amazon.Runtime; using DynamoDBGenerator.Attributes; using Dynatello; using Dynatello.Builders; using Dynatello.Builders.Types; using Dynatello.Handlers; -using Dynatello.Pipelines; ProductRepository productRepository = new ProductRepository("PRODUCTS", new AmazonDynamoDBClient()); -public class ProductRepository +// These attributes is what makes the source generator kick in. Make sure to have the class 'partial' as well. +[DynamoDBMarshaller(EntityType = typeof(Product), AccessName = "FromProduct")] +[DynamoDBMarshaller( + EntityType = typeof(Product), + AccessName = "FromId", + ArgumentType = typeof(string) +)] +[DynamoDBMarshaller( + EntityType = typeof(Product), + AccessName = "FromUpdatePricePayload", + ArgumentType = typeof((string Id, decimal NewPrice, DateTime TimeStamp)) +)] +[DynamoDBMarshaller( + EntityType = typeof(Product), + AccessName = "FromPrice", + ArgumentType = typeof(decimal) +)] +public partial class ProductRepository { private readonly IRequestHandler _getById; private readonly IRequestHandler< @@ -45,45 +140,27 @@ public class ProductRepository private readonly IRequestHandler> _queryByPrice; private readonly IRequestHandler _deleteById; - private class RequestLogger : IRequestPipeLine - { - public async Task Invoke( - RequestContext requestContext, - Func> continuation - ) - { - var stopwatch = Stopwatch.StartNew(); - Console.WriteLine("Starting request"); - var request = await continuation(requestContext); - Console.WriteLine($"Request finished after '{stopwatch.Elapsed}'."); - return request; - } - } - public ProductRepository(string tableName, IAmazonDynamoDB amazonDynamoDb) { - var requestLogger = new RequestLogger(); - _getById = Product - .FromId.OnTable(tableName) + _getById = FromId + .OnTable(tableName) .ToGetRequestHandler( x => x.ToGetRequestBuilder(), x => { x.AmazonDynamoDB = amazonDynamoDb; - // All handlers comes with the ability to add middlewares that will be executed. - x.RequestsPipelines.Add(requestLogger); } ); - _deleteById = Product - .FromId.OnTable(tableName) + _deleteById = FromId + .OnTable(tableName) .ToDeleteRequestHandler( x => x.ToDeleteRequestBuilder(), x => x.AmazonDynamoDB = amazonDynamoDb ); - _updatePrice = Product - .FromUpdatePricePayload.OnTable(tableName) + _updatePrice = FromUpdatePricePayload + .OnTable(tableName) .ToUpdateRequestHandler( x => x.WithUpdateExpression( @@ -96,8 +173,8 @@ public class ProductRepository x => x.AmazonDynamoDB = amazonDynamoDb ); - _createProduct = Product - .FromProduct.OnTable(tableName) + _createProduct = FromProduct + .OnTable(tableName) .ToPutRequestHandler( x => x.WithConditionExpression((db, arg) => $"{db.Id} <> {arg.Id}") // Ensure we don't have an existing Product in DynamoDB @@ -105,8 +182,8 @@ public class ProductRepository x => x.AmazonDynamoDB = amazonDynamoDb ); - _queryByPrice = Product - .FromPrice.OnTable(tableName) + _queryByPrice = FromPrice + .OnTable(tableName) .ToQueryRequestHandler( x => x.WithKeyConditionExpression((db, arg) => $"{db.Price} = {arg}") @@ -116,12 +193,6 @@ public class ProductRepository }, x => x.AmazonDynamoDB = amazonDynamoDb ); - - // You can also use a RequestBuilder if you want to handle the response yourself. - GetRequestBuilder getProductByIdRequestBuilder = Product - .FromId.OnTable(tableName) - .ToRequestBuilderFactory() - .ToGetRequestBuilder(); } public Task> SearchByPrice(decimal price) => @@ -137,15 +208,7 @@ public class ProductRepository _updatePrice.Send((id, price, DateTime.UtcNow), default); } -// These attributes is what makes the source generator kick in. Make sure to have the class 'partial' as well. -[DynamoDBMarshaller(AccessName = "FromProduct")] -[DynamoDBMarshaller(AccessName = "FromId", ArgumentType = typeof(string))] -[DynamoDBMarshaller( - AccessName = "FromUpdatePricePayload", - ArgumentType = typeof((string Id, decimal NewPrice, DateTime TimeStamp)) -)] -[DynamoDBMarshaller(AccessName = "FromPrice", ArgumentType = typeof(decimal))] -public partial record Product( +public record Product( [property: DynamoDBHashKey, DynamoDBGlobalSecondaryIndexRangeKey(Product.PriceIndex)] string Id, [property: DynamoDBGlobalSecondaryIndexHashKey(Product.PriceIndex)] decimal Price, string Description, diff --git a/samples/ExtendDto/ExtendDto.csproj b/samples/ExtendDto/ExtendDto.csproj new file mode 100644 index 0000000..e33e9c4 --- /dev/null +++ b/samples/ExtendDto/ExtendDto.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/samples/ExtendDto/Program.cs b/samples/ExtendDto/Program.cs new file mode 100644 index 0000000..231e4ca --- /dev/null +++ b/samples/ExtendDto/Program.cs @@ -0,0 +1,22 @@ +using DynamoDBGenerator.Attributes; +using Dynatello; +using Dynatello.Builders; +using Dynatello.Handlers; + +IRequestHandler getById = Cat + .FromId.OnTable("TABLE") + .ToGetRequestHandler(x => x.ToGetRequestBuilder()); + +if (args.Length == 1) +{ + var response = await getById.Send(args[0], CancellationToken.None); + + Console.WriteLine(response); +} +else +{ + throw new NotImplementedException(); +} + +[DynamoDBMarshaller(AccessName = "FromId", ArgumentType = typeof(string))] +public partial record Cat(string Id, string Name, double Cuteness); diff --git a/samples/Repository/Program.cs b/samples/Repository/Program.cs index 7749206..4b0bfc8 100644 --- a/samples/Repository/Program.cs +++ b/samples/Repository/Program.cs @@ -1,17 +1,31 @@ -using System.Diagnostics; -using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.DataModel; -using Amazon.Runtime; using DynamoDBGenerator.Attributes; using Dynatello; using Dynatello.Builders; using Dynatello.Builders.Types; using Dynatello.Handlers; -using Dynatello.Pipelines; ProductRepository productRepository = new ProductRepository("PRODUCTS", new AmazonDynamoDBClient()); -public class ProductRepository +// These attributes is what makes the source generator kick in. Make sure to have the class 'partial' as well. +[DynamoDBMarshaller(EntityType = typeof(Product), AccessName = "FromProduct")] +[DynamoDBMarshaller( + EntityType = typeof(Product), + AccessName = "FromId", + ArgumentType = typeof(string) +)] +[DynamoDBMarshaller( + EntityType = typeof(Product), + AccessName = "FromUpdatePricePayload", + ArgumentType = typeof((string Id, decimal NewPrice, DateTime TimeStamp)) +)] +[DynamoDBMarshaller( + EntityType = typeof(Product), + AccessName = "FromPrice", + ArgumentType = typeof(decimal) +)] +public partial class ProductRepository { private readonly IRequestHandler _getById; private readonly IRequestHandler< @@ -22,45 +36,27 @@ private readonly IRequestHandler< private readonly IRequestHandler> _queryByPrice; private readonly IRequestHandler _deleteById; - private class RequestLogger : IRequestPipeLine - { - public async Task Invoke( - RequestContext requestContext, - Func> continuation - ) - { - var stopwatch = Stopwatch.StartNew(); - Console.WriteLine("Starting request"); - var request = await continuation(requestContext); - Console.WriteLine($"Request finished after '{stopwatch.Elapsed}'."); - return request; - } - } - public ProductRepository(string tableName, IAmazonDynamoDB amazonDynamoDb) { - var requestLogger = new RequestLogger(); - _getById = Product - .FromId.OnTable(tableName) + _getById = FromId + .OnTable(tableName) .ToGetRequestHandler( x => x.ToGetRequestBuilder(), x => { x.AmazonDynamoDB = amazonDynamoDb; - // All handlers comes with the ability to add middlewares that will be executed. - x.RequestsPipelines.Add(requestLogger); } ); - _deleteById = Product - .FromId.OnTable(tableName) + _deleteById = FromId + .OnTable(tableName) .ToDeleteRequestHandler( x => x.ToDeleteRequestBuilder(), x => x.AmazonDynamoDB = amazonDynamoDb ); - _updatePrice = Product - .FromUpdatePricePayload.OnTable(tableName) + _updatePrice = FromUpdatePricePayload + .OnTable(tableName) .ToUpdateRequestHandler( x => x.WithUpdateExpression( @@ -73,8 +69,8 @@ public ProductRepository(string tableName, IAmazonDynamoDB amazonDynamoDb) x => x.AmazonDynamoDB = amazonDynamoDb ); - _createProduct = Product - .FromProduct.OnTable(tableName) + _createProduct = FromProduct + .OnTable(tableName) .ToPutRequestHandler( x => x.WithConditionExpression((db, arg) => $"{db.Id} <> {arg.Id}") // Ensure we don't have an existing Product in DynamoDB @@ -82,8 +78,8 @@ public ProductRepository(string tableName, IAmazonDynamoDB amazonDynamoDb) x => x.AmazonDynamoDB = amazonDynamoDb ); - _queryByPrice = Product - .FromPrice.OnTable(tableName) + _queryByPrice = FromPrice + .OnTable(tableName) .ToQueryRequestHandler( x => x.WithKeyConditionExpression((db, arg) => $"{db.Price} = {arg}") @@ -93,12 +89,6 @@ public ProductRepository(string tableName, IAmazonDynamoDB amazonDynamoDb) }, x => x.AmazonDynamoDB = amazonDynamoDb ); - - // You can also use a RequestBuilder if you want to handle the response yourself. - GetRequestBuilder getProductByIdRequestBuilder = Product - .FromId.OnTable(tableName) - .ToRequestBuilderFactory() - .ToGetRequestBuilder(); } public Task> SearchByPrice(decimal price) => @@ -114,15 +104,7 @@ public Task> SearchByPrice(decimal price) => _updatePrice.Send((id, price, DateTime.UtcNow), default); } -// These attributes is what makes the source generator kick in. Make sure to have the class 'partial' as well. -[DynamoDBMarshaller(AccessName = "FromProduct")] -[DynamoDBMarshaller(AccessName = "FromId", ArgumentType = typeof(string))] -[DynamoDBMarshaller( - AccessName = "FromUpdatePricePayload", - ArgumentType = typeof((string Id, decimal NewPrice, DateTime TimeStamp)) -)] -[DynamoDBMarshaller(AccessName = "FromPrice", ArgumentType = typeof(decimal))] -public partial record Product( +public record Product( [property: DynamoDBHashKey, DynamoDBGlobalSecondaryIndexRangeKey(Product.PriceIndex)] string Id, [property: DynamoDBGlobalSecondaryIndexHashKey(Product.PriceIndex)] decimal Price, string Description, diff --git a/samples/Repository/Repository.csproj b/samples/Repository/Repository.csproj index ed9cca5..a4da8c6 100644 --- a/samples/Repository/Repository.csproj +++ b/samples/Repository/Repository.csproj @@ -12,5 +12,4 @@ - diff --git a/samples/RequestPipeline/Program.cs b/samples/RequestPipeline/Program.cs new file mode 100644 index 0000000..b445f11 --- /dev/null +++ b/samples/RequestPipeline/Program.cs @@ -0,0 +1,41 @@ +using Amazon.Runtime; +using DynamoDBGenerator.Attributes; +using Dynatello; +using Dynatello.Builders; +using Dynatello.Handlers; +using Dynatello.Pipelines; + +IRequestHandler getById = Cat + .FromId.OnTable("TABLE") + .ToGetRequestHandler( + x => x.ToGetRequestBuilder(), + x => + { + x.RequestsPipelines.Add(new RequestConsoleLogger()); + } + ); + +if (args.Length == 1) +{ + var response = await getById.Send(args[0], CancellationToken.None); + + Console.WriteLine(response); +} +else +{ + throw new NotImplementedException(); +} + +public class RequestConsoleLogger : IRequestPipeLine +{ + public Task Invoke( + RequestContext requestContext, + Func> continuation + ) + { + throw new NotImplementedException(); + } +} + +[DynamoDBMarshaller(AccessName = "FromId", ArgumentType = typeof(string))] +public partial record Cat(string Id, string Name, double Cuteness); diff --git a/samples/RequestPipeline/RequestPipeline.csproj b/samples/RequestPipeline/RequestPipeline.csproj new file mode 100644 index 0000000..42b2f74 --- /dev/null +++ b/samples/RequestPipeline/RequestPipeline.csproj @@ -0,0 +1,13 @@ + + + + Exe + net8.0 + enable + enable + + + + + +