TL;DR
- AutoMapper 15 and later need a license (RPL 1.5 or commercial, with a free Community tier for companies under $5M revenue), and staying on 14 leaves the GHSA-rvv3-g6hj-g44x stack-overflow advisory unpatched.
- Compare three paths across identical in-memory DTO mapping, EF Core SQL projections, and partial entity updates: AutoMapper 16, Mapster 10 (MIT), and zero-dependency manual mapping.
- Every option ends with a test that fails when the mapping drifts, plus a migration path between them and a look at Riok.Mapperly, a source generator that catches unmapped members at build time.
Level: Intermediate · Prerequisites: ASP.NET Core Web API and EF Core experience with DTO-to-entity patterns · Reading time: 12 min
The build output has a new line: warning NU1903: Package 'AutoMapper' 14.0.0 has a known high severity vulnerability. The advisory (GHSA-rvv3-g6hj-g44x) describes a stack overflow when mapping deeply nested object graphs. The fixed versions are 15.1.1 and 16.1.1, and AutoMapper 15 and later need a license key. Version 14 was the last MIT release.
That warning turns a style debate (“do we even need a mapper?”) into a decision with a deadline. This article maps the same three things in AutoMapper, Mapster, and plain C#: an Order with lines to an OrderResponse with computed fields, the same mapping as an Entity Framework (EF) Core projection that runs in SQL, and an update from a request onto an existing Product that touches only the allowed fields. Each option ends with a test that fails when the mapping drifts.
The short answer
- Choose AutoMapper 16 with a license when you have hundreds of maps, a team that knows its conventions, and the license cost (or a free Community license) is cheaper than rewriting them.
- Choose Mapster when you want convention-based mapping and EF Core projections under the MIT license, with configuration that looks like AutoMapper’s but is shorter.
- Choose manual mapping when you have tens of data transfer objects (DTOs), not hundreds, and want the compiler to catch every renamed property.
- Choose Mapperly (covered in “Switching later”) when you want generated, reflection-free mapping code with build-time diagnostics.
What is being compared
AutoMapper is the convention-based object mapper by Jimmy Bogard, published by Lucky Penny Software. The current version is 16.2.0. Versions 15.0.0 and later are dual-licensed under the Reciprocal Public License 1.5 or a commercial license with a license key; versions before 15.0.0 were open source, and 14.0.0 was the last MIT release. Without a key, 15 and later log a warning and keep working, and non-production use is free under the license agreement.
Mapster is a convention-based mapper that compiles mappings into expression trees. The current version is 10.0.13, under the MIT license, maintained by a group of community contributors on the MapsterMapper GitHub organization. Version 10.0.0 (March 2026) was the first major release after 7.4.0. Mapster.DependencyInjection adds an injectable IMapper.
Manual mapping is C# you write yourself: an expression for projections, an extension method for in-memory mapping, and plain assignments for updates. No package, no license, and no runtime magic.
The same task in each option
Three call sites, identical in every option:
ToResponse(order): map a loadedOrder(withLines) toOrderResponsein memory, for example after placing an order.ListOrdersAsync(userId): return the user’s orders asOrderResponse, projected in SQL, without loading entities.UpdateProductAsync(id, request): copyNameandPricefromUpdateProductRequestonto a trackedProduct, never touchingIdorStock.
Shared code used by all three options
| |
| |
ItemCount (sum of quantities) and LineTotal (quantity times unit price) have no matching entity property, so every option must configure them. Order, OrderLine, and Product are the shared Store.Api entities. The required modifiers matter for the manual option; the libraries ignore them.
Each option provides a StoreService with the same public methods, so one controller works with all three:
| |
ICurrentUser comes from the playbook guide “Issuing and Validating JWT Access Tokens in ASP.NET Core”. Register the service with builder.Services.AddScoped<StoreService>(); in every option.
AutoMapper
| |
| |
| |
| |
MapFromexpressions serve both in-memory mapping andProjectTo, which turns them into SQL.MemberList.Sourceflips validation for the update map: every source member must be used, and unmatched destination members (Id,Stock) are left alone. With the defaultMemberList.Destination, validation fails onIdandStock, and the usual “fix” (ignoring everything) hides real mistakes.- The key can also come from the
AUTOMAPPER_LICENSE_KEYorLUCKYPENNY_LICENSE_KEYenvironment variables.
The configuration test:
| |
Building the configuration through dependency injection (DI) works on 14 and 16. Since 15, MapperConfiguration needs an ILoggerFactory, which is why the test calls AddLogging(). Add a destination property without a source and the test fails with “Unmapped members were found.”
Mapster
| |
| |
| |
| |
RequireDestinationMemberSource = truemakes Mapster reject any destination member without a source, which is the same safety net AutoMapper validation gives you. The update map then has to nameIdandStockinIgnore, so the allowed fields are visible in one place.- Mapping onto an existing object (
mapper.Map(request, product)) only writes the mapped members. - Pass the same
configtoProjectToType. Without it, Mapster usesTypeAdapterConfig.GlobalSettings, which does not contain your registrations.
The configuration test:
| |
Compile() builds every registered map (both “new object” and “onto existing object”) and throws on the first problem. A missing source member fails with a CompileException whose message lists it: “The following members of destination class … do not have a corresponding source member mapped or ignored”.
Manual mapping
| |
| |
- One expression serves both EF Core (
Select(OrderMappings.ToResponseExpression)) and in-memory mapping (compiled once into a delegate), so the two can never disagree. - Because every response property is
required, forgetting one in the object initializer is a compile error (CS9035). That is the manual option’s configuration validation, and it runs on every build. ApplyTois the allow list. A newProductproperty stays untouched until someone writes a line for it.
The test checks behavior, because the compiler already checks completeness:
| |
All three options produced the same SQL in a run against SQL Server LocalDB with EF Core 9 (AutoMapper only reorders the columns). The update writes only the allowed columns:
| |
Side by side
| Criterion | AutoMapper 14 / 16 | Mapster 10 | Manual mapping |
|---|---|---|---|
| Setup effort | Low: profile plus AddAutoMapper | Low: IRegister plus two registrations | None; you write each map |
| Learning curve | Medium: conventions, MemberList, resolvers | Medium: similar concepts, fewer of them | None beyond C# and expression trees |
| Code per new DTO | Lowest when names match | Lowest when names match | One line per property |
| Mistake detection | Runtime test with AssertConfigurationIsValid | Runtime test with Compile() and strict settings | Compiler, through required members |
| EF Core projection | ProjectTo | ProjectToType | Select(expression) |
| Update onto entity | Map(source, destination) with MemberList.Source | Map(source, destination) with Ignore | Explicit assignments |
| Runtime performance | Compiled expression trees, cached per map | Compiled expression trees, cached per map | Plain code; nothing to build |
| Debugging | Hard to step into generated maps | Same | Ordinary breakpoints |
| Refactoring safety | Silent breakage without the test | Silent breakage without the test | Rename and the build fails |
| License and cost | 14 MIT (unpatched advisory); 15+ RPL 1.5 or commercial | MIT, free | Free |
| Maintenance | Active, commercial | Active, community maintainers | You |
How to choose
- Tens of DTOs, not hundreds. Manual mapping. The code is boring, reviewable, and the compiler does the validation.
- Hundreds of maps that mostly match by name. Price the AutoMapper license against migration effort. Companies under 5 million US dollars in gross annual revenue may qualify for the free Community license, with further conditions in the license text; Lucky Penny decides eligibility, so check the current terms.
- You want conventions but not the license. Mapster. The concepts carry over, and
ProjectToTypecovers EF Core. - You cannot upgrade AutoMapper this sprint. Staying on 14.0.0 means living with the advisory. If you do, at least avoid mapping self-referencing types from untrusted input and add
MaxDepthto recursive maps.
Switching later
AutoMapper to Mapster. CreateMap<A, B>() becomes config.NewConfig<A, B>(), ForMember(d => d.X, o => o.MapFrom(s => ...)) becomes .Map(d => d.X, s => ...), Ignore() becomes .Ignore(d => d.X), and ProjectTo<T>(mapper.ConfigurationProvider) becomes ProjectToType<T>(config). Port the validation test first, then the profiles one at a time; both libraries can run side by side because their IMapper interfaces live in different namespaces (AutoMapper and MapsterMapper).
Any library to manual mapping. Do it per endpoint. Write the expression, switch the query to Select(...), and delete the map. Mark DTO properties required as you go so the compiler starts protecting you immediately.
Upgrading Mapster from 7.4 to 10. Read the 10.0.0 release notes before you bump; it changed how records are detected and mapped onto existing instances. Run the Compile() test and your mapping tests after the upgrade.
A fourth option: Mapperly. Mapperly (Riok.Mapperly, version 4.3.1, Apache 2.0 license) is a source generator: you declare partial methods and it writes the mapping code at build time, with no runtime reflection. Unmapped members show up as build diagnostics you can promote to errors.
| |
It also generates IQueryable<T> projections from a method such as public static partial IQueryable<OrderResponse> ProjectToResponse(this IQueryable<Order> query);, and computed members use [MapPropertyFromSource(..., Use = nameof(...))]. Moving from manual mapping to Mapperly is the easiest path of all, because the generated code looks like what you already wrote.
Common pitfalls
- Ignoring all unmapped members to make validation pass. It turns the test into a no-op. Use
MemberList.Sourceor explicitIgnorecalls instead. - Mapping in memory instead of projecting.
mapper.Map<List<OrderResponse>>(await db.Orders.Include(o => o.Lines).ToListAsync())loads full entities and tracks them. UseProjectTo,ProjectToType, orSelect. - Methods that EF Core cannot translate. A
MapFrom(s => FormatPrice(s.Total))works in memory and fails or runs client-side in a projection. Keep projection expressions to members and simple operators. - Mapping requests straight onto entities without an allow list. A request DTO that gains a
Stockproperty would overwrite stock on the next deploy. Keep the update map explicit. - Forgetting the
ILoggerFactoryin AutoMapper 15+ tests.new MapperConfiguration(cfg => ...)without a logger factory no longer compiles; build through DI withAddLogging()as above. IConfigurationProvidername clash. ASP.NET Core projects importMicrosoft.Extensions.Configuration, which has its ownIConfigurationProvider. Qualify it asAutoMapper.IConfigurationProvider.
Related reading
- DTOs and Mapping: Manual Mapping, Mapster, and AutoMapper (in “The .NET Implementation Playbook”)
- Read-Only Queries with AsNoTracking and Projections (in “The .NET Implementation Playbook”)
- Replacing AutoMapper and MediatR After the License Change
- MediatR vs Mediator vs Plain Handler Classes
- EF Core Performance Pitfalls: N+1, Tracking, and Projections (in “Zero to Full-Stack .NET”)
Part of The .NET Field Guide. Previous: MassTransit vs Wolverine vs Raw RabbitMQ.Client After the MassTransit License Change. Next in the series: Replacing AutoMapper and MediatR After the License Change (coming soon).
Get the next articles by email: subscribe to the newsletter.
How this article was made: drafted with AI assistance, then reviewed by Softinbit Technology before publishing.
