Skip to content

Reading the generated C#

You write .koi files; Koine emits idiomatic, self-contained C#. Before you wire the output into a project it helps to know where things land and why. This page maps the output layout, the shared runtime markers, the ID convention, and the Koine-to-C# type mapping — then shows one value object end to end.

koine build writes a flat, predictable tree under your --out directory:

  • One folder per bounded context. A context Customers { … } becomes a Customers/ folder, and every type in it is emitted into the C# namespace Customers.
  • One file per declared type, named after the type. A value Email produces Customers/Email.cs, an entity Customer produces Customers/Customer.cs, and so on.
  • One Koine/Runtime/ folder at the root holding the shared marker types (see below).
  • A module nests one level deeper: types in module Line under context Kitchen land in Kitchen/Line/ and namespace Kitchen.Line.

For the demo pizzeria domain that looks like this:

Generated/
├── Koine/Runtime/ # shared markers (emitted only when used)
├── Menu/
│ ├── Pizza.cs
│ ├── Topping.cs
│ ├── PizzaCode.cs
│ └── ...
├── Kitchen/
│ ├── Line/
│ │ └── KitchenTicket.cs
│ └── ...
└── Ordering/
├── Order.cs
├── OrderId.cs
├── IOrderRepository.cs
└── ...

Generated code has no external dependencies. Instead, a small set of shared types is emitted once into Koine/Runtime/ (namespace Koine.Runtime). Each one is emitted only when something in your model actually uses it — a model with no events never gets IDomainEvent, a model with no versioned aggregate never gets ConcurrencyConflictException.

Runtime typeEmitted when your model…Role
DomainInvariantViolationExceptiondeclares any invariant (and most other guards)thrown when an invariant or illegal transition is violated
ValueObjectdeclares any value, quantity, or *Id typebase class giving by-value equality
IAggregateRootdeclares any aggregatemarks the consistency-boundary root entity
IDomainEventdeclares any event that gets emittedthe base contract for recorded domain facts
Rangeuses a Range<T> fieldthe interval value object (Start/End, Contains, Overlaps)
IQueryHandler<TQuery, TResult>declares any querythe shared CQRS query-handler contract
ConcurrencyConflictExceptionmarks an aggregate versionedoptimistic-concurrency failure for stale writes

Because they are plain framework types with no NuGet dependency, you can commit the generated tree straight into a project and it compiles on its own.

Any type whose name ends in Id and is used as a field or identity is generated as a strongly-typed ID value object — even when no entity declares it via identified by. So a field customer: CustomerId pulls a CustomerId.cs into existence whether or not Customer is in the same context.

By default an ID wraps a Guid and gets a New() factory:

public sealed class CustomerId : ValueObject
{
public Guid Value { get; }
public CustomerId(Guid value) => Value = value;
public static CustomerId New() => new(Guid.NewGuid());
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Value;
}
}

The file is named after the ID type, not the entity: entity Order identified by OrderId emits OrderId.cs. Other identity strategies (as natural(String), as natural(Int), as sequence) change the wrapped primitive and drop New() — see entities & identity.

Codegen is deterministic: the same model always produces byte-identical output. File names, member order, and the set of runtime markers are a pure function of the model, so regenerating into the same folder is a no-op unless the model changed. That makes the generated tree safe to commit and easy to diff in code review — a meaningful diff means a meaningful model change.

In directory mode (koine build ./domain) every .koi file under the folder is read in a stable order and merged into one model, so multi-file domains are just as reproducible as single-file ones.

Primitive Koine types map to their natural C# counterparts:

KoineC#Notes
Stringstring
Intint
Decimaldecimalmoney / quantities
Boolbool
InstantDateTimeOffset
List<T>IReadOnlyList<T>defensively copied in the constructor
Range<T>Range<T>the Koine.Runtime interval value object
<XId>a generated ID value objecta ValueObject wrapping a Guid by default

Here is the Email value object from the demo’s Customers context — first the .koi source:

context Customers {
value Email {
raw: String
normalized: String = raw.trim.lower
invariant raw.trim.length > 0 "an email cannot be blank"
invariant raw matches /^[^@]+@[^@]+\.[^@]+$/ "invalid email address"
}
}

…and the C# Koine emits to Customers/Email.cs:

// <auto-generated/>
#nullable enable
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Koine.Runtime;
namespace Customers;
public sealed class Email : ValueObject
{
public string Raw { get; }
public Email(string raw)
{
if (!(raw.Trim().Length > 0))
throw new DomainInvariantViolationException(
type: nameof(Email),
rule: "an email cannot be blank");
if (!Regex.IsMatch(raw, @"^[^@]+@[^@]+\.[^@]+$", RegexOptions.None, TimeSpan.FromMilliseconds(1000)))
throw new DomainInvariantViolationException(
type: nameof(Email),
rule: "invalid email address");
Raw = raw;
}
public string Normalized => Raw.Trim().ToLowerInvariant();
protected override IEnumerable<object?> GetEqualityComponents()
{
yield return Raw;
}
}

Reading it back against the source:

  • The file opens with an // <auto-generated/> marker and #nullable enable, then a precise using set — only the namespaces actually referenced.
  • value becomes a sealed class : ValueObject; the field raw: String becomes a get-only Raw property and a constructor parameter.
  • Each invariant becomes a guard at the top of the constructor that throws DomainInvariantViolationException before the object is assigned — an invalid Email can never exist.
  • The matches /…/ regex invariant compiles to Regex.IsMatch.
  • The derived field normalized: String = raw.trim.lower (it references another field) becomes a computed get-only property Normalized, not a constructor parameter.
  • Equality is by value: GetEqualityComponents yields the identity-defining fields, and the ValueObject base supplies Equals, GetHashCode, and ==/!=.