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

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);
}

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);
}

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!);
    }
}

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;
    }
}

What makes Layer 2 work:

  1. IUserRolesSpec is a compiler-enforced contract. If Domain claims to implement user roles, it must implement every method.
  2. Each method on the interface is linked to its AC via [ForRequirement(typeof(...), nameof(...))] -- clickable in IDE.
  3. The validator bridge supports both runtime modes: normal production flow (ISpec) and compliance evaluation (validator).
  4. Specifications defines only interfaces. Domain types come from SharedKernel. No duplication.
⬇ Download