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

Before and After

Before -- stringly-typed:

[Feature(Id = "FEATURE-456", AcceptanceCriteria = new[] { "Admin can assign roles" })]
public class UserRoleFeature { }  // Empty marker class -- dead code

[Implements(Requirements.FEATURE_USER_ROLES)]  // const string = "FEATURE-456"
public void AssignRole(User u, Role r) { }

[FeatureTest(Requirements.FEATURE_USER_ROLES, 0)]  // AC[0] by fragile index
public void AdminCanAssign() { }

Problems: strings can be wrong, AC indices are fragile, no compiler check that implementation satisfies ACs, marker class is dead code.

After -- type-safe:

// Requirement IS a type with abstract AC methods
public abstract record UserRolesFeature : Feature<PlatformScalabilityEpic>
{
    public abstract AcceptanceCriterionResult AdminCanAssignRoles(
        UserId actingUser, UserId targetUser, RoleId role);
}

// Specification IS an interface the domain must implement
[ForRequirement(typeof(UserRolesFeature))]
public interface IUserRolesSpec
{
    [ForRequirement(typeof(UserRolesFeature), nameof(UserRolesFeature.AdminCanAssignRoles))]
    Result AssignRole(User actingUser, User targetUser, Role role);
}

// Implementation MUST implement the interface -- compiler enforces it
[ForRequirement(typeof(UserRolesFeature))]
public class AuthorizationService : IUserRolesSpec
{
    public Result AssignRole(User actingUser, User targetUser, Role role) { ... }
    // Remove this method --> compile error
}

// Test linked by type + nameof -- compiler-checked
[Verifies(typeof(UserRolesFeature), nameof(UserRolesFeature.AdminCanAssignRoles))]
public void Admin_can_assign_role() { ... }
// Rename AC method --> compile error. Delete AC method --> compile error.
Aspect Before (strings) After (types)
Requirement "FEATURE-456" const string typeof(UserRolesFeature)
Acceptance criterion new[] { "Admin can..." } abstract AcceptanceCriterionResult AdminCanAssignRoles(...)
Implementation link [Implements("FEATURE-456")] : IUserRolesSpec -- compiler enforces
Test link [FeatureTest("FEATURE-456", 0)] [Verifies(typeof(...), nameof(...AdminCanAssignRoles))]
Add new AC Add string to array, hope Add abstract method --> compile error everywhere
Rename Find-replace, pray Refactoring tools update all references

Features Span DLLs, Processes, and Machines

In a real system, a Feature doesn't live in one project. "Order Processing" touches the API, the payment gateway, the notification worker, the event bus, and the test suite -- each compiled to a separate DLL, potentially running on different machines. The .Requirements project is the cross-cutting source of truth that all of these reference via <ProjectReference>:

MyCompany.sln
├── src/
│   ├── MyCompany.Requirements/          ← Every project references this
│   ├── MyCompany.OrderService/          ← [Implements] OrderProcessingSpec
│   ├── MyCompany.PaymentGateway/        ← [Implements] PaymentValidationSpec
│   ├── MyCompany.NotificationWorker/    ← [Implements] OrderNotificationSpec
│   └── MyCompany.SharedKernel/          ← Domain types shared across services
└── test/
    ├── MyCompany.OrderService.Tests/    ← [Verifies] OrderProcessing ACs
    └── MyCompany.PaymentGateway.Tests/  ← [Verifies] PaymentValidation ACs

Every DLL knows which Features it implements. Every test knows which AC it verifies. The .Requirements project is the single artifact that ties them together -- across solution boundaries, across machines, across teams.

Acceptance Criteria Are Live Code

ACs are not documentation. They are static methods that validate business rules -- and they run in two places:

// In the AC definition (MyCompany.Requirements)
public abstract partial record OrderProcessingFeature : Feature
{
    // This AC is an abstract method -- but its generated validation logic is concrete
    public static bool OrderTotalMustBePositive(Order order)
        => order.Total > 0 && order.Lines.All(l => l.Quantity > 0);
}

// In production code (MyCompany.OrderService)
public class OrderService : IOrderProcessingSpec
{
    public Result<Order, IDomainException> CreateOrder(CreateOrderCommand cmd)
    {
        var order = Order.CreateInstance(cmd);

        // AC validation called in production -- same method the tests use
        if (!OrderProcessingFeature.OrderTotalMustBePositive(order))
            throw new InvalidOrderException("Order total must be positive");

        return Result<Order, IDomainException>.Success(order);
    }
}

// In unit tests (MyCompany.OrderService.Tests)
[Verifies(typeof(OrderProcessingFeature), nameof(OrderProcessingFeature.OrderTotalMustBePositive))]
public void Order_with_positive_total_passes_AC()
{
    var order = CreateValidOrder();
    Assert.True(OrderProcessingFeature.OrderTotalMustBePositive(order));
}

The same AC method is called in production to enforce the rule and in tests to verify it. One definition, two uses. This is what gives value to the entire process: ACs are compiled, tested, and running in production. They are not a checklist in Jira that someone forgets to update.

The Developer Sees the Full Scope

In a 50-project mono-repo, a developer working on OrderLine can Ctrl+Click through the entire chain:

  1. Test[Verifies(typeof(OrderProcessingFeature), nameof(OrderTotalMustBePositive))] → click → lands on the AC method
  2. AC method → defined on OrderProcessingFeature → click → lands on the Feature definition
  3. Feature[Epic(typeof(ECommerceEpic))] → click → lands on the Epic
  4. Epic → see all Features, all ACs, all implementations, all tests

No Jira lookup. No "ask the PO." No context switching. The developer understands the full scope of what they're building, what's already implemented, and what's missing -- because it's all in the type system.

The .Requirements Project Is a Company Asset

This is the intersection of TDD (write the failing spec first), BDD (specs are behavior descriptions), and industrial software practice (traceability is mandatory in DO-178C, IEC 62304, ISO 26262). The .Requirements project is the masterpiece that holds the system together:

  • It's compiled -- invalid requirements produce compiler errors
  • It's tested -- every AC has a [Verifies] test or the build warns
  • It's running in production -- AC methods enforce business rules at runtime
  • It's versioned -- lifecycle states (Draft → Approved → InProgress → Done) are a generated state machine
  • It's traceable -- the source generator produces compliance reports, traceability matrices, and coverage metrics

It deserves the same care as the domain model. It deserves code review, refactoring, and architectural attention. Because it IS the system specification -- not a document about the system, but the system describing itself.


Why This Matters

For developers: Requirements are types with IDE support. Ctrl+Click from a test to the AC it verifies, from the AC to its Feature, from the Feature to its Epic. In a solution with 20+ projects spanning multiple services, every developer can trace from their code to the business requirement it satisfies -- and back down to every test that proves it. ACs are not documentation: they are static methods called in unit tests AND in production code. When you write a test, you're proving an AC. When you read production code, you see which AC it enforces. No context switching. No Jira lookups. No "ask the PO."

For the compiler: Adding an AC to a Feature is adding an abstract method. The build breaks until the specification interface has the method, the domain implements it, and tests verify it. The compiler IS the project manager.

For QA: The traceability matrix is source-generated. Coverage analysis is type-based, not string-matching. Every untested AC is a compiler warning.

For ops: The production host validates compliance at startup. API endpoints are annotated with requirement metadata in the OpenAPI schema. Compliance mode enables runtime AC evaluation for audit trails.

For architects: Requirements are first-class modeling primitives (M2 on M3). They integrate with DDD, Workflow, Admin, Content, and Pages DSLs. The hierarchy is generic-constrained. The lifecycle is a generated state machine. Each layer compiles to a distinct DLL artifact with clear dependencies.


What's Next

This blog post describes the design of a type-safe requirements chain. The implementation follows the CMF's existing multi-stage pipeline:

  1. Requirements (MyApp.Requirements.dll): Features as abstract records with AC methods, plus generated [ForRequirement]/[Verifies] attributes and RequirementRegistry
  2. SharedKernel (MyApp.SharedKernel.dll): Domain value types shared across all layers
  3. Specifications (MyApp.Specifications.dll): Interfaces decorated with [ForRequirement], plus validator bridges for compliance mode
  4. Domain (MyApp.Domain.dll): Implements spec interfaces -- compiler-enforced
  5. Api (MyApp.Api.dll): Production host with DI wiring, controllers with [ForRequirement], startup compliance check, OpenAPI enrichment
  6. Tests (MyApp.Tests.dll): Type-linked verification with [TestsFor] and [Verifies]
  7. Analyzers: Traceability matrix, compiler diagnostics, markdown/JSON/CSV reports

Six DLLs. One compiler. Every link is a type reference. Requirements are not documents -- they are types, and the compiler enforces the chain from definition to production deployment to end-to-end verification. No strings. No indices. No prayer.

⬇ Download