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¶
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:
<ItemGroup>
<PackageReference Include="Deepstaging" />
</ItemGroup>
Now define every ID your system will use:
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¶
Reference Contracts and Deepstaging:
<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:
[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:
initproperties — records withinitsetters let you usewithexpressions: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¶
[DataStore]
public static partial class CatalogStore;
One line. The generator produces:
IBookStore— interface withSave,GetById,QueryPage,DeleteInMemoryBookStore— full in-memory implementationTestBookStore— spy implementation with.Seed()and.SaveCallsCatalogStore.Books— typed accessor for the book store capability
Build and check:
Look in src/Library.Catalog/generated/ — you'll find the generated store interfaces and implementations. Real C# files. Read them anytime.