π‘ AutoMapper Best Practices
βοΈ Configuration Best Practices
β 1. Use Profiles to Organize Mappings
Good:
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<User, UserDTO>();
CreateMap<Address, AddressDTO>();
}
}
public class ProductProfile : Profile
{
public ProductProfile()
{
CreateMap<Product, ProductDTO>();
}
}
Why: Keeps mappings organized by domain, easier to maintain and test.
β 2. Configure AutoMapper Only Once at Startup
Good:
// In Program.cs
builder.Services.AddAutoMapper(typeof(Program).Assembly);
Bad:
// DON'T create mapper in every controller/method
var config = new MapperConfiguration(...);
var mapper = config.CreateMapper();
Why: Configuration is expensive. Do it once, reuse everywhere via DI.
β 3. Inject IMapper, Not MapperConfiguration
Good:
public class MyController : Controller
{
private readonly IMapper _mapper;
public MyController(IMapper mapper)
{
_mapper = mapper;
}
}
Why: IMapper is the correct interface for mapping operations.
β 4. Validate Configuration in Tests
Good:
[Fact]
public void AutoMapper_Configuration_IsValid()
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<UserProfile>();
cfg.AddProfile<ProductProfile>();
});
config.AssertConfigurationIsValid();
}
Why: Catches mapping errors at compile time, not runtime.
πΊοΈ Mapping Best Practices
β 5. Use Convention-Based Mapping When Possible
Good:
// Source and destination have same property names
public class User { public string Name { get; set; } }
public class UserDTO { public string Name { get; set; } }
// Simple mapping
CreateMap<User, UserDTO>();
Why: Less configuration, more maintainable, leverages AutoMapper's strengths.
β 6. Use ForMember for Custom Logic
Good:
CreateMap<User, UserDTO>()
.ForMember(dest => dest.FullName,
opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
.ForMember(dest => dest.Age,
opt => opt.MapFrom<AgeResolver>());
Why: Clear, explicit, and testable custom mapping logic.
β 7. Use Custom Resolvers for Complex Logic
Good:
public class AgeResolver : IValueResolver<User, UserDTO, int>
{
public int Resolve(User source, UserDTO dest, int destMember, ResolutionContext ctx)
{
// Complex calculation logic here
return CalculateAge(source.DateOfBirth);
}
}
// In Profile
.ForMember(dest => dest.Age, opt => opt.MapFrom<AgeResolver>());
Why: Separates complex logic, makes it reusable and testable.
β 8. Map Collections Directly
Good:
var userDTOs = _mapper.Map<List<UserDTO>>(users);
Bad:
// DON'T map one by one
var userDTOs = new List<UserDTO>();
foreach (var user in users)
{
userDTOs.Add(_mapper.Map<UserDTO>(user));
}
Why: More efficient, cleaner code.
β 9. Use ProjectTo for Database Queries
Good:
var userDTOs = await dbContext.Users
.Where(u => u.IsActive)
.ProjectTo<UserDTO>(_mapper.ConfigurationProvider)
.ToListAsync();
Bad:
// DON'T load entire entities then map
var users = await dbContext.Users.Where(u => u.IsActive).ToListAsync();
var userDTOs = _mapper.Map<List<UserDTO>>(users);
Why: ProjectTo translates to SQL, retrieves only needed columns.
β 10. Update Existing Objects When Appropriate
Good:
// Update existing entity (preserves unchanged properties)
var existingUser = await dbContext.Users.FindAsync(id);
_mapper.Map(updateDTO, existingUser);
await dbContext.SaveChangesAsync();
Bad:
// Creates new object, loses tracking
var user = _mapper.Map<User>(updateDTO);
dbContext.Users.Update(user); // Issues with tracking
Why: Preserves entity tracking, handles partial updates correctly.
β‘ Performance Best Practices
β 11. Avoid Mapping in Loops
Good:
var userDTOs = _mapper.Map<List<UserDTO>>(users);
Bad:
foreach (var user in users)
{
var dto = _mapper.Map<UserDTO>(user); // Multiple calls
}
Why: Single collection mapping is more efficient.
β 12. Use ProjectTo for Large Datasets
Good for EF Core:
var products = await dbContext.Products
.ProjectTo<ProductDTO>(_mapper.ConfigurationProvider)
.ToListAsync();
Why: Reduces data transfer from database, faster queries.
β 13. Avoid Complex Logic in MapFrom
Good:
// Use resolver for complex logic
.ForMember(dest => dest.Summary, opt => opt.MapFrom<SummaryResolver>());
Bad:
// Complex inline logic (hard to test/maintain)
.ForMember(dest => dest.Summary, opt => opt.MapFrom(src =>
{
// 50 lines of complex logic here
}));
Why: Resolvers are more maintainable and testable.
ποΈ Architecture Best Practices
β 14. Keep Domain Models Pure
Good:
// Domain Model - business logic only
public class User
{
public int Id { get; set; }
public string Email { get; set; }
// Business methods
public void Activate() { ... }
}
// DTO - data transfer only
public class UserDTO
{
public int Id { get; set; }
public string Email { get; set; }
}
Why: Clear separation of concerns, better architecture.
β 15. Use Different DTOs for Different Purposes
Good:
public class UserReadDTO { /* All readable fields */ }
public class UserCreateDTO { /* Only fields needed for creation */ }
public class UserUpdateDTO { /* Only updatable fields */ }
public class UserListDTO { /* Minimal info for lists */ }
Why: Better security, clearer intent, optimized data transfer.
β 16. Group Profiles by Domain/Feature
Good Structure:
Profiles/
βββ UserProfile.cs // User-related mappings
βββ ProductProfile.cs // Product-related mappings
βββ OrderProfile.cs // Order-related mappings
βββ CommonProfile.cs // Shared/common mappings
Why: Organized, easier to find and maintain mappings.
β οΈ Common Pitfalls to Avoid
β 1. Don't Create Mapper Instance Every Time
// WRONG - creates configuration every call
var config = new MapperConfiguration(...);
var mapper = config.CreateMapper();
var dto = mapper.Map<UserDTO>(user);
β 2. Don't Forget to Call AssertConfigurationIsValid()
Always validate your configuration in tests to catch errors early.
β 3. Don't Use AutoMapper for Everything
Sometimes manual mapping is clearer, especially for very simple or very complex scenarios.
β 4. Don't Map in Both Directions Without ReverseMap()
// WRONG
CreateMap<User, UserDTO>();
CreateMap<UserDTO, User>(); // Duplicate configuration
// RIGHT
CreateMap<User, UserDTO>().ReverseMap();
β 5. Don't Ignore Unmapped Properties Without Reason
If a property isn't mapping, investigate why. Don't just ignore it blindly.
π Quick Reference Checklist
Before Deploying:
- β All mappings are in Profiles
- β Configuration validated with AssertConfigurationIsValid()
- β IMapper injected via DI, not created manually
- β ProjectTo used for database queries
- β Complex logic in custom resolvers, not inline
- β Different DTOs for different operations (Create/Read/Update)
- β Collections mapped directly, not in loops
- β Domain models kept pure, no mapping attributes