Sitemap

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.DependencyInjection

The 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).Assembly

4) 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 here

3) 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 convention

4) 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 set

Value 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 ICurrencyService in 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) so ProjectTo works.

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

  1. “Missing type map configuration or unsupported mapping.”
    You tried to map types without a defined map. Create CreateMap<A,B>() for that pair (and for nested or collection element types if needed).
  2. ProjectTo throws/not translating custom logic.
    You used a non-translatable method inside MapFrom. Replace it with an expression using only EF-supported operations.
  3. Silent overwrites on updates.
    For PATCH/partial updates, add Condition(srcMember != null) to avoid nulls overwriting existing values.
  4. Circular references or deep graphs.
    Map only the fields you need in DTOs. Use projection, not full graphs.
  5. Performance issues mapping huge lists in memory.
    Use ProjectTo to 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 need

Custom 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 instance

Testing 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 message

When 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.

--

--

Vikas Jindal
Vikas Jindal

Written by Vikas Jindal

I am student, learning and want to learn a lot. By profession, Works in Software Industry - Developer and Time Reader - Astrologer.