Skip to main content
Roslyn source generators for .NET 10

You write the domain.
The compiler writes the rest.

Declare an aggregate, a value object or an identifier with one attribute. At compile time DDDToolkit generates the base types, equality, invariant checks, Entity Framework mapping and GraphQL bindings, as plain C# you can open, read and step through.

$dotnet add package Temp.DDDToolkit
Order.cs
[AggregateRoot<Guid>("ORD")]
public partial class Order
{
public Order(OrderId id, Address shipTo) : base(id)
{
ShipTo = shipTo;
RaiseDomainEvent(new OrderPlaced(id));
}
public Address ShipTo { get; private set; }
public partial IReadOnlyList<OrderLine> Lines { get; }
public sealed class MustHaveLines : IInvariant<Order>
{
public string Code => "ORDER_HAS_NO_LINES";
public InvariantFailure? Check(Order order)
=> order.Lines.Count == 0
? "An order has at least one line."
: null;
}
}
58 lines written → 963 lines generated

Why a source generator

The boilerplate is real code. Nobody should have to write it.

Typed ids, equality, invariant checks and mappings are where a DDD code base spends its lines, and where it drifts. A generator writes them at compile time from what you declared, so they are there, correct and current, without a runtime library guessing at your types.

Plain C#, in your project

The output is ordinary C# added to your compilation. Go to definition opens it and the debugger steps through it. Nothing is woven into IL or built at run time.

Never out of step

Add a property to a value object and its equality, its With() and its database mapping follow at the next build. There is no second file to forget.

Mistakes are compile errors

An invariant declared where it would never run, or a module reaching into another one’s internals, is a diagnostic in your editor instead of a bug in production.

Read more →

No reflection in the hot path

Equality, id conversions and invariant checks call your members directly. A struct id costs what the Guid inside it costs, and the benchmarks show it.

Read more →

Only what you use

An entity without rules gets no rule list and no check to run. What you do not need is not generated, so it costs nothing at all.

One declaration, every integration

Reference the Entity Framework or HotChocolate package and the same attribute also brings value converters, GraphQL scalars and Relay node ids.

What gets generated

58 lines in, 963 lines out

An order, its lines, an address and one event. Pick a file to see what the generators wrote for it. This is their real output for these four files, with only the global:: prefixes taken out.

Order’s base class, AggregateRoot<OrderId>, and the constructor Entity Framework needs. The invariant check that runs MustHaveLines before every save. And the list behind Lines: inside Order you add to _lines, outside it callers can only read.

Order.g.cs · 251 lines
// <auto-generated/>
#nullable enable
#pragma warning disable CS1591, CS8618, CS8669
namespace Shop;
partial class Order : DDDToolkit.BaseTypes.AggregateRoot<Shop.OrderId>
{
/// <summary>Parameterless constructor for persistence frameworks and serializers.</summary>
protected Order()
{
}
/// <summary>
/// Implement this in your own part of the class to state what must be true of this
/// aggregate after every change, and throw (for example with <c>InvariantViolation(...)</c>)
/// when it is not. Write it without an accessibility modifier:
/// <code>partial void CheckInvariants() { ... }</code>
/// Leave it out and nothing runs: the compiler removes an unimplemented partial method
/// and every call to it. A rule worth a name of its own is better written as a nested
/// <c>IInvariant&lt;Order&gt;</c>, which this aggregate runs as well.
/// </summary>
partial void CheckInvariants();
/// <summary>The rules declared inside this type, created once and reused for every check.</summary>
private static readonly DDDToolkit.Invariants.IInvariant<Shop.Order>[] __invariants =
[
new Shop.Order.MustHaveLines(),
];
/// <summary>
/// Runs every rule and then the seam, and adds what is broken to <paramref name="violations"/>.
/// Never throws for a broken rule: throwing is the boundary's job, and it is done in exactly
/// one place.
/// </summary>
/// <param name="violations">
/// The list being built, created on the first failure and left <see langword="null"/> while
/// there is none. This runs for every changed entity on every save, so the consistent case is
/// the one that has to cost nothing.
/// </param>
/// <param name="seamFailure">
/// What <c>CheckInvariants()</c> threw, so the caller that throws can keep it as the inner
/// exception and lose no stack trace, or <see langword="null"/> when the seam was happy.
/// </param>
private void CollectInvariantViolations(
ref System.Collections.Generic.List<DDDToolkit.Invariants.InvariantViolation>? violations,
out DDDToolkit.Exceptions.InvariantViolationException? seamFailure)
{
foreach (var invariant in __invariants)
{
// Null means the rule holds, and a rule that holds allocates nothing.
var failure = invariant.Check(this);
if (failure is not null)
{
violations ??= new System.Collections.Generic.List<DDDToolkit.Invariants.InvariantViolation>();
violations.Add(new DDDToolkit.Invariants.InvariantViolation(invariant.Code, failure.Message, typeof(Shop.Order), Id) { Arguments = failure.Arguments });
}
}
seamFailure = null;
try
{
CheckInvariants();
}
catch (DDDToolkit.Exceptions.InvariantViolationException failure)
{
// The seam reports by throwing, because that is the shape that lets it be erased when
// nobody implements it. Catching it here is what lets the asking stage see what it
// found without every caller having to catch.
seamFailure = failure;
violations ??= new System.Collections.Generic.List<DDDToolkit.Invariants.InvariantViolation>();
var reported = failure.Violations;
if (reported.Count == 0)
{
// Thrown with a message of its own rather than through InvariantViolation(...), so
// the message is all there is to report.
violations.Add(new DDDToolkit.Invariants.InvariantViolation(DDDToolkit.Invariants.InvariantViolation.SeamCode, failure.Message, typeof(Shop.Order), Id));
}
else
{
for (var index = 0; index < reported.Count; index++)
{
violations.Add(new DDDToolkit.Invariants.InvariantViolation(DDDToolkit.Invariants.InvariantViolation.SeamCode, reported[index], typeof(Shop.Order), Id));
}
}
}
}
/// <summary>
/// Asks every child entity this aggregate holds what it has broken, and adds the answers to
/// <paramref name="violations"/>. A child answers the same way, so a grandchild is reached
/// without this method having to know it exists.
/// <para>
/// Collections only, and never a single reference to another entity: a child that points back
/// at its parent would recurse forever, and the visited set that would stop it costs an
/// allocation on every call, including the consistent one that has to stay free. Owning its
/// children in collections is the shape an aggregate actually has, so ruling cycles out by
/// construction misses nothing real.
/// </para>
/// </summary>
private void CollectChildInvariantViolations(ref System.Collections.Generic.List<DDDToolkit.Invariants.InvariantViolation>? violations)
{
foreach (var child in _lines)
{
if (child is null)
{
continue;
}
var broken = child.GetInvariantViolations();
if (broken.Count == 0)
{
continue;
}
violations ??= new System.Collections.Generic.List<DDDToolkit.Invariants.InvariantViolation>();
for (var index = 0; index < broken.Count; index++)
{
violations.Add(broken[index]);
}
}
}
/// <summary>
/// Throws the single exception both stages raise, naming this aggregate, its id and every
/// rule found broken.
/// </summary>
/// <param name="violations">
/// Everything that was found, this aggregate's own first. Each names the entity that reported
/// it, which is how the exception tells a child's violation from this aggregate's own.
/// </param>
/// <param name="seamFailure">What the seam threw, kept as the inner exception so no stack trace is lost.</param>
private void ThrowInvariantViolations(
System.Collections.Generic.List<DDDToolkit.Invariants.InvariantViolation> violations,
DDDToolkit.Exceptions.InvariantViolationException? seamFailure)
{
// The violations go in whole, codes and arguments included, so a handler can translate the
// throwing path as well as the asking one. The exception phrases the ones it does not own.
throw new DDDToolkit.Exceptions.InvariantViolationException(typeof(Shop.Order), Id, violations, seamFailure);
}
/// <summary>
/// Runs this aggregate's rules and its <c>CheckInvariants()</c> seam and returns every rule that is
/// broken, without throwing and without asking the children this aggregate holds. Empty means
/// this object is consistent and says nothing about what is inside it.
/// <para>
/// For a caller that already walks the graph and asks each object in it separately, which is
/// what the save does from the change tracker. Anything else wants
/// <see cref="GetInvariantViolations"/>.
/// </para>
/// </summary>
public override System.Collections.Generic.IReadOnlyList<DDDToolkit.Invariants.InvariantViolation> GetOwnInvariantViolations()
{
System.Collections.Generic.List<DDDToolkit.Invariants.InvariantViolation>? violations = null;
CollectInvariantViolations(ref violations, out _);
if (violations is null)
{
return System.Array.Empty<DDDToolkit.Invariants.InvariantViolation>();
}
return violations;
}
/// <summary>
/// Runs this aggregate's rules and its <c>CheckInvariants()</c> seam and throws when it finds something
/// broken, without asking the children this aggregate holds. Called by
/// <c>DDDToolkit.EntityFramework</c> on every object a save writes, which is why it must not
/// walk: the save reaches the children itself, from the change tracker, and would otherwise
/// be told about each of them twice.
/// <para>
/// Throws one <c>InvariantViolationException</c> naming this type, its id and every rule it
/// found broken, with whatever the seam threw as its inner exception. At the save, broken is
/// no longer an answer.
/// </para>
/// </summary>
public override void EnsureOwnInvariants()
{
System.Collections.Generic.List<DDDToolkit.Invariants.InvariantViolation>? violations = null;
CollectInvariantViolations(ref violations, out var seamFailure);
if (violations is null)
{
return;
}
ThrowInvariantViolations(violations, seamFailure);
}
/// <summary>
/// Runs this aggregate's rules and its <c>CheckInvariants()</c> seam, and then asks every child entity this
/// aggregate holds, and returns every rule that is broken, without throwing. Empty means the
/// whole aggregate is consistent. Ask this before the save, while "not yet" is still an answer
/// you want to handle.
/// <para>
/// Each violation names the entity that reported it, so a caller can tell which child is the
/// problem without reading a message. This asks every child this aggregate holds, where the
/// save asks only the ones it is about to write, so an empty answer here is never contradicted
/// by the save.
/// </para>
/// </summary>
public override System.Collections.Generic.IReadOnlyList<DDDToolkit.Invariants.InvariantViolation> GetInvariantViolations()
{
System.Collections.Generic.List<DDDToolkit.Invariants.InvariantViolation>? violations = null;
CollectInvariantViolations(ref violations, out _);
CollectChildInvariantViolations(ref violations);
if (violations is null)
{
return System.Array.Empty<DDDToolkit.Invariants.InvariantViolation>();
}
return violations;
}
/// <summary>
/// Runs this aggregate's rules and its <c>CheckInvariants()</c> seam and the invariants of every child entity
/// this aggregate holds, and throws when it finds something broken. This is the question a
/// command handler asks after it has acted: the aggregate is the consistency boundary, so
/// answering for it means answering for what is inside it. No database is involved.
/// <para>
/// Throws one <c>InvariantViolationException</c> naming this type, its id and every rule it
/// found broken, a child's phrased with that child's own type and id, and with whatever the
/// seam threw as its inner exception. At the save, broken is no longer an answer.
/// </para>
/// </summary>
public override void EnsureInvariants()
{
System.Collections.Generic.List<DDDToolkit.Invariants.InvariantViolation>? violations = null;
CollectInvariantViolations(ref violations, out var seamFailure);
CollectChildInvariantViolations(ref violations);
if (violations is null)
{
return;
}
ThrowInvariantViolations(violations, seamFailure);
}
private readonly System.Collections.Generic.List<Shop.OrderLine> _lines = new();
private System.Collections.Generic.IReadOnlyList<Shop.OrderLine>? __linesView;
/// <summary>Read-only view over <see cref="_lines"/>. Mutate the collection through the field.</summary>
[Microsoft.EntityFrameworkCore.BackingField(nameof(_lines))]
public partial System.Collections.Generic.IReadOnlyList<Shop.OrderLine> Lines => __linesView ??= _lines.AsReadOnly();
}

See it in your own project with go to definition on any generated member, or on disk with EmitCompilerGeneratedFiles. What each attribute generates →

One domain, hosted any way you like

The example is a shop in five modules: Catalog, Ordering, Inventory, Payments and Shipping. The same modules run as one monolith and as three services, over four transports, and the same checkout scenarios pass against every one.

See the samples
Modular monolithSupabase or SQL Server
in process
Services over pgmqone Postgres, a queue per service
pgmq
Services over RabbitMQa database per service
Wolverine
Services over RabbitMQSQL Server per service
MassTransit

Write the domain. Let the compiler write the rest.

Start with the guide