Mapping between layers converts one layer's model into another's: request to command, command to entity, entity to response. Most of that code shouldn't exist at all. EF Core projections eliminate the read-side mapping entirely, and simple extension methods handle the rest. Here are the five approaches I see in .NET projects, when each one makes sense, and the two I'd actually use.
Why Mapping Between Layers Exists
In Clean Architecture, each layer has its own models:
- Domain layer - Entities and Value Objects (business rules)
- Application layer - Commands, Queries, DTOs (use case contracts)
- Infrastructure layer - Persistence models, configuration entities
- Presentation layer - API request/response models
These models look similar but serve different purposes. You need mapping to convert between them.
An HTTP request arrives as a PlaceOrderRequest. You map it to a PlaceOrderCommand. The handler creates an Order entity. You return an OrderResponse to the client.
That's three mappings in one request flow.
When Is Mapping Worth It?
Map when:
- The API contract differs from the internal model (different field names, flattened structures)
- You want to hide internal implementation details from API consumers
- Domain entities expose behaviors you don't want serialized
- You need to return computed or aggregated data that doesn't exist on the entity
Don't map when:
- The source and target are structurally identical
- Adding a mapping layer provides zero value beyond "architecture says so"
Pragmatism matters. A 1:1 mapping between PlaceOrderRequest and PlaceOrderCommand with identical fields is ceremony, not architecture.
Approach 1: Manual Mapping
The simplest option. Extension methods or static factory methods:
public static class OrderMappings
{
public static PlaceOrderCommand ToCommand(this PlaceOrderRequest request)
{
return new PlaceOrderCommand(
request.CustomerId,
request.Items.Select(i => new OrderItemDto(
i.ProductId,
i.Quantity)).ToList());
}
public static OrderResponse ToResponse(this Order order)
{
return new OrderResponse(
order.Id,
order.Customer.Name,
order.TotalAmount.Amount,
order.Status.Name,
order.CreatedAt);
}
}
Usage:
app.MapPost("/api/orders", async (
PlaceOrderRequest request,
ICommandHandler<PlaceOrderCommand, Guid> handler,
CancellationToken cancellationToken) =>
{
var command = request.ToCommand();
var result = await handler.Handle(command, cancellationToken);
return result.IsSuccess
? Results.Created($"/api/orders/{result.Value}", result.Value)
: result.ToProblemDetails();
});
Pros: Simple, explicit, easy to debug, no magic. Cons: Repetitive for large models, manual maintenance when models change.
Approach 2: Projection in Queries (CQRS)
For read operations, skip mapping entirely. Project directly from the database into your response model:
public class GetOrderByIdQueryHandler : IQueryHandler<GetOrderByIdQuery, OrderResponse>
{
private readonly IApplicationDbContext _dbContext;
public GetOrderByIdQueryHandler(IApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Result<OrderResponse>> Handle(
GetOrderByIdQuery query,
CancellationToken cancellationToken)
{
var order = await _dbContext.Orders
.Where(o => o.Id == query.OrderId)
.Select(o => new OrderResponse(
o.Id,
o.Customer.Name,
o.TotalAmount.Amount,
o.Status.Name,
o.CreatedAt))
.FirstOrDefaultAsync(cancellationToken);
if (order is null)
{
return Result.Failure<OrderResponse>(OrderErrors.NotFound(query.OrderId));
}
return order;
}
}
This is the CQRS pattern in action. No domain entity loaded, no mapping needed. EF Core generates an efficient SQL query that returns only the columns you need.
The handler depends on an IApplicationDbContext abstraction rather than the concrete DbContext, so the Application layer never references Infrastructure directly.
This is my preferred approach for queries. It's fast, clean, and eliminating the mapping layer eliminates a whole category of bugs.
Approach 3: Constructor Mapping on DTOs
Let the DTO/response model accept the entity in its constructor:
public sealed record OrderResponse
{
public OrderResponse(Order order)
{
Id = order.Id;
CustomerName = order.Customer.Name;
TotalAmount = order.TotalAmount.Amount;
Status = order.Status.Name;
CreatedAt = order.CreatedAt;
}
public Guid Id { get; init; }
public string CustomerName { get; init; }
public decimal TotalAmount { get; init; }
public string Status { get; init; }
public DateTime CreatedAt { get; init; }
}
Pros: Self-contained, mapping logic lives near the data. Cons: Creates a dependency from the response model to the domain entity. If your response models are in the Presentation layer, this couples Presentation to Domain - which some teams want to avoid.
Approach 4: AutoMapper
AutoMapper handles mapping by convention:
public class OrderProfile : Profile
{
public OrderProfile()
{
CreateMap<Order, OrderResponse>()
.ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src => src.Customer.Name))
.ForMember(dest => dest.TotalAmount, opt => opt.MapFrom(src => src.TotalAmount.Amount))
.ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.Status.Name));
}
}
var response = _mapper.Map<OrderResponse>(order);
Pros: Reduces boilerplate for large models, convention-based for simple mappings. Cons: Magic behavior, runtime errors instead of compile-time errors, hard to debug, performance overhead, easy to misconfigure.
I generally avoid AutoMapper. The magic it provides isn't worth the debugging cost. When a mapping breaks, you're reading AutoMapper source code instead of a simple ToResponse() method.
There's also a licensing angle now: AutoMapper went commercial alongside MediatR. If you were on the fence, that's one more reason to prefer boring, dependency-free mapping code.
Approach 5: Mapster
Mapster is a faster alternative to AutoMapper with code generation support:
var response = order.Adapt<OrderResponse>();
Or with code generation for compile-time safety:
[AdaptFrom(typeof(Order))]
public sealed record OrderResponse(
Guid Id,
string CustomerName,
decimal TotalAmount,
string Status,
DateTime CreatedAt);
Pros: Better performance than AutoMapper, code generation option. Cons: Still involves magic, still another dependency.
My Recommendation
- Use projections for queries - don't load entities just to map them to DTOs
- Use manual mapping for commands - extension methods are simple and explicit
- Skip mapping when models are identical - don't create a
PlaceOrderCommandthat's identical toPlaceOrderRequest - Avoid AutoMapper - the convenience doesn't justify the debugging cost
Here's the practical flow:
How Many Models Do You Actually Need?
Not every layer needs its own model. A pragmatic approach:
On the write side (commands):
- Presentation: a request model
- Application: a command (which can reuse the request type when the shapes match)
- Domain: the entity
On the read side (queries):
- Application: a response DTO
- Infrastructure: nothing - the EF Core projection produces the DTO directly
- Domain: not involved; no entity is loaded
For simple CRUD, the request model might be the command. The response DTO might be the projection result. Two models instead of five.
Going too far in the other direction - a fresh DTO and mapper at every boundary - is what I call mapping mania in my Clean Architecture anti-patterns article.
Summary
Mapping between layers is a means, not an end. Map when it provides value - contract independence, data transformation, security (hiding internal fields). Skip it when it's just ceremony.
Projections eliminate the most common mapping. Manual extension methods handle the rest. You rarely need a mapping framework.
Thanks for reading, and stay awesome!
Frequently Asked Questions
Do I need AutoMapper in Clean Architecture?
No. Projections handle most read-side mapping, and simple extension methods handle the rest. AutoMapper adds runtime magic, harder debugging, and (since it went commercial) licensing considerations that rarely pay for the boilerplate saved.
Should every layer have its own model?
No. Map when the models genuinely differ or when you need to hide internal details. A command that is field-for-field identical to the API request is ceremony; many teams reuse the same type for both.
How do you avoid mapping for queries?
Project directly from the database into the response DTO using Select in EF Core. No entity is loaded and no mapper runs, and the generated SQL fetches only the columns the response needs.
Is it bad for response models to reference domain entities?
It creates a compile-time dependency from the response model to the domain. That is acceptable if the response lives in the application layer, but it couples presentation to domain if the model lives there. Manual mapping methods avoid the issue.
Is Mapster better than AutoMapper?
Mapster is faster and offers source generation for compile-time mapping, which removes much of the runtime magic. But for most Clean Architecture solutions, projections plus manual mapping methods make a mapping library unnecessary.



