Overview
Standardise mapping between DTOs, domain models, and persistence models using source-generated mappers. Mappings live at explicit boundaries (API, infrastructure) with deterministic, testable conversion paths and no runtime reflection magic.
When to Use
- Mapping between API request/response DTOs and domain models
- Converting domain entities to/from persistence models
- Handling integration event contracts at service boundaries
- Reviewing PRs that introduce new mapping logic
- Refactoring existing reflection-based mappers to source-generated alternatives
Core Workflow
- Identify mapping boundaries: Locate where DTOs cross into domain or persistence layers
- Choose source-generated mapper: Select Mapperly or similar compile-time mapper
- Create explicit mappers: Implement mapper classes at boundary projects (API, Infrastructure)
- Handle typed IDs explicitly: Map strongly typed IDs and value objects with explicit conversions
- Apply DI gating rules: Use static mappers unless mapper has injected dependencies
- Add mapping tests: Include round-trip tests for critical boundary conversions
- Verify side-effect free: Ensure mappings have no side effects or external calls
Core
Defaults
- Prefer open-source, source-generated mapping tools.
- Mappings live at boundaries (API project, Infrastructure project), not scattered across the domain.
- Typed IDs and value objects must be mapped explicitly (see
dotnet-domain-primitives).
Anti-patterns to avoid
- Reflection-based "magic mapping" that fails at runtime.
- Mapping logic embedded inside domain entities.
- Implicit conversions that obscure boundary crossings.
Review rules
- New DTO/domain/persistence types must have deterministic, testable mapping paths.
- Mapping must be side-effect free.
Load: examples
- API: map request DTO -> command with explicit typed ID parsing/creation.
- Persistence: map EF entity -> domain aggregate using explicit value object construction.
- Integration: map external event contract -> internal event with boundary validations.
Load: advanced
Projections / query scenarios
- Prefer projection mappings that avoid materialising large object graphs when not needed.
- Keep read models separate where pragmatic.
Error strategy
- Invalid boundary inputs fail fast at the boundary (validation).
- Domain invariants enforced by constructors/factories.
Load: enforcement
Review heuristic: mapper DI gating
If a mapper is injected into a constructor (controller/handler/service), reviewers must check whether the mapper implementation itself has injected dependencies.
If the mapper has no injected dependencies (pure/stateless mapping), require refactor to a static mapper and remove DI registration.
Injection is permitted only when mapping depends on external collaborators and those collaborators are injected into the mapper.
"Inject for testability" is not sufficient when mapping is deterministic; test the mapper directly.
Reject PRs introducing runtime reflection mapping unless justified per
dotnet-source-generation-first.Require mapping tests for critical boundary conversions (typed IDs and value objects).
Load: testing
Mapping Unit Test Template
public class OrderMapperTests
{
[Fact]
public void ToEntity_WithValidDto_MapsAllProperties()
{
// Arrange
var dto = new CreateOrderRequest
{
CustomerId = "cust-123",
Amount = 99.99m,
Currency = "GBP"
};
// Act
var entity = OrderMapper.ToEntity(dto);
// Assert
entity.CustomerId.Value.Should().Be("cust-123");
entity.Amount.Value.Should().Be(99.99m);
entity.Amount.Currency.Should().Be(Currency.GBP);
}
[Fact]
public void ToDto_RoundTrip_PreservesData()
{
// Arrange
var original = new Order(
CustomerId.From("cust-123"),
Money.From(99.99m, Currency.GBP));
// Act
var dto = OrderMapper.ToDto(original);
var roundTripped = OrderMapper.ToEntity(dto);
// Assert
roundTripped.Should().BeEquivalentTo(original);
}
}
DI Gating Checklist
When reviewing mappers registered in DI:
- Does the mapper have zero injected dependencies?
- YES: Refactor to static mapper, remove DI registration
- NO: Proceed to next check
- Are all injected dependencies used during mapping?
- NO: Remove unused dependencies
- YES: Proceed
- Is injection for "testability only"?
- YES: Reject - test static mappers directly
- NO: Injection acceptable
Red Flags - STOP
These statements indicate mapping anti-patterns:
| Thought | Reality |
|---|---|
| "AutoMapper conventions will handle it" | Explicit mapping prevents runtime surprises; be deliberate |
| "Mapping logic belongs in the entity" | Keep entities clean; mappers handle boundary concerns |
| "Inject the mapper for testability" | Pure mappers don't need DI; test static mappers directly |
| "Runtime reflection is fine" | Source-generated mappers are faster and fail at compile time |
| "Implicit conversions are convenient" | Explicit conversions make boundary crossings visible |
| "Round-trip tests aren't worth it" | Round-trip tests catch subtle mapping bugs; always include them |