DynamoDB Integration with FluentDynamoDB
Amazon DynamoDB is a fully managed NoSQL database service that pairs perfectly with AWS Lambda for building serverless applications. With FluentDynamoDB, you can interact with DynamoDB using a clean, fluent API with source-generated strongly-typed entities.
DynamoDB is designed for serverless workloads—it scales automatically, requires no connection pooling, and offers single-digit millisecond latency. Combined with Lambda, you get a fully managed, pay-per-use backend with no servers to maintain.
Prerequisites
- Completed the OpenAPI Generation tutorial (recommended)
- .NET 8 SDK or later
- AWS account with DynamoDB access
- Basic understanding of NoSQL concepts
What FluentDynamoDB Provides
FluentDynamoDB simplifies DynamoDB operations with source-generated code and a fluent interface:
| AWS SDK Approach | FluentDynamoDB |
|---|---|
| Verbose attribute dictionaries | Source-generated strongly-typed entities |
| Manual key construction | Automatic key handling with prefixes |
| Complex query expressions | Lambda expression support |
| Scattered error handling | Optional FluentResults integration |
| Manual pagination | Built-in pagination with tokens |
Project Setup
Add FluentDynamoDB to your existing Lambda project:
dotnet add package Oproto.FluentDynamoDb
dotnet add package AWSSDK.DynamoDBv2
Code Example
Here's the product API from previous tutorials, now with DynamoDB persistence using FluentDynamoDB:
The Product Entity with FluentDynamoDB Attributes
using Oproto.FluentDynamoDb.Attributes;
using Oproto.Lambda.OpenApi.Attributes;
namespace MyLambdaApi.Models;
/// <summary>
/// Product entity with FluentDynamoDB attributes.
/// The partial keyword enables source generation.
/// </summary>
[DynamoDbTable("Products")]
[Scannable] // Required for Scan() operations
public partial class Product
{
[PartitionKey]
[DynamoDbAttribute("pk")]
[OpenApiSchema(Description = "Unique product identifier", Format = "uuid")]
public string Id { get; set; } = string.Empty;
[DynamoDbAttribute("name")]
[OpenApiSchema(Description = "Product name", MinLength = 1, MaxLength = 200)]
public string Name { get; set; } = string.Empty;
[DynamoDbAttribute("price")]
[OpenApiSchema(Description = "Price in USD", Minimum = 0)]
public decimal Price { get; set; }
[DynamoDbAttribute("description")]
[OpenApiSchema(Description = "Product description")]
public string Description { get; set; } = string.Empty;
[GlobalSecondaryIndex("category-index", IsPartitionKey = true)]
[DynamoDbAttribute("category")]
[OpenApiSchema(Description = "Product category for filtering")]
public string Category { get; set; } = string.Empty;
[DynamoDbAttribute("createdAt")]
[OpenApiSchema(Description = "Timestamp when product was created")]
public DateTime CreatedAt { get; set; }
}
The Lambda Functions with FluentDynamoDB Integration
using Amazon.Lambda.Core;
using Amazon.Lambda.Annotations;
using Amazon.Lambda.Annotations.APIGateway;
using Amazon.DynamoDBv2;
using Oproto.FluentDynamoDb;
using Oproto.Lambda.OpenApi.Attributes;
using MyLambdaApi.Models;
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace MyLambdaApi;
[OpenApiTag("Products")]
public class Functions
{
private readonly ProductsTable _table;
public Functions()
{
var client = new AmazonDynamoDBClient();
_table = new ProductsTable(client, "Products", new FluentDynamoDbOptions());
}
/// <summary>
/// Get a product by ID from DynamoDB
/// </summary>
[LambdaFunction]
[HttpApi(LambdaHttpMethod.Get, "/products/{id}")]
[OpenApiOperation(Summary = "Get product by ID",
Description = "Retrieves a single product from DynamoDB by its unique identifier")]
[OpenApiResponseType(typeof(Product), 200, Description = "Returns the product")]
[OpenApiResponseType(typeof(ErrorResponse), 404, Description = "Product not found")]
public async Task<IHttpResult> GetProduct(
[FromRoute] string id,
ILambdaContext context)
{
context.Logger.LogInformation($"Getting product with ID: {id}");
// FluentDynamoDB: Simple get by partition key
var product = await _table.Products.GetAsync(id);
if (product == null)
{
return HttpResults.NotFound(new ErrorResponse
{
Message = $"Product with ID '{id}' not found"
});
}
return HttpResults.Ok(product);
}
/// <summary>
/// Create a new product in DynamoDB
/// </summary>
[LambdaFunction]
[HttpApi(LambdaHttpMethod.Post, "/products")]
[OpenApiOperation(Summary = "Create a new product",
Description = "Creates a new product in DynamoDB and returns the created resource")]
[OpenApiResponseType(typeof(Product), 201, Description = "Product created successfully")]
public async Task<IHttpResult> CreateProduct(
[FromBody] Product product,
ILambdaContext context)
{
product.Id = Guid.NewGuid().ToString();
product.CreatedAt = DateTime.UtcNow;
// FluentDynamoDB: Put with condition to prevent overwrites
await _table.Products.Put(product)
.Where(x => x.Id.AttributeNotExists())
.PutAsync();
context.Logger.LogInformation($"Created product with ID: {product.Id}");
return HttpResults.Created($"/products/{product.Id}", product);
}
/// <summary>
/// List all products from DynamoDB
/// </summary>
[LambdaFunction]
[HttpApi(LambdaHttpMethod.Get, "/products")]
[OpenApiOperation(Summary = "List all products",
Description = "Returns a paginated list of products from DynamoDB")]
[OpenApiResponseType(typeof(ProductListResponse), 200, Description = "List of products")]
public async Task<ProductListResponse> GetProducts(
[FromQuery] int limit = 10,
[FromQuery] string? paginationToken = null,
[FromQuery] string? category = null,
ILambdaContext context = null!)
{
context?.Logger.LogInformation($"Getting products with limit: {limit}");
// FluentDynamoDB: Query by category using GSI, or scan all
if (!string.IsNullOrEmpty(category))
{
// Query the category-index GSI
var query = _table.CategoryIndex.Query<Product>(x => x.Category == category)
.Take(limit);
if (!string.IsNullOrEmpty(paginationToken))
{
query = query.Paginate(new PaginationRequest(limit, paginationToken));
}
var products = await query.ToListAsync();
return new ProductListResponse
{
Products = products,
NextToken = query.Response?.GetEncodedPaginationToken()
};
}
else
{
// Scan all products
var scan = _table.Products.Scan().Take(limit);
if (!string.IsNullOrEmpty(paginationToken))
{
scan = scan.Paginate(new PaginationRequest(limit, paginationToken));
}
var products = await scan.ToListAsync();
return new ProductListResponse
{
Products = products,
NextToken = scan.Response?.GetEncodedPaginationToken()
};
}
}
/// <summary>
/// Update an existing product in DynamoDB
/// </summary>
[LambdaFunction]
[HttpApi(LambdaHttpMethod.Put, "/products/{id}")]
[OpenApiOperation(Summary = "Update a product",
Description = "Updates an existing product in DynamoDB")]
[OpenApiResponseType(typeof(Product), 200, Description = "Product updated successfully")]
[OpenApiResponseType(typeof(ErrorResponse), 404, Description = "Product not found")]
public async Task<IHttpResult> UpdateProduct(
[FromRoute] string id,
[FromBody] Product updates,
ILambdaContext context)
{
// FluentDynamoDB: Update with lambda expressions
// ProductUpdateModel is source-generated by FluentDynamoDB
await _table.Products.Update(id)
.Set(x => new ProductUpdateModel
{
Name = updates.Name,
Price = updates.Price,
Description = updates.Description,
Category = updates.Category
})
.Where(x => x.Id.AttributeExists())
.UpdateAsync();
// Fetch the updated product
var product = await _table.Products.GetAsync(id);
if (product == null)
{
return HttpResults.NotFound(new ErrorResponse
{
Message = $"Product with ID '{id}' not found"
});
}
context.Logger.LogInformation($"Updated product with ID: {id}");
return HttpResults.Ok(product);
}
/// <summary>
/// Delete a product from DynamoDB
/// </summary>
[LambdaFunction]
[HttpApi(LambdaHttpMethod.Delete, "/products/{id}")]
[OpenApiOperation(Summary = "Delete a product",
Description = "Deletes a product from DynamoDB")]
[OpenApiResponseType(204, Description = "Product deleted successfully")]
[OpenApiResponseType(typeof(ErrorResponse), 404, Description = "Product not found")]
public async Task<IHttpResult> DeleteProduct(
[FromRoute] string id,
ILambdaContext context)
{
// FluentDynamoDB: Delete with condition
await _table.Products.Delete(id)
.Where(x => x.Id.AttributeExists())
.DeleteAsync();
context.Logger.LogInformation($"Deleted product with ID: {id}");
return HttpResults.NoContent();
}
}
public class ErrorResponse
{
public string Message { get; set; } = string.Empty;
}
public class ProductListResponse
{
public IEnumerable<Product> Products { get; set; } = [];
public string? NextToken { get; set; }
}
Beyond the Basics
FluentDynamoDB offers much more for production applications: dependency injection integration, composite keys with partition/sort key prefixes, batch operations, transactions, conditional filters with optional parameters, and read-only projections. Visit the FluentDynamoDB documentation for the complete guide.
Summary
You've now completed the full tutorial series, progressing from traditional Lambda functions to a complete serverless application with:
- Lambda Annotations for clean, attribute-based function definitions
- LambdaOpenApi for automatic OpenAPI documentation
- DynamoDB with FluentDynamoDB for source-generated, strongly-typed data access
This stack provides a production-ready foundation for building scalable .NET serverless applications on AWS.
Additional Resources
- FluentDynamoDB Documentation — Complete FluentDynamoDB guide
- Amazon DynamoDB Developer Guide
- OpenAPI Generation Tutorial (Previous)
- LambdaOpenApi Documentation