diff --git a/README.md b/README.md index c4477f6..1cd3497 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,10 @@ Add the following NuGet package as a dependency to your project. [2]: https://www.nuget.org/packages/Dynatello ## Example -The code is also available in the [samples](samples/) directory. +All examples can be found through the links. ### Extend DTO's -Extend a DTO to contain the mashaller functionality. +[Extend a DTO to contain the mashaller functionality:](samples/ExtendDto) ```csharp using DynamoDBGenerator.Attributes; @@ -52,8 +52,9 @@ public partial record Cat(string Id, string Name, double Cuteness); ### Request middleware +[Every `IRequestHandler` supports middlewares:](samples/RequestPipeline) + -Every `RequestHandler` supports having multiple middlewares. ```csharp using System.Diagnostics; @@ -107,10 +108,16 @@ public partial record Cat(string Id, string Name, double Cuteness); ### Repository -Isolate DynamoDB code to a repository class. +[Through a repository pattern:](samples/Repository) ```csharp -public class EmployeeRepository +using Amazon.DynamoDBv2; +using Dynatello; +using Dynatello.Builders; +using Dynatello.Builders.Types; +using Dynatello.Handlers; + +public partial class EmployeeRepository { private readonly IRequestHandler<(string department, string email), Employee?> _getEmployee; private readonly IRequestHandler<(string department, string email), Employee?> _deleteEmployee; @@ -125,24 +132,36 @@ public class EmployeeRepository Employee? > _updateLastname; - public EmployeeRepository(string table, IAmazonDynamoDB dynamoDB) + private readonly IAmazonDynamoDB _database; + + public EmployeeRepository(IAmazonDynamoDB dynamoDB) { + _database = dynamoDB; + var middleware = new RequestLogAnalyzer(); _deleteEmployee = Employee - .GetEmployee.OnTable(table) + .GetEmployee.OnTable(TableName) .ToDeleteRequestHandler( x => x.ToDeleteRequestBuilder(y => y.department, y => y.email), - x => x.AmazonDynamoDB = dynamoDB + x => + { + x.AmazonDynamoDB = dynamoDB; + x.RequestsPipelines.Add(middleware); + } ); _getEmployee = Employee - .GetEmployee.OnTable(table) + .GetEmployee.OnTable(TableName) .ToGetRequestHandler( x => x.ToGetRequestBuilder(y => y.department, y => y.email), - x => x.AmazonDynamoDB = dynamoDB + x => + { + x.AmazonDynamoDB = dynamoDB; + x.RequestsPipelines.Add(middleware); + } ); _queryByEmail = Employee - .GetByEmail.OnTable(table) + .GetByEmail.OnTable(TableName) .ToQueryRequestHandler( x => x.WithKeyConditionExpression((x, y) => $"{x.Email} = {y} ") @@ -150,15 +169,30 @@ public class EmployeeRepository { IndexName = "EmailLookup", }, - x => x.AmazonDynamoDB = dynamoDB + x => + { + x.AmazonDynamoDB = dynamoDB; + x.RequestsPipelines.Add(middleware); + } ); _createEmployee = Employee - .Create.OnTable(table) - .ToPutRequestHandler(x => x.ToPutRequestBuilder(), x => x.AmazonDynamoDB = dynamoDB); + .Create.OnTable(TableName) + .ToPutRequestHandler( + x => + x.WithConditionExpression( + (x, y) => $"{x.Department} <> {y.Department} AND {x.Email} <> {y.Email}" + ) + .ToPutRequestBuilder(), + x => + { + x.AmazonDynamoDB = dynamoDB; + x.RequestsPipelines.Add(middleware); + } + ); _queryByDepartment = Employee - .Query.OnTable(table) + .Query.OnTable(TableName) .ToQueryRequestHandler( x => x.WithKeyConditionExpression( @@ -177,7 +211,7 @@ public class EmployeeRepository ); _updateLastname = Employee - .UpdateLastname.OnTable(table) + .UpdateLastname.OnTable(TableName) .ToUpdateRequestHandler( x => x.WithUpdateExpression((x, y) => $"SET {x.LastName} = {y.NewLastname}") @@ -185,12 +219,14 @@ public class EmployeeRepository { ReturnValues = ReturnValue.ALL_NEW, }, - x => x.AmazonDynamoDB = dynamoDB + x => + { + x.AmazonDynamoDB = dynamoDB; + x.RequestsPipelines.Add(middleware); + } ); } - private static Fixture Fixture = new(); - public Task GetPersonById( string department, string email, @@ -224,47 +260,5 @@ public class EmployeeRepository string email, CancellationToken cancellationToken ) => _deleteEmployee.Send((department, email), cancellationToken); - - public async Task GenerateEmployeesInDeparment( - string department, - int count, - CancellationToken cancellationToken - ) - { - for (var i = 0; i < count; i++) - { - var employee = Fixture.Create() with - { - Department = department, - Metadata = new Metadata(DateTime.UtcNow), - }; - - await _createEmployee.Send(employee, cancellationToken); - } - } } - -[DynamoDBMarshaller(AccessName = "GetByEmail", ArgumentType = typeof(string))] -[DynamoDBMarshaller( - AccessName = "GetEmployee", - ArgumentType = typeof((string department, string email)) -)] -[DynamoDBMarshaller( - AccessName = "Query", - ArgumentType = typeof((string Department, string EmailPrefix, DateTime MustBeLessThan)) -)] -[DynamoDBMarshaller(AccessName = "Create")] -[DynamoDBMarshaller( - AccessName = "UpdateLastname", - ArgumentType = typeof((string Department, string Email, string NewLastname)) -)] -public partial record Employee( - [property: DynamoDBHashKey] string Department, - [property: DynamoDBRangeKey, DynamoDBGlobalSecondaryIndexHashKey("EmailLookup")] string Email, - string LastName, - string[] Skills, - Metadata Metadata -); - -public record Metadata(DateTime Timestamp); ``` diff --git a/samples/Repository/DynamoDBEmployeeRepository.cs b/samples/Repository/DynamoDBEmployeeRepository.cs new file mode 100644 index 0000000..83cad4d --- /dev/null +++ b/samples/Repository/DynamoDBEmployeeRepository.cs @@ -0,0 +1,150 @@ +using Amazon.DynamoDBv2; +using Dynatello; +using Dynatello.Builders; +using Dynatello.Builders.Types; +using Dynatello.Handlers; + +public partial class DynamoDBEmployeeRepository : IEmployeeRepository +{ + private readonly IRequestHandler<(string department, string email), Employee?> _getEmployee; + private readonly IRequestHandler<(string department, string email), Employee?> _deleteEmployee; + private readonly IRequestHandler> _queryByEmail; + private readonly IRequestHandler _createEmployee; + private readonly IRequestHandler< + (string Department, string EmailPrefix, DateTime MustBeLessThan), + IReadOnlyList + > _queryByDepartment; + private readonly IRequestHandler< + (string Department, string Email, string NewLastname), + Employee? + > _updateLastname; + + private readonly IAmazonDynamoDB _database; + + public DynamoDBEmployeeRepository(IAmazonDynamoDB dynamoDB) + { + _database = dynamoDB; + var middleware = new RequestLogAnalyzer(); + _deleteEmployee = Employee + .GetEmployee.OnTable(TableName) + .ToDeleteRequestHandler( + x => x.ToDeleteRequestBuilder(y => y.department, y => y.email), + x => + { + x.AmazonDynamoDB = dynamoDB; + x.RequestsPipelines.Add(middleware); + } + ); + + _getEmployee = Employee + .GetEmployee.OnTable(TableName) + .ToGetRequestHandler( + x => x.ToGetRequestBuilder(y => y.department, y => y.email), + x => + { + x.AmazonDynamoDB = dynamoDB; + x.RequestsPipelines.Add(middleware); + } + ); + + _queryByEmail = Employee + .GetByEmail.OnTable(TableName) + .ToQueryRequestHandler( + x => + x.WithKeyConditionExpression((x, y) => $"{x.Email} = {y} ") + .ToQueryRequestBuilder() with + { + IndexName = "EmailLookup", + }, + x => + { + x.AmazonDynamoDB = dynamoDB; + x.RequestsPipelines.Add(middleware); + } + ); + + _createEmployee = Employee + .Create.OnTable(TableName) + .ToPutRequestHandler( + x => + x.WithConditionExpression( + (x, y) => $"{x.Department} <> {y.Department} AND {x.Email} <> {y.Email}" + ) + .ToPutRequestBuilder(), + x => + { + x.AmazonDynamoDB = dynamoDB; + x.RequestsPipelines.Add(middleware); + } + ); + + _queryByDepartment = Employee + .Query.OnTable(TableName) + .ToQueryRequestHandler( + x => + x.WithKeyConditionExpression( + (x, y) => + $"{x.Department} = {y.Department} and begins_with({x.Email}, {y.EmailPrefix})" + ) + .WithFilterExpression( + (x, y) => $"{x.Metadata.Timestamp} < {y.MustBeLessThan}" + ) + .ToQueryRequestBuilder(), + x => + { + x.RequestsPipelines.Add(new RequestLogAnalyzer()); + x.AmazonDynamoDB = dynamoDB; + } + ); + + _updateLastname = Employee + .UpdateLastname.OnTable(TableName) + .ToUpdateRequestHandler( + x => + x.WithUpdateExpression((x, y) => $"SET {x.LastName} = {y.NewLastname}") + .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.Department, y.Email)) with + { + ReturnValues = ReturnValue.ALL_NEW, + }, + x => + { + x.AmazonDynamoDB = dynamoDB; + x.RequestsPipelines.Add(middleware); + } + ); + } + + public Task GetPersonById( + string department, + string email, + CancellationToken cancellationToken + ) => _getEmployee.Send((department, email), cancellationToken); + + public Task> SearchByEmail( + string email, + CancellationToken cancellationToken + ) => _queryByEmail.Send(email, cancellationToken); + + public Task CreateEmployee(Employee employee, CancellationToken cancellationToken) => + _createEmployee.Send(employee, cancellationToken); + + public Task> QueryByDepartment( + string department, + string emailStartsWith, + DateTime updatedBefore, + CancellationToken cancellationToken + ) => _queryByDepartment.Send((department, emailStartsWith, updatedBefore), cancellationToken); + + public Task UpdateLastName( + string department, + string email, + string lastname, + CancellationToken cancellationToken + ) => _updateLastname.Send((department, email, lastname), cancellationToken); + + public Task DeleteEmployee( + string department, + string email, + CancellationToken cancellationToken + ) => _deleteEmployee.Send((department, email), cancellationToken); +} diff --git a/samples/Repository/DynamoDBEmployeeRepositoryMeta.cs b/samples/Repository/DynamoDBEmployeeRepositoryMeta.cs new file mode 100644 index 0000000..64d6673 --- /dev/null +++ b/samples/Repository/DynamoDBEmployeeRepositoryMeta.cs @@ -0,0 +1,79 @@ +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using AutoFixture; + +public partial class DynamoDBEmployeeRepository +{ + public const string TableName = "Employees"; + + public async Task GenerateEmployeesInDeparment( + string department, + int count, + CancellationToken cancellationToken + ) + { + var fixture = new Fixture(); + for (var i = 0; i < count; i++) + { + var employee = fixture.Create() with + { + Department = department, + Metadata = new Metadata(DateTime.UtcNow), + }; + + await _createEmployee.Send(employee, cancellationToken); + } + } + + public async Task EnsureTableIsCreated(CancellationToken cancellationToken) + { + try + { + await _database.DescribeTableAsync(TableName, cancellationToken); + + return true; + } + catch (ResourceNotFoundException) + { + await _database.CreateTableAsync( + new CreateTableRequest() + { + TableName = TableName, + AttributeDefinitions = new List() + { + new AttributeDefinition() + { + AttributeName = nameof(Employee.Department), + AttributeType = "S", + }, + new AttributeDefinition() + { + AttributeName = nameof(Employee.Email), + AttributeType = "S", + }, + }, + KeySchema = new List() + { + new KeySchemaElement() + { + AttributeName = nameof(Employee.Department), + KeyType = KeyType.HASH, + }, + new KeySchemaElement() + { + AttributeName = nameof(Employee.Email), + KeyType = KeyType.RANGE, + }, + }, + ProvisionedThroughput = new ProvisionedThroughput() + { + ReadCapacityUnits = 5, + WriteCapacityUnits = 5, + }, + }, + cancellationToken + ); + return false; + } + } +} diff --git a/samples/Repository/Employee.cs b/samples/Repository/Employee.cs new file mode 100644 index 0000000..02a235c --- /dev/null +++ b/samples/Repository/Employee.cs @@ -0,0 +1,26 @@ +using Amazon.DynamoDBv2.DataModel; +using DynamoDBGenerator.Attributes; + +[DynamoDBMarshaller(AccessName = "GetByEmail", ArgumentType = typeof(string))] +[DynamoDBMarshaller( + AccessName = "GetEmployee", + ArgumentType = typeof((string department, string email)) +)] +[DynamoDBMarshaller( + AccessName = "Query", + ArgumentType = typeof((string Department, string EmailPrefix, DateTime MustBeLessThan)) +)] +[DynamoDBMarshaller(AccessName = "Create")] +[DynamoDBMarshaller( + AccessName = "UpdateLastname", + ArgumentType = typeof((string Department, string Email, string NewLastname)) +)] +public partial record Employee( + [property: DynamoDBHashKey] string Department, + [property: DynamoDBRangeKey, DynamoDBGlobalSecondaryIndexHashKey("EmailLookup")] string Email, + string LastName, + string[] Skills, + Metadata Metadata +); + +public record Metadata(DateTime Timestamp); diff --git a/samples/Repository/IEmployeeRepository.cs b/samples/Repository/IEmployeeRepository.cs new file mode 100644 index 0000000..a3ad478 --- /dev/null +++ b/samples/Repository/IEmployeeRepository.cs @@ -0,0 +1,35 @@ +public interface IEmployeeRepository +{ + public Task GetPersonById( + string department, + string email, + CancellationToken cancellationToken + ); + + public Task> SearchByEmail( + string email, + CancellationToken cancellationToken + ); + + public Task CreateEmployee(Employee employee, CancellationToken cancellationToken); + + public Task> QueryByDepartment( + string department, + string emailStartsWith, + DateTime updatedBefore, + CancellationToken cancellationToken + ); + + public Task UpdateLastName( + string department, + string email, + string lastname, + CancellationToken cancellationToken + ); + + public Task DeleteEmployee( + string department, + string email, + CancellationToken cancellationToken + ); +} diff --git a/samples/Repository/Program.cs b/samples/Repository/Program.cs index 375879d..12a50a7 100644 --- a/samples/Repository/Program.cs +++ b/samples/Repository/Program.cs @@ -1,100 +1,13 @@ -using System.Diagnostics; -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.DataModel; -using Amazon.DynamoDBv2.Model; -using Amazon.Runtime; -using AutoFixture; -using DynamoDBGenerator.Attributes; -using Dynatello; -using Dynatello.Builders; -using Dynatello.Builders.Types; -using Dynatello.Handlers; -using Dynatello.Pipelines; +using Amazon.DynamoDBv2; internal class Program { - const string TableName = "Employees"; - private static readonly IAmazonDynamoDB DynamoDB = new AmazonDynamoDBClient( - new AmazonDynamoDBConfig() { ServiceURL = "http://localhost:8000" } - ); - - public static async Task EnsureTableIsCreated(CancellationToken cancellationToken) - { - try - { - await DynamoDB.DescribeTableAsync(TableName, cancellationToken); - - return true; - } - catch (ResourceNotFoundException) - { - await DynamoDB.CreateTableAsync( - new CreateTableRequest() - { - TableName = TableName, - AttributeDefinitions = new List() - { - new AttributeDefinition() - { - AttributeName = nameof(Employee.Department), - AttributeType = "S", - }, - new AttributeDefinition() - { - AttributeName = nameof(Employee.Email), - AttributeType = "S", - }, - }, - KeySchema = new List() - { - new KeySchemaElement() - { - AttributeName = nameof(Employee.Department), - KeyType = KeyType.HASH, - }, - new KeySchemaElement() - { - AttributeName = nameof(Employee.Email), - KeyType = KeyType.RANGE, - }, - }, - ProvisionedThroughput = new ProvisionedThroughput() - { - ReadCapacityUnits = 5, - WriteCapacityUnits = 5, - }, - }, - cancellationToken - ); - return false; - } - } - - public static async Task QuerySalesDepartment( - EmployeeRepository repository, - CancellationToken cancellationToken - ) - { - Console.WriteLine("Querying sales department"); - var salesDepartment = await repository.QueryByDepartment( - "Sales", - "Email1", - DateTime.UtcNow, - cancellationToken - ); - - foreach (var x in salesDepartment) - { - Console.WriteLine(x); - } - } - public static async Task CreateDepartments( - EmployeeRepository repository, + DynamoDBEmployeeRepository repository, CancellationToken cancellationToken ) { - if (await EnsureTableIsCreated(cancellationToken) is false) + if (await repository.EnsureTableIsCreated(cancellationToken) is false) { await repository.GenerateEmployeesInDeparment("IT", 100, cancellationToken); await repository.GenerateEmployeesInDeparment("Sales", 100, cancellationToken); @@ -104,10 +17,28 @@ CancellationToken cancellationToken private static async Task Main(string[] args) { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); - var repository = new EmployeeRepository(TableName, DynamoDB); + var dynamoDb = new AmazonDynamoDBClient( + new AmazonDynamoDBConfig() { ServiceURL = "http://localhost:8000" } + ); + var repository = new DynamoDBEmployeeRepository(dynamoDb); await CreateDepartments(repository, cts.Token); - await QuerySalesDepartment(repository, cts.Token); + + await UseRepository(repository, cts.Token); + } + + private static async Task UseRepository( + IEmployeeRepository repository, + CancellationToken cancellationToken + ) + { + Console.WriteLine("Querying employees"); + var salesDepartment = await repository.QueryByDepartment( + "Sales", + "Email1", + DateTime.UtcNow, + cancellationToken + ); Console.WriteLine("Creating employee"); await repository.CreateEmployee( @@ -118,242 +49,33 @@ await repository.CreateEmployee( new[] { "Software Development" }, new Metadata(DateTime.UtcNow) ), - cts.Token + cancellationToken ); Console.WriteLine("Getting employee"); - var employee = await repository.GetPersonById("IT", "someEmail@test.com", cts.Token); - Console.WriteLine(employee); + var employee = await repository.GetPersonById( + "IT", + "someEmail@test.com", + cancellationToken + ); - Console.WriteLine("Updating employee"); var updatedEmployee = await repository.UpdateLastName( employee!.Department, employee.Email, "Sparrow", - cts.Token + cancellationToken ); - Console.WriteLine(updatedEmployee); Console.WriteLine("Deleting employee"); await repository.DeleteEmployee( updatedEmployee!.Department, updatedEmployee.Email, - cts.Token - ); - } -} - -public class RequestLogAnalyzer : IRequestPipeLine -{ - public async Task Invoke( - RequestContext requestContext, - Func> continuation - ) - { - var stopwatch = Stopwatch.StartNew(); - if (requestContext.Request is QueryRequest qr) - { - Console.WriteLine($"Performing {nameof(QueryRequest)} with the following params:"); - foreach (var x in qr.ExpressionAttributeNames) - { - Console.WriteLine($"REF: '{x.Key}', NAME: '{x.Value}'"); - } - - foreach (var x in qr.ExpressionAttributeValues) - { - KeyValuePair f = string.IsNullOrWhiteSpace(x.Value.S) - ? new KeyValuePair(x.Key, x.Value) - : new KeyValuePair(x.Key, x.Value.S); - Console.WriteLine($"VALUE: {(f)}"); - } - - Console.WriteLine($"KeyConditionExpression: '{qr.KeyConditionExpression}'"); - Console.WriteLine($"FilterExpression: '{qr.FilterExpression}'"); - } - - if (requestContext.Request is UpdateItemRequest uir) - { - Console.WriteLine($"Performing {nameof(QueryRequest)} with the following params:"); - foreach (var x in uir.ExpressionAttributeNames) - { - Console.WriteLine($"REF: '{x.Key}', NAME: '{x.Value}'"); - } - - foreach (var x in uir.ExpressionAttributeValues) - { - KeyValuePair f = string.IsNullOrWhiteSpace(x.Value.S) - ? new KeyValuePair(x.Key, x.Value) - : new KeyValuePair(x.Key, x.Value.S); - Console.WriteLine($"VALUE: {(f)}"); - } - Console.WriteLine($"UpdateExpression: '{uir.UpdateExpression}'"); - Console.WriteLine($"ConditionExpression: '{uir.ConditionExpression}'"); - } - - var response = await continuation(requestContext); - - Console.WriteLine( - $"{requestContext.Request.GetType().Name} finished after '{stopwatch.Elapsed}'." + cancellationToken ); - return response; + Console.WriteLine($"Original: {employee}"); + Console.WriteLine($"Updated: {updatedEmployee}"); + foreach (var salesEmployee in salesDepartment) + Console.WriteLine($"Queried: {salesEmployee}"); } } - -public class EmployeeRepository -{ - private readonly IRequestHandler<(string department, string email), Employee?> _getEmployee; - private readonly IRequestHandler<(string department, string email), Employee?> _deleteEmployee; - private readonly IRequestHandler> _queryByEmail; - private readonly IRequestHandler _createEmployee; - private readonly IRequestHandler< - (string Department, string EmailPrefix, DateTime MustBeLessThan), - IReadOnlyList - > _queryByDepartment; - private readonly IRequestHandler< - (string Department, string Email, string NewLastname), - Employee? - > _updateLastname; - - public EmployeeRepository(string table, IAmazonDynamoDB dynamoDB) - { - _deleteEmployee = Employee - .GetEmployee.OnTable(table) - .ToDeleteRequestHandler( - x => x.ToDeleteRequestBuilder(y => y.department, y => y.email), - x => x.AmazonDynamoDB = dynamoDB - ); - - _getEmployee = Employee - .GetEmployee.OnTable(table) - .ToGetRequestHandler( - x => x.ToGetRequestBuilder(y => y.department, y => y.email), - x => x.AmazonDynamoDB = dynamoDB - ); - - _queryByEmail = Employee - .GetByEmail.OnTable(table) - .ToQueryRequestHandler( - x => - x.WithKeyConditionExpression((x, y) => $"{x.Email} = {y} ") - .ToQueryRequestBuilder() with - { - IndexName = "EmailLookup", - }, - x => x.AmazonDynamoDB = dynamoDB - ); - - _createEmployee = Employee - .Create.OnTable(table) - .ToPutRequestHandler(x => x.ToPutRequestBuilder(), x => x.AmazonDynamoDB = dynamoDB); - - _queryByDepartment = Employee - .Query.OnTable(table) - .ToQueryRequestHandler( - x => - x.WithKeyConditionExpression( - (x, y) => - $"{x.Department} = {y.Department} and begins_with({x.Email}, {y.EmailPrefix})" - ) - .WithFilterExpression( - (x, y) => $"{x.Metadata.Timestamp} < {y.MustBeLessThan}" - ) - .ToQueryRequestBuilder(), - x => - { - x.RequestsPipelines.Add(new RequestLogAnalyzer()); - x.AmazonDynamoDB = dynamoDB; - } - ); - - _updateLastname = Employee - .UpdateLastname.OnTable(table) - .ToUpdateRequestHandler( - x => - x.WithUpdateExpression((x, y) => $"SET {x.LastName} = {y.NewLastname}") - .ToUpdateItemRequestBuilder((x, y) => x.Keys(y.Department, y.Email)) with - { - ReturnValues = ReturnValue.ALL_NEW, - }, - x => x.AmazonDynamoDB = dynamoDB - ); - } - - private static Fixture Fixture = new(); - - public Task GetPersonById( - string department, - string email, - CancellationToken cancellationToken - ) => _getEmployee.Send((department, email), cancellationToken); - - public Task> SearchByEmail( - string email, - CancellationToken cancellationToken - ) => _queryByEmail.Send(email, cancellationToken); - - public Task CreateEmployee(Employee employee, CancellationToken cancellationToken) => - _createEmployee.Send(employee, cancellationToken); - - public Task> QueryByDepartment( - string department, - string emailStartsWith, - DateTime updatedBefore, - CancellationToken cancellationToken - ) => _queryByDepartment.Send((department, emailStartsWith, updatedBefore), cancellationToken); - - public Task UpdateLastName( - string department, - string email, - string lastname, - CancellationToken cancellationToken - ) => _updateLastname.Send((department, email, lastname), cancellationToken); - - public Task DeleteEmployee( - string department, - string email, - CancellationToken cancellationToken - ) => _deleteEmployee.Send((department, email), cancellationToken); - - public async Task GenerateEmployeesInDeparment( - string department, - int count, - CancellationToken cancellationToken - ) - { - for (var i = 0; i < count; i++) - { - var employee = Fixture.Create() with - { - Department = department, - Metadata = new Metadata(DateTime.UtcNow), - }; - - await _createEmployee.Send(employee, cancellationToken); - } - } -} - -[DynamoDBMarshaller(AccessName = "GetByEmail", ArgumentType = typeof(string))] -[DynamoDBMarshaller( - AccessName = "GetEmployee", - ArgumentType = typeof((string department, string email)) -)] -[DynamoDBMarshaller( - AccessName = "Query", - ArgumentType = typeof((string Department, string EmailPrefix, DateTime MustBeLessThan)) -)] -[DynamoDBMarshaller(AccessName = "Create")] -[DynamoDBMarshaller( - AccessName = "UpdateLastname", - ArgumentType = typeof((string Department, string Email, string NewLastname)) -)] -public partial record Employee( - [property: DynamoDBHashKey] string Department, - [property: DynamoDBRangeKey, DynamoDBGlobalSecondaryIndexHashKey("EmailLookup")] string Email, - string LastName, - string[] Skills, - Metadata Metadata -); - -public record Metadata(DateTime Timestamp); diff --git a/samples/Repository/README.md b/samples/Repository/README.md index ae471a8..3ca71dc 100644 --- a/samples/Repository/README.md +++ b/samples/Repository/README.md @@ -1 +1,3 @@ -Tested with DynamoDB docker container +Works with DynamoDB through docker with the url `http://localhost:8000`. + +Running this program will print how the requests are being built towards DynamoDB and what's returned. diff --git a/samples/Repository/RequestLogAnalyzer.cs b/samples/Repository/RequestLogAnalyzer.cs new file mode 100644 index 0000000..e01d520 --- /dev/null +++ b/samples/Repository/RequestLogAnalyzer.cs @@ -0,0 +1,150 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Amazon.DynamoDBv2.Model; +using Amazon.Runtime; +using Dynatello.Pipelines; + +public class RequestLogAnalyzer : IRequestPipeLine +{ + private static void WriteLine(string? input) + { + if (input is null) + return; + + Console.WriteLine(input); + } + + private static string? StringifyExpression( + string? expression, + [CallerArgumentExpression("expression")] string? message = null + ) + { + if (expression is null) + { + return null; + } + return $"{message} = {expression}"; + } + + private IEnumerable StringifyAttributes( + IReadOnlyDictionary? items, + [CallerArgumentExpression("items")] string? expression = null + ) + { + if (items is null) + yield break; + + foreach (var item in items) + yield return $"{expression}[{item.Key}] = {item.Value}"; + } + + private IEnumerable StringifyAttributes( + IReadOnlyDictionary? items, + [CallerArgumentExpression("items")] string? expression = null + ) + { + if (items is null) + yield break; + + foreach (var item in items) + yield return $"{expression}[{item.Key}] = {FromAttributeValue(item.Value)}"; + + static string FromAttributeValue(AttributeValue av) + { + return av switch + { + { } a when string.IsNullOrWhiteSpace(a.S) is false => a.S, + { } a when string.IsNullOrWhiteSpace(a.N) is false => a.N, + _ => av.ToString() ?? "N/A", + }; + } + } + + private void OnQueryRequest(QueryRequest queryRequest) + { + foreach (var x in StringifyAttributes(queryRequest.ExpressionAttributeNames)) + WriteLine(x); + + foreach (var x in StringifyAttributes(queryRequest.ExpressionAttributeValues)) + WriteLine(x); + + WriteLine(StringifyExpression(queryRequest.KeyConditionExpression)); + WriteLine(StringifyExpression(queryRequest.FilterExpression)); + } + + private void OnUpdateItemRequest(UpdateItemRequest updateItemRequest) + { + foreach (var x in StringifyAttributes(updateItemRequest.ExpressionAttributeNames)) + WriteLine(x); + + foreach (var x in StringifyAttributes(updateItemRequest.ExpressionAttributeValues)) + WriteLine(x); + + WriteLine(StringifyExpression(updateItemRequest.ConditionExpression)); + WriteLine(StringifyExpression(updateItemRequest.UpdateExpression)); + } + + private void OnPutItemRequest(PutItemRequest putItemRequest) + { + foreach (var x in StringifyAttributes(putItemRequest.ExpressionAttributeNames)) + WriteLine(x); + + foreach (var x in StringifyAttributes(putItemRequest.ExpressionAttributeValues)) + WriteLine(x); + + foreach (var x in StringifyAttributes(putItemRequest.Item)) + WriteLine(x); + + WriteLine(StringifyExpression(putItemRequest.ConditionExpression)); + } + + private void OnGetItemRequest(GetItemRequest getItemRequest) + { + foreach (var x in StringifyAttributes(getItemRequest.Key)) + WriteLine(x); + } + + public async Task Invoke( + RequestContext requestContext, + Func> continuation + ) + { + WriteLine($"===== {requestContext.Request.GetType().Name} started ====="); + + ( + (Action)( + requestContext.Request switch + { + QueryRequest x => () => OnQueryRequest(x), + UpdateItemRequest x => () => OnUpdateItemRequest(x), + PutItemRequest x => () => OnPutItemRequest(x), + GetItemRequest x => () => OnGetItemRequest(x), + DeleteItemRequest x => () => OnDeleteItemRequest(x), + _ => () => { }, + } + ) + )(); + + var stopwatch = Stopwatch.StartNew(); + var response = await continuation(requestContext); + WriteLine( + $"===== {requestContext.Request.GetType().Name} finished after '{stopwatch.Elapsed}' =====" + ); + + return response; + } + + private void OnDeleteItemRequest(DeleteItemRequest deleteItemRequest) + { + foreach (var x in StringifyAttributes(deleteItemRequest.Key)) + WriteLine(x); + + foreach (var x in StringifyAttributes(deleteItemRequest.ExpressionAttributeValues)) + WriteLine(x); + + foreach (var x in StringifyAttributes(deleteItemRequest.ExpressionAttributeNames)) + WriteLine(x); + + StringifyExpression(deleteItemRequest.ConditionExpression); + } +}