BulletsForHumanity.Hermetic
0.5.1
See the version list below for details.
dotnet add package BulletsForHumanity.Hermetic --version 0.5.1
NuGet\Install-Package BulletsForHumanity.Hermetic -Version 0.5.1
<PackageReference Include="BulletsForHumanity.Hermetic" Version="0.5.1" />
<PackageVersion Include="BulletsForHumanity.Hermetic" Version="0.5.1" />
<PackageReference Include="BulletsForHumanity.Hermetic" />
paket add BulletsForHumanity.Hermetic --version 0.5.1
#r "nuget: BulletsForHumanity.Hermetic, 0.5.1"
#:package BulletsForHumanity.Hermetic@0.5.1
#addin nuget:?package=BulletsForHumanity.Hermetic&version=0.5.1
#tool nuget:?package=BulletsForHumanity.Hermetic&version=0.5.1
Hermetic
Early development. Hermetic is under active development and not yet at 1.0. APIs may change or break between releases. Until 1.0, breaking changes bump the minor version and non-breaking changes bump the patch version.
What Is Hermetic?
Attribute-driven code generation for event-sourced .NET applications.
You declare a command. You write a handler. Hermetic generates everything in between — endpoints, projections, DI registrations, and fully typed Refit client methods — at compile time, with zero runtime reflection.
Hermetic is built on four ideas:
- Trine — a formalized domain modelling methodology with three aggregate roles and a hierarchical identity system
- Hierarchical Keys — structured, derived stream keys that eliminate ID generation, database round-trips, and foreign key lookups
- The Event Contract — a bidirectional, compile-time-verified chain that proves every event in the system is correct before the code compiles
- The Sealed Pipeline — Roslyn generators that transmute declarations into running infrastructure with zero hand-wiring
It hooks into plain ASP.NET Core. It is fully trimmable, fully AOT-compatible, and uses no runtime reflection. Every generated path is aggressively inlined.
See It In Action
You write this. A command, an event, an aggregate, and a handler:
// The event
[AppliedBy<Thing>]
public sealed record ThingCreated(ThingId ThingId, string Name) : IEventLaw;
// The command + handler
[RaisesEvent<ThingCreated>]
public sealed record CreateThing(ThingId ThingId, string Name) : ICommandLaw, IThingCommand
{
public sealed class Handler : CommandHandler<CreateThing>
{
public override async IAsyncEnumerable<IEffectLaw> Handle(
CreateThing cmd, [EnumeratorCancellation] CancellationToken ct)
{
yield return new OpenChronicleEffect<Thing>(
cmd.ThingId.Value,
new ThingCreated(cmd.ThingId, cmd.Name));
}
}
}
// The aggregate
[AppliedBy<ThingCreated>]
public sealed partial record Thing(ThingId Id, string Name)
: IAggregateRoot<ThingId, Thing>
{
[Applies<ThingCreated>]
public static Thing Create(IEventEnvelope<ThingId, ThingCreated> e)
=> new(e.Data.ThingId, e.Data.Name);
}
You call this. A fully typed, IntelliSense-visible method on your Refit interface:
await _thingApi.CreateThingAsync(ThingId.New(), "My first thing", ct);
Hermetic generates everything in between:
| What | Where | How |
|---|---|---|
POST /api/command/create-thing |
Backend | Minimal-API endpoint with validation, handler dispatch, event routing, and SaveChangesAsync |
CreateThing.Handler DI registration |
Backend | Scoped service registration |
ThingProjection |
Backend | Marten SingleStreamProjection<Thing, ThingId> with inline lifecycle |
CreateThingAsync(thingId, name, ct) |
Client | Typed extension method on your [CommandApiSeal] Refit interface |
SendCreateThingCommandAsync(cmd, ct) |
Client | Object-form extension method for pre-built commands |
| OpenTelemetry traces + metrics | Server & Client | HermeticTelemetryScope wraps every endpoint, projection, client call, and SignalR dispatch — register HermeticTelemetry.ActivitySourceName / MeterName once and it just works |
No routing code. No projection registration. No Refit method written by hand. No endpoint wiring.
You declared what the domain is. Hermetic manifested the rest.
Trine
Hermetic is the reference implementation of Trine, a formalized domain modelling methodology created by Max Obrist. Trine defines how domains are structured — aggregate roles, identity systems, event flow — in a way that produces models where infrastructure derives mechanically from declarations.
Hermetic implements three aggregate roles, a hierarchical key system, and a sealed event contract — all from Trine. Full documentation on the Trine methodology will be published separately.
Practical guide: Modelling a Domain walks through domain modelling with Hermetic. For aggregate roles and the key system, see Trine overview.
The Hierarchical Key System
Every event stream is keyed by a structured path that encodes the full ancestor chain:
ROOT <- the domain root
ROOT|org:42 <- a top-level aggregate
ROOT|org:42|proj:7 <- a child aggregate
ROOT|org:42|proj:7|task:3 <- a grandchild aggregate
Keys are derived, not assigned. The handler computes the key from the aggregate's current state. No GUIDs. No database round-trips. No client-side ID generation.
- Ancestor derivation without queries. Strip the last segment to navigate up.
- No foreign key lookups. A key like
ROOT|org:42|proj:7|task:3is simultaneously a reference to the task, its project, and its organisation. - Deterministic and immutable. Command handlers compute the next child key from current state. Once assigned, a key never changes.
Full guide: Hierarchical Keys covers the key grammar, Parts, discriminators, parameters, and the Reference/Anchor system.
The Event Contract
Hermetic enforces event correctness through a bidirectional contract — compile-time rules that guarantee every event has a handler and every handler has an event. The contract has three links:
1. Commands declare what events they produce:
[RaisesEvent<ThingCreated>] // always yielded
[CanRaiseEvent<ThingNotified>] // conditionally yielded
public sealed record CreateThing(...) : ICommandLaw { ... }
2. Events declare who handles them:
[AppliedBy<Thing>]
public sealed record ThingCreated(...) : IEventLaw;
3. Handlers declare what events they process:
[Applies<ThingCreated>]
public static Thing Create(IEventEnvelope<ThingId, ThingCreated> e) => ...;
The chain is closed: Command → [RaisesEvent] → Event → [AppliedBy] → Aggregate → [Applies] → back to Event. Break any link and the build fails.
Events can flow through the system in multiple ways — inline application, async projections, upward bubbling to ancestor streams, cross-stream writes, and shared events across multiple streams. Every path is declared via attributes and verified at compile time.
Full guide: Events and Commands covers the contract, handlers, effects, and all propagation modes in detail.
The Sealed Pipeline
Hermetic is the sealed circuit between the Law (domain contracts) and the API boundary. When you declare a command, the pipeline produces — at compile time:
| Generator | What it produces |
|---|---|
CommandEndpointsSigilWork |
POST /api/command/{name} endpoint per command handler |
QueryEndpointsSigilWork |
GET /api/query/{name}/{id} endpoint per query |
CommandHandlersSigilWork |
DI registrations for all command handlers |
MartenProjectionsSigilWork |
Marten projection classes + registration with correct lifecycle |
CommandApiSealWork |
Typed command extension methods on Refit interfaces |
QueryApiSealWork |
Typed query extension methods on Refit interfaces — plus, when Microsoft.AspNetCore.SignalR.Client is referenced, a single shared ObservationConnection wrapper and per-seal Observe{Query} / Observe{Query}Descendants extensions for real-time projection observation |
ProjectionDispatcherSigilWork |
Server-side wireup for the single global ObservationHub — AddHermeticObservation calls AddSignalR() and registers the dispatcher singleton wired to IHubContext<ObservationHub>; MapHermeticObservation emits a single MapHub<ObservationHub>("/observe") call |
PolymorphicLawWork |
[JsonPolymorphic] / [JsonDerivedType] partials |
EssencePrimitiveConverterWork |
JSON converter registrations for primitive types |
NpgsqlTypeResolverWork |
Npgsql type converters for LINQ queries with essence types |
EfCoreValueConverterWork |
EF Core ValueConverter classes + one-line registration for all essence types |
The pipeline is controlled by three kinds of attributes:
- Seals (
[CommandApiSeal<T>],[QueryApiSeal<T>]) — scope which commands/queries appear on a Refit interface - Principles (
[CommandPrinciple],[QueryPrinciple]) — inject additional cross-cutting concerns (retries, caching, custom enrichment) into every generated call. Telemetry is built-in and does not require a Principle - Sigils (
[MartenProjectionsSigil],[CommandHandlersSigil], etc.) — mark where generators emit registration code
Every generated method carries [MethodImpl(AggressiveInlining | AggressiveOptimization)]. There is no dictionary lookup, no string-based dispatch, no reflection.
Full guide: The Sealed Pipeline covers Seals, Principles, and Sigils with examples.
Telemetry
Hermetic ships built-in OpenTelemetry instrumentation. Every command endpoint, query endpoint, API client extension, projection Apply/Create, and SignalR change-notification dispatch is wrapped at generation time. Register the source and meter once and it just works:
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddSource(HermeticTelemetry.ActivitySourceName)) // "Hermetic"
.WithMetrics(m => m.AddMeter(HermeticTelemetry.MeterName)); // "Hermetic"
Every wrapped operation goes through the same HermeticTelemetry.Begin(kind, name, typeFqn, identifier) entry point — only the HermeticTelemetryKind discriminator and the operation name vary. Spans are named for grep-ability (hermetic.command CreateThing, hermetic.query GetThing 42), tagged with the hermetic.* namespace, and wired into kind-specific counters and histograms (hermetic.commands.received, hermetic.commands.duration, hermetic.events.raised, etc.).
Command endpoints stamp the current Activity.Id onto Marten session metadata before SaveChangesAsync, and projection wrappers reconstruct the parent context from IEvent.CorrelationId — so projection apply spans emitted by Marten's async daemon stay linked to the originating command trace across the daemon boundary.
Full reference: Telemetry section in the Hermetic Pipeline doc.
Primitives — Essence
Before declaring commands and aggregates, you need typed identifiers and value objects. Hermetic provides three primitive interfaces, each with compile-time enforcement (WORD analyzer series) and source-generated members:
| Interface | Kind | Declaration |
|---|---|---|
IIdentifier<T> |
Typed ID — parsable, comparable, stringable | public readonly partial record struct ThingId : IIdentifier<Guid>; |
ISmartEnum |
Closed enumeration with a string key | public sealed partial record ThingStatus : ISmartEnum; |
IEssence<T> |
Validated value object — always constructed via Create() |
public sealed partial record ThingName : Essence<string>; |
The partial keyword is always required — the Logos generators fill in serialization, parsing, equality, and validation.
Full guide: Primitives
The Three Aggregate Roles
| Interface | Role | Chronicle | Projection | Accepts commands |
|---|---|---|---|---|
IAggregateRoot<TId, TSelf> |
Owns the chronicle; is its own read model | Own stream | Inline | Yes |
ICommandAggregate<TId, TRoot> |
Command scoping; events land on root's stream | Root's stream | Inline | Yes |
IQueryAggregate<TId, TRoot> |
Pure read model; reacts to events | Root's stream | Async | No |
IAggregateRoot is self-referential — it extends IQueryAggregate<TId, TSelf>, making every aggregate root its own primary read model.
Full guide: Aggregates
Packages
| Package | What it is | Install |
|---|---|---|
BulletsForHumanity.Hermetic |
Core contracts, all attributes, and the Logos generators + code fixers bundled as analyzers | dotnet add package BulletsForHumanity.Hermetic |
For most projects: install BulletsForHumanity.Hermetic. The Logos generators and code fixers are embedded inside it and activate automatically.
Technical Properties
| Property | Detail |
|---|---|
| Runtime reflection | None. Zero. The entire generation pipeline operates at compile time. |
| Trimming | Fully trimmable. All generated code is trim-safe. |
| AOT | Fully compatible with Native AOT. No dynamic type loading, no Reflection.Emit. |
| Inlining | Every generated method carries [MethodImpl(AggressiveInlining \| AggressiveOptimization)]. |
| Targets | net10.0 for runtime projects · netstandard2.0 for analyzers/generators |
| Primary integrations | Marten (event sourcing + document DB) · Refit (typed HTTP) |
| Server framework | Plain ASP.NET Core minimal APIs — no custom server, no middleware framework |
| OpenAPI | Generated endpoints produce standard OpenAPI descriptions for cross-platform client generation |
Hermetic without Marten, without Refit? The framework is designed to hook into plain ASP.NET Core. The Marten and Refit integrations are the primary tested path, but the generation pipeline reads only from attributes and interfaces defined in
Hermetic. Plugging different infrastructure behind the same sealed surface is architecturally possible, though not yet validated.
Documentation
Guides — How to Build With Hermetic
| Guide | What it covers |
|---|---|
| Modelling a Domain | Domain structure, aggregate roles, folder layout, worked example |
| Primitives | IIdentifier, ISmartEnum, IEssence — typed IDs, closed enumerations, value objects |
| Hierarchical Keys | The key system — grammar, Parts, discriminators, parameters, References and Anchors |
| Aggregates | Three aggregate roles — Root, Command, Query — stream ownership, when to use which |
| Events and Commands | The event contract, command handlers, effects, propagation modes |
| The Sealed Pipeline | Seals, Principles, Sigils — how declarations become infrastructure |
Architecture — How Hermetic Works
| Document | What it covers |
|---|---|
| Trine Overview | Trine aggregate roles, hierarchical key system, event contract overview |
| Hermetic — Full Pipeline Reference | All attributes, all generators, the Principle pattern, Marten projection pipeline, end-to-end |
| Analyzer & Fixer Registry | Complete WORD and LAW diagnostic registry with severity, rules, and implementation status |
License
Hermetic is licensed under the Business Source License 1.1.
You may use Hermetic to build applications — commercial or otherwise. You may not use it to create a competing code-generation framework or offer it as a hosted service.
The license converts automatically to Apache 2.0 four years after each versioned release.
Feedback
This is a preview. The API will change. If you are building with Hermetic and something is wrong, missing, or beautiful — open an issue. Early signal shapes everything.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.