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

Overview

The ALM DSL is the runtime side of the lifecycle. It declares the things that exist while the application is running: deployable services, the feature flags that gate their behaviour, the SLOs they must respect, the health probes that prove they are alive, the observability signals they emit, and the rollout strategies that introduce change without breaking production.

The source generator produces:

  • An AlmRegistry of const strings (Alm.SERVICE_*, Alm.FLAG_*, Alm.SLO_*)
  • The Aspire AppHost wiring -- a Builder.AddProject<...>() call per [Service], with health probes and resource references baked in
  • A strongly-typed IFeatureFlagsTyped interface -- one bool property per declared flag, zero magic strings in business code
  • OpenTelemetry instrumentation classes (Meter, ActivitySource) per [ObservabilitySignal]
  • A Grafana dashboard JSON payload per [Slo]
  • Roslyn analyzers ALM001-ALM100 enforcing flag usage, SLO presence and layer isolation

The DSL's most important property is what it does not generate: it does not put any runtime code into AcmeStore.Alm itself. The Alm project is a build-time metadata project. The generated Aspire wiring lands in AcmeStore.AppHost. The generated typed flag interface lands in AcmeStore.FeatureToggles. Domain code never references AcmeStore.Alm. This is enforced by analyzer ALM100 -- see Cross-DSL references below.


Service

namespace Cmf.Alm.Lib;

/// <summary>
/// A deployable runtime unit. References an SDLC BuildTarget for
/// the artifact to ship and a Deployment for where to ship it.
/// The compiler refuses to declare a Service without a matching
/// build target.
/// </summary>
[MetaConcept("Service")]
[MetaReference("Build", "BuildTarget", Multiplicity = "1")]
[MetaConstraint("MustHaveBuildTarget",
    "Build != null",
    Message = "Service must reference a [BuildTarget] from the SDLC DSL")]
public sealed class ServiceAttribute : Attribute
{
    [MetaProperty("Name", "string", Required = true)]
    public string Name { get; }

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

    [MetaProperty("Description", "string")]
    public string? Description { get; set; }

    public ServiceAttribute(string name, string build)
    {
        Name = name;
        Build = build;
    }
}

Deployment

/// <summary>
/// Where and how a Service is deployed. Kind selects the Aspire
/// resource builder method that will be emitted in AppHost.g.cs.
/// </summary>
[MetaConcept("Deployment")]
public sealed class DeploymentAttribute : Attribute
{
    [MetaProperty("Kind", "DeploymentKind", Required = true)]
    public DeploymentKind Kind { get; set; }

    [MetaProperty("Replicas", "int")]
    public int Replicas { get; set; } = 1;

    public DeploymentAttribute(DeploymentKind kind) => Kind = kind;
}

public enum DeploymentKind
{
    AspireProject,    // builder.AddProject<Projects.X>(...)
    AspireContainer,  // builder.AddContainer(...)
    AspireExecutable, // builder.AddExecutable(...)
    EdgeFunction      // out-of-Aspire edge runtime
}

Slo

/// <summary>
/// A Service Level Objective. Lambdas are evaluated at runtime against
/// observed metrics; failures emit ObservabilityEvents and -- when
/// configured -- open Incidents. The same lambda is also exported as a
/// Grafana alerting rule (PromQL) in the generated dashboard JSON.
/// </summary>
[MetaConcept("Slo")]
[MetaInherits("MetaConstraint")]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class SloAttribute : Attribute
{
    [MetaProperty("Name", "string", Required = true)]
    public string Name { get; }

    [MetaProperty("Expression", "lambda<ServiceMetrics, bool>", Required = true)]
    public Expression<Func<ServiceMetrics, bool>> Expression { get; }

    [MetaProperty("Window", "TimeSpan")]
    public string Window { get; set; } = "00:05:00";

    public SloAttribute(string name, Expression<Func<ServiceMetrics, bool>> expression)
    {
        Name = name;
        Expression = expression;
    }
}

FeatureFlag

/// <summary>
/// A runtime toggle. Generates one strongly-typed property on
/// IFeatureFlagsTyped. The Owner property links the flag back to
/// a PLM Product so that flag stewardship aligns with product
/// ownership. The Default value is what production reads when the
/// flag store is unreachable.
/// </summary>
[MetaConcept("FeatureFlag")]
[MetaConstraint("FlagNameIsValidIdentifier",
    "Regex.IsMatch(Name, '^[A-Z][A-Za-z0-9_]*$')",
    Message = "FeatureFlag name must be a valid PascalCase C# identifier")]
public sealed class FeatureFlagAttribute : Attribute
{
    [MetaProperty("Name", "string", Required = true)]
    public string Name { get; }

    [MetaProperty("Default", "bool")]
    public bool Default { get; set; } = false;

    [MetaProperty("Owner", "string")]
    public string? Owner { get; set; }

    [MetaProperty("ExpiresOn", "DateOnly")]
    public string? ExpiresOn { get; set; }

    public FeatureFlagAttribute(string name) => Name = name;
}

Rollout

/// <summary>
/// How a feature flag or a deployment moves from off to on.
/// Strategy controls which runtime selector the typed flag
/// interface uses (canary % bucketing, blue/green pair, dark launch).
/// </summary>
[MetaConcept("Rollout")]
public sealed class RolloutAttribute : Attribute
{
    [MetaProperty("Strategy", "RolloutStrategy", Required = true)]
    public RolloutStrategy Strategy { get; set; }

    [MetaProperty("Percent", "int")]
    public int Percent { get; set; } = 100;

    public RolloutAttribute(RolloutStrategy strategy) => Strategy = strategy;
}

public enum RolloutStrategy { AllAtOnce, Canary, BlueGreen, Dark }

HealthProbe

/// <summary>
/// A liveness, readiness or startup probe exposed by a Service.
/// The Aspire AppHost generator wires the probe path into
/// builder.WithHttpHealthCheck(...).
/// </summary>
[MetaConcept("HealthProbe")]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class HealthProbeAttribute : Attribute
{
    [MetaProperty("Kind", "ProbeKind", Required = true)]
    public ProbeKind Kind { get; set; }

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

    public HealthProbeAttribute(ProbeKind kind, string path)
    {
        Kind = kind;
        Path = path;
    }
}

public enum ProbeKind { Liveness, Readiness, Startup }

ObservabilitySignal

/// <summary>
/// Declares a metric, structured log or distributed trace span
/// that a Service is contractually obliged to emit. The generator
/// produces a Meter or ActivitySource so that consumers (dashboards,
/// alerts, the SLO evaluator) can bind to a stable name.
/// </summary>
[MetaConcept("ObservabilitySignal")]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class ObservabilitySignalAttribute : Attribute
{
    [MetaProperty("Name", "string", Required = true)]
    public string Name { get; }

    [MetaProperty("Kind", "SignalKind", Required = true)]
    public SignalKind Kind { get; set; }

    [MetaProperty("Unit", "string")]
    public string? Unit { get; set; }

    public ObservabilitySignalAttribute(string name, SignalKind kind)
    {
        Name = name;
        Kind = kind;
    }
}

public enum SignalKind { Counter, Histogram, Gauge, Trace, StructuredLog }

What the developer writes

namespace AcmeStore.Alm;

[Service("Api", build: Sdlc.BUILDTARGET_API,
    Description = "Public REST + GraphQL surface")]
[Deployment(DeploymentKind.AspireProject, Replicas = 3)]
[HealthProbe(ProbeKind.Liveness,  "/health/live")]
[HealthProbe(ProbeKind.Readiness, "/health/ready")]
[Slo("p99-latency", m => m.LatencyP99Ms <= 250, Window = "00:05:00")]
[Slo("availability", m => m.SuccessRate >= 0.999, Window = "01:00:00")]
[ObservabilitySignal("acme.api.requests", SignalKind.Counter)]
[ObservabilitySignal("acme.api.latency", SignalKind.Histogram, Unit = "ms")]
public partial class ApiService { }

[Service("Worker", build: Sdlc.BUILDTARGET_WORKER)]
[Deployment(DeploymentKind.AspireProject, Replicas = 2)]
[HealthProbe(ProbeKind.Liveness, "/health/live")]
[Slo("queue-drain", m => m.QueueDepth < 1000)]
public partial class WorkerService { }

[Service("Web", build: Sdlc.BUILDTARGET_WEB)]
[Deployment(DeploymentKind.AspireProject, Replicas = 2)]
public partial class WebService { }

// ── Feature flags ──
[FeatureFlag("NewCheckout", Default = false, Owner = Plm.PRODUCT_STORE,
    ExpiresOn = "2026-09-30")]
[Rollout(RolloutStrategy.Canary, Percent = 10)]
public partial class NewCheckoutFlag { }

[FeatureFlag("DarkMode", Default = true, Owner = Plm.PRODUCT_STORE)]
[Rollout(RolloutStrategy.AllAtOnce)]
public partial class DarkModeFlag { }

[FeatureFlag("ExperimentalSearch", Default = false, Owner = Plm.PRODUCT_STORE)]
[Rollout(RolloutStrategy.Dark)]
public partial class ExperimentalSearchFlag { }

Notice three things:

  1. Sdlc.BUILDTARGET_API is a const string from the SDLC DSL's generated registry. Renaming the build target in AcmeStore.Sdlc will break this file at compile time.
  2. Plm.PRODUCT_STORE is a const string from the PLM DSL. Flag ownership is checked transitively.
  3. AcmeStore.Alm references AcmeStore.Sdlc and AcmeStore.Plm (which both depend on the domain). It does not reference AcmeStore.Lib, AcmeStore.Api, or any other runtime code. The runtime never depends on AcmeStore.Alm either -- see analyzer ALM100.

1. AlmRegistry

// Generated: AlmRegistry.g.cs in AcmeStore.Alm
namespace AcmeStore.Alm;

public static class Alm
{
    public const string SERVICE_API    = "AcmeStore.Alm.ApiService";
    public const string SERVICE_WORKER = "AcmeStore.Alm.WorkerService";
    public const string SERVICE_WEB    = "AcmeStore.Alm.WebService";

    public const string FLAG_NEW_CHECKOUT        = "NewCheckout";
    public const string FLAG_DARK_MODE           = "DarkMode";
    public const string FLAG_EXPERIMENTAL_SEARCH = "ExperimentalSearch";

    public const string SLO_API_P99_LATENCY = "ApiService::p99-latency";
    public const string SLO_API_AVAIL       = "ApiService::availability";
    public const string SLO_WORKER_QUEUE    = "WorkerService::queue-drain";
}

2. Aspire AppHost wiring (emitted into the AppHost project, not Alm)

// Generated: AspireAppHost.g.cs in AcmeStore.AppHost
// Source: AcmeStore.Alm/*.cs
// DO NOT EDIT
using Aspire.Hosting;

public static partial class AcmeStoreAppHost
{
    public static IDistributedApplicationBuilder WireServices(
        this IDistributedApplicationBuilder builder)
    {
        var api = builder.AddProject<Projects.AcmeStore_Api>("api")
            .WithReplicas(3)
            .WithHttpHealthCheck("/health/live")
            .WithHttpHealthCheck("/health/ready");

        var worker = builder.AddProject<Projects.AcmeStore_Worker>("worker")
            .WithReplicas(2)
            .WithHttpHealthCheck("/health/live");

        var web = builder.AddProject<Projects.AcmeStore_Web>("web")
            .WithReplicas(2)
            .WithReference(api);

        return builder;
    }
}

The developer's hand-written Program.cs for the AppHost stays minimal:

var builder = DistributedApplication.CreateBuilder(args);
builder.WireServices();   // partial method emitted by ALM generator
builder.Build().Run();

The AppHost project does not reference AcmeStore.Alm. The generator's [Generator] is registered in AcmeStore.Alm.Generators and targets AcmeStore.AppHost via an <AdditionalFiles> directive in the AppHost csproj. This is the linchpin of the dependency-inversion guarantee.

3. Strongly-typed feature flag interface (emitted into FeatureToggles, not Alm)

// Generated: IFeatureFlagsTyped.g.cs in AcmeStore.FeatureToggles
// Source: AcmeStore.Alm/*.cs
// DO NOT EDIT
namespace AcmeStore.FeatureToggles;

public interface IFeatureFlagsTyped : IFeatureFlags
{
    bool NewCheckout { get; }
    bool DarkMode { get; }
    bool ExperimentalSearch { get; }
}

public sealed class FeatureFlagsTyped : IFeatureFlagsTyped
{
    private readonly IFeatureFlags _inner;
    public FeatureFlagsTyped(IFeatureFlags inner) => _inner = inner;

    public bool IsEnabled(string flagName) => _inner.IsEnabled(flagName);

    public bool NewCheckout         => _inner.IsEnabled("NewCheckout");
    public bool DarkMode            => _inner.IsEnabled("DarkMode");
    public bool ExperimentalSearch  => _inner.IsEnabled("ExperimentalSearch");
}

IFeatureFlags is the base contract declared in AcmeStore.Abstractions (zero dependencies). IFeatureFlagsTyped extends it with one property per declared flag. Business code injects IFeatureFlagsTyped and writes:

public sealed class CheckoutCommand
{
    private readonly IFeatureFlagsTyped _flags;
    public CheckoutCommand(IFeatureFlagsTyped flags) => _flags = flags;

    public Task HandleAsync()
        => _flags.NewCheckout
            ? RunNewCheckoutAsync()
            : RunLegacyCheckoutAsync();
}

No magic string. No reference to AcmeStore.Alm. Renaming the [FeatureFlag] declaration breaks every consuming property at compile time.

4. OpenTelemetry instrumentation

// Generated: ApiServiceTelemetry.g.cs in AcmeStore.FeatureToggles (or a Telemetry project)
public static class ApiServiceTelemetry
{
    public static readonly Meter Meter = new("acme.api", version: "1.0.0");

    public static readonly Counter<long> Requests =
        Meter.CreateCounter<long>("acme.api.requests");

    public static readonly Histogram<double> Latency =
        Meter.CreateHistogram<double>("acme.api.latency", unit: "ms");
}

5. Grafana SLO dashboard

// Generated: slo-dashboard.json
{
  "title": "AcmeStore SLOs",
  "panels": [
    {
      "title": "Api p99 latency (target <= 250ms over 5m)",
      "targets": [{
        "expr": "histogram_quantile(0.99, rate(acme_api_latency_bucket[5m]))"
      }],
      "thresholds": [{ "value": 250, "op": "gt", "color": "red" }]
    },
    {
      "title": "Api availability (target >= 99.9% over 1h)",
      "targets": [{
        "expr": "1 - (sum(rate(acme_api_requests{status=~'5..'}[1h])) / sum(rate(acme_api_requests[1h])))"
      }]
    }
  ]
}

6. Generation pipeline

Diagram
The ALM generation pipeline: one link-check pass feeds five emission targets (registry, Aspire host, typed flags, telemetry, SLO dashboard) plus the ALM100 layer guard.

Cross-DSL references

The ALM DSL sits between SDLC (below) and PLM (above). Its references are strict:

  • [Service(Build = Sdlc.BUILDTARGET_*)] -- every service must point at an SDLC build target
  • [FeatureFlag(Owner = Plm.PRODUCT_*)] -- every flag must be owned by a PLM product
  • [Implements(Requirements.FEATURE_*)] (optional) -- a service may declare which Requirements DSL features it satisfies

Generation direction is inverted from references: ALM source files live in AcmeStore.Alm, but the bulk of generated runtime code is emitted into other projects. This is what keeps AcmeStore.Alm out of the runtime dependency graph.

Diagram
Generation direction is inverted from references: declarations live in AcmeStore.Alm, but generated runtime code is emitted into AppHost, FeatureToggles and Telemetry so that the Alm assembly stays out of the runtime graph.

Analyzers, including the layer guard

ID Severity Message
ALM001 Warning FeatureFlag {F} is declared but never read in any consuming project
ALM002 Warning Service {S} has no [Slo]
ALM003 Error [Service.Build] references unknown BuildTarget {B}
ALM004 Warning FeatureFlag {F} ExpiresOn {date} is in the past
ALM005 Error HealthProbe path {P} is not exposed by service {S}
ALM100 Error Project {X} (layer Domain or Infra) must not reference AcmeStore.Alm. Depend on IFeatureFlags from AcmeStore.Abstractions instead

ALM100 is the most important rule of the chapter. It is implemented as an IIncrementalGenerator in Cmf.Alm.Generators that inspects every consuming project's Compilation.ReferencedAssemblyNames plus the assembly-level [Layer] attribute. If a project marked [assembly: Layer(Layer.Domain)] or [assembly: Layer(Layer.InfraRuntime)] references AcmeStore.Alm, the build fails with ALM100.

This is what makes the rule of "no leaking ALM into business code" mechanical instead of cultural. A new contributor cannot accidentally using AcmeStore.Alm; from AcmeStore.Lib and slip it past code review.


Why this beats ad-hoc tooling

In a typical project, the same information is spread across:

  • An Aspire AppHost/Program.cs (services, replicas, health checks)
  • A appsettings.json flag block read by a LaunchDarkly/ConfigCat SDK
  • A Grafana dashboard cloned from a template
  • A Word document defining "the SLOs we should hit"
  • A Slack channel where someone occasionally remembers to remove a stale flag

Each artifact has a different syntax, lives in a different system and ages independently. Drift is guaranteed.

The ALM DSL collapses all five into one source of truth. The compiler enforces consistency. The generator emits the artifacts that downstream systems need. Removing a feature flag becomes a one-line PR that the build itself certifies as safe.

The next chapter (Part XVII -- PLM DSL) goes one layer up: Products, Releases, Roadmap items and EOL policies, all referencing the SDLC pipelines and ALM services declared so far.

⬇ Download