Deepstaging¶
Source generators for .NET that eliminate infrastructure boilerplate. Declare your intent with attributes — the compiler generates DI registration, effect wrappers, test stubs, HTTP clients, persistence, dispatch, and observability.
Why Deepstaging? Walkthrough → Guides →
How It Works¶
You annotate your types with C# attributes. The Roslyn source generator reads them at compile time and emits all the infrastructure code — DI registration, effect wrappers, test stubs, API endpoints. No reflection, no runtime code generation.
The generated code is visible in your generated/ folder. You can read it, debug into it, and stop using Deepstaging at any time — the generated code still compiles against Deepstaging.Runtime.
A working app in 20 lines¶
This compiles, runs, and is testable — with generated persistence, validation, dispatch, DI registration, and test infrastructure. No database. No message broker.
[TypedId]
public readonly partial struct ArticleId;
[StoredEntity]
public sealed record Article(ArticleId Id, string Title, string Body, string Author);
[DataStore]
public static partial class ScribeStore;
public sealed record CreateArticle(
[Required] string Title,
[StringLength(5000)] string Body,
string Author) : ICommand;
public static class ArticleCommands
{
[CommandHandler(Validated = true, Audited = true)]
public static Eff<ScribeRuntime, ArticleId> Handle(CreateArticle cmd) =>
let id = ArticleId.New()
from _ in ScribeStore.Articles.Save<ScribeRuntime>(
new Article(id, cmd.Title, cmd.Body, cmd.Author))
select id;
}
[Runtime]
public sealed partial class ScribeRuntime;
[DispatchModule]
public static partial class ScribeDispatch;
What's Eff<RT, T>?
Eff<ScribeRuntime, ArticleId> is a lazy computation that requires a ScribeRuntime environment and produces an ArticleId. The from/select syntax is C# LINQ — each from sequences an effect, and errors short-circuit the pipeline. Nothing executes until you call RunAsync. See Effects Composition for the full guide.
Now test it:
[TestRuntime<ScribeRuntime>]
public partial class TestScribeRuntime;
public class CreateArticleTests
{
[Test]
public async Task CreatesArticle()
{
var program = ScribeDispatch.Dispatch(
new CreateArticle("Hello", "World", "Alice"));
var result = await program.RunAsync(
TestScribeRuntime.CreateConfigured());
await Assert.That(result).IsSucc();
}
}
No mocking framework. No DI container setup. CreateConfigured() wires in-memory stubs for every store and service. The same test runs against real Postgres with INTEGRATION=true.
Feature Highlights¶
Effects — wrap any service as composable pipeline¶
[EffectsModule(typeof(IEmailService))]
public sealed partial class EmailEffects;
// Use in handlers:
from _ in EmailEffects.EmailService.SendAsync<AppRuntime>(message)
select unit;
The generator produces Eff<RT, A> wrappers with error context and OpenTelemetry tracing for every method on IEmailService. See Effects.
REST API — dispatch handlers become HTTP endpoints¶
[CommandHandler, HttpPost("/articles")]
public static Eff<ScribeRuntime, ArticleId> Handle(CreateArticle cmd) => ...
[QueryHandler, HttpGet("/articles/{id}")]
public static Eff<ScribeRuntime, Option<Article>> Handle(GetArticleById query) => ...
The generator maps annotated handlers to Minimal API endpoints with OpenAPI, auth, and typed responses. See REST API.
Testing — stubs generated alongside production code¶
// Seed test data, inspect calls — no mocking
var runtime = TestScribeRuntime.CreateConfigured(cfg =>
cfg.WithArticles(new Article(ArticleId.New(), "Test", "Body", "Alice")));
See Testing.
Modules¶
| Module | What It Generates |
|---|---|
| Effects | Eff<RT, A> wrappers with OpenTelemetry tracing for any interface |
| Runtime | Auto-discovered composition root with DI, observability, and startup validation |
| Testing | Generated test runtimes, stubs, dual-mode unit/integration testing |
| REST API | Minimal API endpoints from dispatch handlers with OpenAPI |
| DataStore | CRUD interfaces, in-memory implementations, effect methods, DI registration |
| Dispatch | Typed CQRS dispatch with validation, authorization, and audit |
| Typed IDs | Type-safe ID structs with serialization converters |
| Configuration | Typed config providers with JSON Schema and secrets management |
| HTTP Client | Declarative HTTP clients from annotated methods |
| Event Queue | In-process event queues backed by Channel<T> with background workers |
| Event Store | Event sourcing with projections, upcasting, and Marten backend |
| Background Jobs | Scheduled and recurring jobs with Hangfire integration |
| Cache | Typed caching backed by IDistributedCache |
| Clock | Testable time via IClock with FakeTimeProvider support |
| Integration Events | Cross-service events with stable wire names and transport abstraction |
Built On¶
Deepstaging.Roslyn — a fluent toolkit for building Roslyn source generators, analyzers, and code fixes.
License¶
RPL-1.5 (Reciprocal Public License) — Real reciprocity, no loopholes.
You can use this code, modify it, and share it freely. But when you deploy it — internally or externally, as a service or within your company — you share your improvements back under the same license.
Why? We believe if you benefit from this code, the community should benefit from your improvements. That's the deal we think is fair.
Personal research and experimentation? No obligations. Go learn, explore, and build.
See LICENSE for the full legal text.