Table of Contents

Generated code

The source generator adds members to every partial type with [DynamoDbEntity] or [DynamoDbDocument]. They live in your assembly, so the API reference cannot show them. The examples use Order of the sample:

[PartitionKey("CUSTOMER#{CustomerId}")]
[SortKey("ORDER#{Number:d6}")]
[GlobalIndexKey("ByStatus", PartitionKey = "STATUS#{Status}", SortKey = "{PlacedAt}")]
[GlobalIndexKey("ByTracking", PartitionKey = "TRACK#{TrackingNumber}", Sparse = true)]

What a type gets

Member On Holds
Order.Attrs Every entity and document A typed handle per stored attribute, see Attrs
Order.Keys Every entity Of and TryOf for the table key, and a builder per key, see Keys
Order.Indexes An entity that declares an index Each index name as a const string, see Indexes
DynamoDbEntities.IncludingAll() The assembly, once, when its entities share the keys of one table An Including<T>() of every entity, see DynamoDbEntities

The code that reads and writes the item implements IDynamoDbEntity<Order> explicitly, so you cannot call it. An enum's converter is a file type, and the structs a handle names to reach it are hidden from IntelliSense. Keys, Attrs and Indexes are reserved member names on every mapped type (DYNA014); a type that inherits from an entity hides its base's classes with its own.

Order.Keys.PrimaryKey key = Order.Keys.Of("alice", 42);
PartitionKey<Order> partition = Order.Keys.Pk.Of("alice");
string text = Order.Keys.Pk.Format("alice");                           // CUSTOMER#alice
Order.Keys.SkParts parts = Order.Keys.Sk.Parse("ORDER#000042");        // parts.Number is 42
SortKeyCondition recent = Order.Keys.Sk.Ge(40);                         // ORDER#000040 and after
Attr<decimal, NumberConverter<decimal>> total = Order.Attrs.Total;
Index<Order> placed = orders.Index(Order.Indexes.ByStatus);             // the index "ByStatus"

Keys

Keys.Of and Keys.TryOf

Order.Keys.Of(customerId, number) takes the placeholders of the table's partition key, then its sort key, in template order, each typed as its property. A property both keys use is one parameter. It returns Order.Keys.PrimaryKey, a readonly struct implementing IKey<Order> that holds the values, not the text: no string is made until a command writes the key. Another entity's key does not compile where Get, Delete or Query take this one.

Of checks the values, as Format does, and throws KeyFormatException for one that cannot be part of the key: empty, holding a delimiter of its template, or outside its format, such as -1 or 1000000 under {Number:d6}. The exception names the placeholder and template, never the value.

TryOf(...) is the same check without an exception, for a value from outside: it returns the key, or null for a value that Of throws for, so a read of a value from outside is one expression with is { } key. It exists only where the table key can refuse a value: Guid, DateOnly and DateTimeOffset fit every template, so a key of those alone has none. It checks the table key only; an index template can still refuse the value when a Put writes it. A key of literals alone, such as [SortKey("PROFILE")] beside [PartitionKey("CONFIG")], is Keys.Of() without parameters.

Order? placed = Order.Keys.TryOf(after, 1) is { } key ? await orders.Get(key).ItemAsync(ct) : null;
Order? latest = Order.Keys.Pk.TryOf(after) is { } partition ? await orders.Query(partition).Descending().ItemAsync(ct) : null;

A method that stops at a value that does not fit returns early:

if (Order.Keys.TryOf(after, 1) is not { } first)
{
    Console.WriteLine("Not a customer id.");
    return;
}

Order? found = await orders.Get(first).ItemAsync(ct);

The key is a nullable struct, so the null costs nothing. Keys and every partition key builder that can refuse a value have a TryOf; a sort key builder makes conditions and has none.

Builders

Each key has a static class under Keys:

Key Builder
Partition key of the table Keys.Pk
Sort key of the table Keys.Sk
Global index Keys.ByStatusPk, and Keys.ByStatusSk if it has a sort key
Local index Keys.<Index>Sk. Its partition key is the table's, so Keys.Pk builds it

The index part is the index name in Pascal case: orders-by_date gives OrdersByDate, GSI1 gives Gsi1.

A partition key of a template

Member Exists Gives
Format(values) Always The key as stored: Pk.Format("alice") is CUSTOMER#alice
Of(values) Always A PartitionKey<Order> for Query, bound to the attribute it was built for: a table key in a query of ByStatus throws ArgumentException naming the builder to use
TryOf(values) Where the template can refuse a value The key, or null instead of KeyFormatException, for is { } key, at the cost of Of
Parse(text), Parse(utf8), TryParse(text, out parts), TryParse(utf8, out parts) Always PkParts, a record struct with a property per placeholder. Parse succeeds exactly when Format of the result gives the text back

A sort key of a template

A sort key has no Of: its builder makes conditions for Query; Keys.Of makes the key of one item.

Member Exists Gives
Format(values) Always The key as stored: Sk.Format(42) is ORDER#000042
Eq(all values) Always = Format(...), one item of the partition
Prefix(leading...), UpperBound(leading...) At every literal: before the first placeholder, and after each placeholder a literal follows. A template of one literal has none The text through that delimiter, and the same plus U+10FFFF, which sorts above every key under it
BeginsWith(leading...) Where Prefix is begins_with on that prefix. BeginsWith() is every key of the template. BeginsWith(value) formats the fields as the template does, so a value keeps only what its format holds: under {At:yyyy-MM}#{Id} a day is its month
PrefixPartial(values) When the last placeholder is a string A prefix ending inside that value, for autocomplete. It matches longer values, which BeginsWith does not
Between(leading..., from, to) On a field whose stored text sorts like its value Every key from from to to, both included, with the fields before it equal
Range(leading..., from, to) Where Between is, and something follows the field The same without the keys at to, for [from, to)
Ge, Gt, Le, Lt(values...) When every field through the last argument sorts like its value One end, not bounded by the template: Ge also reaches what sorts after it in the partition
Parse, TryParse Always SkParts, as for a partition key

A field sorts like its value when its width is fixed (d6, a timestamp, a Guid, a date) or it is a string that ends the key or precedes a space. Plain digits sort 10 before 9 and an enum sorts by name, so those get no range. A string before another delimiter is DYNA115. What is not generated would be wrong, so it does not compile.

For the sample, Order.Keys.Sk of ORDER#{Number:d6} has Format, Eq, Prefix(), UpperBound(), BeginsWith(), Between, Ge, Gt, Le, Lt and the parsers; no Range, as nothing follows Number, and no PrefixPartial, as Number is no string. Order.Keys.ByStatusSk of {PlacedAt} has no literal, so no prefix, and its ranges compare whole keys. See Queries and scans. The bounds they write:

Condition Key condition A key that ends in the field
Eq(values) = Format(...) The same
Between(leading..., from, to) BETWEEN Prefix(leading..., from) AND UpperBound(leading..., to) BETWEEN Format(leading..., from) AND Format(leading..., to)
Range(leading..., from, to) BETWEEN Prefix(leading..., from) AND Prefix(leading..., to) Not generated
Ge(x...) >= Prefix(x...) >= Format(x...)
Gt(x...) > UpperBound(x...) > Format(x...)
Le(x...) <= UpperBound(x...) <= Format(x...)
Lt(x...) < Prefix(x...) < Format(x...)

A field a bucket format truncates, such as a day under {At:yyyy-MM-dd}, compares as its bucket, so Gt of a day leaves out that whole day. A value that cannot be part of a key throws KeyFormatException, as Format does.

A plain key

A key on a property, [PartitionKey] public required string Sku, has no template, nor has a template of one bare placeholder over its own property's attribute, such as [SortKey("{At}", AttributeName = "At")]. The attribute is the key, as its converter writes it, so a number stays a number:

Member Exists Gives
Of(value) Always A PartitionKey<T> for a partition key, and the AttributeValue of a sort key for SortKeyCondition
Format(value) When the key is stored as a string The text
Eq(value) On a sort key = on the stored value
Between, Ge, Gt, Le, Lt On a sort key that stores a number, binary, string, Guid, DateTimeOffset, DateTime, DateOnly or TimeOnly The comparison of DynamoDB on the stored value
BeginsWith(prefix) On a sort key of string begins_with

There is nothing to parse, no prefix and no TryOf: no template refuses a value, and an empty string is DynamoDB's to reject.

Attrs

Order.Attrs.Total is an Attr<decimal, NumberConverter<decimal>>: the attribute name and the property's converter, which type conditions, updates and projections. There is one per stored attribute, on entities and documents, named after the property: [DynamoDbAttribute("ttl")] public DateTimeOffset? ExpiresAt gives Attrs.ExpiresAt for the attribute ttl.

  • The value type is the type without its ?, which Set and the conditions take. The converter says the rest: a nullable property reads through NullableHandleConverter or NullableReferenceHandleConverter, [OmitEmpty] through OmitEmptyConverter or a collection kin, and a [Version] with VersionMissing.Allow through one that reads a missing attribute as 0.
  • A handle is internal unless the property, its type and its converters are public.
  • A [KeyOnly] property has no attribute and no handle; neither has an attribute whose name holds . or [, which a path would split, and a raw path cannot address it either.
  • A document has its own handles, and At walks into one: Customer.Attrs.ShipTo.At(Address.Attrs.City).

See Expressions for what a handle can do.

Indexes

Order.Indexes.ByStatus is the const string "ByStatus", for orders.Index(...), shop.Index<Order>(...) and the request API. The constant is named like the builders, so GSI1 is Indexes.Gsi1. An entity without an index has no Indexes. Two indexes of one type that would share a name are reported at compile time.

DynamoDbEntities

Beside the members of each type, an assembly whose entities all have the keys of one table gets one class, named after the assembly. For the sample, whose project is DynaSharp.Sample:

namespace DynaSharp.Sample;

public static class DynamoDbEntities
{
    public static CreateTableCommand IncludingAll(this CreateTableCommand command)
        => command.Including<Cart>().Including<Customer>().Including<EmailClaim>().Including<Order>().Including<Product>();
}

orders.CreateTable().IncludingAll() names every entity of the assembly, so none of their indexes is left out of the table, see Every entity of an assembly. The class is internal when an entity is, and there is none, with the info diagnostic DYNA037, when the entities declare the keys of more than one table or one is nested in a private type. The file is DynaSharp-DynamoDbEntities.g.cs.