Table of Contents

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):

  1. Fluent override.MapProperty<TDto, TEntity>(dto => dto.Prop, entity => entity.Nested.Prop)
  2. [MapFrom] attribute[MapFrom("Customer.Address.City")] on the DTO property
  3. Exact name matchdto.Name maps to entity.Name
  4. Convention flattening (2 levels)dto.CustomerName maps to entity.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:

  1. Navigation property Customer on the entity
  2. Sub-property Name on Customer

Only 2 levels deepCustomerAddressCity 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));