# Bean & Bark: your model is the schema

In the [DynoStudio series](https://www.kanject.com/docs/dynostudio-bean-bark/) 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](https://www.kanject.com/docs/core-bean-bark-orders/) 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.

> **What you'll need:** This chapter and the [capstone](https://www.kanject.com/docs/core-bean-bark-ship/) build on **Kanject Core** — the SDK and the `kanject` CLI (the [CLI series](https://www.kanject.com/docs/cli-bean-bark/) scaffolds and ships the service itself). If you only *query* Bean & Bark, the [DynoStudio series](https://www.kanject.com/docs/dynostudio-bean-bark/) stands alone; this side is for teams whose app *is* the model.

**You'll learn**

- Declare keys and a **uniqueness constraint** as attributes on a plain C# entity
- Register the entity beside `Order` in 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:

```csharp
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:

```csharp
// 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.

> **What `[Unique("Email")]` actually buys you:** One attribute, two guarantees. It **generates a `FindCustomerByEmailAsync(email)`** — the email you want to look up *is* the method's argument, so there is no query to hand-write — and it **enforces one-account-per-email** by writing a reserved row (`$$Unique#Email#…`) into the base table, a constraint DynamoDB has no native concept of. You didn't write the lookup, and you didn't write the guard; the marker is both.

```csharp
// generated from [Unique("Email")] — the email is the argument, no query to write
Customer? ada = await customers.FindCustomerByEmailAsync("ada@beanandbark.co");
```

**Recap**

- 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]`'s `MapEntity`, 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 generated `FindCustomerByEmailAsync` and a table-enforced uniqueness constraint (a `$$Unique#…` reserved row).

**Check yourself**

**1. On the Kanject platform, where does DynoStudio get its understanding of your schema?**

- It parses the same annotated C# model your app is built from ✓ — the `[KeyTemplate]` / marker attributes are the single source of truth — the studio reads them directly, so there's no separate schema to define or drift.
- You redraw the schema in the studio and keep it in sync by hand — that's exactly the drift Kanject removes — the model *is* the schema, read from your code, not re-entered.
- It infers everything by sampling the live table's items — sampling can profile unknown data (the DynoStudio series' job when the model isn't yours), but here the studio reads your declared model — keys and constraints and all — not a guess from the rows.

**2. What does `[Unique("Email")]` on a property give you?**

- Both a generated FindCustomerByEmailAsync and a table-enforced uniqueness guarantee ✓ — the marker is a lookup *and* a constraint — a reserved `$$Unique#Email#…` row makes a duplicate email impossible, and the method is generated for you.
- A new GSI with Email as its partition key — a marker isn't a GSI — it's a reserved row in the base table. It enforces uniqueness, which a GSI cannot do.
- Only compile-time validation; nothing changes in DynamoDB — it very much changes DynamoDB — it writes a reserved row that makes a second account with the same email fail.

**Try it yourself**

**1. Model the product**

Add a `Product` entity to the same `BeanAndBark` table: its partition key is `PRODUCT#{Sku}`, and the SKU must be unique.

_Hint:_ Same shape as `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.

_Solution:_ An `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())`.

```csharp
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;
}
```

> **Where next?:** Your model is the studio's schema and the table's rulebook. [Marker indexes](https://www.kanject.com/docs/dynostudio-marker-indexes/) is the full reference for `[Unique]`, `[Collection]`, and `[Range]` and the reserved rows behind them. Next, the model becomes the *code*: [ship a feature](https://www.kanject.com/docs/core-bean-bark-ship/) generates the data layer and closes the round-trip.

---
_Source: https://www.kanject.com/docs/core-bean-bark-model/ · Kanject Docs_
