Skip to content

1. Project Setup

Every Deepstaging app starts the same way: define your IDs, describe your entities, and declare a store. The generator handles the rest — interfaces, in-memory implementations, effect methods, and test helpers.

Create the Solution

mkdir Library && cd Library
dotnet new slnx -n Library

Contracts First

Before building either context, create the Contracts project. This is where all your TypedIds live — and eventually, your integration events too.

dotnet new classlib -n Library.Contracts -o src/Library.Contracts
dotnet sln add src/Library.Contracts

Add the Deepstaging package:

src/Library.Contracts/Library.Contracts.csproj
<ItemGroup>
    <PackageReference Include="Deepstaging" />
</ItemGroup>

Now define every ID your system will use:

src/Library.Contracts/Ids.cs
using Deepstaging;
using Deepstaging.Ids;

[assembly: TypedIdProfile(Converters = IdConverters.EfCoreValueConverter | IdConverters.JsonConverter)]

namespace Library.Contracts;

// ─── Catalog IDs ────────────────────────────────────────────────────────────────

[TypedId]
public readonly partial struct BookId;

[TypedId]
public readonly partial struct AuthorId;

// ─── Lending IDs ────────────────────────────────────────────────────────────────

[TypedId]
public readonly partial struct PatronId;

[TypedId]
public readonly partial struct LoanId;

[TypedId]
public readonly partial struct HoldId;

Why put all IDs here from the start? Any ID that might appear in a cross-context event needs to live in Contracts. Moving a TypedId between assemblies later is a breaking change — the fully-qualified type name changes, references break, and serialized data with the old name can't deserialize. IDs are shared vocabulary. Put them where everyone can reach them.

The [TypedIdProfile] at the top configures JSON and EF Core converters for all IDs in this assembly. One line, every ID gets serialization support.

The Catalog Context

dotnet new classlib -n Library.Catalog -o src/Library.Catalog
dotnet sln add src/Library.Catalog

Reference Contracts and Deepstaging:

src/Library.Catalog/Library.Catalog.csproj
<ItemGroup>
    <ProjectReference Include="..\Library.Contracts\Library.Contracts.csproj" />
</ItemGroup>
<ItemGroup>
    <PackageReference Include="Deepstaging" />
</ItemGroup>

Entities

A Book is a [StoredEntity] — a record type the generator wraps with a full store interface:

src/Library.Catalog/Domain/Book.cs
[StoredEntity]
public partial record Book
{
    public BookId Id { get; init; }

    [MaxLength(256)]
    [Searchable]
    public string Title { get; init; } = "";

    [MaxLength(20)]
    public string Isbn { get; init; } = "";

    public AuthorId AuthorId { get; init; }

    [MaxLength(256)]
    [Searchable(Weight = 0.5)]
    public string AuthorName { get; init; } = "";

    public int AvailableCopies { get; init; }
    public int TotalCopies { get; init; }
}

A few things to notice:

  • init properties — records with init setters let you use with expressions: book with { AvailableCopies = 3 }. No mutation, just new copies.
  • [Searchable] — marks fields for full-text search. We'll use this in Chapter 3.
  • [MaxLength] — standard validation attributes. These carry into generated API validation.

The DataStore

src/Library.Catalog/Runtime.cs (partial — just the store for now)
[DataStore]
public static partial class CatalogStore;

One line. The generator produces:

  • IBookStore — interface with Save, GetById, QueryPage, Delete
  • InMemoryBookStore — full in-memory implementation
  • TestBookStore — spy implementation with .Seed() and .SaveCalls
  • CatalogStore.Books — typed accessor for the book store capability

Build and check:

dotnet build

Look in src/Library.Catalog/generated/ — you'll find the generated store interfaces and implementations. Real C# files. Read them anytime.

Next → Effects & Runtime