BulletsForHumanity.Hermetic 0.5.1

There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="BulletsForHumanity.Hermetic" Version="0.5.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="BulletsForHumanity.Hermetic" Version="0.5.1" />
                    
Directory.Packages.props
<PackageReference Include="BulletsForHumanity.Hermetic" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add BulletsForHumanity.Hermetic --version 0.5.1
                    
#r "nuget: BulletsForHumanity.Hermetic, 0.5.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package BulletsForHumanity.Hermetic@0.5.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=BulletsForHumanity.Hermetic&version=0.5.1
                    
Install as a Cake Addin
#tool nuget:?package=BulletsForHumanity.Hermetic&version=0.5.1
                    
Install as a Cake Tool

Hermetic

Build & Test NuGet

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:

  1. Trine — a formalized domain modelling methodology with three aggregate roles and a hierarchical identity system
  2. Hierarchical Keys — structured, derived stream keys that eliminate ID generation, database round-trips, and foreign key lookups
  3. The Event Contract — a bidirectional, compile-time-verified chain that proves every event in the system is correct before the code compiles
  4. 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:3 is 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 ObservationHubAddHermeticObservation 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.

Version Downloads Last Updated
0.5.11 120 7/7/2026
0.5.7 120 7/5/2026
0.5.5 108 7/5/2026
0.5.4 117 6/14/2026
0.5.2 117 4/11/2026
0.5.1 140 4/10/2026
0.4.4 138 4/6/2026
0.4.3 115 4/6/2026
0.4.1 117 4/6/2026