Skip to content

Generated code

Sanam edited this page Oct 7, 2026 · 1 revision

The package contains a source generator. For each Mapper.Map<A, B>(x) call where A and B are concrete types, it emits a method like this and redirects the call to it with a C# interceptor:

internal static UserDto Map0(UserEntity inputObject)
{
    if (inputObject is null)
    {
        throw new ArgumentNullException(nameof(inputObject));
    }

    return new UserDto
    {
        Id = inputObject.Id,
        Name = inputObject.Name,
        Tags = inputObject.Tags
    };
}

That is the code you would write by hand, and it runs at the same speed. Nothing to configure: the package turns interceptors on for you.

See it

<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

Build, then open obj/<Configuration>/<tfm>/generated/DotNetMapper.Generator/DotNetMapper.Generator.MapperGenerator/DotNetMapper.Interceptors.g.cs. Each [InterceptsLocation] attribute is one call site.

When a call is not intercepted

These calls run the runtime fallback instead. It gives the same result, and costs a few nanoseconds more per call after the first.

Call Why
Mapper.Map<TIn, TOut>(x) inside your own generic method The types are only known at runtime
Private, protected or file-local types Generated code cannot name them
Anonymous types (through a generic helper) Generated code cannot name them
Func<A, B> f = Mapper.Map<A, B>; A method group is not a call
Inside Expression<...>, including query clauses over IQueryable Deliberate, see below
Package referenced with ExcludeAssets="analyzers" The generator is off

The first call for each type pair compiles an expression tree once and caches it. Later calls cost one delegate call.

IQueryable and EF Core

A call inside an expression tree is left as Mapper.Map, so a query provider sees the real method instead of a generated one it has never heard of:

var dtos = db.Users.Select(u => Mapper.Map<User, UserDto>(u)).ToList();

EF Core runs a method call in the final Select on the client, after it has loaded the whole entity. If you need SQL to fetch only the columns of the DTO, write that projection by hand.

NativeAOT and trimming

Intercepted calls are plain code and need nothing. Under NativeAOT the runtime fallback interprets its expression tree instead of compiling it: still correct, slower.

If you wrap Map in your own generic method with trim analysis on (IsAotCompatible, PublishTrimmed or PublishAot), you get:

warning IL2091: 'TInput' generic argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties'
in 'DotNetMapper.Mapper.Map<TInput, TOutput>(TInput)'. The generic parameter 'TIn' of
'Convert<TIn, TOut>(TIn)' does not have matching annotations.

Copy the attributes from Map onto your type parameters:

using System.Diagnostics.CodeAnalysis;

static TOut Convert<
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] TIn,
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TOut>(TIn input)
    where TOut : new()
    => Mapper.Map<TIn, TOut>(input);

Turning the generator off

<PackageReference Include="DotNetMapper" Version="2.0.0" ExcludeAssets="analyzers" />

Every call then uses the runtime fallback. Useful to rule the generator out when you suspect it.

Clone this wiki locally