askill
dotnet-domain-primitives

dotnet-domain-primitivesSafety 90Repository

Prevent primitive obsession by enforcing StronglyTypedIds and value objects in domain models and at boundaries.

0 stars
1.2k downloads
Updated 1/18/2026

Package Files

Loading 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

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

95/100Analyzed 2/10/2026

An exceptional skill document that provides a comprehensive, actionable guide for implementing domain primitives in .NET. It includes clear workflows, before/after code comparisons, integration details for common libraries (EF Core, Dapper), and a validation checklist.

90
100
90
95
95

Metadata

Licenseunknown
Version-
Updated1/18/2026
Publishermcj-coder

Tags

apidatabasegithub-actionsobservabilitytesting