Using Generated Code¶
Every Deepstaging module generates standard .NET interfaces, implementations, and DI registration. There are two ways to use them: effects composition — the intended path — and direct injection for teams that prefer plain async/await.
Effects Composition¶
Effects composition is the primary way to use Deepstaging. Each [EffectsModule] generates Eff<RT, A> wrappers that let you compose service calls, constrain capabilities at compile time, and get automatic OpenTelemetry tracing.
Why Effects?¶
- Compile-time capability constraints —
where RT : IHasArticleStoremeans your handler can only access what the runtime declares. Undeclared dependencies are compiler errors, not runtime surprises. - Composable pipelines — chain operations with
from/selectsyntax. Errors short-circuit automatically. No try/catch pyramids. - Testable by construction —
[TestRuntime]generates stubs and builders for every capability. No mocking frameworks, no DI containers in tests. - Automatic observability — every effect invocation creates an OpenTelemetry span when
Instrumented = true.
Example: DataStore with Effects¶
[DataStore]
public static partial class ScribeStore
{
[StoredEntity]
public static partial class Articles;
}
[Runtime]
public sealed partial class AppRuntime;
// ScribeStore and NotifyModule are auto-discovered
public static Eff<AppRuntime, ArticleId> Handle(CreateArticle cmd) =>
let id = ArticleId.New()
let article = new Article(id, cmd.Title, cmd.Body, cmd.Author,
DateTimeOffset.UtcNow, false)
from _ in Articles.Save<AppRuntime>(article)
>> NotifyModule.NotificationChannel.SendAsync<AppRuntime>(
new Notification("article-created", $"Article {id} published", cmd.Author))
select id;
The runtime enforces that AppRuntime provides both ScribeStore and NotifyModule. Add a [Uses] declaration and the capability is available. Remove it and the compiler catches every broken handler.
Example: CQRS Dispatch¶
The Dispatch module builds on effects to generate typed command/query routing with built-in validation, authorization, and audit:
[CommandHandler(ValidatorType = typeof(CreateArticleValidator), Audited = true)]
[Authorize("CanCreateArticles")]
public static Eff<ScribeRuntime, ArticleCreated> Handle(CreateArticle cmd) =>
let id = ArticleId.New()
let article = new Article(id, cmd.Title, cmd.Body, cmd.Author,
DateTimeOffset.UtcNow, false)
from _ in Articles.Save<ScribeRuntime>(article)
select new ArticleCreated(id);
Method overload resolution IS the dispatch — no runtime routing, no reflection, no service locator.
Direct Injection¶
Every module also generates standard .NET interfaces that you can inject and call with async/await. No LanguageExt dependency in your code. This is useful for teams adopting Deepstaging incrementally, or for parts of your codebase where functional composition isn't needed.
Example: DataStore¶
[DataStore]
public static partial class ScribeStore
{
[StoredEntity]
public static partial class Articles;
}
Inject the generated interface directly:
public class ArticleService(IArticleStore store)
{
public async Task<Article> CreateAsync(string title, string body, string author)
{
var article = new Article(
ArticleId.New(), title, body, author,
DateTimeOffset.UtcNow, false);
await store.SaveAsync(article);
return article;
}
public Task<QueryResult<Article>> ListAsync(int page, int pageSize) =>
store.Query().ToPageAsync(page, pageSize);
}
Example: Event Store¶
public class OrderService(IOrderEventStore orderStore)
{
public async Task<Order?> GetAsync(OrderId id) =>
await orderStore.AggregateAsync(id);
public async Task CreateAsync(OrderId id, string customerName)
{
await orderStore.StartStreamAsync(id, [new OrderCreated(id, customerName)]);
await orderStore.CommitAsync();
}
}
Example: HTTP Client¶
[HttpClient<ApiConfig>(BaseAddress = "https://api.example.com")]
[BearerAuth]
public partial class UsersClient
{
[Get("/users/{id}")]
private partial User GetUser(int id);
[Post("/users")]
private partial User CreateUser([Body] CreateUserRequest request);
}
public class UserService(IUsersClient client)
{
public User GetUser(int id) => client.GetUser(id);
}
The Runtime Still Works¶
Even without writing a single Eff<RT, A>, the runtime generates one-call DI setup, OpenTelemetry wiring, and compile-time dependency validation:
[Runtime]
[Uses(typeof(StorageModule))] // external — from Deepstaging.Storage package
[Uses(typeof(JobsModule))] // external — from Deepstaging.Jobs package
public sealed partial class ScribeRuntime;
// ScribeStore is auto-discovered
builder.Services.AddScribeRuntime(options =>
{
options.EnableTracing();
options.EnableMetrics();
});
Inject your interfaces via standard DI alongside the runtime — they coexist naturally.
All Injectable Interfaces¶
| Module | Injectable Interface |
|---|---|
| Data Store | IArticleStore — async CRUD with in-memory default |
| Event Store | IOrderEventStore — async event stream with in-memory default |
| Configuration | ISmtpConfigRoot — typed config binding from appsettings.json |
| HTTP Client | IUsersClient — typed HTTP client with bearer auth, retries |
| Typed IDs | ArticleId — type-safe ID struct with JSON/EF converters |
| Event Queue | EventQueueChannel<T> — in-process Channel<T> with background workers |
| File Storage | IFileStore — upload, download, presigned URLs |
| Search | ISearchIndex<T> — full-text search with filtering, facets |
| Background Jobs | IJobScheduler — enqueue, schedule, retry, monitor |
| Notifications | INotificationChannel — send via pluggable channels |
| Audit Trail | IAuditStore — record and query audit entries |
| Cache | ICacheStore — typed get/set with expiration |
| Clock | IClock — testable time abstraction |
| Auth | IUserContext — current user from JWT/OAuth/Supabase |
| Vector Search | IVectorIndex<T> — similarity search over embeddings |
Testing¶
Both approaches are testable by design. Every module ships in-memory implementations, and [TestRuntime] generates stubs and builders for every declared capability — no mocking frameworks or DI containers needed.
This generates a class with With*() builder methods and in-memory implementations for every capability your runtime declares:
var runtime = TestRuntime.Create();
var program =
from _ in Articles.Save<ScribeRuntime>(article)
from loaded in Articles.GetById<ScribeRuntime>(id)
select loaded;
var result = await program.RunAsync(runtime);
For direct injection, the same in-memory implementations work as standard test doubles — register them in a test ServiceCollection or pass them directly.
See Testing for the full testing story.
Incremental Adoption¶
You can mix both approaches in the same application. Start with direct injection, and adopt effects where composition and compile-time constraints add value:
- Start — inject generated interfaces, use
async/await - Add effects — wrap frequently-composed services with
[EffectsModule]for pipeline composition - Add dispatch — use
[DispatchModule]for CQRS handlers with validation, authorization, and audit
The generated interfaces don't change — effects are an additional layer on top. See the Effects module and the Build an eShop walkthrough for the full effects story.