-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorial
You'll build a bookmarks store — a small console program that saves URLs by user, fetches them back, and lists everything one user has saved. Each section adds one capability. By the end you have a runnable program plus a test that exercises the same pipeline against an isolated in-memory database.
You'll need the .NET 10 SDK and a working knowledge of C#, async/await, and basic DynamoDB request shapes (key schemas, attribute values, query expressions). Every code block compiles against the current source.
Create a console project and reference DynamoDbLite from the source repo:
dotnet new console -n Bookmarks
cd Bookmarks
dotnet add reference ../DynamoDbLite/src/DynamoDbLite/DynamoDbLite.csprojPath adjusted for your layout. NuGet packages are not yet published; when they ship, the install becomes dotnet add package MSL.DynamoDbLite. See FAQ for the publisher-prefix story.
DynamoDbClient takes a DynamoDbLiteOptions record. The required parameter is a SQLite connection string — there is no default; the optional UseWriteAheadLog flag enables WAL on file-backed stores (DI and Configuration). For development, an in-memory store with a unique name is the right shape:
using DynamoDbLite;
using var client = new DynamoDbClient(new DynamoDbLiteOptions(
$"Data Source=bookmarks_{Guid.NewGuid():N};Mode=Memory;Cache=Shared"));The Data Source name keys SQLite's shared cache; suffixing with a GUID gives this run an isolated database. See Getting Started for the full reasoning.
using var is required — DynamoDbClient implements IDisposable (not IAsyncDisposable).
Bookmarks will use a composite key — UserId as the partition key, Url as the sort key. That shape lets one user own many bookmarks, addressable by URL.
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
await client.CreateTableAsync(new CreateTableRequest
{
TableName = "Bookmarks",
KeySchema =
[
new KeySchemaElement { AttributeName = "UserId", KeyType = KeyType.HASH },
new KeySchemaElement { AttributeName = "Url", KeyType = KeyType.RANGE },
],
AttributeDefinitions =
[
new AttributeDefinition { AttributeName = "UserId", AttributeType = ScalarAttributeType.S },
new AttributeDefinition { AttributeName = "Url", AttributeType = ScalarAttributeType.S },
],
ProvisionedThroughput = new ProvisionedThroughput
{
// Accepted for API compatibility but not enforced — DynamoDbLite has no capacity limits.
ReadCapacityUnits = 5,
WriteCapacityUnits = 5,
},
});The table is ACTIVE immediately — no CREATING status to wait through. See Table Operations for the full surface.
Items are dictionaries of AttributeValue. The key attributes plus any extras you want to store:
await client.PutItemAsync(new PutItemRequest
{
TableName = "Bookmarks",
Item = new Dictionary<string, AttributeValue>
{
["UserId"] = new() { S = "alice" },
["Url"] = new() { S = "https://example.com/wiki" },
["Title"] = new() { S = "Example wiki" },
["Tags"] = new() { SS = ["docs", "reference"] },
},
});Reading it back uses the same key shape:
var response = await client.GetItemAsync(new GetItemRequest
{
TableName = "Bookmarks",
Key = new Dictionary<string, AttributeValue>
{
["UserId"] = new() { S = "alice" },
["Url"] = new() { S = "https://example.com/wiki" },
},
});
if (response.IsItemSet)
Console.WriteLine(response.Item["Title"].S);response.IsItemSet is false when the key is absent. See Item Operations for conditional puts, expression updates, and projection.
A Query returns every item with a given partition key. The KeyConditionExpression always equals on the partition key and optionally constrains the sort key:
var query = await client.QueryAsync(new QueryRequest
{
TableName = "Bookmarks",
KeyConditionExpression = "UserId = :u",
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
[":u"] = new() { S = "alice" },
},
});
foreach (var item in query.Items)
Console.WriteLine($"{item["Title"].S} — {item["Url"].S}");Saving two bookmarks under alice and one under bob and querying alice returns the two — the third is in a different partition.
The sort key supports =, <, <=, >, >=, BETWEEN, and begins_with — useful for prefix queries (begins_with(Url, "https://example.com/")). See Query and Scan for the full operator set, pagination, and filter expressions.
The same code runs as an xUnit test. The only change is the connection string — a fresh GUID per test class isolates each test's database from every other:
dotnet new xunit -n Bookmarks.Tests
cd Bookmarks.Tests
dotnet add reference ../DynamoDbLite/src/DynamoDbLite/DynamoDbLite.csproj
dotnet add reference ../Bookmarks/Bookmarks.csprojusing Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using DynamoDbLite;
public sealed class BookmarksTests : IDisposable
{
private readonly DynamoDbClient client;
public BookmarksTests()
{
client = new DynamoDbClient(new DynamoDbLiteOptions(
$"Data Source=test_{Guid.NewGuid():N};Mode=Memory;Cache=Shared"));
}
public void Dispose() => client.Dispose();
[Fact]
public async Task Bookmark_RoundTripsThroughPutAndGet()
{
await client.CreateTableAsync(new CreateTableRequest
{
TableName = "Bookmarks",
KeySchema =
[
new KeySchemaElement { AttributeName = "UserId", KeyType = KeyType.HASH },
new KeySchemaElement { AttributeName = "Url", KeyType = KeyType.RANGE },
],
AttributeDefinitions =
[
new AttributeDefinition { AttributeName = "UserId", AttributeType = ScalarAttributeType.S },
new AttributeDefinition { AttributeName = "Url", AttributeType = ScalarAttributeType.S },
],
ProvisionedThroughput = new ProvisionedThroughput
{
ReadCapacityUnits = 5, WriteCapacityUnits = 5,
},
});
await client.PutItemAsync(new PutItemRequest
{
TableName = "Bookmarks",
Item = new Dictionary<string, AttributeValue>
{
["UserId"] = new() { S = "alice" },
["Url"] = new() { S = "https://example.com" },
["Title"] = new() { S = "Example" },
},
});
var response = await client.GetItemAsync(new GetItemRequest
{
TableName = "Bookmarks",
Key = new Dictionary<string, AttributeValue>
{
["UserId"] = new() { S = "alice" },
["Url"] = new() { S = "https://example.com" },
},
});
Assert.True(response.IsItemSet);
Assert.Equal("Example", response.Item["Title"].S);
}
}dotnet test runs in well under a second. Two test classes can run in parallel without seeing each other's data — each holds an independent in-memory database keyed by its GUID-suffixed Data Source.
For richer test patterns — IClassFixture versus per-test instantiation, file-based fixtures, ASP.NET Core integration testing — see the Recipes section.
- Item Operations — conditional puts, update expressions, projections
- Query and Scan — operators, pagination, filter expressions, secondary indexes
-
DI and Configuration — registering with
IServiceCollection -
Recipe: DynamoDBContext for tests — POCO mapping with AWS SDK's
DynamoDBContext - API Parity — what's supported, what's stubbed, what throws
Repo · NuGet · API Parity
Getting started
Reference
- Table Operations
- Item Operations
- Query and Scan
- Batch Operations
- Transactions
- Secondary Indexes
- TTL
- Tags and Admin
- DI and Configuration
- Concurrency
- Performance
- API Parity
- FAQ
Recipes
- DynamoDBContext for tests
- xUnit per-test isolation
- ASP.NET Core integration test fixture
- Migrating tests off DynamoDB Local
Internals