Part IV: DDD DSL -- Domain Modeling
Overview
The DDD DSL is the backbone of the CMF. It provides attributes for declaring domain models following Domain-Driven Design patterns. From a handful of attributed partial classes, the source generator produces:
- Entity implementations with backing fields and property accessors
- Fluent builders with required property enforcement
- Aggregate invariants via
[Invariant]methods returningResult EnsureInvariants()method injected into every command handler- EF Core
DbContext, entity configurations, and repository implementations - CQRS command handlers, domain events, query projections, and sagas
- REST API controllers and GraphQL schemaV
The developer writes the domain model. The compiler generates the infrastructure.
Core Attributes
AggregateRoot
[MetaConcept("AggregateRoot")]
[MetaInherits("Entity")]
[MetaConstraint("MustHaveId",
"Properties.Any(p => p.IsAnnotatedWith('EntityId'))",
Message = "Aggregate root must have an [EntityId] property")]
[MetaConstraint("MustHaveInvariant",
"Methods.Any(m => m.IsAnnotatedWith('Invariant'))",
Message = "Aggregate root should have at least one [Invariant] method",
Severity = DiagnosticSeverity.Warning)]
public sealed class AggregateRootAttribute : Attribute
{
[MetaProperty("Name", "string", Required = true)]
public string Name { get; set; }
[MetaProperty("BoundedContext", "string")]
public string? BoundedContext { get; set; }
}[MetaConcept("AggregateRoot")]
[MetaInherits("Entity")]
[MetaConstraint("MustHaveId",
"Properties.Any(p => p.IsAnnotatedWith('EntityId'))",
Message = "Aggregate root must have an [EntityId] property")]
[MetaConstraint("MustHaveInvariant",
"Methods.Any(m => m.IsAnnotatedWith('Invariant'))",
Message = "Aggregate root should have at least one [Invariant] method",
Severity = DiagnosticSeverity.Warning)]
public sealed class AggregateRootAttribute : Attribute
{
[MetaProperty("Name", "string", Required = true)]
public string Name { get; set; }
[MetaProperty("BoundedContext", "string")]
public string? BoundedContext { get; set; }
}Entity
[MetaConcept("Entity")]
[MetaConstraint("MustBeComposed",
"IsReachableViaCompositionFrom('AggregateRoot')",
Message = "Entity must be reachable via [Composition] from an aggregate root")]
public sealed class EntityAttribute : Attribute
{
[MetaProperty("Name", "string", Required = true)]
public string Name { get; set; }
}[MetaConcept("Entity")]
[MetaConstraint("MustBeComposed",
"IsReachableViaCompositionFrom('AggregateRoot')",
Message = "Entity must be reachable via [Composition] from an aggregate root")]
public sealed class EntityAttribute : Attribute
{
[MetaProperty("Name", "string", Required = true)]
public string Name { get; set; }
}ValueObject
[MetaConcept("ValueObject")]
[MetaConstraint("MustBeOwned",
"IsReferencedBy('Composition')",
Message = "Value object must be owned via [Composition]")]
public sealed class ValueObjectAttribute : Attribute
{
[MetaProperty("Name", "string", Required = true)]
public string Name { get; set; }
}[MetaConcept("ValueObject")]
[MetaConstraint("MustBeOwned",
"IsReferencedBy('Composition')",
Message = "Value object must be owned via [Composition]")]
public sealed class ValueObjectAttribute : Attribute
{
[MetaProperty("Name", "string", Required = true)]
public string Name { get; set; }
}EntityId
[MetaConcept("EntityId")]
public sealed class EntityIdAttribute : Attribute { }[MetaConcept("EntityId")]
public sealed class EntityIdAttribute : Attribute { }Property
[MetaConcept("Property")]
public sealed class PropertyAttribute : Attribute
{
[MetaProperty("Name", "string", Required = true)]
public string Name { get; set; }
[MetaProperty("Required", "bool")]
public bool Required { get; set; } = false;
[MetaProperty("MaxLength", "int")]
public int? MaxLength { get; set; }
}[MetaConcept("Property")]
public sealed class PropertyAttribute : Attribute
{
[MetaProperty("Name", "string", Required = true)]
public string Name { get; set; }
[MetaProperty("Required", "bool")]
public bool Required { get; set; } = false;
[MetaProperty("MaxLength", "int")]
public int? MaxLength { get; set; }
}Relationship Semantics
The DDD DSL defines three relationship types, each with distinct semantics that drive EF Core generation:
Composition (Ownership)
[MetaConcept("Composition")]
[MetaReference("Target", "Entity", IsContainment = true)]
public sealed class CompositionAttribute : Attribute { }[MetaConcept("Composition")]
[MetaReference("Target", "Entity", IsContainment = true)]
public sealed class CompositionAttribute : Attribute { }Semantics: The parent owns the child. The child's lifecycle is controlled by the parent. Deleting the parent deletes the child. The child cannot exist without the parent.
Aggregate rule: Composition defines the aggregate boundary. All entities reachable via Composition from the AggregateRoot belong to that aggregate.
Association (Cross-Aggregate Reference)
[MetaConcept("Association")]
[MetaReference("Target", "Entity", IsContainment = false)]
public sealed class AssociationAttribute : Attribute { }[MetaConcept("Association")]
[MetaReference("Target", "Entity", IsContainment = false)]
public sealed class AssociationAttribute : Attribute { }Semantics: The source references the target but does not own it. Deleting the source does not delete the target. Cross-aggregate references must use Association, not Composition.
Consistency rule: Cross-aggregate associations are eventually consistent -- changes are propagated via domain events, not within a single transaction.
Aggregation (Weak Ownership)
[MetaConcept("Aggregation")]
[MetaReference("Target", "Entity", IsContainment = false)]
public sealed class AggregationAttribute : Attribute { }[MetaConcept("Aggregation")]
[MetaReference("Target", "Entity", IsContainment = false)]
public sealed class AggregationAttribute : Attribute { }Semantics: The parent has a weak reference to the child. The child can exist independently. Deleting the parent sets the child's foreign key to null.
EF Core Mapping
| Attribute | On Type | EF Core Generated | Delete Behavior |
|---|---|---|---|
[Composition] on T (ValueObject) |
Owned type | OwnsOne<T>() |
Embedded (cascade) |
[Composition] on IReadOnlyList<T> (VO) |
Owned collection | OwnsMany<T>() |
Embedded (cascade) |
[Composition] on T (Entity) |
Required 1:1 | HasOne<T>().IsRequired().OnDelete(Cascade) |
Cascade |
[Composition] on IReadOnlyList<T> (Entity) |
1:N | HasMany<T>().OnDelete(Cascade) |
Cascade |
[Association] on T |
Optional 1:1 | HasOne<T>().OnDelete(SetNull) |
SetNull |
[Association] on IReadOnlyList<T> |
M:N | Join table | SetNull |
[Aggregation] on T |
Optional 1:1 | HasOne<T>().OnDelete(SetNull) |
SetNull |
Complete Example: The Order Aggregate
What the Developer Writes
namespace MyStore.Lib.Ordering;
[AggregateRoot("Order", BoundedContext = "Ordering")]
public partial class Order
{
[EntityId]
public partial OrderId Id { get; }
[Property("OrderDate", Required = true)]
public partial DateTime OrderDate { get; }
[Property("Status", Required = true)]
public partial OrderStatus Status { get; }
[Composition]
public partial IReadOnlyList<OrderLine> Lines { get; }
[Composition]
public partial PaymentDetails Payment { get; }
[Composition]
public partial ShippingAddress ShippingAddress { get; }
[Association]
public partial CustomerId CustomerId { get; }
// ── Invariants ──────────────────────────────────
[Invariant("Order must have at least one line item")]
private Result HasLines()
=> Lines.Count > 0
? Result.Success()
: Result.Failure("Order must have at least one line item");
[Invariant("Order total must match sum of line totals")]
private Result TotalMatchesLines()
{
var expected = Lines.Sum(l => l.UnitPrice.Amount * l.Quantity);
var actual = Lines.Sum(l => l.LineTotal.Amount);
return actual == expected
? Result.Success()
: Result.Failure($"Line totals ({actual}) do not match computed sum ({expected})");
}
[Invariant("Order total must be positive")]
private Result TotalIsPositive()
{
var total = Lines.Sum(l => l.LineTotal.Amount);
return total > 0
? Result.Success()
: Result.Failure($"Order total ({total}) is not positive");
}
[Invariant("Shipped orders must have a shipping address")]
private Result ShippedOrdersHaveAddress()
{
if (Status != OrderStatus.Shipped)
return Result.Success(); // Not applicable for non-shipped orders
return ShippingAddress is not null
? Result.Success()
: Result.Failure("Shipped order must have a shipping address");
}
}
[Entity("OrderLine")]
public partial class OrderLine
{
[EntityId]
public partial OrderLineId Id { get; }
[Property("ProductName", Required = true, MaxLength = 200)]
public partial string ProductName { get; }
[Property("Quantity", Required = true)]
public partial int Quantity { get; }
[Composition]
public partial Money UnitPrice { get; }
[Composition]
public partial Money LineTotal { get; }
}
[Entity("PaymentDetails")]
public partial class PaymentDetails
{
[EntityId]
public partial PaymentId Id { get; }
[Property("Method", Required = true)]
public partial PaymentMethod Method { get; }
[Composition]
public partial Money Amount { get; }
[Property("TransactionId")]
public partial string? TransactionId { get; }
}
[ValueObject("Money")]
public partial class Money
{
[ValueComponent("Amount", "decimal", Required = true)]
public partial decimal Amount { get; }
[ValueComponent("Currency", "string", Required = true)]
public partial string Currency { get; }
}
[ValueObject("ShippingAddress")]
public partial class ShippingAddress
{
[ValueComponent("Street", "string", Required = true)]
public partial string Street { get; }
[ValueComponent("City", "string", Required = true)]
public partial string City { get; }
[ValueComponent("PostalCode", "string", Required = true)]
public partial string PostalCode { get; }
[ValueComponent("Country", "string", Required = true)]
public partial string Country { get; }
}
public enum OrderStatus { Draft, Placed, Paid, Shipped, Delivered, Cancelled }
public enum PaymentMethod { CreditCard, BankTransfer, PayPal }namespace MyStore.Lib.Ordering;
[AggregateRoot("Order", BoundedContext = "Ordering")]
public partial class Order
{
[EntityId]
public partial OrderId Id { get; }
[Property("OrderDate", Required = true)]
public partial DateTime OrderDate { get; }
[Property("Status", Required = true)]
public partial OrderStatus Status { get; }
[Composition]
public partial IReadOnlyList<OrderLine> Lines { get; }
[Composition]
public partial PaymentDetails Payment { get; }
[Composition]
public partial ShippingAddress ShippingAddress { get; }
[Association]
public partial CustomerId CustomerId { get; }
// ── Invariants ──────────────────────────────────
[Invariant("Order must have at least one line item")]
private Result HasLines()
=> Lines.Count > 0
? Result.Success()
: Result.Failure("Order must have at least one line item");
[Invariant("Order total must match sum of line totals")]
private Result TotalMatchesLines()
{
var expected = Lines.Sum(l => l.UnitPrice.Amount * l.Quantity);
var actual = Lines.Sum(l => l.LineTotal.Amount);
return actual == expected
? Result.Success()
: Result.Failure($"Line totals ({actual}) do not match computed sum ({expected})");
}
[Invariant("Order total must be positive")]
private Result TotalIsPositive()
{
var total = Lines.Sum(l => l.LineTotal.Amount);
return total > 0
? Result.Success()
: Result.Failure($"Order total ({total}) is not positive");
}
[Invariant("Shipped orders must have a shipping address")]
private Result ShippedOrdersHaveAddress()
{
if (Status != OrderStatus.Shipped)
return Result.Success(); // Not applicable for non-shipped orders
return ShippingAddress is not null
? Result.Success()
: Result.Failure("Shipped order must have a shipping address");
}
}
[Entity("OrderLine")]
public partial class OrderLine
{
[EntityId]
public partial OrderLineId Id { get; }
[Property("ProductName", Required = true, MaxLength = 200)]
public partial string ProductName { get; }
[Property("Quantity", Required = true)]
public partial int Quantity { get; }
[Composition]
public partial Money UnitPrice { get; }
[Composition]
public partial Money LineTotal { get; }
}
[Entity("PaymentDetails")]
public partial class PaymentDetails
{
[EntityId]
public partial PaymentId Id { get; }
[Property("Method", Required = true)]
public partial PaymentMethod Method { get; }
[Composition]
public partial Money Amount { get; }
[Property("TransactionId")]
public partial string? TransactionId { get; }
}
[ValueObject("Money")]
public partial class Money
{
[ValueComponent("Amount", "decimal", Required = true)]
public partial decimal Amount { get; }
[ValueComponent("Currency", "string", Required = true)]
public partial string Currency { get; }
}
[ValueObject("ShippingAddress")]
public partial class ShippingAddress
{
[ValueComponent("Street", "string", Required = true)]
public partial string Street { get; }
[ValueComponent("City", "string", Required = true)]
public partial string City { get; }
[ValueComponent("PostalCode", "string", Required = true)]
public partial string PostalCode { get; }
[ValueComponent("Country", "string", Required = true)]
public partial string Country { get; }
}
public enum OrderStatus { Draft, Placed, Paid, Shipped, Delivered, Cancelled }
public enum PaymentMethod { CreditCard, BankTransfer, PayPal }What the developer writes: ~120 lines of attributed partial classes.
What the Compiler Generates
1. Entity Implementations (Stage 2)
// Generated: Order.g.cs
public partial class Order
{
private OrderId _id;
private DateTime _orderDate;
private OrderStatus _status;
private readonly List<OrderLine> _lines = new();
private PaymentDetails? _payment;
private ShippingAddress? _shippingAddress;
private CustomerId? _customerId;
public partial OrderId Id => _id;
public partial DateTime OrderDate => _orderDate;
public partial OrderStatus Status => _status;
public partial IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();
public partial PaymentDetails Payment => _payment!;
public partial ShippingAddress ShippingAddress => _shippingAddress!;
public partial CustomerId CustomerId => _customerId!;
// Domain event collection
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
public void ClearDomainEvents() => _domainEvents.Clear();
protected void RaiseDomainEvent(IDomainEvent @event) => _domainEvents.Add(@event);
}// Generated: Order.g.cs
public partial class Order
{
private OrderId _id;
private DateTime _orderDate;
private OrderStatus _status;
private readonly List<OrderLine> _lines = new();
private PaymentDetails? _payment;
private ShippingAddress? _shippingAddress;
private CustomerId? _customerId;
public partial OrderId Id => _id;
public partial DateTime OrderDate => _orderDate;
public partial OrderStatus Status => _status;
public partial IReadOnlyList<OrderLine> Lines => _lines.AsReadOnly();
public partial PaymentDetails Payment => _payment!;
public partial ShippingAddress ShippingAddress => _shippingAddress!;
public partial CustomerId CustomerId => _customerId!;
// Domain event collection
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
public void ClearDomainEvents() => _domainEvents.Clear();
protected void RaiseDomainEvent(IDomainEvent @event) => _domainEvents.Add(@event);
}2. Invariant Enforcement (Stage 2)
// Generated: Order.Invariants.g.cs
public partial class Order
{
/// <summary>
/// Calls all [Invariant] methods and aggregates their Results.
/// Injected into every command handler after state mutation.
/// </summary>
public Result EnsureInvariants()
{
return Result.Aggregate(
HasLines(),
TotalMatchesLines(),
TotalIsPositive(),
ShippedOrdersHaveAddress()
);
}
}// Generated: Order.Invariants.g.cs
public partial class Order
{
/// <summary>
/// Calls all [Invariant] methods and aggregates their Results.
/// Injected into every command handler after state mutation.
/// </summary>
public Result EnsureInvariants()
{
return Result.Aggregate(
HasLines(),
TotalMatchesLines(),
TotalIsPositive(),
ShippedOrdersHaveAddress()
);
}
}3. Fluent Builder (Stage 2)
// Generated: OrderBuilder.g.cs
public class OrderBuilder
{
private DateTime _orderDate;
private OrderStatus _status = OrderStatus.Draft;
private readonly List<OrderLine> _lines = new();
private PaymentDetails? _payment;
private ShippingAddress? _shippingAddress;
private CustomerId? _customerId;
public OrderBuilder WithOrderDate(DateTime date) { _orderDate = date; return this; }
public OrderBuilder WithStatus(OrderStatus status) { _status = status; return this; }
public OrderBuilder AddLine(OrderLine line) { _lines.Add(line); return this; }
public OrderBuilder WithPayment(PaymentDetails payment) { _payment = payment; return this; }
public OrderBuilder WithShippingAddress(ShippingAddress addr) { _shippingAddress = addr; return this; }
public OrderBuilder WithCustomer(CustomerId id) { _customerId = id; return this; }
public Result<Order> Build()
{
var order = new Order
{
_id = OrderId.New(),
_orderDate = _orderDate,
_status = _status,
_payment = _payment,
_shippingAddress = _shippingAddress,
_customerId = _customerId
};
order._lines.AddRange(_lines);
var invariantResult = order.EnsureInvariants();
return invariantResult.IsSuccess
? Result<Order>.Success(order)
: Result<Order>.Failure(invariantResult.Reason!);
}
}// Generated: OrderBuilder.g.cs
public class OrderBuilder
{
private DateTime _orderDate;
private OrderStatus _status = OrderStatus.Draft;
private readonly List<OrderLine> _lines = new();
private PaymentDetails? _payment;
private ShippingAddress? _shippingAddress;
private CustomerId? _customerId;
public OrderBuilder WithOrderDate(DateTime date) { _orderDate = date; return this; }
public OrderBuilder WithStatus(OrderStatus status) { _status = status; return this; }
public OrderBuilder AddLine(OrderLine line) { _lines.Add(line); return this; }
public OrderBuilder WithPayment(PaymentDetails payment) { _payment = payment; return this; }
public OrderBuilder WithShippingAddress(ShippingAddress addr) { _shippingAddress = addr; return this; }
public OrderBuilder WithCustomer(CustomerId id) { _customerId = id; return this; }
public Result<Order> Build()
{
var order = new Order
{
_id = OrderId.New(),
_orderDate = _orderDate,
_status = _status,
_payment = _payment,
_shippingAddress = _shippingAddress,
_customerId = _customerId
};
order._lines.AddRange(_lines);
var invariantResult = order.EnsureInvariants();
return invariantResult.IsSuccess
? Result<Order>.Success(order)
: Result<Order>.Failure(invariantResult.Reason!);
}
}4. EF Core Configuration (Stage 2-3)
// Generated: OrderConfiguration.g.cs
public class OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("Orders");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.HasConversion(id => id.Value, v => new OrderId(v));
builder.Property(x => x.OrderDate).IsRequired();
builder.Property(x => x.Status).IsRequired()
.HasConversion<string>();
// Composition: 1:N entity (cascade delete)
builder.HasMany(x => x.Lines)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
// Composition: 1:1 entity (cascade delete)
builder.HasOne(x => x.Payment)
.WithOne()
.HasForeignKey<PaymentDetails>("OrderId")
.IsRequired(false)
.OnDelete(DeleteBehavior.Cascade);
// Composition: owned value object (embedded columns)
builder.OwnsOne(x => x.ShippingAddress, sa =>
{
sa.Property(a => a.Street).HasMaxLength(200).IsRequired();
sa.Property(a => a.City).HasMaxLength(100).IsRequired();
sa.Property(a => a.PostalCode).HasMaxLength(20).IsRequired();
sa.Property(a => a.Country).HasMaxLength(100).IsRequired();
});
// Association: cross-aggregate (set null, no cascade)
builder.Property(x => x.CustomerId)
.HasConversion(id => id.Value, v => new CustomerId(v))
.IsRequired(false);
}
}// Generated: OrderConfiguration.g.cs
public class OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("Orders");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.HasConversion(id => id.Value, v => new OrderId(v));
builder.Property(x => x.OrderDate).IsRequired();
builder.Property(x => x.Status).IsRequired()
.HasConversion<string>();
// Composition: 1:N entity (cascade delete)
builder.HasMany(x => x.Lines)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
// Composition: 1:1 entity (cascade delete)
builder.HasOne(x => x.Payment)
.WithOne()
.HasForeignKey<PaymentDetails>("OrderId")
.IsRequired(false)
.OnDelete(DeleteBehavior.Cascade);
// Composition: owned value object (embedded columns)
builder.OwnsOne(x => x.ShippingAddress, sa =>
{
sa.Property(a => a.Street).HasMaxLength(200).IsRequired();
sa.Property(a => a.City).HasMaxLength(100).IsRequired();
sa.Property(a => a.PostalCode).HasMaxLength(20).IsRequired();
sa.Property(a => a.Country).HasMaxLength(100).IsRequired();
});
// Association: cross-aggregate (set null, no cascade)
builder.Property(x => x.CustomerId)
.HasConversion(id => id.Value, v => new CustomerId(v))
.IsRequired(false);
}
}5. Repository (Stage 2-3)
// Generated: IOrderRepository.g.cs
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct = default);
Task<IReadOnlyList<Order>> GetAllAsync(CancellationToken ct = default);
Task AddAsync(Order order, CancellationToken ct = default);
Task SaveChangesAsync(CancellationToken ct = default);
}
// Generated: OrderRepository.g.cs
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _db;
public OrderRepository(AppDbContext db) => _db = db;
public async Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct = default)
=> await _db.Orders
.Include(o => o.Lines)
.Include(o => o.Payment)
.FirstOrDefaultAsync(o => o.Id == id, ct);
public async Task<IReadOnlyList<Order>> GetAllAsync(CancellationToken ct = default)
=> await _db.Orders.ToListAsync(ct);
public async Task AddAsync(Order order, CancellationToken ct = default)
=> await _db.Orders.AddAsync(order, ct);
public async Task SaveChangesAsync(CancellationToken ct = default)
=> await _db.SaveChangesAsync(ct);
}// Generated: IOrderRepository.g.cs
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct = default);
Task<IReadOnlyList<Order>> GetAllAsync(CancellationToken ct = default);
Task AddAsync(Order order, CancellationToken ct = default);
Task SaveChangesAsync(CancellationToken ct = default);
}
// Generated: OrderRepository.g.cs
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _db;
public OrderRepository(AppDbContext db) => _db = db;
public async Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct = default)
=> await _db.Orders
.Include(o => o.Lines)
.Include(o => o.Payment)
.FirstOrDefaultAsync(o => o.Id == id, ct);
public async Task<IReadOnlyList<Order>> GetAllAsync(CancellationToken ct = default)
=> await _db.Orders.ToListAsync(ct);
public async Task AddAsync(Order order, CancellationToken ct = default)
=> await _db.Orders.AddAsync(order, ct);
public async Task SaveChangesAsync(CancellationToken ct = default)
=> await _db.SaveChangesAsync(ct);
}CQRS: Commands, Events, Queries, Sagas
Command Definition
[Command("PlaceOrder", AggregateRoot = "Order")]
public partial record PlaceOrderCommand
{
[Property("CustomerId", Required = true)]
public partial CustomerId CustomerId { get; }
[Property("Lines", Required = true)]
public partial IReadOnlyList<OrderLineDto> Lines { get; }
[Property("ShippingAddress", Required = true)]
public partial ShippingAddress ShippingAddress { get; }
}[Command("PlaceOrder", AggregateRoot = "Order")]
public partial record PlaceOrderCommand
{
[Property("CustomerId", Required = true)]
public partial CustomerId CustomerId { get; }
[Property("Lines", Required = true)]
public partial IReadOnlyList<OrderLineDto> Lines { get; }
[Property("ShippingAddress", Required = true)]
public partial ShippingAddress ShippingAddress { get; }
}Generated Command Handler
// Generated: PlaceOrderCommandHandler.g.cs
public class PlaceOrderCommandHandler : ICommandHandler<PlaceOrderCommand, Result<OrderId>>
{
private readonly IOrderRepository _repository;
public PlaceOrderCommandHandler(IOrderRepository repository)
=> _repository = repository;
public async Task<Result<OrderId>> HandleAsync(
PlaceOrderCommand command, CancellationToken ct = default)
{
// Build the aggregate
var builder = new OrderBuilder()
.WithOrderDate(DateTime.UtcNow)
.WithCustomer(command.CustomerId)
.WithShippingAddress(command.ShippingAddress);
foreach (var line in command.Lines)
builder.AddLine(line.ToOrderLine());
// Builder calls EnsureInvariants()
var buildResult = builder.Build();
if (!buildResult.IsSuccess)
return Result<OrderId>.Failure(buildResult.Reason!);
var order = buildResult.Value;
// Persist
await _repository.AddAsync(order, ct);
await _repository.SaveChangesAsync(ct);
// Raise domain event
order.RaiseDomainEvent(new OrderPlacedEvent(order.Id, command.CustomerId));
return Result<OrderId>.Success(order.Id);
}
}// Generated: PlaceOrderCommandHandler.g.cs
public class PlaceOrderCommandHandler : ICommandHandler<PlaceOrderCommand, Result<OrderId>>
{
private readonly IOrderRepository _repository;
public PlaceOrderCommandHandler(IOrderRepository repository)
=> _repository = repository;
public async Task<Result<OrderId>> HandleAsync(
PlaceOrderCommand command, CancellationToken ct = default)
{
// Build the aggregate
var builder = new OrderBuilder()
.WithOrderDate(DateTime.UtcNow)
.WithCustomer(command.CustomerId)
.WithShippingAddress(command.ShippingAddress);
foreach (var line in command.Lines)
builder.AddLine(line.ToOrderLine());
// Builder calls EnsureInvariants()
var buildResult = builder.Build();
if (!buildResult.IsSuccess)
return Result<OrderId>.Failure(buildResult.Reason!);
var order = buildResult.Value;
// Persist
await _repository.AddAsync(order, ct);
await _repository.SaveChangesAsync(ct);
// Raise domain event
order.RaiseDomainEvent(new OrderPlacedEvent(order.Id, command.CustomerId));
return Result<OrderId>.Success(order.Id);
}
}Key: The builder calls EnsureInvariants() before returning the Order. If any invariant fails, the command returns a failure Result. No invalid aggregate ever reaches the database.
Domain Event Definition
[DomainEvent("OrderPlaced", SourceAggregate = "Order")]
public partial record OrderPlacedEvent
{
[Property("OrderId", Required = true)]
public partial OrderId OrderId { get; }
[Property("CustomerId", Required = true)]
public partial CustomerId CustomerId { get; }
}[DomainEvent("OrderPlaced", SourceAggregate = "Order")]
public partial record OrderPlacedEvent
{
[Property("OrderId", Required = true)]
public partial OrderId OrderId { get; }
[Property("CustomerId", Required = true)]
public partial CustomerId CustomerId { get; }
}CQRS Flow
Saga Definition
[Saga("OrderFulfillment")]
public partial class OrderFulfillmentSaga
{
[SagaStep(1, Command = "ReserveInventory", CompensateWith = "ReleaseInventory")]
public partial void Step1_ReserveInventory();
[SagaStep(2, Command = "ProcessPayment", CompensateWith = "RefundPayment")]
public partial void Step2_ProcessPayment();
[SagaStep(3, Command = "ShipOrder", CompensateWith = "CancelShipment")]
public partial void Step3_ShipOrder();
}[Saga("OrderFulfillment")]
public partial class OrderFulfillmentSaga
{
[SagaStep(1, Command = "ReserveInventory", CompensateWith = "ReleaseInventory")]
public partial void Step1_ReserveInventory();
[SagaStep(2, Command = "ProcessPayment", CompensateWith = "RefundPayment")]
public partial void Step2_ProcessPayment();
[SagaStep(3, Command = "ShipOrder", CompensateWith = "CancelShipment")]
public partial void Step3_ShipOrder();
}The generator produces a saga orchestrator that executes steps in order, compensating in reverse if any step fails.
BoundedContext and Anti-Corruption Layers
Declaring Bounded Contexts
[BoundedContext("Catalog", Description = "Product catalog and pricing")]
[BoundedContext("Ordering", Description = "Order processing and fulfillment")]
[BoundedContext("Identity", Description = "User authentication and authorization")][BoundedContext("Catalog", Description = "Product catalog and pricing")]
[BoundedContext("Ordering", Description = "Order processing and fulfillment")]
[BoundedContext("Identity", Description = "User authentication and authorization")]Context Mapping
[MappingContext("CatalogToOrdering", "Catalog", "Ordering",
MappingType = ContextMappingType.AntiCorruptionLayer)]
public partial class CatalogToOrderingMapping
{
[MapEntity("Product", "OrderItem",
PropertyMappings = "Name -> ProductName, Price.Amount -> UnitPrice")]
public partial OrderItem TranslateProduct(Product source);
}[MappingContext("CatalogToOrdering", "Catalog", "Ordering",
MappingType = ContextMappingType.AntiCorruptionLayer)]
public partial class CatalogToOrderingMapping
{
[MapEntity("Product", "OrderItem",
PropertyMappings = "Name -> ProductName, Price.Amount -> UnitPrice")]
public partial OrderItem TranslateProduct(Product source);
}Generated: CatalogToOrderingAdapter service that translates between context languages. Injected via DI. Used by cross-context command handlers.
Compile-Time Validation (Stage 1)
The Stage 1 validator evaluates MetaConstraints and emits diagnostics:
| Diagnostic | Severity | Rule |
|---|---|---|
| DDD001 | Error | Aggregate root missing [EntityId] property |
| DDD002 | Error | Entity not reachable via [Composition] from any aggregate root |
| DDD003 | Error | [Composition] across aggregate boundaries (should use [Association]) |
| DDD004 | Error | Value object not owned by any entity via [Composition] |
| DDD005 | Error | Command references non-existent aggregate root |
| DDD006 | Error | Domain event references non-existent source aggregate |
| DDD007 | Warning | Saga step references non-existent command |
| DDD100 | Warning | Aggregate root has zero [Invariant] methods |
| DDD101 | Warning | [Invariant] method does not return Result |
| DDD102 | Error | [Invariant] method is not private |
// Build output:
error DDD001: Order is an [AggregateRoot] but has no [EntityId] property
→ Add [EntityId] to a property of type OrderId
error DDD002: PaymentDetails is an [Entity] but is not reachable via
[Composition] from any [AggregateRoot]
→ Add [Composition] on Order pointing to PaymentDetails
warning DDD100: Order is an [AggregateRoot] with zero [Invariant] methods
→ Add [Invariant] methods that return Result to enforce domain rules// Build output:
error DDD001: Order is an [AggregateRoot] but has no [EntityId] property
→ Add [EntityId] to a property of type OrderId
error DDD002: PaymentDetails is an [Entity] but is not reachable via
[Composition] from any [AggregateRoot]
→ Add [Composition] on Order pointing to PaymentDetails
warning DDD100: Order is an [AggregateRoot] with zero [Invariant] methods
→ Add [Invariant] methods that return Result to enforce domain rulesSummary
The DDD DSL transforms ~120 lines of attributed partial classes into:
| Generated Artifact | Lines (approx.) | Stage |
|---|---|---|
| Entity implementations (backing fields, properties) | 200 | 2 |
| Fluent builders with invariant checking | 150 | 2 |
EnsureInvariants() aggregate method |
20 | 2 |
EF Core configurations (IEntityTypeConfiguration<T>) |
150 | 2-3 |
| Repository interfaces + implementations | 100 | 2-3 |
| CQRS command handlers | 100 per command | 2-3 |
| Domain event records | 30 per event | 2 |
| REST API controllers | 200 per aggregate | 3 |
| GraphQL schema types | 100 per aggregate | 3 |
| Total per aggregate | ~1,000-2,000 |
The developer writes the domain model. The compiler writes the plumbing. The invariants ensure correctness. The analyzers catch violations before runtime.