From da6ed2e86dd5ca4ffdee581158014c28d62d2141 Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Mon, 26 Feb 2024 16:14:12 +0100 Subject: [PATCH 01/16] Add new assembly to improve convenience --- DynamoDBGenerator.sln | 14 ++ .../DynamoDBMarshallerExtensions.cs | 86 +----------- src/DynamoDBGenerator/IDynamoDBClient.cs | 93 ------------- .../Internal/DynamoDBClient.cs | 67 --------- .../Internal/ExceptionHelper.cs | 2 +- .../Internal/RequestFactory.cs | 74 ---------- src/Dynatello/Builders/Extensions.cs | 130 ++++++++++++++++++ .../Builders/GetItemRequestBuilder.cs | 47 +++++++ src/Dynatello/Builders/PutRequestBuilder.cs | 80 +++++++++++ src/Dynatello/Builders/QueryRequestBuilder.cs | 80 +++++++++++ .../Builders/Types/ConditionExpression.cs | 26 ++++ .../Types/ConditionalUpdateExpression.cs | 28 ++++ src/Dynatello/Builders/Types/Extensions.cs | 74 ++++++++++ .../Builders/Types/KeyConditionExpression.cs | 52 +++++++ src/Dynatello/Builders/Types/TableAccess.cs | 23 ++++ .../Builders/Types/UpdateExpression.cs | 25 ++++ .../Builders/UpdateRequestBuilder.cs | 100 ++++++++++++++ src/Dynatello/DynamoDBMarshallerExtensions.cs | 54 ++++++++ src/Dynatello/Dynatello.csproj | 26 ++++ tests/Dynatello.Tests/Dynatello.Tests.csproj | 31 +++++ .../Dynatello.Tests/ToGetItemRequestTests.cs | 107 ++++++++++++++ .../ToPutItemRequestTests.cs | 23 +++- tests/Dynatello.Tests/ToQueryRequestTests.cs | 89 ++++++++++++ .../ToUpdateItemRequestTests.cs | 71 +++++----- tests/Dynatello.Tests/Usings.cs | 1 + 25 files changed, 1038 insertions(+), 365 deletions(-) delete mode 100644 src/DynamoDBGenerator/IDynamoDBClient.cs delete mode 100644 src/DynamoDBGenerator/Internal/DynamoDBClient.cs delete mode 100644 src/DynamoDBGenerator/Internal/RequestFactory.cs create mode 100644 src/Dynatello/Builders/Extensions.cs create mode 100644 src/Dynatello/Builders/GetItemRequestBuilder.cs create mode 100644 src/Dynatello/Builders/PutRequestBuilder.cs create mode 100644 src/Dynatello/Builders/QueryRequestBuilder.cs create mode 100644 src/Dynatello/Builders/Types/ConditionExpression.cs create mode 100644 src/Dynatello/Builders/Types/ConditionalUpdateExpression.cs create mode 100644 src/Dynatello/Builders/Types/Extensions.cs create mode 100644 src/Dynatello/Builders/Types/KeyConditionExpression.cs create mode 100644 src/Dynatello/Builders/Types/TableAccess.cs create mode 100644 src/Dynatello/Builders/Types/UpdateExpression.cs create mode 100644 src/Dynatello/Builders/UpdateRequestBuilder.cs create mode 100644 src/Dynatello/DynamoDBMarshallerExtensions.cs create mode 100644 src/Dynatello/Dynatello.csproj create mode 100644 tests/Dynatello.Tests/Dynatello.Tests.csproj create mode 100644 tests/Dynatello.Tests/ToGetItemRequestTests.cs rename tests/{DynamoDBGenerator.SourceGenerator.Tests/Extensions => Dynatello.Tests}/ToPutItemRequestTests.cs (83%) create mode 100644 tests/Dynatello.Tests/ToQueryRequestTests.cs rename tests/{DynamoDBGenerator.SourceGenerator.Tests/Extensions => Dynatello.Tests}/ToUpdateItemRequestTests.cs (74%) create mode 100644 tests/Dynatello.Tests/Usings.cs diff --git a/DynamoDBGenerator.sln b/DynamoDBGenerator.sln index eb6a406f..9e0f6233 100644 --- a/DynamoDBGenerator.sln +++ b/DynamoDBGenerator.sln @@ -16,6 +16,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{CF34 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{11F1D954-39EC-4EDD-9460-04FCC216E97A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dynatello.Tests", "tests\Dynatello.Tests\Dynatello.Tests.csproj", "{D9C9C74E-B52C-4510-9258-BA78532EAABB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dynatello", "src\Dynatello\Dynatello.csproj", "{E4D47C8F-A0C8-4368-BBE0-DC6045B2D734}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,6 +46,14 @@ Global {A80AC940-3BD8-4377-BDB9-AE82FD4DF944}.Debug|Any CPU.Build.0 = Debug|Any CPU {A80AC940-3BD8-4377-BDB9-AE82FD4DF944}.Release|Any CPU.ActiveCfg = Release|Any CPU {A80AC940-3BD8-4377-BDB9-AE82FD4DF944}.Release|Any CPU.Build.0 = Release|Any CPU + {D9C9C74E-B52C-4510-9258-BA78532EAABB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9C9C74E-B52C-4510-9258-BA78532EAABB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9C9C74E-B52C-4510-9258-BA78532EAABB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9C9C74E-B52C-4510-9258-BA78532EAABB}.Release|Any CPU.Build.0 = Release|Any CPU + {E4D47C8F-A0C8-4368-BBE0-DC6045B2D734}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4D47C8F-A0C8-4368-BBE0-DC6045B2D734}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4D47C8F-A0C8-4368-BBE0-DC6045B2D734}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4D47C8F-A0C8-4368-BBE0-DC6045B2D734}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {D8EABA41-D014-49BD-B109-54829DB835E7} = {E84C4630-5241-4FAA-8F86-964AB25A2C6F} @@ -49,5 +61,7 @@ Global {834A7F6C-3C82-427F-9BFB-9686672A5BDE} = {CF34190F-AC01-42FC-B28A-DD820442243A} {53F899A8-28AA-450F-9C62-FD478119B2B7} = {11F1D954-39EC-4EDD-9460-04FCC216E97A} {648B1DF4-9684-4422-95F5-74BB89862E4D} = {11F1D954-39EC-4EDD-9460-04FCC216E97A} + {D9C9C74E-B52C-4510-9258-BA78532EAABB} = {E84C4630-5241-4FAA-8F86-964AB25A2C6F} + {E4D47C8F-A0C8-4368-BBE0-DC6045B2D734} = {11F1D954-39EC-4EDD-9460-04FCC216E97A} EndGlobalSection EndGlobal diff --git a/src/DynamoDBGenerator/Extensions/DynamoDBMarshallerExtensions.cs b/src/DynamoDBGenerator/Extensions/DynamoDBMarshallerExtensions.cs index 2a46234f..dd27ea6d 100644 --- a/src/DynamoDBGenerator/Extensions/DynamoDBMarshallerExtensions.cs +++ b/src/DynamoDBGenerator/Extensions/DynamoDBMarshallerExtensions.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.Model; using DynamoDBGenerator.Internal; + namespace DynamoDBGenerator.Extensions; /// @@ -74,86 +72,4 @@ params Func[] expressionBuilders expressionBuilders ); } - - /// - /// Converts the into an . - /// - public static IDynamoDBClient ToDynamoDBClient( - this IDynamoDBMarshaller item, - string tableName, - IAmazonDynamoDB dynamoDB - ) - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker - { - return new DynamoDBClient(item, tableName, dynamoDB); - } - - /// - /// Creates a . - /// - public static PutItemRequest ToPutItemRequest( - this IDynamoDBMarshaller item, - T entity, - ReturnValue returnValue, - string tableName - ) - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker - where T : TArg - { - return item.ToPutItemRequestInternal(entity, entity, null, returnValue, tableName); - } - - /// - /// Creates a with condition expression. - /// - public static PutItemRequest ToPutItemRequest( - this IDynamoDBMarshaller item, - T entity, - Func conditionExpressionBuilder, - ReturnValue returnValue, - string tableName - ) - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker - where T : TArg - { - return item.ToPutItemRequestInternal(entity, entity, conditionExpressionBuilder, returnValue, tableName); - } - - /// - /// Creates a . - /// - public static UpdateItemRequest ToUpdateItemRequest( - this IDynamoDBMarshaller item, - TArg argument, - Func> keySelector, - Func updateExpressionBuilder, - ReturnValue returnValue, - string tableName - ) - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker - { - return item.ToUpdateItemRequestInternal(argument, keySelector, updateExpressionBuilder, null, returnValue, tableName); - } - - /// - /// Creates a with a condition expression. - /// - public static UpdateItemRequest ToUpdateItemRequest( - this IDynamoDBMarshaller item, - TArg argument, - Func> keySelector, - Func updateExpressionBuilder, - Func conditionExpressionBuilder, - ReturnValue returnValue, - string tableName - ) - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker - { - return item.ToUpdateItemRequestInternal(argument, keySelector, updateExpressionBuilder, conditionExpressionBuilder, returnValue, tableName); - } } \ No newline at end of file diff --git a/src/DynamoDBGenerator/IDynamoDBClient.cs b/src/DynamoDBGenerator/IDynamoDBClient.cs deleted file mode 100644 index d72b8530..00000000 --- a/src/DynamoDBGenerator/IDynamoDBClient.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Amazon.DynamoDBv2.Model; -namespace DynamoDBGenerator; - -/// -/// Represents a client with asynchronous methods for sending requests to DynamoDB. -/// -/// The type of entity associated with DynamoDB operations. -/// The type of argument used in DynamoDB operations. -/// The type for tracking attribute names related to . -/// The type for tracking argument attribute values related to . -public interface IDynamoDBClient - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker -{ - /// - /// Saves an entity to DynamoDB with an optional condition expression builder. - /// - /// The type of entity to save. - /// The entity to be saved. - /// A function to build a condition expression. - /// A token to cancel the operation. - /// A Task representing the asynchronous operation. - Task Save( - T entity, - Func conditionExpressionBuilder, - CancellationToken cancellationToken = default - ) where T : TEntity, TArgument; - - /// - /// Saves an entity to DynamoDB without a condition expression. - /// - /// The type of entity to save. - /// The entity to be saved. - /// A token to cancel the operation. - /// A Task representing the asynchronous operation. - Task Save( - T entity, - CancellationToken cancellationToken = default - ) where T : TEntity, TArgument; - - /// - /// Updates an entity in DynamoDB. - /// - /// The entity to update. - /// A function to select the keys for the update operation. - /// A function to build the update expression. - /// A token to cancel the operation. - /// A Task representing the asynchronous operation. - Task Update( - TArgument entity, - Func> keySelector, - Func updateExpressionBuilder, - CancellationToken cancellationToken = default - ); - - /// - /// Updates an entity in DynamoDB with optional condition and update expression builders. - /// - /// The entity to update. - /// A function to select the keys for the update operation. - /// A function to build the update expression. - /// A function to build the condition expression. - /// A token to cancel the operation. - /// A Task representing the asynchronous operation. - Task Update( - TArgument entity, - Func> keySelector, - Func updateExpressionBuilder, - Func conditionExpressionBuilder, - CancellationToken cancellationToken = default - ); - - /// - /// Updates an entity in DynamoDB and returns the updated entity. - /// - /// The entity to update. - /// A function to select the keys for the update operation. - /// A function to build the update expression. - /// A function to build the condition expression. - /// A token to cancel the operation. - /// The updated entity. - Task UpdateReturned( - TArgument entity, - Func> keySelector, - Func updateExpressionBuilder, - Func conditionExpressionBuilder, - CancellationToken cancellationToken = default - ); -} \ No newline at end of file diff --git a/src/DynamoDBGenerator/Internal/DynamoDBClient.cs b/src/DynamoDBGenerator/Internal/DynamoDBClient.cs deleted file mode 100644 index 66ebb591..00000000 --- a/src/DynamoDBGenerator/Internal/DynamoDBClient.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.Model; -namespace DynamoDBGenerator.Internal; - -internal class DynamoDBClient : IDynamoDBClient - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker -{ - private readonly IAmazonDynamoDB _amazonDynamoDB; - private readonly IDynamoDBMarshaller _marshaller; - private readonly string _tableName; - - public DynamoDBClient(IDynamoDBMarshaller marshaller, string tableName, IAmazonDynamoDB amazonDynamoDB) - { - _marshaller = marshaller; - _tableName = tableName; - _amazonDynamoDB = amazonDynamoDB; - } - - public Task Save(T1 entity, Func conditionExpressionBuilder, CancellationToken cancellationToken = default) where T1 : T, TArg - { - var putRequest = _marshaller.ToPutItemRequestInternal(entity, entity, conditionExpressionBuilder, ReturnValue.NONE, _tableName); - return _amazonDynamoDB.PutItemAsync(putRequest, cancellationToken); - } - - public Task Save(T1 entity, CancellationToken cancellationToken = default) where T1 : T, TArg - { - var putRequest = _marshaller.ToPutItemRequestInternal(entity, entity, null, ReturnValue.NONE, _tableName); - return _amazonDynamoDB.PutItemAsync(putRequest, cancellationToken); - } - - public Task Update( - TArg entity, - Func> keySelector, - Func updateExpressionBuilder, - CancellationToken cancellationToken = default - ) - { - var updateItemRequest = _marshaller.ToUpdateItemRequestInternal(entity, keySelector, updateExpressionBuilder, null, ReturnValue.NONE, _tableName); - return _amazonDynamoDB.UpdateItemAsync(updateItemRequest, cancellationToken); - } - - public Task Update( - TArg entity, - Func> keySelector, - Func updateExpressionBuilder, - Func conditionExpressionBuilder, - CancellationToken cancellationToken = default - ) - { - var updateItemRequest = _marshaller.ToUpdateItemRequestInternal(entity, keySelector, updateExpressionBuilder, conditionExpressionBuilder, ReturnValue.NONE, _tableName); - return _amazonDynamoDB.UpdateItemAsync(updateItemRequest, cancellationToken); - } - - public async Task UpdateReturned(TArg entity, Func> keySelector, Func updateExpressionBuilder, - Func conditionExpressionBuilder, CancellationToken cancellationToken = default) - { - var updateItemRequest = _marshaller.ToUpdateItemRequestInternal(entity, keySelector, updateExpressionBuilder, conditionExpressionBuilder, ReturnValue.ALL_NEW, _tableName); - var result = await _amazonDynamoDB.UpdateItemAsync(updateItemRequest, cancellationToken); - - return _marshaller.Unmarshall(result.Attributes); - } -} \ No newline at end of file diff --git a/src/DynamoDBGenerator/Internal/ExceptionHelper.cs b/src/DynamoDBGenerator/Internal/ExceptionHelper.cs index 08bb9055..0845f7fe 100644 --- a/src/DynamoDBGenerator/Internal/ExceptionHelper.cs +++ b/src/DynamoDBGenerator/Internal/ExceptionHelper.cs @@ -23,7 +23,7 @@ public static DynamoDBMarshallingException KeysArgumentNotNull(string memberName public static DynamoDBMarshallingException KeysInvalidConversion(string memberName, string argumentName, object value, string expectedType) { - return new DynamoDBMarshallingException(memberName, $"Value '{{{value}}}' from argument '{{nameof({argumentName})}}' is not convertable to '{expectedType}'."); + return new DynamoDBMarshallingException(memberName, $"Value '{{{value}}}' from argument '{argumentName}' is not convertable to '{expectedType}'."); } public static InvalidOperationException KeysValueWithNoCorrespondence(string argumentName, object value) diff --git a/src/DynamoDBGenerator/Internal/RequestFactory.cs b/src/DynamoDBGenerator/Internal/RequestFactory.cs deleted file mode 100644 index 32eb2ef7..00000000 --- a/src/DynamoDBGenerator/Internal/RequestFactory.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.Model; -namespace DynamoDBGenerator.Internal; - -internal static class RequestFactory -{ - internal static PutItemRequest ToPutItemRequestInternal( - this IDynamoDBMarshaller item, - T entity, - TArg argument, - Func? conditionExpressionBuilder, - ReturnValue returnValue, - string tableName - ) - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker - { - - Dictionary? expressionAttributeValues = null; - Dictionary? expressionAttributeNames = null; - string? conditionExpression = null; - - if (conditionExpressionBuilder is not null) - { - var nameTracker = item.AttributeExpressionNameTracker(); - var valueTracker = item.AttributeExpressionValueTracker(); - conditionExpression = conditionExpressionBuilder.Invoke(nameTracker, valueTracker); - expressionAttributeNames = nameTracker.AccessedNames().ToDictionary(x => x.Key, x => x.Value); - expressionAttributeValues = valueTracker.AccessedValues(argument).ToDictionary(x => x.Key, x => x.Value); - } - - return new PutItemRequest - { - TableName = tableName, - ExpressionAttributeNames = expressionAttributeNames, - ExpressionAttributeValues = expressionAttributeValues, - ConditionExpression = conditionExpression, - Item = item.Marshall(entity), - ReturnValues = returnValue - }; - } - internal static UpdateItemRequest ToUpdateItemRequestInternal( - this IDynamoDBMarshaller item, - TArg argument, - Func> keySelector, - Func updateExpressionBuilder, - Func? conditionExpressionBuilder, - ReturnValue returnValue, - string tableName - ) - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker - { - - var nameTracker = item.AttributeExpressionNameTracker(); - var argumentTracker = item.AttributeExpressionValueTracker(); - var updateExpression = updateExpressionBuilder(nameTracker, argumentTracker); - var conditionExpression = conditionExpressionBuilder?.Invoke(nameTracker, argumentTracker); - - return new UpdateItemRequest - { - Key = keySelector(item.PrimaryKeyMarshaller, argument), - TableName = tableName, - ExpressionAttributeNames = nameTracker.AccessedNames().ToDictionary(x => x.Key, x => x.Value), - ExpressionAttributeValues = argumentTracker.AccessedValues(argument).ToDictionary(x => x.Key, x => x.Value), - ConditionExpression = conditionExpression, - UpdateExpression = updateExpression, - ReturnValues = returnValue - }; - } -} \ No newline at end of file diff --git a/src/Dynatello/Builders/Extensions.cs b/src/Dynatello/Builders/Extensions.cs new file mode 100644 index 00000000..25f046fe --- /dev/null +++ b/src/Dynatello/Builders/Extensions.cs @@ -0,0 +1,130 @@ +using Amazon.DynamoDBv2.Model; +using DynamoDBGenerator; +using Dynatello.Builders.Types; + +namespace Dynatello.Builders; + +public static class Extensions +{ + public static QueryRequestBuilder ToQueryRequestBuilder( + this KeyConditionedFilterExpression source + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new QueryRequestBuilder( + source.TableAccess.Item.ComposeAttributeExpression(source.Condition, source.Filter), + source.TableAccess.TableName + ); + } + + public static QueryRequestBuilder ToQueryRequestBuilder( + this KeyConditionExpression source + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new QueryRequestBuilder( + source.TableAccess.Item.ComposeAttributeExpression(source.Condition, null), + source.TableAccess.TableName + ); + } + + public static GetItemRequestBuilder ToGetRequestBuilder( + this TableAccess source, + // ReSharper disable once UnusedParameter.Global is used to determined typing + Func partitionKeySelector) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + where TPartition : notnull + { + return new GetItemRequestBuilder(source.TableName, source.Item.PrimaryKeyMarshaller, + (x, y) => x.PartitionKey(y)); + } + + public static GetItemRequestBuilder<(TPartition partionKey, TRange rangeKey)> ToGetRequestBuilder( + this TableAccess source, + // ReSharper disable once UnusedParameter.Global is used to determined typing + Func partitionKeySelector, + // ReSharper disable once UnusedParameter.Global is used to determined typing + Func rangeKeySelector) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + where TPartition : notnull + where TRange : notnull + { + return new GetItemRequestBuilder<(TPartition partionKey, TRange rangeKey)>(source.TableName, + source.Item.PrimaryKeyMarshaller, (x, y) => x.Keys(y.partionKey, y.rangeKey)); + } + + public static UpdateRequestBuilder ToUpdateItemRequestBuilder( + this UpdateExpression source, + Func> keySelector + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new UpdateRequestBuilder( + source.TableAccess.Item.ComposeAttributeExpression(source.Update, null), + source.TableAccess.TableName, + keySelector, + source.TableAccess.Item.PrimaryKeyMarshaller + ); + } + + public static UpdateRequestBuilder ToUpdateItemRequestBuilder( + this ConditionalUpdateExpression source, + Func> keySelector + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new UpdateRequestBuilder( + source.TableAccess.Item.ComposeAttributeExpression(source.Update, source.Condition), + source.TableAccess.TableName, + keySelector, + source.TableAccess.Item.PrimaryKeyMarshaller + ); + } + + public static KeyConditionExpression WithKeyConditionExpression( + this TableAccess source, + Func condition) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new KeyConditionExpression(source, condition); + } + + public static PutRequestBuilder ToPutRequestBuilder( + this TableAccess source + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new PutRequestBuilder + ( + null, + source.Item.Marshall, + source.TableName + ); + } + + public static PutRequestBuilder ToPutRequestBuilder( + this ConditionExpression source + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new PutRequestBuilder + ( + source.TableAccess.Item.ComposeAttributeExpression(null, source.Condition), + source.TableAccess.Item.Marshall, + source.TableAccess.TableName + ); + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/GetItemRequestBuilder.cs b/src/Dynatello/Builders/GetItemRequestBuilder.cs new file mode 100644 index 00000000..e6508fa7 --- /dev/null +++ b/src/Dynatello/Builders/GetItemRequestBuilder.cs @@ -0,0 +1,47 @@ +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using DynamoDBGenerator; + +namespace Dynatello.Builders; + +public readonly record struct GetItemRequestBuilder where TKey : notnull +{ + private readonly IDynamoDBKeyMarshaller _keyMarshaller; + private readonly Func> _keysSelector; + + /// + public string TableName { get; init; } + + + /// + public bool? ConsistentRead { get; init; } = null; + + /// + public ReturnConsumedCapacity? ReturnConsumedCapacity { get; init; } = null; + + internal GetItemRequestBuilder(string tableName, IDynamoDBKeyMarshaller keyMarshaller, + Func> keysSelector) + { + _keyMarshaller = keyMarshaller; + _keysSelector = keysSelector; + TableName = tableName; + } + + public GetItemRequest Build(TKey key) + { + var request = new GetItemRequest + { + ReturnConsumedCapacity = ReturnConsumedCapacity, + TableName = TableName, + Key = _keysSelector(_keyMarshaller, key) + }; + + if (ConsistentRead is { } consistentRead) + request.ConsistentRead = consistentRead; + + if (ReturnConsumedCapacity is not null) + request.ReturnConsumedCapacity = ReturnConsumedCapacity; + + return request; + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/PutRequestBuilder.cs b/src/Dynatello/Builders/PutRequestBuilder.cs new file mode 100644 index 00000000..711ba01d --- /dev/null +++ b/src/Dynatello/Builders/PutRequestBuilder.cs @@ -0,0 +1,80 @@ +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using DynamoDBGenerator; + +namespace Dynatello.Builders; + +/// +/// A record based builder for creating that can be configured via the `with` syntax. +/// +public readonly record struct PutRequestBuilder +{ + private readonly Func? _attributeExpressionSelector; + private readonly Func> _marshall; + + private readonly string _tableName; + + + internal PutRequestBuilder( + Func? attributeExpressionSelector, + Func> marshall, + string tableName + ) + { + _attributeExpressionSelector = attributeExpressionSelector; + _marshall = marshall; + _tableName = tableName; + } + + /// + public string TableName + { + get => _tableName; + init => _tableName = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + public ReturnValue? ReturnValues { get; init; } = null; + + /// + public ReturnConsumedCapacity? ReturnConsumedCapacity { get; init; } = null; + + /// + public ReturnItemCollectionMetrics? ReturnItemCollectionMetrics { get; init; } = null; + + /// + public ReturnValuesOnConditionCheckFailure? ReturnValuesOnConditionCheckFailure { get; init; } = null; + + + public PutItemRequest Build(T element) + { + var request = new PutItemRequest + { + TableName = TableName, + Item = _marshall(element), + Expected = null, + ConditionalOperator = null, + ConditionExpression = null, + ExpressionAttributeNames = null, + ExpressionAttributeValues = null + }; + + if (ReturnValues is not null) + request.ReturnValues = ReturnValues; + if (ReturnConsumedCapacity is not null) + request.ReturnConsumedCapacity = ReturnConsumedCapacity; + if (ReturnItemCollectionMetrics is not null) + request.ReturnItemCollectionMetrics = ReturnItemCollectionMetrics; + if (ReturnValuesOnConditionCheckFailure is not null) + request.ReturnValuesOnConditionCheckFailure = ReturnValuesOnConditionCheckFailure; + + if (_attributeExpressionSelector is null) return request; + var attributeExpression = _attributeExpressionSelector(element); + + request.ExpressionAttributeNames = attributeExpression.Names; + request.ExpressionAttributeValues = attributeExpression.Values; + request.ConditionExpression = attributeExpression.Expressions[0]; + + return request; + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/QueryRequestBuilder.cs b/src/Dynatello/Builders/QueryRequestBuilder.cs new file mode 100644 index 00000000..e4ce550c --- /dev/null +++ b/src/Dynatello/Builders/QueryRequestBuilder.cs @@ -0,0 +1,80 @@ +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using DynamoDBGenerator; + +namespace Dynatello.Builders; + +public readonly record struct QueryRequestBuilder +{ + private readonly Func _attributeExpressionSelector; + + /// + public string TableName { get; init; } + + /// + public string? IndexName { get; init; } = null; + + /// + public int? Limit { get; init; } = null; + + /// + public bool? ConsistentRead { get; init; } = null; + + /// + public bool? ScanIndexForward { get; init; } = null; + + /// + public Select? Select { get; init; } = null; + + /// + public ReturnConsumedCapacity? ReturnConsumedCapacity { get; init; } = null; + + public QueryRequestBuilder(Func attributeExpressionSelector, string tableName) + { + _attributeExpressionSelector = attributeExpressionSelector; + TableName = tableName; + } + + public QueryRequest Build(T arg) + { + var attributeExpression = _attributeExpressionSelector(arg); + + var queryRequest = new QueryRequest + { + AttributesToGet = null, + QueryFilter = null, + ConditionalOperator = null, + KeyConditions = null, + KeyConditionExpression = attributeExpression.Expressions[0], + ExpressionAttributeValues = attributeExpression.Values, + ExpressionAttributeNames = attributeExpression.Names, + TableName = TableName, + IndexName = IndexName, + ProjectionExpression = null + }; + + + if (ReturnConsumedCapacity is not null) + queryRequest.ReturnConsumedCapacity = ReturnConsumedCapacity; + + if (ConsistentRead is {} consistentRead) + queryRequest.ConsistentRead = consistentRead; + + if (ScanIndexForward is {} scanIndexForward) + queryRequest.ScanIndexForward = scanIndexForward; + + if (Select is not null) + queryRequest.Select = Select; + + if (Limit is { } limit) + queryRequest.Limit = limit; + + if (IndexName is not null) + queryRequest.IndexName = IndexName; + + if (attributeExpression.Expressions.Count == 2) + queryRequest.FilterExpression = attributeExpression.Expressions[1]; + + return queryRequest; + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/Types/ConditionExpression.cs b/src/Dynatello/Builders/Types/ConditionExpression.cs new file mode 100644 index 00000000..8e8d61c4 --- /dev/null +++ b/src/Dynatello/Builders/Types/ConditionExpression.cs @@ -0,0 +1,26 @@ +using DynamoDBGenerator; + +namespace Dynatello.Builders.Types; + +public readonly record struct ConditionExpression + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker +{ + internal readonly Func Condition; + internal readonly TableAccess TableAccess; + + [Obsolete("Do not use this constructor!", true)] + public ConditionExpression() + { + throw new InvalidOperationException("This is an invalid constructor access."); + } + + internal ConditionExpression( + in TableAccess tableAccess, + in Func condition + ) + { + TableAccess = tableAccess; + Condition = condition; + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/Types/ConditionalUpdateExpression.cs b/src/Dynatello/Builders/Types/ConditionalUpdateExpression.cs new file mode 100644 index 00000000..15b251b8 --- /dev/null +++ b/src/Dynatello/Builders/Types/ConditionalUpdateExpression.cs @@ -0,0 +1,28 @@ +using DynamoDBGenerator; + +namespace Dynatello.Builders.Types; + +public readonly record struct ConditionalUpdateExpression + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker +{ + internal readonly Func Condition; + internal readonly TableAccess TableAccess; + internal readonly Func Update; + + [Obsolete("Do not use this constructor!", true)] + public ConditionalUpdateExpression() + { + throw new InvalidOperationException("This is an invalid constructor access."); + } + + internal ConditionalUpdateExpression( + in TableAccess tableAccess, + in Func update, + in Func condition) + { + TableAccess = tableAccess; + Update = update; + Condition = condition; + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/Types/Extensions.cs b/src/Dynatello/Builders/Types/Extensions.cs new file mode 100644 index 00000000..815183f1 --- /dev/null +++ b/src/Dynatello/Builders/Types/Extensions.cs @@ -0,0 +1,74 @@ +using DynamoDBGenerator; + +namespace Dynatello.Builders.Types; + +public static class Extensions +{ + public static KeyConditionedFilterExpression WithFilterExpression( + this KeyConditionExpression source, + Func filter + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new KeyConditionedFilterExpression( + source.TableAccess, + source.Condition, + filter + ); + } + + public static ConditionalUpdateExpression WithConditionExpression( + this UpdateExpression source, + Func condition) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new ConditionalUpdateExpression( + in source.TableAccess, + in source.Update, + condition + ); + } + + public static UpdateExpression WithUpdateExpression( + this TableAccess source, + Func updateExpression + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new UpdateExpression(in source, in updateExpression); + } + + public static ConditionExpression WithConditionExpression( + this TableAccess source, + Func condition + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new ConditionExpression(in source, in condition); + } + + public static ConditionalUpdateExpression WithUpdateExpression( + this ConditionExpression source, + Func update + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new ConditionalUpdateExpression( + in source.TableAccess, + in update, + in source.Condition + ); + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/Types/KeyConditionExpression.cs b/src/Dynatello/Builders/Types/KeyConditionExpression.cs new file mode 100644 index 00000000..c0322d31 --- /dev/null +++ b/src/Dynatello/Builders/Types/KeyConditionExpression.cs @@ -0,0 +1,52 @@ +using DynamoDBGenerator; + +namespace Dynatello.Builders.Types; + +public readonly struct KeyConditionedFilterExpression + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker +{ + internal readonly Func Condition; + internal readonly Func Filter; + internal readonly TableAccess TableAccess; + + [Obsolete("Do not use this constructor!", true)] + public KeyConditionedFilterExpression() + { + throw new InvalidOperationException("This is an invalid constructor access."); + } + + internal KeyConditionedFilterExpression( + in TableAccess tableAccess, + in Func condition, + in Func filter + ) + { + TableAccess = tableAccess; + Condition = condition; + Filter = filter; + } +} + +public readonly struct KeyConditionExpression + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker +{ + internal readonly Func Condition; + internal readonly TableAccess TableAccess; + + [Obsolete("Do not use this constructor!", true)] + public KeyConditionExpression() + { + throw new InvalidOperationException("This is an invalid constructor access."); + } + + internal KeyConditionExpression( + in TableAccess tableAccess, + in Func condition + ) + { + TableAccess = tableAccess; + Condition = condition; + } +} diff --git a/src/Dynatello/Builders/Types/TableAccess.cs b/src/Dynatello/Builders/Types/TableAccess.cs new file mode 100644 index 00000000..421dd04f --- /dev/null +++ b/src/Dynatello/Builders/Types/TableAccess.cs @@ -0,0 +1,23 @@ +using DynamoDBGenerator; + +namespace Dynatello.Builders.Types; + +public readonly record struct TableAccess + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker +{ + [Obsolete("Do not use this constructor!", true)] + public TableAccess() + { + throw new InvalidOperationException("This is an invalid constructor access."); + } + + internal TableAccess(in string tableName, in IDynamoDBMarshaller item) + { + TableName = tableName ?? throw new ArgumentNullException(nameof(tableName)); + Item = item ?? throw new ArgumentNullException(nameof(item)); + } + + internal string TableName { get; } + internal IDynamoDBMarshaller Item { get; } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/Types/UpdateExpression.cs b/src/Dynatello/Builders/Types/UpdateExpression.cs new file mode 100644 index 00000000..6d0e2088 --- /dev/null +++ b/src/Dynatello/Builders/Types/UpdateExpression.cs @@ -0,0 +1,25 @@ +using DynamoDBGenerator; + +namespace Dynatello.Builders.Types; + +public readonly record struct UpdateExpression + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker +{ + internal readonly TableAccess TableAccess; + internal readonly Func Update; + + [Obsolete("Do not use this constructor!", true)] + public UpdateExpression() + { + throw new InvalidOperationException("This is an invalid constructor access."); + } + + internal UpdateExpression( + in TableAccess tableAccess, + in Func update) + { + TableAccess = tableAccess; + Update = update; + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/UpdateRequestBuilder.cs b/src/Dynatello/Builders/UpdateRequestBuilder.cs new file mode 100644 index 00000000..a18af842 --- /dev/null +++ b/src/Dynatello/Builders/UpdateRequestBuilder.cs @@ -0,0 +1,100 @@ +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using DynamoDBGenerator; + +namespace Dynatello.Builders; + +/// +/// A record based builder for creating that can be configured via the `with` syntax. +/// +public readonly record struct UpdateRequestBuilder +{ + private readonly Func _attributeExpressionSelector; + private readonly IDynamoDBKeyMarshaller _keyMarshaller; + private readonly Func> _keySelector; + + private readonly string _tableName; + + [Obsolete("Do not used this constructor!", true)] + public UpdateRequestBuilder() + { + throw new InvalidOperationException("This is an invalid constructor access."); + } + + internal UpdateRequestBuilder( + Func attributeExpressionSelector, + string tableName, + Func> keySelector, + IDynamoDBKeyMarshaller keyMarshaller + ) + { + _attributeExpressionSelector = attributeExpressionSelector; + _tableName = tableName; + _keySelector = keySelector; + _keyMarshaller = keyMarshaller; + } + + /// + public string TableName + { + get => _tableName; + init => _tableName = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + /// A function to specify how the keys should be accessed through the . + /// + public Func> KeySelector + { + get => _keySelector; + init => _keySelector = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + public ReturnConsumedCapacity? ReturnConsumedCapacity { get; init; } = null; + + /// + public ReturnItemCollectionMetrics? ReturnItemCollectionMetrics { get; init; } = null; + + /// + public ReturnValue? ReturnValues { get; init; } = null; + + /// + public ReturnValuesOnConditionCheckFailure? ReturnValuesOnConditionCheckFailure { get; init; } = null; + + + /// + /// Will build a with the specified configurations. + /// + public UpdateItemRequest Build(T arg) + { + var expression = _attributeExpressionSelector(arg); + var update = new UpdateItemRequest + { + UpdateExpression = expression.Expressions[0], + ConditionExpression = expression.Expressions.Count is 2 ? expression.Expressions[1] : null, + TableName = TableName, + Key = KeySelector(_keyMarshaller, arg), + ExpressionAttributeNames = expression.Names, + ExpressionAttributeValues = expression.Values, + Expected = null, + AttributeUpdates = null, + ConditionalOperator = null + }; + + if (ReturnValues is not null) + update.ReturnValues = ReturnValues; + + if (ReturnConsumedCapacity is not null) + update.ReturnConsumedCapacity = ReturnConsumedCapacity; + + if (ReturnItemCollectionMetrics is not null) + update.ReturnItemCollectionMetrics = ReturnItemCollectionMetrics; + + if (ReturnValuesOnConditionCheckFailure is not null) + update.ReturnValuesOnConditionCheckFailure = ReturnValuesOnConditionCheckFailure; + + + return update; + } +} \ No newline at end of file diff --git a/src/Dynatello/DynamoDBMarshallerExtensions.cs b/src/Dynatello/DynamoDBMarshallerExtensions.cs new file mode 100644 index 00000000..346f5583 --- /dev/null +++ b/src/Dynatello/DynamoDBMarshallerExtensions.cs @@ -0,0 +1,54 @@ +using DynamoDBGenerator; +using Dynatello.Builders.Types; +using static DynamoDBGenerator.Extensions.DynamoDBMarshallerExtensions; + +namespace Dynatello; + +public static class DynamoDBMarshallerExtensions +{ + public static TableAccess OnTable + + (this IDynamoDBMarshaller item, string tableName) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return new TableAccess(in tableName, in item); + } + + internal static Func ComposeAttributeExpression( + this IDynamoDBMarshaller source, + Func? update, + Func? condition + ) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + { + return (update, condition) switch + { + (null, null) => throw new ArgumentNullException(""), + (not null, not null) => y => + ToAttributeExpression( + source.AttributeExpressionNameTracker, + source.AttributeExpressionValueTracker, + y, + update, + condition + ), + (not null, null) => y => + ToAttributeExpression( + source.AttributeExpressionNameTracker, + source.AttributeExpressionValueTracker, + y, + update + ), + (null, not null) => y => + ToAttributeExpression( + source.AttributeExpressionNameTracker, + source.AttributeExpressionValueTracker, + y, + condition + ) + }; + } +} \ No newline at end of file diff --git a/src/Dynatello/Dynatello.csproj b/src/Dynatello/Dynatello.csproj new file mode 100644 index 00000000..2674c456 --- /dev/null +++ b/src/Dynatello/Dynatello.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + enable + false + 0.0.0 + Dynatello + Robert Andersson + Contains functionality to improve the convinience when working the DynamoDB.SourceGenerator. + Robert Anderson + https://github.com/inputfalken/DynamoDB.SourceGenerator + MIT + https://github.com/inputfalken/DynamoDB.SourceGenerator + git + true + true + + + + + + + + diff --git a/tests/Dynatello.Tests/Dynatello.Tests.csproj b/tests/Dynatello.Tests/Dynatello.Tests.csproj new file mode 100644 index 00000000..94f35b92 --- /dev/null +++ b/tests/Dynatello.Tests/Dynatello.Tests.csproj @@ -0,0 +1,31 @@ + + + + net6.0 + enable + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + diff --git a/tests/Dynatello.Tests/ToGetItemRequestTests.cs b/tests/Dynatello.Tests/ToGetItemRequestTests.cs new file mode 100644 index 00000000..2a7e0a83 --- /dev/null +++ b/tests/Dynatello.Tests/ToGetItemRequestTests.cs @@ -0,0 +1,107 @@ +using Amazon.DynamoDBv2.Model; +using AutoFixture; +using DynamoDBGenerator.Exceptions; +using Dynatello.Builders; +using FluentAssertions; + +namespace Dynatello.Tests; + +public class ToGetItemRequestTests +{ + private static readonly GetItemRequestBuilder GetCatByPartitionKey; + private static readonly GetItemRequestBuilder<(Guid partionKey, Guid rangeKey)> GetCatByCompositeKeys; + + static ToGetItemRequestTests() + { + GetCatByPartitionKey = Cat.QueryWithCuteness.OnTable("TABLE").ToGetRequestBuilder(x => x.Id); + GetCatByCompositeKeys = Cat.QueryWithCuteness.OnTable("TABLE").ToGetRequestBuilder(x => x.Id, x => x.HomeId); + } + + [Fact] + public void Build_Request_CompositeKeys_InvalidPartition() + { + var act = () => Cat.QueryWithCuteness + .OnTable("TABLE") + .ToGetRequestBuilder(x => x.Name, x => x.HomeId) + .Build(("", Guid.Empty)); + + act.Should() + .Throw() + .WithMessage("Value '*' from argument '*' is not convertable*"); + } + + [Fact] + public void Build_Request_CompositeKeys_InvalidRange() + { + var act = () => Cat.QueryWithCuteness + .OnTable("TABLE") + .ToGetRequestBuilder(x => x.Id, x => x.Name) + .Build((Guid.Empty, "")); + + act.Should() + .Throw() + .WithMessage("Value '*' from argument '*' is not convertable*"); + } + + [Fact] + public void Build_Request_CompositeKeys_InvalidPartitionAndRange() + { + var act = () => Cat.QueryWithCuteness + .OnTable("TABLE") + .ToGetRequestBuilder(x => x.Cuteness, x => x.Name) + .Build((2.3, "")); + + act.Should() + .Throw() + .WithMessage("Value '*' from argument '*' is not convertable*"); + } + + [Fact] + public void Build_Request_WithInvalidPartitionKey() + { + var act = () => Cat.QueryWithCuteness + .OnTable("TABLE") + .ToGetRequestBuilder(x => x.Name) + .Build("TEST"); + act.Should() + .Throw() + .WithMessage("Value '*' from argument '*' is not convertable*"); + } + + [Fact] + public void Build_Request_PartitionKeyOnly() + { + Cat.Fixture.CreateMany().Should().AllSatisfy(partitionKey => + { + var request = GetCatByPartitionKey.Build(partitionKey); + + request.Key + .Should() + .BeEquivalentTo(new Dictionary() + { { nameof(Cat.Id), new AttributeValue { S = partitionKey.ToString() } } } + ); + + request.TableName.Should().Be("TABLE"); + }); + } + + [Fact] + public void Build_Request_CompositeKeys() + { + Cat.Fixture.CreateMany<(Guid PartitionKey, Guid RangeKey)>().Should().AllSatisfy(keys => + { + var request = GetCatByCompositeKeys.Build(keys); + + request.Key + .Should() + .BeEquivalentTo(new Dictionary() + { + { nameof(Cat.Id), new AttributeValue { S = keys.PartitionKey.ToString() } }, + { nameof(Cat.HomeId), new AttributeValue { S = keys.RangeKey.ToString() } } + } + ); + + request.TableName.Should().Be("TABLE"); + }); + } +} \ No newline at end of file diff --git a/tests/DynamoDBGenerator.SourceGenerator.Tests/Extensions/ToPutItemRequestTests.cs b/tests/Dynatello.Tests/ToPutItemRequestTests.cs similarity index 83% rename from tests/DynamoDBGenerator.SourceGenerator.Tests/Extensions/ToPutItemRequestTests.cs rename to tests/Dynatello.Tests/ToPutItemRequestTests.cs index a7c841d7..e6e4fb0f 100644 --- a/tests/DynamoDBGenerator.SourceGenerator.Tests/Extensions/ToPutItemRequestTests.cs +++ b/tests/Dynatello.Tests/ToPutItemRequestTests.cs @@ -1,8 +1,12 @@ using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.DataModel; using AutoFixture; using DynamoDBGenerator.Attributes; -using DynamoDBGenerator.Extensions; -namespace DynamoDBGenerator.SourceGenerator.Tests.Extensions; +using Dynatello.Builders; +using Dynatello.Builders.Types; +using FluentAssertions; + +namespace Dynatello.Tests; [DynamoDBMarshaller(typeof(User))] public partial class ToPutItemRequestTests @@ -13,7 +17,9 @@ public partial class ToPutItemRequestTests public void Without_ConditionExpression_ShouldNotIncludeExpressionFields() { var user = _fixture.Create(); - var putItemRequest = UserMarshaller.ToPutItemRequest(user, ReturnValue.NONE, "TABLE"); + var putItemRequest = UserMarshaller.OnTable("TABLE") + .ToPutRequestBuilder() + .Build(user); putItemRequest.ConditionExpression.Should().BeNullOrWhiteSpace(); putItemRequest.ExpressionAttributeNames.Should().BeNullOrEmpty(); @@ -28,7 +34,7 @@ public void Without_ConditionExpression_ShouldNotIncludeExpressionFields() x.Key.Should().Be(nameof(user.Metadata.ModifiedAt)); x.Value.S.Should().Be(user.Metadata.ModifiedAt.ToString("O")); }); - putItemRequest.ReturnValues.Should().Be(ReturnValue.NONE); + putItemRequest.ReturnValues.Should().Be(null); putItemRequest.TableName.Should().Be("TABLE"); } @@ -36,7 +42,11 @@ public void Without_ConditionExpression_ShouldNotIncludeExpressionFields() public void With_ConditionExpression_ShouldIncludeExpressionFields() { var user = _fixture.Create(); - var putItemRequest = UserMarshaller.ToPutItemRequest(user, (x, y) => $"{x.Email} <> {y.Email} AND {x.Firstname} = {y.Firstname}", ReturnValue.NONE, "TABLE"); + var putItemRequest = UserMarshaller + .OnTable("TABLE") + .WithConditionExpression((x, y) => $"{x.Email} <> {y.Email} AND {x.Firstname} = {y.Firstname}") + .ToPutRequestBuilder() + .Build(user); putItemRequest.ConditionExpression.Should().Be("#Email <> :p1 AND #Firstname = :p2"); putItemRequest.ExpressionAttributeNames.Should().HaveCount(2); @@ -55,10 +65,9 @@ public void With_ConditionExpression_ShouldIncludeExpressionFields() x.Key.Should().Be(nameof(user.Metadata.ModifiedAt)); x.Value.S.Should().Be(user.Metadata.ModifiedAt.ToString("O")); }); - putItemRequest.ReturnValues.Should().Be(ReturnValue.NONE); + putItemRequest.ReturnValues.Should().Be(null); putItemRequest.TableName.Should().Be("TABLE"); } - } public class User diff --git a/tests/Dynatello.Tests/ToQueryRequestTests.cs b/tests/Dynatello.Tests/ToQueryRequestTests.cs new file mode 100644 index 00000000..98a7d63f --- /dev/null +++ b/tests/Dynatello.Tests/ToQueryRequestTests.cs @@ -0,0 +1,89 @@ +using System.Globalization; +using Amazon.DynamoDBv2.DataModel; +using Amazon.DynamoDBv2.Model; +using AutoFixture; +using AutoFixture.Dsl; +using DynamoDBGenerator.Attributes; +using Dynatello.Builders; +using Dynatello.Builders.Types; +using FluentAssertions; + +namespace Dynatello.Tests; + +public class ToQueryRequestTests +{ + + static ToQueryRequestTests() + { + var withKeyConditionExpression = Cat.QueryWithCuteness + .OnTable("TABLE") + .WithKeyConditionExpression((x, y) => $"{x.Id} = {y.Id}"); + QueryCatWithId = withKeyConditionExpression.ToQueryRequestBuilder(); + + QueryCatWithIdAndMinimumCuteness = withKeyConditionExpression + .WithFilterExpression((x, y) => $"{x.Cuteness} > {y.MinimumCuteness}") + .ToQueryRequestBuilder(); + } + + private static readonly QueryRequestBuilder<(Guid Id, double MinimumCuteness)> QueryCatWithIdAndMinimumCuteness; + private static readonly QueryRequestBuilder<(Guid Id, double MinimumCuteness)> QueryCatWithId; + + [Fact] + public void Build_Request() + { + Cat.Fixture.CreateMany<(Guid Id, double MinimumCuteness)>(10).Should().AllSatisfy(tuple => + { + var request = QueryCatWithId.Build(tuple); + + request.TableName.Should().Be("TABLE"); + request.ExpressionAttributeNames.Should().BeEquivalentTo(new Dictionary + { + { "#Id", nameof(Cat.Id) } + }); + + request.ExpressionAttributeValues.Should().BeEquivalentTo(new Dictionary + { + { ":p1", new AttributeValue { S = tuple.Id.ToString() } }, + }); + + request.KeyConditionExpression.Should().Be("#Id = :p1"); + }); + } + + [Fact] + public void Build_Request_FilterExpression() + { + Cat.Fixture.CreateMany<(Guid Id, double MinimumCuteness)>(10).Should().AllSatisfy(tuple => + { + var request = QueryCatWithIdAndMinimumCuteness.Build(tuple); + + request.TableName.Should().Be("TABLE"); + request.ExpressionAttributeNames.Should().BeEquivalentTo(new Dictionary + { + { "#Id", nameof(Cat.Id) }, + { "#Cuteness", nameof(Cat.Cuteness) } + }); + + request.ExpressionAttributeValues.Should().BeEquivalentTo(new Dictionary + { + { ":p1", new AttributeValue { S = tuple.Id.ToString() } }, + { ":p2", new AttributeValue { N = tuple.MinimumCuteness.ToString(CultureInfo.InvariantCulture) } } + }); + + request.KeyConditionExpression.Should().Be("#Id = :p1"); + request.FilterExpression.Should().Be("#Cuteness > :p2"); + }); + } +} + +[DynamoDBMarshaller(typeof(Cat), PropertyName = "QueryWithCuteness", + ArgumentType = typeof((Guid Id, double MinimumCuteness)))] +public readonly partial record struct Cat( + [property: DynamoDBHashKey] Guid Id, + [property: DynamoDBRangeKey] Guid HomeId, + string Name, + double Cuteness) +{ + public static readonly Fixture Fixture = new Fixture(); + +} \ No newline at end of file diff --git a/tests/DynamoDBGenerator.SourceGenerator.Tests/Extensions/ToUpdateItemRequestTests.cs b/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs similarity index 74% rename from tests/DynamoDBGenerator.SourceGenerator.Tests/Extensions/ToUpdateItemRequestTests.cs rename to tests/Dynatello.Tests/ToUpdateItemRequestTests.cs index 800976fb..5e62d447 100644 --- a/tests/DynamoDBGenerator.SourceGenerator.Tests/Extensions/ToUpdateItemRequestTests.cs +++ b/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs @@ -1,8 +1,11 @@ using Amazon.DynamoDBv2; using AutoFixture; using DynamoDBGenerator.Attributes; -using DynamoDBGenerator.Extensions; -namespace DynamoDBGenerator.SourceGenerator.Tests.Extensions; +using Dynatello.Builders; +using Dynatello.Builders.Types; +using FluentAssertions; + +namespace Dynatello.Tests; [DynamoDBMarshaller(typeof(User))] [DynamoDBMarshaller(typeof(User), PropertyName = "UpdateEmail", ArgumentType = typeof(UpdateUserEmail))] @@ -14,27 +17,26 @@ public partial class ToUpdateItemRequestTests public void ArgumentTypeProvided_WithConditionExpression_ShouldIncludeUpdateAndConditionExpressionFields() { var updateUserEmail = _fixture.Create(); - var updateItemRequest = UpdateEmail.ToUpdateItemRequest( - updateUserEmail, - (x, y) => x.Keys(y.UserId, y.UserEmail), - (x, y) => $"SET {x.Email} = {y.UserEmail}, {x.Metadata.ModifiedAt} = {y.TimeStamp}", - (x,y) => $"{x.Id} = {y.UserId} AND {x.Email} <> {y.UserEmail}", - ReturnValue.NONE, - "TABLE" - ); + var updateItemRequest = UpdateEmail + .OnTable("TABLE") + .WithUpdateExpression((x, y) => $"SET {x.Email} = {y.UserEmail}, {x.Metadata.ModifiedAt} = {y.TimeStamp}") + .WithConditionExpression((x, y) => $"{x.Id} = {y.UserId} AND {x.Email} <> {y.UserEmail}") + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.UserId, y.UserEmail)) + .Build(updateUserEmail); updateItemRequest.ConditionExpression.Should().Be("#Id = :p3 AND #Email <> :p1"); updateItemRequest.ExpressionAttributeNames.Should().HaveCount(3); updateItemRequest.ExpressionAttributeNames["#Email"].Should().Be(nameof(User.Email)); updateItemRequest.ExpressionAttributeNames["#Id"].Should().Be(nameof(User.Id)); - updateItemRequest.ExpressionAttributeNames["#Metadata.#ModifiedAt"].Should().Be(nameof(User.Metadata.ModifiedAt)); + updateItemRequest.ExpressionAttributeNames["#Metadata.#ModifiedAt"].Should() + .Be(nameof(User.Metadata.ModifiedAt)); updateItemRequest.ExpressionAttributeValues.Should().HaveCount(3); updateItemRequest.ExpressionAttributeValues[":p1"].S.Should().Be(updateUserEmail.UserEmail); updateItemRequest.ExpressionAttributeValues[":p2"].S.Should().Be(updateUserEmail.TimeStamp.ToString("O")); updateItemRequest.ExpressionAttributeValues[":p3"].S.Should().Be(updateUserEmail.UserId); updateItemRequest.Key[nameof(User.Email)].S.Should().Be(updateUserEmail.UserEmail); updateItemRequest.Key[nameof(User.Id)].S.Should().Be(updateUserEmail.UserId); - updateItemRequest.ReturnValues.Should().Be(ReturnValue.NONE); + updateItemRequest.ReturnValues.Should().Be(null); updateItemRequest.TableName.Should().Be("TABLE"); updateItemRequest.UpdateExpression.Should().Be("SET #Email = :p1, #Metadata.#ModifiedAt = :p2"); } @@ -43,24 +45,23 @@ public void ArgumentTypeProvided_WithConditionExpression_ShouldIncludeUpdateAndC public void ArgumentTypeProvided_WithoutConditionExpression_ShouldOnlyIncludeUpdateExpressionFields() { var updateUserEmail = _fixture.Create(); - var updateItemRequest = UpdateEmail.ToUpdateItemRequest( - updateUserEmail, - (x, y) => x.Keys(y.UserId, y.UserEmail), - (x, y) => $"SET {x.Email} = {y.UserEmail}, {x.Metadata.ModifiedAt} = {y.TimeStamp}", - ReturnValue.NONE, - "TABLE" - ); + var updateItemRequest = UpdateEmail + .OnTable("TABLE") + .WithUpdateExpression((x, y) => $"SET {x.Email} = {y.UserEmail}, {x.Metadata.ModifiedAt} = {y.TimeStamp}") + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.UserId, y.UserEmail)) + .Build(updateUserEmail); updateItemRequest.ConditionExpression.Should().BeNullOrWhiteSpace(); updateItemRequest.ExpressionAttributeNames.Should().HaveCount(2); updateItemRequest.ExpressionAttributeNames["#Email"].Should().Be(nameof(User.Email)); - updateItemRequest.ExpressionAttributeNames["#Metadata.#ModifiedAt"].Should().Be(nameof(User.Metadata.ModifiedAt)); + updateItemRequest.ExpressionAttributeNames["#Metadata.#ModifiedAt"].Should() + .Be(nameof(User.Metadata.ModifiedAt)); updateItemRequest.ExpressionAttributeValues.Should().HaveCount(2); updateItemRequest.ExpressionAttributeValues[":p1"].S.Should().Be(updateUserEmail.UserEmail); updateItemRequest.ExpressionAttributeValues[":p2"].S.Should().Be(updateUserEmail.TimeStamp.ToString("O")); updateItemRequest.Key[nameof(User.Email)].S.Should().Be(updateUserEmail.UserEmail); updateItemRequest.Key[nameof(User.Id)].S.Should().Be(updateUserEmail.UserId); - updateItemRequest.ReturnValues.Should().Be(ReturnValue.NONE); + updateItemRequest.ReturnValues.Should().Be(null); updateItemRequest.TableName.Should().Be("TABLE"); updateItemRequest.UpdateExpression.Should().Be("SET #Email = :p1, #Metadata.#ModifiedAt = :p2"); } @@ -70,13 +71,10 @@ public void NoArgumentTypeProvided_WithoutConditionExpression_ShouldOnlyIncludeU { var user = _fixture.Create(); var updateItemRequest = UserMarshaller - .ToUpdateItemRequest( - user, - (x, y) => x.Keys(y.Id, y.Lastname), - (x, y) => $"SET {x.Email} = {y.Email}, {x.Firstname} = {y.Firstname}", - ReturnValue.NONE, - "TABLE" - ); + .OnTable("TABLE") + .WithUpdateExpression((x, y) => $"SET {x.Email} = {y.Email}, {x.Firstname} = {y.Firstname}") + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.Id, y.Lastname)) + .Build(user); updateItemRequest.ConditionExpression.Should().BeNullOrWhiteSpace(); updateItemRequest.ExpressionAttributeNames.Should().HaveCount(2); @@ -87,7 +85,7 @@ public void NoArgumentTypeProvided_WithoutConditionExpression_ShouldOnlyIncludeU updateItemRequest.ExpressionAttributeValues[":p2"].S.Should().Be(user.Firstname); updateItemRequest.Key[nameof(user.Id)].S.Should().Be(user.Id); updateItemRequest.Key[nameof(user.Email)].S.Should().Be(user.Lastname); - updateItemRequest.ReturnValues.Should().Be(ReturnValue.NONE); + updateItemRequest.ReturnValues.Should().Be(null); updateItemRequest.TableName.Should().Be("TABLE"); updateItemRequest.UpdateExpression.Should().Be("SET #Email = :p1, #Firstname = :p2"); } @@ -97,14 +95,11 @@ public void NoArgumentTypeProvided_WithConditionExpression_ShouldIncludeUpdateAn { var user = _fixture.Create(); var updateItemRequest = UserMarshaller - .ToUpdateItemRequest( - user, - (x, y) => x.Keys(y.Id, y.Lastname), - (x, y) => $"SET {x.Email} = {y.Email}, {x.Firstname} = {y.Firstname}", - (x, y) => $"{x.Id} = {y.Id}", - ReturnValue.NONE, - "TABLE" - ); + .OnTable("TABLE") + .WithUpdateExpression((x, y) => $"SET {x.Email} = {y.Email}, {x.Firstname} = {y.Firstname}") + .WithConditionExpression((x, y) => $"{x.Id} = {y.Id}") + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.Id, y.Lastname)) + .Build(user); updateItemRequest.ConditionExpression.Should().Be("#Id = :p3"); updateItemRequest.ExpressionAttributeNames.Should().HaveCount(3); @@ -117,7 +112,7 @@ public void NoArgumentTypeProvided_WithConditionExpression_ShouldIncludeUpdateAn updateItemRequest.ExpressionAttributeValues[":p3"].S.Should().Be(user.Id); updateItemRequest.Key[nameof(user.Id)].S.Should().Be(user.Id); updateItemRequest.Key[nameof(user.Email)].S.Should().Be(user.Lastname); - updateItemRequest.ReturnValues.Should().Be(ReturnValue.NONE); + updateItemRequest.ReturnValues.Should().Be(null); updateItemRequest.TableName.Should().Be("TABLE"); updateItemRequest.UpdateExpression.Should().Be("SET #Email = :p1, #Firstname = :p2"); } diff --git a/tests/Dynatello.Tests/Usings.cs b/tests/Dynatello.Tests/Usings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/tests/Dynatello.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file From a0b53393c12f5a5a20a58eee18246bd6eab1208b Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Sun, 3 Mar 2024 15:53:24 +0100 Subject: [PATCH 02/16] Use TArg for GetRequest --- src/Dynatello/Builders/Extensions.cs | 27 ++++++++++--------- .../Builders/GetItemRequestBuilder.cs | 15 +++++------ src/Dynatello/DynamoDBMarshallerExtensions.cs | 17 ++++++++++++ .../Dynatello.Tests/ToGetItemRequestTests.cs | 21 ++++++++------- tests/Dynatello.Tests/ToQueryRequestTests.cs | 9 +++++-- 5 files changed, 57 insertions(+), 32 deletions(-) diff --git a/src/Dynatello/Builders/Extensions.cs b/src/Dynatello/Builders/Extensions.cs index 25f046fe..8bcf749a 100644 --- a/src/Dynatello/Builders/Extensions.cs +++ b/src/Dynatello/Builders/Extensions.cs @@ -30,34 +30,37 @@ this KeyConditionExpression source ); } - public static GetItemRequestBuilder ToGetRequestBuilder( + public static GetItemRequestBuilder ToGetRequestBuilder( this TableAccess source, - // ReSharper disable once UnusedParameter.Global is used to determined typing - Func partitionKeySelector) + Func partitionKeySelector) where TReferences : IAttributeExpressionNameTracker where TArgumentReferences : IAttributeExpressionValueTracker where TPartition : notnull { - return new GetItemRequestBuilder(source.TableName, source.Item.PrimaryKeyMarshaller, - (x, y) => x.PartitionKey(y)); + return new GetItemRequestBuilder( + source.TableName, + source.Item.PrimaryKeyMarshaller.ComposeKeys(y => partitionKeySelector(y), null) + ); } - public static GetItemRequestBuilder<(TPartition partionKey, TRange rangeKey)> ToGetRequestBuilder ToGetRequestBuilder( this TableAccess source, - // ReSharper disable once UnusedParameter.Global is used to determined typing - Func partitionKeySelector, - // ReSharper disable once UnusedParameter.Global is used to determined typing - Func rangeKeySelector) + Func partitionKeySelector, + Func rangeKeySelector) where TReferences : IAttributeExpressionNameTracker where TArgumentReferences : IAttributeExpressionValueTracker where TPartition : notnull where TRange : notnull { - return new GetItemRequestBuilder<(TPartition partionKey, TRange rangeKey)>(source.TableName, - source.Item.PrimaryKeyMarshaller, (x, y) => x.Keys(y.partionKey, y.rangeKey)); + return new GetItemRequestBuilder( + source.TableName, + source.Item.PrimaryKeyMarshaller.ComposeKeys(y => partitionKeySelector(y), y => rangeKeySelector(y)) + ); } + public static UpdateRequestBuilder ToUpdateItemRequestBuilder( this UpdateExpression source, Func> keySelector diff --git a/src/Dynatello/Builders/GetItemRequestBuilder.cs b/src/Dynatello/Builders/GetItemRequestBuilder.cs index e6508fa7..f29b0b77 100644 --- a/src/Dynatello/Builders/GetItemRequestBuilder.cs +++ b/src/Dynatello/Builders/GetItemRequestBuilder.cs @@ -4,10 +4,9 @@ namespace Dynatello.Builders; -public readonly record struct GetItemRequestBuilder where TKey : notnull +public readonly record struct GetItemRequestBuilder { - private readonly IDynamoDBKeyMarshaller _keyMarshaller; - private readonly Func> _keysSelector; + private readonly Func< T, Dictionary> _keysSelector; /// public string TableName { get; init; } @@ -19,21 +18,21 @@ namespace Dynatello.Builders; /// public ReturnConsumedCapacity? ReturnConsumedCapacity { get; init; } = null; - internal GetItemRequestBuilder(string tableName, IDynamoDBKeyMarshaller keyMarshaller, - Func> keysSelector) + internal GetItemRequestBuilder( + string tableName, + Func> keysSelector) { - _keyMarshaller = keyMarshaller; _keysSelector = keysSelector; TableName = tableName; } - public GetItemRequest Build(TKey key) + public GetItemRequest Build(T arg) { var request = new GetItemRequest { ReturnConsumedCapacity = ReturnConsumedCapacity, TableName = TableName, - Key = _keysSelector(_keyMarshaller, key) + Key = _keysSelector(arg) }; if (ConsistentRead is { } consistentRead) diff --git a/src/Dynatello/DynamoDBMarshallerExtensions.cs b/src/Dynatello/DynamoDBMarshallerExtensions.cs index 346f5583..e8c89343 100644 --- a/src/Dynatello/DynamoDBMarshallerExtensions.cs +++ b/src/Dynatello/DynamoDBMarshallerExtensions.cs @@ -1,3 +1,4 @@ +using Amazon.DynamoDBv2.Model; using DynamoDBGenerator; using Dynatello.Builders.Types; using static DynamoDBGenerator.Extensions.DynamoDBMarshallerExtensions; @@ -15,6 +16,22 @@ public static TableAccess OnTable return new TableAccess(in tableName, in item); } + internal static Func> ComposeKeys + ( + this IDynamoDBKeyMarshaller source, + Func partitionKeySelector, + Func? rangeKeySelector + ) + { + return (partitionKeySelector, rangeKeySelector) switch + { + (not null, not null) => y => source.Keys(partitionKeySelector(y), rangeKeySelector(y)), + (not null, null) => y => source.PartitionKey(partitionKeySelector(y)), + (null, not null) => y => source.RangeKey(rangeKeySelector(y)), + (null, null) => throw new ArgumentNullException("") + }; + } + internal static Func ComposeAttributeExpression( this IDynamoDBMarshaller source, diff --git a/tests/Dynatello.Tests/ToGetItemRequestTests.cs b/tests/Dynatello.Tests/ToGetItemRequestTests.cs index 2a7e0a83..eae1ae27 100644 --- a/tests/Dynatello.Tests/ToGetItemRequestTests.cs +++ b/tests/Dynatello.Tests/ToGetItemRequestTests.cs @@ -13,16 +13,16 @@ public class ToGetItemRequestTests static ToGetItemRequestTests() { - GetCatByPartitionKey = Cat.QueryWithCuteness.OnTable("TABLE").ToGetRequestBuilder(x => x.Id); - GetCatByCompositeKeys = Cat.QueryWithCuteness.OnTable("TABLE").ToGetRequestBuilder(x => x.Id, x => x.HomeId); + GetCatByPartitionKey = Cat.GetById.OnTable("TABLE").ToGetRequestBuilder(x => x); + GetCatByCompositeKeys = Cat.GetByCompositeKey.OnTable("TABLE").ToGetRequestBuilder(x => x.Id, x => x.HomeId); } [Fact] public void Build_Request_CompositeKeys_InvalidPartition() { - var act = () => Cat.QueryWithCuteness + var act = () => Cat.GetByCompositeInvalidPartition .OnTable("TABLE") - .ToGetRequestBuilder(x => x.Name, x => x.HomeId) + .ToGetRequestBuilder(x => x.Item1, x => x.Item2) .Build(("", Guid.Empty)); act.Should() @@ -33,9 +33,9 @@ public void Build_Request_CompositeKeys_InvalidPartition() [Fact] public void Build_Request_CompositeKeys_InvalidRange() { - var act = () => Cat.QueryWithCuteness + var act = () => Cat.GetByCompositeInvalidRange .OnTable("TABLE") - .ToGetRequestBuilder(x => x.Id, x => x.Name) + .ToGetRequestBuilder(x => x.Item1, x => x.Item2) .Build((Guid.Empty, "")); act.Should() @@ -46,9 +46,9 @@ public void Build_Request_CompositeKeys_InvalidRange() [Fact] public void Build_Request_CompositeKeys_InvalidPartitionAndRange() { - var act = () => Cat.QueryWithCuteness + var act = () => Cat.GetByCompositeInvalidPartitionAndRange .OnTable("TABLE") - .ToGetRequestBuilder(x => x.Cuteness, x => x.Name) + .ToGetRequestBuilder(x => x.Item1, x => x.Item2) .Build((2.3, "")); act.Should() @@ -59,10 +59,11 @@ public void Build_Request_CompositeKeys_InvalidPartitionAndRange() [Fact] public void Build_Request_WithInvalidPartitionKey() { - var act = () => Cat.QueryWithCuteness + var act = () => Cat.GetByInvalidPartition .OnTable("TABLE") - .ToGetRequestBuilder(x => x.Name) + .ToGetRequestBuilder(x => x) .Build("TEST"); + act.Should() .Throw() .WithMessage("Value '*' from argument '*' is not convertable*"); diff --git a/tests/Dynatello.Tests/ToQueryRequestTests.cs b/tests/Dynatello.Tests/ToQueryRequestTests.cs index 98a7d63f..5965261d 100644 --- a/tests/Dynatello.Tests/ToQueryRequestTests.cs +++ b/tests/Dynatello.Tests/ToQueryRequestTests.cs @@ -76,8 +76,13 @@ public void Build_Request_FilterExpression() } } -[DynamoDBMarshaller(typeof(Cat), PropertyName = "QueryWithCuteness", - ArgumentType = typeof((Guid Id, double MinimumCuteness)))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "QueryWithCuteness", ArgumentType = typeof((Guid Id, double MinimumCuteness)))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeKey", ArgumentType = typeof((Guid Id, Guid HomeId)))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetById", ArgumentType = typeof(Guid))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByInvalidPartition", ArgumentType = typeof(string))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidPartition", ArgumentType = typeof((string, Guid)))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidRange", ArgumentType = typeof((Guid, string)))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidPartitionAndRange", ArgumentType = typeof((double, string)))] public readonly partial record struct Cat( [property: DynamoDBHashKey] Guid Id, [property: DynamoDBRangeKey] Guid HomeId, From d6eecf518118e6615fa3085406384a9ecc47855b Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Mon, 4 Mar 2024 23:52:21 +0100 Subject: [PATCH 03/16] Update to new version --- src/Dynatello/Dynatello.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Dynatello/Dynatello.csproj b/src/Dynatello/Dynatello.csproj index 2674c456..cc917ad9 100644 --- a/src/Dynatello/Dynatello.csproj +++ b/src/Dynatello/Dynatello.csproj @@ -19,8 +19,8 @@ - - + + From 2f18fda945718dd58b1292eebd2a6a4aa7aee869 Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Mon, 4 Mar 2024 23:55:49 +0100 Subject: [PATCH 04/16] WIP --- tests/Dynatello.Tests/ToGetItemRequestTests.cs | 6 +++--- tests/Dynatello.Tests/ToQueryRequestTests.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Dynatello.Tests/ToGetItemRequestTests.cs b/tests/Dynatello.Tests/ToGetItemRequestTests.cs index eae1ae27..d052e2a3 100644 --- a/tests/Dynatello.Tests/ToGetItemRequestTests.cs +++ b/tests/Dynatello.Tests/ToGetItemRequestTests.cs @@ -22,7 +22,7 @@ public void Build_Request_CompositeKeys_InvalidPartition() { var act = () => Cat.GetByCompositeInvalidPartition .OnTable("TABLE") - .ToGetRequestBuilder(x => x.Item1, x => x.Item2) + .ToGetRequestBuilder(x => x.Id, x => x.HomeId) .Build(("", Guid.Empty)); act.Should() @@ -35,7 +35,7 @@ public void Build_Request_CompositeKeys_InvalidRange() { var act = () => Cat.GetByCompositeInvalidRange .OnTable("TABLE") - .ToGetRequestBuilder(x => x.Item1, x => x.Item2) + .ToGetRequestBuilder(x => x.Id, x => x.HomeId) .Build((Guid.Empty, "")); act.Should() @@ -48,7 +48,7 @@ public void Build_Request_CompositeKeys_InvalidPartitionAndRange() { var act = () => Cat.GetByCompositeInvalidPartitionAndRange .OnTable("TABLE") - .ToGetRequestBuilder(x => x.Item1, x => x.Item2) + .ToGetRequestBuilder(x => x.Id, x => x.HomeId) .Build((2.3, "")); act.Should() diff --git a/tests/Dynatello.Tests/ToQueryRequestTests.cs b/tests/Dynatello.Tests/ToQueryRequestTests.cs index 5965261d..f4a6a2c3 100644 --- a/tests/Dynatello.Tests/ToQueryRequestTests.cs +++ b/tests/Dynatello.Tests/ToQueryRequestTests.cs @@ -80,9 +80,9 @@ public void Build_Request_FilterExpression() [DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeKey", ArgumentType = typeof((Guid Id, Guid HomeId)))] [DynamoDBMarshaller(typeof(Cat), PropertyName = "GetById", ArgumentType = typeof(Guid))] [DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByInvalidPartition", ArgumentType = typeof(string))] -[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidPartition", ArgumentType = typeof((string, Guid)))] -[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidRange", ArgumentType = typeof((Guid, string)))] -[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidPartitionAndRange", ArgumentType = typeof((double, string)))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidPartition", ArgumentType = typeof((string Id, Guid HomeId)))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidRange", ArgumentType = typeof((Guid Id, string HomeId)))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidPartitionAndRange", ArgumentType = typeof((double Id, string HomeId)))] public readonly partial record struct Cat( [property: DynamoDBHashKey] Guid Id, [property: DynamoDBRangeKey] Guid HomeId, From 95e7daf42389a9bb41bb14f8773d1f47e4b54252 Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Tue, 5 Mar 2024 00:04:30 +0100 Subject: [PATCH 05/16] Reformat code --- src/Dynatello/Builders/Extensions.cs | 8 ++-- ...RequestBuilder.cs => GetRequestBuilder.cs} | 32 +++++++------ src/Dynatello/Builders/PutRequestBuilder.cs | 16 +++++-- src/Dynatello/Builders/QueryRequestBuilder.cs | 46 +++++++++++-------- .../Builders/Types/KeyConditionExpression.cs | 28 +---------- .../Types/KeyConditionedFilterExpression.cs | 29 ++++++++++++ .../Builders/UpdateRequestBuilder.cs | 2 +- .../Dynatello.Tests/ToGetItemRequestTests.cs | 26 +++++------ .../Dynatello.Tests/ToPutItemRequestTests.cs | 1 - tests/Dynatello.Tests/ToQueryRequestTests.cs | 25 +++++----- .../ToUpdateItemRequestTests.cs | 1 - 11 files changed, 115 insertions(+), 99 deletions(-) rename src/Dynatello/Builders/{GetItemRequestBuilder.cs => GetRequestBuilder.cs} (68%) create mode 100644 src/Dynatello/Builders/Types/KeyConditionedFilterExpression.cs diff --git a/src/Dynatello/Builders/Extensions.cs b/src/Dynatello/Builders/Extensions.cs index 8bcf749a..aab1a500 100644 --- a/src/Dynatello/Builders/Extensions.cs +++ b/src/Dynatello/Builders/Extensions.cs @@ -30,7 +30,7 @@ this KeyConditionExpression source ); } - public static GetItemRequestBuilder ToGetRequestBuilder ToGetRequestBuilder( this TableAccess source, Func partitionKeySelector) @@ -38,13 +38,13 @@ this KeyConditionExpression source where TArgumentReferences : IAttributeExpressionValueTracker where TPartition : notnull { - return new GetItemRequestBuilder( + return new GetRequestBuilder( source.TableName, source.Item.PrimaryKeyMarshaller.ComposeKeys(y => partitionKeySelector(y), null) ); } - public static GetItemRequestBuilder ToGetRequestBuilder ToGetRequestBuilder( this TableAccess source, Func partitionKeySelector, @@ -54,7 +54,7 @@ this KeyConditionExpression source where TPartition : notnull where TRange : notnull { - return new GetItemRequestBuilder( + return new GetRequestBuilder( source.TableName, source.Item.PrimaryKeyMarshaller.ComposeKeys(y => partitionKeySelector(y), y => rangeKeySelector(y)) ); diff --git a/src/Dynatello/Builders/GetItemRequestBuilder.cs b/src/Dynatello/Builders/GetRequestBuilder.cs similarity index 68% rename from src/Dynatello/Builders/GetItemRequestBuilder.cs rename to src/Dynatello/Builders/GetRequestBuilder.cs index f29b0b77..c3df2624 100644 --- a/src/Dynatello/Builders/GetItemRequestBuilder.cs +++ b/src/Dynatello/Builders/GetRequestBuilder.cs @@ -1,24 +1,13 @@ using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; -using DynamoDBGenerator; namespace Dynatello.Builders; -public readonly record struct GetItemRequestBuilder +public readonly record struct GetRequestBuilder { - private readonly Func< T, Dictionary> _keysSelector; + private readonly Func> _keysSelector; - /// - public string TableName { get; init; } - - - /// - public bool? ConsistentRead { get; init; } = null; - - /// - public ReturnConsumedCapacity? ReturnConsumedCapacity { get; init; } = null; - - internal GetItemRequestBuilder( + internal GetRequestBuilder( string tableName, Func> keysSelector) { @@ -26,6 +15,21 @@ internal GetItemRequestBuilder( TableName = tableName; } + [Obsolete("Do not used this constructor!", true)] + public GetRequestBuilder() + { + throw new InvalidOperationException("This is an invalid constructor access."); + } + + /// + public string TableName { get; init; } + + /// + public bool? ConsistentRead { get; init; } = null; + + /// + public ReturnConsumedCapacity? ReturnConsumedCapacity { get; init; } = null; + public GetItemRequest Build(T arg) { var request = new GetItemRequest diff --git a/src/Dynatello/Builders/PutRequestBuilder.cs b/src/Dynatello/Builders/PutRequestBuilder.cs index 711ba01d..055e02dd 100644 --- a/src/Dynatello/Builders/PutRequestBuilder.cs +++ b/src/Dynatello/Builders/PutRequestBuilder.cs @@ -26,7 +26,13 @@ string tableName _tableName = tableName; } - /// + [Obsolete("Do not used this constructor!", true)] + public PutRequestBuilder() + { + throw new InvalidOperationException("This is an invalid constructor access."); + } + + /// public string TableName { get => _tableName; @@ -59,13 +65,13 @@ public PutItemRequest Build(T element) ExpressionAttributeValues = null }; - if (ReturnValues is not null) + if (ReturnValues is not null) request.ReturnValues = ReturnValues; - if (ReturnConsumedCapacity is not null) + if (ReturnConsumedCapacity is not null) request.ReturnConsumedCapacity = ReturnConsumedCapacity; - if (ReturnItemCollectionMetrics is not null) + if (ReturnItemCollectionMetrics is not null) request.ReturnItemCollectionMetrics = ReturnItemCollectionMetrics; - if (ReturnValuesOnConditionCheckFailure is not null) + if (ReturnValuesOnConditionCheckFailure is not null) request.ReturnValuesOnConditionCheckFailure = ReturnValuesOnConditionCheckFailure; if (_attributeExpressionSelector is null) return request; diff --git a/src/Dynatello/Builders/QueryRequestBuilder.cs b/src/Dynatello/Builders/QueryRequestBuilder.cs index e4ce550c..4d55ff88 100644 --- a/src/Dynatello/Builders/QueryRequestBuilder.cs +++ b/src/Dynatello/Builders/QueryRequestBuilder.cs @@ -8,33 +8,39 @@ public readonly record struct QueryRequestBuilder { private readonly Func _attributeExpressionSelector; - /// + internal QueryRequestBuilder(Func attributeExpressionSelector, string tableName) + { + _attributeExpressionSelector = attributeExpressionSelector; + TableName = tableName; + } + + [Obsolete("Do not used this constructor!", true)] + public QueryRequestBuilder() + { + throw new InvalidOperationException("This is an invalid constructor access."); + } + + /// public string TableName { get; init; } - /// + /// public string? IndexName { get; init; } = null; - /// + /// public int? Limit { get; init; } = null; - /// + /// public bool? ConsistentRead { get; init; } = null; - /// + /// public bool? ScanIndexForward { get; init; } = null; - /// + /// public Select? Select { get; init; } = null; - /// + /// public ReturnConsumedCapacity? ReturnConsumedCapacity { get; init; } = null; - public QueryRequestBuilder(Func attributeExpressionSelector, string tableName) - { - _attributeExpressionSelector = attributeExpressionSelector; - TableName = tableName; - } - public QueryRequest Build(T arg) { var attributeExpression = _attributeExpressionSelector(arg); @@ -54,22 +60,22 @@ public QueryRequest Build(T arg) }; - if (ReturnConsumedCapacity is not null) + if (ReturnConsumedCapacity is not null) queryRequest.ReturnConsumedCapacity = ReturnConsumedCapacity; - - if (ConsistentRead is {} consistentRead) + + if (ConsistentRead is { } consistentRead) queryRequest.ConsistentRead = consistentRead; - if (ScanIndexForward is {} scanIndexForward) + if (ScanIndexForward is { } scanIndexForward) queryRequest.ScanIndexForward = scanIndexForward; - if (Select is not null) + if (Select is not null) queryRequest.Select = Select; - + if (Limit is { } limit) queryRequest.Limit = limit; - if (IndexName is not null) + if (IndexName is not null) queryRequest.IndexName = IndexName; if (attributeExpression.Expressions.Count == 2) diff --git a/src/Dynatello/Builders/Types/KeyConditionExpression.cs b/src/Dynatello/Builders/Types/KeyConditionExpression.cs index c0322d31..cd88f79e 100644 --- a/src/Dynatello/Builders/Types/KeyConditionExpression.cs +++ b/src/Dynatello/Builders/Types/KeyConditionExpression.cs @@ -2,32 +2,6 @@ namespace Dynatello.Builders.Types; -public readonly struct KeyConditionedFilterExpression - where TReferences : IAttributeExpressionNameTracker - where TArgumentReferences : IAttributeExpressionValueTracker -{ - internal readonly Func Condition; - internal readonly Func Filter; - internal readonly TableAccess TableAccess; - - [Obsolete("Do not use this constructor!", true)] - public KeyConditionedFilterExpression() - { - throw new InvalidOperationException("This is an invalid constructor access."); - } - - internal KeyConditionedFilterExpression( - in TableAccess tableAccess, - in Func condition, - in Func filter - ) - { - TableAccess = tableAccess; - Condition = condition; - Filter = filter; - } -} - public readonly struct KeyConditionExpression where TReferences : IAttributeExpressionNameTracker where TArgumentReferences : IAttributeExpressionValueTracker @@ -49,4 +23,4 @@ in Func condition TableAccess = tableAccess; Condition = condition; } -} +} \ No newline at end of file diff --git a/src/Dynatello/Builders/Types/KeyConditionedFilterExpression.cs b/src/Dynatello/Builders/Types/KeyConditionedFilterExpression.cs new file mode 100644 index 00000000..904faf1f --- /dev/null +++ b/src/Dynatello/Builders/Types/KeyConditionedFilterExpression.cs @@ -0,0 +1,29 @@ +using DynamoDBGenerator; + +namespace Dynatello.Builders.Types; + +public readonly struct KeyConditionedFilterExpression + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker +{ + internal readonly Func Condition; + internal readonly Func Filter; + internal readonly TableAccess TableAccess; + + [Obsolete("Do not use this constructor!", true)] + public KeyConditionedFilterExpression() + { + throw new InvalidOperationException("This is an invalid constructor access."); + } + + internal KeyConditionedFilterExpression( + in TableAccess tableAccess, + in Func condition, + in Func filter + ) + { + TableAccess = tableAccess; + Condition = condition; + Filter = filter; + } +} \ No newline at end of file diff --git a/src/Dynatello/Builders/UpdateRequestBuilder.cs b/src/Dynatello/Builders/UpdateRequestBuilder.cs index a18af842..aba20758 100644 --- a/src/Dynatello/Builders/UpdateRequestBuilder.cs +++ b/src/Dynatello/Builders/UpdateRequestBuilder.cs @@ -34,7 +34,7 @@ IDynamoDBKeyMarshaller keyMarshaller _keyMarshaller = keyMarshaller; } - /// + /// public string TableName { get => _tableName; diff --git a/tests/Dynatello.Tests/ToGetItemRequestTests.cs b/tests/Dynatello.Tests/ToGetItemRequestTests.cs index d052e2a3..63b28c78 100644 --- a/tests/Dynatello.Tests/ToGetItemRequestTests.cs +++ b/tests/Dynatello.Tests/ToGetItemRequestTests.cs @@ -8,15 +8,6 @@ namespace Dynatello.Tests; public class ToGetItemRequestTests { - private static readonly GetItemRequestBuilder GetCatByPartitionKey; - private static readonly GetItemRequestBuilder<(Guid partionKey, Guid rangeKey)> GetCatByCompositeKeys; - - static ToGetItemRequestTests() - { - GetCatByPartitionKey = Cat.GetById.OnTable("TABLE").ToGetRequestBuilder(x => x); - GetCatByCompositeKeys = Cat.GetByCompositeKey.OnTable("TABLE").ToGetRequestBuilder(x => x.Id, x => x.HomeId); - } - [Fact] public void Build_Request_CompositeKeys_InvalidPartition() { @@ -63,7 +54,7 @@ public void Build_Request_WithInvalidPartitionKey() .OnTable("TABLE") .ToGetRequestBuilder(x => x) .Build("TEST"); - + act.Should() .Throw() .WithMessage("Value '*' from argument '*' is not convertable*"); @@ -72,13 +63,16 @@ public void Build_Request_WithInvalidPartitionKey() [Fact] public void Build_Request_PartitionKeyOnly() { + var getCatByPartitionKey = Cat.GetById + .OnTable("TABLE") + .ToGetRequestBuilder(x => x); Cat.Fixture.CreateMany().Should().AllSatisfy(partitionKey => { - var request = GetCatByPartitionKey.Build(partitionKey); + var request = getCatByPartitionKey.Build(partitionKey); request.Key .Should() - .BeEquivalentTo(new Dictionary() + .BeEquivalentTo(new Dictionary { { nameof(Cat.Id), new AttributeValue { S = partitionKey.ToString() } } } ); @@ -89,13 +83,17 @@ public void Build_Request_PartitionKeyOnly() [Fact] public void Build_Request_CompositeKeys() { + var getCatByCompositeKeys = Cat.GetByCompositeKey + .OnTable("TABLE") + .ToGetRequestBuilder(x => x.Id, x => x.HomeId); + Cat.Fixture.CreateMany<(Guid PartitionKey, Guid RangeKey)>().Should().AllSatisfy(keys => { - var request = GetCatByCompositeKeys.Build(keys); + var request = getCatByCompositeKeys.Build(keys); request.Key .Should() - .BeEquivalentTo(new Dictionary() + .BeEquivalentTo(new Dictionary { { nameof(Cat.Id), new AttributeValue { S = keys.PartitionKey.ToString() } }, { nameof(Cat.HomeId), new AttributeValue { S = keys.RangeKey.ToString() } } diff --git a/tests/Dynatello.Tests/ToPutItemRequestTests.cs b/tests/Dynatello.Tests/ToPutItemRequestTests.cs index e6e4fb0f..1866f199 100644 --- a/tests/Dynatello.Tests/ToPutItemRequestTests.cs +++ b/tests/Dynatello.Tests/ToPutItemRequestTests.cs @@ -1,4 +1,3 @@ -using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.DataModel; using AutoFixture; using DynamoDBGenerator.Attributes; diff --git a/tests/Dynatello.Tests/ToQueryRequestTests.cs b/tests/Dynatello.Tests/ToQueryRequestTests.cs index f4a6a2c3..1b2454c0 100644 --- a/tests/Dynatello.Tests/ToQueryRequestTests.cs +++ b/tests/Dynatello.Tests/ToQueryRequestTests.cs @@ -2,7 +2,6 @@ using Amazon.DynamoDBv2.DataModel; using Amazon.DynamoDBv2.Model; using AutoFixture; -using AutoFixture.Dsl; using DynamoDBGenerator.Attributes; using Dynatello.Builders; using Dynatello.Builders.Types; @@ -12,6 +11,8 @@ namespace Dynatello.Tests; public class ToQueryRequestTests { + private static readonly QueryRequestBuilder<(Guid Id, double MinimumCuteness)> QueryCatWithIdAndMinimumCuteness; + private static readonly QueryRequestBuilder<(Guid Id, double MinimumCuteness)> QueryCatWithId; static ToQueryRequestTests() { @@ -19,15 +20,12 @@ static ToQueryRequestTests() .OnTable("TABLE") .WithKeyConditionExpression((x, y) => $"{x.Id} = {y.Id}"); QueryCatWithId = withKeyConditionExpression.ToQueryRequestBuilder(); - + QueryCatWithIdAndMinimumCuteness = withKeyConditionExpression .WithFilterExpression((x, y) => $"{x.Cuteness} > {y.MinimumCuteness}") .ToQueryRequestBuilder(); } - private static readonly QueryRequestBuilder<(Guid Id, double MinimumCuteness)> QueryCatWithIdAndMinimumCuteness; - private static readonly QueryRequestBuilder<(Guid Id, double MinimumCuteness)> QueryCatWithId; - [Fact] public void Build_Request() { @@ -43,7 +41,7 @@ public void Build_Request() request.ExpressionAttributeValues.Should().BeEquivalentTo(new Dictionary { - { ":p1", new AttributeValue { S = tuple.Id.ToString() } }, + { ":p1", new AttributeValue { S = tuple.Id.ToString() } } }); request.KeyConditionExpression.Should().Be("#Id = :p1"); @@ -76,19 +74,22 @@ public void Build_Request_FilterExpression() } } -[DynamoDBMarshaller(typeof(Cat), PropertyName = "QueryWithCuteness", ArgumentType = typeof((Guid Id, double MinimumCuteness)))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "QueryWithCuteness", + ArgumentType = typeof((Guid Id, double MinimumCuteness)))] [DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeKey", ArgumentType = typeof((Guid Id, Guid HomeId)))] [DynamoDBMarshaller(typeof(Cat), PropertyName = "GetById", ArgumentType = typeof(Guid))] [DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByInvalidPartition", ArgumentType = typeof(string))] -[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidPartition", ArgumentType = typeof((string Id, Guid HomeId)))] -[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidRange", ArgumentType = typeof((Guid Id, string HomeId)))] -[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidPartitionAndRange", ArgumentType = typeof((double Id, string HomeId)))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidPartition", + ArgumentType = typeof((string Id, Guid HomeId)))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidRange", + ArgumentType = typeof((Guid Id, string HomeId)))] +[DynamoDBMarshaller(typeof(Cat), PropertyName = "GetByCompositeInvalidPartitionAndRange", + ArgumentType = typeof((double Id, string HomeId)))] public readonly partial record struct Cat( [property: DynamoDBHashKey] Guid Id, [property: DynamoDBRangeKey] Guid HomeId, string Name, double Cuteness) { - public static readonly Fixture Fixture = new Fixture(); - + public static readonly Fixture Fixture = new(); } \ No newline at end of file diff --git a/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs b/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs index 5e62d447..1fd1b9ca 100644 --- a/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs +++ b/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs @@ -1,4 +1,3 @@ -using Amazon.DynamoDBv2; using AutoFixture; using DynamoDBGenerator.Attributes; using Dynatello.Builders; From 50f57c44565cd08d32bcdbd0b3874be293e437bc Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Tue, 5 Mar 2024 00:28:43 +0100 Subject: [PATCH 06/16] Improve unit test for successful request --- .../Dynatello.Tests/ToGetItemRequestTests.cs | 59 ++++++++++++------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/tests/Dynatello.Tests/ToGetItemRequestTests.cs b/tests/Dynatello.Tests/ToGetItemRequestTests.cs index 63b28c78..459e4af3 100644 --- a/tests/Dynatello.Tests/ToGetItemRequestTests.cs +++ b/tests/Dynatello.Tests/ToGetItemRequestTests.cs @@ -66,18 +66,26 @@ public void Build_Request_PartitionKeyOnly() var getCatByPartitionKey = Cat.GetById .OnTable("TABLE") .ToGetRequestBuilder(x => x); - Cat.Fixture.CreateMany().Should().AllSatisfy(partitionKey => - { - var request = getCatByPartitionKey.Build(partitionKey); - request.Key + Cat.Fixture + .CreateMany() + .Should() + .AllSatisfy(x => getCatByPartitionKey + .Build(x) .Should() - .BeEquivalentTo(new Dictionary - { { nameof(Cat.Id), new AttributeValue { S = partitionKey.ToString() } } } - ); - - request.TableName.Should().Be("TABLE"); - }); + .BeEquivalentTo(new GetItemRequest + { + Key = new Dictionary + { + { nameof(Cat.Id), new AttributeValue { S = x.ToString() } } + }, + TableName = "TABLE", + ConsistentRead = false, + ExpressionAttributeNames = new Dictionary(), + ProjectionExpression = null, + ReturnConsumedCapacity = null, + AttributesToGet = new List() + })); } [Fact] @@ -87,20 +95,27 @@ public void Build_Request_CompositeKeys() .OnTable("TABLE") .ToGetRequestBuilder(x => x.Id, x => x.HomeId); - Cat.Fixture.CreateMany<(Guid PartitionKey, Guid RangeKey)>().Should().AllSatisfy(keys => - { - var request = getCatByCompositeKeys.Build(keys); - - request.Key + Cat.Fixture + .CreateMany<(Guid PartitionKey, Guid RangeKey)>() + .Should() + .AllSatisfy(x => getCatByCompositeKeys + .Build(x) .Should() - .BeEquivalentTo(new Dictionary + .BeEquivalentTo(new GetItemRequest { - { nameof(Cat.Id), new AttributeValue { S = keys.PartitionKey.ToString() } }, - { nameof(Cat.HomeId), new AttributeValue { S = keys.RangeKey.ToString() } } + Key = new Dictionary + { + { nameof(Cat.Id), new AttributeValue { S = x.PartitionKey.ToString() } }, + { nameof(Cat.HomeId), new AttributeValue { S = x.RangeKey.ToString() } } + }, + TableName = "TABLE", + ConsistentRead = false, + ExpressionAttributeNames = new Dictionary(), + ProjectionExpression = null, + ReturnConsumedCapacity = null, + AttributesToGet = new List() } - ); - - request.TableName.Should().Be("TABLE"); - }); + ) + ); } } \ No newline at end of file From ec852e44f59b492b2312d4b392885b24aeeda9ae Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Tue, 5 Mar 2024 16:25:29 +0100 Subject: [PATCH 07/16] Rework unit test --- .../Dynatello.Tests/ToPutItemRequestTests.cs | 125 ++++++++++++------ 1 file changed, 85 insertions(+), 40 deletions(-) diff --git a/tests/Dynatello.Tests/ToPutItemRequestTests.cs b/tests/Dynatello.Tests/ToPutItemRequestTests.cs index 1866f199..cca3cc30 100644 --- a/tests/Dynatello.Tests/ToPutItemRequestTests.cs +++ b/tests/Dynatello.Tests/ToPutItemRequestTests.cs @@ -1,4 +1,5 @@ using Amazon.DynamoDBv2.DataModel; +using Amazon.DynamoDBv2.Model; using AutoFixture; using DynamoDBGenerator.Attributes; using Dynatello.Builders; @@ -15,57 +16,101 @@ public partial class ToPutItemRequestTests [Fact] public void Without_ConditionExpression_ShouldNotIncludeExpressionFields() { - var user = _fixture.Create(); - var putItemRequest = UserMarshaller.OnTable("TABLE") - .ToPutRequestBuilder() - .Build(user); - - putItemRequest.ConditionExpression.Should().BeNullOrWhiteSpace(); - putItemRequest.ExpressionAttributeNames.Should().BeNullOrEmpty(); - putItemRequest.ExpressionAttributeValues.Should().BeNullOrEmpty(); - putItemRequest.Item.Should().HaveCount(5); - putItemRequest.Item[nameof(user.Email)].S.Should().Be(user.Email); - putItemRequest.Item[nameof(user.Firstname)].S.Should().Be(user.Firstname); - putItemRequest.Item[nameof(user.Lastname)].S.Should().Be(user.Lastname); - putItemRequest.Item[nameof(user.Id)].S.Should().Be(user.Id); - putItemRequest.Item[nameof(user.Metadata)].M.Should().SatisfyRespectively(x => + var builder = UserMarshaller + .OnTable("TABLE") + .ToPutRequestBuilder(); + _fixture.CreateMany().Should().AllSatisfy(user => { - x.Key.Should().Be(nameof(user.Metadata.ModifiedAt)); - x.Value.S.Should().Be(user.Metadata.ModifiedAt.ToString("O")); + builder.Build(user) + .Should() + .BeEquivalentTo(new PutItemRequest + { + ConditionExpression = null, + ExpressionAttributeNames = null, + ExpressionAttributeValues = null, + Item = new Dictionary + { + { nameof(user.Email), new AttributeValue { S = user.Email } }, + { nameof(user.Firstname), new AttributeValue { S = user.Firstname } }, + { nameof(user.Lastname), new AttributeValue { S = user.Lastname } }, + { nameof(user.Id), new AttributeValue { S = user.Id } }, + { + nameof(user.Metadata), new AttributeValue + { + M = new Dictionary + { + { + nameof(user.Metadata.ModifiedAt), + new AttributeValue { S = user.Metadata.ModifiedAt.ToString("O") } + } + } + } + }, + }, + ReturnValues = null, + TableName = "TABLE", + Expected = null, + ReturnConsumedCapacity = null, + ConditionalOperator = null, + ReturnItemCollectionMetrics = null, + ReturnValuesOnConditionCheckFailure = null + }); }); - putItemRequest.ReturnValues.Should().Be(null); - putItemRequest.TableName.Should().Be("TABLE"); } [Fact] public void With_ConditionExpression_ShouldIncludeExpressionFields() { - var user = _fixture.Create(); - var putItemRequest = UserMarshaller + var builder = UserMarshaller .OnTable("TABLE") .WithConditionExpression((x, y) => $"{x.Email} <> {y.Email} AND {x.Firstname} = {y.Firstname}") - .ToPutRequestBuilder() - .Build(user); - - putItemRequest.ConditionExpression.Should().Be("#Email <> :p1 AND #Firstname = :p2"); - putItemRequest.ExpressionAttributeNames.Should().HaveCount(2); - putItemRequest.ExpressionAttributeNames["#Email"].Should().Be(nameof(user.Email)); - putItemRequest.ExpressionAttributeNames["#Firstname"].Should().Be(nameof(user.Firstname)); - putItemRequest.ExpressionAttributeValues.Should().HaveCount(2); - putItemRequest.ExpressionAttributeValues[":p1"].S.Should().Be(user.Email); - putItemRequest.ExpressionAttributeValues[":p2"].S.Should().Be(user.Firstname); - putItemRequest.Item.Should().HaveCount(5); - putItemRequest.Item[nameof(user.Email)].S.Should().Be(user.Email); - putItemRequest.Item[nameof(user.Firstname)].S.Should().Be(user.Firstname); - putItemRequest.Item[nameof(user.Lastname)].S.Should().Be(user.Lastname); - putItemRequest.Item[nameof(user.Id)].S.Should().Be(user.Id); - putItemRequest.Item[nameof(user.Metadata)].M.Should().SatisfyRespectively(x => + .ToPutRequestBuilder(); + + _fixture.CreateMany().Should().AllSatisfy(user => { - x.Key.Should().Be(nameof(user.Metadata.ModifiedAt)); - x.Value.S.Should().Be(user.Metadata.ModifiedAt.ToString("O")); + builder.Build(user) + .Should() + .BeEquivalentTo(new PutItemRequest + { + ConditionExpression = "#Email <> :p1 AND #Firstname = :p2", + ExpressionAttributeNames = new Dictionary + { + { "#Email", nameof(user.Email) }, + { "#Firstname", nameof(user.Firstname) } + }, + ExpressionAttributeValues = new Dictionary + { + { ":p1", new AttributeValue { S = user.Email } }, + { ":p2", new AttributeValue { S = user.Firstname } } + }, + Item = new Dictionary + { + { nameof(user.Email), new AttributeValue { S = user.Email } }, + { nameof(user.Firstname), new AttributeValue { S = user.Firstname } }, + { nameof(user.Lastname), new AttributeValue { S = user.Lastname } }, + { nameof(user.Id), new AttributeValue { S = user.Id } }, + { + nameof(user.Metadata), new AttributeValue + { + M = new Dictionary + { + { + nameof(user.Metadata.ModifiedAt), + new AttributeValue { S = user.Metadata.ModifiedAt.ToString("O") } + } + } + } + }, + }, + ReturnValues = null, + TableName = "TABLE", + Expected = null, + ReturnConsumedCapacity = null, + ConditionalOperator = null, + ReturnItemCollectionMetrics = null, + ReturnValuesOnConditionCheckFailure = null + }); }); - putItemRequest.ReturnValues.Should().Be(null); - putItemRequest.TableName.Should().Be("TABLE"); } } From 658ca2ce953c0068a6d2e40b278661849b2c47c8 Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Tue, 5 Mar 2024 16:42:13 +0100 Subject: [PATCH 08/16] Restrucutre tests --- tests/Dynatello.Tests/ToQueryRequestTests.cs | 114 ++++++++++++------- 1 file changed, 70 insertions(+), 44 deletions(-) diff --git a/tests/Dynatello.Tests/ToQueryRequestTests.cs b/tests/Dynatello.Tests/ToQueryRequestTests.cs index 1b2454c0..f7064c27 100644 --- a/tests/Dynatello.Tests/ToQueryRequestTests.cs +++ b/tests/Dynatello.Tests/ToQueryRequestTests.cs @@ -11,65 +11,91 @@ namespace Dynatello.Tests; public class ToQueryRequestTests { - private static readonly QueryRequestBuilder<(Guid Id, double MinimumCuteness)> QueryCatWithIdAndMinimumCuteness; - private static readonly QueryRequestBuilder<(Guid Id, double MinimumCuteness)> QueryCatWithId; - - static ToQueryRequestTests() - { - var withKeyConditionExpression = Cat.QueryWithCuteness - .OnTable("TABLE") - .WithKeyConditionExpression((x, y) => $"{x.Id} = {y.Id}"); - QueryCatWithId = withKeyConditionExpression.ToQueryRequestBuilder(); - - QueryCatWithIdAndMinimumCuteness = withKeyConditionExpression - .WithFilterExpression((x, y) => $"{x.Cuteness} > {y.MinimumCuteness}") - .ToQueryRequestBuilder(); - } - [Fact] public void Build_Request() { + var builder = Cat.QueryWithCuteness + .OnTable("TABLE") + .WithKeyConditionExpression((x, y) => $"{x.Id} = {y.Id}") + .ToQueryRequestBuilder(); Cat.Fixture.CreateMany<(Guid Id, double MinimumCuteness)>(10).Should().AllSatisfy(tuple => { - var request = QueryCatWithId.Build(tuple); - - request.TableName.Should().Be("TABLE"); - request.ExpressionAttributeNames.Should().BeEquivalentTo(new Dictionary - { - { "#Id", nameof(Cat.Id) } - }); + builder.Build(tuple).Should().BeEquivalentTo( + new QueryRequest + { + TableName = "TABLE", + ExpressionAttributeNames = new Dictionary + { + { "#Id", nameof(Cat.Id) } + }, - request.ExpressionAttributeValues.Should().BeEquivalentTo(new Dictionary - { - { ":p1", new AttributeValue { S = tuple.Id.ToString() } } - }); - - request.KeyConditionExpression.Should().Be("#Id = :p1"); + ExpressionAttributeValues = new Dictionary + { + { ":p1", new AttributeValue { S = tuple.Id.ToString() } } + }, + KeyConditionExpression = "#Id = :p1", + KeyConditions = null, + ConditionalOperator = null, + AttributesToGet = null, + ReturnConsumedCapacity = null, + FilterExpression = null, + Limit = 0, + ConsistentRead = false, + Select = null, + ProjectionExpression = null, + IndexName = null, + QueryFilter = null, + ExclusiveStartKey = new Dictionary(), + IsLimitSet = false, + ScanIndexForward = false + }); }); } [Fact] public void Build_Request_FilterExpression() { + var builder = Cat.QueryWithCuteness + .OnTable("TABLE") + .WithKeyConditionExpression((x, y) => $"{x.Id} = {y.Id}") + .WithFilterExpression((x, y) => $"{x.Cuteness} > {y.MinimumCuteness}") + .ToQueryRequestBuilder(); Cat.Fixture.CreateMany<(Guid Id, double MinimumCuteness)>(10).Should().AllSatisfy(tuple => { - var request = QueryCatWithIdAndMinimumCuteness.Build(tuple); - - request.TableName.Should().Be("TABLE"); - request.ExpressionAttributeNames.Should().BeEquivalentTo(new Dictionary - { - { "#Id", nameof(Cat.Id) }, - { "#Cuteness", nameof(Cat.Cuteness) } - }); - - request.ExpressionAttributeValues.Should().BeEquivalentTo(new Dictionary - { - { ":p1", new AttributeValue { S = tuple.Id.ToString() } }, - { ":p2", new AttributeValue { N = tuple.MinimumCuteness.ToString(CultureInfo.InvariantCulture) } } - }); + builder.Build(tuple).Should().BeEquivalentTo( + new QueryRequest + { + TableName = "TABLE", + ExpressionAttributeNames = new Dictionary + { + { "#Id", nameof(Cat.Id) }, + { "#Cuteness", nameof(Cat.Cuteness) } + }, - request.KeyConditionExpression.Should().Be("#Id = :p1"); - request.FilterExpression.Should().Be("#Cuteness > :p2"); + ExpressionAttributeValues = new Dictionary + { + { ":p1", new AttributeValue { S = tuple.Id.ToString() } }, + { + ":p2", + new AttributeValue { N = tuple.MinimumCuteness.ToString(CultureInfo.InvariantCulture) } + } + }, + KeyConditionExpression = "#Id = :p1", + KeyConditions = null, + ConditionalOperator = null, + AttributesToGet = null, + ReturnConsumedCapacity = null, + FilterExpression = "#Cuteness > :p2", + Limit = 0, + ConsistentRead = false, + Select = null, + ProjectionExpression = null, + IndexName = null, + QueryFilter = null, + ExclusiveStartKey = new Dictionary(), + IsLimitSet = false, + ScanIndexForward = false + }); }); } } From 4a725fd25f3dc16952708169d80573ef72ccb38d Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Tue, 5 Mar 2024 17:47:31 +0100 Subject: [PATCH 09/16] Update --- .../ToUpdateItemRequestTests.cs | 196 +++++++++++++----- 1 file changed, 144 insertions(+), 52 deletions(-) diff --git a/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs b/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs index 1fd1b9ca..9f087a43 100644 --- a/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs +++ b/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs @@ -1,3 +1,4 @@ +using Amazon.DynamoDBv2.Model; using AutoFixture; using DynamoDBGenerator.Attributes; using Dynatello.Builders; @@ -15,78 +16,169 @@ public partial class ToUpdateItemRequestTests [Fact] public void ArgumentTypeProvided_WithConditionExpression_ShouldIncludeUpdateAndConditionExpressionFields() { - var updateUserEmail = _fixture.Create(); - var updateItemRequest = UpdateEmail + var updateFirst = UpdateEmail .OnTable("TABLE") .WithUpdateExpression((x, y) => $"SET {x.Email} = {y.UserEmail}, {x.Metadata.ModifiedAt} = {y.TimeStamp}") .WithConditionExpression((x, y) => $"{x.Id} = {y.UserId} AND {x.Email} <> {y.UserEmail}") - .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.UserId, y.UserEmail)) - .Build(updateUserEmail); + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.UserId, y.UserEmail)); - updateItemRequest.ConditionExpression.Should().Be("#Id = :p3 AND #Email <> :p1"); - updateItemRequest.ExpressionAttributeNames.Should().HaveCount(3); - updateItemRequest.ExpressionAttributeNames["#Email"].Should().Be(nameof(User.Email)); - updateItemRequest.ExpressionAttributeNames["#Id"].Should().Be(nameof(User.Id)); - updateItemRequest.ExpressionAttributeNames["#Metadata.#ModifiedAt"].Should() - .Be(nameof(User.Metadata.ModifiedAt)); - updateItemRequest.ExpressionAttributeValues.Should().HaveCount(3); - updateItemRequest.ExpressionAttributeValues[":p1"].S.Should().Be(updateUserEmail.UserEmail); - updateItemRequest.ExpressionAttributeValues[":p2"].S.Should().Be(updateUserEmail.TimeStamp.ToString("O")); - updateItemRequest.ExpressionAttributeValues[":p3"].S.Should().Be(updateUserEmail.UserId); - updateItemRequest.Key[nameof(User.Email)].S.Should().Be(updateUserEmail.UserEmail); - updateItemRequest.Key[nameof(User.Id)].S.Should().Be(updateUserEmail.UserId); - updateItemRequest.ReturnValues.Should().Be(null); - updateItemRequest.TableName.Should().Be("TABLE"); - updateItemRequest.UpdateExpression.Should().Be("SET #Email = :p1, #Metadata.#ModifiedAt = :p2"); + var conditionFirst = UpdateEmail + .OnTable("TABLE") + .WithConditionExpression((x, y) => $"{x.Id} = {y.UserId} AND {x.Email} <> {y.UserEmail}") + .WithUpdateExpression((x, y) => $"SET {x.Email} = {y.UserEmail}, {x.Metadata.ModifiedAt} = {y.TimeStamp}") + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.UserId, y.UserEmail)); + + _fixture + .CreateMany() + .Should() + .AllSatisfy(x => Expected(x, updateFirst)); + _fixture + .CreateMany() + .Should() + .AllSatisfy(x => Expected(x, conditionFirst)); + + static void Expected(UpdateUserEmail updateUserEmail, UpdateRequestBuilder builder) => + builder.Build(updateUserEmail) + .Should() + .BeEquivalentTo(new UpdateItemRequest + { + ConditionExpression = "#Id = :p3 AND #Email <> :p1", + ExpressionAttributeNames = new Dictionary + { + { "#Email", nameof(User.Email) }, { "#Id", nameof(User.Id) }, + { "#Metadata.#ModifiedAt", nameof(User.Metadata.ModifiedAt) } + }, + ExpressionAttributeValues = new Dictionary + { + { ":p1", new AttributeValue { S = updateUserEmail.UserEmail } }, + { ":p2", new AttributeValue { S = updateUserEmail.TimeStamp.ToString("O") } }, + { ":p3", new AttributeValue { S = updateUserEmail.UserId } } + }, + Key = new Dictionary + { + { nameof(User.Id), new AttributeValue { S = updateUserEmail.UserId } }, + { nameof(User.Email), new AttributeValue { S = updateUserEmail.UserEmail } } + }, + ReturnValues = null, + TableName = "TABLE", + UpdateExpression = "SET #Email = :p1, #Metadata.#ModifiedAt = :p2", + ReturnConsumedCapacity = null, + ConditionalOperator = null, + Expected = null, + ReturnItemCollectionMetrics = null, + ReturnValuesOnConditionCheckFailure = null, + AttributeUpdates = null + }); } [Fact] public void ArgumentTypeProvided_WithoutConditionExpression_ShouldOnlyIncludeUpdateExpressionFields() { - var updateUserEmail = _fixture.Create(); - var updateItemRequest = UpdateEmail + var builder = UpdateEmail .OnTable("TABLE") .WithUpdateExpression((x, y) => $"SET {x.Email} = {y.UserEmail}, {x.Metadata.ModifiedAt} = {y.TimeStamp}") - .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.UserId, y.UserEmail)) - .Build(updateUserEmail); + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.UserId, y.UserEmail)); - updateItemRequest.ConditionExpression.Should().BeNullOrWhiteSpace(); - updateItemRequest.ExpressionAttributeNames.Should().HaveCount(2); - updateItemRequest.ExpressionAttributeNames["#Email"].Should().Be(nameof(User.Email)); - updateItemRequest.ExpressionAttributeNames["#Metadata.#ModifiedAt"].Should() - .Be(nameof(User.Metadata.ModifiedAt)); - updateItemRequest.ExpressionAttributeValues.Should().HaveCount(2); - updateItemRequest.ExpressionAttributeValues[":p1"].S.Should().Be(updateUserEmail.UserEmail); - updateItemRequest.ExpressionAttributeValues[":p2"].S.Should().Be(updateUserEmail.TimeStamp.ToString("O")); - updateItemRequest.Key[nameof(User.Email)].S.Should().Be(updateUserEmail.UserEmail); - updateItemRequest.Key[nameof(User.Id)].S.Should().Be(updateUserEmail.UserId); - updateItemRequest.ReturnValues.Should().Be(null); - updateItemRequest.TableName.Should().Be("TABLE"); - updateItemRequest.UpdateExpression.Should().Be("SET #Email = :p1, #Metadata.#ModifiedAt = :p2"); + _fixture + .CreateMany() + .Should() + .AllSatisfy(updateUserEmail => builder + .Build(updateUserEmail) + .Should() + .BeEquivalentTo(new UpdateItemRequest + { + ConditionExpression = null, + ExpressionAttributeNames = new Dictionary + { + { "#Email", nameof(User.Email) }, + { "#Metadata.#ModifiedAt", nameof(User.Metadata.ModifiedAt) } + }, + ExpressionAttributeValues = new Dictionary + { + { + ":p1", new AttributeValue + { + S = updateUserEmail.UserEmail + } + }, + { + ":p2", new AttributeValue + { + S = updateUserEmail.TimeStamp.ToString("O") + } + } + }, + Key = new Dictionary + { + { nameof(User.Id), new AttributeValue { S = updateUserEmail.UserId } }, + { nameof(User.Email), new AttributeValue { S = updateUserEmail.UserEmail } } + }, + ReturnValues = null, + TableName = "TABLE", + UpdateExpression = "SET #Email = :p1, #Metadata.#ModifiedAt = :p2", + ReturnConsumedCapacity = null, + ConditionalOperator = null, + Expected = null, + ReturnItemCollectionMetrics = null, + ReturnValuesOnConditionCheckFailure = null, + AttributeUpdates = null + })); } [Fact] public void NoArgumentTypeProvided_WithoutConditionExpression_ShouldOnlyIncludeUpdateExpressionFields() { - var user = _fixture.Create(); - var updateItemRequest = UserMarshaller + var builder = UserMarshaller .OnTable("TABLE") .WithUpdateExpression((x, y) => $"SET {x.Email} = {y.Email}, {x.Firstname} = {y.Firstname}") - .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.Id, y.Lastname)) - .Build(user); + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.Id, y.Email)); - updateItemRequest.ConditionExpression.Should().BeNullOrWhiteSpace(); - updateItemRequest.ExpressionAttributeNames.Should().HaveCount(2); - updateItemRequest.ExpressionAttributeNames["#Email"].Should().Be(nameof(user.Email)); - updateItemRequest.ExpressionAttributeNames["#Firstname"].Should().Be(nameof(user.Firstname)); - updateItemRequest.ExpressionAttributeValues.Should().HaveCount(2); - updateItemRequest.ExpressionAttributeValues[":p1"].S.Should().Be(user.Email); - updateItemRequest.ExpressionAttributeValues[":p2"].S.Should().Be(user.Firstname); - updateItemRequest.Key[nameof(user.Id)].S.Should().Be(user.Id); - updateItemRequest.Key[nameof(user.Email)].S.Should().Be(user.Lastname); - updateItemRequest.ReturnValues.Should().Be(null); - updateItemRequest.TableName.Should().Be("TABLE"); - updateItemRequest.UpdateExpression.Should().Be("SET #Email = :p1, #Firstname = :p2"); + _fixture.CreateMany() + .Should() + .AllSatisfy(user => + { + builder + .Build(user) + .Should() + .BeEquivalentTo(new UpdateItemRequest + { + ConditionExpression = null, + ExpressionAttributeNames = new Dictionary + { + { "#Email", nameof(User.Email) }, + { "#Firstname", nameof(User.Firstname) } + }, + ExpressionAttributeValues = new Dictionary + { + { + ":p1", new AttributeValue + { + S = user.Email + } + }, + { + ":p2", new AttributeValue + { + S = user.Firstname + } + } + }, + Key = new Dictionary + { + { nameof(User.Id), new AttributeValue { S = user.Id } }, + { nameof(User.Email), new AttributeValue { S = user.Email } } + }, + ReturnValues = null, + TableName = "TABLE", + UpdateExpression = "SET #Email = :p1, #Firstname = :p2", + ReturnConsumedCapacity = null, + ConditionalOperator = null, + Expected = null, + ReturnItemCollectionMetrics = null, + ReturnValuesOnConditionCheckFailure = null, + AttributeUpdates = null + }); + }); } [Fact] From 92897a294e3535af374736ec9f27127624a2c280 Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Wed, 6 Mar 2024 00:28:29 +0100 Subject: [PATCH 10/16] Check all properties --- .../ToUpdateItemRequestTests.cs | 173 +++++++++--------- 1 file changed, 88 insertions(+), 85 deletions(-) diff --git a/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs b/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs index 9f087a43..4bd0f9bf 100644 --- a/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs +++ b/tests/Dynatello.Tests/ToUpdateItemRequestTests.cs @@ -42,7 +42,10 @@ static void Expected(UpdateUserEmail updateUserEmail, UpdateRequestBuilder { { "#Email", nameof(User.Email) }, { "#Id", nameof(User.Id) }, @@ -59,15 +62,12 @@ static void Expected(UpdateUserEmail updateUserEmail, UpdateRequestBuilder { { "#Email", nameof(User.Email) }, @@ -95,33 +98,20 @@ public void ArgumentTypeProvided_WithoutConditionExpression_ShouldOnlyIncludeUpd }, ExpressionAttributeValues = new Dictionary { - { - ":p1", new AttributeValue - { - S = updateUserEmail.UserEmail - } - }, - { - ":p2", new AttributeValue - { - S = updateUserEmail.TimeStamp.ToString("O") - } - } + { ":p1", new AttributeValue { S = updateUserEmail.UserEmail } }, + { ":p2", new AttributeValue { S = updateUserEmail.TimeStamp.ToString("O") } } }, Key = new Dictionary { { nameof(User.Id), new AttributeValue { S = updateUserEmail.UserId } }, { nameof(User.Email), new AttributeValue { S = updateUserEmail.UserEmail } } }, - ReturnValues = null, - TableName = "TABLE", - UpdateExpression = "SET #Email = :p1, #Metadata.#ModifiedAt = :p2", ReturnConsumedCapacity = null, - ConditionalOperator = null, - Expected = null, ReturnItemCollectionMetrics = null, + ReturnValues = null, ReturnValuesOnConditionCheckFailure = null, - AttributeUpdates = null + TableName = "TABLE", + UpdateExpression = "SET #Email = :p1, #Metadata.#ModifiedAt = :p2" })); } @@ -135,76 +125,89 @@ public void NoArgumentTypeProvided_WithoutConditionExpression_ShouldOnlyIncludeU _fixture.CreateMany() .Should() - .AllSatisfy(user => - { - builder - .Build(user) - .Should() - .BeEquivalentTo(new UpdateItemRequest + .AllSatisfy(x => builder + .Build(x) + .Should() + .BeEquivalentTo(new UpdateItemRequest + { + AttributeUpdates = null, + ConditionalOperator = null, + ConditionExpression = null, + Expected = null, + ExpressionAttributeNames = new Dictionary { - ConditionExpression = null, - ExpressionAttributeNames = new Dictionary - { - { "#Email", nameof(User.Email) }, - { "#Firstname", nameof(User.Firstname) } - }, - ExpressionAttributeValues = new Dictionary - { - { - ":p1", new AttributeValue - { - S = user.Email - } - }, - { - ":p2", new AttributeValue - { - S = user.Firstname - } - } - }, - Key = new Dictionary - { - { nameof(User.Id), new AttributeValue { S = user.Id } }, - { nameof(User.Email), new AttributeValue { S = user.Email } } - }, - ReturnValues = null, - TableName = "TABLE", - UpdateExpression = "SET #Email = :p1, #Firstname = :p2", - ReturnConsumedCapacity = null, - ConditionalOperator = null, - Expected = null, - ReturnItemCollectionMetrics = null, - ReturnValuesOnConditionCheckFailure = null, - AttributeUpdates = null - }); - }); + { "#Email", nameof(User.Email) }, + { "#Firstname", nameof(User.Firstname) } + }, + ExpressionAttributeValues = new Dictionary + { + { ":p1", new AttributeValue { S = x.Email } }, + { ":p2", new AttributeValue { S = x.Firstname } } + }, + Key = new Dictionary + { + { nameof(User.Id), new AttributeValue { S = x.Id } }, + { nameof(User.Email), new AttributeValue { S = x.Email } } + }, + ReturnConsumedCapacity = null, + ReturnItemCollectionMetrics = null, + ReturnValues = null, + ReturnValuesOnConditionCheckFailure = null, + TableName = "TABLE", + UpdateExpression = "SET #Email = :p1, #Firstname = :p2" + })); } [Fact] public void NoArgumentTypeProvided_WithConditionExpression_ShouldIncludeUpdateAndConditionExpressionFields() { - var user = _fixture.Create(); - var updateItemRequest = UserMarshaller + var updateFirst = UserMarshaller + .OnTable("TABLE") + .WithUpdateExpression((x, y) => $"SET {x.Email} = {y.Email}, {x.Firstname} = {y.Firstname}") + .WithConditionExpression((x, y) => $"{x.Id} = {y.Id}") + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.Id, y.Email)); + + var conditionFirst = UserMarshaller .OnTable("TABLE") .WithUpdateExpression((x, y) => $"SET {x.Email} = {y.Email}, {x.Firstname} = {y.Firstname}") .WithConditionExpression((x, y) => $"{x.Id} = {y.Id}") - .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.Id, y.Lastname)) - .Build(user); + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.Id, y.Email)); - updateItemRequest.ConditionExpression.Should().Be("#Id = :p3"); - updateItemRequest.ExpressionAttributeNames.Should().HaveCount(3); - updateItemRequest.ExpressionAttributeNames["#Email"].Should().Be(nameof(user.Email)); - updateItemRequest.ExpressionAttributeNames["#Firstname"].Should().Be(nameof(user.Firstname)); - updateItemRequest.ExpressionAttributeNames["#Id"].Should().Be(nameof(user.Id)); - updateItemRequest.ExpressionAttributeValues.Should().HaveCount(3); - updateItemRequest.ExpressionAttributeValues[":p1"].S.Should().Be(user.Email); - updateItemRequest.ExpressionAttributeValues[":p2"].S.Should().Be(user.Firstname); - updateItemRequest.ExpressionAttributeValues[":p3"].S.Should().Be(user.Id); - updateItemRequest.Key[nameof(user.Id)].S.Should().Be(user.Id); - updateItemRequest.Key[nameof(user.Email)].S.Should().Be(user.Lastname); - updateItemRequest.ReturnValues.Should().Be(null); - updateItemRequest.TableName.Should().Be("TABLE"); - updateItemRequest.UpdateExpression.Should().Be("SET #Email = :p1, #Firstname = :p2"); + _fixture.CreateMany().Should().AllSatisfy(x => Expected(x, conditionFirst)); + _fixture.CreateMany().Should().AllSatisfy(x => Expected(x, updateFirst)); + + static void Expected(User x, UpdateRequestBuilder builder) => + builder + .Build(x) + .Should() + .BeEquivalentTo(new UpdateItemRequest + { + AttributeUpdates = null, + ConditionalOperator = null, + ConditionExpression = "#Id = :p3", + Expected = null, + ExpressionAttributeNames = new Dictionary() + { + { "#Email", "Email" }, + { "#Firstname", "Firstname" }, + { "#Id", "Id" } + }, + ExpressionAttributeValues = new Dictionary() + { + { ":p1", new AttributeValue { S = x.Email } }, + { ":p2", new AttributeValue { S = x.Firstname } }, { ":p3", new AttributeValue { S = x.Id } } + }, + Key = new Dictionary + { + { "Id", new AttributeValue { S = x.Id } }, + { "Email", new AttributeValue { S = x.Email } } + }, + ReturnConsumedCapacity = null, + ReturnItemCollectionMetrics = null, + ReturnValues = null, + ReturnValuesOnConditionCheckFailure = null, + TableName = "TABLE", + UpdateExpression = "SET #Email = :p1, #Firstname = :p2" + }); } } \ No newline at end of file From 814335804a1974c8034c77511019a285a8fbbb13 Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Wed, 6 Mar 2024 00:42:00 +0100 Subject: [PATCH 11/16] Default to null --- src/Dynatello/Builders/QueryRequestBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dynatello/Builders/QueryRequestBuilder.cs b/src/Dynatello/Builders/QueryRequestBuilder.cs index 4d55ff88..71f979f8 100644 --- a/src/Dynatello/Builders/QueryRequestBuilder.cs +++ b/src/Dynatello/Builders/QueryRequestBuilder.cs @@ -55,7 +55,7 @@ public QueryRequest Build(T arg) ExpressionAttributeValues = attributeExpression.Values, ExpressionAttributeNames = attributeExpression.Names, TableName = TableName, - IndexName = IndexName, + IndexName = null, ProjectionExpression = null }; From 4be06da50a15fc63ace5aa9155356f7fa0414546 Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Wed, 6 Mar 2024 15:21:32 +0100 Subject: [PATCH 12/16] Add reusable exception utility --- src/Dynatello/Builders/GetRequestBuilder.cs | 4 ++-- src/Dynatello/Builders/PutRequestBuilder.cs | 4 ++-- src/Dynatello/Builders/QueryRequestBuilder.cs | 4 ++-- src/Dynatello/Builders/Types/ConditionExpression.cs | 4 ++-- .../Builders/Types/ConditionalUpdateExpression.cs | 4 ++-- .../Builders/Types/KeyConditionExpression.cs | 4 ++-- .../Types/KeyConditionedFilterExpression.cs | 4 ++-- src/Dynatello/Builders/Types/TableAccess.cs | 4 ++-- src/Dynatello/Builders/Types/UpdateExpression.cs | 4 ++-- src/Dynatello/Builders/UpdateRequestBuilder.cs | 4 ++-- src/Dynatello/Constants.cs | 13 +++++++++++++ 11 files changed, 33 insertions(+), 20 deletions(-) create mode 100644 src/Dynatello/Constants.cs diff --git a/src/Dynatello/Builders/GetRequestBuilder.cs b/src/Dynatello/Builders/GetRequestBuilder.cs index c3df2624..311f58e1 100644 --- a/src/Dynatello/Builders/GetRequestBuilder.cs +++ b/src/Dynatello/Builders/GetRequestBuilder.cs @@ -15,10 +15,10 @@ internal GetRequestBuilder( TableName = tableName; } - [Obsolete("Do not used this constructor!", true)] + [Obsolete(Constants.ObsoleteConstructorMessage, true)] public GetRequestBuilder() { - throw new InvalidOperationException("This is an invalid constructor access."); + throw Constants.InvalidConstructor(); } /// diff --git a/src/Dynatello/Builders/PutRequestBuilder.cs b/src/Dynatello/Builders/PutRequestBuilder.cs index 055e02dd..cf7b718b 100644 --- a/src/Dynatello/Builders/PutRequestBuilder.cs +++ b/src/Dynatello/Builders/PutRequestBuilder.cs @@ -26,10 +26,10 @@ string tableName _tableName = tableName; } - [Obsolete("Do not used this constructor!", true)] + [Obsolete(Constants.ObsoleteConstructorMessage, true)] public PutRequestBuilder() { - throw new InvalidOperationException("This is an invalid constructor access."); + throw Constants.InvalidConstructor(); } /// diff --git a/src/Dynatello/Builders/QueryRequestBuilder.cs b/src/Dynatello/Builders/QueryRequestBuilder.cs index 71f979f8..2ecff3d0 100644 --- a/src/Dynatello/Builders/QueryRequestBuilder.cs +++ b/src/Dynatello/Builders/QueryRequestBuilder.cs @@ -14,10 +14,10 @@ internal QueryRequestBuilder(Func attributeExpressionSe TableName = tableName; } - [Obsolete("Do not used this constructor!", true)] + [Obsolete(Constants.ObsoleteConstructorMessage, true)] public QueryRequestBuilder() { - throw new InvalidOperationException("This is an invalid constructor access."); + throw Constants.InvalidConstructor(); } /// diff --git a/src/Dynatello/Builders/Types/ConditionExpression.cs b/src/Dynatello/Builders/Types/ConditionExpression.cs index 8e8d61c4..2b856361 100644 --- a/src/Dynatello/Builders/Types/ConditionExpression.cs +++ b/src/Dynatello/Builders/Types/ConditionExpression.cs @@ -9,10 +9,10 @@ public readonly record struct ConditionExpression Condition; internal readonly TableAccess TableAccess; - [Obsolete("Do not use this constructor!", true)] + [Obsolete(Constants.ObsoleteConstructorMessage, true)] public ConditionExpression() { - throw new InvalidOperationException("This is an invalid constructor access."); + throw Constants.InvalidConstructor(); } internal ConditionExpression( diff --git a/src/Dynatello/Builders/Types/ConditionalUpdateExpression.cs b/src/Dynatello/Builders/Types/ConditionalUpdateExpression.cs index 15b251b8..911ad497 100644 --- a/src/Dynatello/Builders/Types/ConditionalUpdateExpression.cs +++ b/src/Dynatello/Builders/Types/ConditionalUpdateExpression.cs @@ -10,10 +10,10 @@ public readonly record struct ConditionalUpdateExpression TableAccess; internal readonly Func Update; - [Obsolete("Do not use this constructor!", true)] + [Obsolete(Constants.ObsoleteConstructorMessage, true)] public ConditionalUpdateExpression() { - throw new InvalidOperationException("This is an invalid constructor access."); + throw Constants.InvalidConstructor(); } internal ConditionalUpdateExpression( diff --git a/src/Dynatello/Builders/Types/KeyConditionExpression.cs b/src/Dynatello/Builders/Types/KeyConditionExpression.cs index cd88f79e..e6b61b7b 100644 --- a/src/Dynatello/Builders/Types/KeyConditionExpression.cs +++ b/src/Dynatello/Builders/Types/KeyConditionExpression.cs @@ -9,10 +9,10 @@ public readonly struct KeyConditionExpression Condition; internal readonly TableAccess TableAccess; - [Obsolete("Do not use this constructor!", true)] + [Obsolete(Constants.ObsoleteConstructorMessage, true)] public KeyConditionExpression() { - throw new InvalidOperationException("This is an invalid constructor access."); + throw Constants.InvalidConstructor(); } internal KeyConditionExpression( diff --git a/src/Dynatello/Builders/Types/KeyConditionedFilterExpression.cs b/src/Dynatello/Builders/Types/KeyConditionedFilterExpression.cs index 904faf1f..ec610ac7 100644 --- a/src/Dynatello/Builders/Types/KeyConditionedFilterExpression.cs +++ b/src/Dynatello/Builders/Types/KeyConditionedFilterExpression.cs @@ -10,10 +10,10 @@ public readonly struct KeyConditionedFilterExpression Filter; internal readonly TableAccess TableAccess; - [Obsolete("Do not use this constructor!", true)] + [Obsolete(Constants.ObsoleteConstructorMessage, true)] public KeyConditionedFilterExpression() { - throw new InvalidOperationException("This is an invalid constructor access."); + throw Constants.InvalidConstructor(); } internal KeyConditionedFilterExpression( diff --git a/src/Dynatello/Builders/Types/TableAccess.cs b/src/Dynatello/Builders/Types/TableAccess.cs index 421dd04f..c92f2f88 100644 --- a/src/Dynatello/Builders/Types/TableAccess.cs +++ b/src/Dynatello/Builders/Types/TableAccess.cs @@ -6,10 +6,10 @@ public readonly record struct TableAccess { - [Obsolete("Do not use this constructor!", true)] + [Obsolete(Constants.ObsoleteConstructorMessage, true)] public TableAccess() { - throw new InvalidOperationException("This is an invalid constructor access."); + throw Constants.InvalidConstructor(); } internal TableAccess(in string tableName, in IDynamoDBMarshaller item) diff --git a/src/Dynatello/Builders/Types/UpdateExpression.cs b/src/Dynatello/Builders/Types/UpdateExpression.cs index 6d0e2088..80fd8b4e 100644 --- a/src/Dynatello/Builders/Types/UpdateExpression.cs +++ b/src/Dynatello/Builders/Types/UpdateExpression.cs @@ -9,10 +9,10 @@ public readonly record struct UpdateExpression TableAccess; internal readonly Func Update; - [Obsolete("Do not use this constructor!", true)] + [Obsolete(Constants.ObsoleteConstructorMessage, true)] public UpdateExpression() { - throw new InvalidOperationException("This is an invalid constructor access."); + throw Constants.InvalidConstructor(); } internal UpdateExpression( diff --git a/src/Dynatello/Builders/UpdateRequestBuilder.cs b/src/Dynatello/Builders/UpdateRequestBuilder.cs index aba20758..bc643fd8 100644 --- a/src/Dynatello/Builders/UpdateRequestBuilder.cs +++ b/src/Dynatello/Builders/UpdateRequestBuilder.cs @@ -15,10 +15,10 @@ public readonly record struct UpdateRequestBuilder private readonly string _tableName; - [Obsolete("Do not used this constructor!", true)] + [Obsolete(Constants.ObsoleteConstructorMessage, true)] public UpdateRequestBuilder() { - throw new InvalidOperationException("This is an invalid constructor access."); + throw Constants.InvalidConstructor(); } internal UpdateRequestBuilder( diff --git a/src/Dynatello/Constants.cs b/src/Dynatello/Constants.cs new file mode 100644 index 00000000..53d74748 --- /dev/null +++ b/src/Dynatello/Constants.cs @@ -0,0 +1,13 @@ +namespace Dynatello; + +internal static class Constants +{ + internal const string ObsoleteConstructorMessage = "Do not use this constructor!"; + private const string ConstructorException = "This is an invalid constructor access."; + + internal static Exception InvalidConstructor() + { + return new InvalidOperationException(ConstructorException); + } + +} \ No newline at end of file From e6515ba952890bcd106525207128ad816319deb2 Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Wed, 6 Mar 2024 15:27:08 +0100 Subject: [PATCH 13/16] Use extension method --- src/Dynatello/DynamoDBMarshallerExtensions.cs | 25 +++---------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/src/Dynatello/DynamoDBMarshallerExtensions.cs b/src/Dynatello/DynamoDBMarshallerExtensions.cs index e8c89343..16fc73cd 100644 --- a/src/Dynatello/DynamoDBMarshallerExtensions.cs +++ b/src/Dynatello/DynamoDBMarshallerExtensions.cs @@ -44,28 +44,9 @@ internal static Func> ComposeKeys return (update, condition) switch { (null, null) => throw new ArgumentNullException(""), - (not null, not null) => y => - ToAttributeExpression( - source.AttributeExpressionNameTracker, - source.AttributeExpressionValueTracker, - y, - update, - condition - ), - (not null, null) => y => - ToAttributeExpression( - source.AttributeExpressionNameTracker, - source.AttributeExpressionValueTracker, - y, - update - ), - (null, not null) => y => - ToAttributeExpression( - source.AttributeExpressionNameTracker, - source.AttributeExpressionValueTracker, - y, - condition - ) + (not null, not null) => y => source.ToAttributeExpression(y, update, condition), + (not null, null) => y => source.ToAttributeExpression(y, update), + (null, not null) => y => source.ToAttributeExpression(y, condition) }; } } \ No newline at end of file From 777283a3f274c6651e0fc5053f84f17f4051c639 Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Wed, 6 Mar 2024 16:12:41 +0100 Subject: [PATCH 14/16] Add sample app --- samples/ConsoleApp/ConsoleApp.csproj | 3 +- samples/ConsoleApp/ProductEntity.cs | 4 + samples/ConsoleApp/ProductRepository.cs | 118 ++++++++++++++++++++++++ samples/ConsoleApp/Program.cs | 5 +- src/Dynatello/Builders/Extensions.cs | 12 +++ 5 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 samples/ConsoleApp/ProductEntity.cs create mode 100644 samples/ConsoleApp/ProductRepository.cs diff --git a/samples/ConsoleApp/ConsoleApp.csproj b/samples/ConsoleApp/ConsoleApp.csproj index d7a49aaa..ed9cca51 100644 --- a/samples/ConsoleApp/ConsoleApp.csproj +++ b/samples/ConsoleApp/ConsoleApp.csproj @@ -9,8 +9,7 @@ - - + diff --git a/samples/ConsoleApp/ProductEntity.cs b/samples/ConsoleApp/ProductEntity.cs new file mode 100644 index 00000000..03c1ed95 --- /dev/null +++ b/samples/ConsoleApp/ProductEntity.cs @@ -0,0 +1,4 @@ +using Amazon.DynamoDBv2.DataModel; +using DynamoDBGenerator.Attributes; + +namespace SampleApp; diff --git a/samples/ConsoleApp/ProductRepository.cs b/samples/ConsoleApp/ProductRepository.cs new file mode 100644 index 00000000..3716c94d --- /dev/null +++ b/samples/ConsoleApp/ProductRepository.cs @@ -0,0 +1,118 @@ +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; + +namespace SampleApp; + +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 = ProductEntity.GetById + .OnTable(tableName) + .ToGetRequestBuilder(argumentType => argumentType); // Since the ArgumentType is set to string, we don't need to select a property. + + _updatePrice = ProductEntity.UpdatePrice + .OnTable(tableName) + .WithUpdateExpression((x, y) => $"SET {x.Price} = {y.NewPrice}, {x.Metadata.ModifiedAt} = {y.TimeStamp}") // Specify the update operation + .ToUpdateItemRequestBuilder((x, y) => x.PartitionKey(y.Id)) + with + { + ReturnValues = ReturnValue.ALL_NEW + }; + + _createProduct = ProductEntity.PutProduct + .OnTable(tableName) + .WithConditionExpression((x, y) => $"{x.Id} <> {y.Id}") // Ensure we don't have an existing Product in DynamoDB + .ToPutRequestBuilder(); + + _queryByPrice = ProductEntity.QueryByPrice + .OnTable(tableName) + .WithKeyConditionExpression((x, y) => $"{x.Price} = {y}") + .ToQueryRequestBuilder() with + { + IndexName = ProductEntity.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 => ProductEntity.QueryByPrice.Unmarshall(x)) + .ToArray(); + } + + public async Task Create(ProductEntity productEntity) + { + PutItemRequest request = _createProduct.Build(productEntity); + 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("..."); + + ProductEntity productEntity = ProductEntity.GetById.Unmarshall(response.Item); + + return productEntity; + } + + 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; + + ProductEntity productEntity = ProductEntity.UpdatePrice.Unmarshall(response.Attributes); + + return productEntity; + } +} + +[DynamoDBMarshaller(typeof(ProductEntity), PropertyName = "PutProduct")] +[DynamoDBMarshaller(typeof(ProductEntity), ArgumentType = typeof(string), PropertyName = "GetById")] +[DynamoDBMarshaller(typeof(ProductEntity), ArgumentType = typeof((string Id, decimal NewPrice, DateTime TimeStamp)), PropertyName = "UpdatePrice")] +[DynamoDBMarshaller(typeof(ProductEntity), ArgumentType = typeof(decimal), PropertyName = "QueryByPrice")] +public partial record ProductEntity( + [property: DynamoDBHashKey] string Id, + [property: DynamoDBGlobalSecondaryIndexHashKey(ProductEntity.PriceIndex)] + decimal Price, + string Description, + ProductEntity.MetadataEntity Metadata +) +{ + public const string PriceIndex = "PriceIndex"; + + public record MetadataEntity(DateTime CreatedAt, DateTime ModifiedAt); +} \ No newline at end of file diff --git a/samples/ConsoleApp/Program.cs b/samples/ConsoleApp/Program.cs index 5ad2c947..1534162a 100644 --- a/samples/ConsoleApp/Program.cs +++ b/samples/ConsoleApp/Program.cs @@ -1,4 +1,4 @@ -using DynamoDBGenerator.Attributes; +using Amazon.DynamoDBv2; namespace SampleApp; @@ -6,6 +6,7 @@ internal static class Program { public static void Main() { + ProductRepository productRepository = new ProductRepository("MY_TABLE", new AmazonDynamoDBClient()); + } - } \ No newline at end of file diff --git a/src/Dynatello/Builders/Extensions.cs b/src/Dynatello/Builders/Extensions.cs index aab1a500..c296fc54 100644 --- a/src/Dynatello/Builders/Extensions.cs +++ b/src/Dynatello/Builders/Extensions.cs @@ -30,6 +30,18 @@ this KeyConditionExpression source ); } + public static GetRequestBuilder ToGetRequestBuilder( + this TableAccess source) + where TReferences : IAttributeExpressionNameTracker + where TArgumentReferences : IAttributeExpressionValueTracker + where TArg : notnull + { + return new GetRequestBuilder( + source.TableName, + source.Item.PrimaryKeyMarshaller.ComposeKeys(y => y, null) + ); + } + public static GetRequestBuilder ToGetRequestBuilder( this TableAccess source, From b97d8f8eee82947a310be1f2fe379587b867fca8 Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Wed, 6 Mar 2024 17:26:55 +0100 Subject: [PATCH 15/16] Add range --- samples/ConsoleApp/ProductRepository.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/samples/ConsoleApp/ProductRepository.cs b/samples/ConsoleApp/ProductRepository.cs index 3716c94d..d2447100 100644 --- a/samples/ConsoleApp/ProductRepository.cs +++ b/samples/ConsoleApp/ProductRepository.cs @@ -105,9 +105,8 @@ public async Task Create(ProductEntity productEntity) [DynamoDBMarshaller(typeof(ProductEntity), ArgumentType = typeof((string Id, decimal NewPrice, DateTime TimeStamp)), PropertyName = "UpdatePrice")] [DynamoDBMarshaller(typeof(ProductEntity), ArgumentType = typeof(decimal), PropertyName = "QueryByPrice")] public partial record ProductEntity( - [property: DynamoDBHashKey] string Id, - [property: DynamoDBGlobalSecondaryIndexHashKey(ProductEntity.PriceIndex)] - decimal Price, + [property: DynamoDBHashKey, DynamoDBGlobalSecondaryIndexRangeKey(ProductEntity.PriceIndex)] string Id, + [property: DynamoDBGlobalSecondaryIndexHashKey(ProductEntity.PriceIndex)] decimal Price, string Description, ProductEntity.MetadataEntity Metadata ) From 912dc44fef1737ad7d2c6ca2f6998c9cf0ea21ca Mon Sep 17 00:00:00 2001 From: Robert Andersson Date: Tue, 26 Mar 2024 15:41:08 +0100 Subject: [PATCH 16/16] Update readme and sample --- DynamoDBGenerator.sln | 2 +- README.md | 12 +- samples/ConsoleApp/ProductEntity.cs | 4 - samples/ConsoleApp/ProductRepository.cs | 117 ------------------ samples/ConsoleApp/Program.cs | 12 -- .../DynatelloRepository.csproj} | 0 samples/DynatelloRepository/Program.cs | 114 +++++++++++++++++ 7 files changed, 126 insertions(+), 135 deletions(-) delete mode 100644 samples/ConsoleApp/ProductEntity.cs delete mode 100644 samples/ConsoleApp/ProductRepository.cs delete mode 100644 samples/ConsoleApp/Program.cs rename samples/{ConsoleApp/ConsoleApp.csproj => DynatelloRepository/DynatelloRepository.csproj} (100%) create mode 100644 samples/DynatelloRepository/Program.cs diff --git a/DynamoDBGenerator.sln b/DynamoDBGenerator.sln index 9e0f6233..563f44c6 100644 --- a/DynamoDBGenerator.sln +++ b/DynamoDBGenerator.sln @@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamoDBGenerator.SourceGenerator", "src\DynamoDBGenerator.SourceGenerator\DynamoDBGenerator.SourceGenerator.csproj", "{648B1DF4-9684-4422-95F5-74BB89862E4D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "samples\ConsoleApp\ConsoleApp.csproj", "{834A7F6C-3C82-427F-9BFB-9686672A5BDE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynatelloRepository", "samples\DynatelloRepository\DynatelloRepository.csproj", "{834A7F6C-3C82-427F-9BFB-9686672A5BDE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamoDBGenerator", "src\DynamoDBGenerator\DynamoDBGenerator.csproj", "{53F899A8-28AA-450F-9C62-FD478119B2B7}" EndProject diff --git a/README.md b/README.md index 26de8862..cb7fe38f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,15 @@ This source generator is crafted to simplify DynamoDB integration for your proje This project has not been tested in any real scenario and currently serves as a hobby project. ## Installation -Install the following packages from Nuget: +Theres two way to install this project either by the using `Dynatello` or by `DynamoDBGenerator.SourceGenerator` & `DynamoDBGenerator`. + +* `DynamoDBGenerator.SourceGenerator` & `DynamoDBGenerator` is the base functionality where you get reusable marshaller that's source generated. +* Dyntello extends the marshaller in order to create reusable generic request builders. See this [example](https://github.com/inputfalken/DynamoDB.SourceGenerator/blob/main/samples/DynatelloRepository/Program.cs). + * NOTE: If you install Dynatello, you do not need to specify `DynamoDBGenerator.SourceGenerator` & `DynamoDBGenerator` as dependencies. + +[![DynamoDBGenerator][5]][6] + +--- [![DynamoDBGenerator][1]][2] @@ -16,6 +24,8 @@ Install the following packages from Nuget: [2]: https://www.nuget.org/packages/DynamoDBGenerator [3]: https://img.shields.io/nuget/v/DynamoDBGenerator.SourceGenerator.svg?label=DynamoDBGenerator.SourceGenerator [4]: https://www.nuget.org/packages/DynamoDBGenerator.SourceGenerator +[5]: https://img.shields.io/nuget/v/Dynatello.svg?label=Dynatello +[6]: https://www.nuget.org/packages/Dynatello The `DynamoDBGenerator.SourceGenerator` is where the source generator is implemented. The source generator will look for attributes and implement interfaces that exists in `DynamoDBGenerator`. diff --git a/samples/ConsoleApp/ProductEntity.cs b/samples/ConsoleApp/ProductEntity.cs deleted file mode 100644 index 03c1ed95..00000000 --- a/samples/ConsoleApp/ProductEntity.cs +++ /dev/null @@ -1,4 +0,0 @@ -using Amazon.DynamoDBv2.DataModel; -using DynamoDBGenerator.Attributes; - -namespace SampleApp; diff --git a/samples/ConsoleApp/ProductRepository.cs b/samples/ConsoleApp/ProductRepository.cs deleted file mode 100644 index d2447100..00000000 --- a/samples/ConsoleApp/ProductRepository.cs +++ /dev/null @@ -1,117 +0,0 @@ -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; - -namespace SampleApp; - -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 = ProductEntity.GetById - .OnTable(tableName) - .ToGetRequestBuilder(argumentType => argumentType); // Since the ArgumentType is set to string, we don't need to select a property. - - _updatePrice = ProductEntity.UpdatePrice - .OnTable(tableName) - .WithUpdateExpression((x, y) => $"SET {x.Price} = {y.NewPrice}, {x.Metadata.ModifiedAt} = {y.TimeStamp}") // Specify the update operation - .ToUpdateItemRequestBuilder((x, y) => x.PartitionKey(y.Id)) - with - { - ReturnValues = ReturnValue.ALL_NEW - }; - - _createProduct = ProductEntity.PutProduct - .OnTable(tableName) - .WithConditionExpression((x, y) => $"{x.Id} <> {y.Id}") // Ensure we don't have an existing Product in DynamoDB - .ToPutRequestBuilder(); - - _queryByPrice = ProductEntity.QueryByPrice - .OnTable(tableName) - .WithKeyConditionExpression((x, y) => $"{x.Price} = {y}") - .ToQueryRequestBuilder() with - { - IndexName = ProductEntity.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 => ProductEntity.QueryByPrice.Unmarshall(x)) - .ToArray(); - } - - public async Task Create(ProductEntity productEntity) - { - PutItemRequest request = _createProduct.Build(productEntity); - 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("..."); - - ProductEntity productEntity = ProductEntity.GetById.Unmarshall(response.Item); - - return productEntity; - } - - 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; - - ProductEntity productEntity = ProductEntity.UpdatePrice.Unmarshall(response.Attributes); - - return productEntity; - } -} - -[DynamoDBMarshaller(typeof(ProductEntity), PropertyName = "PutProduct")] -[DynamoDBMarshaller(typeof(ProductEntity), ArgumentType = typeof(string), PropertyName = "GetById")] -[DynamoDBMarshaller(typeof(ProductEntity), ArgumentType = typeof((string Id, decimal NewPrice, DateTime TimeStamp)), PropertyName = "UpdatePrice")] -[DynamoDBMarshaller(typeof(ProductEntity), ArgumentType = typeof(decimal), PropertyName = "QueryByPrice")] -public partial record ProductEntity( - [property: DynamoDBHashKey, DynamoDBGlobalSecondaryIndexRangeKey(ProductEntity.PriceIndex)] string Id, - [property: DynamoDBGlobalSecondaryIndexHashKey(ProductEntity.PriceIndex)] decimal Price, - string Description, - ProductEntity.MetadataEntity Metadata -) -{ - public const string PriceIndex = "PriceIndex"; - - public record MetadataEntity(DateTime CreatedAt, DateTime ModifiedAt); -} \ No newline at end of file diff --git a/samples/ConsoleApp/Program.cs b/samples/ConsoleApp/Program.cs deleted file mode 100644 index 1534162a..00000000 --- a/samples/ConsoleApp/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Amazon.DynamoDBv2; - -namespace SampleApp; - -internal static class Program -{ - public static void Main() - { - ProductRepository productRepository = new ProductRepository("MY_TABLE", new AmazonDynamoDBClient()); - - } -} \ No newline at end of file diff --git a/samples/ConsoleApp/ConsoleApp.csproj b/samples/DynatelloRepository/DynatelloRepository.csproj similarity index 100% rename from samples/ConsoleApp/ConsoleApp.csproj rename to samples/DynatelloRepository/DynatelloRepository.csproj diff --git a/samples/DynatelloRepository/Program.cs b/samples/DynatelloRepository/Program.cs new file mode 100644 index 00000000..4644d670 --- /dev/null +++ b/samples/DynatelloRepository/Program.cs @@ -0,0 +1,114 @@ +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; + } +} + +[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); +}