Skip to main content

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.

Why DynamoDB for Serverless?

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 ApproachFluentDynamoDB
Verbose attribute dictionariesSource-generated strongly-typed entities
Manual key constructionAutomatic key handling with prefixes
Complex query expressionsLambda expression support
Scattered error handlingOptional FluentResults integration
Manual paginationBuilt-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