Skip to main content

Diagnostics

A source generator that produces nothing when it is misused is the hardest kind of bug to find: the type looks annotated and behaves like a plain class. Every misuse below reports a diagnostic instead.

IdSeverityMeaning
DDD00001ErrorValue objects must be records
DDD00002ErrorEntities must be classes
DDD00003ErrorEntity ids must be records
DDD00004WarningEntity id structs should be readonly
DDD00005ErrorDDDToolkit types must be partial
DDD00006ErrorDDDToolkit types cannot be generic
DDD00007ErrorThe generated identifier name is already taken
DDD00008ErrorThe identifier type argument is not supported
DDD00009ErrorA type is either an entity or an aggregate root
DDD00010ErrorValue object properties must use protected setters
DDD00011ErrorValue object properties must use init setters
DDD00013ErrorValue objects cannot be sealed
DDD00020ErrorGenerated collection properties must be get-only
DDD00021WarningReference another aggregate by its id
DDD00022WarningUse only what another module publishes
DDD00023WarningDo not hold another module's entity
DDD00024WarningAn invariant must be nested inside the entity it is about
DDD00025WarningAn invariant is nested inside a type it is not about
DDD00026WarningTwo invariants of one entity share a code
DDD00027ErrorAn invariant needs an accessible parameterless constructor
DDD00028ErrorA key part belongs on an entity or aggregate root
DDD00029WarningA key part should not have a public setter
DDD00030ErrorDeclare all key parts of a type in one file
DDD00031ErrorA [SupabaseMigrations] factory must be one the build can create
DDD00032WarningDo not ask HotChocolate's generator for a toolkit identifier's node id serializer
DDD00033WarningThe generated integration event registration must be able to construct the class
DDD00034WarningAn event's class name and its Version disagree
DDD00035ErrorAn event's class name ends in something that is not a version
DDD00036ErrorTwo events of one module share a name and version
DDD00037ErrorTwo event names give one constant name
DDD00038ErrorA row access rule is a static partial class with one Allows method
DDD00039ErrorA row access rule can only say what the database can check
DDD00040ErrorA row access rule guards an aggregate root

Most of these say the generator could not do what you asked. The rest are a different kind: they are rules about the model rather than about the declaration, and each of them names code that compiles, reads well and does not do what it looks like it does. DDD00021 is about the boundary between two aggregates; DDD00022 and DDD00023 are about the boundary between two modules and say nothing at all until a project declares itself one; DDD00024 to DDD00027 are about invariants, where the failure worth catching is a rule that is written, tested, and never run; DDD00028 to DDD00030 are about composite keys, and the section after them lists the one key-part mistake that can only be caught when the Entity Framework model is built. DDD00031 is about the Supabase export, where the failure worth catching is a module whose migrations never reach Supabase. DDD00032 is about Relay node ids, where it is a node id that silently carries nothing. DDD00033 is about the generated integration event registration, where it is an outbound class or a handler that is never registered. DDD00034 to DDD00037 are about event names, where it is a stored row or a message read back as the wrong type, or as the wrong shape. DDD00038 to DDD00041 are about row access rules, where it is a rule the database enforces differently from the C# that states it, or not at all.

That split is what the numbering is for. DDD00001 to DDD00019 are reserved for "the generator could not do what you asked", and DDD00020 upwards for rules about the model. Severity does not follow the split. DDD00020 and DDD00027 are errors even though they sit in the second group, because in both the generator drops the member rather than emitting something wrong, and a warning would leave you with a rule that silently never runs.

The table above is the complete list: DDD00012 and DDD00014 to DDD00019 have never been assigned, and the gaps are room to grow rather than something that was removed. Ids are stable and are never reused, so a number that is missing here is missing from the compiler too.


DDD00001​

Value objects must be records.

[ValueObject]
public partial class Address { } // DDD00001

[ValueObject] and [SingleValueObject<T>] generate structural equality members and an always-valid twin that derives from your type. Both require a reference record.

[ValueObject]
public partial record Address { }

Nothing is generated for the type until it is a record, so expect follow-on errors about missing members until you fix this one.


DDD00002​

Entities must be classes.

[AggregateRoot<OrderId>]
public partial record Order { } // DDD00002

Entities have identity, not value semantics, and derive from a generated base class. A record would give you value equality across all properties, which is wrong for an entity: an order whose status changed is still the same order.

[AggregateRoot<OrderId>]
public partial class Order { }

DDD00003​

Entity ids must be records.

[EntityId<Guid>]
public partial class OrderId { } // DDD00003

Identifiers rely on record equality. Use a record struct for an allocation-free id, or a record class when you need inheritance or the always-valid twin:

[EntityId<Guid>]
public readonly partial record struct OrderId;

[EntityId<Guid>]
public partial record OrderId;

See Identifiers for which to choose.


DDD00004​

Entity id structs should be readonly.

[EntityId<Guid>]
public partial record struct OrderId; // DDD00004, generation still happens

A warning, not an error: the identifier is generated either way. Adding readonly states that the id cannot be mutated after construction and lets the compiler skip defensive copies when the struct is passed around.

[EntityId<Guid>]
public readonly partial record struct OrderId;

DDD00005​

DDDToolkit types must be partial.

[AggregateRoot<OrderId>]
public class Order { } // DDD00005

The generator adds a second declaration of your type, which requires partial. Add the keyword:

[AggregateRoot<OrderId>]
public partial class Order { }

DDD00006​

DDDToolkit types cannot be generic.

[EntityId<Guid>]
public readonly partial record struct Reference<T>; // DDD00006

public partial class Repository<T>
{
[AggregateRoot<OrderId>]
public partial class Entry { } // DDD00006, through its container
}

A type nested in a non-generic container is fine and generates normally.

The generated members have to name your type from places that cannot see a type parameter. A struct identifier carries [JsonConverter(typeof(Reference<T>.SystemTextJsonConverter))], and an attribute argument may not name an open generic. The Add<Module>Converters and Add<Module>GraphQlRuntimeBindings registrations live outside the type and cannot name it at all.

The rule covers a type nested inside a generic type for the same reason: the type parameter is still in scope, so the same references are still unspeakable.

Give the type a concrete identity instead:

[EntityId<Guid>]
public readonly partial record struct OrderReference;

DDD00007​

The generated identifier name is already taken.

public sealed class OrderId { } // something else, in the same namespace

[AggregateRoot<Guid>("ORD")]
public partial class Order { } // DDD00007

[AggregateRoot<Guid>] and [Entity<Guid>] name a raw value, so the toolkit generates the identifier as well, called OrderId. Another type of that name in the same namespace or containing type would be a duplicate definition. Without this diagnostic the compiler would report CS0101 against generated code you never wrote.

Either point the attribute at the identifier you already have:

[EntityId<Guid>("ORD")]
public readonly partial record struct OrderId;

[AggregateRoot<OrderId>]
public partial class Order { }

Or rename whichever of the two types should not be called OrderId.

The one exception is a partial record struct of that name with no [EntityId<T>] on it. That is taken as your own half of the generated identifier and reports nothing, which is how you add members to it:

public readonly partial record struct OrderId
{
public string Short => Value.ToString("N")[..8];
}

Such a part must be partial, must be a record struct, and must not state an accessibility different from the entity's. A part that states internal where the generated part says public would be CS0262, so it reports DDD00007 instead.

Nothing is generated for the entity until the clash is gone, so expect follow-on errors about its missing base class.


DDD00008​

The identifier type argument is not supported.

public sealed class Money { }

[AggregateRoot<Money>]
public partial class Order { } // DDD00008

The type argument of [Entity<T>] and [AggregateRoot<T>] is one of two things: an identifier you already have, meaning any type carrying [EntityId<T>] or implementing IEntityId, or the raw value an identifier should wrap. A reference type that is not a string is neither. It can be null and it is not copied by value, and an identifier has to be both.

[AggregateRoot<Guid>("ORD")] // a value the toolkit can wrap
public partial class Order { }

[AggregateRoot<OrderId>] // an identifier you declared yourself
public partial class Order { }

Guid, int, long, string, DateOnly and your own structs all work. Guid? does not: an optional identifier is OrderId?, not an identifier over a nullable value.

Nothing is generated for the entity until the type argument is one of the two.


DDD00009​

A type is either an entity or an aggregate root.

[Entity<ThingId>]
[AggregateRoot<ThingId>]
public partial class Widget { } // DDD00009

An aggregate root is a consistency boundary. A child entity lives inside one. A type cannot be both, and the two attributes generate different base types for the same declaration.

Keep whichever describes the type. Use [AggregateRoot<T>] for something you load, save and reference from elsewhere, and [Entity<T>] for something that only exists inside one aggregate. See Entities and aggregates.

Before this diagnostic existed, both attributes on one class made the generator throw and contribute nothing at all, leaving only a CS8785 about a crashed generator and no hint about the cause.


DDD00010​

Value object properties must use protected setters.

[ValueObject]
public partial record Address
{
public string City { get; init; } // DDD00010
}

A record with a publicly writable property can be cloned with with into a state that never passed validation:

var invalid = address with { City = "" };

Making the setter protected keeps with available inside the type and its always-valid twin while closing it to callers:

public string City { get; protected init; }

The copy does not go through validation, and on the always-valid twin that produces a ValidAddress holding an invalid value, even from code that only knows about Address. Why with is closed to callers goes through the mechanics, and why checking the copy at the with is not possible.

A code fix makes that change for you: protected init, or private protected init on an internal property. Callers that need a changed copy use the generated With(...) instead of with.

A positional record does not report DDD00010. Its parameters would become public init properties, so the generator declares them itself as protected init; see Positional records.


DDD00011​

Value object properties must use init setters.

[ValueObject]
public partial record Address
{
public string City { get; protected set; } // DDD00011
}

A non-init setter lets any deriving type change the value after construction. Value objects are immutable, so the setter must be init:

public string City { get; protected init; }

DDD00010 and DDD00011 are separate rules and a property with a plain public set reports both. The fix for both is protected init, and the code fix described under DDD00010 applies it.


DDD00013​

Value objects cannot be sealed.

[SingleValueObject<string>]
public sealed partial record EmailAddress { } // DDD00013

The generator emits ValidEmailAddress, which derives from EmailAddress. A sealed record cannot be derived from, so the twin cannot exist. Remove sealed.

Record structs are never affected: they are implicitly sealed but get no twin, since a struct cannot be derived from at all.


DDD00020​

Generated collection properties must be get-only.

[AggregateRoot<OrderId>]
public partial class Order
{
public partial IReadOnlyList<OrderLine> Lines { get; set; } // DDD00020
}

The generator implements the property as a read-only view over a private backing field. A setter would let a caller replace the whole collection and bypass the aggregate's invariants, which is the thing the read-only view exists to prevent.

public partial IReadOnlyList<OrderLine> Lines { get; }

Mutate through the generated field instead:

public void AddLine(OrderLine line) => _lines.Add(line);

See Entities and aggregates.


DDD00021​

Reference another aggregate by its id.

[AggregateRoot<CustomerId>]
public partial class Customer { }

[AggregateRoot<OrderId>]
public partial class Order
{
public Customer Buyer { get; private set; } // DDD00021
public IReadOnlyList<Customer> Watchers { get; } // DDD00021
}

Hold the other aggregate's id instead:

[AggregateRoot<OrderId>]
public partial class Order
{
public CustomerId Buyer { get; private set; }

public void PlaceFor(Customer customer) => Buyer = customer.Id;
}

Each aggregate is a separate loading and consistency boundary. A field or property typed as another root pulls that root inside this one: Entity Framework builds a navigation from it, one save then writes two roots, and neither Version guards its own aggregate any more. See Entities and aggregates for the longer version.

A warning, not an error. The code compiles, everything is still generated, and a team that disagrees can turn the rule off for a project:

<PropertyGroup>
<NoWarn>$(NoWarn);DDD00021</NoWarn>
</PropertyGroup>

#pragma warning disable DDD00021 around one property does not work, and neither does [SuppressMessage]. That is a limitation of source generators rather than a choice: a generator reports its diagnostics with a location rebuilt from a file path and holding no syntax tree, which is what keeps the pipeline cacheable, and the compiler has no tree to match a pragma against. NoWarn is read from the compilation options, so it reaches them. The same is true of every DDD diagnostic; this is the only one you are likely to want to switch off.

What reports and what does not​

Reports
A field or property typed as another rootYes
A collection, array or dictionary holding another rootYes
A Customer? propertyYes
A root holding another instance of its own type, such as Employee.ManagerYes
A child [Entity<T>] of the same aggregateNo
A child entity navigating back to the root that owns itNo
A property typed as the other aggregate's idNo
A method parameter or return typeNo
A static memberNo

Two rows deserve a word.

The back-navigation. A child entity may hold the root that owns it, which is the inverse navigation Entity Framework wants:

[AggregateRoot<OrderId>]
public partial class Order
{
public partial IReadOnlyList<OrderLine> Lines { get; }
}

[Entity<OrderLineId>]
public partial class OrderLine
{
public Order Order { get; private set; } // allowed
}

The exemption is checked, not assumed: Order has to hold OrderLine back, through a collection or a single property, and the reference from the child has to be single valued. A child entity pointing at some other root still reports, because that reference does widen the boundary.

Methods. order.PlaceFor(customer) takes the other root, reads what it needs and stores nothing, which is how two aggregates are meant to cooperate. Entity Framework cannot see a method either, so no navigation comes of it. The rule is about what an aggregate holds.

The rule reads the declared type of a member, so a property typed as an interface that an aggregate root happens to implement is not recognised, and neither is object.


DDD00022​

Use only what another module publishes.

// Crm.csproj: [assembly: Module("Crm")]
namespace Crm;

[AggregateRoot<CustomerId>]
public partial class Customer { }

// Sales.csproj: [assembly: Module("Sales")]
namespace Sales;

public sealed class OrderReport
{
public string Describe(Customer customer) => customer.Name; // DDD00022
}

Either publish the type, in the module that owns it:

namespace Crm;

[ModuleContract]
public sealed record CustomerSummary(CustomerId Id, string Name);

or go through something already published, which for an aggregate is usually its id and its integration events:

namespace Sales;

public sealed class OrderReport
{
public string Describe(CustomerSummary customer) => customer.Name;
}

A module is an assembly carrying [assembly: Module("Name")]. Its published contract is every type marked [ModuleContract], every type marked [IntegrationEvent], and anything nested inside one of those. Everything else the assembly declares is the owning team's business, public or not.

The rule is silent unless both assemblies declare a module, so it reports nothing in a codebase that has not opted in, and never against the framework, a NuGet package or a shared kernel. Two assemblies that declare the same module name are one module and no boundary runs between them.

It reports where you name another module's unpublished type: a parameter, a field, a base type, a generic argument, an attribute, a typeof, a new, a static call, a using alias. It cannot see a type you never name (var), an extension method called on an instance, an inherited member, or anything resolved by reflection. Modules has the full list, which is worth reading before you trust the rule.

A warning, for the same reason as DDD00021: it states a design decision, and a team adopting modules wants the list before it has to fix it. This one comes from an analyzer rather than from a generator, so #pragma warning disable DDD00022, [SuppressMessage] and NoWarn all work, and <WarningsAsErrors>$(WarningsAsErrors);DDD00022</WarningsAsErrors> turns it into a build break once the list is empty.


DDD00023​

Do not hold another module's entity.

// Sales
[AggregateRoot<Guid>("ORD")]
public partial class Order
{
public Customer Buyer { get; private set; } // DDD00023, Customer belongs to Crm
}

Hold the other module's published id, and react to what it publishes:

[AggregateRoot<Guid>("ORD")]
public partial class Order
{
public CustomerId Buyer { get; private set; }

public void PlaceFor(CustomerId customer) => Buyer = customer;
}

A property typed as another module's entity is a navigation. Entity Framework maps it, a query in one module loads rows owned by the other, and one SaveChanges writes into both inside one transaction. The two modules can then no longer be tested, migrated or separated on their own, and nothing in the code looks wrong.

Publishing the entity does not help and does not silence this rule. [ModuleContract] says you may name a type; it cannot say you may make that type part of your own transaction.

What reports and what does not​

Reports
A field or property typed as another module's [Entity<T>] or [AggregateRoot<T>]Yes
A collection, array or dictionary holding oneYes
One that the other module publishes with [ModuleContract]Yes
A property typed as the other module's idNo
An entity of the same moduleNo
An entity of an assembly that declares no moduleNo
A method parameter or return typeNo
A static memberNo
The generated backing field of a collection propertyNo, the property is reported instead

Like DDD00022 this is a warning and comes from an analyzer, so pragmas, [SuppressMessage], NoWarn and WarningsAsErrors all work on it.

Holding another module's aggregate root reports DDD00021 as well: one says hold the id, the other says do not reach across the boundary, and both are answered by the same edit. See Modules for the longer version.


DDD00024​

An invariant must be nested inside the entity it is about.

// Ordering/MustHaveLines.cs
public sealed class MustHaveLines : IInvariant<Order> // DDD00024
{
public string Code => "ORDER_HAS_NO_LINES";

public InvariantFailure? Check(Order order) => order.Lines.Count == 0 ? "..." : null;
}

Nest it inside the entity it judges, in a part of your own:

// Ordering/Invariants/MustHaveLines.cs
public partial class Order
{
public sealed class MustHaveLines : IInvariant<Order>
{
// the same body
}
}

An entity runs the rules it finds among its own nested types. That is what lets a rule read the entity's private state, and it is what keeps discovery free of a scan over the whole compilation. A rule declared anywhere else compiles, reads well, passes the unit test you wrote for it, and never runs on a save. It is the one failure the invariants feature is built to make impossible to ship unnoticed, which is why this fires at all.

This is the only one of the four that comes from a real analyzer rather than from the generator, and for a plain reason: a type outside an entity is by definition not among any entity's nested types, so only a pass over the compilation can see it. That also means #pragma warning disable DDD00024, [SuppressMessage], NoWarn and WarningsAsErrors all work on it, which the three below cannot offer.

A warning rather than an error, because an author who really did mean to hand the rule to something else of their own should be able to say so and move on.

What reports and what does not​

Reports
A top-level type implementing IInvariant<T>Yes
One nested inside a class that is not an [Entity<T>] or [AggregateRoot<T>]Yes
One that takes its Check from a base classYes, the concrete type is the rule
An abstract base shared between several rulesNo, it is not a rule
An interface deriving from IInvariant<T>No
An open generic rule, such as Rule<T>No
One nested inside the wrong entityNo, that is DDD00025

See Invariants.


DDD00025​

An invariant is nested inside a type it is not about.

public partial class Order
{
public sealed class LineMustCostSomething : IInvariant<OrderLine> // DDD00025
{
public string Code => "LINE_IS_FREE";

public InvariantFailure? Check(OrderLine line) => line.Price <= 0 ? "..." : null;
}
}

Nest it inside the type it is about instead:

public partial class OrderLine
{
public sealed class MustCostSomething : IInvariant<OrderLine> { }
}

An entity runs the nested rules that are about itself. This one is in the right kind of place and still never runs, which makes it harder to spot than DDD00024 rather than easier. In practice the type argument was copied from a rule next to it.

A rule about a base class or an interface of the entity is accepted and reported by nothing: IInvariant<T> is contravariant in T, so IInvariant<IHasCustomer> nested inside Order runs on an Order perfectly well. Only a type argument the entity cannot be handed to is reported.

Reported by the generator, so NoWarn reaches it and a pragma does not. The same is true of DDD00026 and DDD00027; DDD00021 explains why.


DDD00026​

Two invariants of one entity share a code.

public partial class Order
{
public sealed class MustHaveLines : IInvariant<Order>
{
public string Code => "ORDER_INVALID";
}

public sealed class MustStayWithinTheCreditLimit : IInvariant<Order>
{
public string Code => "ORDER_INVALID"; // DDD00026
}
}

The code exists so that a caller can act on a broken rule without matching on its message, which is the difference between a rule you can branch on and a string you can only display. Two rules answering to one code takes that back: GetInvariantViolations() returns two entries the caller cannot tell apart, and the branch that was supposed to catch one catches both.

Both rules are still generated and both still run. Nothing about this stops compiling; what stops working is the thing the code was for.

Only the second rule is reported, and only codes this pass can read as a constant are compared at all:

Code written asCompared
=> "ORDER_INVALID"Yes
{ get; } = "ORDER_INVALID"Yes
=> ViolationCode, a constYes, the constant's value
=> $"ORDER_{Reason}" or anything computedNo
Inherited from a base class as a constantYes

A code built at run time is left alone rather than guessed at, because a wrong guess would report two rules as sharing a code they do not share.


DDD00027​

An invariant needs an accessible parameterless constructor.

public partial class Order
{
public sealed class MustStayWithinTheCreditLimit : IInvariant<Order>
{
// DDD00027, the generated code inside Order cannot write new MustStayWithinTheCreditLimit()
public MustStayWithinTheCreditLimit(decimal limit) => _limit = limit;
}
}

The generator creates one instance of every rule per entity type and reuses it for every check, so a rule has to be constructible without arguments, and has to be stateless for that reuse to be sound. The limit in the example belongs to the entity, not to the rule, and the rule reads it off the Order it is handed:

public InvariantFailure? Check(Order order)
=> order.Total > order.CreditLimit ? $"An order may not exceed {order.CreditLimit}." : null;

An error rather than a warning. Nothing is generated for a rule the generated code cannot build, so a warning would leave the rule silently dropped, which is the exact silence the other three exist to prevent. The entity itself is still generated and everything else about it still works: you get this one error rather than a page of follow-on ones.

Accessibility is asked of the compiler rather than read off the modifiers, because the two do not line up here. A rule nested inside the entity may be private and is still reachable from the generated code, which is written inside that same entity; a private constructor on it is not.

Reports
A constructor taking parameters, and no otherYes
A private constructor on a rule nested in the entityYes
No constructor at all, so the implicit public oneNo
A private rule with an accessible constructorNo
An abstract base or an open generic ruleNo, neither is a rule

See Invariants.


DDD00028​

A key part belongs on an entity or aggregate root.

[ValueObject]
public partial record Address
{
[KeyPart]
public RegionId Region { get; protected init; } // DDD00028
}

[KeyPart] puts a property into a primary key ahead of the identifier. Only an [AggregateRoot<T>] or an [Entity<T>] has an identifier and a key, so anywhere else the attribute would do nothing, silently. Remove it, or move the property to the entity that is keyed on it. See Composite keys.


DDD00029​

A key part should not have a public setter.

[AggregateRoot<ProjectId>]
public partial class Project
{
[KeyPart]
public RegionId RegionId { get; set; } // DDD00029
}

Set it once, in the constructor, and make it get-only:

[KeyPart]
public RegionId RegionId { get; }

A key part is part of the primary key, and a primary key does not change once the row exists: Entity Framework refuses to save a modified key value, and every owned child's foreign key carries the same value. { get; }, private set, protected set and init are all fine; only a public, non-init setter reports. A warning: everything is still generated.


DDD00030​

Declare all key parts of a type in one file.

// Project.cs
[AggregateRoot<ProjectId>]
public partial class Project
{
[KeyPart] public RegionId RegionId { get; }
}

// Project.Period.cs
public partial class Project
{
[KeyPart] public int Period { get; } // DDD00030, reported on Project
}

Move them into one part of the class, in the order you want the key:

public partial class Project
{
[KeyPart] public RegionId RegionId { get; }
[KeyPart] public int Period { get; }
}

Key parts join the primary key in declaration order. Within one file that order is plain; between the files of a partial class there is none, only the order the compiler happens to read the files in, and a key whose column order could change with a file rename is not a key you want. Nothing is generated for the type until the key parts are together.


DDD00031​

A [SupabaseMigrations] factory must be one the build can create.

Reported in the project that turns the Supabase export on (<SupabaseMigrationsExport>), normally the host, about a factory in a module it references.

[SupabaseMigrations]
internal sealed class OrderingContextFactory : IDesignTimeDbContextFactory<OrderingContext> // DDD00031: the host cannot see it

The build creates every marked factory from code generated into the host, as SupabaseMigrationSource.For<TContext, TFactory>(). That needs a public, non-abstract, non-generic class with a public parameterless constructor, implementing IDesignTimeDbContextFactory<TContext> for a context the host can see too:

[SupabaseMigrations]
public sealed class OrderingContextFactory : IDesignTimeDbContextFactory<OrderingContext>
{
public OrderingContext CreateDbContext(string[] args) { ... }
}

The message names what is missing. A factory that fails is left out of the export, which is why this is an error: a module whose migrations were silently skipped would be found by a failing deployment, or by a branch database without its tables, instead of by the build.

DDD00032​

Do not ask HotChocolate's generator for a toolkit identifier's node id serializer.

graphql.AddNodeIdValueSerializerFrom<OrderId>(); // DDD00032

AddNodeIdValueSerializerFrom<T>() is intercepted by HotChocolate's own generator, which writes a serializer from the properties T declares in source. A toolkit identifier's Value is written by the toolkit's generator, and source generators do not see each other's output, so HotChocolate's finds no property and writes a serializer that stores nothing. Every order's node id becomes Order: and reads back as an empty OrderId. Nothing fails to compile and nothing throws.

Remove the call. Add{Module}GraphQlRuntimeBindings() already registers a serializer for every identifier over a Guid, string, int, long or short, in HotChocolate's own format; see Relay node ids.


DDD00033​

The generated integration event registration must be able to construct the class.

public sealed class PublishOrderPlaced : IOutboundIntegrationEvent<OrderPlaced, OrderPlacedV2>
{
private PublishOrderPlaced() { } // DDD00033: no constructor this assembly can call
// ...
}

The generated Add{Module}IntegrationEvents() registers every outbound class and every handler in the module by writing new for it, and takes each constructor parameter from the scope the message is delivered in. That only works for a class with one accessible constructor with the most parameters, whose parameters are not ref, out or params, and whose parameter types the assembly can see. The message says which of those failed.

A class the registration cannot construct is left out of it, so its domain event is never published, or its contract is never handled. That is a warning rather than silence because nothing else would tell you. Give the class a constructor the registration can call, or register it by hand.


DDD00034​

An event's class name and its Version disagree.

[IntegrationEvent(Version = 3)] // DDD00034, at Version = 3
public sealed record OrderPlacedV2(OrderId OrderId);

A class name that ends in V and a number is that version of its event by convention, and Version on [IntegrationEvent] states one explicitly. The stated version wins wherever the version is read: the outbox, the published message, the generated registration. So this event is version 3, and the V2 in its name is ignored. Nothing is ambiguous, which is why this is a warning and not an error, but a name that says 2 about an event that is 3 is how somebody ends up writing an upcaster for the wrong version.

There are two fixes. The first renames the class to the version it is, OrderPlacedV3, everywhere it is used; the event does not change. The second removes Version = 3, which makes the event version 2, the name's, for when the name was right and the attribute was not. See Versions are in the class name.


DDD00035​

An event's class name ends in something that is not a version.

public sealed record OrderPlacedV0(OrderId OrderId) : DomainEvent; // DDD00035
public sealed record OrderPlacedV01(OrderId OrderId) : DomainEvent; // DDD00035

Every event's class name is read for a version suffix. Versions start at 1 and are written without leading zeros, so V0 and V01 cannot be one, and reading them as part of the name instead would give this event a version rule of its own. Rename the class: V1 for a first version, or a name that does not end in V and digits. Digits that do not follow a V, as in Level2Reached, are part of the name and fine.


DDD00036​

Two events of one module share a name and version.

[assembly: Module("Ordering")]

namespace Ordering.Orders { public sealed record OrderPlaced(OrderId OrderId) : DomainEvent; } // DDD00036
namespace Ordering.Returns { public sealed record OrderPlaced(OrderId OrderId) : DomainEvent; } // DDD00036

An event is found by its name and version when a stored row or a delivered message is read back, and both of these are ordering.order-placed version 1. The registry would refuse the second at start-up; this refuses it when the module compiles, on both classes, because neither is more wrong than the other.

The code fix pins another name on the class you invoke it on, made from its namespace or containing type: [DomainEventName("ordering.returns-order-placed")] on a domain event, the name in its [IntegrationEvent("...")] on a contract. A class whose name is already pinned is offered nothing, since that name was chosen by hand. Renaming one of the classes works as well.

Two domain events are compared with each other, and two contracts with each other. A domain event and the contract it is published as share a name on purpose, and so do the versions of one event; neither is reported. The toolkit does not make a name unique from the namespace by itself, because that name would change when the class moved. See Two events with one name.


DDD00037​

Two event names give one constant name.

[DomainEventName("ordering.order-placed")]
public sealed record OrderPlaced(OrderId OrderId) : DomainEvent;

[DomainEventName("ordering.order.placed")] // DDD00037, on both
public sealed record PlacedOrder(OrderId OrderId) : DomainEvent;

Every name gets a constant in the generated {Module}EventNames, named after the name without its module and in PascalCase: both of these would be OrderingEventNames.OrderPlaced. Whichever name the constant held, code that reached for it meaning the other event would bind a topic or a test to the wrong event and never find out, so this is an error, on the events of both names. Pin one of the names to something that reads differently. Until then the first name in ordinal order keeps the constant, only so that code already using it does not add errors of its own to this one.

DDD00038​

A row access rule is a static partial class with one Allows method.

[RowAccess<Order>(RowOperations.Read)]
public static class ACustomerSeesTheirOrders // DDD00038: not partial
{
public static bool Allows(Order order) => order.PlacedBy == null; // DDD00038: no Caller
}

The generator writes the rule's SQL into another part of the class, so the class is static partial. And it translates Allows, which is a static method taking the aggregate the rule is about and a Caller, returning bool, with a single expression for a body: after =>, or as its only return statement. Nothing is generated until the rule has that shape, and a rule without SQL never reaches the database.

DDD00039​

A row access rule can only say what the database can check.

public static bool Allows(Order order, Caller caller)
=> order.Team!.StartsWith("north"); // DDD00039, on the call

A rule becomes a condition the database evaluates for every row, so it can use the aggregate's own properties, constants written in the rule, and the caller: caller.UserId, caller.IsSignedIn, caller.Role and caller.Claim("app_metadata.team"). It can compare them, with ==, !=, <, <=, > and >=, and combine the comparisons with &&, || and !. A method call, a local variable, another object or the clock has no column and no claim to become, and leaving it out would make the database answer differently from the C# method, so it is an error on the part that cannot be translated. Store what the rule needs as a property of the aggregate, or put it in the caller's app_metadata.

DDD00040​

A row access rule guards an aggregate root.

[RowAccess<OrderLine>(RowOperations.Read)] // DDD00040
public static partial class LinesOfBigOrders { ... }

An aggregate is read and changed as a whole. A rule on one of its entities could hide some of an order's lines and not the order, and Entity Framework would load half an aggregate whose invariants then check half the data. Write the rule for the root. The export gives the tables of the aggregate's entities a policy that follows it: a line is visible exactly when its order is.


DDD00041​

A row access rule reads the aggregate's entities through an access function.

[RowAccess<Project>(RowOperations.Read)]
public static partial class MembersSeeTheirProjects
{
public static bool Allows(Project project, Caller caller)
=> project.Members.Any(member => member.UserId == caller.UserId); // DDD00041
}

The tables of an aggregate's entities have policies that ask the aggregate's table whether their row is visible. A policy on the aggregate's table that read those tables would ask itself, and Postgres stops the query with infinite recursion. Put the question in an access function, which runs as its owner and reads the entities without their policies, and call it from the rule:

[AccessFunction<Project>("projects.is_member")]
public static partial class ProjectMembership
{
public static bool Allows(Project project, Caller caller)
=> project.Members.Any(member => member.UserId == caller.UserId);
}

[RowAccess<Project>(RowOperations.Read)]
public static partial class MembersSeeTheirProjects
{
public static bool Allows(Project project, Caller caller) => ProjectMembership.Allows(project, caller);
}

Building the model fails: the owned type must carry the key part​

Not a compiler diagnostic, because it depends on how the Entity Framework model is put together, but it is reported as early as that allows: when the context builds its model, before the first query.

'Project' is keyed on 'RegionId', so the foreign key of its owned 'Milestone' (through
'Project.Milestones') must carry it too, but 'Milestone' has no such property. ...

Project has [KeyPart] RegionId, so every table it owns carries RegionId in its foreign key, and Milestone has nowhere to keep it. Give the child the property and set it from the parent:

[Entity<MilestoneId>]
public partial class Milestone
{
public Milestone(RegionId regionId, MilestoneId id) : base(id) => RegionId = regionId;

[KeyPart]
public RegionId RegionId { get; }
}

The property must have the same name and the same type as the owner's. If the child really should not carry it, configure that ownership's foreign key yourself with OwnsMany(...).WithOwner().HasForeignKey(...); the convention leaves explicit configuration alone.


Nothing was generated and there is no diagnostic​

Check, in order:

  1. The generator package is referenced. DDDToolkit brings the core generators; the Entity Framework, FluentValidation and HotChocolate generators come with their own packages.
  2. The type is partial and the attribute is the generic one, [EntityId<Guid>] rather than a hand-written attribute with the same name.
  3. The build output, with EmitCompilerGeneratedFiles turned on, as described in Getting started.

If a generator throws, the compiler reports it as CS8785 or CS8784 rather than as a DDD diagnostic. That is a bug in the toolkit; please report it with the declaration that triggered it.