DTO Mapping
MapCrud supports separate Data Transfer Object (DTO) types for POST/PUT request bodies
and GET response bodies. Mapping is performed by ReflectionMapper<TEntity, TDto> by default
and can be replaced with a custom IMapCrudMapper<TEntity, TDto> implementation.
Basic Usage
app.MapCrud<Product>("products", o => o
.WithRequest<CreateProductDto>() // POST/PUT body type
.WithResponse<ProductDto>()); // GET response type
Configuring Request and Response Separately
// Request only (POST/PUT accept CreateDto, but GET returns the entity directly)
app.MapCrud<Order>("orders", o => o.WithRequest<CreateOrderDto>());
// Response only (GET returns ViewDto, POST/PUT accept the entity)
app.MapCrud<User>("users", o => o.WithResponse<UserViewDto>());
ReflectionMapper Property Resolution
ReflectionMapper<TEntity, TDto> resolves each DTO property using a 4-tier strategy (first match wins):
- Fluent override —
.MapProperty<TDto, TEntity>(dto => dto.Prop, entity => entity.Nested.Prop) - [MapFrom] attribute —
[MapFrom("Customer.Address.City")]on the DTO property - Exact name match —
dto.Namemaps toentity.Name - Convention flattening (2 levels) —
dto.CustomerNamemaps toentity.Customer.Name
Example
public class OrderDto
{
public Guid Id { get; set; }
// Tier 3: exact match (Order.Status -> OrderDto.Status)
public string Status { get; set; } = "";
// Tier 4: convention flattening (Order.Customer.Name -> OrderDto.CustomerName)
public string CustomerName { get; set; } = "";
// Tier 2: [MapFrom] attribute (any depth)
[MapFrom("Customer.Address.City")]
public string City { get; set; } = "";
}
Fluent Property Mapping
Use .MapProperty() when the DTO property name does not follow the naming convention
or when you don't own the DTO type:
app.MapCrud<Order>("orders", o => o
.WithResponse<OrderDto>()
.MapProperty<OrderDto, Order>(
dto => dto.BuyerCity,
order => order.Customer.Address.City));
[MapFrom] Attribute
Apply [MapFrom("dotted.path")] directly on DTO properties. Supports unlimited depth:
public class ProductDto
{
[MapFrom("Category.ParentCategory.Name")]
public string CategoryPath { get; set; } = "";
}
Convention-Based Flattening (2 Levels)
For a DTO property named CustomerName, the mapper looks for:
- Navigation property
Customeron the entity - Sub-property
NameonCustomer
Only 2 levels deep — CustomerAddressCity does not auto-flatten to entity.Customer.Address.City.
Use [MapFrom] or .MapProperty() for deeper paths.
Reverse Mapping (DTO to Entity)
For POST requests, the DTO is mapped to a new entity instance. For PUT requests, DTO fields are applied onto the existing entity. Both directions use exact name match only — nested path support is not available in the reverse direction.
Custom Mapper
Implement IMapCrudMapper<TEntity, TDto> to replace ReflectionMapper entirely:
public class ProductMapper : IMapCrudMapper<Product, ProductDto>
{
public ProductDto Map(Product entity) => new() { Id = entity.Id, Name = entity.Name };
public Product Map(ProductDto dto) => new() { Name = dto.Name };
public void Map(ProductDto source, Product target) { target.Name = source.Name; }
}
builder.Services.AddSingleton<IMapCrudMapper<Product, ProductDto>, ProductMapper>();
Hidden Filters
Control which DTO properties are filterable via query string:
// Attribute approach
public class ProductDto
{
[HiddenFilter]
public decimal InternalCost { get; set; }
}
// Fluent approach at registration
app.MapCrud<Product>("products", o => o
.HideFilter<ProductDto>(x => x.InternalCost));
Related Features
- Mapperly Integration — Source-generated mappers replacing ReflectionMapper
- Filtering — Filterability of DTO properties