The DDD DSL (M2) -- Domain Modeling
This DSL maps Diem's schema.yml to C# attributes. Every attribute is annotated with M3 primitives from the meta-metamodel, making the DSL self-describing and validatable at compile time.
AggregateRoot and Entity
The [AggregateRoot] attribute is the transactional consistency boundary. It inherits from [Entity] at the metamodel level and adds constraints: every aggregate root must have a strongly-typed ID and belong to a bounded context.
// ============================================================
// M2: AggregateRoot -- the DDD consistency boundary
// ============================================================
[MetaConcept("AggregateRoot")]
[MetaInherits("Entity")]
[MetaConstraint("MustHaveId",
"Properties.Any(p => p.IsAnnotatedWith('EntityId'))",
Message = "Every aggregate root must have a strongly-typed ID")]
[MetaConstraint("MustBelongToContext",
"BoundedContext != null",
Message = "Every aggregate root must belong to a bounded context")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class AggregateRootAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaReference("BoundedContext", "BoundedContext", Multiplicity = Multiplicity.One)]
public string BoundedContext { get; set; }
[MetaProperty("IsEventSourced")]
public bool IsEventSourced { get; set; }
public AggregateRootAttribute(string name) => Name = name;
}// ============================================================
// M2: AggregateRoot -- the DDD consistency boundary
// ============================================================
[MetaConcept("AggregateRoot")]
[MetaInherits("Entity")]
[MetaConstraint("MustHaveId",
"Properties.Any(p => p.IsAnnotatedWith('EntityId'))",
Message = "Every aggregate root must have a strongly-typed ID")]
[MetaConstraint("MustBelongToContext",
"BoundedContext != null",
Message = "Every aggregate root must belong to a bounded context")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class AggregateRootAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaReference("BoundedContext", "BoundedContext", Multiplicity = Multiplicity.One)]
public string BoundedContext { get; set; }
[MetaProperty("IsEventSourced")]
public bool IsEventSourced { get; set; }
public AggregateRootAttribute(string name) => Name = name;
}The [Entity] attribute marks a class as an entity within an aggregate. Entities must be reachable via a [Composition] chain from their aggregate root -- the generator validates this at compile time.
[MetaConcept("Entity")]
[MetaConstraint("MustBeComposed",
"IsReachableViaCompositionFrom('AggregateRoot')",
Message = "Every entity must be reachable via [Composition] from an aggregate root")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class EntityAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
public EntityAttribute(string name) => Name = name;
}[MetaConcept("Entity")]
[MetaConstraint("MustBeComposed",
"IsReachableViaCompositionFrom('AggregateRoot')",
Message = "Every entity must be reachable via [Composition] from an aggregate root")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class EntityAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
public EntityAttribute(string name) => Name = name;
}The [EntityId] attribute declares a strongly-typed identifier. Each aggregate root must have exactly one.
[MetaConcept("EntityId")]
[AttributeUsage(AttributeTargets.Property)]
public sealed class EntityIdAttribute : Attribute
{
[MetaProperty("IdType", DefaultValue = "Guid")]
public IdType IdType { get; set; } = IdType.Guid;
[MetaProperty("GenerationStrategy", DefaultValue = "Sequential")]
public IdGeneration GenerationStrategy { get; set; } = IdGeneration.Sequential;
}
public enum IdType { Guid, Int, Long, String }
public enum IdGeneration { Sequential, Random, Manual }[MetaConcept("EntityId")]
[AttributeUsage(AttributeTargets.Property)]
public sealed class EntityIdAttribute : Attribute
{
[MetaProperty("IdType", DefaultValue = "Guid")]
public IdType IdType { get; set; } = IdType.Guid;
[MetaProperty("GenerationStrategy", DefaultValue = "Sequential")]
public IdGeneration GenerationStrategy { get; set; } = IdGeneration.Sequential;
}
public enum IdType { Guid, Int, Long, String }
public enum IdGeneration { Sequential, Random, Manual }The [Property] attribute defines a typed slot on an entity or aggregate:
[MetaConcept("Property")]
[AttributeUsage(AttributeTargets.Property)]
public sealed class PropertyAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaProperty("Required")]
public bool Required { get; set; }
[MetaProperty("MaxLength")]
public int? MaxLength { get; set; }
[MetaProperty("DefaultValue")]
public string? DefaultValue { get; set; }
public PropertyAttribute(string name) => Name = name;
}[MetaConcept("Property")]
[AttributeUsage(AttributeTargets.Property)]
public sealed class PropertyAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaProperty("Required")]
public bool Required { get; set; }
[MetaProperty("MaxLength")]
public int? MaxLength { get; set; }
[MetaProperty("DefaultValue")]
public string? DefaultValue { get; set; }
public PropertyAttribute(string name) => Name = name;
}ValueObject
Value objects are immutable, identified by their structural content, and have no lifecycle of their own. They must be owned via [Composition].
[MetaConcept("ValueObject")]
[MetaConstraint("MustBeOwned",
"IsOwnedViaComposition()",
Message = "Every value object must be owned via [Composition]")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class ValueObjectAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
public ValueObjectAttribute(string name) => Name = name;
}
[MetaConcept("ValueComponent")]
[AttributeUsage(AttributeTargets.Property)]
public sealed class ValueComponentAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaProperty("Type", Required = true)]
public string TypeName { get; }
[MetaProperty("Required")]
public bool Required { get; set; }
[MetaProperty("MaxLength")]
public int? MaxLength { get; set; }
public ValueComponentAttribute(string name, string typeName)
{
Name = name;
TypeName = typeName;
}
}[MetaConcept("ValueObject")]
[MetaConstraint("MustBeOwned",
"IsOwnedViaComposition()",
Message = "Every value object must be owned via [Composition]")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class ValueObjectAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
public ValueObjectAttribute(string name) => Name = name;
}
[MetaConcept("ValueComponent")]
[AttributeUsage(AttributeTargets.Property)]
public sealed class ValueComponentAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaProperty("Type", Required = true)]
public string TypeName { get; }
[MetaProperty("Required")]
public bool Required { get; set; }
[MetaProperty("MaxLength")]
public int? MaxLength { get; set; }
public ValueComponentAttribute(string name, string typeName)
{
Name = name;
TypeName = typeName;
}
}Usage:
[ValueObject("Money")]
public partial class Money
{
[ValueComponent("Amount", "decimal", Required = true)]
public partial decimal Amount { get; }
[ValueComponent("Currency", "string", Required = true, MaxLength = 3)]
public partial string Currency { get; }
}
[ValueObject("Address")]
public partial class Address
{
[ValueComponent("Street", "string", Required = true, MaxLength = 200)]
public partial string Street { get; }
[ValueComponent("City", "string", Required = true, MaxLength = 100)]
public partial string City { get; }
[ValueComponent("ZipCode", "string", Required = true, MaxLength = 20)]
public partial string ZipCode { get; }
[ValueComponent("Country", "string", Required = true, MaxLength = 2)]
public partial string Country { get; }
}[ValueObject("Money")]
public partial class Money
{
[ValueComponent("Amount", "decimal", Required = true)]
public partial decimal Amount { get; }
[ValueComponent("Currency", "string", Required = true, MaxLength = 3)]
public partial string Currency { get; }
}
[ValueObject("Address")]
public partial class Address
{
[ValueComponent("Street", "string", Required = true, MaxLength = 200)]
public partial string Street { get; }
[ValueComponent("City", "string", Required = true, MaxLength = 100)]
public partial string City { get; }
[ValueComponent("ZipCode", "string", Required = true, MaxLength = 20)]
public partial string ZipCode { get; }
[ValueComponent("Country", "string", Required = true, MaxLength = 2)]
public partial string Country { get; }
}The generator produces sealed records with value equality, implicit conversions, and EF Core owned-type configurations.
BoundedContext and MappingContext
Bounded contexts are the strategic DDD boundary. Mapping contexts define how concepts translate between contexts.
[MetaConcept("BoundedContext")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class BoundedContextAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaProperty("Description")]
public string? Description { get; set; }
public BoundedContextAttribute(string name) => Name = name;
}
[MetaConcept("MappingContext")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class MappingContextAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaReference("SourceContext", "BoundedContext", Multiplicity = Multiplicity.One)]
public string SourceContext { get; }
[MetaReference("TargetContext", "BoundedContext", Multiplicity = Multiplicity.One)]
public string TargetContext { get; }
[MetaProperty("MappingType")]
public ContextMappingType MappingType { get; set; } = ContextMappingType.AntiCorruptionLayer;
public MappingContextAttribute(string name, string sourceContext, string targetContext)
{
Name = name;
SourceContext = sourceContext;
TargetContext = targetContext;
}
}
public enum ContextMappingType
{
SharedKernel,
CustomerSupplier,
Conformist,
AntiCorruptionLayer,
PublishedLanguage
}[MetaConcept("BoundedContext")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class BoundedContextAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaProperty("Description")]
public string? Description { get; set; }
public BoundedContextAttribute(string name) => Name = name;
}
[MetaConcept("MappingContext")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class MappingContextAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaReference("SourceContext", "BoundedContext", Multiplicity = Multiplicity.One)]
public string SourceContext { get; }
[MetaReference("TargetContext", "BoundedContext", Multiplicity = Multiplicity.One)]
public string TargetContext { get; }
[MetaProperty("MappingType")]
public ContextMappingType MappingType { get; set; } = ContextMappingType.AntiCorruptionLayer;
public MappingContextAttribute(string name, string sourceContext, string targetContext)
{
Name = name;
SourceContext = sourceContext;
TargetContext = targetContext;
}
}
public enum ContextMappingType
{
SharedKernel,
CustomerSupplier,
Conformist,
AntiCorruptionLayer,
PublishedLanguage
}Usage:
[BoundedContext("Catalog", Description = "Product catalog management")]
public partial class CatalogContext { }
[BoundedContext("Ordering", Description = "Order processing and fulfillment")]
public partial class OrderingContext { }
[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);
}[BoundedContext("Catalog", Description = "Product catalog management")]
public partial class CatalogContext { }
[BoundedContext("Ordering", Description = "Order processing and fulfillment")]
public partial class OrderingContext { }
[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);
}The generator produces anti-corruption layer translation services and integration events for cross-context communication.
DomainEvent, Command, and Query
Commands target aggregates. Events originate from aggregates. Queries read from aggregates. The generator validates these references at compile time.
[MetaConcept("Command")]
[MetaConstraint("MustTargetAggregate",
"TargetAggregate != null",
Message = "Every command must target an aggregate root")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class CommandAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaReference("TargetAggregate", "AggregateRoot", Multiplicity = Multiplicity.One)]
public string TargetAggregate { get; set; }
[MetaProperty("ReturnsResult")]
public bool ReturnsResult { get; set; } = true;
[MetaProperty("IdempotencyKey")]
public string? IdempotencyKey { get; set; }
public CommandAttribute(string name) => Name = name;
}
[MetaConcept("DomainEvent")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class DomainEventAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaReference("SourceAggregate", "AggregateRoot", Multiplicity = Multiplicity.One)]
public string SourceAggregate { get; set; }
[MetaProperty("Version")]
public int Version { get; set; } = 1;
public DomainEventAttribute(string name) => Name = name;
}
[MetaConcept("Query")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class QueryAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaReference("SourceAggregate", "AggregateRoot", Multiplicity = Multiplicity.One)]
public string SourceAggregate { get; set; }
[MetaProperty("IsCacheable")]
public bool IsCacheable { get; set; }
[MetaProperty("CacheDurationSeconds")]
public int CacheDurationSeconds { get; set; } = 300;
public QueryAttribute(string name) => Name = name;
}[MetaConcept("Command")]
[MetaConstraint("MustTargetAggregate",
"TargetAggregate != null",
Message = "Every command must target an aggregate root")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class CommandAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaReference("TargetAggregate", "AggregateRoot", Multiplicity = Multiplicity.One)]
public string TargetAggregate { get; set; }
[MetaProperty("ReturnsResult")]
public bool ReturnsResult { get; set; } = true;
[MetaProperty("IdempotencyKey")]
public string? IdempotencyKey { get; set; }
public CommandAttribute(string name) => Name = name;
}
[MetaConcept("DomainEvent")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class DomainEventAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaReference("SourceAggregate", "AggregateRoot", Multiplicity = Multiplicity.One)]
public string SourceAggregate { get; set; }
[MetaProperty("Version")]
public int Version { get; set; } = 1;
public DomainEventAttribute(string name) => Name = name;
}
[MetaConcept("Query")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class QueryAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaReference("SourceAggregate", "AggregateRoot", Multiplicity = Multiplicity.One)]
public string SourceAggregate { get; set; }
[MetaProperty("IsCacheable")]
public bool IsCacheable { get; set; }
[MetaProperty("CacheDurationSeconds")]
public int CacheDurationSeconds { get; set; } = 300;
public QueryAttribute(string name) => Name = name;
}Usage:
[Command("CreateOrder", TargetAggregate = "Order", ReturnsResult = true)]
public partial class CreateOrderCommand
{
[Property("CustomerId", Required = true)]
public partial CustomerId CustomerId { get; }
[Property("ShippingAddress", Required = true)]
public partial Address ShippingAddress { get; }
}
[DomainEvent("OrderPlaced", SourceAggregate = "Order")]
public partial class OrderPlacedEvent
{
[Property("OrderId", Required = true)]
public partial OrderId OrderId { get; }
[Property("CustomerId", Required = true)]
public partial CustomerId CustomerId { get; }
[Property("Total", Required = true)]
public partial Money Total { get; }
[Property("Timestamp", Required = true)]
public partial DateTimeOffset Timestamp { get; }
}
[Query("GetOrderById", SourceAggregate = "Order", IsCacheable = true)]
public partial class GetOrderByIdQuery
{
[Property("OrderId", Required = true)]
public partial OrderId OrderId { get; }
}[Command("CreateOrder", TargetAggregate = "Order", ReturnsResult = true)]
public partial class CreateOrderCommand
{
[Property("CustomerId", Required = true)]
public partial CustomerId CustomerId { get; }
[Property("ShippingAddress", Required = true)]
public partial Address ShippingAddress { get; }
}
[DomainEvent("OrderPlaced", SourceAggregate = "Order")]
public partial class OrderPlacedEvent
{
[Property("OrderId", Required = true)]
public partial OrderId OrderId { get; }
[Property("CustomerId", Required = true)]
public partial CustomerId CustomerId { get; }
[Property("Total", Required = true)]
public partial Money Total { get; }
[Property("Timestamp", Required = true)]
public partial DateTimeOffset Timestamp { get; }
}
[Query("GetOrderById", SourceAggregate = "Order", IsCacheable = true)]
public partial class GetOrderByIdQuery
{
[Property("OrderId", Required = true)]
public partial OrderId OrderId { get; }
}Saga
Sagas coordinate processes that span multiple aggregates or bounded contexts. They are state machines with compensation logic.
[MetaConcept("Saga")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class SagaAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaReference("StartsWithEvent", "DomainEvent", Multiplicity = Multiplicity.One)]
public string StartsWithEvent { get; set; }
[MetaProperty("CompensationStrategy")]
public CompensationStrategy CompensationStrategy { get; set; } = CompensationStrategy.Backward;
public SagaAttribute(string name) => Name = name;
}
[MetaConcept("SagaStep")]
[AttributeUsage(AttributeTargets.Method)]
public sealed class SagaStepAttribute : Attribute
{
[MetaProperty("Order", Required = true)]
public int Order { get; }
[MetaReference("Command", "Command", Multiplicity = Multiplicity.One)]
public string Command { get; set; }
[MetaReference("CompensatingCommand", "Command")]
public string? CompensatingCommand { get; set; }
[MetaProperty("TimeoutSeconds")]
public int TimeoutSeconds { get; set; } = 30;
public SagaStepAttribute(int order) => Order = order;
}
public enum CompensationStrategy { Backward, Forward, Custom }[MetaConcept("Saga")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class SagaAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaReference("StartsWithEvent", "DomainEvent", Multiplicity = Multiplicity.One)]
public string StartsWithEvent { get; set; }
[MetaProperty("CompensationStrategy")]
public CompensationStrategy CompensationStrategy { get; set; } = CompensationStrategy.Backward;
public SagaAttribute(string name) => Name = name;
}
[MetaConcept("SagaStep")]
[AttributeUsage(AttributeTargets.Method)]
public sealed class SagaStepAttribute : Attribute
{
[MetaProperty("Order", Required = true)]
public int Order { get; }
[MetaReference("Command", "Command", Multiplicity = Multiplicity.One)]
public string Command { get; set; }
[MetaReference("CompensatingCommand", "Command")]
public string? CompensatingCommand { get; set; }
[MetaProperty("TimeoutSeconds")]
public int TimeoutSeconds { get; set; } = 30;
public SagaStepAttribute(int order) => Order = order;
}
public enum CompensationStrategy { Backward, Forward, Custom }Usage:
[Saga("OrderFulfillment", StartsWithEvent = "OrderPlaced",
CompensationStrategy = CompensationStrategy.Backward)]
public partial class OrderFulfillmentSaga
{
[SagaStep(1, Command = "ReserveInventory",
CompensatingCommand = "ReleaseInventory", TimeoutSeconds = 30)]
public partial SagaStepResult ReserveStock();
[SagaStep(2, Command = "ProcessPayment",
CompensatingCommand = "RefundPayment", TimeoutSeconds = 60)]
public partial SagaStepResult ChargeCustomer();
[SagaStep(3, Command = "CreateShipment")]
public partial SagaStepResult Ship();
}[Saga("OrderFulfillment", StartsWithEvent = "OrderPlaced",
CompensationStrategy = CompensationStrategy.Backward)]
public partial class OrderFulfillmentSaga
{
[SagaStep(1, Command = "ReserveInventory",
CompensatingCommand = "ReleaseInventory", TimeoutSeconds = 30)]
public partial SagaStepResult ReserveStock();
[SagaStep(2, Command = "ProcessPayment",
CompensatingCommand = "RefundPayment", TimeoutSeconds = 60)]
public partial SagaStepResult ChargeCustomer();
[SagaStep(3, Command = "CreateShipment")]
public partial SagaStepResult Ship();
}The generator produces a state machine implementation with typed states, compensation paths, and timeout handling: