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 Dev-Side DSLs — What We Already Have

"The metadata for complete documentation already exists in your codebase. It is hiding inside attributes that nobody reads for docs."


DSL Anatomy — The Pattern Every DSL Follows

Every DSL in the system follows the same four-part structure:

DSL = Attributes (declare) + Generator (produce) + Analyzer (guard) + .Lib project (package)
Cmf.Ddd.Lib/
├── Attributes/
│   ├── AggregateRootAttribute.cs      ← declare intent
│   ├── EntityAttribute.cs
│   ├── ValueObjectAttribute.cs
│   ├── CompositionAttribute.cs
│   └── DomainEventAttribute.cs
├── Generators/
│   └── DddGenerator.cs               ← produce code (and soon, docs)
├── Analyzers/
│   └── DddAnalyzer.cs                ← guard boundaries
└── Cmf.Ddd.Lib.csproj                ← package as NuGet

A team adds <ProjectReference Include="Cmf.Ddd.Lib" /> to their project and immediately gets: typed attributes to declare domain concepts, source-generated implementations, and compile-time diagnostics when they violate DDD rules. The DSL is mixed into C# code — no separate files, no configuration.


DDD DSL — Architecture Metadata

The DDD DSL provides the structural vocabulary for domain modeling:

// ── Attribute definitions (Cmf.Ddd.Lib) ──────────────────────

[AttributeUsage(AttributeTargets.Class)]
public sealed class AggregateRootAttribute : Attribute
{
    public string BoundedContext { get; init; } = "";
    public string Description { get; init; } = "";
}

[AttributeUsage(AttributeTargets.Class)]
public sealed class ValueObjectAttribute : Attribute
{
    public string Description { get; init; } = "";
}

[AttributeUsage(AttributeTargets.Property)]
public sealed class CompositionAttribute : Attribute { }

[AttributeUsage(AttributeTargets.Method)]
public sealed class DomainEventAttribute : Attribute
{
    public Type EventType { get; }
    public DomainEventAttribute(Type eventType) => EventType = eventType;
}

// ── Usage (consumer project) ──────────────────────────────────

[AggregateRoot(BoundedContext = "Ordering",
    Description = "Represents a customer order with line items and payment")]
public partial class Order
{
    [EntityId]
    public partial OrderId Id { get; }

    [Composition]
    public partial IReadOnlyList<OrderLine> Lines { get; }

    [Composition]
    public partial PaymentInfo Payment { get; }

    [DomainEvent(typeof(OrderCreatedEvent))]
    public Result Create(CustomerId customer, IReadOnlyList<OrderLineDto> lines)
    {
        if (lines.Count == 0) return Result.Failure("Order must have at least one line");
        // ...
    }

    [DomainEvent(typeof(OrderPaidEvent))]
    public Result MarkAsPaid(PaymentReference reference) { ... }
}

[ValueObject(Description = "Monetary amount with currency")]
public partial record Money(decimal Amount, Currency Currency);

// ── What the SG generates (today) ─────────────────────────────

// Order.g.cs         → entity implementation (partial class completion)
// OrderRepository.g.cs → IOrderRepository + EF Core implementation
// OrderConfiguration.g.cs → EF Core IEntityTypeConfiguration<Order>

// ── What the SG COULD generate (with Document DSL) ────────────

// docs/architecture/Order.md → entity diagram, compositions, events
// docs/architecture/Order.mermaid → class diagram with relationships
// docs/architecture/Ordering-context.md → bounded context map

Documentation metadata in DDD DSL:

  • BoundedContext → where this aggregate lives in the architecture
  • Description → human-readable summary
  • [Composition] → relationship to child entities/value objects
  • [DomainEvent] → what events this aggregate publishes
  • [EntityId] → identity type

Every property is a field in the generated architecture documentation.

Requirements DSL — Traceability Metadata

// ── Attribute definitions (Cmf.Requirements.Lib) ──────────────

[AttributeUsage(AttributeTargets.Class)]
public sealed class FeatureAttribute : Attribute
{
    public string Description { get; init; } = "";
    public Priority Priority { get; init; } = Priority.Medium;
}

[AttributeUsage(AttributeTargets.Method)]
public sealed class AcceptanceCriteriaAttribute : Attribute
{
    public string Description { get; }
    public AcceptanceCriteriaAttribute(string description) => Description = description;
}

public enum Priority { Critical, High, Medium, Low }

// ── Usage (consumer project) ──────────────────────────────────

[Feature(Description = "Process customer orders with payment and fulfillment",
    Priority = Priority.Critical)]
public abstract class OrderProcessingFeature : FeatureBase
{
    [AcceptanceCriteria("Order can be created with at least one line item")]
    public abstract Result OrderCanBeCreated();

    [AcceptanceCriteria("Order total is calculated from line quantities × unit prices")]
    public abstract Result TotalIsCalculated();

    [AcceptanceCriteria("Order can be marked as paid with a valid payment reference")]
    public abstract Result OrderCanBePaid();

    [AcceptanceCriteria("Paid order triggers fulfillment workflow")]
    public abstract Result PaidOrderTriggersFulfillment();
}

// ── What the SG generates (today) ─────────────────────────────

// RequirementRegistry.g.cs → const strings for all requirements
// RequirementConstants.g.cs → FEATURE_ORDER_PROCESSING, etc.

// ── Documentation metadata available ──────────────────────────

// Feature name, description, priority
// 4 acceptance criteria with descriptions
// Each AC is an abstract method → typed, navigable, refactorable
// The feature CLASS is the spec. The abstract methods ARE the ACs.

Testing DSL — Coverage Metadata

// ── Attribute definitions (Cmf.Testing.Lib) ───────────────────

[AttributeUsage(AttributeTargets.Class)]
public sealed class ForRequirementAttribute : Attribute
{
    public Type RequirementType { get; }
    public ForRequirementAttribute(Type requirementType) => RequirementType = requirementType;
}

[AttributeUsage(AttributeTargets.Method)]
public sealed class VerifiesAttribute : Attribute
{
    public Type FeatureType { get; }
    public string AcceptanceCriteria { get; }
    public VerifiesAttribute(Type featureType, string ac)
    {
        FeatureType = featureType;
        AcceptanceCriteria = ac;
    }
}

// ── Usage (test project) ──────────────────────────────────────

[ForRequirement(typeof(OrderProcessingFeature))]
public sealed class OrderProcessingTests
{
    [Fact]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.OrderCanBeCreated))]
    public void Order_can_be_created_with_valid_lines()
    {
        var order = Order.Create(customerId, new[] { validLine });
        order.IsSuccess.Should().BeTrue();
        order.Value.Lines.Should().HaveCount(1);
    }

    [Fact]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.OrderCanBeCreated))]
    public void Order_creation_fails_with_empty_lines()
    {
        var order = Order.Create(customerId, Array.Empty<OrderLineDto>());
        order.IsFailure.Should().BeTrue();
    }

    [Fact]
    [Verifies(typeof(OrderProcessingFeature),
        nameof(OrderProcessingFeature.TotalIsCalculated))]
    public void Order_total_equals_sum_of_line_totals()
    {
        var order = Order.Create(customerId, new[] { line10, line20 });
        order.Value.Total.Should().Be(new Money(30m, Currency.EUR));
    }

    // AC3 (OrderCanBePaid) → 2 tests
    // AC4 (PaidOrderTriggersFulfillment) → 0 tests ← gap!
}

// ── Documentation metadata available ──────────────────────────

// typeof(OrderProcessingFeature) → which feature this test class covers
// nameof(...OrderCanBeCreated) → which specific AC each test verifies
// Count of [Verifies] per AC → test coverage per acceptance criteria
// Missing: AC4 has no [Verifies] → coverage gap detected

API DSL — Contract Metadata

// ── Attribute definitions (Cmf.Api.Lib) ───────────────────────

[AttributeUsage(AttributeTargets.Class)]
public sealed class TypedEndpointAttribute : Attribute
{
    public string Method { get; }
    public string Route { get; }
    public string Description { get; init; } = "";
    public string Version { get; init; } = "v1";
    public TypedEndpointAttribute(string method, string route)
    {
        Method = method;
        Route = route;
    }
}

[AttributeUsage(AttributeTargets.Class)]
public sealed class AuthorizedAttribute : Attribute
{
    public string[] Roles { get; init; } = [];
    public string Policy { get; init; } = "";
}

// ── Usage (API project) ───────────────────────────────────────

[TypedEndpoint("POST", "/api/orders",
    Description = "Create a new order from line items",
    Version = "v2")]
[Authorized(Roles = ["OrderManager", "Admin"])]
[ForRequirement(typeof(OrderProcessingFeature))]
public partial class CreateOrderEndpoint
{
    public record Request(CustomerId CustomerId, IReadOnlyList<OrderLineDto> Lines);
    public record Response(OrderId Id, Money Total);
}

[TypedEndpoint("POST", "/api/orders/{id}/pay",
    Description = "Mark an order as paid")]
[Authorized(Roles = ["PaymentProcessor"])]
[ForRequirement(typeof(OrderProcessingFeature))]
public partial class PayOrderEndpoint
{
    public record Request(OrderId Id, PaymentReference Reference);
    public record Response(OrderId Id, OrderStatus Status);
}

// ── What the SG generates (today) ─────────────────────────────

// CreateOrderEndpoint.g.cs → Minimal API route registration
// PayOrderEndpoint.g.cs → Minimal API route registration
// OpenApiSpec.g.json → OpenAPI 3.0 specification fragment

// ── Documentation metadata available ──────────────────────────

// HTTP method + route → API surface
// Request/Response records → typed contracts
// Roles/Policy → authorization matrix
// typeof(OrderProcessingFeature) → which feature this endpoint serves
// Version → API versioning documentation

The Documentation Gap

Here is the problem: every DSL generates code from attributes. None generates documentation.

DSL SG Generates Today Could Also Generate
DDD Entity .g.cs, Repository .g.cs, EF Config .g.cs Architecture diagram, entity relationship docs
Requirements Registry .g.cs, Constants .g.cs Traceability matrix, AC table, coverage report
Testing (no SG — attributes are read at test time) Test coverage per AC, gap analysis
API Endpoint .g.cs, OpenAPI .g.json API docs with requirement links, auth matrix

The metadata is there. It is rich, typed, validated by analyzers. It just needs a reader that produces documentation instead of code. That reader is the Document DSL (Part III).


The Artifact Graph — Find All Consumers Automatically

Before the Document DSL can generate documentation, it needs to answer one question: "What touches this artifact?"

Given Order, the graph must find:

  • Which feature requires it → OrderProcessingFeature
  • Which tests verify it → OrderProcessingTests (4 test methods)
  • Which endpoints expose it → CreateOrderEndpoint, PayOrderEndpoint
  • Which deployment steps migrate it → (not yet typed — see Parts IV–V)

This is the Artifact Graph — a Roslyn-built data structure that maps every DSL artifact to every other artifact that references it:

// ── The Graph Model ───────────────────────────────────────────

public sealed record ArtifactNode(
    string TypeName,
    string FilePath,
    int Line,
    ArtifactKind Kind);

public enum ArtifactKind
{
    Requirement,     // [Feature], [Story], [Epic]
    Specification,   // abstract AC methods
    Implementation,  // [AggregateRoot], [Entity], service classes
    Test,            // [Verifies], [ForRequirement] on test classes
    Endpoint,        // [TypedEndpoint]
    Deployment,      // [DeploymentOrchestrator], [MigrationStep]
    Observation      // [HealthCheck], [Metric], [AlertRule]
}

public sealed record ArtifactEdge(
    ArtifactNode Source,
    ArtifactNode Target,
    EdgeKind Relation);

public enum EdgeKind
{
    Implements,    // code → requirement
    Verifies,      // test → requirement AC
    Serves,        // endpoint → requirement
    Deploys,       // deployment step → implementation
    Observes,      // health check → app
    Configures,    // config transform → app
    RollsBack      // rollback → migration step
}

public sealed class ArtifactGraph
{
    private readonly ImmutableArray<ArtifactNode> _nodes;
    private readonly ImmutableArray<ArtifactEdge> _edges;

    /// Find everything related to a feature
    public TraceabilityChain GetChain(Type featureType)
    {
        // Walks: Feature → Specs (ACs) → Implementations → Tests → Endpoints → Deployments
        // Returns the complete chain for documentation
    }

    /// Find artifacts with DSL attributes but no incoming edges from a Requirement
    public IReadOnlyList<ArtifactNode> FindOrphans()
    {
        // [AggregateRoot] Order exists but no [ForRequirement] links to any feature
        // → orphaned implementation, possibly undocumented
    }

    /// Impact analysis: what is affected if this requirement changes?
    public IReadOnlyList<ArtifactNode> GetImpact(Type requirementType)
    {
        // Returns all implementations, tests, endpoints, deployments
        // that would need updating
    }
}

Building the Graph with Roslyn

The graph is built by a DiagnosticAnalyzer that walks all symbol references in the compilation:

// ── Graph Builder (runs during compilation) ───────────────────

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ArtifactGraphAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor DOC001 = new(
        "DOC001",
        "Orphaned artifact",
        "'{0}' has [{1}] but is not linked to any requirement via [ForRequirement]",
        "Documentation",
        DiagnosticSeverity.Warning,
        isEnabledByDefault: true);

    public override void Initialize(AnalysisContext context)
    {
        context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType);
    }

    private void AnalyzeNamedType(SymbolAnalysisContext context)
    {
        var symbol = (INamedTypeSymbol)context.Symbol;

        // Has a DSL attribute (AggregateRoot, TypedEndpoint, etc.)?
        var dslAttributes = symbol.GetAttributes()
            .Where(a => IsDslAttribute(a.AttributeClass));

        if (!dslAttributes.Any()) return;

        // Has a [ForRequirement] link?
        var hasRequirementLink = symbol.GetAttributes()
            .Any(a => a.AttributeClass?.Name == "ForRequirementAttribute");

        if (!hasRequirementLink)
        {
            // DOC001: Orphaned artifact
            context.ReportDiagnostic(Diagnostic.Create(
                DOC001, symbol.Locations[0],
                symbol.Name,
                dslAttributes.First().AttributeClass!.Name));
        }
    }
}

Usage — the analyzer fires at compile time:

warning DOC001: 'PaymentGatewayClient' has [Injectable] but is not linked to any
                requirement via [ForRequirement]
                — consider adding [ForRequirement(typeof(...))] for traceability

Querying the Graph

// "Show me everything related to OrderProcessingFeature"
var chain = graph.GetChain(typeof(OrderProcessingFeature));

// chain.Requirements  → OrderProcessingFeature (1 feature)
// chain.Criteria      → 4 acceptance criteria (abstract methods)
// chain.Implementations → Order aggregate, OrderLine value object
// chain.Tests         → 4 test methods across OrderProcessingTests
// chain.Endpoints     → CreateOrderEndpoint, PayOrderEndpoint
// chain.Deployments   → (empty — not yet typed, see Parts IV–V)

// Coverage analysis
chain.CoverageByAC:
//   OrderCanBeCreated        → 2 tests ✓
//   TotalIsCalculated        → 1 test  ✓
//   OrderCanBePaid           → 2 tests ✓
//   PaidOrderTriggersFulfillment → 0 tests ✗ ← gap!

// Impact analysis: "What breaks if we change OrderProcessingFeature?"
var impact = graph.GetImpact(typeof(OrderProcessingFeature));
// → Order, OrderLine, OrderProcessingTests, CreateOrderEndpoint, PayOrderEndpoint
// → 5 types across 3 projects need review

What's Next

The dev-side DSLs provide the typed metadata. The Artifact Graph provides the cross-referencing engine. What's missing is the documentation generator — the DSL that reads all of this and produces human-readable output.

That is the Document DSL (Part III): Document<Feature<T>> → traceability matrix. Document<AggregateRoot<T>> → architecture diagram. Document<Document<T>> → its own reference docs.