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

CMF Integration -- Ops DSLs Meet Domain DSLs

The Content Management Framework has six dev-side DSLs: DDD, Content, Admin, Pages, Workflow, and Requirements. The Ops ecosystem adds twenty-two DSLs for operational concerns. This part shows how the two sides connect.

The connection is not conceptual. It is typed. C# references link domain concepts to operational specifications. The source generators resolve those references at compile time. The analyzers validate them.


The Six Dev-Side DSLs

A brief summary of what already exists:

  1. DDD DSL. Aggregate roots, entities, value objects, domain events, repositories. [AggregateRoot("Order")] declares the Order aggregate with its invariants and event publications.

  2. Content DSL. Content parts, content types, content fields. [ContentPart("BlogPost")] declares a content structure with typed fields and validation rules.

  3. Admin DSL. Admin modules, CRUD pages, list views, edit forms. [AdminModule("Orders")] generates an admin interface for the Order aggregate.

  4. Pages DSL. Page templates, zones, widgets. Declares the page structure that the Content DSL fills with data.

  5. Workflow DSL. States, transitions, approval gates. [RequiresApproval("manager")] on a workflow transition enforces a human gate before state change.

  6. Requirements DSL. Features, stories, acceptance criteria, compliance verification. [Feature("OrderCancellation")] with acceptance criteria that are traced to tests and implementations.

Each DSL produces typed artifacts through the five-stage source generation pipeline. The Ops DSLs plug into the same pipeline.


The fundamental connection between dev-side and ops-side DSLs:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
    AllowMultiple = true)]
public sealed class OpsRequirementLinkAttribute : Attribute
{
    public OpsRequirementLinkAttribute(
        Type requirementFeature,
        int acceptanceCriterionIndex) { }

    /// <summary>
    /// The operational concern that satisfies this requirement.
    /// e.g., "chaos-experiment", "performance-budget", "compliance-control".
    /// </summary>
    public string OpsConcern { get; init; } = "";

    /// <summary>
    /// How this ops artifact verifies the requirement:
    /// Validates, Implements, Documents, Tests.
    /// </summary>
    public OpsLinkType LinkType { get; init; }
        = OpsLinkType.Validates;
}

public enum OpsLinkType
{
    /// <summary>This ops artifact validates the requirement (e.g., chaos test).</summary>
    Validates,

    /// <summary>This ops artifact implements the requirement (e.g., circuit breaker).</summary>
    Implements,

    /// <summary>This ops artifact documents the requirement (e.g., compliance evidence).</summary>
    Documents,

    /// <summary>This ops artifact tests the requirement (e.g., load test).</summary>
    Tests
}

This attribute links a specific acceptance criterion in the Requirements DSL to a specific operational artifact in the Ops DSLs. The link is a typed reference -- typeof() to the feature, integer index to the acceptance criterion. The source generator resolves it and builds a traceability matrix.


Chain 1: DDD to Deployment

The domain model defines the service boundary. The Ops DSLs define how that boundary is deployed, scaled, and operated.

// Domain-side: DDD DSL
[AggregateRoot("Order")]
public class Order
{
    public OrderId Id { get; }
    public CustomerId CustomerId { get; }
    public Money TotalAmount { get; }
    public OrderStatus Status { get; }

    [DomainEvent("OrderCreated")]
    public void Create(CustomerId customerId, IEnumerable<OrderLine> lines) { }

    [DomainEvent("OrderCancelled")]
    public void Cancel(string reason) { }
}

// Ops-side: the same logical service
[DeploymentApp("order-service",
    Image = "order-service",
    Port = 8080,
    Replicas = 3)]

[ContainerSpec("order-service",
    CpuRequest = "250m", CpuLimit = "1000m",
    MemoryRequest = "256Mi", MemoryLimit = "1Gi")]

[AutoscaleRule("order-service",
    MinReplicas = 3, MaxReplicas = 20,
    CpuThreshold = 70,
    CustomMetric = "order_created_total_rate",
    CustomMetricThreshold = 500)]

[ResourceBudget("order-service",
    MonthlyCpuHours = 2160,
    MonthlyBudgetUsd = 3500)]

public partial class OrderServiceOps { }

The chain: [AggregateRoot("Order")] defines the domain boundary. [DeploymentApp("order-service")] maps that boundary to a deployable unit. [ContainerSpec] defines the resource envelope. [AutoscaleRule] defines how the envelope scales. [ResourceBudget] constrains the cost.

The cross-DSL analyzer validates the chain:

  • DDD-OPS001: Every [AggregateRoot] that publishes domain events should have a corresponding [DeploymentApp]. An aggregate root without a deployment target is a domain model without a home.
  • DDD-OPS002: The [AutoscaleRule] custom metric references order_created_total_rate, which must match a [MetricDefinition] derived from the [DomainEvent("OrderCreated")]. The domain event IS the scaling signal.
  • DDD-OPS003: The [ContainerSpec] resource limits times [AutoscaleRule] max replicas must stay within [ResourceBudget]. Domain growth has a cost ceiling.

The domain model IS the deployment model. Not by convention -- by typed reference.


Chain 2: Requirements to Ops

The Requirements DSL declares features with acceptance criteria. The Ops DSLs declare operational behaviors. The [OpsRequirementLink] bridges them.

// Requirements-side: feature with acceptance criteria
[Feature("OrderCancellation",
    Description = "Customers can cancel orders within 24 hours")]
public class OrderCancellationFeature
{
    [AcceptanceCriterion(0,
        "Cancellation within 24h triggers full refund")]
    [AcceptanceCriterion(1,
        "Payment timeout does not block cancellation")]
    [AcceptanceCriterion(2,
        "Cancellation under load completes within SLO")]
    public static readonly FeatureSpec Spec;
}

// Ops-side: linking operational artifacts to acceptance criteria
[ChaosExperiment("payment-timeout-cancellation",
    Tier = Tier.InProcess,
    FaultKind = FaultKind.Timeout,
    TargetService = "payment-gateway",
    Hypothesis = "Cancellation completes despite payment timeout")]
[OpsRequirementLink(typeof(OrderCancellationFeature), 1,
    OpsConcern = "chaos-experiment",
    LinkType = OpsLinkType.Validates)]

[PerformanceBudget("/api/v3/orders/{id}/cancel", "POST",
    P50 = "100ms", P95 = "300ms", P99 = "500ms")]
[OpsRequirementLink(typeof(OrderCancellationFeature), 2,
    OpsConcern = "performance-budget",
    LinkType = OpsLinkType.Implements)]

[CircuitBreaker("payment-gateway",
    FailureThreshold = 5,
    SamplingDuration = "30s",
    BreakDuration = "60s")]
[OpsRequirementLink(typeof(OrderCancellationFeature), 1,
    OpsConcern = "resilience",
    LinkType = OpsLinkType.Implements)]

public partial class OrderServiceOps { }

The generated traceability matrix:

# Requirement Traceability: OrderCancellationFeature

| AC# | Criterion | Ops Artifact | Link Type | Status |
|-----|-----------|-------------|-----------|--------|
| 0 | Cancellation triggers full refund | (domain logic) | - | Covered by unit tests |
| 1 | Payment timeout does not block cancellation | ChaosExperiment: payment-timeout-cancellation | Validates | PASS |
| 1 | Payment timeout does not block cancellation | CircuitBreaker: payment-gateway | Implements | Active |
| 2 | Cancellation under load within SLO | PerformanceBudget: /api/v3/orders/{id}/cancel | Implements | P99 < 500ms |

Acceptance criterion 0 is pure domain logic -- tested by unit tests in the Requirements DSL. Acceptance criteria 1 and 2 have operational implications -- the chaos experiment verifies that the payment timeout does not block cancellation, the performance budget enforces the SLO. The traceability is not a spreadsheet. It is generated from typed references.

The analyzer validates completeness: every [AcceptanceCriterion] that is tagged as requiring operational verification must have at least one [OpsRequirementLink]. An acceptance criterion about "under load" without a load test or performance budget is a gap.


Chain 3: Workflow to Resilience

The Workflow DSL declares approval gates. The Resilience DSL declares rollback strategies. The connection: a deployment rollback that exceeds a threshold requires the same approval gate as a workflow transition.

// Workflow-side: deployment approval
[WorkflowState("production-deployment")]
[RequiresApproval("engineering-manager",
    Condition = "error_rate > 2%",
    Timeout = "30m")]
public class ProductionDeploymentWorkflow { }

// Ops-side: rollback plan with manual approval threshold
[RollbackPlan("order-service-rollback",
    Strategy = RollbackStrategy.BlueGreen,
    AutoRollbackOnErrorRate = 5.0,
    ManualApprovalThreshold = 2.0,
    HealthCheckTimeout = "60s")]

public partial class OrderServiceOps { }

The ManualApprovalThreshold = 2.0 on the rollback plan matches the Condition = "error_rate > 2%" on the workflow approval gate. When the error rate exceeds 2% but is below 5%, the system does not auto-rollback -- it triggers the workflow approval gate. The engineering manager gets a notification: "Error rate is 3.2%. Auto-rollback threshold is 5%. Approve manual rollback?"

The cross-DSL analyzer validates: if a [RollbackPlan] has a ManualApprovalThreshold, there must be a corresponding [RequiresApproval] workflow gate. A manual threshold without an approval workflow is a threshold that nobody acts on.


Chain 4: Admin to Observability

The Admin DSL generates CRUD pages for domain entities. The Observability DSL generates dashboards. The connection: every admin module has operational metrics worth monitoring.

// Admin-side: admin module for orders
[AdminModule("Orders",
    Entity = typeof(Order),
    ListFields = new[] { "Id", "CustomerId", "TotalAmount", "Status" },
    SearchFields = new[] { "CustomerId", "Status" })]
public class OrderAdminModule { }

// Ops-side: dashboard that includes admin-derived metrics
[Dashboard("order-service",
    Panels = new[]
    {
        "order_created_total:rate:1m",
        "order_admin_operations:rate:operation_type",
        "order_admin_response_time:p50,p95,p99"
    })]

[MetricDefinition("order_admin_operations",
    Type = MetricType.Counter,
    Description = "Admin CRUD operations on orders",
    Labels = new[] { "operation_type" })]

public partial class OrderServiceOps { }

The source generator sees [AdminModule("Orders")] and knows that the admin interface will produce create, read, update, and delete operations. It generates metric instrumentation for those operations: how many admin CRUD calls per minute, what is the response time, which operation types are most frequent. The dashboard includes these admin-specific panels alongside the public API metrics.

The connection is not manual. The Admin DSL and the Observability DSL share the same compilation unit. The source generator reads both and generates the wiring.


Chain 5: Content to Data Governance

The Content DSL declares content parts with typed fields. Some of those fields contain personal data. The Data Governance DSL declares GDPR data maps. The connection: every content field that contains PII must be covered by a data map.

// Content-side: content part with user data
[ContentPart("CustomerProfile")]
public class CustomerProfilePart
{
    [ContentField("FullName", Required = true)]
    public string FullName { get; set; }

    [ContentField("Email", Required = true)]
    public string Email { get; set; }

    [ContentField("ShippingAddress")]
    public Address ShippingAddress { get; set; }

    [ContentField("PhoneNumber")]
    public string PhoneNumber { get; set; }
}

// Ops-side: GDPR data map
[GdprDataMap("customer_profiles",
    PersonalDataFields = new[] { "FullName", "Email",
        "ShippingAddress", "PhoneNumber" },
    LegalBasis = LegalBasis.ContractPerformance,
    DataSubjectType = "customer",
    DeletionStrategy = DeletionStrategy.Anonymize)]

[RetentionPolicy("customer_profiles",
    RetentionPeriod = "3y",
    ArchiveAfter = "1y",
    DeleteAfter = "3y")]

public partial class CustomerServiceOps { }

The cross-DSL analyzer validates:

  • CNT-OPS001: Every [ContentField] that is a string type on a content part named *Profile*, *User*, or *Customer* should be reviewed for PII. If a GDPR data map exists, the field must be listed. If it is not listed, the analyzer warns: "ContentField 'Email' on CustomerProfilePart is not included in the GDPR data map for customer_profiles. Is this intentional?"

  • CNT-OPS002: The [RetentionPolicy] deletion timeline must comply with the legal basis. ContractPerformance data can be retained for the duration of the contract plus a reasonable period. Consent data must be deletable on request regardless of the retention policy.

The content author declares the fields. The ops engineer declares the governance. The compiler validates that nothing falls through the cracks.


The Unified M3 Metamodel Registry

Both dev-side and ops-side DSLs register their concepts in the same M3 metamodel registry. The registry is the single catalog of all typed concepts in the system.

// M3 registration -- happens at source generation time
MetamodelRegistry.Register(new ConceptType
{
    Name = "AggregateRoot",
    Layer = ConceptLayer.Domain,
    Source = "DDD DSL"
});

MetamodelRegistry.Register(new ConceptType
{
    Name = "DeploymentApp",
    Layer = ConceptLayer.Operations,
    Source = "Deployment DSL"
});

MetamodelRegistry.Register(new ConceptType
{
    Name = "ChaosExperiment",
    Layer = ConceptLayer.Operations,
    Source = "Chaos DSL"
});

MetamodelRegistry.Register(new ConceptType
{
    Name = "Feature",
    Layer = ConceptLayer.Requirements,
    Source = "Requirements DSL"
});

The registry enables:

  1. Cross-layer reference resolution. When [OpsRequirementLink(typeof(OrderCancellationFeature), 1)] appears on a chaos experiment, the registry resolves OrderCancellationFeature as a Requirements-layer concept and validates that acceptance criterion index 1 exists.

  2. Completeness checking. The registry knows all concepts across all layers. It can ask: "Does every Domain-layer AggregateRoot have a corresponding Operations-layer DeploymentApp?" This is cross-layer coverage analysis.

  3. Impact analysis. When a developer changes [AggregateRoot("Order")] to add a new domain event, the registry traces the impact: the new event needs a metric definition (Observability), the metric might be a scaling signal (Capacity), the scaling might affect the cost budget (Cost). The analyzer reports all downstream impacts.

  4. Documentation generation. The registry produces a complete concept map: every domain concept, every operational concept, every requirement, and every link between them. This is the architecture documentation that is always current because it is compiled.


The Injectable Decorator Bridge

The CMF's Injectable library uses source-generated DI decorators. The Ops DSLs generate operational decorators. The bridge: Injectable's [Injectable] attribute on a service interface, combined with the Chaos DSL's [ChaosExperiment], generates a chaos decorator in the DI chain.

// Domain-side: Injectable service
[Injectable(Lifetime = ServiceLifetime.Scoped)]
public interface IPaymentGateway
{
    Task<PaymentResult> ProcessPayment(PaymentRequest request);
}

public class StripePaymentGateway : IPaymentGateway
{
    public async Task<PaymentResult> ProcessPayment(PaymentRequest request)
    {
        // actual Stripe API call
    }
}

// Ops-side: chaos experiment targeting this service
[ChaosExperiment("payment-timeout",
    Tier = Tier.InProcess,
    FaultKind = FaultKind.Timeout,
    TargetService = "IPaymentGateway",
    Duration = "30s")]

public partial class OrderServiceOps { }

The source generator sees both attributes -- [Injectable] on IPaymentGateway and [ChaosExperiment] targeting IPaymentGateway. It generates:

// Auto-generated: PaymentGatewayChaosDecorator.g.cs
public class PaymentGatewayChaosDecorator : IPaymentGateway
{
    private readonly IPaymentGateway _inner;
    private readonly IChaosController _chaos;

    public PaymentGatewayChaosDecorator(
        IPaymentGateway inner, IChaosController chaos)
    {
        _inner = inner;
        _chaos = chaos;
    }

    public async Task<PaymentResult> ProcessPayment(PaymentRequest request)
    {
        if (_chaos.IsExperimentActive("payment-timeout"))
        {
            await Task.Delay(_chaos.GetInjectedDelay("payment-timeout"));
            if (_chaos.ShouldFault("payment-timeout"))
                throw new TimeoutException("Chaos: payment-timeout injected");
        }
        return await _inner.ProcessPayment(request);
    }
}

The DI registration chains the decorator:

// Normal registration (from Injectable):
services.AddScoped<IPaymentGateway, StripePaymentGateway>();

// Chaos decorator (from Ops DSL, test builds only):
#if CHAOS_ENABLED
services.Decorate<IPaymentGateway, PaymentGatewayChaosDecorator>();
#endif

// Resilience decorator (from Ops DSL):
services.Decorate<IPaymentGateway, PaymentGatewayCircuitBreakerDecorator>();
services.Decorate<IPaymentGateway, PaymentGatewayRetryDecorator>();

The final chain: RetryDecorator -> CircuitBreakerDecorator -> ChaosDecorator -> StripePaymentGateway. In test builds, the chaos decorator injects faults. In production builds, it is compiled out. The resilience decorators are always present.

Injectable provides the decorator pattern. The Ops DSLs provide the decorator implementations. The source generator wires them together. No manual DI registration.


The 7-Stage Pipeline With Ops

The CMF uses a five-stage source generation pipeline (Stages 0-4) for dev-side DSLs. The Ops DSLs extend it to seven stages:

Stage Name Dev-Side Ops-Side
0 Collection Collect domain attributes Collect ops attributes
1 Metamodel Register domain concepts in M3 Register ops concepts in M3
2 Validation Validate domain invariants Validate ops cross-references
3 Generation Generate domain code (repos, events, handlers) Generate InProcess code (decorators, policies, middleware)
4 Integration Generate admin pages, content types Generate Container configs (docker-compose, prometheus, k6)
5 Cross-layer - Cross-DSL validation (domain + ops)
6 Deployment - Generate Cloud configs (terraform, k8s, litmus)

Stages 0-4 run during dotnet build. Stages 5-6 run during dotnet ops generate, which is a post-build step that has access to the full compiled assembly.

Stage 5 is where the cross-layer magic happens. The generator has access to both domain attributes ([AggregateRoot], [ContentPart], [Feature]) and ops attributes ([DeploymentApp], [ChaosExperiment], [GdprDataMap]). It resolves all [OpsRequirementLink] references, validates all cross-layer chains, and produces the traceability matrix.

Stage 6 generates the cloud-tier artifacts: Terraform modules, Kubernetes manifests, LitmusChaos CRDs, cert-manager configs. These require the full cross-layer context from Stage 5 -- the Terraform module for the order service needs to know the container spec, the autoscale rule, the storage spec, and the network policy. All of which were validated in Stage 5.


What Integration Means

Without integration, the domain model and the operational model are two separate worlds. The developer writes [AggregateRoot("Order")] and the ops engineer writes a Helm chart. The two artifacts have the same name but no typed connection. The Helm chart can reference a container image that does not exist. The domain model can publish events that nothing monitors.

With integration, the two worlds share a type system. The [AggregateRoot("Order")] and the [DeploymentApp("order-service")] are attributes on classes in the same compilation unit. The source generator sees both. The analyzer validates both. The generated artifacts are consistent because they come from the same source of truth.

The domain model is not deployed. It is compiled into a deployment. The difference matters. A deployed domain model is a binary artifact that someone maps to infrastructure. A compiled deployment is a typed specification that the compiler maps to infrastructure. The mapping is validated, not hoped for.

One dotnet build. Domain logic, operational specifications, cross-layer validation, generated artifacts. The wiki, the runbook, the Helm chart, the Terraform module, the PagerDuty config, the Grafana dashboard -- all generated from typed attributes on C# classes that reference each other through the M3 metamodel registry.

⬇ Download