Skip to content

Tutorial

Mark Lauter edited this page May 16, 2026 · 1 revision

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.

Project setup

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.csproj

Path 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.

Build a client

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).

Create the table

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.

Save and load a bookmark

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.

List one user's bookmarks

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.

Wrap it as an isolated test

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.csproj
using 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.

Where to next

Clone this wiki locally