AutoMapper in C#
A Practical Guide (with Patterns, Pitfalls, and Pro Tips)
AutoMapper is a small library that removes the boring code you write to copy data from one object to another.
Get Vikas Jindal’s stories in your inbox
Join Medium for free to get updates from this writer.
Most commonly: mapping domain entities to DTOs (and back). Instead of dozens of dto.Name = user.Name; lines, you describe the relationship once and let AutoMapper do the rest—safely and consistently.
When to use it (and when not to)
- Use it when mapping is repetitive and mechanical: Entity → DTO/ViewModel, API request → command object, etc.
- Don’t use it when mapping requires complex domain logic or hidden side effects. Keep domain behavior explicit.
Install
dotnet add package AutoMapper
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjectionThe 5-minute quickstart
1) Create your source and destination models
public class User {
public int Id { get; set; }
public string FullName { get; set; } = "";
public Address Address { get; set; } = new();
}
public class Address {
public string Line1 { get; set; } = "";
public string City { get; set; } = "";
public string Country { get; set; } = "";
}
public class UserDto {
public int Id { get; set; }
public string Name { get; set; } = "";
public string City { get; set; } = "";
}2) Define a Profile
Profiles are classes where you describe mappings once.
using AutoMapper;
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<User, UserDto>()
.ForMember(d => d.Name, m => m.MapFrom(s => s.FullName))
.ForMember(d => d.City, m => m.MapFrom(s => s.Address.City));
}
}3) Register in ASP.NET Core DI
// Program.cs / Startup.cs
builder.Services.AddAutoMapper(typeof(UserProfile)); // or typeof(Program).Assembly4) Inject and map
public class UsersController : ControllerBase
{
private readonly IMapper _mapper;
public UsersController(IMapper mapper) => _mapper = mapper;
[HttpGet("{id}")]
public ActionResult<UserDto> Get(int id)
{
var user = new User {
Id = id,
FullName = "Vikas Jindal",
Address = new Address { City = "Delhi", Country = "India" }
};
var dto = _mapper.Map<UserDto>(user);
return Ok(dto);
}
}Done! No manual copying.
Core concepts you’ll use every day
1) Simple property-to-property mapping
AutoMapper matches by name (case-insensitive) and compatible type. If names differ, use ForMember + MapFrom as shown above.
2) Reverse mapping
If your DTO maps back to the entity:
CreateMap<User, UserDto>()
.ForMember(d => d.Name, m => m.MapFrom(s => s.FullName))
.ReverseMap()
.ForMember(d => d.FullName, m => m.MapFrom(s => s.Name))
.ForMember(d => d.Address, m => m.Ignore()); // maybe you don’t want reverse here3) Flattening and nesting
AutoMapper can flatten nested objects automatically if names align:
// If UserDto had property AddressCity, AutoMapper maps Address.City -> AddressCity
public class UserFlatDto { public string AddressCity { get; set; } = ""; }
CreateMap<User, UserFlatDto>(); // Works by convention4) Collections
CreateMap<User, UserDto>();
var dtos = _mapper.Map<List<UserDto>>(usersList);5) Nulls and defaults
- By default, null sources map to null destinations (for reference types).
- To ignore nulls from the source:
CreateMap<UserUpdateDto, User>()
.ForAllMembers(opt => opt.Condition((src, dest, srcMember) => srcMember != null));Customizing mappings (the good parts)
ForMember, Ignore, MapFrom, Condition
CreateMap<Order, OrderDto>()
.ForMember(d => d.Total, m => m.MapFrom(s => s.Items.Sum(i => i.Price * i.Qty)))
.ForMember(d => d.InternalNotes, m => m.Ignore()) // don’t leak internal fields
.ForMember(d => d.Slug, m => {
m.PreCondition(s => !string.IsNullOrWhiteSpace(s.Title)); // runs before MapFrom
m.MapFrom(s => s.Title.ToLower().Replace(' ', '-'));
})
.ForMember(d => d.Vip, m => m.Condition(s => s.Customer.IsVip)); // conditional setValue converters
For simple type transforms:
public class UnixTimeToDateTimeConverter : IValueConverter<long, DateTime>
{
public DateTime Convert(long source, ResolutionContext context)
=> DateTimeOffset.FromUnixTimeSeconds(source).UtcDateTime;
}
CreateMap<Event, EventDto>()
.ForMember(d => d.When, m => m.ConvertUsing(new UnixTimeToDateTimeConverter(), s => s.WhenUnix));Value resolvers (access multiple source values or services)
public class PriceResolver : IValueResolver<OrderItem, OrderItemDto, decimal>
{
private readonly ICurrencyService _currency;
public PriceResolver(ICurrencyService currency) => _currency = currency;
public decimal Resolve(OrderItem src, OrderItemDto dest, decimal destMember, ResolutionContext ctx)
=> _currency.Convert(src.UnitPrice * src.Qty, src.Currency, "INR");
}
CreateMap<OrderItem, OrderItemDto>()
.ForMember(d => d.PriceInInr, m => m.MapFrom<PriceResolver>());Register
ICurrencyServicein DI; AutoMapper will resolve it.
AfterMap / BeforeMap (post-processing)
CreateMap<User, UserDto>()
.AfterMap((src, dest) => dest.Name = dest.Name.Trim());Global rules
Apply a rule to every member in a map:
CreateMap<Source, Dest>()
.ForAllMembers(m => m.Condition((src, dest, srcMember) => srcMember != null));Inheritance, base maps, and includes
When you have hierarchies:
public class Animal { public string Name { get; set; } = ""; }
public class Cat : Animal { public int Lives { get; set; } }
public class Dog : Animal { public bool GoodBoy { get; set; } }
public class AnimalDto { public string Name { get; set; } = ""; }
public class CatDto : AnimalDto { public int Lives { get; set; } }
public class DogDto : AnimalDto { public bool GoodBoy { get; set; } }
CreateMap<Animal, AnimalDto>()
.Include<Cat, CatDto>()
.Include<Dog, DogDto>();
CreateMap<Cat, CatDto>();
CreateMap<Dog, DogDto>();Or share base rules:
CreateMap<Base, BaseDto>().ForMember(...);
CreateMap<Child, ChildDto>().IncludeBase<Base, BaseDto>();Mapping enums, strings, and nullable types
CreateMap<Status, string>().ConvertUsing(s => s.ToString());
// or strict mapping:
CreateMap<string, Status>().ConvertUsing(src =>
Enum.TryParse<Status>(src, true, out var value) ? value : Status.Unknown);Nullable handling is automatic when types match (int? → int?). To map int? to int with default:
CreateMap<Source, Dest>()
.ForMember(d => d.Count, m => m.MapFrom(s => s.Count ?? 0));EF Core + AutoMapper: ProjectTo vs Map
One of AutoMapper’s superpowers is query projection with ProjectTo, which translates your mapping into SQL via EF Core. This is far more efficient than pulling full entities into memory and then mapping.
using AutoMapper.QueryableExtensions;
// registration
builder.Services.AddAutoMapper(typeof(Program).Assembly);
// usage (inside EF Core query)
var users = await _dbContext.Users
.Where(u => u.IsActive)
.ProjectTo<UserDto>(_mapper.ConfigurationProvider) // translates to SELECT only needed cols
.ToListAsync();When to use what
ProjectTo<T>(): Use in LINQ to Entities to let EF build efficient SQL._mapper.Map<T>(entity): Use on in-memory objects (after materialization).
Common gotcha: Don’t call _mapper.Map inside a LINQ to Entities query—it cannot translate; call ProjectTo instead.
Organizing profiles in real apps
- Create one profile per bounded context or feature (e.g.,
UsersProfile,OrdersProfile). - Group maps by direction (Entity→DTO vs DTO→Entity).
- Keep profiles small; prefer expression-based
MapFrom(not methods with side effects) soProjectToworks.
Example:
public class OrdersProfile : Profile
{
public OrdersProfile()
{
CreateMap<Order, OrderListItemDto>()
.ForMember(d => d.Total, m => m.MapFrom(s => s.Items.Sum(i => i.Price * i.Qty)));
CreateMap<CreateOrderRequest, Order>()
.ForMember(d => d.Id, m => m.Ignore())
.ForMember(d => d.Items, m => m.MapFrom(s => s.Items));
CreateMap<CreateOrderItemRequest, OrderItem>();
}
}Validating your configuration (do this!)
Add a quick unit test to ensure all maps are valid:
using AutoMapper;
using Xunit;
public class MappingTests
{
[Fact]
public void Profiles_AreValid()
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddMaps(typeof(UserProfile).Assembly);
});
config.AssertConfigurationIsValid();
}
}This catches missing members, incompatible types, or broken MapFrom expressions early.
Common errors & fixes
- “Missing type map configuration or unsupported mapping.”
You tried to map types without a defined map. CreateCreateMap<A,B>()for that pair (and for nested or collection element types if needed). ProjectTothrows/not translating custom logic.
You used a non-translatable method insideMapFrom. Replace it with an expression using only EF-supported operations.- Silent overwrites on updates.
For PATCH/partial updates, addCondition(srcMember != null)to avoid nulls overwriting existing values. - Circular references or deep graphs.
Map only the fields you need in DTOs. Use projection, not full graphs. - Performance issues mapping huge lists in memory.
UseProjectToto push work to the database, or batch in pages.
Patterns you’ll reach for
Partial update (ignore nulls)
CreateMap<UserUpdateDto, User>()
.ForAllMembers(o => o.Condition((src, dest, srcMember) => srcMember != null));Strict destination control (avoid accidental leaks)
CreateMap<Order, OrderDto>()
.ForAllOtherMembers(m => m.Ignore()); // then explicitly map only what you needCustom naming conventions
If your source/destination follow different conventions (e.g., snake_case → PascalCase):
var config = new MapperConfiguration(cfg =>
{
cfg.SourceMemberNamingConvention = new LowerUnderscoreNamingConvention();
cfg.DestinationMemberNamingConvention = new PascalCaseNamingConvention();
cfg.AddMaps(typeof(Program).Assembly);
});Map existing destination instance
Useful in update handlers:
_mapper.Map(updateDto, existingEntity); // fills into the existing instanceTesting mappings with sample data
A tiny helper to ensure a map actually runs at runtime:
[Fact]
public void User_Maps_To_UserDto()
{
var config = new MapperConfiguration(cfg => cfg.AddProfile<UserProfile>());
var mapper = config.CreateMapper();
var user = new User { Id = 5, FullName = "Vikas", Address = new Address { City = "Delhi" } };
var dto = mapper.Map<UserDto>(user);
Assert.Equal(5, dto.Id);
Assert.Equal("Vikas", dto.Name);
Assert.Equal("Delhi", dto.City);
}Debugging: see what AutoMapper is doing
Enable internal validation and inspect configuration:
var config = new MapperConfiguration(cfg => cfg.AddMaps(typeof(Program).Assembly));
var maps = config.GetAllTypeMaps(); // inspect in debugger
config.AssertConfigurationIsValid(); // throws with a helpful messageWhen you shouldn’t AutoMap
- Mapping triggers domain rules (e.g., calculating credit limits, applying discounts). Keep that as explicit application service logic.
- You need stepwise domain operations between fields (e.g., updating invariants). AutoMapper is about data shape transformation, not business processes.
Mini cheat-sheet
CreateMap<Src, Dest>()— basic map by name/type..ForMember(d => d.X, m => m.MapFrom(s => ...))— customize a member..ReverseMap()— generate reverse mapping (customize further if names differ)..ForAllMembers(o => o.Condition((s,d,sm) => sm != null))— ignore nulls (partial updates)..Include<TChild, TChildDto>()/.IncludeBase<,>()— inheritance.ProjectTo<TDto>(config)— translate mapping into SQL with EF Core.AssertConfigurationIsValid()— validate at test time.
Final thoughts
AutoMapper shines when your problem is shape transformation, not business logic. Keep maps declarative and expression-based. Use ProjectTo for database efficiency, validate your config in tests, and resist the urge to hide domain rules inside mapping profiles.





