Table of Contents

The configuration analyzer

Mapperion.Analyzers is a separate, optional package. It reads your mapping configuration while the project compiles and reports the mistakes that are visible in what you wrote.

dotnet add package Mapperion.Analyzers

It is separate so that Mapperion itself keeps having no dependencies at all. Nothing else changes: the analyzer ships no runtime code and nothing of it reaches your output.

What it reports

Everything is a warning, never an error. Two of the four describe something that throws when the configuration is built, so an error would be defensible, but the same two can be written across branches of an if where only one of them ever runs. A project that wants these to stop the build already turns warnings into errors.

MPR1001 — the pair is already declared

cfg.CreateMap<Person, PersonDto>();
cfg.CreateMap<Person, PersonDto>();   // MPR1001

A pair may be declared once per configuration. The second one raises MapperConfigurationException when the configuration is built, so this only moves the complaint earlier — but earlier is where it is cheap.

MPR1002 — the member is given a source more than once

cfg.CreateMap<Person, PersonDto>()
   .ForMember(d => d.Name, o => o.MapFrom(s => s.Name))
   .ForMember(d => d.Name, o => o.MapFrom(s => s.Nickname));   // MPR1002

MapFrom replaces whatever source the member had, so the first one is simply gone while still reading as though it applies.

This is about the source alone. Settings accumulate, so splitting a member's configuration across two calls is a style rather than a mistake:

cfg.CreateMap<Person, PersonDto>()
   .ForMember(d => d.Name, o => o.MapFrom(s => s.Name))
   .ForMember(d => d.Name, o => o.SetMappingOrder(5));         // nothing reported

MPR1003 — the member is both ignored and given a source

cfg.CreateMap<Person, PersonDto>()
   .ForMember(d => d.Name, o => { o.MapFrom(s => s.Name); o.Ignore(); });   // MPR1003

Ignore drops the source and MapFrom clears the ignore, so one of the two is doing nothing and which one depends on the order. It reads the same written across two calls, where it is easier to miss, and it is reported there too.

MPR1004 — the destination is built two ways

cfg.CreateMap<Person, PersonRecord>()
   .ConstructUsing(s => new PersonRecord(s.Name, s.Age))
   .ForCtorParam("name", o => o.MapFrom(s => s.Name));   // MPR1004

ConstructUsing hands the whole construction to a factory, so there are no constructor arguments left for ForCtorParam to configure.

What it does not do

It does not tell you whether a destination member will find a source. That is what AssertIsValid() is for. Answering it at compile time would mean a second copy of the convention engine living beside the first one and drifting from it, and a wrong answer from an analyzer is worse than no answer.

It follows fluent chains. A configuration that holds the expression in a local and calls into it later is not followed, and nothing is reported about it rather than something wrong:

var map = cfg.CreateMap<Person, PersonDto>();
map.ForMember(d => d.Name, o => o.MapFrom(s => s.Name));
map.ForMember(d => d.Name, o => o.MapFrom(s => s.Nickname));   // not reported

It knows the difference between your builder and ours. Everything is matched by symbol, so a ForMember on somebody else's fluent API is left alone.

Turning one off

Like any analyzer, in .editorconfig:

dotnet_diagnostic.MPR1002.severity = none

Or at one site, where the mistake is deliberate — a test that checks the configuration rejects it, for instance:

#pragma warning disable MPR1001
// ...
#pragma warning restore MPR1001

The library's own test suite does exactly that in three places, and runs the analyzer over itself everywhere else.