πŸ’‘ 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