Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

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;
}

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;
}

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 }

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;
}

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;
    }
}

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; }
}

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
}

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);
}

The generator produces anti-corruption layer translation services and integration events for cross-context communication.

Diagram
Three bounded contexts sharing data through explicit context mappings — an Anti-Corruption Layer protects Ordering from Catalog's shape, while Shipping consumes a stable Published Language.

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;
}

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; }
}

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 }

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();
}

The generator produces a state machine implementation with typed states, compensation paths, and timeout handling:

Diagram
The OrderFulfillment saga compiled to a state machine — successful steps march forward to shipment, while failures fall back through compensating commands in strict reverse order.
⬇ Download