Home β€Ί .NET on AWS β€Ί Using DynamoDB with .NET

Using DynamoDB with .NET

How to use Amazon DynamoDB from .NET: SDK options, data modeling in C#, AoT-compatible patterns, and CDK setup.

The .NET DynamoDB Landscape

AWS provides three ways to interact with DynamoDB from .NET:

  1. Low-level SDK (IAmazonDynamoDB). Raw PutItem, GetItem, Query calls with Dictionary<string, AttributeValue>. Maximum control, maximum boilerplate.
  2. Document Model (Table, Document). Slightly higher-level, still untyped.
  3. Object Persistence Model (DynamoDBContext). Attribute-decorated POCOs, automatic serialization. Easy to start, breaks under AoT, and fights single-table design.

For modern .NET on Lambda with AoT, options 1-2 work but are verbose. Option 3 uses reflection and won't compile with AoT unless you jump through source-generator hoops. This is exactly why I built FluentDynamoDB: type-safe repositories with source generation, no reflection, designed for single-table patterns.

Basic SDK Usage

Writing an item

using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;

var client = new AmazonDynamoDBClient();

await client.PutItemAsync(new PutItemRequest
{
    TableName = "my-app-table",
    Item = new Dictionary<string, AttributeValue>
    {
        ["PK"] = new AttributeValue { S = $"USER#{userId}" },
        ["SK"] = new AttributeValue { S = "PROFILE" },
        ["Name"] = new AttributeValue { S = user.Name },
        ["Email"] = new AttributeValue { S = user.Email },
        ["CreatedAt"] = new AttributeValue { S = DateTime.UtcNow.ToString("O") },
    },
    ConditionExpression = "attribute_not_exists(PK)",
});

Querying items

var response = await client.QueryAsync(new QueryRequest
{
    TableName = "my-app-table",
    KeyConditionExpression = "PK = :pk AND begins_with(SK, :prefix)",
    ExpressionAttributeValues = new Dictionary<string, AttributeValue>
    {
        [":pk"] = new AttributeValue { S = $"USER#{userId}" },
        [":prefix"] = new AttributeValue { S = "ORDER#" },
    },
});

foreach (var item in response.Items)
{
    var orderId = item["SK"].S.Replace("ORDER#", "");
    var amount = decimal.Parse(item["Amount"].N);
}

This works but it's stringly-typed and error-prone. Typo in a key name? Runtime error. Wrong attribute type? Runtime error.

AoT-Compatible Patterns

The DynamoDBContext (Object Persistence Model) uses reflection for serialization. Under Native AoT, reflection is either unavailable or requires extensive trimming configuration. The cleaner approach is source-generated serialization:

// Define your model with System.Text.Json source generation
[JsonSerializable(typeof(UserProfile))]
internal partial class AppJsonContext : JsonSerializerContext { }

public record UserProfile(string UserId, string Name, string Email, DateTime CreatedAt);

// Serialize/deserialize manually with the low-level SDK
public static Dictionary<string, AttributeValue> ToItem(UserProfile profile) => new()
{
    ["PK"] = new AttributeValue { S = $"USER#{profile.UserId}" },
    ["SK"] = new AttributeValue { S = "PROFILE" },
    ["Name"] = new AttributeValue { S = profile.Name },
    ["Email"] = new AttributeValue { S = profile.Email },
    ["CreatedAt"] = new AttributeValue { S = profile.CreatedAt.ToString("O") },
};

public static UserProfile FromItem(Dictionary<string, AttributeValue> item) => new(
    UserId: item["PK"].S.Replace("USER#", ""),
    Name: item["Name"].S,
    Email: item["Email"].S,
    CreatedAt: DateTime.Parse(item["CreatedAt"].S)
);

This is verbose but it compiles under AoT without issues. For a production app with dozens of entity types, you want a library that generates this mapping code for you.

FluentDynamoDB Approach

FluentDynamoDB uses source generators to create type-safe repositories at compile time:

[DynamoTable("my-app-table")]
public partial class UserRepository : DynamoRepository<UserProfile>
{
    [PartitionKey] 
    public string PK => $"USER#{Entity.UserId}";
    
    [SortKey]
    public string SK => "PROFILE";
}

// Usage
var repo = new UserRepository(dynamoClient);
await repo.PutAsync(new UserProfile("12345", "Dan", "dan@example.com", DateTime.UtcNow));
var user = await repo.GetAsync("USER#12345", "PROFILE");

No reflection. The source generator writes the ToItem/FromItem mapping at build time.

CDK Setup (C#)

using Amazon.CDK;
using Amazon.CDK.AWS.DynamoDB;

var table = new Table(this, "AppTable", new TableProps
{
    TableName = "my-app-table",
    PartitionKey = new Attribute { Name = "PK", Type = AttributeType.STRING },
    SortKey = new Attribute { Name = "SK", Type = AttributeType.STRING },
    BillingMode = BillingMode.PAY_PER_REQUEST,
    PointInTimeRecovery = true,
    DeletionProtection = true,
});

table.AddGlobalSecondaryIndex(new GlobalSecondaryIndexProps
{
    IndexName = "GSI1",
    PartitionKey = new Attribute { Name = "GSI1PK", Type = AttributeType.STRING },
    SortKey = new Attribute { Name = "GSI1SK", Type = AttributeType.STRING },
});

// Grant Lambda access
appFunction.AddEnvironment("TABLE_NAME", table.TableName);
table.GrantReadWriteData(appFunction);

Tips for .NET Developers

  • Don't try to use EF Core patterns. DynamoDB isn't relational. No joins, no transactions across partitions, no ad-hoc queries. Design your keys around access patterns.
  • Use ServiceURL for local development. Point the SDK at DynamoDB Local or LocalStack for integration tests.
  • Batch operations have limits. BatchWriteItem handles 25 items max. For bulk loads, you need to chunk and handle UnprocessedItems in the response.
  • Watch for hot partitions. If all your traffic hits one partition key, you'll get throttled regardless of your capacity settings. Spread writes across partitions.

Further Reading

Looking for hands-on help? View my .NET on AWS services β†’

DynamoDB modeling getting complex?

Drop me a message β€” I typically respond within one business day.