-
Notifications
You must be signed in to change notification settings - Fork 1
Migrating from AutoMapper
| AutoMapper | Mapsicle |
|---|---|
CreateMap<S,D>() |
Same. |
ForMember().MapFrom() |
Same. |
.Ignore() |
Same. |
BeforeMap/AfterMap |
Same. |
Include<Derived>() |
Same. |
ConstructUsing() |
Different: the factory's object is kept as built. Convention mapping does not run after it, so a member the factory does not set stays default. ForMember and AfterMap still apply. |
services.AddAutoMapper() |
services.AddMapsicle() |
_mapper.Map<T>() |
mapper.Map<T>() or obj.MapTo<T>()
|
Simple mappings (no profiles): use core Mapsicle package
Profiles with configuration: use Mapsicle.Fluent
EF Core ProjectTo: use Mapsicle.EntityFramework
dotnet remove package AutoMapper
dotnet remove package AutoMapper.Extensions.Microsoft.DependencyInjection
dotnet add package Mapsicle.Fluent # Includes coreBefore (AutoMapper):
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<User, UserDto>()
.ForMember(d => d.FullName, opt => opt.MapFrom(s => s.FirstName + " " + s.LastName));
}
}After (Mapsicle):
// In Program.cs/Startup.cs
services.AddMapsicle(cfg =>
{
cfg.CreateMap<User, UserDto>()
.ForMember(d => d.FullName, opt => opt.MapFrom(s => s.FirstName + " " + s.LastName));
}, validateConfiguration: true);Before:
services.AddAutoMapper(typeof(UserProfile).Assembly);After:
services.AddMapsicle(cfg =>
{
cfg.CreateMap<User, UserDto>();
cfg.CreateMap<Order, OrderDto>();
// ... all your mappings
}, validateConfiguration: true);Before:
public class UserService
{
private readonly IMapper _mapper;
public UserService(IMapper mapper) => _mapper = mapper;
public UserDto GetUser(User user) => _mapper.Map<UserDto>(user);
}After (same interface!):
public class UserService
{
private readonly IMapper _mapper;
public UserService(IMapper mapper) => _mapper = mapper;
// Option 1: Same as AutoMapper
public UserDto GetUser(User user) => _mapper.Map<UserDto>(user);
// Option 2: Extension method (no DI needed for simple cases)
public UserDto GetUser(User user) => user.MapTo<UserDto>();
}❌ Not Supported:
-
IMemberValueResolverinterface - useResolveUsing(func)instead -
ITypeConverterinterface - useCreateConverter<T, U>()instead - Conditional mapping with complex predicates
- MaxDepth per individual mapping (only global
Mapper.MaxDepth)
✅ Now Supported (via extension packages):
- Custom naming conventions:
Mapsicle.NamingConventions - Post-mapping validation:
Mapsicle.Validation
-
Circular references: Mapsicle returns the default at
MaxDepthwith no configuration. AutoMapper needsPreserveReferences()orMaxDepth(...)to handle them. -
Unmapped properties: Both ignore, but Mapsicle has
GetUnmappedProperties<T, U>()for validation - Null handling: Both return null for null source, but Mapsicle is more aggressive with null-safe navigation
This page is about moving a real codebase, not about why you might want to. If you have forty
profiles and IMapper injected in thirty handlers, this is what changes and what does not.
Every Mapsicle sample here is compiled and executed by tests/Mapsicle.Docs.Tests, so one that
stops working fails the build. A migration guide whose samples quietly stopped compiling is the
documentation version of a benchmark that prints a number and exits 0.
The AutoMapper snippets are not compiled. They are there to show what you are moving away from, and
the project deliberately does not reference AutoMapper outside tests/Mapsicle.Parity.Tests and the
benchmarks. Treat those blocks as illustrative and the Mapsicle ones as verified.
| You have | You get |
|---|---|
A Profile per area with CreateMap per pair |
Nothing. Convention handles it. Delete them. |
IMapper injected everywhere |
IMapperInstance injected everywhere, one package to install |
ForMember with a resolver |
Mapsicle.Fluent, same shape |
ProjectTo<T>() |
Mapsicle.EntityFramework, same shape |
AssertConfigurationIsValid() |
No direct equivalent, and see below |
The bulk of a migration is deleting configuration, not translating it.
AutoMapper:
services.AddAutoMapper(typeof(Startup).Assembly);Mapsicle, after installing Mapsicle.DependencyInjection:
services.AddMapsicle();There is no assembly to scan because there is nothing to find. A type pair that was never registered still maps.
AutoMapper:
public class OrderHandler
{
private readonly IMapper _mapper;
public OrderHandler(IMapper mapper) => _mapper = mapper;
public OrderDto Handle(Order order) => _mapper.Map<OrderDto>(order);
}Mapsicle:
public class OrderHandler
{
private readonly IMapperInstance _mapper;
public OrderHandler(IMapperInstance mapper) => _mapper = mapper;
public OrderDto Handle(Order order) => _mapper.MapTo<OrderDto>(order)!;
}Two differences worth knowing. The interface is IMapperInstance, and the method is MapTo rather
than Map. Map exists but means something else here: it maps onto an existing destination, which
is AutoMapper's Map(source, destination) overload.
The return is nullable, because a null source maps to null rather than throwing.
Most of these disappear. This AutoMapper profile:
public class OrderProfile : Profile
{
public OrderProfile()
{
CreateMap<Order, OrderDto>();
CreateMap<Customer, CustomerDto>();
CreateMap<Address, AddressDto>();
}
}has no Mapsicle equivalent. Matching names map by convention, including nested objects and
flattening (Address.City fills AddressCity). Delete the profile and the maps keep working.
Keep configuration only where convention is wrong:
var config = new MapperConfiguration(c =>
c.CreateMap<Order, OrderDto>()
.ForMember(d => d.Total, o => o.MapFrom(s => s.Lines.Sum(l => l.Price)))
.ForMember(d => d.InternalNote, o => o.Ignore()));
var mapper = config.CreateMapper();That needs Mapsicle.Fluent, and the shape is close enough that most ForMember chains port
directly.
AutoMapper throws when it reaches a pair you never configured, and
AssertConfigurationIsValid() tells you at startup which ones you missed. That safety net exists
because forgetting a CreateMap is the most common AutoMapper bug.
Mapsicle has no such failure mode, because there is nothing to forget. It also means there is no startup check that catches a destination property you expected to be filled and which is not.
What it offers instead is per-pair:
Mapper.AssertMappingValid<Order, OrderDto>(); // throws, listing unmapped members
var unmapped = Mapper.GetUnmappedProperties<Order, OrderDto>();If your team relied on AssertConfigurationIsValid() as a release gate, put
AssertMappingValid for the pairs you care about into a test. That is a real change in habit, not
a like-for-like swap.
Cycles. AutoMapper needs PreserveReferences() or MaxDepth(...) and an unhandled cycle can
still overflow the stack. Mapsicle returns the destination default once MaxDepth (32) is reached,
with no configuration. If you configured PreserveReferences, note that Mapsicle does not preserve
identity: two references to the same object become two mapped objects.
Unconfigured pairs. AutoMapper throws. Mapsicle maps by convention. Code that relied on the throw as a signal loses that signal.
Wrong-typed dictionary values. Since 2.0.0 they are dropped rather than parsed, matching the
object path. Set Mapper.CoerceDictionaryValues = true if you were relying on parsing.
Shallow copy. A destination member that can hold the source instance receives that instance rather than a copy, so mutating the source afterwards reaches into the destination. AutoMapper behaves the same way, so this is usually not a change, but it is worth knowing if you map onto long-lived entities.
The comparison table in the README is the honest one and it is worth reading before committing to a migration. In short:
- If every pair is known at compile time and you are willing to declare them, Mapperly is 2.5x to 3x faster and generates code with no runtime apparatus at all.
- If collection throughput at around a hundred elements is what your workload is bound by, Mapperly
beats both by about 12 percent. Mapsicle is ahead of AutoMapper there, 1.20x on x64 and 1.09x on
arm64, because a
List<T>is mapped by a loop compiled for its element type. Arrays, and lists whose element type isobject, an interface or abstract, keep an older loop and do not get that. At ten thousand elements Mapsicle is 2.36x faster, because AutoMapper allocates enough more to reach generation 2 collections. All of it is measured and it is in the README. - If you configure with
Mapsicle.Fluentrather than the static API, a complex object costs about 1.03x AutoMapper and a hundred of them about 1.22x, because the fluent path maps a collection element by element rather than through that compiled loop. The static API is the one the fast numbers describe. - If you need NativeAOT, only pairs declared with
[MapsicleGenerate]or found by[MapsicleGenerateAll]map, and only throughMapTo. Scalar conversions such asinttolongor an enum tointstill work without a declaration. Any other pair throwsNotSupportedExceptionat first use, so a pair you forgot to declare fails at run time, not at build time.
Mapsicle wins where the shapes are not all known when you compile, where the licence has to be
permissive, and where you would rather not write a CreateMap per pair.
- Install
Mapsicle.DependencyInjectionand callAddMapsicle()alongside the AutoMapper registration. Both can coexist. - Move one handler. Change
IMappertoIMapperInstanceandMap<T>toMapTo<T>. - Write
AssertMappingValidtests for the pairs that handler uses, so you find convention mismatches now rather than in production. - Repeat by area. Delete each profile once nothing references it.
- Remove the AutoMapper package last, and let the licence-boundary question go away with it.
Mapsicle
Packages
- Core
- Fluent
- EntityFramework
- Validation
- NamingConventions
- Serilog
- Dapper
- AspNetCore
- Json
- Caching
- Audit
- DataAnnotations
Reference