agentleFS
martincjarvismartincjarvis/development-skills/skills/dotnet-domain-primitives/SKILL.md

dotnet-domain-primitives

Skill4 starsChanged 10 months agoMITView on GitHub

Use when modelling domain types or designing API/persistence boundaries in .NET - prevent primitive obsession by enforcing strongly-typed IDs and value objects.

The file

We split this skill into the rules it gives an agent

10 rules. About 0 are common, 9 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈9

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Define strongly typed IDs: Create typed ID types using source-generation libraries for entity identifiers

    line 27
  2. Distinctive:

    Define value objects: Create value objects for meaningful primitives with validation (EmailAddress, Money)

    line 28
  3. Distinctive:

    Configure serialization: Add JSON converters, EF Core value converters, and type handlers as needed

    line 30
  4. Distinctive:

    Add boundary tests: Test that boundary conversion works and invalid primitives are rejected

    line 32
  5. Very specific:

    Use value objects for meaningful primitives (e.g., EmailAddress, Money, Percentage, TenantId, CorrelationId).

    line 40
  6. Distinctive:

    Prefer open-source libraries that use source generation for typed IDs.

    line 44
  7. Distinctive:

    Ensure typed ID types are stable for logging/telemetry (string representation).

    line 77
  8. Distinctive:

    Avoid implicit conversions that obscure boundary crossings.

    line 78
  9. Distinctive:

    No type safety between layers

    line 127
  10. Distinctive:

    No domain-layer primitive IDs added.

    line 218

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Overview

Prevent primitive obsession by enforcing strongly typed identifiers and value objects in domain models. Conversions to/from primitives are permitted only at explicit boundaries (persistence, transport, serialization), ensuring type safety and validation throughout the domain layer.

When to Use

  • Designing or reviewing domain models (entities, aggregates, commands/events)
  • Introducing new entity identifiers (CustomerId, OrderId, TenantId, etc.)
  • Working with meaningful primitives that carry validation (EmailAddress, Money, Percentage)
  • Reviewing code where primitive types (Guid, string, int) are used for domain concepts
  • Implementing API boundaries, persistence layers, or serialization

Core Workflow

  1. Identify primitive obsession: Locate places where Guid, string, int, or long represent domain concepts
  2. Define strongly typed IDs: Create typed ID types using source-generation libraries for entity identifiers
  3. Define value objects: Create value objects for meaningful primitives with validation (EmailAddress, Money)
  4. Establish boundary conversions: Map to/from primitives only at explicit boundaries (API, persistence, transport)
  5. Configure serialization: Add JSON converters, EF Core value converters, and type handlers as needed
  6. Update service signatures: Change service methods to accept domain primitives, not raw types
  7. Add boundary tests: Test that boundary conversion works and invalid primitives are rejected

Core

Defaults (non-negotiable)

  • StronglyTypedIds by default for all entity identifiers in domain/application code.
  • No primitive IDs (Guid, int, long, string) in the domain layer.
  • Use value objects for meaningful primitives (e.g., EmailAddress, Money, Percentage, TenantId, CorrelationId).

Preferred approach

  • Prefer open-source libraries that use source generation for typed IDs.
  • Conversions to/from primitives are permitted only at explicit boundaries:
    • persistence adapters,
    • transport adapters (HTTP, messaging),
    • serialization/deserialization.

Review rules

  • New domain types must not introduce primitive ID properties/fields.
  • Mapping layers must map typed IDs explicitly; no "magic" conversions hidden in core domain types.

Load: examples

Strongly typed ID (source generator style)

  • Define a CustomerId type and use it on entities/commands.
  • Map to primitive Guid at the persistence boundary and transport boundary.

Value object boundaries

  • Allow string in DTOs if required by external contracts.
  • Convert to EmailAddress (value object) inside the application layer.

Load: advanced

Integration guidance

  • EF Core: value converters for typed IDs and value objects.
  • System.Text.Json: custom converters where needed for typed IDs.
  • Dapper: type handlers if Dapper is used for read models.

Operational concerns

  • Ensure typed ID types are stable for logging/telemetry (string representation).
  • Avoid implicit conversions that obscure boundary crossings.

API Boundary Mapping & Validation

Before: Primitive Obsession at Boundaries

// API Controller - accepts primitives
[ApiController]
[Route("api/[controller]")]
public class CustomersController
{
    private readonly ICustomerService _service;

    [HttpPost]
    public async Task<IActionResult> CreateCustomer(CreateCustomerRequest request)
    {
        // No type safety - primitives passed directly to service
        var result = await _service.CreateCustomer(request.Id, request.Email);
        return Ok(result);
    }
}

// Service layer - accepts primitives, loses domain context
public class CustomerService
{
    public async Task<CustomerDto> CreateCustomer(string id, string email)
    {
        // Validation scattered across layers
        if (string.IsNullOrWhiteSpace(email))
            throw new ArgumentException("Email required");

        // No connection to domain types
        var customer = new Customer { Id = Guid.Parse(id), Email = email };
        await _repository.AddAsync(customer);
        return new CustomerDto { Id = customer.Id.ToString(), Email = email };
    }
}

// DTO - exposes internal structure
public class CreateCustomerRequest
{
    public string Id { get; set; }
    public string Email { get; set; }
}

Problems:

  • No type safety between layers
  • Validation scattered across concerns
  • Easy to pass invalid primitives

After: Domain Primitives at Boundaries

// Domain types
public partial class CustomerId : IStronglyTypedId<Guid> { }
public partial class EmailAddress : IValueObject<string> { }

// API Controller - explicit boundary conversion
[ApiController]
[Route("api/[controller]")]
public class CustomersController
{
    private readonly ICustomerService _service;
    private readonly ICustomerMapper _mapper;

    [HttpPost]
    public async Task<IActionResult> CreateCustomer(CreateCustomerRequest request)
    {
        // Explicit conversion at boundary
        var customerId = new CustomerId(Guid.Parse(request.Id));
        var email = EmailAddress.Create(request.Email).ThrowIfFailure();

        var result = await _service.CreateCustomer(customerId, email);
        return Ok(_mapper.ToResponse(result));
    }
}

// Service layer - type-safe, domain-focused
public class CustomerService
{
    public async Task<Customer> CreateCustomer(CustomerId id, EmailAddress email)
    {
        // Domain types ensure validity before service runs
        var customer = Customer.Create(id, email).ThrowIfFailure();
        await _repository.AddAsync(customer);
        return customer;
    }
}

// Mapper - explicit conversion layer
public class CustomerMapper
{
    public CustomerResponse ToResponse(Customer customer)
    {
        return new CustomerResponse
        {
            Id = customer.Id.Value.ToString(),  // Explicit back to primitive
            Email = customer.Email.Value        // Explicit back to primitive
        };
    }
}

Benefits:

  • Type safety enforced across layers
  • Validation centralized in domain types
  • Clear boundary crossing
  • Compiler prevents invalid combinations

Validation Steps for Domain Primitive Implementation

  1. API Controllers:
    • DTO properties remain primitives
    • Convert DTOs to domain types immediately upon entry
    • Use mapper/converter class for boundary crossing
  2. Service Layer:
    • Accept domain primitives, not raw types
    • Never accept Guid when CustomerId exists
    • Ensure validation runs before service logic
  3. Mapping & Serialization:
    • JSON serialization handles conversion via custom converters
    • EF Core value converters map domain types ↔ database columns
    • No implicit conversions in constructors
  4. Testing:
    • Unit test boundary conversion in mapper
    • Integration test proves invalid primitives rejected at API
    • Verify domain type validation runs before service

Load: enforcement

Acceptance criteria for PRs

  • New entities/aggregates use typed IDs.
  • No domain-layer primitive IDs added.
  • Boundary conversion is explicit and covered by unit tests.

Red Flags - STOP

These statements indicate primitive obsession patterns:

ThoughtReality
"Guid is fine for identifiers"Primitive IDs lose type safety; use strongly typed IDs
"String is good enough for email"Value objects centralise validation; prevent invalid data
"Implicit conversions are convenient"Implicit conversions obscure boundaries; be explicit
"Domain types add too much ceremony"Source generators eliminate boilerplate; use them
"We'll add types later"Retrofitting types is expensive; start with them
"Validation can happen anywhere"Centralise validation in domain types; single source of truth

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.