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¶
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:
<ItemGroup>
<ProjectReference Include="..\..\src\Library.Catalog\Library.Catalog.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Deepstaging.Testing"/>
</ItemGroup>
Create a test runtime:
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:
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.