The Fundamental Shift
A Feature is not an attribute on a marker class. A Feature is a class. Each acceptance criterion is an abstract method -- its signature defines the inputs, its return type enforces verifiability.
Base Types
namespace MyApp.Requirements;
public abstract record RequirementMetadata
{
public abstract string Title { get; }
public abstract RequirementPriority Priority { get; }
public abstract string Owner { get; }
}
public enum RequirementPriority { Critical, High, Medium, Low, Backlog }
public enum BugSeverity { Critical, Major, Minor, Cosmetic }namespace MyApp.Requirements;
public abstract record RequirementMetadata
{
public abstract string Title { get; }
public abstract RequirementPriority Priority { get; }
public abstract string Owner { get; }
}
public enum RequirementPriority { Critical, High, Medium, Low, Backlog }
public enum BugSeverity { Critical, Major, Minor, Cosmetic }Hierarchy Through Generics
The hierarchy (Epic > Feature > Story > Task, Bug) is enforced by generic constraints:
public abstract record Epic : RequirementMetadata;
public abstract record Feature<TParent> : RequirementMetadata
where TParent : Epic;
public abstract record Feature : RequirementMetadata; // root-level, no parent
public abstract record Story<TParent> : RequirementMetadata
where TParent : RequirementMetadata;
public abstract record Task<TParent> : RequirementMetadata
where TParent : RequirementMetadata
{
public abstract int EstimatedHours { get; }
}
public abstract record Bug : RequirementMetadata
{
public abstract BugSeverity Severity { get; }
}public abstract record Epic : RequirementMetadata;
public abstract record Feature<TParent> : RequirementMetadata
where TParent : Epic;
public abstract record Feature : RequirementMetadata; // root-level, no parent
public abstract record Story<TParent> : RequirementMetadata
where TParent : RequirementMetadata;
public abstract record Task<TParent> : RequirementMetadata
where TParent : RequirementMetadata
{
public abstract int EstimatedHours { get; }
}
public abstract record Bug : RequirementMetadata
{
public abstract BugSeverity Severity { get; }
}You cannot write Feature<JwtRefreshStory> -- the constraint where TParent : Epic prevents it. The type system enforces the hierarchy.
The AC Result Type
Every acceptance criterion method returns this:
public readonly record struct AcceptanceCriterionResult
{
public bool IsSatisfied { get; }
public string? FailureReason { get; }
private AcceptanceCriterionResult(bool satisfied, string? reason)
{
IsSatisfied = satisfied;
FailureReason = reason;
}
public static AcceptanceCriterionResult Satisfied() => new(true, null);
public static AcceptanceCriterionResult Failed(string reason) => new(false, reason);
public static implicit operator bool(AcceptanceCriterionResult r) => r.IsSatisfied;
}public readonly record struct AcceptanceCriterionResult
{
public bool IsSatisfied { get; }
public string? FailureReason { get; }
private AcceptanceCriterionResult(bool satisfied, string? reason)
{
IsSatisfied = satisfied;
FailureReason = reason;
}
public static AcceptanceCriterionResult Satisfied() => new(true, null);
public static AcceptanceCriterionResult Failed(string reason) => new(false, reason);
public static implicit operator bool(AcceptanceCriterionResult r) => r.IsSatisfied;
}Lightweight Domain Concepts
AC method signatures use lightweight value types that represent domain concepts without importing domain implementation details:
public readonly record struct Email(string Value);
public readonly record struct UserId(Guid Value, string FirstName, string LastName, Email Email);
public readonly record struct RoleId(string Value);
public readonly record struct ResourceId(string Value);
public readonly record struct TokenId(Guid Value);public readonly record struct Email(string Value);
public readonly record struct UserId(Guid Value, string FirstName, string LastName, Email Email);
public readonly record struct RoleId(string Value);
public readonly record struct ResourceId(string Value);
public readonly record struct TokenId(Guid Value);Concrete Requirements -- The "What" Without the "How"
namespace MyApp.Requirements.Epics;
public abstract record PlatformScalabilityEpic : Epic
{
public override string Title => "Multi-tenant platform enablement";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "platform-team";
}namespace MyApp.Requirements.Epics;
public abstract record PlatformScalabilityEpic : Epic
{
public override string Title => "Multi-tenant platform enablement";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "platform-team";
}namespace MyApp.Requirements.Features;
/// <summary>
/// Feature: User roles and permissions.
/// Each abstract method IS an acceptance criterion.
/// The method signature defines what must be true.
/// The parameters define the domain inputs.
/// The return type enforces that every AC produces a verifiable result.
/// </summary>
public abstract record UserRolesFeature : Feature<PlatformScalabilityEpic>
{
public override string Title => "User roles and permissions";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "auth-team";
// AC1: Admin users can assign roles to other users
public abstract AcceptanceCriterionResult AdminCanAssignRoles(
UserId actingUser, UserId targetUser, RoleId role);
// AC2: Viewer users have read-only access
public abstract AcceptanceCriterionResult ViewerHasReadOnlyAccess(
UserId viewer, ResourceId resource);
// AC3: Role changes take effect immediately (no restart required)
public abstract AcceptanceCriterionResult RoleChangeTakesEffectImmediately(
UserId user, RoleId previousRole, RoleId newRole);
}namespace MyApp.Requirements.Features;
/// <summary>
/// Feature: User roles and permissions.
/// Each abstract method IS an acceptance criterion.
/// The method signature defines what must be true.
/// The parameters define the domain inputs.
/// The return type enforces that every AC produces a verifiable result.
/// </summary>
public abstract record UserRolesFeature : Feature<PlatformScalabilityEpic>
{
public override string Title => "User roles and permissions";
public override RequirementPriority Priority => RequirementPriority.High;
public override string Owner => "auth-team";
// AC1: Admin users can assign roles to other users
public abstract AcceptanceCriterionResult AdminCanAssignRoles(
UserId actingUser, UserId targetUser, RoleId role);
// AC2: Viewer users have read-only access
public abstract AcceptanceCriterionResult ViewerHasReadOnlyAccess(
UserId viewer, ResourceId resource);
// AC3: Role changes take effect immediately (no restart required)
public abstract AcceptanceCriterionResult RoleChangeTakesEffectImmediately(
UserId user, RoleId previousRole, RoleId newRole);
}namespace MyApp.Requirements.Stories;
public abstract record JwtRefreshStory : Story<UserRolesFeature>
{
public override string Title => "Implement JWT refresh token flow";
public override RequirementPriority Priority => RequirementPriority.Medium;
public override string Owner => "alice@company.com";
public abstract AcceptanceCriterionResult TokensExpireAfterOneHour(
TokenId token, DateTimeOffset issuedAt, DateTimeOffset checkedAt);
public abstract AcceptanceCriterionResult RefreshExtendsBySevenDays(
TokenId token, DateTimeOffset refreshedAt);
}namespace MyApp.Requirements.Stories;
public abstract record JwtRefreshStory : Story<UserRolesFeature>
{
public override string Title => "Implement JWT refresh token flow";
public override RequirementPriority Priority => RequirementPriority.Medium;
public override string Owner => "alice@company.com";
public abstract AcceptanceCriterionResult TokensExpireAfterOneHour(
TokenId token, DateTimeOffset issuedAt, DateTimeOffset checkedAt);
public abstract AcceptanceCriterionResult RefreshExtendsBySevenDays(
TokenId token, DateTimeOffset refreshedAt);
}namespace MyApp.Requirements.Bugs;
public abstract record OrderNegativeTotalBug : Bug
{
public override string Title => "Orders crash when total is negative";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "qa-team";
public override BugSeverity Severity => BugSeverity.Critical;
public abstract AcceptanceCriterionResult NegativeTotalIsRejected(
decimal totalAmount);
}namespace MyApp.Requirements.Bugs;
public abstract record OrderNegativeTotalBug : Bug
{
public override string Title => "Orders crash when total is negative";
public override RequirementPriority Priority => RequirementPriority.Critical;
public override string Owner => "qa-team";
public override BugSeverity Severity => BugSeverity.Critical;
public abstract AcceptanceCriterionResult NegativeTotalIsRejected(
decimal totalAmount);
}What makes this work:
UserRolesFeatureis a type.typeof(UserRolesFeature)works everywhere -- in attributes, in generics, in reflection, in source generators.- Each AC is an abstract method with a typed signature. The parameters document exactly what the AC needs. The return type enforces verifiability.
- The generic constraint
Feature<PlatformScalabilityEpic>creates a compile-time hierarchy. You cannot accidentally parent a Feature under a Story. - Metadata (Title, Priority, Owner) is overridden properties, not string attributes on marker classes.
- No string IDs anywhere. The type IS the identifier.
Layer 1 Generates Navigable Artifacts
Layer 1 includes a Roslyn source generator that scans all types inheriting RequirementMetadata and produces attributes used by every subsequent layer as clickable glue:
// Generated by Requirements source generator
/// <summary>
/// Links any type, interface, or method to a requirement.
/// Ctrl+Click on typeof(Feature) in IDE jumps to the requirement definition.
/// "Find All References" on Feature shows specs, implementations, and tests.
/// </summary>
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method,
AllowMultiple = true)]
public sealed class ForRequirementAttribute : Attribute
{
public Type RequirementType { get; }
public string? AcceptanceCriterion { get; }
public ForRequirementAttribute(Type requirementType, string? acceptanceCriterion = null)
{
RequirementType = requirementType;
AcceptanceCriterion = acceptanceCriterion;
}
}
/// <summary>
/// Links a test method to a specific acceptance criterion.
/// The AC is identified by nameof(Feature.ACMethod) -- compiler-checked.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public sealed class VerifiesAttribute : Attribute
{
public Type RequirementType { get; }
public string AcceptanceCriterionName { get; }
public VerifiesAttribute(Type requirementType, string acceptanceCriterionName)
{
RequirementType = requirementType;
AcceptanceCriterionName = acceptanceCriterionName;
}
}
/// <summary>
/// Links a test class to a requirement type.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class TestsForAttribute : Attribute
{
public Type RequirementType { get; }
public TestsForAttribute(Type requirementType) => RequirementType = requirementType;
}// Generated by Requirements source generator
/// <summary>
/// Links any type, interface, or method to a requirement.
/// Ctrl+Click on typeof(Feature) in IDE jumps to the requirement definition.
/// "Find All References" on Feature shows specs, implementations, and tests.
/// </summary>
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method,
AllowMultiple = true)]
public sealed class ForRequirementAttribute : Attribute
{
public Type RequirementType { get; }
public string? AcceptanceCriterion { get; }
public ForRequirementAttribute(Type requirementType, string? acceptanceCriterion = null)
{
RequirementType = requirementType;
AcceptanceCriterion = acceptanceCriterion;
}
}
/// <summary>
/// Links a test method to a specific acceptance criterion.
/// The AC is identified by nameof(Feature.ACMethod) -- compiler-checked.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public sealed class VerifiesAttribute : Attribute
{
public Type RequirementType { get; }
public string AcceptanceCriterionName { get; }
public VerifiesAttribute(Type requirementType, string acceptanceCriterionName)
{
RequirementType = requirementType;
AcceptanceCriterionName = acceptanceCriterionName;
}
}
/// <summary>
/// Links a test class to a requirement type.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class TestsForAttribute : Attribute
{
public Type RequirementType { get; }
public TestsForAttribute(Type requirementType) => RequirementType = requirementType;
}The generator also produces a requirement registry:
// Generated: RequirementRegistry.g.cs
public static class RequirementRegistry
{
public static IReadOnlyDictionary<Type, RequirementInfo> All { get; } =
new Dictionary<Type, RequirementInfo>
{
[typeof(PlatformScalabilityEpic)] = new(
Type: typeof(PlatformScalabilityEpic),
Kind: RequirementKind.Epic,
Title: "Multi-tenant platform enablement",
Parent: null,
AcceptanceCriteria: Array.Empty<string>()),
[typeof(UserRolesFeature)] = new(
Type: typeof(UserRolesFeature),
Kind: RequirementKind.Feature,
Title: "User roles and permissions",
Parent: typeof(PlatformScalabilityEpic),
AcceptanceCriteria: new[]
{
nameof(UserRolesFeature.AdminCanAssignRoles),
nameof(UserRolesFeature.ViewerHasReadOnlyAccess),
nameof(UserRolesFeature.RoleChangeTakesEffectImmediately)
}),
};
}
public record RequirementInfo(
Type Type, RequirementKind Kind, string Title,
Type? Parent, string[] AcceptanceCriteria);
public enum RequirementKind { Epic, Feature, Story, Task, Bug }// Generated: RequirementRegistry.g.cs
public static class RequirementRegistry
{
public static IReadOnlyDictionary<Type, RequirementInfo> All { get; } =
new Dictionary<Type, RequirementInfo>
{
[typeof(PlatformScalabilityEpic)] = new(
Type: typeof(PlatformScalabilityEpic),
Kind: RequirementKind.Epic,
Title: "Multi-tenant platform enablement",
Parent: null,
AcceptanceCriteria: Array.Empty<string>()),
[typeof(UserRolesFeature)] = new(
Type: typeof(UserRolesFeature),
Kind: RequirementKind.Feature,
Title: "User roles and permissions",
Parent: typeof(PlatformScalabilityEpic),
AcceptanceCriteria: new[]
{
nameof(UserRolesFeature.AdminCanAssignRoles),
nameof(UserRolesFeature.ViewerHasReadOnlyAccess),
nameof(UserRolesFeature.RoleChangeTakesEffectImmediately)
}),
};
}
public record RequirementInfo(
Type Type, RequirementKind Kind, string Title,
Type? Parent, string[] AcceptanceCriteria);
public enum RequirementKind { Epic, Feature, Story, Task, Bug }