Skip to content

Recipe DynamoDbContext For Tests

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

Recipe: DynamoDBContext (POCO mapping) for tests

You have a repository class that uses AWS SDK's DynamoDBContext to map POCOs to DynamoDB items, and you want to test it without a container or AWS credentials. DynamoDBContext works against DynamoDbClient unchanged — your test fixture wraps the in-memory client in a DynamoDBContext and your repository code runs as-is.

The interesting wrinkle is DisableFetchingTableMetadata. By default, DynamoDBContext calls DescribeTableAsync to learn each table's schema. DynamoDbLite answers that call from its own metadata — the call works — but disabling the fetch removes a round-trip and lets POCO attributes drive the mapping. The DynamoDbLite test fixtures set the flag; production code typically does not.

Project setup

NuGet packages are not yet published; reference the project directly from the source repo and let it pull AWSSDK.DynamoDBv2 transitively:

<ItemGroup>
  <ProjectReference Include="..\..\DynamoDbLite\src\DynamoDbLite\DynamoDbLite.csproj" />
  <PackageReference Include="AWSSDK.DynamoDBv2" />
  <PackageReference Include="xunit.v3" />
</ItemGroup>

AWSSDK.DynamoDBv2 arrives transitively, but listing it explicitly in your test project keeps the version intent clear. Once a real MSL.DynamoDbLite ships, swap the ProjectReference for <PackageReference Include="MSL.DynamoDbLite" />.

Define the POCO

Use the standard AWS SDK attributes — DynamoDbLite reads them via DynamoDBContext exactly as the real service does:

using Amazon.DynamoDBv2.DataModel;

[DynamoDBTable("Users")]
public sealed class User
{
    [DynamoDBHashKey]
    public string UserId { get; set; } = "";

    public string Name { get; set; } = "";

    public int Age { get; set; }

    [DynamoDBProperty("created_at")]
    public DateTime CreatedAt { get; set; }

    [DynamoDBIgnore]
    public string TransientCache { get; set; } = "";
}

Composite-key models add [DynamoDBRangeKey] to the sort-key property.

Build the test fixture

The fixture creates a DynamoDbClient with a unique in-memory connection string, wraps it in a DynamoDBContext via the builder, and disposes both at the end of the test class. The unique Data Source name keeps each test class's database isolated — see Recipe: xUnit per-test isolation for the deeper isolation patterns.

using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;
using DynamoDbLite;

public sealed class UserRepositoryFixture : IDisposable
{
    public DynamoDbClient Client { get; }
    public DynamoDBContext Context { get; }

    public UserRepositoryFixture()
    {
        Client = new DynamoDbClient(new DynamoDbLiteOptions(
            $"Data Source=test_{Guid.NewGuid():N};Mode=Memory;Cache=Shared"));

        Context = new DynamoDBContextBuilder()
            .ConfigureContext(cfg => cfg.DisableFetchingTableMetadata = true)
            .WithDynamoDBClient(() => Client)
            .Build();
    }

    public void Dispose()
    {
        Context.Dispose();
        Client.Dispose();
    }
}

Disposal order matters in the file-based variant — see FAQ.

Create the table

DynamoDBContext does not create tables; create them through the underlying client before the test exercises the repository:

await Client.CreateTableAsync(new CreateTableRequest
{
    TableName = "Users",
    KeySchema =
    [
        new KeySchemaElement { AttributeName = "UserId", KeyType = KeyType.HASH }
    ],
    AttributeDefinitions =
    [
        new AttributeDefinition { AttributeName = "UserId", AttributeType = ScalarAttributeType.S }
    ],
    ProvisionedThroughput = new ProvisionedThroughput
    {
        ReadCapacityUnits = 5,
        WriteCapacityUnits = 5
    }
});

A small helper method in the fixture, called from the constructor, keeps tests free of setup boilerplate.

Write the test

public sealed class UserRepositoryTests : IClassFixture<UserRepositoryFixture>
{
    private readonly UserRepositoryFixture fx;

    public UserRepositoryTests(UserRepositoryFixture fx) =>
        this.fx = fx;

    [Fact]
    public async Task SaveAndLoad_RoundTripsScalarTypes()
    {
        var user = new User
        {
            UserId = "u-1",
            Name = "Alice",
            Age = 30,
            CreatedAt = DateTime.UtcNow,
        };

        await fx.Context.SaveAsync(user);

        var loaded = await fx.Context.LoadAsync<User>("u-1");

        Assert.NotNull(loaded);
        Assert.Equal("Alice", loaded.Name);
        Assert.Equal(30, loaded.Age);
    }

    [Fact]
    public async Task QueryAsync_ByHashKey_ReturnsAllItems()
    {
        await fx.Context.SaveAsync(new User { UserId = "u-2", Name = "Bob" });
        await fx.Context.SaveAsync(new User { UserId = "u-3", Name = "Carol" });

        var query = fx.Context.QueryAsync<User>("u-2");
        var results = await query.GetRemainingAsync();

        Assert.Single(results);
        Assert.Equal("Bob", results[0].Name);
    }
}

For composite-key models, LoadAsync<T>(hashKey, sortKey) and QueryAsync<T>(hashKey).GetRemainingAsync() work the same way they do against the real service.

What's verified

The DynamoDbLite test suite exercises DynamoDBContext end-to-end across attribute mapping, type conversions, batch operations, versioning, enum handling, GSI queries, numeric sort keys, and edge cases — 13 dedicated test files under tests/DynamoDbLite.Tests/DynamoDbContext/. If your repository uses any of these surfaces, the existing tests are evidence that the surface works.

DynamoDBContext operations that depend on capabilities DynamoDbLite does not implement — DynamoDB Streams, Backup & Restore, PartiQL — fail the same way they do anywhere else. The API Parity page lists what's supported.

Next steps

Clone this wiki locally