in Architettura Software, Informatica

Mapperly: La Mappatura degli Oggetti in C# resa Semplice ed Efficiente

Nel mondo dello sviluppo software moderno, in particolare con C# e .NET, la mappatura degli oggetti è una task comune ma spesso ripetitiva. Ci troviamo costantemente a trasformare dati da un formato all’altro: da entità di database a DTO (Data Transfer Object), da modelli di vista a entità di dominio, e così via. Questo processo, se gestito manualmente, può portare a boilerplate code, errori e difficoltà di manutenzione. È qui che entra in gioco Mapperly, una libreria innovativa che mira a semplificare drasticamente la mappatura degli oggetti in C#.

Che cos’è Mapperly?

Mapperly è un generatore di sorgenti (source generator) per C# che automatizza la creazione di mappatori di oggetti. A differenza di altre librerie di mappatura popolari come AutoMapper o Mapster, che si basano sulla reflection o sulla compilazione in fase di runtime, Mapperly genera il codice di mappatura in fase di compilazione. Questo significa che il codice generato è ottimizzato, tipizzato staticamente e non introduce alcun overhead di runtime. Il risultato? Prestazioni eccellenti e completa integrazione con il compilatore C#, fornendo un feedback immediato su eventuali errori di mappatura.

Perché Scegliere Mapperly? I Vantaggi Principali

Mapperly si distingue per una serie di vantaggi significativi che lo rendono una scelta attraente per molti sviluppatori:

  • Zero Runtime Overhead: Poiché il codice di mappatura è generato in fase di compilazione, non ci sono costi di performance aggiuntivi durante l’esecuzione dell’applicazione. Il codice generato è semplicemente codice C# diretto.
  • Strongly Typed (Fortemente Tipizzato): Ogni mappatura è verificata dal compilatore. Questo elimina errori di typo o mappature incomplete che potrebbero passare inosservate con librerie basate sulla reflection fino al runtime. Se un campo non corrisponde o un tipo è errato, il compilatore ti avviserà immediatamente.
  • Ottime Prestazioni: Grazie alla generazione del codice, Mapperly produce mappatori altamente ottimizzati che spesso superano le prestazioni di librerie basate sulla reflection o sulla compilazione just-in-time (JIT) al primo utilizzo.
  • Facilità d’Uso: La configurazione di base è incredibilmente semplice. Nella maggior parte dei casi, è sufficiente definire un’interfaccia o una classe parziale e annotarla con un attributo per far sì che Mapperly faccia la sua magia.
  • Integrazione con IDE: Essendo un source generator, Mapperly si integra perfettamente con Visual Studio, Rider e altri IDE. Gli errori di compilazione relativi alla mappatura sono mostrati in tempo reale, proprio come qualsiasi altro errore di codice.
  • Manutenzione Semplificata: Meno boilerplate code significa meno codice da mantenere e meno opportunità per introdurre bug. Le modifiche ai modelli di dati spesso richiedono solo piccoli aggiustamenti alle definizioni del mappatore.
  • Debugging Semplificato: Poiché il codice è generato e visibile, puoi facilmente eseguire il debug del processo di mappatura passo dopo passo, cosa che non è sempre intuitiva con le librerie basate sulla reflection.

Come Iniziare con Mapperly: Un Esempio Pratico

Iniziare a usare Mapperly è piuttosto semplice. Vediamo un esempio pratico.

1. Installazione

Per prima cosa, aggiungi il pacchetto NuGet Riok.Mapperly al tuo progetto:

Bash

dotnet add package Riok.Mapperly

2. Definizione dei Modelli

Supponiamo di avere due classi: un’entità UserEntity e un DTO UserDto.

// Entità del database
public class UserEntity
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string EmailAddress { get; set; }
    public DateTime DateOfBirth { get; set; }
    public AddressEntity Address { get; set; } // Nested object
}

public class AddressEntity
{
    public string Street { get; set; }
    public string City { get; set; }
    public string PostalCode { get; set; }
    public string Country { get; set; }
}

// DTO per l'esposizione esterna
public class UserDto
{
    public int Id { get; set; }
    public string FullName { get; set; } // Combinazione di FirstName e LastName
    public string Email { get; set; } // Mappatura da EmailAddress
    public DateOnly BirthDate { get; set; } // Mappatura da DateTime a DateOnly
    public AddressDto UserAddress { get; set; } // Nested object
}

public class AddressDto
{
    public string StreetName { get; set; } // Mappatura da Street
    public string CityName { get; set; } // Mappatura da City
    public string ZipCode { get; set; } // Mappatura da PostalCode
    public string CountryCode { get; set; } // Mappatura da Country
}

3. Creazione del Mapper

Ora, creiamo un’interfaccia (o una classe parziale) e annotiamola con l’attributo [Mapper]:

using Riok.Mapperly.Abstractions;

[Mapper]
public partial interface IUserMapper
{
    // Mappatura da UserEntity a UserDto
    UserDto Map(UserEntity userEntity);

    // Mappatura da AddressEntity a AddressDto
    // Mapperly riconosce automaticamente che deve mappare questo tipo annidato
    AddressDto Map(AddressEntity addressEntity);

    // Esempio di mappatura custom per FullName e BirthDate
    [MapProperty(nameof(UserEntity.EmailAddress), nameof(UserDto.Email))]
    [MapProperty(nameof(UserEntity.DateOfBirth), nameof(UserDto.BirthDate))]
    partial UserDto MapWithCustomizations(UserEntity userEntity);

    // Metodo per combinare nome e cognome
    [UserMapping(Ignore = true)] // Ignora questa proprietà per la mappatura automatica
    public string MapFullName(string firstName, string lastName) => $"{firstName} {lastName}";

    // Metodo per convertire DateTime a DateOnly
    [UserMapping(Ignore = true)] // Ignora questa proprietà per la mappatura automatica
    public DateOnly MapDateOfBirthToDateOnly(DateTime dateOfBirth) => DateOnly.FromDateTime(dateOfBirth);
}

Spiegazione:

  • L’attributo [Mapper] indica a Mapperly di generare il codice per questa interfaccia.
  • Il metodo Map(UserEntity userEntity) indica a Mapperly di creare una mappatura da UserEntity a UserDto. Mapperly tenterà di mappare automaticamente le proprietà con lo stesso nome.
  • Per le proprietà con nomi diversi (EmailAddress a Email, DateOfBirth a BirthDate), usiamo l’attributo [MapProperty].
  • Per FullName, che è una proprietà composita, Mapperly cerca un metodo MapFullName che accetti i tipi delle proprietà sorgenti (string firstName, string lastName) e restituisca il tipo della proprietà di destinazione (string). In questo caso, abbiamo fornito noi il metodo.
  • Per BirthDate, analogamente, abbiamo fornito MapDateOfBirthToDateOnly per la conversione.
  • L’attributo partial sulla classe/interfaccia è fondamentale, in quanto permette a Mapperly di estendere la nostra definizione con il codice generato.

4. Utilizzo del Mapper

Ora puoi usare il tuo mappatore in qualsiasi parte del codice:

public class UserService
{
    private readonly IUserMapper _userMapper;

    public UserService(IUserMapper userMapper)
    {
        _userMapper = userMapper;
    }

    public UserDto GetUserDto(UserEntity userEntity)
    {
        return _userMapper.Map(userEntity);
    }

    public UserDto GetUserDtoWithCustomizations(UserEntity userEntity)
    {
        return _userMapper.MapWithCustomizations(userEntity);
    }

    public void ExampleUsage()
    {
        var addressEntity = new AddressEntity
        {
            Street = "Via Roma",
            City = "Milano",
            PostalCode = "20100",
            Country = "Italy"
        };

        var userEntity = new UserEntity
        {
            Id = 1,
            FirstName = "Mario",
            LastName = "Rossi",
            EmailAddress = "mario.rossi@example.com",
            DateOfBirth = new DateTime(1990, 5, 15),
            Address = addressEntity
        };

        var userDto = _userMapper.Map(userEntity);
        Console.WriteLine($"User DTO (automatic): {userDto.FullName}, {userDto.Email}, {userDto.BirthDate}");
        Console.WriteLine($"Address DTO (automatic): {userDto.UserAddress.StreetName}, {userDto.UserAddress.CityName}");

        var userDtoCustom = _userMapper.MapWithCustomizations(userEntity);
        Console.WriteLine($"User DTO (custom): {userDtoCustom.FullName}, {userDtoCustom.Email}, {userDtoCustom.BirthDate}");
    }
}

In Visual Studio o Rider, espandendo i nodi del progetto in Esplora soluzioni, potresti trovare il codice generato da Mapperly sotto Dependencies -> Analyzers -> Riok.Mapperly -> Riok.Mapperly.MapperGenerator.

Questo codice è completamente leggibile e debuggabile.

Funzionalità Avanzate di Mapperly

Mapperly offre molte funzionalità avanzate per gestire scenari di mappatura più complessi:

  • Mappatura di Collezioni: Mapperly supporta automaticamente la mappatura di List<T>, IEnumerable<T>, Array, e altri tipi di collezioni.
  • Mappatura Condizionale: Puoi specificare condizioni per la mappatura di proprietà usando l’attributo [MapProperty] con la proprietà Condition.
  • Ignorare Proprietà: L’attributo [UserMapping(Ignore = true)] o [MapProperty(Ignore = true)] può essere usato per ignorare proprietà specifiche durante la mappatura.
  • Mappatura di Enum: Supporto per la mappatura di tipi enum.
  • Mappatura di Tipi Annidati: Come visto nell’esempio, Mapperly gestisce automaticamente la mappatura di oggetti complessi annidati.
  • Customizzazione dei Nomi: Regole di naming personalizzate per la mappatura delle proprietà.
  • Inversion of Control (IoC) Container Integration: Mapperly può essere facilmente integrato con container IoC come .NET’s built-in DI, Autofac, ecc., registrando il tuo mappatore come singleton o scoped.

Mapperly vs. Altre Librerie di Mappatura

È utile confrontare Mapperly con altre soluzioni popolari per la mappatura degli oggetti in C#:


CaratteristicaMapperlyAutoMapperMapster
Tipo di GenerazioneSource Generator (Compile-time)Reflection (Runtime) o Expression Trees (JIT)Expression Trees (JIT)
PerformanceEccellente (codice C# diretto)Buona (ma con un piccolo overhead di reflection)Molto buona (ma con un overhead iniziale di JIT)
Feedback ErroriCompilazione (Strongly Typed)Runtime (se non configurato correttamente)Runtime (se non configurato correttamente)
ConfigurazioneAttributi e metodi esplicitiConvenzioni e configurazione fluenteConvenzioni e configurazione fluente
DebuggabilitàOttima (codice generato visibile)Moderata (astrazione della reflection)Moderata (astrazione delle expression trees)
Dipendenze RuntimeNessuna dipendenza runtime per la mappaturaDipendenza da AutoMapperDipendenza da Mapster
Curva di ApprendimentoBassa per le basi, un po’ più alta per scenari complessiModerata per le basi, più alta per scenioni complessiBassa per le basi, un po’ più alta per scenioni complessi

Quando usare Mapperly:

  • Quando la performance è critica e vuoi evitare qualsiasi overhead di runtime.
  • Quando vuoi un feedback immediato sugli errori di mappatura in fase di compilazione.
  • Quando preferisci un approccio “convention over configuration” ma con la possibilità di personalizzare facilmente le mappature complesse.
  • Quando lavori in un ambiente che beneficia della generazione di codice (ad esempio, ambienti serverless dove ogni millisecondo conta).

Quando potrebbero essere considerate altre librerie:

  • AutoMapper: Se hai un progetto legacy che lo usa già, o se hai esigenze di mappatura molto dinamiche e complesse che beneficiano della sua configurazione flessibile basata sulla reflection.
  • Mapster: Offre un buon equilibrio tra configurazione e performance, simile a Mapperly in termini di prestazioni ma con un approccio leggermente diverso alla generazione.

Considerazioni Finali

Mapperly rappresenta un passo avanti significativo nella mappatura degli oggetti in C#. Sfruttando la potenza dei source generator, offre una soluzione che combina le migliori prestazioni con una eccellente esperienza di sviluppo, riducendo il rischio di errori e il codice boilerplate.

Se stiamo cercando un modo efficiente e moderno per gestire le tue esigenze di mappatura in un’applicazione .NET, Mapperly merita sicuramente di essere considerato e provato. La sua semplicità d’uso e i chiari vantaggi in termini di prestazioni e affidabilità lo rendono uno strumento prezioso nel toolbox di qualsiasi sviluppatore C#.

  • Articoli Correlati per Tag :