BFH.Hermetic.Logos
0.3.5
The functionality of this package has been merged into BulletsForHumanity.Hermetic
dotnet add package BFH.Hermetic.Logos --version 0.3.5
NuGet\Install-Package BFH.Hermetic.Logos -Version 0.3.5
<PackageReference Include="BFH.Hermetic.Logos" Version="0.3.5" />
<PackageVersion Include="BFH.Hermetic.Logos" Version="0.3.5" />
<PackageReference Include="BFH.Hermetic.Logos" />
paket add BFH.Hermetic.Logos --version 0.3.5
#r "nuget: BFH.Hermetic.Logos, 0.3.5"
#:package BFH.Hermetic.Logos@0.3.5
#addin nuget:?package=BFH.Hermetic.Logos&version=0.3.5
#tool nuget:?package=BFH.Hermetic.Logos&version=0.3.5
BFH.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 domain modelling philosophy that organises every domain as three interdependent planes: MIND (identity), ENERGY (process), MATTER (containers)
- 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 |
| Telemetry routing | Client | Transparent middleware via [CommandPrinciple] — zero boilerplate at the call site |
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 — How to Model a Domain
Trine is the structural principle behind every domain built with Hermetic. It holds that every coherent domain can be understood as three planes:
| Plane | Element | Nature | In a domain |
|---|---|---|---|
| MIND | Sulphur — the animating principle | Identity · being · what a thing is | The root entity, identity aggregates, the atoms of meaning |
| ENERGY | Mercury — the bridge, the messenger | Process · movement · what crosses boundaries | Aggregates that coordinate between MIND and MATTER |
| MATTER | Salt — the body, crystallised form | Container · persistence · what holds shape | Structures that hold and organise what MIND produces |
Every domain has exactly one omnipresent root at the apex of MIND — the single entity from which all identity emanates. The pattern is recursive: an aggregate that owns children becomes the root for those children. The same structural relationship — source, derivation, containment — repeats at every scale.
The three planes map to a folder structure, because the structure is the architecture:
MIND/
[root entity, identity aggregates, atoms of meaning]
Laws/ <- all contracts live here
ENERGY/
[process and coordination aggregates]
MATTER/
[container and persistence aggregates]
ENERGY nests inside MIND. MATTER nests inside ENERGY. What is above contains what is below; what is below depends on what is above.
Full guide: Modelling a Domain walks through Trine with a complete example domain. For the philosophical background, see Trine — Tripartite Domain Architecture.
The Hierarchical Key System
Every event stream is keyed by a structured path that encodes the full ancestor chain:
ROOT <- the omnipresent root
ROOT|org:42 <- a top-level identity 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 and injects it into the event. No GUIDs. No database round-trips. No client-side ID generation.
Why this matters:
- Ancestor derivation without queries. Strip the last segment to navigate up.
[BubblesTo<TAncestor>]uses this to copy events to any ancestor stream without a database lookup. - 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. Command handlers run sequentially against the latest aggregate state — the next child key is always computable. No race conditions.
- Immutable. An entity's position in the hierarchy is permanent. If something needs to move, it carries its own stable identity and holds a reference to its current location.
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.
This extends across five propagation modes:
| Mode | What happens | Compile-time guarantee |
|---|---|---|
| P1 — Own stream | Event applied by aggregate root | [AppliedBy<T>] ↔ [Applies<T>] |
| P2 — Query aggregate | Event updates a read model | [UpdatesQueryAggregate<T>] ↔ [Applies<T>] |
| P3 — Parent bubble | Event copies to ancestor stream via key derivation | [BubblesTo<T>] requires [AppliedBy<T>] |
| P4 — Cross-stream | Handler writes different event to different stream | Both events checked independently |
| P5 — Shared event | Same event written to multiple streams | Multiple [AppliedBy<T>], each verified |
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 |
PolymorphicLawWork |
[JsonPolymorphic] / [JsonDerivedType] partials |
EssencePrimitiveConverterWork |
JSON converter registrations for primitive types |
NpgsqlTypeResolverWork |
Npgsql type converters for LINQ queries with 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 cross-cutting concerns (telemetry, retries) into every generated call - 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.
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. The three roles mirror the Tria Prima at the aggregate level: the root is (Sulphur), the command aggregate acts (Mercury), the query aggregate holds a view (Salt).
Full guide: Aggregates
Packages
| Package | What it is | Install |
|---|---|---|
BFH.Hermetic |
Core contracts, all attributes, and the Logos generators bundled as analyzers | dotnet add package BFH.Hermetic |
BFH.Hermetic.Trine |
Meta-package — a semantic dependency on the Trine domain modelling style; pulls in BFH.Hermetic |
dotnet add package BFH.Hermetic.Trine |
BFH.Hermetic.Logos |
The standalone Roslyn analyzer + generator package — already bundled inside BFH.Hermetic |
Advanced use only |
BFH.Hermetic.Logos.Fixes |
Code fix providers for Logos analyzers — already bundled inside BFH.Hermetic |
Advanced use only |
For most projects: install BFH.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 + net10.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
BFH.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 | Trine in practice — three planes, the omnipresent root, folder structure, 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, five propagation modes |
| The Sealed Pipeline | Seals, Principles, Sigils — how declarations become infrastructure |
Architecture — How Hermetic Works
| Document | What it covers |
|---|---|
| Trine — Tripartite Domain Architecture | Full Trine model: three planes, omnipresent root, hierarchical key system, event propagation |
| 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.
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.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.