This guide shows how Simple.AutoMapper eliminates repetitive mapping code. Each section shows the manual approach ("Without") and the equivalent mapper call ("With").
- Object Mapping
- Collection Mapping
- In-Place Update (PUT)
- Partial Update (PATCH)
- Property Exclusion
- Custom Property Mapping
- Conditional Mapping
- Null Default Values
- Pre/Post Callbacks
- Custom Construction
- Bidirectional Mapping
- Dependency Injection
- Web API Integration
- EF Core Integration
- API Reference
var dto = new UserDto();
dto.Id = user.Id;
dto.FirstName = user.FirstName;
dto.LastName = user.LastName;
dto.Email = user.Email;
dto.BirthDate = user.BirthDate;
dto.Address = new AddressDto();
dto.Address.Street = user.Address.Street;
dto.Address.City = user.Address.City;
dto.Address.ZipCode = user.Address.ZipCode;
dto.Orders = user.Orders.Select(o => new OrderDto
{
OrderId = o.OrderId,
OrderDate = o.OrderDate,
TotalAmount = o.TotalAmount
}).ToList();var dto = Mapper.Map<User, UserDto>(user);One line. Nested objects and collections are mapped recursively.
var dtos = new List<UserDto>();
foreach (var user in users)
{
var dto = new UserDto();
dto.Id = user.Id;
dto.FirstName = user.FirstName;
dto.LastName = user.LastName;
dto.Email = user.Email;
// ... repeat for every property
dtos.Add(dto);
}var dtos = Mapper.Map<User, UserDto>(users);entity.Name = dto.Name;
entity.Description = dto.Description;
entity.Price = dto.Price;
entity.Stock = dto.Stock;
entity.IsActive = dto.IsActive;
entity.UpdatedAt = DateTime.UtcNow;
// Must add a line here every time a new property is added to the entityMapper.Map(dto, entity);
entity.UpdatedAt = DateTime.UtcNow;New properties are automatically included — no code changes needed when the model grows.
This is the most impactful use case. HTTP PATCH requires updating only the fields that were provided (non-null).
if (dto.Name != null) entity.Name = dto.Name;
if (dto.Description != null) entity.Description = dto.Description;
if (dto.Price.HasValue) entity.Price = dto.Price.Value;
if (dto.Stock.HasValue) entity.Stock = dto.Stock.Value;
if (dto.IsActive.HasValue) entity.IsActive = dto.IsActive.Value;
entity.UpdatedAt = DateTime.UtcNow;
// Every new nullable property needs another if-statementMapper.Patch(dto, entity);
entity.UpdatedAt = DateTime.UtcNow;Patch skips null source properties automatically. No if-chains needed.
var dto = new UpdateDto { Name = "New Name", Price = null };
var entity = new Product { Name = "Old", Price = 50m, Stock = 200 };
Mapper.Map(dto, entity);
// Name="New Name", Price=0 (null overwritten to default), Stock=0
Mapper.Patch(dto, entity);
// Name="New Name", Price=50 (preserved), Stock=200 (preserved)var dto = new UserDto();
dto.Id = user.Id;
dto.FirstName = user.FirstName;
dto.LastName = user.LastName;
dto.Email = user.Email;
// dto.Password = user.Password; // manually skip
// dto.InternalNotes = user.InternalNotes; // manually skipMapper.CreateMap<User, UserDto>()
.Ignore(d => d.Password)
.Ignore(d => d.InternalNotes);
var dto = Mapper.Map<User, UserDto>(user);When property names differ or values need transformation.
var dto = new EmployeeDto();
dto.Id = emp.Id;
dto.FullName = $"{emp.FirstName} {emp.LastName}";
dto.Age = DateTime.Now.Year - emp.BirthYear;
dto.Department = emp.Department;Mapper.CreateMap<Employee, EmployeeDto>()
.ForMember<string>(d => d.FullName,
opt => opt.MapFrom(s => $"{s.FirstName} {s.LastName}"))
.ForMember<int>(d => d.Age,
opt => opt.MapFrom(s => DateTime.Now.Year - s.BirthYear));
var dto = Mapper.Map<Employee, EmployeeDto>(emp);Department is auto-mapped by matching name. Only mismatched or computed properties need ForMember.
var dto = new UserDto();
dto.Id = user.Id;
dto.Name = user.Name;
if (user.IsEmailVerified)
{
dto.Email = user.Email;
}Mapper.CreateMap<User, UserDto>()
.ForMember<string>(d => d.Email, opt => {
opt.MapFrom(s => s.Email);
opt.Condition(s => s.IsEmailVerified);
});
var dto = Mapper.Map<User, UserDto>(user);var dto = new UserDto();
dto.Id = user.Id;
dto.DisplayName = user.Nickname ?? "Anonymous";Mapper.CreateMap<User, UserDto>()
.ForMember<string>(d => d.DisplayName, opt => {
opt.MapFrom(s => s.Nickname);
opt.NullSubstitute("Anonymous");
});
var dto = Mapper.Map<User, UserDto>(user);var dto = new UserDto();
dto.MappedAt = DateTime.UtcNow; // before
dto.Id = user.Id;
dto.FirstName = user.FirstName;
dto.LastName = user.LastName;
// ... all properties ...
dto.FullName = $"{dto.FirstName} {dto.LastName}"; // afterMapper.CreateMap<User, UserDto>()
.BeforeMap((src, dest) => dest.MappedAt = DateTime.UtcNow)
.AfterMap((src, dest) => dest.FullName = $"{dest.FirstName} {dest.LastName}");
var dto = Mapper.Map<User, UserDto>(user);Execution order: BeforeMap → property mapping → AfterMap.
var record = new ImmutableRecord(source.Id, source.Name, source.Email);Mapper.CreateMap<Source, ImmutableRecord>()
.ConstructUsing(src => new ImmutableRecord(src.Id, src.Name, src.Email));
var record = Mapper.Map<Source, ImmutableRecord>(source);Useful when the destination type requires constructor parameters, or when you need factory logic centralized in one place instead of scattered across the codebase.
// Entity → DTO (manual)
var dto = new UserDto { Id = entity.Id, Name = entity.Name, Email = entity.Email };
// DTO → Entity (manual, duplicated logic)
var entity = new User { Id = dto.Id, Name = dto.Name, Email = dto.Email };Mapper.CreateMap<User, UserDto>()
.ReverseMap();
var dto = Mapper.Map<User, UserDto>(entity); // forward
var entity = Mapper.Map<UserDto, User>(dto); // reverse (auto-generated)// Manual mapping scattered across controllers, services, repositories...
public class UserService
{
public UserDto GetUser(int id)
{
var entity = _db.Users.Find(id);
return new UserDto
{
Id = entity.Id,
FirstName = entity.FirstName,
LastName = entity.LastName,
// ... repeat for every property
};
}
}// Program.cs — register once
builder.Services.AddSimpleMapper(cfg => {
cfg.AddProfile<UserProfile>();
});
// Service — inject and use
public class UserService
{
private readonly ISimpleMapper _mapper;
public UserService(ISimpleMapper mapper) => _mapper = mapper;
public UserDto GetUser(int id)
{
var entity = _db.Users.Find(id);
return _mapper.Map<User, UserDto>(entity);
}
}public class UserProfile : Profile
{
protected override void Configure()
{
CreateMap<User, UserDto>()
.ForMember<string>(d => d.FullName,
opt => opt.MapFrom(s => $"{s.FirstName} {s.LastName}"))
.ReverseMap();
CreateMap<Address, AddressDto>();
}
}[HttpGet("{id}")]
public ActionResult<ProductDto> Get(int id)
{
var entity = _db.Products.Find(id);
return new ProductDto
{
Id = entity.Id,
Name = entity.Name,
Description = entity.Description,
Price = entity.Price,
Stock = entity.Stock,
IsActive = entity.IsActive,
CreatedAt = entity.CreatedAt
};
}
[HttpPatch("{id}")]
public ActionResult Patch(int id, UpdateProductDto dto)
{
var entity = _db.Products.Find(id);
if (dto.Name != null) entity.Name = dto.Name;
if (dto.Description != null) entity.Description = dto.Description;
if (dto.Price.HasValue) entity.Price = dto.Price.Value;
if (dto.Stock.HasValue) entity.Stock = dto.Stock.Value;
if (dto.IsActive.HasValue) entity.IsActive = dto.IsActive.Value;
entity.UpdatedAt = DateTime.UtcNow;
_db.SaveChanges();
return Ok();
}[HttpGet("{id}")]
public ActionResult<ProductDto> Get(int id)
{
var entity = _db.Products.Find(id);
return _mapper.Map<Product, ProductDto>(entity);
}
[HttpPatch("{id}")]
public ActionResult Patch(int id, UpdateProductDto dto)
{
var entity = _db.Products.Find(id);
_mapper.Patch(dto, entity);
entity.UpdatedAt = DateTime.UtcNow;
_db.SaveChanges();
return Ok();
}// Read
var users = await db.Users.AsNoTracking().Include(u => u.Address).ToListAsync();
var dtos = new List<UserDto>();
foreach (var u in users)
{
dtos.Add(new UserDto
{
Id = u.Id, FirstName = u.FirstName, LastName = u.LastName,
Address = new AddressDto { Street = u.Address.Street, City = u.Address.City }
});
}
// Create
var entity = new User { Name = dto.Name, Email = dto.Email, /* ... */ };
await db.Users.AddAsync(entity);
// Update
var entity = await db.Users.FindAsync(id);
entity.Name = dto.Name;
entity.Email = dto.Email;
entity.Price = dto.Price;
// ... repeat for all properties
await db.SaveChangesAsync();// Read
var users = await db.Users.AsNoTracking().Include(u => u.Address).ToListAsync();
var dtos = Mapper.Map<User, UserDto>(users);
// Create
var entity = Mapper.Map<CreateUserDto, User>(dto);
await db.Users.AddAsync(entity);
// Full Update (PUT)
var entity = await db.Users.FindAsync(id);
Mapper.Map(dto, entity);
await db.SaveChangesAsync();
// Partial Update (PATCH)
var entity = await db.Users.FindAsync(id);
Mapper.Patch(dto, entity);
await db.SaveChangesAsync();| Overload | Description |
|---|---|
Map<TS, TD>(source) |
Create new TD from TS |
Map<TD>(object source) |
Create new TD, source type inferred |
Map<TS, TD>(IEnumerable<TS>) |
Map collection to List<TD> |
Map<TS, TD>(source, dest) |
In-place update (overwrites all) |
| Overload | Description |
|---|---|
Patch<TS, TD>(source) |
Create new TD, skip null properties |
Patch<TD>(object source) |
Create new TD, source type inferred, skip null |
Patch<TS, TD>(IEnumerable<TS>) |
Patch collection to List<TD> |
Patch<TS, TD>(source, dest) |
In-place partial update (skip null) |
| Method | Description |
|---|---|
CreateMap<TS, TD>() |
Register a mapping pair |
.Ignore(d => d.Prop) |
Exclude a property |
.ForMember<T>(d => d.Prop, opt => opt.MapFrom(...)) |
Custom mapping |
opt.Condition(s => bool) |
Conditional mapping |
opt.NullSubstitute(value) |
Default when source is null |
.BeforeMap((s, d) => ...) |
Pre-mapping callback |
.AfterMap((s, d) => ...) |
Post-mapping callback |
.ConstructUsing(s => new T(...)) |
Custom construction |
.ReverseMap() |
Bidirectional mapping |
.PreserveReferences() |
Circular reference tracking |
.MaxDepth(n) |
Recursion depth limit |
// Program.cs
builder.Services.AddSimpleMapper(cfg => {
cfg.AddProfile<MyProfile>();
});
// Or auto-scan assembly
builder.Services.AddSimpleMapper(typeof(MyProfile).Assembly);net8.0, net9.0, net10.0
| Sample | Location | Demonstrates |
|---|---|---|
| Console | samples/Console/ |
All 19 features with static Mapper |
| WebAPI | samples/WebAPI/ |
REST API with DI, PUT/PATCH endpoints |