Sixty milliseconds
I had a read endpoint taking about 60ms to respond, and most of that made no sense. It sits on a cache, so there was no slow query to blame and no network hop to chase. The work between the cached data and the response should have been close to nothing, and 60ms is not close to nothing.
AutoMapper sat in that gap, turning cached entities into response DTOs. Across the size of result this endpoint returned, the per-object overhead and the allocations the resolution context drags along stopped being microseconds and turned into real wall-clock time.
So I asked Claude to rip it out. It replaced the DTOs with records and wrote a plain static mapper for every type, direct construction, no reflection. The endpoint dropped to between 1 and 4ms depending on which path it took. Same data, same shape, an order of magnitude off the clock. The framework had been the slow part of an endpoint with nothing else slow in it.
Mapping was always assignment
Strip the marketing off and a mapper does one thing. It reads a value from one object and writes it to another.
var dto = new CustomerDto
{
Id = customer.Id,
DisplayName = customer.DisplayName,
Email = customer.Email,
SignedUpUtc = customer.SignedUpUtc,
};
That is the entire problem domain. No algorithm, no edge case, no clever data structure. It is a list of assignments, and the compiler checks every one. Rename DisplayName on the entity and this file goes red before you've left the editor.
A framework replaces that with configuration:
public class CustomerProfile : Profile
{
public CustomerProfile()
{
CreateMap<Customer, CustomerDto>();
}
}
Less to type, so it looks like a win. What you've actually done is move the field list out of code the compiler reads and into reflection the compiler ignores. Rename DisplayName to Name on the entity now and nothing goes red. The map still "works". CustomerDto.DisplayName is silently null in production, and you find out when a customer emails support asking why their account shows no name.
The framework didn't remove the field list. It hid it, then stopped checking it.
Why it got a seat anyway
I don't think anyone adopted AutoMapper after weighing this up. It arrived for softer reasons.
Typing the assignments felt like drudgery, and drudgery feels like something a good engineer should automate. A mapper let you delete a wall of a.X = b.X and replace it with one line, and a smaller diff reads as a better one. It demoed beautifully: a senior shows a junior CreateMap resolving twenty fields by convention, the junior is sold for life, and now it's in every new project on the team by default.
Then DRY got invoked, as it always does. Two assignments that look alike feel like duplication that ought to be abstracted, even though they share no logic and change for completely different reasons. The explicit version got branded as boilerplate, and boilerplate became something to be ashamed of rather than the clearest possible statement of what the code does.
None of those are engineering reasons. The framework won because hand-writing the mapping looked junior, not because it was worse.
What you actually bought
The convenience was never free. You paid for it on a delay.
You traded compile-time errors for runtime ones. A mistake the build used to catch now waits for the request that exercises that path. AutoMapper's answer is AssertConfigurationIsValid(), a test you have to remember to write and run, which only confirms the maps resolve, not that they resolve to the right thing. A green run of that test feels like proof the mapping is correct. It is proof of nothing of the sort.
You traded greppability for magic. A new starter asks where SignedUpUtc gets populated on the DTO. With the manual version you search the field name and land on the line. With the framework the honest answer is "convention, somewhere in the reflection, unless a profile overrides it, in which case go and find the profile."
And you traded a stack you can read for one you can't. The exceptions surface deep inside the mapper, the traces are noise, and the fix is usually a configuration incantation you copy from a six-year-old GitHub issue.
A hand-written mapper is just an adapter
The thing people reach for a framework to avoid building already has a name, and it's older than the framework.
A class that takes one shape and presents another is the Adapter pattern. A function that converts between two models the rest of the system shouldn't have to reconcile is a Translator. Fowler called the dedicated object that moves data between layers a Data Mapper twenty years ago. These aren't workarounds for the absence of AutoMapper. AutoMapper is a generic, reflective implementation of a pattern that is clearer when you write the specific one.
public static class CustomerMapper
{
public static CustomerDto ToDto(this Customer customer) => new()
{
Id = customer.Id,
Name = customer.DisplayName,
Email = customer.Email,
SignedUpUtc = customer.SignedUpUtc,
};
}
An extension method, discoverable on the type, trivial to test, and the rename that silently broke the profile breaks this at compile time instead. Want it generated at build time rather than typed by hand? A source generator like Mapperly does exactly that and the output is ordinary code you can step through. The pattern was never the problem. Doing it through runtime reflection was.
The keystrokes went free

Every honest argument for a mapping framework reduced to one thing: nobody wanted to type the assignments. Hand-mapping was dull, and the framework sold you out of the dullness. For years that was a real trade.
I select the entity and the DTO, ask the model for a mapper, and it writes all forty fields correctly in the time it takes to read this sentence. Renames, type conversions, nullable mismatches, nested objects. The boilerplate that made hand-mapping a chore now costs nothing to produce, and what comes out is plain code: greppable, compile-checked, with no dependency behind it.
That is the whole story. The framework solved a typing problem, the typing problem is gone, and nothing it charged you in return went anywhere with it.
Then the bill arrived
A few weeks after that PR merged, AutoMapper and MediatR went commercial. I watched the Reddit thread go up in flames from a safe distance, with one fewer dependency to account for.
The licence is the visible version of a risk that was always there. A library is given away until it's load-bearing in enough codebases to be expensive to remove, and then the terms change. HashiCorp did it to Terraform, Redis did it to itself. Free open source carries a clause everyone reads as permanent and isn't, and you never price it in at adoption because at adoption it's free.
The quieter cost is security. Every dependency is code you didn't write, running with your privileges, updated by people you've never met, and a single compromised utility package can put credentials in production. A mapper is the easiest dependency in your tree to retire, because its whole job is something the language already does. Carrying that risk to save some typing made a kind of sense once. It doesn't now.
This isn't a grudge
None of this is a swing at AutoMapper, and it isn't really about AutoMapper. It's the example everyone knows. Jimmy Bogard built a good library, maintained it for over a decade, and earned every right to charge for the work. Back when typing the field list was a real cost, reasonable engineers landed on both sides of whether the framework paid for itself. The ground has moved under that decision, and a call that used to be close isn't anymore.
It was never a .NET problem either. MapStruct and ModelMapper in Java, the mapping helpers scattered through the JavaScript and Python worlds, anywhere a library exists to copy fields between two shapes: the trade is identical, and so is the verdict once the keystrokes are free.
The code that replaces all of it is boring. Dull, obvious, impossible to be surprised by, and that is the point. Boring code is the code a new starter reads once and understands, that a stack trace points straight at, that no vendor can relicense and no maintainer can compromise. Enterprise software should be predictable to the point of tedium. The mapper that made it clever was never an asset.
Closing
Mapping was always assignment, and assignment was never hard enough to need a vendor. The one thing the framework did for you, the typing, is now something a machine does for free, while everything it charged in return is still on the invoice. Keep the pattern. Delete the dependency. Write the adapter, or have the model write it, and own the forty lines instead of renting them.