MyApp.SharedKernel -- Domain Types Shared Across Layers
Domain types live in their own project -- not in Specifications, not in Domain. Both reference it.
namespace MyApp.SharedKernel;
// Value types from Requirements are reused
using MyApp.Requirements;
public record User(UserId Id, string Name, IReadOnlySet<Role> Roles);
public record Role(RoleId Id, string Name, IReadOnlySet<Permission> Permissions);
public record Resource(ResourceId Id, string Type);
public record Permission(string Name);
public enum Operation { Read, Write, Delete, Admin }
/// <summary>
/// Strongly-typed result for domain operations.
/// Every specification method returns this -- not bool, not void.
/// </summary>
public record Result
{
public bool IsSuccess { get; }
public string? Reason { get; }
private Result(bool success, string? reason) { IsSuccess = success; Reason = reason; }
public static Result Success() => new(true, null);
public static Result Failure(string reason) => new(false, reason);
public static Result Aggregate(params Result[] results) =>
new(results.All(r => r.IsSuccess), null);
}namespace MyApp.SharedKernel;
// Value types from Requirements are reused
using MyApp.Requirements;
public record User(UserId Id, string Name, IReadOnlySet<Role> Roles);
public record Role(RoleId Id, string Name, IReadOnlySet<Permission> Permissions);
public record Resource(ResourceId Id, string Type);
public record Permission(string Name);
public enum Operation { Read, Write, Delete, Admin }
/// <summary>
/// Strongly-typed result for domain operations.
/// Every specification method returns this -- not bool, not void.
/// </summary>
public record Result
{
public bool IsSuccess { get; }
public string? Reason { get; }
private Result(bool success, string? reason) { IsSuccess = success; Reason = reason; }
public static Result Success() => new(true, null);
public static Result Failure(string reason) => new(false, reason);
public static Result Aggregate(params Result[] results) =>
new(results.All(r => r.IsSuccess), null);
}SharedKernel references Requirements (for UserId, RoleId value types). Specifications and Domain both reference SharedKernel. This keeps interfaces and implementations using the same User, Role, Result types without duplication.
Layer 2: MyApp.Specifications -- Contracts the Domain Must Satisfy
This project references MyApp.Requirements and MyApp.SharedKernel. It defines only interfaces -- no entity definitions, no implementations. Every spec interface and method is decorated with [ForRequirement] for IDE navigability.
Specification Interface
namespace MyApp.Specifications;
using MyApp.Requirements.Features;
using MyApp.SharedKernel;
/// <summary>
/// Specification for UserRolesFeature.
/// Any domain service claiming to implement user roles MUST implement this interface.
/// The compiler enforces it -- you cannot compile without satisfying every AC.
/// </summary>
[ForRequirement(typeof(UserRolesFeature))]
public interface IUserRolesSpec
{
[ForRequirement(typeof(UserRolesFeature), nameof(UserRolesFeature.AdminCanAssignRoles))]
Result AssignRole(User actingUser, User targetUser, Role role);
[ForRequirement(typeof(UserRolesFeature), nameof(UserRolesFeature.ViewerHasReadOnlyAccess))]
Result EnforceReadOnlyAccess(User viewer, Resource resource, Operation operation);
[ForRequirement(typeof(UserRolesFeature), nameof(UserRolesFeature.RoleChangeTakesEffectImmediately))]
Result VerifyImmediateRoleEffect(User user, Role previousRole, Role newRole);
}namespace MyApp.Specifications;
using MyApp.Requirements.Features;
using MyApp.SharedKernel;
/// <summary>
/// Specification for UserRolesFeature.
/// Any domain service claiming to implement user roles MUST implement this interface.
/// The compiler enforces it -- you cannot compile without satisfying every AC.
/// </summary>
[ForRequirement(typeof(UserRolesFeature))]
public interface IUserRolesSpec
{
[ForRequirement(typeof(UserRolesFeature), nameof(UserRolesFeature.AdminCanAssignRoles))]
Result AssignRole(User actingUser, User targetUser, Role role);
[ForRequirement(typeof(UserRolesFeature), nameof(UserRolesFeature.ViewerHasReadOnlyAccess))]
Result EnforceReadOnlyAccess(User viewer, Resource resource, Operation operation);
[ForRequirement(typeof(UserRolesFeature), nameof(UserRolesFeature.RoleChangeTakesEffectImmediately))]
Result VerifyImmediateRoleEffect(User user, Role previousRole, Role newRole);
}In the IDE: right-click typeof(UserRolesFeature) in the attribute > "Go To Definition" > jumps to the abstract feature with its AC methods. Right-click nameof(UserRolesFeature.AdminCanAssignRoles) > jumps directly to that AC method signature.
Validator Bridge -- AC Evaluation at Runtime
The bridge connects abstract UserRolesFeature AC methods to the spec interface. In normal production flow, code calls IUserRolesSpec directly. For compliance-sensitive operations (audit, regulatory), the validator bridge evaluates ACs explicitly:
[ForRequirement(typeof(UserRolesFeature))]
public record UserRolesValidator : UserRolesFeature
{
private readonly IUserRolesSpec _spec;
public UserRolesValidator(IUserRolesSpec spec) => _spec = spec;
public override AcceptanceCriterionResult AdminCanAssignRoles(
UserId actingUser, UserId targetUser, RoleId role)
{
var result = _spec.AssignRole(
new User(actingUser, "acting", new HashSet<Role>()),
new User(targetUser, "target", new HashSet<Role>()),
new Role(role, role.Value, new HashSet<Permission>()));
return result.IsSuccess
? AcceptanceCriterionResult.Satisfied()
: AcceptanceCriterionResult.Failed(result.Reason!);
}
public override AcceptanceCriterionResult ViewerHasReadOnlyAccess(
UserId viewer, ResourceId resource)
{
var result = _spec.EnforceReadOnlyAccess(
new User(viewer, "viewer", new HashSet<Role>()),
new Resource(resource, "generic"),
Operation.Write);
return !result.IsSuccess
? AcceptanceCriterionResult.Satisfied()
: AcceptanceCriterionResult.Failed("Viewer was allowed to write");
}
public override AcceptanceCriterionResult RoleChangeTakesEffectImmediately(
UserId user, RoleId previousRole, RoleId newRole)
{
var result = _spec.VerifyImmediateRoleEffect(
new User(user, "user", new HashSet<Role>()),
new Role(previousRole, previousRole.Value, new HashSet<Permission>()),
new Role(newRole, newRole.Value, new HashSet<Permission>()));
return result.IsSuccess
? AcceptanceCriterionResult.Satisfied()
: AcceptanceCriterionResult.Failed(result.Reason!);
}
}[ForRequirement(typeof(UserRolesFeature))]
public record UserRolesValidator : UserRolesFeature
{
private readonly IUserRolesSpec _spec;
public UserRolesValidator(IUserRolesSpec spec) => _spec = spec;
public override AcceptanceCriterionResult AdminCanAssignRoles(
UserId actingUser, UserId targetUser, RoleId role)
{
var result = _spec.AssignRole(
new User(actingUser, "acting", new HashSet<Role>()),
new User(targetUser, "target", new HashSet<Role>()),
new Role(role, role.Value, new HashSet<Permission>()));
return result.IsSuccess
? AcceptanceCriterionResult.Satisfied()
: AcceptanceCriterionResult.Failed(result.Reason!);
}
public override AcceptanceCriterionResult ViewerHasReadOnlyAccess(
UserId viewer, ResourceId resource)
{
var result = _spec.EnforceReadOnlyAccess(
new User(viewer, "viewer", new HashSet<Role>()),
new Resource(resource, "generic"),
Operation.Write);
return !result.IsSuccess
? AcceptanceCriterionResult.Satisfied()
: AcceptanceCriterionResult.Failed("Viewer was allowed to write");
}
public override AcceptanceCriterionResult RoleChangeTakesEffectImmediately(
UserId user, RoleId previousRole, RoleId newRole)
{
var result = _spec.VerifyImmediateRoleEffect(
new User(user, "user", new HashSet<Role>()),
new Role(previousRole, previousRole.Value, new HashSet<Permission>()),
new Role(newRole, newRole.Value, new HashSet<Permission>()));
return result.IsSuccess
? AcceptanceCriterionResult.Satisfied()
: AcceptanceCriterionResult.Failed(result.Reason!);
}
}Two runtime modes:
// Mode 1: Normal production flow -- call ISpec directly
public class RoleController
{
private readonly IUserRolesSpec _spec;
[HttpPost("users/{targetId}/role")]
public IActionResult AssignRole(Guid targetId, [FromBody] AssignRoleRequest req)
{
var result = _spec.AssignRole(actingUser, targetUser, role);
return result.IsSuccess ? Ok() : BadRequest(result.Reason);
}
}
// Mode 2: Compliance mode -- validate ACs explicitly (audit trail, regulatory)
public class ComplianceService
{
private readonly UserRolesValidator _validator;
public AcceptanceCriterionResult AuditRoleAssignment(
UserId acting, UserId target, RoleId role)
{
var result = _validator.AdminCanAssignRoles(acting, target, role);
_auditLog.Record("FEATURE:UserRoles", "AC:AdminCanAssignRoles", result);
return result;
}
}// Mode 1: Normal production flow -- call ISpec directly
public class RoleController
{
private readonly IUserRolesSpec _spec;
[HttpPost("users/{targetId}/role")]
public IActionResult AssignRole(Guid targetId, [FromBody] AssignRoleRequest req)
{
var result = _spec.AssignRole(actingUser, targetUser, role);
return result.IsSuccess ? Ok() : BadRequest(result.Reason);
}
}
// Mode 2: Compliance mode -- validate ACs explicitly (audit trail, regulatory)
public class ComplianceService
{
private readonly UserRolesValidator _validator;
public AcceptanceCriterionResult AuditRoleAssignment(
UserId acting, UserId target, RoleId role)
{
var result = _validator.AdminCanAssignRoles(acting, target, role);
_auditLog.Record("FEATURE:UserRoles", "AC:AdminCanAssignRoles", result);
return result;
}
}What makes Layer 2 work:
IUserRolesSpecis a compiler-enforced contract. If Domain claims to implement user roles, it must implement every method.- Each method on the interface is linked to its AC via
[ForRequirement(typeof(...), nameof(...))]-- clickable in IDE. - The validator bridge supports both runtime modes: normal production flow (ISpec) and compliance evaluation (validator).
- Specifications defines only interfaces. Domain types come from SharedKernel. No duplication.