-
Notifications
You must be signed in to change notification settings - Fork 0
Recipe DynamoDbContext 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.
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" />.
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.
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.
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.
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.
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.
- Recipe: xUnit per-test isolation — per-test factories versus class fixtures, when to pick which
-
Recipe: ASP.NET Core integration test fixture — using
WebApplicationFactorywith a DynamoDbLite-backedIAmazonDynamoDB - 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