AutoMapper Without Tears
Layered apps map constantly β entity to view model, view model to DTO β and AutoMapper exists to kill that boilerplate. Used with discipline it's a quiet workhorse; used carelessly it's a famous source of runtime surprises. The difference is three habits.
Profiles: mapping as configuration
Mappings live in Profile classes, grouped by feature, registered once at startup:
public class PermitProfile : Profile
{
public PermitProfile()
{
CreateMap<Permit, PermitRowVm>()
.ForMember(d => d.StatusName, o => o.MapFrom(s => s.Status.Name));
}
}
By convention, same-named properties map automatically; ForMember handles the rest. Flattening
is built in too β a destination ApplicantName picks up source.Applicant.Name without
configuration. Keep profiles near the feature they serve, and keep them boring: heavy
MapFrom lambdas full of conditionals are business logic hiding in mapping config, where
nobody will look for it.
ProjectTo: mapping inside the SQL
The underrated feature. Map runs on objects already in memory; ProjectTo<T> translates
the mapping into the LINQ projection itself:
var rows = _db.Permits
.ProjectTo<PermitRowVm>(_mapper.ConfigurationProvider)
.ToDataSourceResult(request);
EF generates a SELECT of exactly the mapped columns β related data becomes joins, not lazy
loads. This combines everything Module 5 preached (project early, avoid N+1, no tracking
overhead) with zero hand-written projections. For grid read actions, ProjectTo +
ToDataSourceResult is the canonical pairing. Limitation: only expression-translatable mappings
work β no method calls SQL can't understand β which is a feature, since it keeps mapping
config translatable and simple.
When explicit mapping beats magic
AutoMapper's failure mode is invisibility: rename an entity property and the mapping silently stops populating a field β no compile error, just a blank column a user notices three weeks later. So don't use it everywhere. Write explicit mapping code (plain constructors or extension methods) when the transformation has real logic in it, when the shapes barely align, or when the object is critical enough that you want the compiler enforcing every assignment. A reasonable team rule: AutoMapper for wide, mechanical, property-for-property mappings (grids, lists, DTOs); explicit code for anything with decisions in it.
Test the configuration
The silent-breakage risk has a cheap antidote β one unit test that runs
AssertConfigurationIsValid() against the full mapper configuration. It fails if any
destination property on any map has no source, catching renames and forgotten ForMembers at
build time instead of in production. Pair it with Ignore() for deliberately unmapped members
so the assertion stays meaningful. If your project uses AutoMapper and doesn't have this test,
adding it is a genuinely valuable first-week pull request.