Mapperion.Extensions.DependencyInjection
0.15.1
dotnet add package Mapperion.Extensions.DependencyInjection --version 0.15.1
NuGet\Install-Package Mapperion.Extensions.DependencyInjection -Version 0.15.1
<PackageReference Include="Mapperion.Extensions.DependencyInjection" Version="0.15.1" />
<PackageVersion Include="Mapperion.Extensions.DependencyInjection" Version="0.15.1" />
<PackageReference Include="Mapperion.Extensions.DependencyInjection" />
paket add Mapperion.Extensions.DependencyInjection --version 0.15.1
#r "nuget: Mapperion.Extensions.DependencyInjection, 0.15.1"
#:package Mapperion.Extensions.DependencyInjection@0.15.1
#addin nuget:?package=Mapperion.Extensions.DependencyInjection&version=0.15.1
#tool nuget:?package=Mapperion.Extensions.DependencyInjection&version=0.15.1
Mapperion
Object-to-object mapper for .NET, MIT licensed, built as a drop-in alternative to AutoMapper for commercial projects.
Documentation — getting started, migrating from AutoMapper, ahead-of-time, performance, and the full API reference.
dotnet add package Mapperion
Status: 0.x, and used by nobody yet. Everything listed below works and is covered by a suite that runs on .NET Framework 4.7.2 and 4.8 as well as .NET 8, 9 and 10, plus two applications that run on every build: one that uses the whole library, and one that configures the same invoicing layer on AutoMapper and on Mapperion and fails if they disagree. There is nothing left that AutoMapper does and Mapperion does not.
Before 1.0 the API may still move. What it is waiting for is written down below, so the wait has an end you can check rather than a date somebody keeps moving.
What 1.0 is waiting for
1.0 is a promise, not a grade. It says the public API is settled, and that breaking it costs a 2.0. The code is ready for the grade; the project is not yet in a position to make the promise, for two reasons.
Somebody outside this repository has to have used it for something real. The first application
to use the library in earnest — the pilot in samples/ — found a
semantic bug within minutes of printing anything: ProjectTo was carrying enums across by number
while the mapping engine carried them across by name, so one configuration gave two different
answers for the same record, with green tests on both sides. A test suite does not find that; use
does. Freezing the API before a second application has had a go is a bet that there is no third
thing of that kind.
The public API has to stop moving on its own. The measure is PublicAPI.Unshipped.txt sitting
empty across a couple of releases because nothing needed adding, rather than because nobody got
round to it. Six public types arrived with the naming conventions in 0.10.0, days after this file
last said there was nothing left to add.
Neither of those is a date, and the first does not depend on writing more code. If you put this into something and it goes badly, say so — that is the thing 1.0 is short of, and a bad report is worth more than a good one.
The short version of the licence
MIT, and every version already published stays MIT: an MIT grant cannot be withdrawn from something already released. There is no contributor licence agreement here and no copyright assignment, which is what would make relicensing future versions possible for one party to decide alone. GOVERNANCE.md explains the mechanism, and is honest about where that protection is still thin.
Why
AutoMapper moved to a paid commercial licence at v15. Version 14 and earlier stay MIT — an MIT grant cannot be withdrawn from something already released — but that line is frozen: no new features, no new target frameworks, and no security fixes.
That last one is not hypothetical. AutoMapper 14 carries
CVE-2026-32933: an object graph that loops
recurses until the stack is gone, and it will not be patched on the MIT line. Mapperion bounds
recursion by default, so the same graph raises a RecursionLimitException you can catch instead of
a StackOverflowException you cannot. Staying on 14 means carrying that; moving to 15 means paying
for it.
Mapperion exists to fill that gap with three commitments:
- MIT forever — free commercial use, no per-seat or per-company licensing.
- Broad reach — .NET Framework 4.7.2 and 4.8 with a native target, .NET Standard 2.0/2.1, .NET 8/9/10. No dependencies on any of them.
- Cheap migration — and checked rather than promised.
samples/Mapperion.Migrationconfigures one invoicing layer twice, on AutoMapper 14 and on Mapperion, and fails the build if the two disagree on any field. The whole diff between them is oneusingper file and one!.
Example
var config = new MapperConfiguration(cfg =>
{
cfg.AddProfile<OrderProfile>();
cfg.CreateMap<Order, OrderDto>()
.ForMember(d => d.Total, o => o.MapFrom(s => s.Lines.Sum(l => l.Price)))
.ForMember(d => d.AuditToken, o => o.Ignore());
});
IMapper mapper = config.CreateMapper();
OrderDto dto = mapper.Map<Order, OrderDto>(order);
mapper.Map(order, existingDto);
Maps are compiled to delegates the first time each type pair is used, so nothing is resolved by reflection once a map is warm.
Everything not configured by hand is resolved by convention, in the same order AutoMapper uses:
cfg.CreateMap<Person, PersonDto>();
// PersonDto.Name <- Name
// PersonDto.ContactEmail <- Contact.Email
// PersonDto.CompanyContactEmail<- Company.Contact.Email
Validation reports everything it finds in one go, rather than stopping at the first problem:
config.AssertIsValid();
// MapperConfigurationException: The mapper configuration is not valid. 2 problems were found:
// 1. Sale -> SaleDto: destination member 'Reference' has no source. Map it with ForMember, or ignore it.
// 2. Sale -> SaleDto: member 'Lines' needs a map from 'Line' to 'LineDto'. Declare it with CreateMap<Line, LineDto>().
One map covers every closing of a generic pair:
cfg.CreateMap(typeof(Page<>), typeof(PageDto<>));
cfg.CreateMap<Order, OrderDto>();
PageDto<OrderDto> page = mapper.Map<Page<Order>, PageDto<OrderDto>>(source);
Records are built through their constructor, matching parameter names against source members:
public sealed record EmployeeDto(string Name, int Age);
cfg.CreateMap<Employee, EmployeeDto>();
Queries are projected in the database rather than materialised:
List<BookDto> books = await context.Books
.Where(b => b.Pages > 200)
.ProjectTo<BookDto>(configuration)
.ToListAsync();
Anything a query provider cannot run — type converters, value converters, resolvers, BeforeMap
and AfterMap — is reported rather than skipped. AutoMapper skips them silently, which lets a
projection quietly disagree with the same map run through Map.
In an ASP.NET Core application:
builder.Services.AddMapperion(typeof(Program).Assembly);
That registers the configuration as a singleton and IMapper as scoped, so a resolver may depend
on scoped services. Every mapper shares the same compiled plans, so one per request costs nothing.
For a trimmed or ahead-of-time compiled application, the same mapping can be written at compile time instead, with no reflection anywhere:
[Mapper]
public partial class OrderMapper
{
[MapProperty("Customer.Address.City", "CustomerCity")]
public partial OrderDto ToDto(Order source);
public partial LineDto ToDto(Line source);
}
The generator writes the bodies and reports what it cannot write as a compiler error rather than leaving it to fail later.
Profiles work the way you already know them:
public sealed class OrderProfile : Profile
{
public OrderProfile()
{
CreateMap<Order, OrderDto>();
CreateMap<OrderLine, OrderLineDto>();
}
}
cfg.AddProfile<OrderProfile>();
cfg.AddProfiles(typeof(Program).Assembly);
Coming from AutoMapper
| AutoMapper | Mapperion |
|---|---|
MapperConfiguration(cfg => ...) |
same |
Profile, CreateMap<S,D>() |
same |
ForMember(d => d.X, o => o.MapFrom(...)) |
same |
Ignore(), Condition(), PreCondition(), NullSubstitute() |
same |
MaxDepth(), PreserveReferences() |
same |
AddProfile<T>(), AddProfiles(assembly) |
same |
CreateMapper(), IMapper.Map<T>(...) |
same |
ForCtorParam(name, o => o.MapFrom(...)) |
same |
ReverseMap() |
same, minus unflattening |
Include<,>(), IncludeBase<,>() |
same |
CreateMap(typeof(A<>), typeof(B<>)) |
same |
ITypeConverter, IValueConverter, IValueResolver |
same, with ResolutionContext |
ConvertUsing<T>(), MapFrom<TResolver>() |
same |
BeforeMap(...), AfterMap(...), IMappingAction |
same |
AddAutoMapper(...) |
AddMapperion(...) |
ProjectTo<T>(configuration) |
same, and on IMapper too |
RecognizePrefixes / RecognizePostfixes |
RecognizeSourcePrefixes / RecognizeDestinationPostfixes |
AssertConfigurationIsValid() |
same name works, or the shorter AssertIsValid() |
Deliberate difference: a type pair with no CreateMap is an error rather than an implicit map.
Validation stays opt-in, exactly as in AutoMapper.
Supported frameworks
Built for netstandard2.0, netstandard2.1, net472, net8.0, net9.0 and net10.0.
.NET Framework gets a native net472 target rather than the netstandard shims, so a project on
4.7.2 or 4.8 does not drag in dozens of System.* compatibility packages. A slice of the test
suite runs on net472 and net48 on every build, so Framework support is verified rather than
assumed.
Trimming and AOT: the runtime engine compiles expression trees, so it is annotated
[RequiresUnreferencedCode] and [RequiresDynamicCode] and is not for a trimmed or
ahead-of-time application. The source generator is, and writes plain C# with no reflection in it.
samples/Mapperion.Aot is an application published with PublishAot=true that maps with the
generated code and checks its own output. It builds with the trimming and AOT analysers turned on,
and every build publishes it natively and runs it, so what this section claims is measured rather
than asserted.
What works today
Fluent configuration:
CreateMap,ForMemberwithMapFrom,Ignore,Condition,NullSubstitute,SetMappingOrder,UseDestinationValue; plusValidateMemberList,MaxDepth,PreserveReferences.Profiles, including assembly scanning.
Conventions: exact name, case-insensitive name, configurable prefixes and suffixes, and flattening up to a configurable depth.
Validation:
AssertIsValid()reports every problem at once — unmapped destination members, missing nested maps (looking through nullables and collections), and, withMemberListValidation.Source, source members nobody reads.Failures at run time name the member that caused them, with the path through nested maps and collections:
Batch.Readings[0].Ratio.Object graphs that loop, through
PreserveReferencesorMaxDepth. A cycle with neither is reported byAssertIsValid(), and bounded at run time regardless, so it fails with an exception the caller can catch rather than exhausting the stack.Inheritance:
Includedispatches to the derived map so a base reference still produces the right destination, andIncludeBasetakes the base map's configuration as a starting point.Open generics: one
CreateMap(typeof(Page<>), typeof(PageDto<>))serves every closing of the pair, worked out on first use and kept.A source generator that writes mappers at compile time, for trimmed and AOT applications. Its output is checked against the run-time engine's on the same cases, and a sample published with
PublishAot=trueruns on every build.ConstructUsingto build the destination with a factory, andForPathto assign a member that sits inside it.IncludeMembersto build one destination out of several nested source objects, taking the renames and converters of the included maps with it.MapperFor, for when mapping is on the hot path: it settles everything that does not depend on the object — the generic dispatch through the interface, finding the plan — and hands back aFunc<TSource, TDestination>to call in the loop. That work is most of what a small map costs, and none of it depends on the object, so a loop was paying for it once per item. It is also the one arrangement measured under Mapster.MapFast, the same idea for a single call with nowhere to keep a function.Per-operation values through
IMappingOperationOptions.Items, read back from a converter, resolver or step asResolutionContext.Items.A recursion ceiling on any map that can reach itself, so a looping object graph raises
RecursionLimitExceptioninstead of taking the process down with the stack.Mapping: flat and nested POCOs, flattened paths with null guards, nullables, numeric conversions, enums by name or value,
ToString,IConvertible, collections into arrays,List<>,HashSet<>and the sequence interfaces, and dictionaries with both keys and values converted.Records and any destination built through a constructor, with
ForCtorParamto override an argument and parameter defaults filling what the source does not provide.ReverseMap(), which inverts renamed members and leaves the rest to the conventions.Type converters, value converters and value resolvers. With no container they are built through a public parameterless constructor and kept; with
Mapperion.Extensions.DependencyInjectionone registered in the container comes from it instead, with its own dependencies and under the lifetime you gave it, and one that is not falls back to that constructor.BeforeMapandAfterMap, as a lambda or as anIMappingActiontype. A map with a type converter runs neither: the converter replaces the whole map.Mapperion.Extensions.DependencyInjection, which registers the mapper and lets converters and resolvers take their dependencies from the container.ProjectTo, which rewrites a query so the database returns only the columns the destination needs. Verified against EF Core with SQLite, not just built.A frozen configuration model exposed through
MapperConfiguration.Model.Explain(), which writes out what a map resolved to, member by member, and says which of those you configured and which a convention decided.An optional analyser package,
Mapperion.Analyzers, that reads your configuration at compile time and reports the mistakes it can see there: a pair declared twice, a member given two sources, a member both ignored and sourced, andConstructUsingsitting next toForCtorParam. It is a separate package on purpose, so the mapper itself keeps its one selling point of having no dependencies at all:dotnet add package Mapperion.Analyzers
Not yet
A projection cannot build a dictionary or dispatch to a derived map: its shape is fixed
before any row is read. The source generator covers the common shapes but not yet dictionaries,
value resolvers or inheritance. ReverseMap does not unflatten: a member mapped from a nested path
is resolved by convention on the way back, not written into the nested object.
A pilot you can run
samples/Mapperion.Bookshop is a small application that uses the library the way an application
does: a container, two profiles, EF Core over SQLite, AssertIsValid() at startup, a list view
through ProjectTo and a detail view mapped in memory, all over one configuration. It checks its
own output and runs in CI.
dotnet run --project samples/Mapperion.Bookshop
It found a bug on its first day, which is written up in its readme.
Development
dotnet build
dotnet test
Requires the .NET 10 SDK, pinned in global.json. The build treats warnings as errors and enforces
.editorconfig style, so an analyzer failure is intentional.
License
MIT. See LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 is compatible. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.7.2
- Mapperion (>= 0.15.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
-
.NETStandard 2.0
- Mapperion (>= 0.15.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
-
.NETStandard 2.1
- Mapperion (>= 0.15.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
-
net10.0
- Mapperion (>= 0.15.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
-
net8.0
- Mapperion (>= 0.15.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
-
net9.0
- Mapperion (>= 0.15.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.15.1 | 47 | 10/1/2026 |
| 0.15.0 | 46 | 9/30/2026 |
| 0.14.0 | 55 | 9/30/2026 |
| 0.13.0 | 46 | 9/29/2026 |
| 0.12.0 | 51 | 9/29/2026 |
| 0.11.0 | 81 | 9/26/2026 |
| 0.10.0 | 79 | 9/26/2026 |
| 0.9.0-preview.1 | 54 | 9/23/2026 |
| 0.8.0-preview.3 | 53 | 9/23/2026 |