Skip to content
Client v5: BLE, BLE Hosting, HTTP, Jobs - Linux, MacOS, & Blazor Support! Full AOT, RX on BLE only & MANY other features! Power up!

Amazon DynamoDB

The Shiny.DocumentDb.DynamoDb package provides a document store over Amazon DynamoDB using AWSSDK.DynamoDBv2. Every document type lives in one table: partition key pk = typeName (HASH), sort key sk = id (RANGE).

NuGet package Shiny.DocumentDb.DynamoDb

DynamoDB is a NoSQL key-partitioned store. Rich LINQ queries evaluate client-side after a single-partition query (the LiteDB model); optimistic concurrency is enforced with a conditional write on a top-level Version attribute. There is no spatial / vector / full-text / temporal support on this provider.

  • Serverless, auto-scaling document storage on AWS
  • Existing AWS-hosted .NET workloads using DynamoDB for the rest of the data model
  • Workloads whose reads are almost always “all documents of a type” or point reads by id
  • You want a managed change feed later (DynamoDB Streams — planned phase-2 addition)
Terminal window
dotnet add package Shiny.DocumentDb.DynamoDb
  1. Direct instantiation

    using Shiny.DocumentDb.DynamoDb;
    var store = new DynamoDbDocumentStore(new DynamoDbDocumentStoreOptions
    {
    TableName = "Documents", // one table; pk=typeName (HASH), sk=id (RANGE)
    Region = Amazon.RegionEndpoint.USEast1,
    AutoCreateTable = true // dev convenience; off by default
    });
  2. Dependency injection

    using Shiny.DocumentDb;
    builder.Services.AddDynamoDbDocumentStore(o =>
    {
    o.TableName = "Documents";
    o.Region = Amazon.RegionEndpoint.USEast1;
    o.MapVersionProperty<Order>(x => x.Version); // opt-in optimistic concurrency
    });

    AddDynamoDbDocumentStore registers IDocumentStore and IDocumentMaintenance as singletons.

The AWS standard credential chain is used by default. Override with explicit credentials, a region, a pre-built client, or a service URL (for DynamoDB Local):

// Explicit credentials + region
o.Credentials = new Amazon.Runtime.BasicAWSCredentials("<access>", "<secret>");
o.Region = Amazon.RegionEndpoint.EuWest1;
// DynamoDB Local (integration tests)
o.ServiceUrl = "http://localhost:8000";
// Or a fully pre-configured client (wins over everything else)
o.Client = new Amazon.DynamoDBv2.AmazonDynamoDBClient(/* … */);
PropertyTypeDefaultDescription
ClientIAmazonDynamoDB?nullPre-built client (wins over the credential/region options)
CredentialsAWSCredentials?nullExplicit credentials (else the default chain)
RegionRegionEndpoint?nullAWS region (ignored when ServiceUrl is set)
ServiceUrlstring?nullExplicit endpoint — set for DynamoDB Local
TableNamestring"Documents"The single table holding every type
AutoCreateTableboolfalseCreate the table (on-demand billing) if it doesn’t exist
ConsistentReadboolfalseStrongly-consistent Get/Query (default eventual)
TypeNameResolutionTypeNameResolutionShortNameHow type names (partition keys) are derived
JsonSerializerOptionsJsonSerializerOptions?nullJSON serialization settings
UseReflectionFallbackbooltrueSet false for AOT safety
LoggingAction<string>?nullDiagnostic callback

Additional mapping methods: MapTypeToPartition<T>(pk), MapIdProperty<T>(...), MapIdType<TId>(...), AddQueryFilter<T>(...), AddInterceptor / OnBeforeWrite<T> / OnAfterWrite<T>, and MapVersionProperty<T>(...).

The table (when AutoCreateTable is on) is created with pk (HASH) / sk (RANGE) string keys and on-demand (PAY_PER_REQUEST) billing.

ConceptDynamoDB
Partitionpk = typeName (HASH)
Document keysk = id.ToString() (RANGE)
PayloadData string attribute (+ CreatedAt / UpdatedAt, Version when mapped)
Point readGetItem(pk, sk)
Type-scoped queryQuery with KeyConditionExpression pk = :t
Concurrency (CAS)ConditionExpression on the Version attribute
BatchBatchWriteItem ≤ 25 per request

There is no server-side query translation. Query<T>() runs a single-partition Query to load every document of the type, then evaluates Where / OrderBy / Paginate / Select / aggregates in memory via the shared ExpressionInterpreter:

var open = await store.Query<Order>()
.Where(o => o.Status == "Open") // evaluated client-side
.OrderByDescending(o => o.CreatedAt)
.Paginate(0, 25)
.ToList();
Section titled “Promoted attributes & server-side pushdown”

Promote a scalar property to a native top-level DynamoDB attribute so predicates over it are pushed into a server-side FilterExpression (the full predicate still re-runs client-side, so results are always exact):

var opts = new DynamoDbDocumentStoreOptions { TableName = "Documents", Region = RegionEndpoint.USEast1 }
.MapIndexedProperty<Order>(o => o.Status)
.MapIndexedProperty<Order>(o => o.Total);
var open = await store.Query<Order>().Where(o => o.Status == "Open" && o.Total > 100).ToList();

Inspect the query the builder would run with ToQueryString().

The string Query/QueryStream/Count overloads take a raw PartiQL WHERE condition scoped to the type’s partition. It targets top-level attributes — the promoted attributes (reference them by their CLR/JSON property name) — not fields inside the opaque JSON body. parameters supplies @name token substitutions.

var open = await store.Query<Order>("Status = @s AND Total > @min",
parameters: new { s = "Open", min = 100 });

Project(string) is not supported.

Change observation & the Streams change feed

Section titled “Change observation & the Streams change feed”

DynamoDB implements both change surfaces:

// In-process — this store instance's own writes
await foreach (var change in ((IObservableDocumentStore)store).NotifyOnChange<Order>(ct))
// Native DynamoDB Streams — changes from ANY writer/process
await using var sub = await ((IChangeFeedDocumentStore)store).SubscribeChanges<Order>(async (change, ct) =>
{
Console.WriteLine($"{change.ChangeType} {change.Id}");
});

When the store auto-creates the table it enables a stream (NEW_AND_OLD_IMAGES). SubscribeChanges polls the stream’s shards (latest-first) and delivers Inserted/Updated/Removed changes with the deserialized document (null on delete). Dispose the returned handle to stop.

  • Guid / string Ids auto-generate on Insert when default. Explicit int/long Ids round-trip fine.
  • Int/Long Id auto-generation is unsupported — inserting a default int/long Id throws NotSupportedException (no cheap MAX). Use Guid/string Ids, or assign the int/long Id yourself.
  • Optimistic concurrency: MapVersionProperty<T>(x => x.Version) seeds the version to 1 on insert (stored as a top-level Version number attribute), checks & increments on update/upsert, and guards the write with ConditionExpression: Version = :expected. A stale write throws ConcurrencyException. Blind (unversioned) upsert is last-write-wins. Insert uses attribute_not_exists(sk) so a duplicate id throws.

Reads are eventually consistent by default (matching DynamoDB). Set ConsistentRead = true for strongly-consistent Get/Query at extra read-capacity cost.

Each document is an attribute map:

AttributeTypeValue
pkSthe type name
skSthe document id (string form)
DataSthe serialized JSON body
CreatedAt / UpdatedAtSISO-8601 timestamps
VersionNpresent only when a version property is mapped
  • 400 KB item cap — a document larger than the limit throws a clear NotSupportedException (not a raw storage error). Store large fields externally (e.g. S3) or use a provider without the item cap.
  • Client-side queries by default — an unindexed Query<T>() is a full type scan; promote the filtered attribute with MapIndexedProperty<T> to push the filter server-side.
  • Int/Long Id auto-generation unsupported — use Guid/string.
  • No spatial / vector / full-text / temporalSupportsSpatial / SupportsVector / SupportsFullText are false and there is no ITemporalDocumentStore.
  • Compensating unit of workCreateUnitOfWork() tracks inserts and rolls them back on failure; there is no cross-partition transaction (same model as Cosmos).
  • IDocumentMaintenance.ClearAll() is supported (scans and deletes every item) — handy for tests/dev resets.
  • BatchInsert / BatchRemove use native BatchWriteItem in ≤ 25-item waves, retrying UnprocessedItems with backoff.
  • Upsert deep-merges in C# with recursive null stripping (RFC 7396 semantics).