The MappingExtensions class now provides generic mapping methods that can map any domain model to any DTO type, making the mapping logic reusable across the entire application.
Maps a single domain model to a DTO using a provided mapping function.
Signature:
public static TDestination MapTo<TSource, TDestination>(
this TSource source,
Func<TSource, TDestination> mapper)
where TSource : class
where TDestination : classUsage Examples:
// Map an Order to OrderResponseDto inline
var orderDto = order.MapTo<Order, OrderResponseDto>(o => new OrderResponseDto
{
Id = o.Id,
CustomerName = o.CustomerName,
ProductName = o.ProductName,
Quantity = o.Quantity,
TotalAmount = o.TotalAmount,
Status = o.Status.ToString(),
CreatedAt = o.CreatedAt
});// Define a static mapper function
public static OrderResponseDto OrderMapper(Order order)
{
return new OrderResponseDto
{
Id = order.Id,
CustomerName = order.CustomerName,
// ... other properties
};
}
// Use the mapper
var orderDto = order.MapTo(OrderMapper);// Map to a summary DTO
var summary = order.MapTo<Order, OrderSummaryDto>(o => new OrderSummaryDto
{
OrderId = o.Id,
Total = o.TotalAmount,
Status = o.Status.ToString()
});
// Map to a detailed DTO with additional info
var detailed = order.MapTo<Order, OrderDetailedDto>(o => new OrderDetailedDto
{
Id = o.Id,
CustomerName = o.CustomerName,
ProductName = o.ProductName,
Quantity = o.Quantity,
TotalAmount = o.TotalAmount,
Status = o.Status.ToString(),
CreatedAt = o.CreatedAt,
// Additional calculated fields
IsExpedited = o.TotalAmount > 1000,
EstimatedDelivery = DateTime.UtcNow.AddDays(3)
});Maps a collection of domain models to a collection of DTOs.
Signature:
public static IEnumerable<TDestination> MapToList<TSource, TDestination>(
this IEnumerable<TSource> source,
Func<TSource, TDestination> mapper)
where TSource : class
where TDestination : classUsage Examples:
// Map a list of orders to DTOs
List<Order> orders = GetOrders();
var orderDtos = orders.MapToList<Order, OrderResponseDto>(o => new OrderResponseDto
{
Id = o.Id,
CustomerName = o.CustomerName,
ProductName = o.ProductName,
Quantity = o.Quantity,
TotalAmount = o.TotalAmount,
Status = o.Status.ToString(),
CreatedAt = o.CreatedAt
});
// Convert to List if needed
var dtoList = orderDtos.ToList();// Filter and map in one expression
var recentOrders = orders
.Where(o => o.CreatedAt > DateTime.UtcNow.AddDays(-7))
.MapToList<Order, OrderResponseDto>(o => new OrderResponseDto
{
Id = o.Id,
CustomerName = o.CustomerName,
// ... other properties
})
.ToList();// Define a reusable mapper
Func<Order, OrderResponseDto> orderMapper = o => new OrderResponseDto
{
Id = o.Id,
CustomerName = o.CustomerName,
ProductName = o.ProductName,
Quantity = o.Quantity,
TotalAmount = o.TotalAmount,
Status = o.Status.ToString(),
CreatedAt = o.CreatedAt
};
// Use it for multiple collections
var todayOrders = todaysOrders.MapToList(orderMapper).ToList();
var weekOrders = weeklyOrders.MapToList(orderMapper).ToList();For convenience, Order-specific mapping methods are still available:
Recommended for Order to OrderResponseDto mapping.
// Simple, clean syntax
var dto = order.ToResponseDto();Use when you need a Func<Order, OrderResponseDto> reference.
// Pass as a function reference
var dtos = orders.MapToList(MappingExtensions.ToOrderResponseDto);[HttpGet]
public async Task<ActionResult<ApiResponse<List<OrderResponseDto>>>> GetOrders()
{
try
{
var orders = await _orderRepository.GetAllAsync();
// Option 1: Using specific method
var orderDtos = orders.Select(o => o.ToResponseDto()).ToList();
// Option 2: Using generic method
var orderDtos = orders.MapToList<Order, OrderResponseDto>(o => o.ToResponseDto()).ToList();
// Option 3: Using static mapper with generic method
var orderDtos = orders.MapToList(MappingExtensions.ToOrderResponseDto).ToList();
var response = ApiResponse<List<OrderResponseDto>>.SuccessResponse(
orderDtos,
"Orders retrieved successfully");
return Ok(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving orders");
var errorResponse = ApiResponse<List<OrderResponseDto>>.FailureResponse(
"An error occurred while retrieving orders",
ex.Message);
return StatusCode(500, errorResponse);
}
}public class OrderService
{
private readonly IOrderRepository _orderRepository;
public async Task<List<OrderSummaryDto>> GetOrderSummariesAsync(string customerId)
{
var orders = await _orderRepository.GetByCustomerIdAsync(customerId);
// Map to summary DTO with calculated fields
return orders.MapToList<Order, OrderSummaryDto>(o => new OrderSummaryDto
{
OrderId = o.Id,
ProductName = o.ProductName,
Total = o.TotalAmount,
Status = o.Status.ToString(),
DaysOld = (DateTime.UtcNow - o.CreatedAt).Days,
IsRecent = (DateTime.UtcNow - o.CreatedAt).Days <= 7
}).ToList();
}
}// Map to a DTO that combines data from multiple sources
public OrderDetailedDto MapToDetailedDto(Order order, List<OrderItem> items, Customer customer)
{
return order.MapTo<Order, OrderDetailedDto>(o => new OrderDetailedDto
{
// Order properties
Id = o.Id,
CustomerName = o.CustomerName,
ProductName = o.ProductName,
Quantity = o.Quantity,
TotalAmount = o.TotalAmount,
Status = o.Status.ToString(),
CreatedAt = o.CreatedAt,
// Additional data from other sources
Items = items.Select(i => new OrderItemDto
{
ProductId = i.ProductId,
ProductName = i.ProductName,
Price = i.Price
}).ToList(),
CustomerEmail = customer.Email,
CustomerPhone = customer.Phone,
// Calculated fields
EstimatedDelivery = CalculateDeliveryDate(o),
IsExpedited = o.TotalAmount > 1000
});
}- One method works for all domain model ? DTO mappings
- No need to create specific extension methods for each type
- Inline mapping for simple cases
- Reusable mapper functions for complex scenarios
- Easy to add calculated fields
- Generic constraints ensure only reference types
- Compile-time checking of mappings
- IntelliSense support
- Built-in null checks using
ArgumentNullException.ThrowIfNull - Prevents null reference exceptions
- Clear, declarative mapping logic
- Easy to modify mappings in one place
- Self-documenting code
| Scenario | Recommended Method | Example |
|---|---|---|
| Single Order mapping | ToResponseDto() |
var dto = order.ToResponseDto(); |
| List of Orders | Select + ToResponseDto() |
orders.Select(o => o.ToResponseDto()) |
| Custom DTO | MapTo<T, T> inline |
order.MapTo<Order, CustomDto>(...) |
| Reusable mapper | MapTo + static method |
orders.MapToList(MyMapper) |
| Collection mapping | MapToList<T, T> |
orders.MapToList<Order, Dto>(...) |
| Complex mapping | MapTo with function |
order.MapTo(o => new Dto { ... }) |
- Use
ToResponseDto()for simple Order to OrderResponseDto mappings - Use
MapTofor custom or one-off mappings - Use
MapToListwhen mapping collections - Create static mapper functions for reusable complex mappings
- Add null checks in custom mapper functions
- Mix mapping logic with business logic
- Create mapper functions inside loops
- Use reflection-based mapping (performance cost)
- Forget to handle null values in custom mappers
// No function allocation overhead
var dto = order.ToResponseDto();// Function pointer, minimal overhead
var dtos = orders.MapToList(MappingExtensions.ToOrderResponseDto);// Lambda allocation per call
var dtos = orders.MapToList<Order, OrderResponseDto>(o => new OrderResponseDto { ... });Recommendation: For hot paths (called frequently), use ToResponseDto() or static mapper functions. For cold paths, use inline lambdas for clarity.
public static OrderResponseDto ToResponseDto(this Order order) { ... }
public static CustomerResponseDto ToResponseDto(this Customer customer) { ... }
public static ProductResponseDto ToResponseDto(this Product product) { ... }
// ... one method per type pair// Generic method for any type
public static TDto MapTo<TSource, TDto>(this TSource source, Func<TSource, TDto> mapper) { ... }
// Keep specific methods for convenience
public static OrderResponseDto ToResponseDto(this Order order) { ... }
// Now you can also:
var customDto = order.MapTo<Order, CustomDto>(o => new CustomDto { ... });The generic mapping pattern opens doors for:
-
AutoMapper Integration
public static TDto MapTo<TSource, TDto>(this TSource source) where TSource : class where TDto : class { return AutoMapper.Map<TDto>(source); }
-
Async Mapping (for mapping with database lookups)
public static async Task<TDto> MapToAsync<TSource, TDto>( this TSource source, Func<TSource, Task<TDto>> asyncMapper)
-
Validation During Mapping
public static TDto MapToWithValidation<TSource, TDto>( this TSource source, Func<TSource, TDto> mapper, Action<TDto> validator)
The generic mapping extensions provide:
- ? Flexibility - Map any type to any other type
- ? Reusability - One method for all mappings
- ? Type Safety - Compile-time checking
- ? Performance - Minimal overhead
- ? Simplicity - Clean, declarative syntax
- ? Backward Compatibility - Specific methods still available
Use the generic methods for flexibility and the specific methods for convenience!
Document Version: 1.0
Last Updated: 2025-01-21
Author: Dariem Carlos Macias