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
Setup
Installeer nuget package in je project
Automapper
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>(); } }
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.
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.
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));
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())
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();
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"); });
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>());
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.