Skip to content

2. Effects & Runtime

You have a DataStore with generated interfaces. Now you need something to run them. That's the Runtime — the composition root that wires capabilities together and makes effects executable.

What's an Effect?

An Eff<RT, T> is a description of something that will happen — not something that has happened. It's a value you can compose, chain, and test before anything actually executes.

// This doesn't save anything. It describes "save this book."
Eff<CatalogRuntime, Unit> saveBook = CatalogStore.Books.Save<CatalogRuntime>(book);

// This describes "save a book, then query all books."
var program =
    from _ in CatalogStore.Books.Save<CatalogRuntime>(book)
    from books in CatalogStore.Books.QueryPage<CatalogRuntime>(1, 20)
    select books;

// Nothing has happened yet. Now run it:
var result = await program.RunAsync(runtime);

The from/select syntax is C# LINQ — you're composing a pipeline of operations. Each from is a step. The runtime provides the capabilities (store implementations, HTTP clients, whatever) that the effects need to execute.

If this looks like a lot of ceremony for a save operation — fair. The payoff comes when you compose 5 operations that each might fail, and the whole pipeline short-circuits on the first error without a single try/catch. And when you can swap the runtime for a test double with one line.

Create the Runtime

src/Library.Catalog/Runtime.cs
namespace Library.Catalog;

using Deepstaging;

[Runtime]
public partial class CatalogRuntime;

[DataStore]
public static partial class CatalogStore;

[Runtime] tells the generator to produce:

  • A capability interface (ICatalogRuntimeCapabilities) listing every module the runtime needs
  • A constructor accepting all capabilities
  • A bootstrapper (AddCatalogRuntime()) for DI registration
  • An implicit conversion from test runtimes

The generator discovers CatalogStore automatically because it's in the same assembly. No [Uses] needed for local modules.

Your First Test

dotnet new classlib -n Library.Tests -o test/Library.Tests --framework net10.0
dotnet sln add test/Library.Tests

Add the testing packages and project references:

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

Create a test runtime:

test/Library.Tests/Catalog/TestCatalogRuntime.cs
namespace Library.Tests.Catalog;

[TestRuntime<CatalogRuntime>]
public partial class TestCatalogRuntime;

One line. The generator creates Create() (with spy-capable test stores) and CreateConfigured() (returns the production runtime with in-memory stores).

Write a test:

test/Library.Tests/Catalog/Domain/Commands/AddBookTests.cs
public class AddBookTests
{
    [Test]
    public async Task AddBook_CreatesBookInStore()
    {
        var runtime = TestCatalogRuntime.CreateConfigured();

        var program =
            from bookId in CatalogDispatch.AddBook("The Pragmatic Programmer", "978-0135957059", AuthorId.New(), "David Thomas", 3)
            from result in CatalogDispatch.GetAvailableBooks()
            select result;

        var fin = await program.RunAsync(runtime);

        await Assert.That(fin).IsSuccMatching(r => r.Data.Count == 1);
    }
}

Don't worry about CatalogDispatch yet — that's the next chapter. The key idea: you compose a program from effects, run it against a runtime, and assert on the Fin<T> result. IsSucc() means it worked. IsFail() means something went wrong.

dotnet test

Next → Commands & Queries