Skip to main content

Diagnostics

A source generator that cannot handle a declaration produces nothing. The type then compiles as a plain class without the generated members, and nothing says why. The DDDToolkit generators and analyzers report a diagnostic for each such misuse instead. Each section below shows code that triggers one diagnostic, why the code is a problem, and the fix.

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
DDD00014WarningThe generators cannot read the project's MSBuild properties
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] context or factory must be one the build can make
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, an access function or a database question has the shape the generator reads
DDD00039ErrorA row access rule can only say what the database can check
DDD00040ErrorA row access rule guards an aggregate root
DDD00041ErrorA row access rule reads the aggregate's entities through an access function
DDD00042ErrorA parent for entities is an abstract generic class whose first type parameter is the id
DDD00043ErrorA template's first type argument is an entity id
DDD00044ErrorA template takes a type from a class nobody declares
DDD00045ErrorA template takes a type from a class declared more than once
DDD00046ErrorA template attribute fills exactly the type parameters of its parent
DDD00047ErrorA class is declared an entity or aggregate root once
DDD00048ErrorA class a template takes meets its parent's constraints
DDD00049ErrorA template registration needs a class declared with each of its templates
DDD00050ErrorA type a template registration takes meets the method's constraints
DDD00051ErrorA set-shaped question is asked once per statement, so its arguments do not read the row
DDD00052ErrorA function named without its schema belongs to a module
DDD00053ErrorA type argument of a template meets its parent's constraints
DDD00054WarningA package's row access contribution is made from what the application marks
DDD00055WarningA context's migration files are named after its module
DDD00056ErrorA request interface is one a behavior can be written for
DDD00057ErrorThe mediator's pipeline behavior has the shape the generator writes a behavior for
DDD00058ErrorA notification implements no request interface
DDD00059WarningThe member list of a resource is written from what the resource declares
DDD00060WarningA member class names an aggregate root whose members it is
DDD00061WarningA request that declares its access is sent, not handed to its handler
DDD00062ErrorA class of one GraphQL schema is one the toolkit alone registers
DDD00064WarningDDD_Module declares the module where the package's build step runs
DDD00065InfoThe class a package's use cases are named through is written where each of its templates has one class
DDD00066ErrorA package's switch writes a class or an id where its name is free and its id is known
DDD00067ErrorA class whose package makes its new ids is declared over an id with a Create()
DDD00068WarningDDD_ModuleContracts makes a project its module's contracts where the project can name the attribute
DDD00069WarningA module's row access contribution is listed by the project that runs the export
DDD00070ErrorA member a library marks for a package's row access contribution is public
DDD00071ErrorWhat a package's row access contribution is made from is found once, and is what it takes
DDD00072ErrorA row access contribution a package writes is not listed again
DDD00073WarningWhat [assembly: LeaveOutRowAccessContribution] names is a contribution a package writes, and a context
DDD00074WarningA design-time factory keeps the migration history where the application does
DDD00075WarningEvery class a package's use cases are named through has a name of its own where a project sees it
DDD00076Warning[assembly: TemplateFacadeName] names, once, a class this project gets
DDD00077ErrorA request says what it requires of its caller
DDD00078ErrorA request says what it requires in one way
DDD00079Error[AllowAnonymous] goes with no other requirement attribute
DDD00080ErrorA permission marker names its permission, and a marker argument belongs to one marker
DDD00081ErrorA marker is on a member of the id it marks
DDD00082ErrorA requirement for queries only is on a query
DDD00083ErrorA request's requirement attributes are read where its module's interface is partial
DDD00084WarningA requirement attribute beside another adds what that one does not require already
DDD00085Error[OrOwn] pairs with one permission, and [Own] needs [OwnOnly]
DDD00086Error[OrOwn] and [Own] mark a member of a type something says the caller's own of
DDD00087Error[Owner] marks the one owner of an aggregate root
DDD00088Error[Owner] marks a property of a type the caller has an own of
DDD00089ErrorA module's default requirement is one any of its requests may take
DDD00090WarningThe names of resources with members are those of the newest access file
DDD00091Error[Members] names a resource with what can be converted to a code and a function's name
DDD00092ErrorA permission is a public constant of a [Permissions] class, with a level
DDD00093ErrorA permission's level fits what it is asked on
DDD00094ErrorThe rules of a resource are made from the permissions declared on it
DDD00095ErrorA module's permissions are declared in a [Permissions] class, not on a list of its own
DDD00096Warning[OrOwn] beside a permission the owner of the resource holds already says nothing more
DDD00097Error[At] marks the one place an aggregate root hangs at
DDD00098ErrorRules that reach a resource from above read where it hangs from its [At] property
DDD00099WarningA permission says what it lets somebody do
DDD00100ErrorA restrictive rule narrows a table it can reach
DDD00101ErrorCurrentSeat answers with the application's seat class
DDD00102ErrorA marker of a requirement's argument goes beside the attribute whose call it is
DDD00103ErrorAn operator's command takes its tenant from the request or from itself
DDD00104Error[Operator] stands beside no requirement an operator cannot meet
DDD00105Warning[Operator] on a query names no tenant and no scope
DDD00106InfoA member or role manager is not written
DDD00107ErrorThe registration of an access behavior is written and in scope where it is called
DDD00108WarningThe migrations make the model the access files are written from
DDD00109ErrorA permission marker on a collection of ids takes no marker argument and no [OrOwn]
DDD00110ErrorA request, a command or a query has one handler

The Meaning column is the title the IDE shows for each id.

Most of these diagnostics say that a generator could not do what a declaration asks. The others are rules about the model. The code they report compiles, reads well, and does something other than what it seems to do. These rules, by topic:

The numbers follow this split. DDD00001 to DDD00019 are reserved for "the generator could not do what you asked". DDD00020 and up are rules about the model. These ids are exceptions, numbered later because they were added later:

Each of these says what the generator could not do.

The severity does not follow the split:

  • DDD00020 and DDD00027 are errors in the second group. In both, the generator drops the member instead of emitting something wrong. A warning would leave you with a rule that never runs, and no error.
  • DDD00014 is a warning in the first group. The generator could not read the name the project asked for. The code it generates under the assembly's name still compiles.

The table above lists every id. DDD00012 and DDD00015 to DDD00019 were never assigned. DDD00063 was removed when a module's permissions moved into a [Permissions] class. DDD00095 now reports a list marked [TenancyPermissions] by hand. Ids are stable and never reused, so an id missing from this page 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 need a record class:

[ValueObject]
public partial record Address { }

The generator generates nothing for the type until it is a record. Expect follow-on errors about missing members until you fix this one.


DDD00002​

Entities must be classes.

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

An entity has identity. Two entities are the same entity when their ids are equal. A record gives value equality over all its properties. For an entity this is wrong, because an order whose status changed is still the same order. An entity also derives from a generated base class, and only a class can do that. Declare it as a partial class:

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

The generator generates nothing for the type until it is a class.


DDD00003​

Entity ids must be records.

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

An identifier uses record equality. Declare it as a readonly partial record struct for an id that allocates nothing. Declare it as a partial record (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;

Identifiers explains which one to choose. The generator generates nothing for the type until it is a record.


DDD00004​

Entity id structs should be readonly.

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

DDD00004 is a warning. The identifier is generated either way. With readonly, the compiler refuses any change to the id after construction, and it skips the defensive copies it otherwise makes 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. Two declarations of one type need partial. Add the keyword:

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

The generator generates nothing for the type until it is partial.


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 code names your type from places where a type parameter is not in scope:

  • A struct identifier carries [JsonConverter(typeof(Reference<T>.SystemTextJsonConverter))]. An attribute argument cannot name an open generic type.
  • The Add<Module>Converters and Add<Module>GraphQlRuntimeBindings registrations are outside the type. They cannot name an open generic type at all.

A type nested inside a generic type has the same problem. The outer type parameter is still in scope, so the generated code still cannot name the type.

Give the type a concrete identity instead:

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

The generator generates nothing for the type until it has no type parameters.


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 also generates the identifier. It names the identifier after the entity, with Id added: OrderId. A second type of that name in the same namespace or containing type would be a duplicate definition. Without DDD00007, the compiler would report CS0101 against generated code you never wrote.

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 the type that should not be called OrderId.

One case reports nothing: a partial record struct of that name without [EntityId<T>]. The generator takes it as your part of the generated identifier. Use such a part to add members to the identifier:

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

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

The generator generates nothing for the entity until the name clash is gone. Expect follow-on errors about the entity's 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: a type with [EntityId<T>], or any type that implements IEntityId.
  • The raw value that a generated identifier wraps.

A reference type other than string is neither. It can be null, and it is copied by reference. An identifier must never be null and must be copied by value.

[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 work. For an optional identifier, make the identifier nullable: OrderId?.

The generator generates nothing 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 aggregate. A type cannot be both, and the two attributes generate different base types for the same declaration.

Keep the attribute that describes the type:

  • [AggregateRoot<T>] for something you load, save and reference from elsewhere.
  • [Entity<T>] for something that exists only inside one aggregate.

See Entities and aggregates. The generator generates nothing for the type until one of the two attributes is removed.

Before DDD00009 existed, both attributes on one class made the generator throw. The generator then added nothing, and the only error was CS8785, a crashed generator, without the cause.


DDD00010​

Value object properties must use protected setters.

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

A caller can copy a record with a public init setter into a state that never passed validation:

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

A protected setter keeps with available inside the type and its always-valid twin, and closes it to callers:

public string City { get; protected init; }

The copy skips validation. On the always-valid twin, with produces a ValidAddress that holds an invalid value, even from code that only knows Address. Why with is closed to callers explains the mechanics, and why the toolkit cannot check the copy at the with.

A code fix makes the change: protected init, or private protected init on an internal property. A caller that needs a changed copy uses 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 (Positional records).


DDD00011​

Value object properties must use init setters.

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

A setter without init lets every derived 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. A property with a plain public set reports both. protected init fixes both, and the code fix 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. No type can derive from a sealed record, so the twin cannot exist. Remove sealed.

A record struct never reports DDD00013. A struct is sealed implicitly, and it gets no twin, because no type can derive from a struct.


DDD00014​

The generators cannot read the project's MSBuild properties.

<PropertyGroup>
<DDD_Module>Billing</DDD_Module> <!-- ignored: the generators never see it -->
</PropertyGroup>

<ItemGroup>
<PackageReference Include="DylansTechStack.DDD.Analyzers" Version="3.1.0"
PrivateAssets="all" ExcludeAssets="build;buildTransitive" /> <!-- DDD00014 -->
</ItemGroup>

A source generator sees only the MSBuild properties that the project lists as CompilerVisibleProperty. The DTS.DDD.Analyzers package lists the properties the toolkit's generators read, in a props file. NuGet imports that file into every project the generators arrive in. Here the generators arrived and the props file did not. The generators then ignore DDD_Module, whatever the project sets. They name {Module}EventNames, Add{Module}Converters, Add{Module}IntegrationEvents and Add{Module}GraphQlRuntimeBindings after the assembly. Before DDD00014 existed, this happened without any message.

The generator tells the two cases apart:

  • A property that is listed and not set reaches the generator as an empty value. An empty value means "use the assembly name". It is never reported.
  • A property that is not listed does not reach the generator at all. DDD00014 reports this case.

It happens in two ways. The fix for each:

  • The reference leaves out the package's build assets, with ExcludeAssets, or with an IncludeAssets that names analyzers and not build and buildTransitive. Remove the restriction. PrivateAssets="all" is fine. It decides what flows on to projects that reference yours, and does not limit what your project gets.

  • The generator is referenced as an assembly instead of a package: an <Analyzer Include="...dll" />, or a project reference with OutputItemType="Analyzer". Then nothing imports a props file. List the property yourself, in the project or in Directory.Build.props:

    <ItemGroup>
    <CompilerVisibleProperty Include="DDD_Module" />
    </ItemGroup>

    Such a project also gets no build step from the package. DDD_Module then names the project's generated code and declares no module, and DDD00064 reports that. Import the package's targets file as well, or declare the module with [assembly: Module].

DDD00014 is reported once per project, without a line, because no line of your code is wrong. An assembly that declares [assembly: Module] is not reported. The module names its generated code, and the generator does not read the property. See DDD_Module, and the package that brings it.


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 skip the aggregate's invariants. The read-only view exists to prevent that. Remove the setter:

public partial IReadOnlyList<OrderLine> Lines { get; }

Change the collection through the generated field, from inside the entity:

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

The generator does not implement the property until it is get-only, so DDD00020 is an error. 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 boundary for loading and for consistency. 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.
  • The Version of each root no longer guards its own aggregate.

Entities and aggregates explains this in more detail.

DDD00021 is a warning. The code compiles, and everything is still generated. 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 [SuppressMessage] does not work either. This is a limit of source generators. A generator reports a diagnostic with a location that it rebuilds from a file path, without a syntax tree. This keeps the generator's pipeline cacheable. The compiler then has no tree to match a pragma against. NoWarn is read from the compilation options, so it works. The same holds for every DDD diagnostic that a generator reports. DDD00021 is the one you are most likely to turn 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 of these rows need more detail.

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

[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 rule checks this exception. Order must hold OrderLine, through a collection or a single property, and the reference from the child must be a single value. A child entity that points at another root still reports, because that reference widens the boundary.

Methods. order.PlaceFor(customer) takes the other root, reads what it needs and stores nothing. This is how two aggregates work together. Entity Framework does not see methods, so a method creates no navigation. The rule is about what an aggregate holds.

The rule reads the declared type of a member. It does not recognise a property typed as an interface that an aggregate root implements, or a property typed as object.


DDD00022​

Use only what another module publishes.

// Crm.csproj: <DDD_Module>Crm</DDD_Module>
namespace Crm;

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

// Sales.csproj: <DDD_Module>Sales</DDD_Module>
namespace Sales;

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

Publish the type in the module that owns it:

namespace Crm;

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

Or put the type in Crm's contracts project, if it belongs there. A project that declares itself its module's contracts, with <DDD_ModuleContracts>true</DDD_ModuleContracts> or [assembly: ModuleContracts], publishes every public type it declares, without a mark on each (A contracts project). Or use something the module publishes already. For an aggregate, that 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 that declares a module. It declares it with DDD_Module in its project file, which the build turns into [assembly: Module("Name")], or with that attribute in its source. A module's published contract is:

  • every type marked [ModuleContract];
  • every type marked [IntegrationEvent];
  • every type nested inside one of those;
  • every public type of a project of the module that declares itself the module's contracts.

Everything else the assembly declares belongs to the team that owns the module, public or not. A project is never the module's contracts because of its name. Legal.Contracts may be a domain about legal contracts. Only the property or the attribute makes a project the contracts project. An internal type of a contracts project is not published, even to a project that its InternalsVisibleTo lets in.

The rule reports nothing unless both assemblies declare a module. So it reports nothing in a codebase that has not opted in, and nothing against the framework, a NuGet package or a shared kernel. Two assemblies that declare the same module name are one module, with no boundary between them. If the rule reports the types of a shared kernel, the kernel sets DDD_Module to name its generated code, and the build declared it a module because of that. Set <DDD_DeclareModule>false</DDD_DeclareModule> beside it, or remove DDD_Module (A module named by its folder).

The rule 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 does not 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. Read it before you rely on the rule.

DDD00022 is a warning, for the same reason as DDD00021. It states a design decision, and a team that adopts modules wants the list of findings before it has to fix them. DDD00022 comes from an analyzer, so #pragma warning disable DDD00022, [SuppressMessage] and NoWarn all work. <WarningsAsErrors>$(WarningsAsErrors);DDD00022</WarningsAsErrors> turns it into a build error 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 the events the other module 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 that the other module owns.
  • One SaveChanges writes into both modules in one transaction.

The two modules can then no longer be tested, migrated or separated on their own, and the code shows no sign of it.

Publishing the entity does not fix this, and does not silence DDD00023. Declaring the entity in the other module's contracts project does not either. [ModuleContract] and [assembly: ModuleContracts] let you name a type. They do not let you 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
One in the other module's contracts project, DDD_ModuleContracts or [assembly: ModuleContracts]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, DDD00023 is a warning from an analyzer. Pragmas, [SuppressMessage], NoWarn and WarningsAsErrors all work on it.

A property that holds another module's aggregate root also reports DDD00021. DDD00021 asks for the id, and DDD00023 forbids the reference across the module boundary. The same edit fixes both. See Modules for more detail.


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 checks, 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. This lets a rule read the entity's private state, and the generator finds the rules without a scan over the whole compilation. A rule declared anywhere else compiles, reads well and passes the unit test you wrote for it. It never runs on a save. DDD00024 reports such a rule, because otherwise nothing would.

DDD00024 is the only invariant diagnostic that comes from an analyzer. The generator reports the other three. A type outside an entity is not among any entity's nested types, so only a pass over the whole compilation can find it. As a result, #pragma warning disable DDD00024, [SuppressMessage], NoWarn and WarningsAsErrors all work on DDD00024. A pragma and [SuppressMessage] do not work on the other three.

DDD00024 is a warning. An author who means to hand the rule to other code of their own can suppress it 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:

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

An entity runs only the nested rules that are about itself. This rule is in the right kind of place and still never runs, so it is harder to spot than DDD00024. Usually 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 nothing reports it. IInvariant<T> is contravariant in T, so an IInvariant<IHasCustomer> nested inside Order runs on an Order. Only a type argument that the entity cannot be passed as is reported.

The generator reports DDD00025, so NoWarn works on it and a pragma does not. The same holds for 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 lets a caller act on a broken rule without matching on its message. A caller can branch on a code. A caller can only display a message. Two rules with one code take this away. GetInvariantViolations() returns two entries that the caller cannot tell apart, and a branch meant for one rule catches both.

Both rules are still generated, and both still run. The project still compiles. Only the caller loses the ability to tell the two rules apart.

Only the second rule is reported. The generator compares only codes it can read as a constant:

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

The generator skips a code that is built at run time. A guess could report two rules as sharing a code that 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 must have a constructor without parameters, and it must be stateless for the reuse to be safe. In the example, the limit belongs to the entity. The rule reads it from the Order it is given:

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

DDD00027 is an error. The generator generates nothing for a rule that the generated code cannot construct. As a warning, DDD00027 would let that rule disappear with no error, and the other three invariant diagnostics exist to prevent a rule that never runs. The entity itself is still generated, and the rest of it works. You get this one error and no follow-on errors.

The generator asks the compiler whether the constructor is accessible. It does not read the modifiers, because here the two differ. A rule nested inside the entity may be private, and the generated code still reaches it, because the generated code is written inside the same entity. A private constructor on the rule is out of reach.

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 the primary key, ahead of the identifier. Only an [AggregateRoot<T>] or an [Entity<T>] has an identifier and a primary key. On any other type the attribute would do nothing, and nothing would say so. 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. A primary key does not change once the row exists. Entity Framework refuses to save a changed key value, and the foreign key of every owned child carries the same value. { get; }, private set, protected set and init are all fine. Only a public setter without init reports. DDD00029 is a warning, and 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 in the key:

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

Key parts join the primary key in the order they are declared. Within one file, that order is clear. Between the files of a partial class, there is no declaration order, only the order in which the compiler reads the files. A file rename could then change the column order of the key. The generator generates nothing for the type until its key parts are in one file.


DDD00031​

A [SupabaseMigrations] context or factory must be one the build can make.

DDD00031 is reported in two projects:

  • In the project of a context marked [SupabaseMigrations], about a context that the build cannot write a design-time factory for.
  • In the project that turns the Supabase export on (<SupabaseMigrationsExport>), normally the host, about a context or a factory in a module it references.
[SupabaseMigrations]
public sealed class OrderingContext(DbContextOptions<OrderingContext> options, string schema) : DbContext(options) // DDD00031: no constructor that takes its options alone

For a marked context, the build writes a design-time factory beside it, OrderingContextDesignTimeFactory. That factory makes the context with new OrderingContext(options) on Npgsql. The build can write the factory when:

  • the context is neither abstract nor generic, and not private inside another type;
  • the context has a constructor, not private or protected, that takes its options alone: DbContextOptions<OrderingContext> or DbContextOptions, followed only by optional or params parameters;
  • that constructor is marked [SetsRequiredMembers] if the context has required members;
  • the project references Npgsql.EntityFrameworkCore.PostgreSQL.

If the build cannot write a factory, write a factory of your own beside the context. The build then writes none, uses yours, and reports nothing.

The project that exports also reports:

  • A marked context whose assembly has no factory for it.
  • A marked context with more than one factory, in its assembly or in the exporting project. The export cannot tell which factory to make the context with. Mark the factory to use [SupabaseMigrations] instead of the context.
  • A marked context that the exporting project cannot see.
  • A module's own factory that the exporting project cannot create: a factory that is not public, or that has no public parameterless constructor.

A factory may be marked instead of its context, as in versions up to 3.1. The project that exports reports it:

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

The build creates every marked factory from code that it generates into the host, as SupabaseMigrationSource.For<TContext, TFactory>(). For this, the factory must be a public, non-abstract, non-generic class with a public parameterless constructor. It must implement IDesignTimeDbContextFactory<TContext> for a context that the host can also see:

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

The message names what is missing. The export leaves out a context or a factory that fails these checks, so DDD00031 is an error. Without it, the first sign of the missing migrations would be a failed deployment, or a branch database without the module's tables.

DDD00032​

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

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

HotChocolate's own generator intercepts AddNodeIdValueSerializerFrom<T>(). It writes a serializer from the properties that T declares in source. The toolkit's generator writes a toolkit identifier's Value, and source generators do not see each other's output. So HotChocolate's generator finds no property and writes a serializer that stores nothing. Every order's node id becomes Order:, and every node id reads back as an empty OrderId. The code compiles, and nothing throws.

Remove the call. Add{Module}GraphQlRuntimeBindings() already registers a serializer, in HotChocolate's own format, for every identifier over a Guid, string, int, long or short (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 of the module. It constructs each one with new, and takes each constructor parameter from the scope that the message is delivered in. For this, a class needs:

  • one accessible constructor with the most parameters;
  • no ref, out or params parameters on that constructor;
  • parameter types that the assembly can see.

The message says which of these failed.

The registration leaves out a class that it cannot construct. The domain event of a left-out outbound class is never published. The contract of a left-out handler is never handled. Nothing else would tell you, so DDD00033 is a warning. Give the class a constructor that the registration can call, or register the class by hand.

The registration also names the domain events of the module's projects that do not reference Entity Framework. An example is a domain project next to the infrastructure project that holds the outbox. A domain event that this project cannot see, such as an internal event, is left out in the same way. DDD00033 reports it on the project's [assembly: Module] attribute. If no file of the project that somebody edits declares the module, DDD00033 is reported at the project file instead. In that case the build declared the module from DDD_Module, or an <AssemblyAttribute> item wrote the attribute into obj/ (A module named by its folder). Set the severity there with <NoWarn>, <WarningsAsErrors> or a .globalconfig. A [*.cs] section of .editorconfig does not reach the project file.

'Sales.Domain.OrderAudited' is left out of the generated integration event registration: a domain event of
Sales.Domain, a project of this module, that this project cannot see; make it public

Make the event public. A left-out event is still stored when it is raised, under the same name, and it is never delivered. The processor records the missing registration on the event's row at every attempt (Registering event types).


DDD00034​

An event's class name and its Version disagree.

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

By convention, a class name that ends in V and a number is that version of its event. Version on [IntegrationEvent] states a version 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 toolkit ignores the V2 in its name. The version is clear, so DDD00034 is a warning. A name that says 2 on an event that is version 3 leads someone to write an upcaster for the wrong version.

Two code fixes make the name and the version agree:

  • Rename the class to the version it is, OrderPlacedV3, everywhere it is used. The event does not change. This fix is offered first. It is not offered when the namespace or the containing type already has a type of the new name.
  • Remove Version = 3. The event then becomes version 2, as its name says. Use this fix when the name was right and the attribute was wrong.

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

The toolkit reads every event's class name for a version suffix. Versions start at 1 and have no leading zeros, so V0 and V01 cannot be versions. Reading them as part of the name would make this event the only one whose suffix is not its version. 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 are 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

When a stored row or a delivered message is read back, the toolkit finds its event by name and version. Both events above are ordering.order-placed version 1. The registry would refuse the second event at start-up. DDD00036 refuses it when the module compiles. It is reported on both classes, because neither is more wrong than the other.

The code fix pins another name on the class you run it on. It makes the name from the class's namespace or containing type: [DomainEventName("ordering.returns-order-placed")] on a domain event, or the name in [IntegrationEvent("...")] on a contract. A class whose name is pinned already gets no code fix, because somebody chose that name by hand. Renaming one of the classes also fixes the error.

The toolkit compares domain events with domain events, and contracts with contracts. A domain event and the contract it is published as share a name on purpose. The versions of one event also share a name. Neither case 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 event name gets a constant in the generated {Module}EventNames class. The constant is named after the event name without its module, in PascalCase. Both names above give OrderingEventNames.OrderPlaced. Whichever name the constant held, code that used it for the other event would bind a topic or a test to the wrong event, and nothing would show it. So DDD00037 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. This only stops code that uses the constant from adding errors of its own.

DDD00038​

A row access rule, an access function or a database question has the shape the generator reads.

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

The generator writes the rule's SQL into another part of the class, so the class is static partial. The generator translates Allows. Allows is a static method that takes the aggregate the rule is about and an IRowUser, returns bool, and has a single expression for a body: after =>, or as its only return statement. A rule that takes an ICurrentUser is reported, and the message gives the signature it needs. The generator generates nothing until the rule has this shape. A rule without SQL never reaches the database.

The same holds for the other classes the generator reads, with more requirements:

  • An [AccessFunction] has a rule's shape. After the caller it may take more parameters: string, bool, int, long, Guid or an id. Each one becomes a parameter of the SQL function, after the row's primary key. A decimal, a list or a nullable value type is not allowed. With Shape = AccessFunctionShape.Set, the aggregate's primary key must be one column, because the function answers with the primary keys of the rows it allows.

  • An [AccessFunctionContract] declares nothing, or one question for the generator to implement: static partial bool Allows(TKey key, IRowUser user, ...), or static partial AccessSet<TKey> Ids(IRowUser user, ...). Ids also needs Shape = AccessFunctionShape.Set if the attribute names a shape.

  • A question of an [AccessFunctions] class is a static partial method without a body, in a static partial class. It declares this IRowUser user as its first parameter, so it cannot be called where there is no rule. An [AccessSet] question returns AccessSet<T>, and an [AccessScalar] question returns a value. Its parameters after the user are the types above, or a type parameter constrained to IEntityId. An [AccessRow] question takes none after the user. No generator writes the body of an [AccessSet] or [AccessScalar] method in a class without [AccessFunctions], so such a method is reported.

  • A function's name is schema.name, owner/name, or a name relative to its owner. Each part has letters, digits and underscores. The owner has lower-case letters, digits and dashes. A relative name without an owner is DDD00052.

  • A column rule, a rule with Columns, is for RowOperations.Change alone. Reading, adding and removing are about whole rows. Each name in Columns is one of these:

    • a property of the aggregate, of its parent, or of its template's parent;
    • a property of a value object the aggregate holds, written with a dot: "Planned.From".

    A name in Columns cannot be a collection of the aggregate's entities. Their rows are in a table of their own and follow the rules for the aggregate's row. A collection of values, such as a list of strings stored as an array, is one column and may be named as a whole. DDD00038 is reported on the Columns argument. Only the export knows whether the model stores the property in a column of the aggregate's table. The export refuses a property that is not stored there, and names the rule.

  • A function that a column rule calls with Sql.Call names its schema: "public.is_agent", or "pg_catalog.lower" for a built-in Postgres function. A column rule is checked in a trigger whose search path is empty. There, a name without a schema finds only Postgres's built-in functions. DDD00038 is reported on the name.

[RowAccess<Project>(RowOperations.Read | RowOperations.Change, Columns = [nameof(Project.State)])] // DDD00038: Change alone
[RowAccess<Project>(RowOperations.Change, Columns = ["Stat", nameof(Project.Crew)])] // DDD00038: no property, a collection of entities

DDD00039​

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

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

The generator turns a rule into a condition that the database checks for every row. So a rule can use only these:

  • the aggregate's own properties;
  • constants written in the rule;
  • the rule's user: user.Id, user.IsAuthenticated, user.Role and user.FindClaim("app_metadata.team");
  • the time, as DateTimeOffset.UtcNow or DateTime.UtcNow, which the database answers as now();
  • SQL written with Sql.Call or Sql.Raw.

A rule compares these with ==, !=, <, <=, > and >=, and combines the comparisons with &&, || and !. A rule asks what only the database knows through an access function, a question of an [AccessFunctions] class, or a question about a resource by its id. It asks each of the rule's own user parameter, and a set-shaped question only with Contains.

A method call, a local variable or another object has no column and no claim to become. If the generator left it out, the database would answer differently from the C# method. So DDD00039 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. Entity Framework would then load half an aggregate, and the aggregate's invariants would check half the data. Write the rule for the root. The export gives the tables of the aggregate's entities a policy that follows the root. A line is visible when its order is visible, and hidden when its order is hidden.

A rule about a supporting domain's parent, [RowAccess<SubscriptionAggregate<SubscriptionId>>], also reports DDD00040. The parent is abstract and has no table. Write the rule for the application's class declared with the parent's template. That class reads the parent's properties like its own.


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, IRowUser user)
=> project.Members.Any(member => member.UserId == user.Id); // 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. Postgres stops such a query with infinite recursion. Put the question in an access function. An access function runs as its owner (SECURITY DEFINER), so it reads the entities without their policies. Call it from the rule:

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

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

The rule calls the function with the row's id. The SQL function is written once, however many rules call it.


DDD00042​

A parent for entities is an abstract generic class whose first type parameter is the id.

[AggregateRootBase]
public partial class SubscriptionAggregate<TSubscriptionId> { ... } // DDD00042: not abstract

A package ships a parent class that the application's own classes derive from. The generator writes the parent's base class as it does for any aggregate root or entity: AggregateRoot<TId> or Entity<TId>, closed over the parent's first type parameter. So the parent must have this shape:

  • It is abstract, because only the classes that derive from it are ever created.
  • Its first type parameter is the id.
  • It is not nested in a generic type.
  • The id parameter has the constraint that the toolkit's base classes require: where TId : IEntityId, IEquatable<TId>.

Like every entity, the parent is also a partial class. DDD00005 and DDD00002 report that:

[AggregateRootBase]
public abstract partial class SubscriptionAggregate<TSubscriptionId>
where TSubscriptionId : IEntityId, IEquatable<TSubscriptionId>
{
public string Plan { get; private set; } = "";
}

A parent with more than one problem reports the first one. The next one is reported once the first is fixed. A class declared with the template of such a parent gets nothing generated either, and no diagnostic of its own. Fix the parent. The compiler errors in that class, such as a base(...) call it cannot make or a parent's property it cannot find, come from the missing base class. They go away once the parent is fixed.


DDD00043​

A template's first type argument is an entity id.

[Subscription<Guid>] // DDD00043
public sealed partial class ShopSubscription;

A template attribute names the id of the class it declares, and that class derives from a parent closed over the id. Unlike [AggregateRoot<Guid>], a template never generates an id from a raw value. The id belongs to the application. The application declares the id where every module that refers to it can see it, usually in a contracts project:

[EntityId<Guid>]
public readonly partial record struct SubscriptionId; // in Shop.Contracts

[Subscription<SubscriptionId>]
public sealed partial class ShopSubscription;

The generator generates nothing for the class until its id is an [EntityId<T>].


DDD00044​

A template takes a type from a class nobody declares.

[Invoice<InvoiceId>] // DDD00044: no [Subscription] class
public sealed partial class ShopInvoice;

Some parents need more than the id of the class that derives from them. The parent of an invoice needs the subscription's id and the class of its lines. The template attribute takes those types from the one class declared with the template it names. So each type is declared once, and every class agrees on it. The generator looks in the project first. If the project declares no such class, the generator looks in the projects it references. Declare the class that the message names, with the attribute it names, in one of them.

A code fix declares the class for you, next to the class with the error: [Subscription<SubscriptionId>] public sealed partial class ShopSubscription;.

  • It names the new class after the class with the error. ShopInvoice gives the prefix Shop.
  • It declares the class with the id whose name matches the parent's id parameter. TSubscriptionId gives SubscriptionId. If no id has that name, it looks for the template's name with Id after it. So [OrganizationUnit], whose parent's parameter is TUnitId, finds your OrganizationUnitId.
  • It is offered only when it finds exactly one such id, in the project or in a project it references.
  • For a template name of more than one word, the new class gets the prefix and the last word only. An organization called ShopOrganization that has no unit gets ShopUnit.
  • A class named after its template alone has no prefix. The new class is then named after its template in full. Organization gets OrganizationUnit.

DDD00045​

A template takes a type from a class declared more than once.

[Subscription<SubscriptionId>] public sealed partial class ShopSubscription;
[Subscription<SubscriptionId>] public sealed partial class TrialSubscription;

[Invoice<InvoiceId>] // DDD00045: which subscription?
public sealed partial class ShopInvoice;

The parent takes a type argument from the one class declared with another template. With two such classes, the generator cannot tell which one is meant. Picking one would bind the parent to it with no message. Keep one.

A registration closed over your classes takes its type arguments in the same way, and also reports DDD00045. It reports it on the first class of the project declared with one of its templates: 'SubscriptionModelBuilderExtensions.AddSubscriptions' is declared with [TemplateRegistration], which takes 'TSubscription' from the class declared with [Subscription], and there are several.

A package can declare that a template is meant to be used more than once, with AllowSeveral = true on its marker. Examples are the comments on a subscription and the comments on an invoice. For a registration, several classes of such a template are fine. The registration is written once per class, named after the class. DDD00045 then reports only the cases that still cannot be told apart, with a fix for each:

  • Two templates of one method each have several classes. The generator cannot tell which class of one template goes with which class of the other. Call the package's generic method for these classes, with its type arguments written out.
  • Two of the classes have one name, in two namespaces. The registrations are named after their classes. Give each class a name of its own.
  • Two registrations would get the same name. A method that says what its registration is called names each registration after what its class says of itself, such as the aggregate it belongs to. Two classes that say the same thing give one name: the registration of each would be called 'AddShopInvoiceDiscounts'. Make the things they are named after differ. With Membership, a resource has one member class. The other classes keep their registrations.

The last case is reported once, however many of the package's registrations it stops, on the class to fix. If the type that the registrations are named after holds one of the classes in a property or field, as a resource holds its members, DDD00045 is reported on the other class. Otherwise it is reported on the class declared last.

A parent that takes a type from such a template is still closed over one class. So two classes are reported there, as for any other template.


DDD00046​

A template attribute fills exactly the type parameters of its parent.

[AggregateRootTemplate(typeof(InvoiceAggregate<,,,>))]
public sealed class InvoiceAttribute<TInvoiceId> : Attribute; // DDD00046: nothing fills the other three

DDD00046 reports a mistake in the package that declares the template attribute. It is reported on the attribute when the package is built. It is also reported on a class that uses the attribute when the package was built without the generator, because there the generator first sees the attribute. The rules:

  • The marker names an open parent marked [AggregateRootBase] for [AggregateRootTemplate], or [EntityBase] for [EntityTemplate].
  • The attribute's own type arguments fill the parent's first type parameters, the id first.
  • Exactly one [TemplateArgument] fills each parameter after those.
[AggregateRootTemplate(typeof(InvoiceAggregate<,,,>))]
[TemplateArgument(1, typeof(SubscriptionAttribute<>))]
[TemplateArgument(2, typeof(InvoiceLineAttribute<>), Take = TemplateArgumentKind.Type)]
[TemplateArgument(3, typeof(InvoiceLineAttribute<>))]
public sealed class InvoiceAttribute<TInvoiceId> : Attribute;

A parameter that takes the application's class, with Take = TemplateArgumentKind.Type, has no new() constraint. The generator writes a parameterless constructor for that class, and that constructor is never public. So the class could never meet a new() constraint. A parent creates the application's class through a static abstract factory instead, as Writing your own supporting domain shows.

The attribute may have more type arguments than its parent takes. These belong to the template, such as the aggregate a class belongs to, and a registration takes them by position.

The generator generates nothing for a class declared with the attribute until the package is fixed.


DDD00047​

A class is declared an entity or aggregate root once.

[AggregateRoot<SubscriptionId>]
[Subscription<SubscriptionId>] // DDD00047
public sealed partial class ShopSubscription;

[AggregateRoot<TId>], [Entity<TId>], [AggregateRootBase], [EntityBase] and a package's template attributes each give the class a base class. A class has only one base class. Keep the attribute that describes the class. Keep the template when the class is meant to extend what the package ships. [AggregateRoot<TId>] together with [Entity<TId>] is DDD00009. The generator generates nothing for the class until one attribute is left.


DDD00048​

A class a template takes meets its parent's constraints.

[InvoiceLine<InvoiceLineId>]
public sealed partial class ShopInvoiceLine; // no IInvoiceLineFactory

[Invoice<InvoiceId>] // DDD00048
public sealed partial class ShopInvoice;

A [TemplateArgument] with Take = TemplateArgumentKind.Type passes one of the application's own classes to the parent as a type argument. Here it is the class of an invoice's lines. The parent may require more of that class than the right template, such as an interface that the parent creates the class through. If the class does not have it, the parent closed over the class would be a compile error inside generated code, where you cannot fix it. So the generator generates nothing, and the message names what the class is missing:

[InvoiceLine<InvoiceLineId>]
public sealed partial class ShopInvoiceLine : IInvoiceLineFactory<ShopInvoiceLine, InvoiceLineId>
{
private ShopInvoiceLine(InvoiceLineId id, decimal amount) : base(id, amount) { }

public static ShopInvoiceLine Create(InvoiceLineId id, decimal amount) => new(id, amount);
}

DDD00049​

A template registration needs a class declared with each of its templates.

[Subscription<SubscriptionId>] // DDD00049: no [Invoice] class, nor [InvoiceLine]
public sealed partial class ShopSubscription;

modelBuilder.AddSubscriptions(); // closed over the subscription and the invoice

A package's [TemplateRegistration] method, such as modelBuilder.AddSubscriptions(), is written into every project that declares a class with one of its templates. It is closed over that project's classes. Each of its [TemplateType] type parameters takes the one class declared with a template, or that class's id. The generator looks for the class in the project first, then in the projects it references, as it does for a [TemplateArgument]. This project declares a class with one of the method's templates, so it is meant to get the registration. No project declares a class with the template that the message names:

A class declared with [Invoice] is needed by 'SubscriptionModelBuilderExtensions.AddSubscriptions' and
'SubscriptionServiceCollectionExtensions.AddSubscriptions', and neither this project nor a project it
references declares one; declare one, once

DDD00049 is reported once for each missing template, on the first class of the project declared with one of the methods' templates. It names every registration that needs the template. No registration is written until the class is declared. The call to the registration then also fails to compile, because the method does not exist. Fix DDD00049, and the call compiles again. The same code fix as for DDD00044 declares the class next to the class with the error.

DDD00049 is not reported where another diagnostic reports the same cause:

  • A class of the project whose parent takes a type from the missing class reports DDD00044. The registrations do not report it again.
  • Two classes declared with the template are DDD00045.
  • Two classes whose registrations would get the same name are reported once, as DDD00045, however many registrations they share, on the class to fix.

A module split into projects by layer declares its classes in its domain project, and calls the registration in its infrastructure project, which declares none. The infrastructure project gets the registration too. It is closed over the classes that the projects of its module declare, the projects with the same [assembly: Module], and only those. If those projects declare a class with one of the method's templates and no class with another, DDD00049 is reported on the infrastructure project's [assembly: Module] attribute, without a code fix. The missing class belongs in the project that declares the others. DDD00045 and DDD00050 are reported there too, for such a project.

If no file of the project that somebody edits declares the module, these diagnostics are reported at the project file. In that case the build declared the module from DDD_Module, or an <AssemblyAttribute> item wrote the attribute into obj/ (A module named by its folder). Set their severity with <NoWarn>, <WarningsAsErrors> or a .globalconfig. A [*.cs] section of .editorconfig does not reach the project file.


DDD00050​

A type a template registration takes meets the method's constraints.

[TemplateRegistration]
public static IServiceCollection AddAudit<
[TemplateType(typeof(SubscriptionAttribute<>), Take = TemplateArgumentKind.Type)] TSubscription>(this IServiceCollection services)
where TSubscription : class, IAudited;

[Subscription<SubscriptionId>]
public sealed partial class ShopSubscription; // DDD00050: not IAudited

A [TemplateType] with Take = TemplateArgumentKind.Type passes one of your classes to the package's method as a type argument. The method may require more of the class than the template, such as an interface. The registration closed over a class without it would be a compile error inside generated code. So no registration is written, and the message names what the class is missing. The generator judges the class by the parent it derives the class from, closed over the class's own id, as DDD00048 judges a class that a template takes. Give the class what the message names.

A [TemplateType] that takes an id is held to the method's struct or class constraint in the same way. An id declared as a record class, where the method says where TTenantId : struct, is reported on the class declared with the template. Without this check, the registration would fail inside generated code. The id's interfaces are not checked here. The generator writes them for an entity id, and DDD00043 already requires an entity id.

Create() is the exception. The generator writes Create() only for an id over a Guid. When the method requires ICreatableEntityId<T>, an id without Create() is reported here. If the template says that its package makes the ids of its classes, CreatesIds = true, as Tenancy's templates do, DDD00067 reports it on the class instead, and the registration reports nothing more.

A later type argument of the template, taken with Argument = 1 and up, is whatever type you wrote there. It is held to the method's constraints as DDD00053 holds it to the parent's. Whether it is a struct or a class is always checked. An interface that the method requires is checked only when no generator will still complete the type.

[TemplateRegistration]
public static ModelBuilder AddCommentsByAuthor<
[TemplateType(typeof(CommentAttribute<,>), Take = TemplateArgumentKind.Type)] TComment,
[TemplateType(typeof(CommentAttribute<,>))] TCommentId,
[TemplateType(typeof(CommentAttribute<,>), Argument = 1)] TAuthorId>(this ModelBuilder modelBuilder)
where TComment : CommentAggregate<TCommentId, TAuthorId>
where TAuthorId : struct, IEquatable<TAuthorId>, IComparable<TAuthorId>;

public readonly record struct Initials(string Letters); // equatable, as the parent asks, and without an order

[Comment<InvoiceCommentId, Initials>] // DDD00050: 'Initials' as 'TAuthorId', which requires 'IComparable<Initials>'
public sealed partial class ShopInvoiceComment;

A registration can require two more things of a later type argument. Both are reported in the same way, on the class that wrote the type argument:

  • A type argument whose id the method takes, with IdOfArgument = true, must be one of your entities or aggregate roots, because the generator reads its id from how the class is declared. The message says requires an entity or an aggregate root, whose id it takes. Name the class that the template asks for, such as the aggregate your member class is a member of. A child entity where the registration requires an aggregate root is reported too, with requires an aggregate root. A class of your project is judged by what the generator will make of it.
  • A type that the registration is named after, with [TemplateRegistration(Name = ...)], must have a name of its own. An array has none.
public sealed class Note; // no [AggregateRoot<TId>], so no id

[Member<NoteShareId, UserId, NamedRole, Note>] // DDD00050: 'Note' as 'TResourceId'
public sealed partial class NoteShare;

DDD00051​

A set-shaped question is asked once per statement, so its arguments do not read the row.

public static bool Allows(Ticket ticket, IRowUser user)
=> user.TicketsForTeam(ticket.Team!).Contains(ticket.Id); // DDD00051, on ticket.Team!

A set-shaped question becomes "Id" = ANY (ARRAY(SELECT desk.tickets_for_team(...))) in the policy. Postgres works out the set once, before it reads the table. It then finds the rows through the column's index. An argument that reads the row, such as a column of the row or a question about the row, would make the set different for every row. Postgres would then call the function once per row. The set-shaped form exists to avoid that. Only the value compared with the set, the argument of Contains, reads the row.

Pass the question constants, the caller, or, inside an access function, the function's own parameters. To ask about the row itself, ask a question about one row: an access function's Allows, or an [AccessScalar] question. Such a question may take the row's columns, and the database then asks it once per row. The generator generates nothing for the rule until its arguments are constants, the caller, or the parameters of the access function that the rule is part of.


DDD00052​

A function named without its schema belongs to a module.

// an assembly without [assembly: Module(...)]
[AccessFunctions]
public static partial class DeskQuestions
{
[AccessSet("tickets_i_watch")] // DDD00052
public static partial AccessSet<TicketId> TicketsIWatch(this IRowUser user);
}

A function name without a schema is relative to the module that owns it. tickets_i_watch in the Desk module is desk/tickets_i_watch. The export writes it as the function of that name in the schema of the context that defines it, whatever the host calls that schema. The owner is one of these:

  • the module that the declaring assembly declares, with DDD_Module or [assembly: Module];
  • the Owner of the class's [AccessFunctions], for a package that declares no module.

With neither, the name has no owner. A function of the same name in another module could then not be told apart from it.

Add [assembly: Module("Desk")], give the class [AccessFunctions(Owner = "desk")], or write the name with its schema: desk.tickets_i_watch. A question named with its schema is a function that you create yourself. Rules call it by that name. The generator generates nothing for the class until it has an owner or the name has its schema.


DDD00053​

A type argument of a template meets its parent's constraints.

[EntityId<Guid>]
public partial record SubscriptionId; // a class

[Subscription<SubscriptionId>] // DDD00053
public sealed partial class ShopSubscription;

A class declared with a template derives from the package's parent, closed over the type arguments that the template supplies:

  • the attribute's own type arguments, the id first;
  • the ids that a [TemplateArgument] takes from other classes.

The parent may require more of an id than DDD00043 does. Most often it requires a struct, where TSubscriptionId : struct, because it holds ids by value. The parent closed over a record class would be a compile error inside generated code. So the generator generates nothing for the class. The message names the type, the parent's type parameter, and what that parameter requires. Declare the id as a struct:

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

A class that takes the id from another class reports DDD00053 on its own line. An invoice whose parent holds the subscription's id by value reports the subscription's id, whatever the subscription's own parent requires. So one id of the wrong kind is reported on every class whose parent refuses it, and one fix clears all of them.

The generator always checks whether the type is a struct or a class. It checks the parent's other constraints, such as an interface, for two kinds of type:

  • a type from a referenced project, such as an id in a contracts project;
  • a type written out in full.

A generator may still complete a partial type declared in the same project. This is how an [EntityId<T>] gets IEntityId<T>. So DDD00053 leaves an interface that such a type does not show yet to the compiler. An [EntityId<T>] with an error of its own, such as DDD00003, is not completed. DDD00053 reports it as missing IEntityId, and fixing its own error clears both. DDD00048 judges a class that the template takes.


DDD00054​

A package's row access contribution is made from what the application marks.

DDD00054 is reported in the project that turns the Supabase export on (<SupabaseMigrationsExport>), normally the host, about a package that the project references:

// The host references DTS.Membership.Postgres, and marks no resource's rules: DDD00054
services.AddDocumentMembership<FilingContext>(DocumentMembership.Rules);

A package that declares itself a contributor, with [assembly: RowAccessContribution], writes its row level security into the migrations of every application that references it. Referencing the package is your consent. The build makes the package's class in the project where the export runs, before your host exists. So the class cannot read values from your host. A value that only your application knows, such as a resource's rules, comes from a static property or field that you mark with the attribute the package names.

If nothing is marked that the class cannot do without, the build does not make the class, and none of its SQL reaches your migrations. The package's tables may then have no policies at all. A rule that asks one of the package's functions fails the export, and the export names the rule. The message names the type of the member and the attribute. Mark the member:

public static class DocumentMembership
{
[MembershipRules<DocumentShare>]
public static MembershipRules Rules { get; } = new("documents", ...);
}

If your application must not have the package's SQL, because you write those policies yourself, say so with [assembly: LeaveOutRowAccessContribution(typeof(MembershipRowAccessContribution<>))]. DDD00054 is then not reported. A value that the package can do without is never reported. Tenancy's catalogue is such a value. It is new ApplicationCatalogue() when you mark none.

DDD00055​

A context's migration files are named after its module.

DDD00055 is reported in the project that turns the Supabase export on (<SupabaseMigrationsExport>), normally the host, about a marked context, or a marked factory, in a project it references:

// no module in the context's project: no DDD_Module, no [assembly: Module]
[SupabaseMigrations]
public sealed class OrderingContext(DbContextOptions<OrderingContext> options) : DbContext(options) // DDD00055

The export names every file it writes after the module the context belongs to, as in 20260922120000_AddOrders.ordering.ddd.sql. It finds a module's files again by that name. The module is the one that the assembly of the export's factory for the context declares. If that assembly declares none, it is the one that the context's assembly declares. For a marked context, the two are the same assembly. If neither declares a module, the name comes from the context's class name, without Context. The message says which name that is.

This works until somebody renames the class. The export then looks for files under the new name and recognizes none of the files it wrote:

  • It reports every migration as VersionTaken, because the file under the old name holds its timestamp.
  • It writes the module's access file again.
  • You have to rename the files by hand.
<DDD_Module>Ordering</DDD_Module>

Declare the module in the project that holds the context, with DDD_Module or [assembly: Module("Ordering")]. The files then keep their name, whatever the class is called. Files already written under the context's name, ordering for OrderingContext, stay recognized when the module has the same name, Ordering. If the module has another name, rename the files once.

DDD00056​

A request interface is one a behavior can be written for.

[AccessRequests]
public interface IBillingRequest; // DDD00056: does not derive from IRequireAccess

[AccessRequests]
public interface IBillingRequest<TAnswer> : IRequireAccess; // DDD00056: has type parameters

[AccessRequests] marks the interface that a module's commands and queries implement to say what they require of their caller. The generator writes a pipeline behavior of the toolkit's mediator for the interface. The behavior is declared beside the interface and named after it. It asks the module's access checks before every handler. For this, the interface must meet these rules:

ItBecause
derives from IRequireAccessthe checks read a request's requirement from IRequireAccess, and AccessChecks<TRequests> is closed over it
has no type parametersthe behavior and the module's set of checks are closed over the interface itself
is not declared inside a type, and is not file-localthe generator writes the behavior beside the interface, in a file of its own, and the behavior must name the interface
gives a behavior name that no other marked interface in its namespace givesIBillingRequest and IBilling would both give BillingAccessBehavior
[AccessRequests]
public partial interface IBillingRequest : IRequireAccess;

The generator writes no behavior for the interface until it meets these rules, so nothing checks its requests. That is why DDD00056 is an error. Fix the interface, or call AccessChecks<TRequests>.RequireAsync in front of the handlers yourself. The generator checks the interface in the same way in a project for netstandard2.0, where it writes no behavior anyway.

DDD00057​

The mediator's pipeline behavior has the shape the generator writes a behavior for.

DDD00057 is reported on an [AccessRequests] interface, about the DTS.DDD.Abstractions that the project references:

No access behavior is written for 'IBillingRequest': the DTS.DDD.Abstractions this project references declares
IPipelineBehavior<,> otherwise than the generator knows it, with a Handle that does not take the message, a
cancellation token and the delegate that runs the next step, each once.

The generator implements the mediator's IPipelineBehavior<TMessage, TResponse>, and reads the shape of that interface from the abstractions the project references:

  • one method, Handle, that takes the message, the delegate that runs the next step and a cancellation token;
  • Handle returns a ValueTask of the response;
  • the delegate takes the message and the token, and returns the same.

The analyzers and the abstractions of one version agree on that shape. An abstractions package of another version may declare the interface in another shape, one that the generator does not know. The generator then writes nothing instead of guessing. A behavior that does not compile, or that never asks the checks, would be worse than none.

The same holds for IStreamPipelineBehavior<TMessage, TResponse>, the pipeline of the messages that are answered with a stream. Its Handle returns an IAsyncEnumerable of the response. The message names the interface that the generator could not read. If either interface is unknown, the generator writes neither behavior. With one behavior and not the other, half of a module's messages would be checked, and the module would look fully checked.

Without the behavior, the requests of the interface reach their handlers unchecked, so DDD00057 is an error. Reference the packages of one version. Until then, write the behavior by hand and remove [AccessRequests] from the interface. The interface still names the checks that its requests are held to. Write Handle with async and await, as below. RequireAsync puts the request in hand for the flow of work that asked. The handler that Handle calls next runs in that flow and finds the request in hand (the request in hand). Run the next step inside BeginWork, in the same method, so the work a check begins reaches the handler, such as the operator work of an operator's command. The stream behavior begins no work.

public sealed class BillingAccessBehavior<TMessage, TResponse>(AccessChecks<IBillingRequest> checks) : IPipelineBehavior<TMessage, TResponse>
where TMessage : IBillingRequest, IMessage
{
public async ValueTask<TResponse> Handle(TMessage message, MessageHandlerDelegate<TMessage, TResponse> next, CancellationToken cancellationToken)
{
await checks.RequireAsync(message, cancellationToken);
using (checks.BeginWork(message, isQuery: message is IBaseQuery))
{
return await next(message, cancellationToken);
}
}
}

// Only where the module has messages answered with a stream, which pass this pipeline and not the other
public sealed class BillingAccessStreamBehavior<TMessage, TResponse>(AccessChecks<IBillingRequest> checks) : IStreamPipelineBehavior<TMessage, TResponse>
where TMessage : IBillingRequest, IStreamMessage
{
public async IAsyncEnumerable<TResponse> Handle(TMessage message, StreamHandlerDelegate<TMessage, TResponse> next, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await checks.RequireAsync(message, cancellationToken);
await foreach (var item in next(message, cancellationToken))
{
yield return item;
}
}
}

DDD00058​

A notification implements no request interface.

[AccessRequests]
public partial interface IBillingRequest : IRequireAccess;

public sealed record InvoiceClosed(InvoiceId Invoice) : INotification, IBillingRequest // DDD00058
{
AccessRequirement IRequireAccess.RequiredAccess => new BillingAccess.OnInvoice("billing.view", Invoice);
}

The behavior written for an [AccessRequests] interface is part of the mediator's pipeline for commands and queries. The second behavior is part of its pipeline for messages that are answered with a stream. The mediator publishes a notification to its handlers through neither pipeline. So a notification that implements the interface declares a requirement that nothing checks. Every handler of the notification runs for whoever published it, while the declaration looks as if it were checked. That is why DDD00058 is an error.

DDD00058 is reported on the type where the notification and the interface are first joined: a class, a struct or an interface of your own that is both. It is not reported again on the types that implement or derive from that type. A partial type is reported once, at the part that lists what it implements. A project for netstandard2.0 has no notifications, and reports nothing.

Send what needs a check before it is handled as a command or a query:

public sealed record CloseInvoice(InvoiceId Invoice) : ICommand, IBillingRequest
{
AccessRequirement IRequireAccess.RequiredAccess => new BillingAccess.OnInvoice("billing.close", Invoice);
}

public sealed record InvoiceClosed(InvoiceId Invoice) : INotification; // says what happened, to whoever listens

A notification says that something happened. Whoever publishes it has passed its own check already. If the handlers of a notification must not run for every publisher, ask the checks before you publish: AccessChecks<IBillingRequest>.RequireAsync(request, cancellationToken), with a request of the module that declares the requirement.

DDD00059​

The member list of a resource is written from what the resource declares.

[Member<DocumentShareId, UserId, NamedRole, Document>]
public sealed partial class DocumentShare;

[AggregateRoot<DocumentId>]
public sealed partial class Document // DDD00059: no property marked [Owner]
{
public UserId OwnerId { get; private set; }

public UserId WrittenBy { get; private set; }

public partial IReadOnlyList<DocumentShare> Shares { get; }
}

For a class declared with Membership's member template, the package's generator writes the member list on the resource that the template names. The member list is a private property Members. The member manager changes the members through it, and no manager is written without it (DDD00106). The generator writes it from three things the resource declares and from the member class's own id. It writes the list only when it can tell each of them without a guess:

The list needsWhere it comes fromNot written when
the membersthe one get-only partial property of IReadOnlyList<TMember>, IReadOnlyCollection<TMember> or IEnumerable<TMember>, which the toolkit keeps in a listthere is none, or there are two. A set property does not count, because the toolkit keeps a set in a HashSet
the ownerthe one property marked [Owner], of the type a member is known by, the template's second type argumentno property is marked, two are marked, or the marked one has another type
a new row's idthe Create() of the member class's own id, which the generator writes for an [EntityId<Guid>]the id is over another type, such as a long, and declares no Create()
the codesthe one static property or field of type MembershipCodes on the resource. If the resource declares none, the codes that the generator writes under the resource's namethere are two, or there is a member called Codes of another type

The generator also does not write the list in these cases:

  • The resource already has a member called Members.
  • Two member classes name the same resource, and the resource keeps a collection of both, or of neither. Both lists would get the same name.
  • A type of the member class is one that the generator cannot see. The id of an [AggregateRoot<Guid>] is written by another generator, and no generator sees what another generator writes. Declare such an id yourself, with [EntityId<Guid>], as the member class's own id is declared.

A member class that names no aggregate root, or that names a resource that keeps its members as another member class, is DDD00060, on the member class. The message names each thing that stands in the way.

Declare what is missing, and the generator writes the list:

[AggregateRoot<DocumentId>]
public sealed partial class Document
{
[Owner] public UserId OwnerId { get; private set; }

public UserId WrittenBy { get; private set; }

public partial IReadOnlyList<DocumentShare> Shares { get; }
}

Or write the list yourself. This works for every shape above. The generator leaves alone a resource that declares a MemberList of its member class, under any name, and reports nothing for it:

private MemberList<DocumentShare, DocumentShareId, UserId, NamedRole> Members
=> new(_shares, OwnerId, DocumentRefusals.Membership);

DDD00059 is a warning, because the resource compiles. It has no member list, so its members cannot be changed. The first method you write over Members only reports that the name does not exist. A resource that changes its members in another way turns the warning off for the project: <NoWarn>DDD00059</NoWarn>.

DDD00060​

A member class names an aggregate root whose members it is.

[AggregateRoot<DocumentId>]
public sealed partial class Document
{
public partial IReadOnlyList<DocumentShare> Shares { get; }
}

[Member<DocumentShareId, UserId, NamedRole, Document>]
public sealed partial class DocumentShare;

[Member<DocumentWatcherId, UserId, NamedRole, Document>]
public sealed partial class DocumentWatcher; // DDD00060: the document keeps its members as shares already

[Member<NoteShareId, UserId, NamedRole, DocumentShare>]
public sealed partial class NoteShare; // DDD00060: a share is no aggregate root

The fourth type argument of the member template is the resource that the members belong to. It is an aggregate root of yours. The resource keeps its members in a collection of the member class, and the generator writes the member list over that collection.

A resource has one member class. If two member classes name one resource, the class that the resource keeps a collection of is its member class. The generator writes the list for that class, so the resource's own methods still compile. The other class is reported, on itself, as the class to fix. Name the resource it is for, or remove it.

A member class whose resource is no aggregate root is also reported on itself. Such a resource is a child entity, the member class itself, or a class that is no entity. Name the aggregate root that the members belong to.

A project that also gets the package's registrations, such as the infrastructure project that holds the context, reports the same problem as the errors DDD00045 and DDD00050, on the same class. DDD00060 is not reported beside them. If the resource keeps a collection of both member classes, or of neither, the generator cannot tell which class is the extra one. That case is DDD00059, reported once, on the resource.

DDD00060 is a warning. Nothing else is wrong with the member class, and the generator writes nothing for it.

DDD00061​

A request that declares its access is sent, not handed to its handler.

[AccessRequests]
public partial interface IBillingRequest : IRequireAccess;

public sealed class InvoiceReminders(CloseInvoiceHandler handler)
{
public async Task CloseOverdueAsync(InvoiceId invoice, CancellationToken cancellationToken)
=> await handler.Handle(new CloseInvoice(invoice), cancellationToken); // DDD00061
}

The behavior written for an [AccessRequests] interface asks the module's checks in the mediator's pipeline. A command or a query passes the pipeline when it is sent. A handler called directly passes no pipeline. The handler then runs without any check of what the request declares, and nothing at run time notices. Where the database has policies, only the policies stand in the way. A policy works per table, so it is coarser than the requirement of one request. A policy must let every caller who may write a project's row write it, whether to close the project or to rename it. So the policy lets a rename through for a caller who may only close the project.

So DDD00061 is reported where the call is written. It reports a call to Handle with a message that implements a marked interface, on one of these:

  • one of the mediator's handler interfaces: ICommandHandler, IQueryHandler, IRequestHandler or one of their stream kinds;
  • a class that implements one of them, or derives from such a class.

A reference to Handle that makes a delegate of it is reported too.

Send the request instead. It then passes the behavior on its way to the same handler:

public sealed class InvoiceReminders(ISender sender)
{
public async Task CloseOverdueAsync(InvoiceId invoice, CancellationToken cancellationToken)
=> await sender.Send(new CloseInvoice(invoice), cancellationToken);
}

A code fix does this when the call can use an ISender, or an IMediator, held in a local, a parameter, a field or a property. It cannot use a field from a static member, or a local of the method from inside a static lambda. It sends a query answered with a stream with CreateStream, and puts named arguments in the order of the handler's parameters. If the call can use no sender, inject one. The fix does not change a constructor for you.

DDD00061 does not report these cases:

  • Constructing a handler, and injecting one. The call skips the check, so DDD00061 is reported at the call. This includes a call through the interface that the handler was injected as.
  • Generated code. It calls a handler only after the pipeline.
  • base.Handle in a handler that overrides it. It passes on the request that passed the pipeline on its way in.
  • A decorator: a handler that wraps another handler of the same message, and passes it the message it was given, unchanged. The request passed the pipeline on its way to the decorator. Sending it again would bring it back to the decorator. A message that the decorator makes itself is reported.
  • A test project: a project with IsTestProject or IsTestingPlatformApplication set, as Microsoft.NET.Test.Sdk and Microsoft.Testing.Platform set them. A test that calls a handler on purpose tests the handler alone. A test of what a request may do sends the request.
  • A project for netstandard2.0. It cannot see the mediator, and the generator writes no behavior there. You ask the checks yourself, in front of every handler.

Outside a test project, suppress a call that you mean, where it is made, with the reason beside it:

#pragma warning disable DDD00061 // replayed from the outbox after the behavior let it through once
await handler.Handle(replayed, cancellationToken);
#pragma warning restore DDD00061

The build cannot see every way around the behavior. It does not report a helper that calls Handle on a handler of a type parameter that has no constraint to a marked interface, such as a transport's own dispatcher. It does not report the call that passes such a helper a handler either. Write such a dispatcher so that it sends through the sender, or asks the checks itself. A module whose behavior is not in the pipeline is stopped when the host starts, by the start-up check access.behaviors-registered. See When nothing asks the checks.

DDD00062​

A class of one GraphQL schema is one the toolkit alone registers.

[GraphQLSchema("admin", OperationType.Query)]
[QueryType] // DDD00062
internal static class SeatsAdminQueries
{
[Query] // DDD00062
public static Task<IReadOnlyList<SeatGrant>> GetSeatGrantsAsync(SeatId seat, [Service] ISender sender, CancellationToken cancellationToken) => ...;
}

A class marked [GraphQLSchema] belongs to the schema it names and to no other. The toolkit's generator registers its public static methods in the module's Add{Module}GraphQlRuntimeBindings(), for a builder of that schema name only. HotChocolate's own generator registers everything it finds in a project in one method, and every schema the project is added to calls that method. A class that HotChocolate's generator finds is therefore in every schema. The administration field above would be offered to every caller as well. So the class must carry nothing that HotChocolate's generator registers:

  • no [QueryType], [MutationType], [SubscriptionType], [ExtendObjectType] or [ObjectType];
  • no base class such as ObjectTypeExtension;
  • no static method marked [Query], [Mutation] or [Subscription].

The attribute on the class says what its methods are. Remove what the message names.

DDD00062 also reports what would leave a field out with no message:

  • An instance method. Nothing makes an instance of the class, so a field of one schema is a public static method. A former [QueryType] class often has instance methods. Make them static.
  • A class with no field: no public static method that is not [GraphQLIgnore].
  • Two methods that are one field of one schema and operation type: two overloads, or GetLedger beside GetLedgerAsync. HotChocolate keeps one and drops the other. Give one of them another name, or another [GraphQLName].
  • A method whose name another public static method has, and that takes a parameter of a type another generator writes, such as the interface that HotChocolate's generator writes for a data loader. The generator finds a method by its name. For a shared name it also uses the parameter types, and it cannot name a type that it does not see. Give the method a name of its own.

DDD00062 also reports an attribute that names no schema, and a class that the generated registration cannot name: a generic class, a file class, or a class that is private or protected inside another class.

Each of these puts a field where it should not be, or leaves a field out where it should be, with no message. So DDD00062 is an error. See A field for one schema only.

DDD00064​

DDD_Module declares the module where the package's build step runs.

<!-- Ordering.Domain.csproj: the generators as a project reference, and no targets file -->
<PropertyGroup>
<DDD_Module>Ordering</DDD_Module> <!-- DDD00064 -->
</PropertyGroup>
<ItemGroup>
<CompilerVisibleProperty Include="DDD_Module" />
<ProjectReference Include="..\DTS.DDD.Analyzers\DTS.DDD.Analyzers.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

DDD_Module declares the project's module through a build step. The DTS.DDD.Analyzers package carries a targets file beside its props file. The targets file writes the module declaration into the project. Every generator of the project, and of the projects that reference it, reads the declaration there. A package reference brings both files.

The generators can arrive in another way: as an analyzer assembly, or as a project reference with OutputItemType="Analyzer". The property then reaches them through the props file alone, or through a CompilerVisibleProperty that you list yourself, as DDD00014 suggests. In that case the name arrives and the declaration does not:

DDD_Module names this project's generated code after 'Ordering' and declares no module, because the build step
of the DTS.DDD.Analyzers package that declares it did not run: the project is no project of module
'Ordering'. Import the package's targets file beside its props file, or declare [assembly: Module("Ordering")].

The generated code is named after the module, OrderingEventNames and AddOrderingConverters, and the project is no module:

  • Its domain events are stored and published without the module's name.
  • The module boundary is not checked.
  • The module's other projects do not take the project for one of theirs. So a registration such as modelBuilder.AddTenancy() is not written into a project of the module that declares none of the classes.

Without DDD00064, the first sign would be a call that does not compile, or an event stored under a name that changes once the targets file arrives.

Import DTS.DDD.Analyzers.targets as well, from the package's build folder or from the source, in the same way that you import its props file. Or declare the module with [assembly: Module("Ordering")]. The targets file tells the generators that it was imported, by passing them DDD_DeclareModule. Where it was imported, a test project and a project that sets DDD_DeclareModule to false are no module on purpose, and are not reported. A test project is also not reported without the targets file, if the props file says that it is a test project.

DDD00064 is reported at the project file, because no line of code is wrong. The project file is outside every source file, so a severity in an .editorconfig section for *.cs files does not reach it. Set the severity with <NoWarn> or <WarningsAsErrors> in the project file or a Directory.Build.props, or in a .globalconfig file with is_global = true.

DDD00065​

The class a package's use cases are named through is written where each of its templates has one class.

// Shop.Tenants.Domain, [assembly: Module("Tenants")], with no class declared with [SeatAggregate]
[TenantAggregate<TenantId>] public sealed partial class Tenant; // DDD00065: 'SeatManager' is not written, ...
[OrganizationAggregate<TenantId>] public sealed partial class Organization;
[OrganizationUnit<OrganizationUnitId>] public sealed partial class OrganizationUnit;
[RoleAggregate<RoleId>] public sealed partial class Role;
// The same project, with all the classes, and a type of the class's name in a namespace of its own
namespace Shop.Tenants.Settings;

public static class SeatManager; // DDD00065, on this type: the class would hide it where Shop.Tenants.Settings is imported

A package names its generic classes in [assembly: TemplateFacade]. For each, the toolkit's generator writes a class into the project that declares your classes. The class derives from the package's class, closed over your classes, and has the package's class name without the type parameters. Tenancy names seven: TenantManager, OrganizationManager, SeatManager, RoleManager, InvitationManager, TenancyAccessManager and the record TenantToProvision. So your code takes a SeatManager and names SeatManager.SeatOverview (The managers and the access manager). Each type parameter of the package's class is one of your classes or ids. So the generator writes a class only when every template it takes from has one class, in that project or in a project of the same module that the project references.

If a class is not written, the projects above that name it only get CS0246: the name does not exist. The project that registers Tenancy would give more detail. That project references the application project, and does not build once the application project fails. So the project that declares the classes reports the cause, on the first of the classes, once for each class that is not written:

What keeps it outWhat to do
no class is declared with one of the templatesdeclare it, next to the others
several classes are declared with one templatekeep one
a class does not meet the constraints of the package's classgive it what the message names
the project declares a type of that name in a namespace. The class would hide that type wherever the namespace is importedrename that type, or name the class otherwise: [assembly: TemplateFacadeName("SeatManager", "ShopSeatManager")]

DDD00065 has the severity Info. A module whose classes are split over two projects gets DDD00065 in the first project, where nothing is wrong. The second project, where the classes are complete, gets the class.

Some causes are reported elsewhere, and not again here:

  • A template whose class a parent in the project needs is that class's error already: DDD00044 or DDD00045.
  • A template that a registration the project can call takes from is reported by the registration: DDD00049 or DDD00045.
  • A class that cannot be generated has its own diagnostic.
  • A class that the package marks OnlyWith a template is written only where that template has a class. A project without one gets no class and no diagnostic. Tenancy's InvitationManager is written only where the module declares an invitation class.

A package's interface gets an alias instead of a class, such as Tenancy's TenancyStore. The same causes keep the alias out of the project that declares the classes. A project above that declares a type of the alias's name in a namespace gets DDD00065 on that type, because the alias would hide it wherever the namespace is imported.

A name that you keep in the global namespace yourself is your own way of naming the classes: an alias of the name at the top of a file, or a type or a namespace of that name. The generator leaves that name alone and reports nothing. A name that you give with TemplateFacadeName, and that a type or a namespace in the global namespace already has, is DDD00076.

DDD00066​

A package's switch writes a class or an id where its name is free and its id is known.

// Shop, with the switch, and a class of its own called Role that has nothing to do with Tenancy
[assembly: GenerateTenancyClasses] // DDD00066: [assembly: GenerateTenancyClasses] does not write 'Role': this project has a class 'Shop.Role' already, which is not declared with [RoleAggregate]: if it is meant to be that class, declare it [RoleAggregate<RoleId>] and it is yours; ...

namespace Shop;

public sealed class Role;
// Two projects this one references each declare a RoleId
[assembly: GenerateTenancyClasses] // DDD00066: ... does not write 'Role': its id would be 'RoleId', and the projects it references declare 'Shop.One.RoleId' and 'Shop.Two.RoleId'; keep one

A package's switch, such as Tenancy's [assembly: GenerateTenancyClasses] (Quick start), makes the generator write the package's classes that your project leaves out, and their ids. It writes them as the package ships them, for example Tenant and TenantId. They are public, in the project's root namespace. The switch writes nothing that would replace something you have. If it cannot write a class or an id, it reports DDD00066 on the switch:

What is in the wayWhat to do
a class of yours with the class's name in the root namespace, not declared with the templateif it is meant to be the package's class, declare it with the template, [RoleAggregate<RoleId>]. If not, rename it, or declare the package's class yourself under another name: [RoleAggregate<RoleId>] public sealed partial class ShopRole
another type of that name, yours or one that a referenced project declaresrename it, or declare the package's class yourself under another name
a namespace of that name, such as a folder Organization/ directly in the projectdeclare the package's class yourself under another name, or rename the namespace
a type of the id's name that is no entity idmark it [EntityId<Guid>], or rename it
an id of that name that the generator writes for an [AggregateRoot<Guid>] class of yours, such as a Tenant that rents somethingrename that class, or declare the package's class yourself under another name, with its own id
several ids of one name, in the project or in the projects of its module that it referenceskeep one
your own classes of templates that share an id, such as the tenant and the organization, declared over two different idsdeclare them over one id

DDD00066 reports each cause once:

  • A class that takes a type from a class the switch cannot write, or that shares its id, is not written either, and is not reported again. The organization shares the tenant's id, and the role and the seat take it. So a TenantId in the way leaves out four classes, with one error.
  • No id is written for a class that is not written.
  • AddTenancy() and the class that the use cases are named through do not report the left-out classes as missing (DDD00049, DDD00065).

A class or an id that you declare yourself always wins, and is never reported. The switch takes it, or writes the rest around it.

DDD00067​

A class whose package makes its new ids is declared over an id with a Create().

[EntityId<long>]
public readonly partial record struct TenantId;

[TenantAggregate<TenantId>] // DDD00067: 'TenantId' has no public static Create(), and 'Tenant' is declared with [TenantAggregate], whose package makes each new TenantId with TenantId.Create(), in code and before the save. ...
public sealed partial class Tenant;

An id is made in code before the save. The database never makes it. So a change and every event of the change know the id from the start. Tenancy makes the ids of the tenants, units, seats, roles and invitations that it creates itself, with the id's own Create(). The generator writes Create() for every [EntityId<Guid>]. It makes a time-ordered id, as CreateSequential() does. An id over a long, an int or a string has no Create(), because the generator cannot know how to make a new one. Write it in the id's partial declaration:

[EntityId<long>]
public readonly partial record struct TenantId
{
public static TenantId Create() => new(Snowflakes.Next()); // a snowflake, or a number of a HiLo block
}

A Create() of your own also wins over the generator's for a Guid. The message says what the id lacks, and what to write:

  • An [EntityId<T>] without a Create(): write one in its partial declaration, as above. The message gives an example of a new value for the type that the id wraps.
  • An id written by hand, without [EntityId<T>], which no generator completes: write the Create(), and add ICreatableEntityId<TenantId> to the interfaces it implements.
  • An id with a fitting Create() and without the interface: add the interface. For an id of a project that does not target net10.0, change that project's target instead. Only the net10.0 build of DTS.DDD.Abstractions has ICreatableEntityId<T>, and the generator implements it where the project sees it.
  • A Create() that is not public static, or that returns another type: make it public static, returning the id.

The error is on the class declared with the template, and names the id. A class that a package's switch writes is in no file of yours. For such a class, the error is on the id when the id is declared in the same project. When the id is in another project, such as the module's contracts project, the error is on the switch, [assembly: GenerateTenancyClasses].

The code fix goes to the id in every case, wherever in the solution the id is declared. It adds what is missing: a Create() whose body throws until you write it, the interface, or both. If the fix is not in the id's declaration, such as a Create() that does not fit or a project's target, the code fix offers nothing.

The class itself is generated. The package's registrations and the class that its use cases are named through require the same with a constraint. They do not report the class again, and are not written for it, so they do not fail inside generated code.

A package says which of its templates it makes the ids of, on the marker: [AggregateRootTemplate(typeof(SeatAggregate<,,,>), CreatesIds = true)] (A package that makes the ids of its classes). Only Tenancy's templates do this. Any other id, one that the application makes itself, needs no Create(), and nothing reports it.

DDD00068​

DDD_ModuleContracts makes a project its module's contracts where the project can name the attribute.

<!-- Billing.Contracts.csproj: the analyzers of this version, and a DTS.DDD.Abstractions from before the attribute -->
<PropertyGroup>
<DDD_ModuleContracts>true</DDD_ModuleContracts> <!-- DDD00068 -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DTS.DDD.Abstractions" Version="3.1.0" />
<PackageReference Include="DTS.DDD.Analyzers" Version="..." />
</ItemGroup>

DDD_ModuleContracts set to true makes a project its module's contracts. The toolkit's generator writes [assembly: ModuleContracts] into the project, and every public type of the project is published to the other modules (A contracts project). The attribute is declared in DTS.DDD.Abstractions. The generator ships in DTS.DDD.Analyzers. A contracts project may reference these two packages separately. If the project references a DTS.DDD.Abstractions older than the attribute, or none, the generator cannot write the attribute:

DDD_ModuleContracts is true, and the project publishes nothing by it: the toolkit's generator writes
[assembly: ModuleContracts] from it, and no DTS.DDD.Abstractions the project references declares that
attribute. Reference the DTS.DDD.Abstractions of the same version as DTS.DDD.Analyzers, or mark each type
the other modules may name [ModuleContract].

The property is the project's explicit choice. Without DDD00068, the build would drop it with no message. The first sign would be DDD00022 in every module that names one of the project's types. DDD00022 says that the type is not published, which points away from the cause.

Reference the DTS.DDD.Abstractions of the same version as the analyzers. A project that references the DDDToolkit package gets both at one version. If you cannot do that yet, mark each type the other modules may name [ModuleContract], and remove the property. A project whose property is anything but true is not reported. A project that declares [assembly: ModuleContracts] itself is not reported either.

DDD00068 is reported at the project file, because no line of code is wrong. The project file is outside every source file, so a severity in an .editorconfig section for *.cs files does not reach it. Set the severity with <NoWarn> or <WarningsAsErrors> in the project file or a Directory.Build.props, or in a .globalconfig file with is_global = true.

DDD00069​

A module's row access contribution is listed by the project that runs the export.

DDD00069 is reported in the project that turns the Supabase export on (<SupabaseMigrationsExport>), about a module it references:

// In the module's infrastructure project
[assembly: RowAccessContribution(typeof(UnitChangesWithItsPermissions))]

// In the project that runs the export: without this line, DDD00069
[assembly: UseRowAccessContribution(typeof(UnitChangesWithItsPermissions))]

A module that writes SQL of its own, a trigger or a policy, declares its class with [assembly: RowAccessContribution], as a package does. The module's assembly declares a module, with DDD_Module or [assembly: Module]. So the declaration is an offer, and the build does not write the SQL by itself. The SQL is your application's own. The project that runs the export decides what your database gets, with [assembly: UseRowAccessContribution(typeof(X))]. The export makes each class it lists with new X(). The offer makes a forgotten line visible. Without it, nothing would report that a module's trigger never reached the migrations, and the trigger's limits on the rows would hold nowhere.

List the class, a class of yours derived from it, or a generic one closed with your types. The message says which one the offer needs, because the export makes it with a constructor that takes no parameters. If this application's database must not have the module's SQL, leave the line out. Suppress the warning for the project with <NoWarn>, with the reason beside it. A package is an assembly that declares no module. A package writes its SQL by itself and needs no line.

DDD00070​

A member a library marks for a package's row access contribution is public.

DDD00070 is reported in the library that declares the member, at the member:

public static class ShopCatalogue
{
[TenancyCatalogue]
internal static ApplicationCatalogue Application { get; } = new(...); // DDD00070
}

A package's row access contribution is made in the project that runs the Supabase export, from the members you mark:

  • [TenancyCatalogue];
  • [TenancyOperators];
  • [MembershipRules<TMember>];
  • the mark of another package, whose attribute says that it is a mark with [ApplicationMark].

The exporting project finds the marks in the projects it references. Of a library, it sees what is public and nothing else. It does not see an internal member, a member in an internal class, or a property whose getter is not public. The export would then write Tenancy's policies from the default catalogue instead of yours, or leave out a resource's functions, and the exporting project could not say why. Make the member public, in public types, with a public getter.

An application, the program that runs the export or the host, may keep its own marks internal. No project references an application for its marks.

DDD00071​

What a package's row access contribution is made from is found once, and is what it takes.

DDD00071 is reported in the project that turns the Supabase export on (<SupabaseMigrationsExport>), about a package it references that declares itself a contributor. It is reported at the member you marked, when that member is declared in the same project:

public static class ShopCatalogue
{
[TenancyCatalogue]
public static ApplicationCatalogue Application { get; } = new(...);

[TenancyCatalogue]
public static TenancyCatalogue Built { get; } = TenancyCatalogue.Build(Application, ...); // DDD00071: it takes an 'ApplicationCatalogue'
}

A package that declares itself a contributor writes its row level security into your migrations. It makes the SQL from the static properties and fields that you mark with the attributes it names: [TenancyCatalogue] for Tenancy's catalogue, [MembershipRules<TMember>] for a resource's rules. The build reads them in the project that runs the export, so a marked member must be:

WhatWhy
marked once in all the projects the export sees, or once per type for a generic markthe build cannot tell which of two your application runs with, so it writes neither instead of guessing
of a type the package takes: an ApplicationCatalogue for [TenancyCatalogue], not the catalogue built from itthe package builds the catalogue itself, from your part and your modules' permissions, as your registration does
static, with a getter, in a type that is not genericthe code the build writes reads it as Type.​Member
readable from the exporting projectthe code the build writes is in that project

A member that another project declares, and that is not public or is in an internal class, is not reported here. The exporting project does not see it at all, so for that project nothing is marked. The project that declares the member reports it instead, as DDD00070.

Policies written from another catalogue, or from other rules than your host runs with, would grant other access than your application grants. So DDD00071 is an error, and the contribution is not made until it is fixed.

DDD00071 also reports a class that the package declares in a way the build cannot make:

  • a class whose constructor takes a value without saying where to find it, with [FromApplication];
  • a generic class that nothing you mark closes.

The message then ends with "That is the package's to fix". Tell the package's authors, or leave its contribution out with [assembly: LeaveOutRowAccessContribution(typeof(X))] and write those policies yourself.

DDD00072​

A row access contribution a package writes is not listed again.

DDD00072 is reported in the project that turns the Supabase export on (<SupabaseMigrationsExport>), on the line that lists the contribution:

// The project that runs the export, as the toolkit once asked for
[assembly: UseRowAccessContribution(typeof(ShopTenancyRowAccess))] // DDD00072

public sealed class ShopTenancyRowAccess()
: TenancyRowAccessContribution(TenancyCatalogue.Build(ShopCatalogue.Application, TenancyPermissionsOfModules.All));

A package that declares itself a contributor writes its row level security into your migrations because the project references it. It uses what you mark. An application built against an earlier version may still list the package's contribution, because the toolkit once asked for that line. If you list the package's contribution, a class derived from it, or a closing of it over a type the package already closes it with, the export gets the same SQL twice. The result is two functions of one name, or two contributions that each claim the same tables. The export refuses both when it runs. Remove the line and the class, and mark what the class passed in:

public static class ShopCatalogue
{
[TenancyCatalogue]
public static ApplicationCatalogue Application { get; } = new(...);
}

To write a package's SQL with a class of your own on purpose, leave the package's contribution out and keep listing yours: [assembly: LeaveOutRowAccessContribution(typeof(TenancyRowAccessContribution))]. A contributor that another referenced project declares, derived from a package's contribution, is the same SQL a second time. It is reported too, and the message names the project that declares it.

DDD00073​

What [assembly: LeaveOutRowAccessContribution] names is a contribution a package writes, and a context.

DDD00073 is reported in the project that turns the Supabase export on (<SupabaseMigrationsExport>), at the line:

[assembly: LeaveOutRowAccessContribution(typeof(AuditRowAccess), Context = typeof(string))] // DDD00073: no context
[assembly: LeaveOutRowAccessContribution(typeof(UnitChangesWithItsPermissions))] // DDD00073: a module's own

The line keeps a package's SQL out of your migrations. With Context, it keeps the SQL out of one context's access file only. The line names the contribution as the package declares it: open where it is generic, or one closing of it that you mark. Context names a class derived from DbContext, closed where it is generic. A line that names anything else leaves nothing out, and still looks as if it does. So it is reported, and nothing is left out for it:

The line namesWhat to do
a Context that is no class derived from DbContext, or a generic context left openname the context whose access file the SQL is kept out of
a class that no referenced package declares as a contributorname what the package declares, or remove the line
a closing that nothing you mark makes, such as MembershipRowAccessContribution<Refund> with no rules marked for Refundremove the line. That closing is never written
a module's own contributionremove the module's [assembly: UseRowAccessContribution] line instead. Only that line writes the module's SQL

DDD00074​

A design-time factory keeps the migration history where the application does.

public sealed class OrderingContextFactory : IDesignTimeDbContextFactory<OrderingContext>
{
public OrderingContext CreateDbContext(string[] args) // DDD00074: 'OrderingContextFactory' makes its 'OrderingContext' without UseDDDToolkitDesignTime(), ...
=> new(new DbContextOptionsBuilder<OrderingContext>().UseNpgsql("Host=unused").Options);
}

UseDDDToolkit and UseDDDToolkitCore keep a context's migration history in the context's default schema, beside its tables. For a model with HasDefaultSchema("ordering"), the history table is ordering."__EFMigrationsHistory" (The migration history). dotnet ef and the Supabase export make the context through its design-time factory instead, before any host exists. So the factory must do the same, with UseDDDToolkitDesignTime().

Without that call, these record every migration in the provider's default schema, public or dbo: dotnet ef database update, a migrations script, a migration bundle and the exported files. The running application reads the history in the context's own schema. Migrate() then tries to apply the first migration again, and fails on a table that exists. On Supabase, the start-up check reports every migration as missing.

public OrderingContext CreateDbContext(string[] args)
=> new(new DbContextOptionsBuilder<OrderingContext>().UseNpgsql("Host=unused").UseDDDToolkitDesignTime().Options);

Add the call after the provider. The code fix adds it, before the options' .Options.

DDD00074 is reported in a project that references DTS.DDD.EntityFramework, at CreateDbContext of a factory. It is reported when the factory's class calls none of UseDDDToolkitDesignTime, UseDDDToolkit, UseDDDToolkitCore and MigrationsHistoryTable, in CreateDbContext or in a method, property or field of the class that CreateDbContext reads. The analyzer reads one class at a time. So a factory whose options a helper of another class makes is reported too. Add the call after the helper. The call adds nothing that the options have already.

A context that the host does not wire with the toolkit, and whose history stays where Entity Framework keeps it, names the table in the factory's options and in the host's options alike: MigrationsHistoryTable(HistoryRepository.DefaultTableName). Or suppress the warning.

A context marked [SupabaseMigrations] needs no factory written by hand. The build writes one beside it that calls UseDDDToolkitDesignTime() wherever the project references DTS.DDD.EntityFramework. The analyzer does not read generated code. See Supabase.

DDD00075​

Every class a package's use cases are named through has a name of its own where a project sees it.

Shop.Host.csproj: warning DDD00075: 'SeatManager' is the name of more than one class in this project: it sees the
one 'Shop.Customers.Domain' has for the module Customers and the one 'Shop.Partners.Domain' has for the module
Partners. Give one of them a name of its own, with [assembly: TemplateFacadeName("SeatManager",
"CustomersSeatManager")] in 'Shop.Customers.Domain'.

The toolkit's generator writes Tenancy's managers, its access manager and TenantToProvision into the project that declares your Tenancy classes. Each class is named after the package's own class: SeatManager, TenancyAccessManager and so on (Two modules with the classes). An application with two modules that each declare Tenancy's classes gets two classes of each name, one in each module's domain project. While neither module references the other, neither project sees the other's class, and neither reports anything. The two classes meet in every project that references both modules, first the host and your tests. There, the first line that names SeatManager gets the compiler's CS0433, with no hint of what to do. So DDD00075 is reported in such a project, at its project file, because no line of its code is wrong. It is reported once for each name, so a host that sees two modules with Tenancy's classes gets seven warnings for the classes.

The stores are aliases, TenancyStore and InvitationStore, which the generator writes into every project that sees the classes. A project that sees two modules' classes would get two aliases of one name. It gets neither, and DDD00075 says so at its project file, once for each alias:

Shop.Host.csproj: warning DDD00075: 'TenancyStore' is the name of more than one alias in this project: it sees the
classes 'Shop.Customers.Domain' declares for the module Customers and the classes 'Shop.Partners.Domain' declares for
the module Partners, and would get an alias of ITenancyStore for each. Give one of them a name of its own, with
[assembly: TemplateFacadeName("TenancyStore", "CustomersTenancyStore")] in 'Shop.Customers.Domain'.

What happens depends on where the two classes meet. DDD00075 is a warning in every case. It stops no build unless warnings are errors there. The compiler's error on the first line that names the class stops the build:

Where the two classes meetWhat the generator writes thereWhere DDD00075 is reportedWhat your code gets while the warning stands
a project that references both modules' projects, such as the host or a test projectnothing. The classes belong to the modulesat the project file, because no line of it is wrongCode that names neither class builds and runs. The first line that names SeatManager gets CS0433, an error
a project that declares one module's classes, and references another module's project that has a class of that nameits own class, as usualon the project's first Tenancy class, and not also at the project fileThe project's own code builds and names its own module's classes. The compiler takes the class from the project's own source and reports CS0436, a warning. Every project above it sees both classes, gets DDD00075 at its project file, and gets CS0433 on the first line that names the class
a project that would get two classes of one name itself: two packages' classes of one name, or a name you gave that another of its classes hasneither class, because the two cannot be told aparton the project's first classCode that names neither class builds. A line that names the class gets the compiler's error that no such type exists

Give one of the classes a name of its own, with the line that the message writes out, in the project it names. That is the project that declares that module's classes. If you named that class already, the message names the line that names it. Give another name in that line. A second line for one class changes nothing. Name every class of the module that the host sees twice, one line each:

// Shop.Customers.Domain, beside [assembly: Module("Customers")] or Tenancy's switch
using DTS.DDD.Abstractions.Attributes;

[assembly: TemplateFacadeName("TenantManager", "CustomersTenantManager")]
[assembly: TemplateFacadeName("OrganizationManager", "CustomersOrganizationManager")]
[assembly: TemplateFacadeName("SeatManager", "CustomersSeatManager")]
[assembly: TemplateFacadeName("RoleManager", "CustomersRoleManager")]
[assembly: TemplateFacadeName("InvitationManager", "CustomersInvitationManager")] // where the module declares an invitation
[assembly: TemplateFacadeName("TenancyAccessManager", "CustomersTenancyAccessManager")]
[assembly: TemplateFacadeName("TenantToProvision", "CustomersTenantToProvision")]
[assembly: TemplateFacadeName("TenancyStore", "CustomersTenancyStore")]
[assembly: TemplateFacadeName("InvitationStore", "CustomersInvitationStore")] // where the module declares an invitation

Every project above it then takes Customers' managers as CustomersSeatManager and the others, and Partners keeps SeatManager. Any name that a class can have works, CustomersSeats too, as long as no namespace or type of yours in the global namespace has it. The message only suggests a name.

If two packages' classes share a name in one project, the line names the one it means with its namespace: [assembly: TemplateFacadeName("Acme.Billing.UseCases", "BillingUseCases")]. A type, a namespace or an alias of the name that you keep in the global namespace yourself is your own way of naming the classes. DDD00075 does not report it.

DDD00075 is a warning in all three cases. A host that names neither class builds and runs, and the compiler stops the first line that names one. A project that builds with TreatWarningsAsErrors stops at DDD00075 itself, and in the second case also at CS0436. To make DDD00075 an error everywhere, add it to <WarningsAsErrors>.

Where DDD00075 is reported at the project file, it is outside every source file. Set its severity there with <NoWarn> or <WarningsAsErrors>, or in a .globalconfig file with is_global = true. An .editorconfig section for *.cs files does not reach it. Where DDD00075 is reported on a class, an .editorconfig section works as well.

DDD00076​

[assembly: TemplateFacadeName] names, once, a class this project gets.

[assembly: TemplateFacadeName("SeatManagers", "ShopSeats")] // DDD00076: ... changes nothing: no package this project
// references asks for a class of that name; it can name
// 'InvitationManager', 'OrganizationManager', ... and 'TenantToProvision'

The line gives your own name to a class that the generator writes for a package's [TemplateFacade]. It works in the project where the generator writes the class: the project that declares the classes. A module whose classes are split over two projects may put the line beside the module declaration in either project. The project that writes the class reads the line in the projects of its module that it takes classes from. A line that changes nothing looks as if it changed the name. So it is reported at the line, and the class keeps its name:

The lineWhat to do
names no class that a referenced package asks forwrite the class name as the message lists it: the package's class name without its type parameters
names two classes: two packages' classes of one namename the class you mean with its namespace, "DTS.​Tenancy.​Managers.​SeatManager"
is in a project that declares none of the classes, such as an application project above the domain projectmove it into the project that declares the classes. The class is written and named there
names a class that an earlier line names already, in this project or in a project of its module that it takes classes fromkeep one line. This project's line wins, then the line of the first other project by name
gives a name that no class can have, such as a keyword or a name with a spacegive a name that a class can have
gives a name that a namespace or a type in the global namespace already has, such as the module's root namespace, Customers for Customers.​Domaingive another name. The class cannot have that name too, and the projects above would only get CS0234

A line in another project of the module is reported in the project that writes the class, on the line in that project or on its first class. The message names the project that the line is in. An alias of the name at the top of one of the project's own files is your own way of naming the classes. The generator leaves it alone and reports nothing.

DDD00077​

A request says what it requires of its caller.

[AccessRequests]
public partial interface IBillingRequest : IRequireAccess;

public sealed record CloseInvoice(InvoiceId Invoice) : IBillingRequest; // DDD00077: says nothing of what it requires

Every command and query says who may send it. The module's checks hold the caller to that before the handler runs. A request says it in one of two ways (What a request declares):

  • with an attribute, such as [SignedIn], or with a permission marker on the member where the permission is held, such as [OnResource(permission)];
  • by answering RequiredAccess itself, where the requirement is computed.

If the module's interface is partial, the generator implements RequiredAccess in it. The compiler then no longer requires a request to implement the property. So a request that does neither is reported here. If such a request were sent, the access check would refuse it every time, with an exception that says it declares nothing. A request that anyone may send says [AllowAnonymous].

[RequiresPermission(BillingPermissions.Close)]
public sealed record CloseInvoice(InvoiceId Invoice) : IBillingRequest;

A request in another project than its interface is reported too. The line that the generator writes into the interface knows only the requests of the interface's own project. So such a request answers RequiredAccess itself. An abstract request is not reported. The requests that derive from it are reported. If the interface is not partial, the compiler's own CS0535 reports the same problem.

A module whose requests nearly all require the same thing says it once, on its interface: [AccessRequests, InTenant]. A request of that module that states nothing then takes this default, on a line of its own in the generated switch, and is not reported. A request of another project is not reported either. The interface's last arm answers it with the default (A module's default). The default is the module's choice, and it has a cost. A command of the module that forgets its permission requires what the default says, and the build reports nothing.

DDD00078​

A request says what it requires in one way.

[SignedIn] // DDD00078: and the property as well
public sealed record MyInvoices : IBillingRequest
{
AccessRequirement IRequireAccess.RequiredAccess => AccessRequirement.AllowAnonymous();
}

C# takes the request's own property over the line that the generator writes from the attribute. So the attribute does nothing, while a review reads it as the requirement. That is why DDD00078 is an error. Keep one of the two:

  • the attribute, where the requirement reads no value of the request;
  • the property, where it computes the requirement from the request's values.

A permission marker is a requirement too, so a marker beside the property is reported the same way.

The same holds for an interface between the request and its module's interface that gives RequiredAccess a default of its own, such as interface IPublicBillingRequest : IBillingRequest. C# takes that default over the module's. The message then says that the request answers RequiredAccess through that interface. A request that derives from one that answers RequiredAccess answers it too, so it has no requirement attribute or marker of its own.

DDD00079​

[AllowAnonymous] goes with no other requirement attribute.

[AllowAnonymous, RequiresPermission(BillingPermissions.Close)] // DDD00079: open beside closed
public sealed record CloseInvoice(InvoiceId Invoice) : IBillingRequest;

Requirement attributes stacked on one request all hold, as several [Authorize] attributes do in ASP.NET Core (Stacked attributes are all). [AllowAnonymous] says that anyone may send the request, including a caller who did not sign in. A requirement beside it limits who may send the request. The two contradict each other, and a review cannot tell which one was meant. So DDD00079 is an error beside any other requirement, including [RequiresPermission], [OwnOnly] and a permission marker. The generator writes no line for the request. Keep the attribute that the request needs. A request that anyone may send has [AllowAnonymous] alone.

A request also holds to the attributes of the requests it derives from, theirs first. So [AllowAnonymous] on a request that derives from one with [SignedIn] is the same contradiction. DDD00079 reports it at the derived request, and names the base's attribute. A derived request cannot take back what its base requires.

Two other requirement attributes on one request are not a mistake. They stack, and both hold. An attribute that its neighbour already requires is DDD00084, a warning.

DDD00080​

A permission marker names its permission, and a marker argument belongs to one marker.

[SignedIn]
public sealed record OpenLedger([AtUnit] OrganizationUnitId Unit) : IBillingRequest; // DDD00080: names no permission

public sealed record FileDocument(
[OnResource(DocumentPermissions.Edit)] DocumentId Document,
[OnResource(FolderPermissions.File)] FolderId Folder,
[ExpectedVersion] long? Version = null) : IFilingRequest; // DDD00080: whose version?

A permission marker on one of the request's members names a permission. The member is the place where the caller must hold it (Where the permission is held):

  • [AtUnit(permission)]: at that unit or above it;
  • [OnResource(permission)]: on that resource, through its members. [ExpectedVersion] beside it gives the version the caller read.

A request requires every marker on it, also two markers of one kind. [RequiresPermission(permission)] on the request names a permission without a marker. A package declares where such a permission is held. Tenancy declares that it is held for the whole tenant.

DDD00080 is reported at the marker, the attribute or the request:

ItBecauseFix
marks a member [AtUnit] without a permissionthe marker's argument is the permission that the caller holds at the unit. Without an argument, [AtUnit] is the level of a permission constant[AtUnit(permission)]
marks [ExpectedVersion] and no member [OnResource]the version belongs to a resource markermark the resource, or take the version off
marks a bare [ExpectedVersion] beside [OnResource] on two kinds of resourcethe bare form cannot say which resource the version belongs toname the resource, [ExpectedVersion<Document>]
marks [ExpectedVersion<TResource>] and no [OnResource] on the id of TResourcethe version names a resource that the request does not markname the resource of a marker on the request
marks [ExpectedVersion] or [ExpectedVersion<TResource>] beside two [OnResource] of one kindthe type argument cannot say which of the two calls the version belongs towrite RequiredAccess, with the version in the right call
marks one argument twice for one marker, such as two [ExpectedVersion]the call takes each argument once. The bare and the generic form are the same argumentkeep one
marks a parameter of a class's primary constructorthe requirement cannot read such a parameter. A positional record's parameter is its property, and is finemark the property
marks a private or protected propertythe requirement is read in the module's interface, which needs a public or internal gettergive it a public or internal getter
has [RequiresPermission], where no package declares where a permission without a marker is heldTenancy declares it with [assembly: PermissionWithoutMarker], for the whole tenant. A project with Membership alone, or with two such packages, cannot tellput the permission on the marker of the place where it is held, [OnResource(permission)]
is a module's interface whose default is [RequiresPermission(permission)], where no package declares where a permission without a marker is helda default reads no member of a request, so it cannot use a markerput the permission on each request, on its marker

A marker on a base request is a requirement of every request that derives from it. A bare marker there is reported at the base, and at each derived request that inherits it.

[OnResource] without a permission does not compile, because its constructor takes the permission. The compiler reports CS7036, and DDD00080 is not reported.

A record's positional parameter is the property that its base record declares, if the base record has it. So [AtUnit(permission)] goes on the derived record's parameter as well:

public abstract record UnitCommand(OrganizationUnitId Unit) : IBillingRequest;

public sealed record OpenLedger([AtUnit(BillingPermissions.OpenLedger)] OrganizationUnitId Unit) : UnitCommand(Unit);

DDD00081​

A marker is on a member of the id it marks.

public sealed record MakePlacement(SeatId Seat, [AtUnit(TenancyPermissions.SeatsManage)] SeatId Unit) : ITenantsRequest; // DDD00081: a seat's id, no unit's

public sealed record ListLedgers([AtUnit(BillingPermissions.Read)] OrganizationUnitId? Unit) : IBillingRequest; // DDD00081: may be null

The call that a marker stands for takes any id. So a unit marker on a seat's id compiles and lets nobody through. The package's access check refuses a case over an id that it does not decide. The marked member must have the right type:

  • [AtUnit(permission)] marks your unit id: the id that your class declared with [OrganizationUnit<TUnitId>] is declared over.
  • [OnResource(permission)] marks the id of a resource with members: the aggregate root that a class declared with [Member<TId, TMemberId, TRoleId, TResource>] names.

The package declares which id a marker takes, with [PermissionMarker(..., IdOf = ...)]. DDD00081 is reported for each marker separately, in a project that sees such classes, as a module's application project sees its domain's classes. In a project that sees none, the analyzer cannot tell, and the access check reports the problem when the request is sent.

A member that may be null is a requirement computed from whether the value is given. Write it as RequiredAccess: Unit is { } unit ? TenancyAccess.AtUnit(permission, unit) : TenancyAccess.AtTenant(permission). [ExpectedVersion] marks a member of the type that the call takes there: a long? or a long.

DDD00082​

A requirement for queries only is on a query.

[SeenWith<DocumentId>(DocumentPermissions.View)]
public sealed record ArchiveDocuments : ICommand, IDocumentsRequest; // DDD00082

Some requirements are for queries only. Their packages say so with [StatesRequirement(..., QueriesOnly = true)]. Membership's [SeenWith<TResourceId>(permission)] is one. It refuses nobody for lack of the permission. It lets every signed-in caller through, also one that holds nothing, and says what the query's own statement filters by. That filter is the check. On a command, the handler would run unchecked.

DDD00082 reports a message of the toolkit's mediator that is not a query and carries such an attribute:

  • a command, ICommand or IStreamCommand;
  • a plain IRequest or IStreamRequest, which may change as much as a command does.

What to do instead:

The requestInstead
only readsmake it a query, IQuery or IStreamQuery
changes something, [SeenWith] on itrequire the permission where it acts, [OnResource(permission)] on the resource

Tenancy's [Operator] is no requirement for queries only. On a command, the pipeline runs the handler as operator work (An operator's request).

A request of another dispatcher is not reported, because the generator cannot tell whether it is a command or a query. The same requirement answered as RequiredAccess on a command is not reported either. A test covers both cases (Holding it with a test).

DDD00083​

A request's requirement attributes are read where its module's interface is partial.

[AccessRequests]
public interface IBillingRequest : IRequireAccess; // not partial

[SignedIn] // DDD00083: nothing turns this into what it requires
public sealed record MyInvoices : IBillingRequest;

The generator turns a request's attributes into RequiredAccess at compile time, one line per request. It writes the line into the interface marked [AccessRequests] that the request implements, declared partial in the same project. Without such an interface, nothing reads the attributes, and the request looks as if it declares what it requires. DDD00083 reports each case, with what to change:

The requestChange
implements an interface that is not partialdeclare it public partial interface IBillingRequest : IRequireAccess;
implements no interface marked [AccessRequests]implement the module's request interface
implements one declared in another projectdeclare the request in that project, or answer RequiredAccess
has type parametersanswer RequiredAccess. A generated line names one closed type
is nested private in another typemake it internal, or answer RequiredAccess. The interface cannot name a private nested type
implements two module interfaces that are both partialanswer RequiredAccess, or implement one of them. Each interface implements the property, so C# takes neither (CS8705)

A requirement attribute on an interface is the default of a module's requests (A module's default). The generator reads it only on the module's interface, marked [AccessRequests] and partial. On a module's interface that is not partial, the attribute is reported at the interface, with the line to write. On any other interface, such as one between the module's interface and its requests, nothing reads the attribute either. Such an interface answers RequiredAccess itself where its requests share a requirement. C# takes that over the module's default.

DDD00084​

A requirement attribute beside another adds what that one does not require already.

[InTenant] // DDD00084: the permission requires it already
[RequiresPermission(BillingPermissions.Close)]
public sealed record CloseTheBooks(int Year) : IBillingRequest;

[SignedIn, Operator] // DDD00084: an operator is signed in
public sealed record AllLedgers : IQuery<IReadOnlyList<Ledger>>, IBillingRequest;

[OwnOnly, SignedIn] // DDD00084: only a signed-in caller has an identity
public sealed record CloseAccount([Own] Guid User) : IAccountsRequest;

[InTenant] // DDD00084: the permission at the unit requires it already
public sealed record OpenLedger([AtUnit(BillingPermissions.OpenLedger)] OrganizationUnitId Unit) : IBillingRequest;

A request requires every requirement attribute and marker on it. So an attribute that its neighbour already requires adds nothing:

  • Only a seat in the tenant, or system work there, holds a permission of Tenancy's, from [RequiresPermission] or from [AtUnit(permission)]. So [InTenant] beside such a permission adds nothing.
  • [Operator] lets through only a signed-in caller with an operator's token role. So [SignedIn] beside it adds nothing.

The extra attribute changes nothing that is checked, and the generator writes the line. A reader takes it for a second condition and looks for what it adds. So DDD00084 is a warning. Remove the attribute.

A package says what its requirement already requires, with Implies on its mark: [StatesRequirement], [PermissionMarker] or [PermissionWithoutMarker]. A module's own attribute is held to the same rule. The package of a type says what the caller's own of that type requires, with Implies on [assembly: OwnedByCurrentUser]:

  • A Guid is a signed-in caller's identity, so it implies [SignedIn].
  • Tenancy's seat id is a seat in the tenant, so it implies [InTenant] and [SignedIn].

Beside [OwnOnly] with [Own] on such a member, these attributes are reported. For the permission that [OrOwn] pairs with, only what both the permission and the caller's own require counts. Every other permission of the request, on a marker or in [RequiresPermission], counts in full.

These are reported too:

  • An attribute that a request's base request carries already. The base's attributes hold for the request as well.
  • A request's own attributes that say what its module's default says, call for call. Without them, the request takes the default and requires the same (A module's default). They are reported only on a request that nothing derives from, because a request that derives from it holds to those attributes beside its own.
  • On the module's interface, an attribute of the default that another attribute there already requires. It is reported as on a request.

Examples:

BesideReportedBecause
[SignedIn] beside [RequiresPermission]nosystem work holds every permission and has no sign-in. So the two together require more than the permission alone
[InTenant] beside [RequiresPermission] with [OrOwn] on the seat idyesboth ways through need a caller in the tenant: the caller's own seat, or a permission of Tenancy's
[InTenant] beside [RequiresPermission] with [OrOwn] on a Guidnoa verified identity is no seat, so the caller's own way through does not require the tenant
[InTenant] beside [OnResource(permission)] and [AtUnit(permission)]yesthe permission at the unit is held only in the tenant, whatever the resource requires
[SignedIn] beside [RequiresPermission] with [OrOwn] on the seat idnosystem work holds the permission and has no sign-in
[InTenant] on CloseTheBooks : Closing, where Closing is [RequiresPermission(permission)]yesthe base's permission holds for the derived request too
[InTenant] on a sealed request of a module whose default is [InTenant]yeswithout it the request takes the default, which is the same
[SignedIn, InTenant] on a request of a module whose default is [InTenant]noit requires more than the default: a signed-in caller in the tenant
[InTenant] on a request another derives from, in that modulenothe derived request holds to this attribute beside its own, and would require less without it
[InTenant] beside [RequiresPermission(permission)] on the module's interfaceyesas on a request, the default's permission requires it already

DDD00085​

[OrOwn] pairs with one permission, and [Own] needs [OwnOnly].

[InTenant]
public sealed record RenameSeat([OrOwn] SeatId Seat) : ITenantsRequest; // DDD00085: no permission to be the alternative of

public sealed record MoveSeat([OrOwn] SeatId Seat, [AtUnit(TenancyPermissions.SeatsManage)] OrganizationUnitId To) : ITenantsRequest; // DDD00085: beside [AtUnit]

public sealed record DetachAssetCommand(
[OnResource(BoardPermissions.Edit)] BoardId Board,
[OnResource(AssetPermissions.Use)] AssetId Asset,
[OrOwn] SeatId AttachedBy) : ICommand<Board>, IBoardsRequest; // DDD00085: whose alternative?

A request about the caller's own declares it in one of two ways (Yours, or the permission):

  • [OrOwn] on the member, beside a permission: the caller's own, or the permission. The bare form pairs with the request's only permission, on a marker or in [RequiresPermission(permission)]. Beside two or more permissions, [OrOwn<TResource>] names the resource of the [OnResource] it pairs with.
  • [Own] on the member, with [OwnOnly] on the request: the caller's own alone.

DDD00085 is reported at the marker or the request:

ItBecauseFix
marks [OrOwn], and requires no permission[OrOwn] makes the caller's own an alternative to a permissionput the permission on the request; for the caller's own alone, use [OwnOnly] with [Own]
marks a bare [OrOwn] beside permissions of two kinds[OrOwn] pairs with one permissionname the resource, [OrOwn<Board>]
marks [OrOwn<TResource>] and no marker on the id of TResource[OrOwn<TResource>] pairs with the marker on that resourcemark the resource's id, or name the resource of a marker on the request
marks [OrOwn] or [OrOwn<TResource>] beside two permissions of one kind, such as two boardsthe type argument cannot say which of the two permissions the caller's own is an alternative tocompute the requirement as RequiredAccess
marks [OrOwn] beside [RequiresPermission] and [AtUnit(permission)]neither permission has a resource that [OrOwn<TResource>] could namecompute the requirement as RequiredAccess
marks [Own], and is not [OwnOnly][Own] names the value for a request about the caller's own alone, which [OwnOnly] declaresput [OwnOnly] on the request, or mark the member [OrOwn]
has [OwnOnly], and marks no member [Own]the request names no value to compare with the caller's ownmark the member [Own]
marks one member [Own] and [OrOwn][Own] means the caller's own alone, and [OrOwn] means the caller's own or the permissionkeep the one it means
marks two members [OrOwn], or two [Own]the caller's own is one valuecompute the requirement as RequiredAccess
pairs [OrOwn] with [AtUnit(permission)]the unit names where the permission is checked, and the request acts on something else. A caller could send a unit where it holds the permission, together with another person's seat. A permission held at one unit would then act on a seat anywherecompute the requirement as RequiredAccess
marks a parameter of a class's primary constructor, or a private propertythe line is written in the module's interface, which reads a property with a public or internal gettermark the property, and give it a public or internal getter

[OrOwn] beside [OnResource] is allowed, because the resource is what the request acts on. Membership declares this with OwnAlternative on its [PermissionMarker]. OwnAlternative names the case that the caller's own way through must still pass on the resource: MemberAccess.Sees. The caller must see the resource, and the access check reads the resource, compares its version and keeps its hold (The caller's own on a resource). A package marker of the place where a permission is checked, when the request acts on something else, names no OwnAlternative. [OrOwn] paired with such a marker is reported in the same way as with [AtUnit].

DDD00086​

[OrOwn] and [Own] mark a member of a type something says the caller's own of.

[RequiresPermission(ProfilePermissions.Edit)]
public sealed record EditProfile([OrOwn] UserId User) : IProfilesRequest; // DDD00086: nothing says the caller's own UserId

[OwnOnly]
public sealed record LeaveTenant([Own] SeatId? Seat) : ITenantsRequest; // DDD00086: may be null

The access check compares the value that the request names with the caller's own value of the same type. It does this before the handler runs, so the request does not need to know who sends it. An IOwnedByCurrentUser<TValue> says what the caller's own value of a type is:

  • the core's, for a Guid: the caller's verified identity, the id that its token names;
  • Tenancy's, for your seat id: the caller's seat in the tenant of the request;
  • yours, for a kind of your own: a small implementation beside your requests, which the host registers.
public sealed class CurrentUserId : IOwnedByCurrentUser<UserId>
{
public UserId? Find(ICurrentUser user) => user.Id is { } id ? new UserId(id) : null;
}

services.AddOwnedByCurrentUser<UserId, CurrentUserId>();

On a member of a type without such an implementation, the access check would refuse the request every time it is sent. So DDD00086 reports the member, and the generator writes no line. The generator finds an implementation in the requests' project, or a public one in a project that the requests' project references. A package names its types with [assembly: OwnedByCurrentUser(...)]. A package may name its type by the template that an application declares its class with, as Tenancy names its seat id. If the project sees no class declared with that template, nothing can be told, and the access check reports the problem when the request is sent.

A member that may be null names no value when it is left out. Write a requirement computed from whether the value is given as RequiredAccess. The caller's own is a value compared with the caller's: a struct that is IEquatable of itself, such as a Guid or an id declared with [EntityId]. A member that is a string, a class or an enum has no own value, whatever the project sees, and is always reported. [OrOwn] string Name is an example.

The id of an aggregate root that marks its owner with [Owner] is an exception, even though nothing says the caller's own of the id. [Own] BoardId Board is then about the board's owner, and the generated line is AccessRequirement.OwnerOf(request.Board) (The owner of an aggregate). The id of an aggregate that marks no owner is reported as above, and the message says where to mark the owner. If an IOwnedByCurrentUser exists for the id itself, that implementation wins, and the owner is not read.

DDD00087​

[Owner] marks the one owner of an aggregate root.

[Entity<ColumnId>]
public sealed partial class Column
{
[Owner] public UserId Keeper { get; private set; } // DDD00087: an entity inside an aggregate
}

[AggregateRoot<BoardId>]
public sealed partial class Board
{
[Owner] public UserId Owner { get; private set; }

[Owner] public UserId CoOwner { get; private set; } // DDD00087: an aggregate has one owner
}

The aggregate states who owns it. That is a fact about what the aggregate is. A request about the owner's own names the aggregate by its id. The access check reads the owner column of the one row that the id names, as the caller (The owner of an aggregate). So [Owner] goes on one instance property of the aggregate root itself. DDD00087 is reported where the property is declared:

It is onBecause
an entity inside an aggregatea request names the root by the root's id, and the access check reads the root's row
a type that is no aggregate rootno request names it by an id whose row the access check reads
a static propertythe owner is a column of each row
a second property of one aggregatean aggregate has one owner. Several owners are the members of a resource with members, which Membership keeps

Of two [Owner] properties, the first in the order they are written is the owner, and the second is reported.

DDD00087 is also reported at a request whose id the project sees as the id of two aggregate roots that each mark an owner. The request cannot say which aggregate it is about. Give each aggregate an id of its own.

DDD00088​

[Owner] marks a property of a type the caller has an own of.

[AggregateRoot<BoardId>]
public sealed partial class Board
{
[Owner] public string Owner { get; private set; } // DDD00088: a string has no own
}

[OwnOnly]
public sealed record ArchiveBoard([Own] BoardId Board) : IBoardsRequest; // DDD00088: nothing says what the caller's own UserId is

The access check compares the owner that the row holds with the caller's own value of the same type, as DDD00086 compares a value that the request names. The owner's type must be one of these:

  • a Guid: the caller's verified identity;
  • your seat id: the caller's seat, from Tenancy;
  • a kind of your own, with a small IOwnedByCurrentUser<TValue> beside your requests, which the host registers with services.AddOwnedByCurrentUser<UserId, CurrentUserId>().

An owner that may be null is fine. A row whose owner is null belongs to nobody, and nobody comes through as its owner.

A string, a class or an enum can never be compared, whatever a project sees. So DDD00088 reports such an owner where the property is declared.

Whether an IOwnedByCurrentUser exists for the owner's type can only be told where the requests are. Your IOwnedByCurrentUser is declared beside them, and the domain does not see it. So a request that names the aggregate is reported there, at its marker, where the project can tell that none exists. The generator writes no line for such a request. If a package names the owner's type by a template whose classes the project does not see, nothing can be told, and the access check reports the problem when the request is sent.

DDD00089​

A module's default requirement is one any of its requests may take.

[AccessRequests, AllowAnonymous] // DDD00089: a request that forgets its requirement would let anyone in
public partial interface IBillingRequest : IRequireAccess;

[AccessRequests, SeenWith<DocumentId>(DocumentPermissions.View)] // DDD00089: a command that says nothing would take it
public partial interface IDocumentsRequest : IRequireAccess;

A requirement attribute on a module's interface is the default of its requests. A request that states nothing of its own takes the default, on a line of its own in the generated switch (A module's default). So every request that forgets its requirement takes the default, including a command that forgets its permission. A default must therefore be a requirement that any of the requests may take. DDD00089 is reported at the attribute:

On the interfaceBecauseInstead
[AllowAnonymous]a request that forgot its requirement would let anyone in. Without a default, the access check refuses such a request[AllowAnonymous] on each request that anyone may send, where a review sees it
[SeenWith<TResourceId>(permission)], any attribute its package declares for queries onlya command of the module that states nothing would take it, and on a command it checks nothing that the command changes (DDD00082)the attribute on each query
[OwnOnly]it reads the member of a request marked [Own], and a default reads no member[OwnOnly] on each request about the caller's own
an attribute whose class's AttributeUsage leaves out interfaces, such as Tenancy's [Operator]the attribute is declared for requests only. The compiler reports this as well (CS0592)for a module's own attribute, add AttributeTargets.​Interface to its class's AttributeUsage, if it may be a default. Put [Operator] on each request of the application's staff

A refused default is not used. The interface's last arm then stops a request that states nothing, as it does without a default. The attributes that a package ships and that may be a default already allow an interface: [SignedIn], [InTenant], and [RequiresPermission(permission)]. As a default, the permission is held for the whole tenant, because a default reads no member and has no marker.

DDD00090​

The names of resources with members are those of the newest access file.

warning DDD00090: Membership names changed since the last access file. Gone: boards (was Board). New: whiteboards
(Whiteboard). Codes clients read change with them, and the database gets new functions. A class you renamed keeps
its old name with [Members("...")].

A resource with members is named after its class. Its refusal codes start with that name, boards.already-member, and its five functions in the database are named after it, boards_i_see. Renaming the class renames both. A client that branches on a refusal code then reads another code. The next access file writes new functions and drops the old ones.

The Supabase export that the build runs compares the names of the resources it writes with the names that the newest access file of each module recorded, with the class each name came from. In that build, it reports once which names are gone and which are new. It does not guess which name became which.

A class renamed on purpose that should keep its name says so: [Members("Boards")]. A class that should take the new name needs nothing more. The warning is not given again once the new access file exists. Without the export, with SupabaseMigrationsExport unset, nothing compares the names, and no warning is given.

DDD00091​

[Members] names a resource with what can be converted to a code and a function's name.

[Members("Crane boards")] // DDD00091: a space
[AggregateRoot<BoardId>]
public sealed partial class Board;

The name that [Members] gives is written in two forms, as a class name is:

  • in lower case with a dash between words, for the resource's refusal codes: crane-boards.already-member;
  • with an underscore between words, for its functions: crane_boards_i_see.

The generator converts the name in the same way as a class name, and splits PascalCase into words. It takes the name as it is, and does not make it plural. Write the name as a class name, CraneBoards, or as it comes out, crane-boards. Use letters and digits, with - or _ between words, starting with a letter. A space, another symbol, a digit or a separator first, or a separator last cannot be converted. The generator then writes nothing for the resource. Its rules, MembershipRules.For<Board>, do not compile until the name is fixed (Names).

DDD00092​

A permission is a public constant of a [Permissions] class, with a level.

[Permissions]
public class BoardPermissions // DDD00092: not static
{
public const string View = "boards.view"; // DDD00092: no level

[On<Board>] internal const string Edit = "boards.edit"; // DDD00092: no public const string

[On<Board>] public const string Comment = "boards.view"; // DDD00092: 'View' declares it already
}

public static class BoardPermissions
{
[AtTenant] public const string Manage = "boards.manage"; // DDD00092: the class is not marked [Permissions]
}

[Permissions]
public static class LedgerPermissions
{
[AtUnit("ledgers.open")] public const string Open = "ledgers.open"; // DDD00092: a level names no permission
}

A module declares its permissions in a static class marked [Permissions]. Every public const string of the class is a permission. A request names it on a marker or in [RequiresPermission], and a role gives it. Each permission has its level: the attribute that says where the permission is asked. The levels are Tenancy's [AtTenant] and [AtUnit], and Membership's [On<TResource>] (The settings of a permission, A module states its permissions once). The packages' generators read this class and nothing else:

  • Tenancy's generator puts every permission into its catalogue.
  • Membership's generator puts every permission on a resource into the rules of that resource.

So each case below would be read as something else, or not at all, and is refused:

ReportedBecause
a class marked [Permissions] that is not staticit holds constants and nothing else
a public const string of such a class without a levelnobody could tell where it is asked, so no catalogue and no rules would hold it
a level on a field that is no public const stringa request names a permission by its constant, also from another module, so the constant must be public
a level on a constant of a class not marked [Permissions]nothing reads that class
one permission declared twice in one classa permission is declared once. The second constant is the mistake
a level written with a permission, [AtUnit(permission)], on a constantthe constant is the permission, and the level says where it is asked. [AtUnit(permission)] is the marker of a request's unit. On the constant, write [AtUnit] without an argument

DDD00093​

A permission's level fits what it is asked on.

[Permissions]
public static class BoardPermissions
{
[On<Column>] public const string Move = "columns.move"; // DDD00093: no member class names Column

[On<Board>(EveryMember = true)] public const string View = "boards.view";
[On<Board>(EveryMember = true)] public const string Read = "boards.read"; // DDD00093: one permission every member holds
}

public sealed record AttachAssetCommand(
[OnResource(BoardPermissions.Edit)] BoardId Board,
[OnResource(BoardPermissions.Edit)] AssetId Asset) : ICommand<Board>, IBoardsRequest; // DDD00093: a board's permission, on an asset

[On<TResource>] says that a permission is asked on one resource with members, and how members hold it there. TResource is the aggregate root that a member class names, [Member<TId, TMemberId, TRoleId, TResource>]. On another class, the permission would be asked on something that has no members, and its rules could not be made.

Of the permissions on one resource:

  • At most one is held by every member (EveryMember). That permission is also the one that sees the resource from above.
  • At most one manages the members (ManagesMembers), and at most one names the owner (NamesOwner). The member manager requires each of them, and the database's lock checks each of them, as one permission.

A permission declared on one resource by two constants is reported as well. Each case is reported where the permission is declared, among the permissions that the project sees.

On a request, the permission of an [OnResource(permission)] marker must be declared on the resource whose id the marker marks. The rules of that resource give only the permissions declared [On<TResource>] for it. A permission declared only on another resource, or with no level on a resource, would let nobody through. DDD00093 is reported at each such marker separately, in a project that sees the permission's declaration. Name a permission declared on the marked resource, or mark the id of the resource the permission is declared on.

[AtUnit(permission)] takes a permission of any level. Tenancy's catalogue holds every declared permission. A role granted at a unit can hold a permission on a resource. The role reaches the resource from above.

DDD00094​

The rules of a resource are made from the permissions declared on it.

public static MembershipRules Rules { get; } = MembershipRules.For<Board>( // DDD00094: no permission on Board
Roles.Custom());

MembershipRules.For<TResource> makes the rules of a resource from the permissions declared on it. These are the constants of a [Permissions] class marked [On<TResource>], in the project that makes the rules or in a project it references. The package's generator passes them in when the project is first used. Rules without a permission give the owner nothing and every role nothing, and the call would throw when the rules are made. So DDD00094 reports the problem where the rules are made. Declare the permissions asked on the resource: The settings of a permission.

DDD00095​

A module's permissions are declared in a [Permissions] class, not on a list of its own.

public static class OrderingList
{
[TenancyPermissions] // DDD00095
public static IReadOnlyList<Permission> Permissions { get; } = [new("orders.view", "Ordering", "See the orders")];
}

A module declares its permissions once, as the constants of a static class marked [Permissions]. Each constant has its level. Its Description or its summary says what it lets somebody do. Tenancy's generator writes the module's list for the catalogue from that class, under the module that DDD_Module or [assembly: Module] names, and marks the list [TenancyPermissions]. The project that composes the modules collects every such list (A module states its permissions once). A list that a module marks with [TenancyPermissions] by hand repeats the permissions, apart from the constants that a request requires. DDD00095 refuses it. Declare each permission as a constant instead:

[Permissions]
public static class OrderingPermissions
{
[AtTenant(Description = "See the orders")]
public const string View = "orders.view";
}

DDD00096​

[OrOwn] beside a permission the owner of the resource holds already says nothing more.

public sealed record RenameBoard([OnResource(BoardPermissions.Edit), OrOwn] BoardId Board, string Name) : IBoardsRequest; // DDD00096

[OrOwn] on a request's member lets the caller through when the member names the caller's own, or when the caller holds the permission it pairs with. On the id of a resource with members, beside [OnResource(permission)] on the same member, the permission is asked on that resource. The resource's owner holds every permission declared [On<TResource>] with Owner = true, because the owner owns the resource. So the permission already lets the owner through. [OrOwn] adds nothing, and only makes the request look as if it did. Remove it.

[Own] and [OrOwn] are meant for two cases:

  • an aggregate with an [Owner] and no members (The owner of an aggregate);
  • a permission that the owner does not hold, declared with Owner = false.

DDD00096 is a warning, because the request does the same with or without the attribute. It is reported for each [OrOwn] or [OrOwn<TResource>] with an [OnResource] on the same member, also on a request with markers on other resources.

DDD00097​

[At] marks the one place an aggregate root hangs at.

[AggregateRoot<BoardId>]
public sealed partial class Board
{
[At] public OrganizationUnitId UnitId { get; private set; }

[At] public OrganizationUnitId? FormerUnitId { get; private set; } // DDD00097: a second, and may be empty
}

[Entity<ColumnId>]
public sealed partial class Column
{
[At] public OrganizationUnitId UnitId { get; private set; } // DDD00097: an entity inside an aggregate
}

[At] marks the property of an aggregate root that says where the aggregate hangs: the unit of an organization that a board belongs to, or the bay a crate stands in. This is a fact of the domain. The rules of a resource with members read it where they let the resource be reached from above. A permission held at that place, or above it, reaches the aggregate (Beside an organization).

So [At] goes on one instance property of an aggregate root, on the root itself and never on an entity inside it. That property is never empty. An aggregate that hangs nowhere could not be reached from above, and reaching from above is what [At] is for. DDD00097 refuses each of these, where the property is declared:

  • a static property;
  • a property on a type that is no aggregate root;
  • a second [At] property;
  • a nullable property.

Keep one [At] property, on the root, of a type that is never empty:

[AggregateRoot<BoardId>]
public sealed partial class Board
{
[At] public OrganizationUnitId UnitId { get; private set; } // the one place, never empty

public OrganizationUnitId? FormerUnitId { get; private set; } // a plain property
}

[At] is Membership's marker, in the namespace DTS.Membership. It ships in the package DylansTechStack.Membership.Abstractions, which references DTS.DDD.Abstractions alone and no Entity Framework, so a domain project references it for the marker. Membership's generator reports DDD00097. That generator ships in the package DylansTechStack.Membership, which references the markers' package. A project that declares the aggregate and references only the markers' package gets no DDD00097. [Owner] stays in the core, because the core's checks of the owner read it.

DDD00098​

Rules that reach a resource from above read where it hangs from its [At] property.

public static MembershipRules Rules { get; } = MembershipRules.For<Board>( // DDD00098: Board marks no property [At]
Roles.Custom(),
reachFromAbove: true);

reachFromAbove in the rules of a resource with members says that a permission held where the resource hangs, or above it, reaches the resource. Where the resource hangs is a fact of the domain. The resource's class declares it with [At] on the property that holds it. The mapping and the functions in the database read it from there. Rules that reach from above a resource that marks no such property would be refused when the resource is first asked about, and when its functions are written. DDD00098 reports the problem earlier, where the rules are made. Mark the property [At], or remove reachFromAbove.

DDD00099​

A permission says what it lets somebody do.

[Permissions]
public static class BoardPermissions
{
[On<Board>] public const string Pin = "boards.pin"; // DDD00099: no description

[On<Board>(Description = "Archive a board")] public const string Archive = "boards.archive"; // said on the level

/// <summary>Comment on a board.</summary>
[On<Board>] public const string Comment = "boards.comment"; // or in its summary
}

The catalogue gives every permission a description. A screen that puts permissions into roles shows the description beside the permission. It says what holding the permission lets somebody do. The description comes from one of two places:

  • Description on the permission's level. This wins when the constant also has a <summary>.
  • The constant's <summary>, when no level gives a description. The analyzer reads it from the comment above the constant, whether or not the project writes a documentation file.

A permission with neither would show as a bare name, so DDD00099 is a warning, on the constant. A description of white space alone counts as none. A retired permission, which no screen offers, needs no description. A translation under the permission's name, in a resx that your failure localizer reads, is applied over either description (A module states its permissions once).

DDD00100​

A restrictive rule narrows a table it can reach.

[RowAccess<Organization>(RowOperations.Read, Restrictive = true)] // the organization, its units and its tree: fine
public static partial class OnlyEmployeesReadTheOrganization { ... }

[RowAccess<Tenant>(RowOperations.Read, Restrictive = true)] // DDD00100: a tenant of Tenancy's
public static partial class OnlyEmployeesReadTheTenant { ... }

A restrictive rule (Restrictive = true) is AND-ed with every policy that allows rows on its aggregate's table and on the tables of the aggregate's entities. It narrows what those policies allow. On Tenancy's tables, a restrictive rule can narrow:

  • the organization, with its units and the closure of its tree;
  • the seats, with their placements and grants;
  • the roles.

An application narrows these to keep a seat that is not an employee out of the organization. Tenancy reads tenants and invitations in its own operations. A person who accepts an invitation reads it before having a seat. So a restrictive rule on a tenant or an invitation is refused. Put the rule on one of the other tables, or remove Restrictive (Rules that narrow the organization).

DDD00101​

CurrentSeat answers with the application's seat class.

user.CurrentSeat<Seat>().Kind == SeatKind.Employee // the class declared with [SeatAggregate]
user.CurrentSeat<Role>().Name == "Employee" // DDD00101: no seat's row

CurrentSeat<TSeat>() is the calling seat's own row, and a rule reads a field of it. The function behind it answers with a row of Tenancy's seats table. So TSeat must be the class that this table is mapped to: the class declared with [SeatAggregate]. In a rule of a module whose context does not map the seats, use user.CurrentSeatId() for the seat's id.

DDD00102​

A marker of a requirement's argument goes beside the attribute whose call it is.

[InTenant]
public sealed record RenameTenant([ForTenant] TenantId Tenant, string Name) : ICommand, ITenancyRequest; // DDD00102: no [Operator]

[Operator]
public sealed record MergeTenants([ForTenant] TenantId Tenant, [ForTenant] TenantId Into) : ICommand, ITenancyRequest; // DDD00102: two members

A package marks an attribute as one more argument of a requirement's call with [RequirementArgument]. Tenancy's [ForTenant] is one. The generator hands the marked member to the call of the attribute it goes with: [Operator] with [ForTenant] TenantId Tenant is TenancyAccess.Operator(request.Tenant) (An operator's request). DDD00102 reports a marker that the call cannot take:

  • The attribute is not on the request. The member is handed to nothing, and the request reads as if it said something it does not.
  • Two members carry one marker. The call would get two arguments where it takes one.
  • The module's interface cannot read the member. The generator writes the call into the module's interface. So the marker goes on a positional parameter of a record, which is its property, or on a property the interface can read. A parameter of a class's primary constructor is no member, and neither is a private or a protected property.

Put the attribute on the request, or take the marker off. Mark one member:

[Operator]
public sealed record SuspendTenant([ForTenant] TenantId Tenant, string Reason) : ICommand<Tenant>, ITenancyRequest;

A member of another type than the marker names is DDD00081: [ForTenant] marks the application's tenant id.

DDD00103​

An operator's command takes its tenant from the request or from itself.

[Operator(Tenant = OperatorTenant.FromRequest)]
public sealed record SuspendTenant([ForTenant] TenantId Tenant, string Reason) : ICommand<Tenant>, ITenancyRequest; // DDD00103

[Operator] on a command runs its handler as operator work. The work acts in one place:

The command saysThe work acts in
a [ForTenant] memberthe tenant the command names
Tenant = OperatorTenant.​FromRequestthe tenant the request names beside the command, as tenant selection reads it
neitherno tenant, as provisioning needs

Both at once name two tenants for one piece of work. Keep [ForTenant] for a command that names its tenant, or FromRequest for a command that takes the tenant the request names:

[Operator]
public sealed record SuspendTenant([ForTenant] TenantId Tenant, string Reason) : ICommand<Tenant>, ITenancyRequest;

[Operator(Tenant = OperatorTenant.FromRequest)]
public sealed record CloseStaleInvitations : ICommand, ITenancyRequest;

The same requirement written as the property, TenancyAccess.Operator(Tenant, OperatorTenant.FromRequest), throws an ArgumentException that names DDD00103.

DDD00104​

[Operator] stands beside no requirement an operator cannot meet.

[Operator, InTenant] // DDD00104
public sealed record ArchiveUnits : ICommand, ITenancyRequest;

[Operator, RequiresPermission(TenancyPermissions.SeatsManage)] // DDD00104
public sealed record TenantSeats : IQuery<IReadOnlyList<SeatRow>>, ITenancyRequest;

[Operator] // DDD00104
public sealed record ArchiveUnit([AtUnit(TenancyPermissions.UnitsManage)] OrganizationUnitId Unit) : ICommand, ITenancyRequest;

A request requires every requirement attribute and marker on it. [Operator] lets through only a signed-in user with an operator's token role. An operator holds no seat in any tenant and no permission, so [InTenant], [RequiresPermission(permission)] and a permission marker refuse every operator. A seat is no operator, so [Operator] refuses every seat. Together they refuse every caller, and the request can never be sent.

Keep [Operator] for a request of the application's staff, or the other attribute for a request of a tenant's seats. A request that either may send implements RequiredAccess itself, with AccessRequirement.Any:

public sealed record TenantSeats : IQuery<IReadOnlyList<SeatRow>>, ITenancyRequest
{
AccessRequirement IRequireAccess.RequiredAccess => AccessRequirement.Any(
TenancyAccess.Operator(),
TenancyAccess.AtTenant(TenancyPermissions.SeatsManage));
}

DDD00105​

[Operator] on a query names no tenant and no scope.

[Operator(Tenant = OperatorTenant.FromRequest)] // DDD00105, a warning
public sealed record AllTenants(string? After) : IQuery<TenantManager.TenantPage>, ITenancyRequest;

On a command, [Operator] runs the handler as operator work. Tenant and Scope say where the work acts and in which scope. On a query nothing begins. An operator reads across tenants through the database role of its token. So Tenant and Scope on a query say nothing, and a reader takes them for the tenant the query reads in. Take them off:

[Operator]
public sealed record AllTenants(string? After) : IQuery<TenantManager.TenantPage>, ITenancyRequest;

[ForTenant] on a query is not reported. It still refuses a request that names another tenant than the query does, with tenancy.tenant-mismatch.

DDD00106​

A member or role manager is not written.

namespace Filing.Sharing;

public sealed class DocumentShareManager; // DDD00106, information: the generated manager would hide it

[Member<DocumentShareId, UserId, NamedRole, Document>]
public sealed partial class DocumentShare;

Membership's generator writes the managers in the global namespace. The member manager is named after the member class, DocumentShareManager for DocumentShare. The role manager is named after the role class, BoardRoleManager for BoardRole. The generator writes a manager only where the name is free and the manager can do its work. This information names the manager and says why it is not written:

WhyFix
The project declares a type of that name in a namespace. The manager in the global namespace would hide it in every file that imports the namespace.Rename your type, or move it to the global namespace if it is your own manager.
Two member classes, or two role classes, have the same name in different namespaces. Their managers would be one class.Rename one of them.
The resource's owner property has no setter the manager can use when it names another owner.Give the [Owner] property a private set.
The resource's id cannot be told.Declare the resource with [AggregateRoot<TId>] over an id the project can see.
The role class's resource is declared in another project. The resource's registration cannot name the manager.Declare the role class in the resource's project.

A type of the manager's name in the global namespace is your own manager, and the generator writes none and reports nothing. A resource whose member list is not written gets no manager either. DDD00059 or DDD00060 says why.

DDD00107​

The registration of an access behavior is written and in scope where it is called.

namespace Shop.Billing.Access;

[AccessRequests]
public partial interface IBillingRequest : IRequireAccess;
namespace Shop.Billing;

public static class BillingApplication
{
public static IServiceCollection AddBillingApplication(this IServiceCollection services)
=> services.AddBillingAccessBehavior(); // CS1061 and DDD00107
}

The compiler reports CS1061 at this call, and CS1061 does not name the cause. DDD00107 is reported beside it, at the same call. The message names one of two causes.

The namespace of the interface is not imported. The generator declares the registration in the namespace of the interface, here Shop.Billing.Access. Code in that namespace and in the namespaces inside it sees the registration. Code anywhere else needs a using directive, and this includes a class one namespace up. Add it to the file, or add a global using to the project:

using Shop.Billing.Access;

The project of the interface was built without the mediator. The mediator is declared for .NET 10, so a project for netstandard2.0 gets no behavior and no registration. The message names the project of the interface, which can differ from the project of the call. Build that project for .NET 10 with DylansTechStack.DDD. Or remove the call and ask the checks yourself, as Asking the checks yourself shows.

A project without a behavior that asks the checks itself writes no such call, so DDD00107 is not reported there. For that reason the diagnostic is on the call and not on the interface.

DDD00107 comes from an analyzer. It is an error because the call does not compile in either case. In a project that sees the mediator, an interface without a behavior is DDD00056 or DDD00057, and DDD00107 is not reported beside them.

DDD00108​

The migrations make the model the access files are written from.

warning DDD00108: The model of OrderingContext has changes no migration makes yet, so the build writes no access
file. A file written now would name columns and tables that no migration has made, and would come before the
migration that makes them. Add the migration with dotnet ef migrations add, and the next build writes the access
files after it.

The Supabase export writes each module's access file from the context's model. The file grants UPDATE on each column that may change, and its policies name the module's tables. The Entity Framework migrations make those tables and columns.

A migration comes after the change to the model. You add a property, and then you run dotnet ef migrations add. A package upgrade can add a property as well. Every build between the two runs the export. An access file written in such a build grants a column that no migration has made. The migration that makes the column is added later, so its file sorts after the access file. Supabase applies the access file first, and the grant fails.

So the export asks Entity Framework whether the model has changes that the migrations do not make. Entity Framework compares the model with the migrations' model snapshot, as dotnet ef migrations add does. While a context has such changes, the export writes no access file. It reports each file it would have written as Deferred, and the build gives this warning once. The files of the migrations themselves are still written. The access files of the other modules wait as well, because one module's file can call a function that another module's file makes.

Add the migration with dotnet ef migrations add. The next build writes the file of the migration, and then the access files, numbered after it.

The warning is given only where an access file waits. A change that leaves every access file as it was needs no new file. Check gives the same warning and does not fail. A context whose migrations have no model snapshot, such as migrations written by hand, is taken to have no such changes.

DDD00109​

A permission marker on a collection of ids takes no marker argument and no [OrOwn].

public sealed record ArchiveBoardsCommand(
[OnResource(BoardPermissions.Edit)] IReadOnlyCollection<BoardId> Boards,
[ExpectedVersion] long? Version = null) : ICommand, IBoardsRequest; // DDD00109: whose version?

public sealed record LeaveBoardsCommand(
[OnResource(BoardPermissions.ManageMembers)] BoardId[] Boards,
[OrOwn] SeatId Seat) : ICommand, IBoardsRequest; // DDD00109: own of which board?

[OnResource(p)] on a collection of ids becomes MemberAccess.OnEach(p, request.Boards) (A permission on each resource of a list). That call takes the permission and the collection, and nothing more.

  • [ExpectedVersion] names the version of one resource. Beside a marker on a collection there is no resource for it to belong to.
  • [OrOwn] makes the caller's own an alternative to the permission on one resource. A collection names several.

A request that also has [OnResource] on one id of the same resource is not reported: the marker argument belongs to that marker. Remove the marker argument, or send one request for each resource.

DDD00110​

A request, a command or a query has one handler.

internal sealed class CloseInvoiceHandler : ICommandHandler<CloseInvoice> { ... }

internal sealed class CloseInvoiceAndLog : ICommandHandler<CloseInvoice> { ... } // DDD00110: a second handler

The toolkit's mediator sends a request, a command or a query to exactly one handler, and so do the stream kinds. The generated Add{Module}Handlers() registers each handler, and a second registration for the same message throws when the host starts. So the generator reports the second handler of one project, ordered by file and position, and the registration keeps the first.

  • A notification may have any number of handlers. It is not reported.
  • A class derived from a handler is a handler too. Make the base class abstract, or the derived class file-local, so the registration leaves one of them out.
  • Something that runs around a handler is a pipeline behavior, IPipelineBehavior<TMessage, TResponse>, and not a second handler (Sending requests).

Handlers of one message in two projects are not reported, because neither project sees the other's. The registration that comes second throws at start-up and names both handlers.

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

This is no compiler diagnostic, because it depends on how the Entity Framework model is built. It is reported as early as possible: 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. Milestone has no property to keep it in. 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 property. If the child should not carry it, configure the foreign key of that ownership yourself with OwnsMany(...).WithOwner().HasForeignKey(...). The convention leaves explicit configuration alone.


Nothing was generated and there is no diagnostic​

Check these, 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>], and not a hand-written attribute with the same name.
  3. The build output, with EmitCompilerGeneratedFiles turned on, as Getting started describes.

If a generator throws, the compiler reports CS8785 or CS8784. That is a bug in the toolkit. Please report it, with the declaration that triggered it.