Categorie > Programmeren

Van entiteit naar DTO met AutoMapper

geschreven door Wilco van Dijk


In dit artikel bespreken we de voordelen van het gebruik van AutoMapper binnen je API voor de transformatie van entiteiten naar DTO's. Door AutoMapper te gebruiken voor objecttransformatie, kun je profiteren van een schonere, meer consistente en efficiëntere codebase. Dit leidt niet alleen tot een hogere productiviteit van ontwikkelaars, maar ook tot robuustere en beter onderhoudbare software.

Objecttransformaties binnen je applicatie

Objecttransformaties vinden plaats op meerdere plaatsen binnen de verschillende lagen van de applicatie. In onderstaand figuur wordt er twee transformaties weergegeven:

  • Van database entiteit naar domein model. In het geval van een code-first model zijn de database entiteiten gelijk de domein entiteiten, waardoor er geen transformatie nodig is (heeft mijn voorkeur).

  • Van domein model naar DTO

Wat is AutoMapper?

AutoMapper is een bibliotheek in .NET die ontworpen is om object-naar-object mapping te automatiseren. Het maakt het eenvoudiger om data van het ene object naar het andere over te brengen op basis van vooraf gedefinieerde regels en configuraties. Dit is vooral nuttig in scenario's waarin je vaak objecten moet transformeren, zoals bij het overbrengen van gegevens tussen verschillende lagen van een applicatie (bijvoorbeeld tussen de data-laag en de presentatielaag).

AutoMapper kan automatisch eigenschap-naar-eigenschap mapping uitvoeren tussen objecten met vergelijkbare structuren, waardoor handmatige code overbodig wordt. Maar ook voor complexere mapping zijn er voldoende mogelijkheden, zoals conditionele logica en aangepaste converters (resolvers).

Redenen om Automapper te gebruiken

De belangrijkste voordelen op een rijtje.

  • Vermindert boilerplate code

    Handmatige mapping kan resulteren in veel repetitieve en foutgevoelige code. AutoMapper automatiseert dit proces, waardoor de hoeveelheid boilerplate code drastisch vermindert en de leesbaarheid van de code toeneemt.

  • Standaardisatie

    AutoMapper zorgt voor een consistente manier van mapping tussen verschillende objecten, waardoor standaardisatie wordt bereikt. Dit maakt het gemakkelijker om wijzigingen aan te brengen en te onderhouden, vooral in grote projecten met meerdere ontwikkelaars.

  • Makkelijker te onderhouden

    Door het centraliseren van mapping logica in een configureerbaar en herbruikbaar framework, wordt de onderhoudbaarheid van de code verbeterd. Wijzigingen in de mapping hoeven slechts op één plaats te worden doorgevoerd.

  • Minder kans op fouten

    Handmatige mapping is gevoelig voor menselijke fouten. AutoMapper minimaliseert deze risico's door betrouwbare en goed geteste mapping regels toe te passen, wat de kans op bugs vermindert.

  • Tijdsefficiëntie

    Met AutoMapper kunnen ontwikkelaars zich concentreren op de kerntaken van hun applicatie in plaats van tijd te besteden aan het schrijven en debuggen van mapping code. Dit verhoogt de productiviteit en verkort de doorlooptijd van projecten.

AutoMapper toevoegen aan je .Net applicatie

  1. Setup

    Installeer nuget package in je project

    Automapper
  2. Aanmaken configuratiebestanden

    Mapping configuratie kun je toevoegen door een klasse te definiëren die erft van Automapper.Profile. Vervolgens leg je vast voor welke objecttransformatie deze configuratie is en hoe de properties aan elkaar moeten worden gemapt. Dit kan van simpel tot meer complexe transformaties. Ik zal het toelichten aan de hand van een simpel voorbeeld.

    internal class Address
    {
        public string Street { get; set; }
        public int Number { get; set; }
        public string PostalCode { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Country { get; set; }
    }
    
    internal class AddressDto
    {
        public string Street { get; set; }
        public int Number { get; set; }
        public string PostalCode { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Country { get; set; }
    }

    De configuratie is heel overzichtelijk. Bij conventie worden de properties automatisch gemapt als ze van hetzelfde type zijn en dezelfde naam hebben.

    using AutoMapper;
    using AutoMapperExamples.Models;
    
    namespace AutoMapperExamples.Mapping;
    
    internal class AddressProfile : Profile
    {
        public AddressProfile()
        {
            CreateMap<Address, AddressDto>();
        }
    }
  3. Startup.cs

    Om AutoMapper te kunnen gebruiken in je applicatie moet deze eerst worden geregistreerd in de service collectie in de startup class van je applicatie.

    services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

    Dit zorgt er niet alleen voor dat AutoMapper wordt toegevoegd aan de service collectie, maar ook alle mapping configuratiebestanden in de geladen assemblies worden ingelezen.

  4. Toepassen mapping

    Vervolgens kun je in jouw services gebruik maken van AutoMapper via dependency injection.

    private readonly IMapper _mapper;
    
    /// <summary>
    /// Added constructor to support DI
    /// </summary>
    public Examples(IMapper mapper)
    {
        _mapper = mapper;
    }

    Alle configuratie is nu gereed om met één regel code object transformatie van Adress toe te passen in je applicatie. Dit gaat net zo makkelijk voor een lijst met objecten.

    // Enkel object
    AddressDto result = _mapper.Map<Address, AddressDto>(address);
    
    // Lijst met objecten
    List<AddressDto> results = _mapper.Map<List<Address>, List<AddressDto>>(addressList);

Ik heb een voorbeeld project gemaakt die beschikbaar is via het Github account. Dan kun je alle voorbeelden zelf nakijken en uitproberen.

Meer geavanceerde configuratiemogelijkheden

Mocht meer geavanceerde configuratie nodig zijn dan kunnen de volgende mogelijkheden handig zijn.

  1. Zelf eigenschappen mappen

    Vaak kunnen niet alle eigenschappen bij conventie worden gemapt. In dat geval is het mogelijk ervan af te wijken en zelf een mapping te definiëren.

    CreateMap<Customer, CustomerDto>()
        .ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.Name))
        .ForMember(dest => dest.EmailAddress, opt => opt.MapFrom(src => src.Email));
  2. Eigenschappen met ander type mappen

    Soms is het nodig om te mappen naar een ander type veld. In dat geval kun je kleine transformaties opnemen, zoals de uitsplitsing van datum naar dag/maand/jaar.

    CreateMap<Users, User>()
        .ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.UserId))
        .ForMember(dest => dest.FirstName, opt => opt.MapFrom(src => src.FirstName))
        .ForMember(dest => dest.LastName, opt => opt.MapFrom(src => src.LastName))
        .ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email))
        .ForMember(dest => dest.BirthYear, opt => opt.MapFrom(src => src.Birthday.Year))
        .ForMember(dest => dest.BirthMonth, opt => opt.MapFrom(src => src.Birthday.Month))
        .ForMember(dest => dest.BirthDay, opt => opt.MapFrom(src => src.Birthday.Day))
        .ForMember(dest => dest.OccupationName, opt => opt.Ignore())
  3. Reverse mapping

    Ook handig is de ReverseMap() methode. Door deze op te nemen in je configuratie is het mogelijk om van Address naar AdressDto te transformeren en de andere kant op, van AddressDto naar Address.

    CreateMap<Address, AddressDto>()
        .ReverseMap();
  4. Conditionele mapping

    Het is ook mogelijk om condities in te stellen. Hieraan moet worden voldaan voordat de property gemapt wordt. Dit kan worden gerealiseerd door gebruik te maken van precondities.

    CreateMap<Person, PersonDto>()
        .ForMember(dest => dest.CanVote, opt =>
        {
            opt.PreCondition(src => src.Age > 18);
            opt.MapFrom(src => "This person is eligible to vote");
        });
  5. Resolvers

    In een aantal gevallen kom je niet weg met het mappen van properties. In deze gevallen is er wat meer complexe mapping nodig. Denk hierbij aan een berekening of een service die aangeroepen moet worden. Voor deze gevallen zijn Resolvers bedacht. Je kunt een eigen Resolver schrijven door een class te definiëren en de Automapper.IValueResolver interface te implementeren.

    internal class TotalAmountResolver : IValueResolver<Order, OrderDto, double>
    {
        private readonly VatOptions _options;
    
        public TotalAmountResolver(IOptions<VatOptions> options)
        {
            _options = options.Value;
        }
    
        /// <summary>
        /// Calculate total amount including VAT
        /// </summary>
        public double Resolve(Order source, OrderDto destination, double destMember, ResolutionContext context)
        {
            double amount = 0;
    
            foreach (var item in source.OrderLines)
              amount += item.Quantity * item.Price;
    
            return amount * (1 + _options.Percentage / 100);
        }
    }

    Resolvers kunnen als volgt worden toegevoegd aan de configuratie.

    CreateMap<Order, OrderDto>()
        .ForMember(dest => dest.AmountTotal, opt => opt.MapFrom<TotalAmountResolver>());    
  6. Context meegeven

    Er is ook een mogelijkheid om context mee te geven aan een resolver. Denk daarbij aan een specifieke waarde die alleen bekend is op het punt waar de transformatie moet plaatsvinden.

    var uniqueIdentifier = "20230001";
    
    // Pass parameter to resolver
    var result = _mapper.Map<Order, OrderDto>(order, opt => opt.Items["UniqueIdentifier"] = uniqueIdentifier);

    In de resolver kan deze vervolgens worden opgevraagd en toegepast waar nodig.

    using AutoMapper;
    using AutoMapperExamples.Models;
    
    namespace AutoMapperExamples.Resolvers;
    
    internal class OrderNumberResolver : IValueResolver<Order, OrderDto, int>
    {
      public OrderNumberResolver()
      {
      }
    
      /// <summary>
      /// Make order number equal to unique identifier passed as parameter 
      /// </summary>
      public int Resolve(Order source, OrderDto destination, int destMember, ResolutionContext context)
      {
        return Convert.ToInt32(context.Items["UniqueIdentifier"]);
      }
    }

We hebben de belangrijkste mogelijkheden van Automapper nu besproken. Mocht je op zoek zijn naar alle mogelijkheden van Automapper dan wil ik je naar de officiële documentatie verwijzen.

Heeft dit artikel jou op weg geholpen?  Buy Me A Coffee

Effectief unittests schrijven

Wilco van Dijk
Lees meer

Dependency Injection in .NET

Wilco van Dijk
Lees meer