# Bean & Bark: the orders table, typed

The Orders API is live on `dev` — but it's still returning **sample data**. Tunde's storefront posts a real order and gets a cheerful `201`; nothing is stored. This chapter gives the API a spine: a typed order that writes into the single-table **BeanAndBark** design — the *same* DynamoDB table the roastery's analyst reads in the [DynoStudio series](https://www.kanject.com/docs/dynostudio-bean-bark/). You write the shape once; the generator writes the repository.

**You'll learn**

- Model an order as a typed entity whose `pk`/`sk` compose from `[KeyTemplate]`
- Register a `[DbContext]` and let `[Repository]` generate the CRUD + typed finders
- Insert an order and read one back with the generated `FindOrderAsync`
- Read a whole customer's partition with the paginated `FindOrdersAsync`
- Keep `dev` and `prod` on separate physical tables with one config line

> **Wednesday, 2:10pm — the ticket:** **Nadia** (founder): *"Tunde says the API works but the orders never show up in our reports. the analyst's looking at the real table and it's empty 😅 can we actually save them? same table she queries, ideally — I don't want two sources of truth."*

## One order, typed

In a single-table design every item shares one physical table and is told apart by its key **prefix**. An order lives under its customer's partition (`CUSTOMER#<id>`) with a sort key that marks it as an order (`ORDER#<n>`). You don't hand-format those strings — you declare a `[KeyTemplate]` and the generator composes the key from the entity's own properties:

```csharp
using Amazon.DynamoDBv2.DataModel;
using Kanject.Core.NoSqlDatabase.Provider.DynamoDb.Abstractions.Interfaces;
using Kanject.Core.NoSqlDatabase.Provider.DynamoDb.Annotations.Attributes;

// One wholesale order in the single-table BeanAndBark design. The two keys
// are composed from templates; everything else is a plain attribute. The
// [DynamoDB*] attributes are AWS's (Amazon.DynamoDBv2.DataModel); only
// [KeyTemplate] is Kanject's.
public sealed class Order : IDynamoDbEntity
{
    [DynamoDBHashKey("pk")]
    [KeyTemplate("CUSTOMER#{CustomerId}")]
    public string PartitionKey { get; set; } = string.Empty;

    [DynamoDBRangeKey("sk")]
    [KeyTemplate("ORDER#{OrderId}")]
    public string SortKey { get; set; } = string.Empty;

    [DynamoDBProperty] public string CustomerId   { get; set; } = string.Empty;
    [DynamoDBProperty] public string OrderId      { get; set; } = string.Empty;
    [DynamoDBProperty] public string CustomerName { get; set; } = string.Empty;
    [DynamoDBProperty] public string Region       { get; set; } = string.Empty;
    [DynamoDBProperty] public string RoastLevel   { get; set; } = string.Empty;
    [DynamoDBProperty] public long   TotalCents   { get; set; }
}
```

The entity implements `IDynamoDbEntity`, so it carries `PartitionKey` and `SortKey`. The `[DynamoDBHashKey("pk")]` / `[DynamoDBRangeKey("sk")]` attributes are **AWS's** — they name the physical key attributes. The `[KeyTemplate]` attributes are **Kanject's** — they say how each key's value is built. Set `CustomerId = "c-014"` and `OrderId = "8842"`, and the stored keys become `CUSTOMER#c-014` / `ORDER#8842`.

## The table, and the repository

A `[DbContext]` names the physical table and maps every entity that lives in it. One table, one context — you add `Customer`, `Subscription`, and `Product` to the same `MapEntity` call as the model grows:

```csharp
using Kanject.Core.NoSqlDatabase.Provider.DynamoDb.Abstractions.DataContext.EntityBuilder;
using Kanject.Core.NoSqlDatabase.Provider.DynamoDb.Annotations.Attributes;

// One table, many entity shapes. MapEntity names the physical table and
// registers each entity's key layout. Add Customer, Subscription, Product
// here as the model grows — they all share "BeanAndBark".
[DbContext]
public partial class BeanAndBarkDbContext
{
    protected override void OnModelCreating(EntityModelBuilder builder)
        => builder.MapEntity("BeanAndBark", new Order());
}
```

Then the repository — the part you'd normally hand-write and get subtly wrong. You declare four lines; the source generator emits the CRUD methods, the typed finders, and an `IOrderRepository` interface it registers for you:

```csharp
using Kanject.Core.NoSqlDatabase.Provider.DynamoDb.Annotations.Attributes;

// The generator reads this attribute and emits a partial with typed finders,
// plus an IOrderRepository interface (GenerateInterface defaults true) that it
// auto-registers in DI. You write four lines; the CRUD + finders are generated.
[Repository(Name = "Orders", Version = 1, Entity = typeof(Order))]
public partial class OrderRepository;
```

## Wire it up

Registration mirrors the [CloudFunction](https://www.kanject.com/docs/core-cloudfunction/) hosting you already have: one `AddDynamoDbContext<T>` call, plus `RegisterDynamoDbRepository()` to pick up the generated repositories. The `Namespace` is the quiet hero here — it prefixes the physical table per stage, so `dev` and `prod` can never write to each other's data even by accident:

```csharp
using Kanject.Core.NoSqlDatabase.Provider.DynamoDb.Extensions;

builder.Services.AddDynamoDbContext<BeanAndBarkDbContext>(
    options: cfg =>
    {
        cfg.Namespace = builder.Configuration["Stage"];   // "dev" or "prod"
        cfg.AwsRegion = "eu-west-1";
        cfg.CreateTableCheck = true;   // create the table on first run if missing
    },
    dbContextOptions: options => options.ServiceCollection.RegisterDynamoDbRepository());
```

> **Namespace is your blast radius:** Two stages sharing one table is the classic single-table footgun — a `dev` load test that pollutes production reports, a bad migration that can't be undone per-stage. Setting `cfg.Namespace` per stage gives each its own physical table, so the worst a `dev` mistake can do is wreck `dev`. Set it once, at the composition root; every repository inherits it.

## Write, then read it back

Inject `IOrderRepository` and the whole data layer is three methods. Note what you *don't* write: no `PutItem`, no key-string concatenation, no marshalling — the generated finders take your domain identifiers and compose the keys:

```csharp
public sealed class OrderService(IOrderRepository orders)
{
    // Write: set the template fields (CustomerId, OrderId) and the plain
    // attributes; the pk/sk strings compose themselves at insert time.
    public Task PlaceAsync(Order order) => orders.InsertAsync(order);

    // One order by its two identifiers. Keys resolve to
    //   pk = CUSTOMER#c-014 , sk = ORDER#8842
    public Task<Order?> GetAsync(string customerId, string orderId)
        => orders.FindOrderAsync(customerId, orderId);

    // Every order in a customer's partition — paginated, because DynamoDB
    // hands back a page and a continuation token, never an unbounded list.
    // This is the write-side twin of the analyst's
    //   WHERE pk = 'CUSTOMER#c-014' AND begins_with(sk, 'ORDER#')
    public async Task<IReadOnlyList<Order>> ForCustomerAsync(string customerId)
    {
        var (page, _) = await orders.FindOrdersAsync(
            page: (token: null, size: 100), customerId: customerId);
        return page;
    }
}
```

The generated finder name follows the entity: `FindOrderAsync` for one item (it takes every key-template parameter — `customerId` for the partition, `orderId` for the sort key), and the pluralized `FindOrdersAsync` for a whole partition. The collection finder is **paginated by construction** — you pass a page size and get back a page plus a continuation token. That's not ceremony; it's DynamoDB refusing to let you accidentally read an unbounded partition, the same discipline the analyst learns from the read side.

## The payoff: same table, both sides

Seed the [Bean & Bark dataset](https://www.kanject.com/docs/dynostudio-bean-bark/) (or let the API write a few orders), then ask the repository for customer `c-014` — Dana Okafor:

```csharp
var (danas, _) = await orders.FindOrdersAsync(
    page: (token: null, size: 100), customerId: "c-014");

// danas.Count == 3
//   ORDER#8842  Dana Okafor  East  Decaf   54000
//   ORDER#9013  Dana Okafor  East  House   36000
//   ORDER#9188  Dana Okafor  East  Single  42000
```

Three orders. Those are the *exact* rows the DynoStudio series pulls with `WHERE pk = 'CUSTOMER#c-014' AND begins_with(sk, 'ORDER#')` — because it's one table. You just met it from the write side: a typed C# repository. The analyst meets it from the read side: ad-hoc PartiQL. Neither side had to agree on anything but the key layout.

> **🎯 One shape, one table, two doors:** The report is no longer empty. But the real win is that there was never a second source of truth to reconcile — the API writes `CUSTOMER#c-014 / ORDER#8842`, and that's the literal item the analyst reads. You wrote a POCO and four lines of repository; the generator wrote the key composition, the CRUD, and the finders. Getting an order into DynamoDB is easy. Getting the *whole team* to agree on one table without a meeting is the job.

**Recap**

- A single-table entity carries `pk`/`sk` from `IDynamoDbEntity`; `[KeyTemplate]` composes their values from the entity's own properties (`CUSTOMER#{CustomerId}` → `CUSTOMER#c-014`).
- `[DynamoDBHashKey/RangeKey/Property]` are AWS attributes (physical layout); `[KeyTemplate]`, `[DbContext]`, `[Repository]` are Kanject (code generation).
- `[Repository]` generates CRUD, typed finders, and an auto-registered `IOrderRepository` — you declare a partial with no body.
- `FindOrderAsync(customerId, orderId)` reads one item; the paginated `FindOrdersAsync(page, customerId)` reads a whole partition — the write-side twin of the analyst's `begins_with(sk, 'ORDER#')` query.
- `cfg.Namespace` prefixes the physical table per stage, so `dev` and `prod` never share data.

**Check yourself**

**1. You set `CustomerId = "c-014"` and `OrderId = "8842"` on a new `Order` and call `InsertAsync`. What are the stored `pk` and `sk`?**

- `pk = CUSTOMER#c-014`, `sk = ORDER#8842` ✓ — the `[KeyTemplate]` on each key property composes its value from the entity's own fields at insert time — you never set `PartitionKey`/`SortKey` by hand.
- `pk = c-014`, `sk = 8842` — the raw ids are the *template inputs*; the stored keys include the `CUSTOMER#` / `ORDER#` prefixes the templates add. The prefixes are what make single-table design work.
- `pk = CUSTOMER#c-014#ORDER#8842`, `sk = META` — that would be a single composite partition key — but this entity declares a hash key *and* a range key, so the order id lives in the sort key, not folded into the partition.

**2. Why does `FindOrdersAsync` require a page size, when `FindOrderAsync` doesn't?**

- `FindOrdersAsync` reads a whole partition, which can be unbounded; paging forces you to acknowledge that cost ✓ — a single-item get is one key lookup with a fixed cost. A partition query can return any number of items, so the finder makes you page rather than silently reading (and billing for) everything — the same cost-awareness the read-side series teaches.
- Pagination is required by C# for any method returning a list — nothing in C# requires that — it's a deliberate API choice by the repository generator to keep partition reads bounded.
- `FindOrderAsync` is cached and `FindOrdersAsync` is not — neither finder caches; the difference is that one reads a single known key and the other reads an open-ended partition.

**Try it yourself**

**1. Add the customer profile**

A customer's profile lives at `pk = CUSTOMER#<id>`, `sk = PROFILE` — same partition as their orders, a constant sort key. Sketch the `Customer` entity's two key properties.

_Hint:_ The partition template matches the order's. The sort key is a literal — no interpolation, like the sample's `META` row.

_Solution:_ `[KeyTemplate("CUSTOMER#{CustomerId}")]` on `PartitionKey`, and `[KeyTemplate("PROFILE")]` on `SortKey` — a literal-only template composes `sk = "PROFILE"`. Now a customer's profile and all their orders share one partition, so a single query can fetch the customer *and* their order history together.

```csharp
[DynamoDBHashKey("pk")]  [KeyTemplate("CUSTOMER#{CustomerId}")] public string PartitionKey { get; set; } = "";
[DynamoDBRangeKey("sk")] [KeyTemplate("PROFILE")]              public string SortKey    { get; set; } = "";
```

**2. Predict the isolation**

The API runs on `dev` with `cfg.Namespace = "dev"` and on `prod` with `"prod"`. A load test hammers `dev` with 10,000 fake orders. Without looking anything up: can the analyst's production reports be affected?

_Hint:_ The namespace prefixes the *physical* table name. Where do `dev` writes actually land?

_Solution:_ No. Each stage's namespace gives it a separate physical DynamoDB table, so `dev` writes and `prod` reads never touch the same data. The fake orders pile up in the `dev` table and are invisible to production. That one config line is the entire blast-radius guarantee — which is exactly why it belongs at the composition root, not scattered per call.

> **Where next?:** The orders are real and queryable. Next, the *physical* product: every wholesale bag needs a label a buyer can scan for provenance. [The bag tells its story](https://www.kanject.com/docs/core-bean-bark-labels/) generates a QR for each roast batch with `Kanject.Core.Qr` — and meets the scannability verifier the first time an on-brand label is too faint to scan. For the full API surface of what you built here, see the [Kanject.Core.NoSqlDatabase reference](https://www.kanject.com/docs/core-nosql/).

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