<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Stanislav Vyshchepan</title>
    <description>The latest articles on DEV Community by Stanislav Vyshchepan (@gandjustas).</description>
    <link>https://dev.to/gandjustas</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1298586%2F17164348-c645-4590-9804-d93cbfa2d7a5.png</url>
      <title>DEV Community: Stanislav Vyshchepan</title>
      <link>https://dev.to/gandjustas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gandjustas"/>
    <language>en</language>
    <item>
      <title>How and Why I Built My Own .NET Mapper</title>
      <dc:creator>Stanislav Vyshchepan</dc:creator>
      <pubDate>Mon, 28 Sep 2026 14:16:11 +0000</pubDate>
      <link>https://dev.to/gandjustas/how-and-why-i-built-my-own-net-mapper-32m</link>
      <guid>https://dev.to/gandjustas/how-and-why-i-built-my-own-net-mapper-32m</guid>
      <description>&lt;p&gt;I don't like Clean Architecture, I don't like DTOs and I don't like mappers.&lt;/p&gt;

&lt;p&gt;Together they turn saving one row to the database into a journey through four projects, three layers of abstraction and a &lt;code&gt;Mappings&lt;/code&gt; folder nobody has opened since the initial commit. To add one field to a form you change the entity, the DTO, the command, the validator, the mapping profile and the mapping profile test. Six files for one column. And if you forget one of them, nobody tells you.&lt;/p&gt;

&lt;p&gt;So, naturally, I wrote a mapper.&lt;/p&gt;

&lt;p&gt;Seriously though, there are two places where the problem is real. In this post I'll show what they are, why popular mappers don't really solve them, and what I did about it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI SLOP WARNING. Most of the code in this library was written by LLMs: Qwen, GLM-5.3-Flash and Claude. I set the tasks, reviewed, rewrote the architecture and fixed what they broke - and what I broke myself. If that offends you, feel free to stop reading here. For everyone else, I'll point out who did what along the way.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Originally published in Russian on &lt;a href="https://habr.com/ru/articles/1087338/" rel="noopener noreferrer"&gt;Habr&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a mapper at all
&lt;/h2&gt;

&lt;p&gt;A typical CRUD app. There's an entity with thirty columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;FirstName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;LastName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Bio&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsBlocked&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ...and two dozen more&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;There's a read model and a write model:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserResponse&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;FirstName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;LastName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsBlocked&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpdateUserRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;FirstName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;LastName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Bio&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Nobody designs these models. They are just slices of &lt;code&gt;User&lt;/code&gt;. Two hand-written places connect them to the entity.&lt;/p&gt;

&lt;p&gt;Place one - the projection in a query:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsBlocked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;UserResponse&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;FirstName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;LastName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IsBlocked&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsBlocked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The projection is there so you don't load the whole entity for four fields. Less data over the wire, no change tracking.&lt;/p&gt;

&lt;p&gt;Place two - assignments in a command handler or an endpoint:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bio&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bio&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This code is needed to validate incoming data and to prevent overposting in endpoints.&lt;/p&gt;

&lt;p&gt;Nobody writes blog posts about this code, because there's nothing to discuss. But this is exactly the code that breaks when you least expect it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What can go wrong
&lt;/h2&gt;

&lt;p&gt;A new requirement: the response needs &lt;code&gt;CreatedAt&lt;/code&gt;. I add the property:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserResponse&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// new&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If I forget to add &lt;code&gt;CreatedAt = u.CreatedAt&lt;/code&gt; to the projection, nothing happens. An object initializer doesn't have to set every property. The build is green, the tests are green, and the API returns &lt;code&gt;0001-01-01T00:00:00&lt;/code&gt;. According to the service, your user signed up in the year 1 AD.&lt;/p&gt;

&lt;p&gt;Same with the handler. You add a property to the entity that must be set on write - the assignments are not obliged to set it.&lt;/p&gt;

&lt;p&gt;The compiler only checks one side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rename a property in the source - the build fails&lt;/li&gt;
&lt;li&gt;a new unassigned property in the target - silence&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  What about required?
&lt;/h2&gt;

&lt;p&gt;At this point an attentive reader says: C# 11 has &lt;code&gt;required&lt;/code&gt; for exactly this.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserResponse&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;UserResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="c1"&gt;// error CS9035: Required member 'CreatedAt' must be set in the object initializer.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It works. But only where the compiler creates the object. Runtime mappers create objects via reflection or compiled expressions, bypassing the object initializer. To them &lt;code&gt;required&lt;/code&gt; is just another property. It's not a bug in a particular library, it's a direct consequence of the runtime approach.&lt;/p&gt;

&lt;p&gt;So here's what I actually need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an &lt;code&gt;IQueryable&lt;/code&gt; projection that translates to SQL and follows model changes on its own&lt;/li&gt;
&lt;li&gt;updating an existing entity tracked by EF, without recreating it&lt;/li&gt;
&lt;li&gt;an unassigned target property is a build-time signal, not a &lt;code&gt;null&lt;/code&gt; in production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not a mapping framework. Two places and a mismatch detector.&lt;/p&gt;
&lt;h2&gt;
  
  
  What's on the market
&lt;/h2&gt;

&lt;p&gt;Let's go one by one. Spoiler: it's all bad.&lt;/p&gt;
&lt;h3&gt;
  
  
  AutoMapper
&lt;/h3&gt;

&lt;p&gt;I never added AutoMapper to a project myself. But almost every project I joined already had it. Usually bundled with Clean Architecture, MediatR and that very &lt;code&gt;Mappings&lt;/code&gt; folder.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserProfile&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Profile&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CreateMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UserResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;CreateMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UpdateUserRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Looks innocent. Now point by point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Errors only at runtime.&lt;/strong&gt; Forgot a &lt;code&gt;CreateMap&lt;/code&gt; - you'll find out from &lt;code&gt;AutoMapperMappingException: Missing type map configuration or unsupported mapping&lt;/code&gt; in the logs. A target property has no source - you won't find out at all, it just stays &lt;code&gt;default&lt;/code&gt;. There's &lt;code&gt;AssertConfigurationIsValid()&lt;/code&gt; for that, which you have to remember to call in tests. Guess how often people forget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Renaming silently breaks mappings.&lt;/strong&gt; Convention-based mapping is string matching. Rename a property with an IDE refactoring - the IDE updates every reference, the profile notices nothing. Find Usages on a DTO property won't show where it's populated. F12 leads nowhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Configuration is code, only worse.&lt;/strong&gt; As soon as conventions aren't enough, you get &lt;code&gt;ForMember&lt;/code&gt;, &lt;code&gt;MapFrom&lt;/code&gt;, &lt;code&gt;ConvertUsing&lt;/code&gt;, &lt;code&gt;IValueResolver&lt;/code&gt;, &lt;code&gt;AfterMap&lt;/code&gt;. Resolvers pull dependencies from DI, calculations move into &lt;code&gt;MapFrom&lt;/code&gt;, and a year later the profiles contain business logic nobody sees, because nobody opens profiles. To figure out where a value in the response came from, you find the profile, in the profile the &lt;code&gt;ForMember&lt;/code&gt;, in it the resolver, in the resolver the service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Used for the wrong job.&lt;/strong&gt; AutoMapper's author himself &lt;a href="https://www.jimmybogard.com/automappers-design-philosophy/" rel="noopener noreferrer"&gt;wrote&lt;/a&gt; that the library is designed for one scenario: the destination is a subset of the source, a rich model gets flattened into a view model. If your problem is different, the tool isn't a good fit. Meanwhile projects use AutoMapper to map commands onto entities - exactly the opposite direction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. &lt;code&gt;IMapper&lt;/code&gt; everywhere.&lt;/strong&gt; &lt;code&gt;Map&lt;/code&gt; needs &lt;code&gt;IMapper&lt;/code&gt;, &lt;code&gt;ProjectTo&lt;/code&gt; needs &lt;code&gt;IConfigurationProvider&lt;/code&gt;. So it's &lt;code&gt;AddAutoMapper&lt;/code&gt; in DI, assembly scanning at startup and &lt;code&gt;IMapper&lt;/code&gt; in the constructor of every handler. A dependency injected just to copy fields from one object to another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. &lt;code&gt;required&lt;/code&gt; doesn't work&lt;/strong&gt;, see above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Slow.&lt;/strong&gt; AutoMapper is 8x slower than hand-written code. Numbers below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;License.&lt;/strong&gt; Since version 15 (July 2025) AutoMapper is commercially licensed, the last MIT version is 14. On top of the technical problems that came with the project, you now have a legal one.&lt;/p&gt;

&lt;p&gt;That's a legitimate decision by the author. But if you have to migrate anyway, why migrate to another AutoMapper?&lt;/p&gt;
&lt;h3&gt;
  
  
  Mapperly
&lt;/h3&gt;

&lt;p&gt;Mapperly is the best of the three, which makes its downsides all the more annoying. A source generator, readable C# output, zero reflection. It catches unmapped target members (RMG012) and respects &lt;code&gt;required&lt;/code&gt;, because it emits a plain object initializer and the language itself reports CS9035.&lt;/p&gt;

&lt;p&gt;Now the price:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Mapper&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserMapper&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="n"&gt;UserResponse&lt;/span&gt; &lt;span class="nf"&gt;ToResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;UpdateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UpdateUserRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="n"&gt;IQueryable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ProjectToResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IQueryable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;1. A mapper method per pair.&lt;/strong&gt; Thirty DTOs - thirty partial methods. Plus a separate method for each projection. Plus a separate method for mapping into an existing object. The hand-written mapping we wanted to get rid of became hand-written mapping declarations. Fewer lines, same ceremony.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Configuration moved into attributes.&lt;/strong&gt; A non-standard case is &lt;code&gt;[MapProperty(nameof(User.FirstName), nameof(UserResponse.Name))]&lt;/code&gt; or &lt;code&gt;[MapperIgnoreTarget(...)]&lt;/code&gt;. The same &lt;code&gt;ForMember&lt;/code&gt; from AutoMapper, just in square brackets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Not at the call site.&lt;/strong&gt; Want a mapping - find which mapper declares the method you need. None - go declare one. The mapper becomes one more layer to maintain. Exactly what Clean Architecture already has enough of.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Generic code doesn't work.&lt;/strong&gt; Mapperly only generates code for type pairs known at compile time. If the mapping is called inside a generic method, say a base repository or a generic handler, there are no concrete types, only &lt;code&gt;TEntity&lt;/code&gt; and &lt;code&gt;TDto&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GetAll&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEntity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TEntity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
&lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;IQueryable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;IQueryable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;projected&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;???;&lt;/span&gt; &lt;span class="c1"&gt;// which partial mapper method goes here?&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;projected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Code like this needs a runtime fallback that builds the mapping once the types are known. Mapperly doesn't have one: its generic mapper methods can only pick from pairs declared upfront in the same mapper.&lt;/p&gt;
&lt;h3&gt;
  
  
  Mapster
&lt;/h3&gt;

&lt;p&gt;Mapster is AutoMapper that decided to skip the profiles. Easier to start with and faster, but it has architectural problems too.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Adapt&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;                                    &lt;span class="c1"&gt;// new object&lt;/span&gt;
&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Adapt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                                                     &lt;span class="c1"&gt;// into existing object&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProjectToType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// projection&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Non-standard cases go into configuration:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;TypeAdapterConfig&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UserResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FullName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FirstName&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;Adapt&lt;/code&gt; hangs off &lt;code&gt;object&lt;/code&gt;.&lt;/strong&gt; The method is an extension on &lt;code&gt;object&lt;/code&gt;, so IntelliSense offers it on everything. &lt;code&gt;42.Adapt&amp;lt;UserResponse&amp;gt;()&lt;/code&gt; compiles just fine. The compiler doesn't know the source type, so there's nothing to check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Global mutable state.&lt;/strong&gt; By default configuration lives in the static &lt;code&gt;TypeAdapterConfig.GlobalSettings&lt;/code&gt;. Any code in any assembly can change it, and initialization order is your problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Errors at runtime.&lt;/strong&gt; A mapping is compiled on first use. You can enable a check that every target member has a source: &lt;code&gt;RequireDestinationMemberSource = true&lt;/code&gt; plus a &lt;code&gt;Compile()&lt;/code&gt; call at startup. It's off by default. And even when enabled, it fires when the app starts, not when it builds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A generator on the side.&lt;/strong&gt; Mapster does have code generation, but it's a separate tool, Mapster.Tool. You install it as a dotnet tool and wire it up to run after build:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Target&lt;/span&gt; &lt;span class="na"&gt;Name=&lt;/span&gt;&lt;span class="s"&gt;"Mapster"&lt;/span&gt; &lt;span class="na"&gt;AfterTargets=&lt;/span&gt;&lt;span class="s"&gt;"AfterBuild"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Exec&lt;/span&gt; &lt;span class="na"&gt;WorkingDirectory=&lt;/span&gt;&lt;span class="s"&gt;"$(ProjectDir)"&lt;/span&gt; &lt;span class="na"&gt;Command=&lt;/span&gt;&lt;span class="s"&gt;"dotnet tool restore"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;Exec&lt;/span&gt; &lt;span class="na"&gt;WorkingDirectory=&lt;/span&gt;&lt;span class="s"&gt;"$(ProjectDir)"&lt;/span&gt;
        &lt;span class="na"&gt;Command=&lt;/span&gt;&lt;span class="s"&gt;"dotnet mapster extension -a &amp;amp;quot;$(TargetDir)$(ProjectName).dll&amp;amp;quot;"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Target&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The tool reads the already built DLL and generates mapper files from attributes, interfaces or fluent configuration. For example, with attributes:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;AdaptTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[name]Dto"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;GenerateMapper&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Generates a UserDto class and a static UserMapper:&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AdaptToDto&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AdaptTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existingDto&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserMapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProjectToDto&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;What's wrong here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it's not a Roslyn source generator that runs in the IDE on every change, it's a separate post-build step&lt;/li&gt;
&lt;li&gt;generation happens after compilation, so a fresh mapper only makes it into the build on the next build&lt;/li&gt;
&lt;li&gt;the tool has to be installed locally, on CI and for every new developer&lt;/li&gt;
&lt;li&gt;the generated code has a different API, not &lt;code&gt;Adapt&amp;lt;T&amp;gt;()&lt;/code&gt;: switching from runtime to generation means rewriting every call&lt;/li&gt;
&lt;li&gt;no diagnostics at the call site&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Who provides it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Call at the site, no mapper classes&lt;/td&gt;
&lt;td&gt;Mapster, AutoMapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No DI registration&lt;/td&gt;
&lt;td&gt;Mapster, Mapperly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build error if a target member is unassigned&lt;/td&gt;
&lt;td&gt;Mapperly (at a price)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;IQueryable&lt;/code&gt; projection to SQL&lt;/td&gt;
&lt;td&gt;AutoMapper, Mapster, Mapperly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works in generic code&lt;/td&gt;
&lt;td&gt;AutoMapper, Mapster (runtime)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;required&lt;/code&gt; is respected&lt;/td&gt;
&lt;td&gt;Mapperly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All of the above, no ceremony&lt;/td&gt;
&lt;td&gt;Nobody&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When the market is empty, you have two options: live with it or write your own. I chose the second.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I want
&lt;/h2&gt;

&lt;p&gt;At first I wanted this API:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MapTo&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;            &lt;span class="c1"&gt;// new object&lt;/span&gt;
&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                             &lt;span class="c1"&gt;// into existing object&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProjectTo&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// projection to SQL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It works for a runtime implementation: the method is declared as &lt;code&gt;MapTo&amp;lt;TTarget&amp;gt;(this object source)&lt;/code&gt;, and the source type comes from &lt;code&gt;source.GetType()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It doesn't work for a generator. The generator needs the static source type, so it has to be in the signature: &lt;code&gt;MapTo&amp;lt;TSource, TTarget&amp;gt;(this TSource source)&lt;/code&gt;. And C# infers generic arguments all or nothing. You can't specify only &lt;code&gt;TTarget&lt;/code&gt;, so the call turns into this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MapTo&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UserResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProjectTo&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UserResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Verbose, and the source type is repeated at every call. Mapping into an existing object is fine - both types are inferred from the arguments. But you don't design an API around one method.&lt;/p&gt;

&lt;p&gt;So the call is split into two steps. An extension method returns a wrapper struct that captures the source type, and the wrapper has &lt;code&gt;To&amp;lt;TTarget&amp;gt;()&lt;/code&gt; where you only specify the target:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;FusionSource&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TSource&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;TSource&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;TTarget&lt;/span&gt; &lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TTarget&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;FusionMapper&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TTarget&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;TTarget&lt;/span&gt; &lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TTarget&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;TTarget&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;FusionMapper&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TTarget&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The wrapper is a struct, so there are no extra allocations. The final API:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;            &lt;span class="c1"&gt;// new object&lt;/span&gt;
&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                             &lt;span class="c1"&gt;// into existing object&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// projection to SQL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No profiles, no mapper classes, no DI registration. A target member without a source - a warning at the call site. A &lt;code&gt;required&lt;/code&gt; member without a source - a compile error.&lt;/p&gt;

&lt;p&gt;The library is called FusionMapper: a fusion of a runtime mapper and a source-generated one. Considering how much of the code AI generated, I wanted to call it SlopMapper.&lt;/p&gt;
&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;Here's how I did it and what I ran into.&lt;/p&gt;
&lt;h3&gt;
  
  
  Runtime first
&lt;/h3&gt;

&lt;p&gt;The first commit is August 5. I deliberately started not with a generator but with a prototype on expression trees.&lt;/p&gt;

&lt;p&gt;Generators are slow to iterate on. And mapping conventions have to be defined first, which is easiest on a working core: add a rule, run the tests.&lt;/p&gt;

&lt;p&gt;The runtime mapper and its tests were mostly written by Qwen. The free one, right in the chat on its website, no IDE integration. I described a rule, got code and tests, ran them, pasted back the errors. I only did some refactoring myself. The first days went into semantics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name matching: exact, case-insensitive, ignoring a leading underscore; fields on par with properties&lt;/li&gt;
&lt;li&gt;flattening: &lt;code&gt;CustomerAddressCity&lt;/code&gt; ← &lt;code&gt;Customer.Address.City&lt;/code&gt;, at any depth&lt;/li&gt;
&lt;li&gt;nullable reference types: going through a nullable object yields &lt;code&gt;null&lt;/code&gt;, not a &lt;code&gt;NullReferenceException&lt;/code&gt;; a nullable string into a non-nullable member is a contract error&lt;/li&gt;
&lt;li&gt;constructors, &lt;code&gt;required&lt;/code&gt; and &lt;code&gt;init&lt;/code&gt;: constructor selection with parameters matched by name, &lt;code&gt;[SetsRequiredMembers]&lt;/code&gt; is honored&lt;/li&gt;
&lt;li&gt;aggregates - more on those below&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By August 12 there were benchmarks, and the expression-based version was already faster than Mapster. But it didn't deliver the main thing: errors still appeared only at runtime, and the IDE stayed silent until you ran the tests.&lt;/p&gt;
&lt;h3&gt;
  
  
  Aggregates
&lt;/h3&gt;

&lt;p&gt;Almost every view model has a summary over a collection: the sum of order lines, "has active lines", the last buyer.&lt;/p&gt;

&lt;p&gt;I was curious how others handle this. AutoMapper has exactly one aggregate by convention - &lt;code&gt;Count&lt;/code&gt;. And it's not a feature but a side effect of flattening: the name &lt;code&gt;LinesCount&lt;/code&gt; splits into &lt;code&gt;Lines&lt;/code&gt; + &lt;code&gt;Count&lt;/code&gt;, and &lt;code&gt;Count&lt;/code&gt; is just a property of &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;. Declare the collection as &lt;code&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/code&gt; and it stops working. I checked on AutoMapper 15: &lt;code&gt;LinesCount&lt;/code&gt; maps without configuration, &lt;code&gt;LinesAmountSum&lt;/code&gt; silently stays zero. Everything else is written by hand:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;CreateMap&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OrderDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ForMember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ForMember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasActive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsActive&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In FusionMapper the aggregate is described by the property name:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;DTO property name&lt;/th&gt;
&lt;th&gt;Generated code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LinesCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Lines.Count&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LinesAmountSum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Lines.Sum(x =&amp;gt; x.Amount)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;LinesAmountAverage&lt;/code&gt;, &lt;code&gt;...Max&lt;/code&gt;, &lt;code&gt;...Min&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;aggregate over a selector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;LinesIsActiveAny&lt;/code&gt;, &lt;code&gt;LinesIsActiveAll&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Lines.Any/All(x =&amp;gt; x.IsActive)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LinesBuyerLastOrDefault&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Lines.Select(x =&amp;gt; x.Buyer).LastOrDefault()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eleven aggregates in total. No configuration, and inside &lt;code&gt;Project().To&amp;lt;&amp;gt;()&lt;/code&gt; it all goes to SQL.&lt;/p&gt;

&lt;p&gt;Two honest caveats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the property name is a magic string, a typo in the suffix is caught not by an error but by the FMAP005 warning&lt;/li&gt;
&lt;li&gt;empty collections behave like LINQ: &lt;code&gt;Average&lt;/code&gt;, &lt;code&gt;Max&lt;/code&gt;, &lt;code&gt;First&lt;/code&gt; throw, which is what the &lt;code&gt;*OrDefault&lt;/code&gt; variants are for&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And yes, if you really want the property to be called just &lt;code&gt;Total&lt;/code&gt;, you'll have to give in. There's no &lt;code&gt;ForMember&lt;/code&gt; equivalent.&lt;/p&gt;
&lt;h3&gt;
  
  
  Source generator
&lt;/h3&gt;

&lt;p&gt;I took the source generator scaffold with interceptors from Andrew Lock - he has detailed posts and samples on the topic. Then I asked Qwen to turn the runtime mapper into a source generator on top of that scaffold. The mapping logic moved over almost unchanged, only the output changed: C# strings instead of &lt;code&gt;Expression&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Interceptors
&lt;/h3&gt;

&lt;p&gt;The main idea I wanted to test was C# interceptors. A generator can replace a specific call on a specific line with its own method. The user writes:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UserDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And the compiler calls the generated method:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;UserDto&lt;/span&gt; &lt;span class="nf"&gt;Map__User__To__UserDto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;UserDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Age&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Age&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A plain object initializer, zero reflection. And an impossible mapping becomes a compile error, not an exception in production.&lt;/p&gt;

&lt;p&gt;Sounds great, but in practice debugging is very hard. If you mess up the interceptors, the call simply doesn't get replaced, and you don't even know whether the generated method or the runtime fallback ran. You only see it in the stack trace if a test fails inside the generated method.&lt;/p&gt;

&lt;p&gt;There's one more constraint that cost me a lot of time. You can't apply an interceptor inside an expression tree: there's no method call there, only data describing the call. So the generator has to filter call sites and skip those inside a lambda compiled into &lt;code&gt;Expression&amp;lt;...&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first working version of interceptors for all &lt;code&gt;.To&amp;lt;T&amp;gt;()&lt;/code&gt; calls was generated by Qwen. It had that filter. Then I accidentally broke it during a refactoring. Interceptors started being applied to calls inside expressions, and the compiler started crashing. Not reporting an error in the code - crashing entirely, with no meaningful message and no line number.&lt;/p&gt;

&lt;p&gt;I debugged it for a long time. The commit history for August 15-16:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Added interceptors for To method calls
Refactoring? temporary disabled generators
Восстановлены интерсепторы
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;When I finally fixed it, I wrote the commit message in Russian, my native language ("Interceptors restored"). The only time in the entire repository history. I think that says it all.&lt;/p&gt;

&lt;p&gt;So we end up with two modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;.NET 9+ - interceptors by default, no reflection, an impossible mapping is a compile error&lt;/li&gt;
&lt;li&gt;.NET 8 or &lt;code&gt;EnableFusionMapperInterceptor=false&lt;/code&gt; - generated mappers are registered at startup via &lt;code&gt;[ModuleInitializer]&lt;/code&gt;, whatever the generator couldn't build is built at runtime, and diagnostics become warnings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The expression-based runtime version never went away, and that solves the generic code problem. If a call sits inside a generic method and the types aren't known at compile time, the generator skips it, and the mapping is built at runtime on the first call with concrete types:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;GetAll&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEntity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;TEntity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
    &lt;span class="err"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Where types are known, generated code runs. Where they aren't, the runtime fallback does. The calling code is the same.&lt;/p&gt;
&lt;h3&gt;
  
  
  Projections
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Project().To&amp;lt;T&amp;gt;()&lt;/code&gt; needs not code but data - an &lt;code&gt;Expression&amp;lt;Func&amp;lt;TSource, TTarget&amp;gt;&amp;gt;&lt;/code&gt; that EF Core translates to SQL. Interceptors don't help here. So the generator builds the expressions ahead of time and, via the same &lt;code&gt;[ModuleInitializer]&lt;/code&gt;, puts them in a cache when the assembly loads. No expression trees are built at runtime.&lt;/p&gt;
&lt;h3&gt;
  
  
  Caching in the generator
&lt;/h3&gt;

&lt;p&gt;What Qwen generated worked, but it worked slowly. Roslyn runs a generator on practically every keystroke, and if the generator recomputes every mapping each time, Visual Studio on a large solution gets noticeably sluggish.&lt;/p&gt;

&lt;p&gt;An incremental generator caches the results of pipeline steps, but only if the models between steps compare by value. Arrays don't compare by value, and you can't keep references to Roslyn symbols in the model. So I rewrote the generator architecture myself: models became immutable records without references to the semantic model, collections became &lt;code&gt;EquatableArray&lt;/code&gt;, hash codes are computed explicitly. I borrowed &lt;code&gt;EquatableArray&lt;/code&gt; and the generator caching tests from Andrew Lock as well. Such a test runs the generator a second time on a copy of the same compilation and checks that every tracked pipeline step returned &lt;code&gt;Cached&lt;/code&gt; or &lt;code&gt;Unchanged&lt;/code&gt;, meaning nothing was recomputed. Meanwhile Qwen was fixing the failing tests.&lt;/p&gt;

&lt;p&gt;Boring engineering, but it's what separates "works on my machine" from "works for users". And it's the part the LLM didn't do on its own: code without caching passes all the tests, the problem is only visible in the IDE.&lt;/p&gt;
&lt;h2&gt;
  
  
  Control
&lt;/h2&gt;

&lt;p&gt;This is what the whole thing was for.&lt;/p&gt;

&lt;p&gt;You added a property to &lt;code&gt;UserResponse&lt;/code&gt; and forgot the source - every call site gets a warning:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;warning FMAP005: The following members of 'UserResponse' have no matching source members
and will keep their default values: CreatedAt.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A member is intentionally not mapped - put &lt;code&gt;[FusionMapperIgnore]&lt;/code&gt; on it. It's the only attribute in the whole API. You can suppress FMAP005 for the whole project, but then it's unclear why you need this library.&lt;/p&gt;

&lt;p&gt;If a member is &lt;code&gt;required&lt;/code&gt; and has no source, that's FMAP001: a compile error with interceptors, or a &lt;code&gt;MappingException&lt;/code&gt; listing the members in runtime mode. The generator builds a real object initializer, so the language's promise is finally kept.&lt;/p&gt;

&lt;p&gt;One rule for every ambiguity: a build diagnostic or an exception listing the members. Never a zero in a field.&lt;/p&gt;
&lt;h2&gt;
  
  
  Collections
&lt;/h2&gt;

&lt;p&gt;A naive generator emits &lt;code&gt;Select(...).ToList()&lt;/code&gt; for everything. But the target can be &lt;code&gt;IReadOnlyList&amp;lt;T&amp;gt;&lt;/code&gt;, a collection with an &lt;code&gt;IEnumerable&amp;lt;T&amp;gt;&lt;/code&gt; constructor, a collection with &lt;code&gt;AddRange&lt;/code&gt;, or with just &lt;code&gt;Add&lt;/code&gt;. The generator looks at the target type and picks how to create it. For interfaces it emits a collection expression &lt;code&gt;[.. items]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Mapping into an existing object is a separate story. You can't just replace the collection with a new one: WPF or MAUI bindings break, and EF's change tracker sees something very different from what you expect. So it calls &lt;code&gt;Clear()&lt;/code&gt; and then &lt;code&gt;AddRange()&lt;/code&gt; or &lt;code&gt;Add()&lt;/code&gt; in a loop, keeping the same collection reference. A small thing you only learn about after catching the bug.&lt;/p&gt;
&lt;h2&gt;
  
  
  Benchmarks
&lt;/h2&gt;

&lt;p&gt;BenchmarkDotNet, Intel i9-9900KF, .NET 10.&lt;/p&gt;

&lt;p&gt;Flat object:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Mean&lt;/th&gt;
&lt;th&gt;Allocated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FusionMapper&lt;/td&gt;
&lt;td&gt;6.24 ns&lt;/td&gt;
&lt;td&gt;48 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mapperly&lt;/td&gt;
&lt;td&gt;6.56 ns&lt;/td&gt;
&lt;td&gt;48 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hand-written&lt;/td&gt;
&lt;td&gt;6.61 ns&lt;/td&gt;
&lt;td&gt;48 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mapster&lt;/td&gt;
&lt;td&gt;16.66 ns&lt;/td&gt;
&lt;td&gt;48 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AutoMapper&lt;/td&gt;
&lt;td&gt;52.08 ns&lt;/td&gt;
&lt;td&gt;48 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Faster than hand-written code by 0.37 nanoseconds is noise, not a win. Parity with Mapperly, which is even slightly ahead on nested graphs. The only real difference is against AutoMapper on hot paths: 6-8x.&lt;/p&gt;

&lt;p&gt;Nested object, 4 levels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Mean&lt;/th&gt;
&lt;th&gt;Allocated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hand-written&lt;/td&gt;
&lt;td&gt;8.88 ns&lt;/td&gt;
&lt;td&gt;48 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mapperly&lt;/td&gt;
&lt;td&gt;16.68 ns&lt;/td&gt;
&lt;td&gt;48 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FusionMapper&lt;/td&gt;
&lt;td&gt;17.44 ns&lt;/td&gt;
&lt;td&gt;48 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mapster&lt;/td&gt;
&lt;td&gt;39.17 ns&lt;/td&gt;
&lt;td&gt;48 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AutoMapper&lt;/td&gt;
&lt;td&gt;104.04 ns&lt;/td&gt;
&lt;td&gt;48 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;EF query projection, 1000 rows: hand-written 1.09 ms, AutoMapper 1.11 ms, Mapster 1.13 ms, FusionMapper 1.14 ms. Parity, which makes sense: the database does all the work.&lt;/p&gt;

&lt;p&gt;Performance was never the goal. It's a consequence of the approach: if you generate the same code you'd write by hand, it runs just as fast.&lt;/p&gt;

&lt;p&gt;The optimizations after benchmarking were done by GLM-5.3-Flash.&lt;/p&gt;
&lt;h2&gt;
  
  
  A real project
&lt;/h2&gt;

&lt;p&gt;A mapper always looks good on a &lt;code&gt;User&lt;/code&gt; class, so I took Jason Taylor's Clean Architecture template: .NET 10, Aspire, EF Core, MediatR. And removed AutoMapper 16 from it completely.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5 files changed&lt;/li&gt;
&lt;li&gt;3 profiles deleted entirely, along with the only &lt;code&gt;ForMember&lt;/code&gt; in the whole project&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AddAutoMapper&lt;/code&gt; removed from DI, there's nothing to register anymore&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IMapper&lt;/code&gt; is no longer injected into the handler&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hardest part - the EF projection:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="n"&gt;Lists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TodoLists&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProjectTo&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TodoListDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;_mapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConfigurationProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="n"&gt;Lists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TodoLists&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Project&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TodoListDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Both translate to SQL. The mapping didn't go anywhere, the models are the same. The infrastructure around it is gone.&lt;/p&gt;

&lt;p&gt;The example was converted by GLM-5.3-Flash too. A perfect task for it: mechanical, with a clear definition of done - it builds and the tests are green.&lt;/p&gt;

&lt;p&gt;The example code is here: &lt;a href="https://github.com/gandjustas/FusionMapper/tree/main/examples/CleanArchitecture" rel="noopener noreferrer"&gt;https://github.com/gandjustas/FusionMapper/tree/main/examples/CleanArchitecture&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What's not there
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;manual configuration: no &lt;code&gt;MapFrom&lt;/code&gt;, &lt;code&gt;ConvertUsing&lt;/code&gt; or resolvers&lt;/li&gt;
&lt;li&gt;cyclic graphs&lt;/li&gt;
&lt;li&gt;runtime polymorphism: mapping uses static types&lt;/li&gt;
&lt;li&gt;support for anything below .NET 8&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I checked every feature with one question: is it needed in those two places from the beginning of the post? Polymorphism - no. Cycles - no. Configuration - no.&lt;/p&gt;
&lt;h2&gt;
  
  
  When you don't need FusionMapper
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;your project has three DTOs - write the mapping by hand or let AI generate it, that's fine&lt;/li&gt;
&lt;li&gt;the mapping can't be derived from names and you need manual configuration&lt;/li&gt;
&lt;li&gt;models with cyclic references or polymorphism - that's not about DTOs anymore&lt;/li&gt;
&lt;li&gt;your project is on .NET Framework or .NET 6&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  When you do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;lots of EF projections and handlers that populate entities&lt;/li&gt;
&lt;li&gt;you want a model change to break the build, not production&lt;/li&gt;
&lt;li&gt;you're moving off AutoMapper after the license change and don't want a mapper class per type pair&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Who did what, all in one place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Qwen - runtime mapper, tests, port to a source generator, fixing tests&lt;/li&gt;
&lt;li&gt;GLM-5.3-Flash - optimizations after benchmarks, converting the examples&lt;/li&gt;
&lt;li&gt;Claude - final polishing&lt;/li&gt;
&lt;li&gt;me - API, conventions, generator architecture with caching, decisions about what the library won't do&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the first commit to a NuGet package took 16 days. 37 commits, 201 tests. Without LLMs it would have taken many times longer. But the things the LLMs didn't do on their own - caching in the generator and saying no to extra features - are exactly what decided whether this became a library at all.&lt;/p&gt;

&lt;p&gt;The main thing I took away: silence is the worst kind of error. Or, as a wise book put it: crash, don't trash.&lt;/p&gt;

&lt;p&gt;The moral: if your project has three DTOs, write the mapping by hand. If it has thirty, let the build watch them, not your users in production.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/gandjustas" rel="noopener noreferrer"&gt;
        gandjustas
      &lt;/a&gt; / &lt;a href="https://github.com/gandjustas/FusionMapper" rel="noopener noreferrer"&gt;
        FusionMapper
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Modern, high-performance object mapping library for .NET
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;FusionMapper&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;
&lt;a href="https://www.nuget.org/packages/FusionMapper" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/373abb51a58503aa53b0c578dd6f94e3d0ba54573b0e007d1870896155d3582f/68747470733a2f2f696d672e736869656c64732e696f2f6e756765742f762f467573696f6e4d61707065722e7376673f6c6162656c3d4e7547657426636f6c6f723d696e666f726d6174696f6e616c" alt="NuGet"&gt;&lt;/a&gt;
&lt;a href="https://www.nuget.org/packages/FusionMapper" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e45f6be5460bd8ffe3dffa53bafea305489a0cf3de483e4c6487dee5b65958df/68747470733a2f2f696d672e736869656c64732e696f2f6e756765742f767072652f467573696f6e4d61707065722e7376673f6c6162656c3d4e754765742532307072657669657726636f6c6f723d6f72616e6765" alt="NuGet Pre-release"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/8883ba754d12b1f734ea7e94372278479a4b1ba094675aaaa5c52c3d0ff6f8aa/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2e4e45542d382532302537432532303925323025374325323031302d353132424434"&gt;&lt;img src="https://camo.githubusercontent.com/8883ba754d12b1f734ea7e94372278479a4b1ba094675aaaa5c52c3d0ff6f8aa/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2e4e45542d382532302537432532303925323025374325323031302d353132424434" alt=".NET"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/1ccb4446d6793530c72d20c9bb7c0dc7b4bd00c16a7b232aecc76fba806bb15a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f432532332d3132253230253743253230313325323025374325323031342d323339313230"&gt;&lt;img src="https://camo.githubusercontent.com/1ccb4446d6793530c72d20c9bb7c0dc7b4bd00c16a7b232aecc76fba806bb15a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f432532332d3132253230253743253230313325323025374325323031342d323339313230" alt="C#"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FusionMapper&lt;/strong&gt; is a modern, high-performance object mapping library for .NET. It combines the developer-friendly, zero-configuration convention-based approach of &lt;strong&gt;AutoMapper&lt;/strong&gt; with the compile-time code generation and zero-overhead performance of &lt;strong&gt;Mapperly&lt;/strong&gt; and &lt;strong&gt;Mapster&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Built for .NET 8/9/10 and C# 12/13/14, it leverages &lt;strong&gt;Source Generators&lt;/strong&gt; and &lt;strong&gt;C# Interceptors&lt;/strong&gt; to eliminate runtime reflection, boilerplate configuration, and mapping overhead.&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;📦 1. Installation&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;Install FusionMapper via NuGet:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Package Manager:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="highlight highlight-source-powershell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c1"&gt;Install-Package&lt;/span&gt; FusionMapper &lt;span class="pl-k"&gt;-&lt;/span&gt;AllowPrereleaseVersions&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;.NET CLI:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;dotnet add package FusionMapper --prerelease &lt;/pre&gt;

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; FusionMapper uses Source Generators and C# Interceptors. Ensure your project targets at least &lt;strong&gt;.NET 8.0&lt;/strong&gt; and uses &lt;strong&gt;C# 12&lt;/strong&gt; or higher. Interceptors are fully utilized on .NET 9+.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;🚀 2. Basic Usage&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;FusionMapper requires &lt;strong&gt;zero configuration&lt;/strong&gt;. No profiles, no &lt;code&gt;CreateMap&lt;/code&gt;, no manual setup. Just use the fluent extension methods.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Create a new object&lt;/h3&gt;

&lt;/div&gt;

&lt;div class="highlight highlight-source-cs notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;var&lt;/span&gt; &lt;span class="pl-s1"&gt;source&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-smi"&gt;User&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-s1"&gt;FirstName&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;"Alice"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;LastName&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;"Smith"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;Age&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;30&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/gandjustas/FusionMapper" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package FusionMapper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you find a case where the conventions broke silently, that's a bug by definition. Open an issue on GitHub.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
