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
Mapstercon 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