|
| 1 | +# Design Document: Operation Samples Showcase |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This project provides a standalone .NET 8 class library containing compilable code samples that demonstrate FluentDynamoDb API usage compared to the raw AWS SDK for DynamoDB. The samples are designed for screenshots and video presentations, showing the verbosity reduction and API improvements offered by FluentDynamoDb. |
| 6 | + |
| 7 | +The project uses a simple Order/OrderLine domain with a single-table design pattern. Each DynamoDB operation type has a dedicated sample file containing four methods demonstrating different coding styles: |
| 8 | +1. Raw AWS SDK with explicit AttributeValue dictionaries |
| 9 | +2. FluentDynamoDb manual builder with WithAttribute()/WithValue() |
| 10 | +3. FluentDynamoDb formatted string with {0}, {1} placeholders |
| 11 | +4. FluentDynamoDb lambda expressions with entity accessors |
| 12 | + |
| 13 | +## Architecture |
| 14 | + |
| 15 | +``` |
| 16 | +examples/ |
| 17 | +└── OperationSamples/ |
| 18 | + ├── OperationSamples.csproj |
| 19 | + ├── Models/ |
| 20 | + │ ├── Order.cs |
| 21 | + │ ├── OrderLine.cs |
| 22 | + │ └── OrdersTable.cs |
| 23 | + └── Samples/ |
| 24 | + ├── GetSamples.cs |
| 25 | + ├── PutSamples.cs |
| 26 | + ├── UpdateSamples.cs |
| 27 | + ├── DeleteSamples.cs |
| 28 | + ├── QuerySamples.cs |
| 29 | + ├── ScanSamples.cs |
| 30 | + ├── TransactionGetSamples.cs |
| 31 | + ├── TransactionWriteSamples.cs |
| 32 | + ├── BatchGetSamples.cs |
| 33 | + └── BatchWriteSamples.cs |
| 34 | +``` |
| 35 | + |
| 36 | +The project is located under the `examples/` folder alongside other example projects (InvoiceManager, StoreLocator, TodoList, TransactionDemo). |
| 37 | + |
| 38 | +### DynamoDB Table Schema |
| 39 | + |
| 40 | +Single-table design with the following key structure: |
| 41 | +- **Table Name**: `Orders` |
| 42 | +- **Partition Key (pk)**: `ORDER#{OrderId}` |
| 43 | +- **Sort Key (sk)**: |
| 44 | + - `META` for Order metadata |
| 45 | + - `LINE#{LineId}` for OrderLine items |
| 46 | + |
| 47 | +### Sample Method Naming Convention |
| 48 | + |
| 49 | +Each sample file contains a static class with four methods following this pattern: |
| 50 | +- `RawSdk{Operation}Async` - Direct AWS SDK usage |
| 51 | +- `FluentManual{Operation}Async` - FluentDynamoDb with manual expressions |
| 52 | +- `FluentFormatted{Operation}Async` - FluentDynamoDb with format strings |
| 53 | +- `FluentLambda{Operation}Async` - FluentDynamoDb with lambda expressions |
| 54 | + |
| 55 | +## Components and Interfaces |
| 56 | + |
| 57 | +### Models |
| 58 | + |
| 59 | +#### Order Entity |
| 60 | +```csharp |
| 61 | +[DynamoDbEntity] |
| 62 | +public partial class Order : IDynamoDbEntity |
| 63 | +{ |
| 64 | + [PartitionKey] |
| 65 | + public string Pk { get; set; } // "ORDER#{OrderId}" |
| 66 | + |
| 67 | + [SortKey] |
| 68 | + public string Sk { get; set; } // "META" |
| 69 | + |
| 70 | + public string OrderId { get; set; } |
| 71 | + public string CustomerId { get; set; } |
| 72 | + public DateTime OrderDate { get; set; } |
| 73 | + public string Status { get; set; } // "Pending", "Shipped", "Delivered" |
| 74 | + public decimal TotalAmount { get; set; } |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +#### OrderLine Entity |
| 79 | +```csharp |
| 80 | +[DynamoDbEntity] |
| 81 | +public partial class OrderLine : IDynamoDbEntity |
| 82 | +{ |
| 83 | + [PartitionKey] |
| 84 | + public string Pk { get; set; } // "ORDER#{OrderId}" |
| 85 | + |
| 86 | + [SortKey] |
| 87 | + public string Sk { get; set; } // "LINE#{LineId}" |
| 88 | + |
| 89 | + public string LineId { get; set; } |
| 90 | + public string ProductId { get; set; } |
| 91 | + public string ProductName { get; set; } |
| 92 | + public int Quantity { get; set; } |
| 93 | + public decimal UnitPrice { get; set; } |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +#### OrdersTable |
| 98 | +```csharp |
| 99 | +public partial class OrdersTable : DynamoDbTableBase |
| 100 | +{ |
| 101 | + public OrdersTable(IAmazonDynamoDB client) : base(client, "Orders") { } |
| 102 | + |
| 103 | + // Generated entity accessors: |
| 104 | + // public OrderAccessor Orders { get; } |
| 105 | + // public OrderLineAccessor OrderLines { get; } |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +#### OrderUpdateModel (for lambda expression updates) |
| 110 | +```csharp |
| 111 | +/// <summary> |
| 112 | +/// Update model for Order entity used with lambda expression Set() operations. |
| 113 | +/// </summary> |
| 114 | +public class OrderUpdateModel |
| 115 | +{ |
| 116 | + [DynamoDbAttribute("orderStatus")] |
| 117 | + public string? Status { get; set; } |
| 118 | + |
| 119 | + [DynamoDbAttribute("modifiedAt")] |
| 120 | + public DateTime? ModifiedAt { get; set; } |
| 121 | + |
| 122 | + [DynamoDbAttribute("totalAmount")] |
| 123 | + public decimal? TotalAmount { get; set; } |
| 124 | +} |
| 125 | +``` |
| 126 | + |
| 127 | +### Sample File Structure |
| 128 | + |
| 129 | +Each sample file follows this structure: |
| 130 | +```csharp |
| 131 | +namespace FluentDynamoDb.OperationSamples.Samples; |
| 132 | + |
| 133 | +public static class {Operation}Samples |
| 134 | +{ |
| 135 | + // Returns AWS SDK response, then converts to domain model for equivalency |
| 136 | + public static async Task<TEntity> RawSdk{Operation}Async(IAmazonDynamoDB client, ...) { } |
| 137 | + public static async Task<TEntity> FluentManual{Operation}Async(OrdersTable table, ...) { } |
| 138 | + public static async Task<TEntity> FluentFormatted{Operation}Async(OrdersTable table, ...) { } |
| 139 | + public static async Task<TEntity> FluentLambda{Operation}Async(OrdersTable table, ...) { } |
| 140 | +} |
| 141 | +``` |
| 142 | + |
| 143 | +### Lambda Expression Patterns |
| 144 | + |
| 145 | +FluentLambda methods should demonstrate the full power of lambda expressions: |
| 146 | + |
| 147 | +#### Update Operations |
| 148 | +```csharp |
| 149 | +// Use Set(x => new UpdateModel { ... }) syntax |
| 150 | +await table.Orders.Update(pk, sk) |
| 151 | + .Set(x => new OrderUpdateModel |
| 152 | + { |
| 153 | + Status = newStatus, |
| 154 | + ModifiedAt = modifiedAt |
| 155 | + }) |
| 156 | + .UpdateAsync(); |
| 157 | +``` |
| 158 | + |
| 159 | +#### Condition Expressions |
| 160 | +```csharp |
| 161 | +// Use AttributeExists/AttributeNotExists via lambda |
| 162 | +await table.Orders.Put(order) |
| 163 | + .Where(x => x.Pk.AttributeNotExists()) |
| 164 | + .PutAsync(); |
| 165 | + |
| 166 | +await table.Orders.Update(pk, sk) |
| 167 | + .Where(x => x.Pk.AttributeExists()) |
| 168 | + .Set(x => new OrderUpdateModel { Status = newStatus }) |
| 169 | + .UpdateAsync(); |
| 170 | +``` |
| 171 | + |
| 172 | +#### Express-Route Methods |
| 173 | +```csharp |
| 174 | +// Use express-route methods instead of builder chains |
| 175 | +await table.Orders.PutAsync(order); // NOT: Put(order).PutAsync() |
| 176 | +var order = await table.Orders.GetAsync(pk, sk); // NOT: Get(pk, sk).GetItemAsync() |
| 177 | +await table.Orders.DeleteAsync(pk, sk); // NOT: Delete(pk, sk).DeleteAsync() |
| 178 | +``` |
| 179 | + |
| 180 | +### Response Handling Pattern |
| 181 | + |
| 182 | +Raw SDK methods should demonstrate full equivalency by converting responses to domain models: |
| 183 | + |
| 184 | +```csharp |
| 185 | +public static async Task<Order?> RawSdkGetAsync(IAmazonDynamoDB client, string orderId) |
| 186 | +{ |
| 187 | + var response = await client.GetItemAsync(request); |
| 188 | + |
| 189 | + if (response.Item == null || response.Item.Count == 0) |
| 190 | + return null; |
| 191 | + |
| 192 | + // Manual conversion to show equivalency |
| 193 | + return new Order |
| 194 | + { |
| 195 | + Pk = response.Item["pk"].S, |
| 196 | + Sk = response.Item["sk"].S, |
| 197 | + OrderId = response.Item["orderId"].S, |
| 198 | + // ... other properties |
| 199 | + }; |
| 200 | +} |
| 201 | +``` |
| 202 | + |
| 203 | +## Data Models |
| 204 | + |
| 205 | +### Key Patterns |
| 206 | + |
| 207 | +| Entity | Partition Key | Sort Key | |
| 208 | +|--------|--------------|----------| |
| 209 | +| Order | `ORDER#{OrderId}` | `META` | |
| 210 | +| OrderLine | `ORDER#{OrderId}` | `LINE#{LineId}` | |
| 211 | + |
| 212 | +### Sample Data Values |
| 213 | + |
| 214 | +For consistency across samples: |
| 215 | +- OrderId: `"ORD-001"`, `"ORD-002"` |
| 216 | +- LineId: `"LN-001"`, `"LN-002"` |
| 217 | +- CustomerId: `"CUST-123"` |
| 218 | +- Status values: `"Pending"`, `"Shipped"`, `"Delivered"` |
| 219 | + |
| 220 | +## Correctness Properties |
| 221 | + |
| 222 | +*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* |
| 223 | + |
| 224 | +Based on the prework analysis, most requirements for this showcase project are structural/stylistic and not amenable to property-based testing. The primary testable property is: |
| 225 | + |
| 226 | +### Property 1: Sample File Method Structure |
| 227 | +*For any* sample file in the Samples folder, the file SHALL contain exactly four public static async Task methods with names matching the patterns: `RawSdk*Async`, `FluentManual*Async`, `FluentFormatted*Async`, and `FluentLambda*Async`. |
| 228 | + |
| 229 | +**Validates: Requirements 1.1** |
| 230 | + |
| 231 | +### Verification Examples (Not Properties) |
| 232 | + |
| 233 | +The following requirements are verified through specific examples rather than universal properties: |
| 234 | + |
| 235 | +- **Build Verification**: Running `dotnet build` produces exit code 0 (Requirements 3.1) |
| 236 | +- **Project Configuration**: The csproj contains `<TargetFramework>net8.0</TargetFramework>` and `<Nullable>enable</Nullable>` (Requirements 3.2, 3.3) |
| 237 | +- **Model Files Exist**: `Order.cs` and `OrderLine.cs` exist in Models folder (Requirements 4.1) |
| 238 | +- **Namespace Consistency**: All files use `FluentDynamoDb.OperationSamples` namespace (Requirements 4.4) |
| 239 | +- **Sample Files Exist**: All 10 sample files exist (Requirements 5.1-5.10) |
| 240 | + |
| 241 | +## Error Handling |
| 242 | + |
| 243 | +Since this is a showcase project that doesn't execute against real AWS resources, error handling is minimal: |
| 244 | + |
| 245 | +- Methods may throw `NotImplementedException` only if absolutely necessary for compilation (avoided per requirements) |
| 246 | +- No try-catch blocks needed as code is for demonstration only |
| 247 | +- AWS SDK exceptions are not handled as the code won't execute |
| 248 | + |
| 249 | +## Testing Strategy |
| 250 | + |
| 251 | +### Dual Testing Approach |
| 252 | + |
| 253 | +Given the nature of this showcase project (compilable but non-executable code), testing focuses on: |
| 254 | + |
| 255 | +#### Unit Tests |
| 256 | +- Verify project compiles successfully with `dotnet build` |
| 257 | +- Verify all expected files exist in the correct locations |
| 258 | +- Verify namespace consistency across all files |
| 259 | + |
| 260 | +#### Property-Based Tests |
| 261 | + |
| 262 | +**Property-Based Testing Library**: FsCheck with xUnit integration |
| 263 | + |
| 264 | +**Property 1 Implementation**: Sample File Method Structure |
| 265 | +```csharp |
| 266 | +// Feature: operation-samples-showcase, Property 1: Sample File Method Structure |
| 267 | +// Validates: Requirements 1.1 |
| 268 | +[Property] |
| 269 | +public Property AllSampleFilesHaveFourMethods() |
| 270 | +{ |
| 271 | + // For each sample file, verify it contains exactly 4 methods |
| 272 | + // with the correct naming patterns |
| 273 | +} |
| 274 | +``` |
| 275 | + |
| 276 | +### Test Configuration |
| 277 | +- Property tests run minimum 100 iterations |
| 278 | +- Tests tagged with feature and property references per design requirements |
0 commit comments