Bean & Bark: your model is the schema
In the DynoStudio series you met this table from the read side, shaping keys by hand — CUSTOMER#c-014, begins_with(sk, 'ORDER#') — and holding the design in your head. In the orders chapter the API started writing typed orders. This chapter closes the loop between them: you write the model once as annotated C#, and the keys, the constraints the table enforces, and the schema DynoStudio reads all flow from that one place.
- Declare keys and a uniqueness constraint as attributes on a plain C# entity
- Register the entity beside
Orderin the same[DbContext]— one physical table, many shapes - See DynoStudio read that same model — schema, markers, and access patterns — with nothing defined twice
Declare it, don't remember it
Instead of remembering that a customer's key is CUSTOMER#{id}, you declare it — and you mark the email unique, right on the property:
using Amazon.DynamoDBv2.DataModel;using Kanject.Core.NoSqlDatabase.Provider.DynamoDb.Abstractions.Interfaces;using Kanject.Core.NoSqlDatabase.Provider.DynamoDb.Annotations.Attributes;// The customer's profile row: pk = CUSTOMER#<id>, sk = "PROFILE" — the same// partition the customer's orders already live under.public sealed class Customer : IDynamoDbEntity{ [DynamoDBHashKey("pk")] [KeyTemplate("CUSTOMER#{CustomerId}")] // the partition key, declared public string PartitionKey { get; set; } = string.Empty; [DynamoDBRangeKey("sk")] [KeyTemplate("PROFILE")] // one profile row per customer public string SortKey { get; set; } = string.Empty; [DynamoDBProperty] public string CustomerId { get; set; } = string.Empty; [DynamoDBProperty] public string Name { get; set; } = string.Empty; [Unique("Email")] // one account per email — enforced [DynamoDBProperty] public string Email { get; set; } = string.Empty; [DynamoDBProperty] public string Region { get; set; } = string.Empty;} Note what the entity does not carry: a table name. The physical table is named exactly once, in the [DbContext] — Customer simply joins Order in the MapEntity call from the orders chapter, and a four-line [Repository] partial asks the generator for the data layer:
// The same context from the orders chapter — Customer joins Order in the one// physical table. This is where "BeanAndBark" is named; entities never carry// the table name themselves.[DbContext]public partial class BeanAndBarkDbContext{ protected override void OnModelCreating(EntityModelBuilder builder) => builder.MapEntity("BeanAndBark", new Order(), new Customer());}// Four lines; the generator emits the CRUD, FindCustomerAsync — and, from the// [Unique] marker, FindCustomerByEmailAsync.[Repository(Name = "Customers", Version = 1, Entity = typeof(Customer))]public partial class CustomerRepository; Open this in DynoStudio's Model Builder and it parses the very same file. The entity, its key templates, and the marker all appear — because the studio reads exactly the attributes you wrote. There is no second schema to define, and nothing to keep in sync.
// generated from [Unique("Email")] — the email is the argument, no query to writeCustomer? ada = await customers.FindCustomerByEmailAsync("ada@beanandbark.co"); - On Kanject, the model is the source: keys are attributes (
[KeyTemplate]), not strings you remember — and the physical table is named once, in the[DbContext]'sMapEntity, never on the entity. - DynoStudio reads the same annotated C# your app is built from — so its schema, markers, and access patterns are never defined twice.
[Unique("Email")]is one attribute that yields both a generatedFindCustomerByEmailAsyncand a table-enforced uniqueness constraint (a$$Unique#…reserved row).
[Unique("Email")] on a property give you?Product entity to the same BeanAndBark table: its partition key is PRODUCT#{Sku}, and the SKU must be unique.Customer: an IDynamoDbEntity with [KeyTemplate]s on the key properties and [Unique(...)] on the SKU — then add new Product() to the context's MapEntity call.Show solution
IDynamoDbEntity with [KeyTemplate("PRODUCT#{Sku}")] on the partition key and [Unique("Sku")] on the SKU — which generates FindProductBySkuAsync and guarantees no two products share a SKU. It joins the table via the same MapEntity call: builder.MapEntity("BeanAndBark", new Order(), new Customer(), new Product()).public sealed class Product : IDynamoDbEntity{ [DynamoDBHashKey("pk")] [KeyTemplate("PRODUCT#{Sku}")] public string PartitionKey { get; set; } = string.Empty; [DynamoDBRangeKey("sk")] [KeyTemplate("PROFILE")] public string SortKey { get; set; } = string.Empty; [Unique("Sku")] [DynamoDBProperty] public string Sku { get; set; } = string.Empty; [DynamoDBProperty] public string Name { get; set; } = string.Empty;}