Programmazione

AOT-Friendly DTO Mapping in .NET: Source Generators al posto della reflection

Dario Fadda Aprile 19, 2026

Con l’adozione crescente di NativeAOT e il trimming in .NET, uno dei nodi critici per molti progettisti è la gestione del mapping tra oggetti: le librerie classiche come AutoMapper si basano pesantemente sulla reflection a runtime, che è incompatibile con la compilazione Ahead-of-Time. In questo articolo esploreremo ElBruno.AotMapper, una libreria che risolve il problema spostando la generazione del codice di mapping a compile time grazie ai Roslyn Source Generators.

Il problema: reflection e AOT non vanno d’accordo

Quando si compila un’applicazione .NET con PublishAot=true oppure si abilita il trimming aggressivo, il runtime non può più analizzare dinamicamente i tipi come farebbe normalmente. Le librerie che usano System.Reflection per scoprire proprietà e invocare setter al volo — come AutoMapper nella sua configurazione classica — provocano errori in fase di esecuzione o vengono tagliate dal trimmer.

Le alternative tradizionali per chi vuole restare AOT-compatible sono:

  • Scrivere il mapping a mano (tedioso e soggetto a errori)
  • Usare Mapster con configurazione esplicita (verbosa)
  • Affidarsi a Source Generators che generano il codice prima della compilazione

ElBruno.AotMapper percorre la terza strada: usa un Source Generator basato su Roslyn per emettere metodi di estensione fortemente tipizzati già al momento del build, con zero reflection a runtime.

Come funziona: Source Generators al posto della reflection

I Roslyn Source Generators sono componenti che vengono eseguiti durante la compilazione e possono produrre file C# aggiuntivi. In questo caso, il generator analizza le vostre classi annotate con attributi specifici e genera automaticamente i metodi di mapping corrispondenti.

I vantaggi concreti di questo approccio:

  • Gli errori di mapping emergono in fase di compilazione, non a runtime
  • Zero overhead da reflection: il codice generato è codice C# diretto, ottimizzabile dal JIT o dall’AOT compiler
  • Compatibilità completa con NativeAOT e applicazioni trimmate
  • Il codice generato è visibile e debuggabile (potete ispezionarlo nelle cartelle di build)

Installazione

La libreria si divide in due package NuGet distinti:

# Attributi e interfacce core
dotnet add package ElBruno.AotMapper

# Il Source Generator (solo per il progetto che contiene i DTO)
dotnet add package ElBruno.AotMapper.Generator

Il Generator va aggiunto come PrivateAssets="all" in genere, per evitare che la dipendenza si propaghi ai progetti dipendenti:

<PackageReference Include="ElBruno.AotMapper.Generator" Version="x.y.z">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

Utilizzo degli attributi di mapping

Mapping base con [MapFrom]

Il caso più semplice: due classi con le stesse proprietà. Si annota il DTO destinazione con [MapFrom] specificando il tipo sorgente:

// Entità del dominio
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public DateTime CreatedAt { get; set; }
}

// DTO di risposta annotato
[MapFrom(typeof(Product))]
public class ProductDto
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
}

Il Source Generator analizza questo codice e genera automaticamente un metodo di estensione. Dopo la compilazione potrete usarlo così:

var product = await dbContext.Products.FindAsync(id);
var dto = product.ToProductDto(); // Metodo generato, zero reflection

Rimappatura di proprietà con [MapProperty]

Quando i nomi delle proprietà non corrispondono tra sorgente e destinazione, si usa [MapProperty] per indicare esplicitamente il mapping:

[MapFrom(typeof(User))]
public class UserSummaryDto
{
    public int Id { get; set; }

    [MapProperty(nameof(User.FirstName))]
    public string Nome { get; set; } = string.Empty;  // Diverso da FirstName

    [MapProperty(nameof(User.LastName))]
    public string Cognome { get; set; } = string.Empty;
}

Ignorare proprietà con [MapIgnore]

Per escludere campi sensibili o non necessari:

[MapFrom(typeof(User))]
public class PublicUserDto
{
    public int Id { get; set; }
    public string UserName { get; set; } = string.Empty;

    [MapIgnore]
    public string? PasswordHash { get; set; }  // Non verrà mappato
}

Conversioni custom con [MapConverter]

Per logiche di conversione non banali, si implementa IMapConverter<TSource, TDestination>:

public class PriceToStringConverter : IMapConverter<decimal, string>
{
    public string Convert(decimal source) => source.ToString("C2");
}

[MapFrom(typeof(Product))]
public class ProductDisplayDto
{
    public string Name { get; set; } = string.Empty;

    [MapConverter(typeof(PriceToStringConverter))]
    public string FormattedPrice { get; set; } = string.Empty;
}

Integrazione in un Minimal API ASP.NET Core

Il package AspNetCore della libreria facilita l’integrazione con Dependency Injection. Ecco un esempio completo di endpoint:

// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(...);

var app = builder.Build();

app.MapGet("/products/{id}", async (int id, AppDbContext db) =>
{
    var product = await db.Products.FindAsync(id);
    if (product is null) return Results.NotFound();

    // ToProductDto() è un metodo generato dal Source Generator
    return Results.Ok(product.ToProductDto());
});

app.Run();

Quando si pubblica con dotnet publish -r win-x64 -p:PublishAot=true, il codice di mapping generato viene incluso senza problemi perché è codice C# statico, non reflection dinamica.

Considerazioni pratiche

La libreria è indicata soprattutto per chi:

  • Sta migrando applicazioni verso NativeAOT o vuole abilitare il trimming
  • Sviluppa microservizi con startup time critico (AOT avvia le app molto più velocemente)
  • Preferisce avere errori di mapping evidenziati a compile time
  • Vuole ispezionare il codice generato per capire esattamente cosa succede

La libreria è ancora in evoluzione (work-in-progress secondo l’autore), quindi prima di adottarla in produzione è consigliabile valutare alternative mature come Mapperly, anch’essa basata su Source Generators e con una community più consolidata. Detto questo, ElBruno.AotMapper è un ottimo punto di partenza per capire come funziona il mapping AOT-friendly e i Source Generators in generale.

Conclusione

Il passaggio a NativeAOT e al trimming in .NET è una tendenza inesorabile, specialmente per applicazioni cloud-native e microservizi che richiedono avvio rapido e footprint ridotto. Le librerie di mapping basate su reflection appartengono a un’era che sta tramontando: i Source Generators sono il futuro, e ElBruno.AotMapper mostra come si possa risolvere un problema pratico quotidiano — il mapping DTO — con questo approccio moderno.

Se volete esplorare ulteriormente l’argomento, la documentazione ufficiale di Roslyn Source Generators è disponibile su Microsoft Learn, e il progetto di riferimento industriale è Mapperly.


Fonte originale: AOT-Friendly DTO Mapping in .NET – El Bruno

💬 Unisciti alla discussione!


Questo è un blog del Fediverso: puoi trovare quindi questo articolo ovunque con @blog@spcnet.it e ogni commento/risposta apparirà qui sotto.

Se vuoi commentare su AOT-Friendly DTO Mapping in .NET: Source Generators al posto della reflection, utilizza la discussione sul Forum.
Condividi la tua esperienza, confrontati con altri professionisti e approfondisci i dettagli tecnici nel nostro 👉 forum community