- Published on
- ·4 min read
Zbogom AutoMapper
Pošto je AutoMapper odlučio da pređe na komercijalnu licencu počevši od verzije 15, u timu smo odlučili da ga se rešimo.
Umesto da posegnemo za još jednom bibliotekom, odlučili smo se za .NET extension metode. Performanse ćemo testirati
na dnu posta. Imamo 4 para klasa (entity–DTO), kao i generičku baznu servis klasu kroz koju persistujemo i
čitamo sve iz baze. Zbog generičnosti, bazna klasa sadrži samo osnovne operacije — GetAll, GetOne i Save — dok svaka servis
klasa koja je nasleđuje može imati dodatne metode. Sve entity klase nasleđuju isti BaseEntity. Struktura projekta izgleda ovako:
/YourProject
├── Services
│ ├── BaseService.cs
│ ├── BookService.cs
│ ├── ....
│
├── Dto
│ ├── BookDto.cs
│ ├── ....
│
├── Entities
│ ├── BaseEntity.cs
│ ├── BookEntity.cs
│ ├── ....
Za svaki par klasa dodajemo extension klasu koja sadrži bidirekciono mapiranje — iz entitija u DTO i obrnuto — kao i
metodu za mapiranje kolekcije (IEnumerable) iz entitija u DTO. To izgleda ovako:
public static class BookMappingExtensions
{
public static BookDto ToDto(this BookEntity entity)
{
if (entity == null) return null;
return new BookDto
{
Id = entity.Id,
Title = entity.Title,
Author = entity.Author,
CreatedBy = entity.CreatedBy,
CreateDate = entity.CreateDate,
};
}
public static BookEntity FromDto(this BookDto dto)
{
if (dto == null) return null;
return new BookEntity
{
Title = dto.Title,
Author = dto.Author,
CreatedBy = dto.CreatedBy,
CreateDate = dto.CreateDate
};
}
public static IEnumerable<BookDto> ToDtos(this IEnumerable<BookEntity> entities)
{
return entities?.Select(e => e.ToDto()) ?? Enumerable.Empty<BookDto>();
}
}
I da ovo ne bi zvučalo kao najbesmisleniji blog na svetu — tu dolazi generičnost, odnosno kako ovo proslediti baznoj servis klasi i njenim 3 osnovnim metodama koje dele svi entiteti. Odgovor je u delegatima. I nije me sramota da priznam da sam ih, posle 8 godina, prvi put koristio.
I jedno vrlo često pitanje na intervjuima, šta je delegat? Delegat je type-safe pokazivač na metodu. Možeš da čuvaš referencu na metodu i pozoveš je kasnije. Omogućavaju da metodu prosleđuješ kao parametar – callback pattern, i to je poenta bloga, produkcijski primer callback patterna:
Proširenje base klase:
public class BaseFileService{
+ private readonly Func<TDto, TEntity> _fromDto;
+ private readonly Func<TEntity, TDto> _toEntity;
public BaseFileService(
ILogger<BaseFileService> _logger,
+ Func<TDto, TEntity> fromDto,
+ Func<TEntity, TDto> toEntity,
...
)
{
+ _fromDto = fromDto;
+ _toEntity = toEntity;
}
}
A zatim i u BookServiceu:
public BookService(...)
: base(BookMappingExtensions.FromDto, BookMappingExtensions.ToDto, ...)
{ }
A kako to izgleda u samim base service metodama:
public async Task<TDto> SaveFile(TDto dto)
{
- var dtoEntity = Mapper.Map<TEntity>(dto);
+ var dtoEntity = _fromDto(dto);
....
var savedEntity = await _baseFileRepository.AddFile(fileEntity, _currentUserService.CurrentUserId());
....
- return Mapper.Map<TDto>(savedEntity);
+ return _toEntity(savedEntity);
}
Performanse
Razlike u performansama sam testirao preko BenchmarkDotNet biblioteke, testirajući brzinu pojedinačnog mapiranja kao i mapiranja lista od 100.000 objekata. Zarad preciznijeg testa, svaka operacija je rađena 5 puta, i rezultate možete videti u tabeli ispod:
BenchmarkDotNet v0.15.8, Windows 10 (10.0.19045.6456/22H2/2022Update)
12th Gen Intel Core i7-1255U 1.70GHz, 1 CPU, 12 logical and 10 physical cores
IterationCount=5 LaunchCount=1 WarmupCount=3
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
|------------------ |-----------------:|------------------:|-----------------:|----------:|----------:|---------:|-----------:|
| Single_Extension | 15.39 ns | 3.073 ns | 0.476 ns | 0.0179 | - | - | 112 B |
| Single_AutoMapper | 46.82 ns | 4.266 ns | 1.108 ns | 0.0178 | - | - | 112 B |
| List_Extension | 18,444,408.44 ns | 2,616,191.363 ns | 679,416.590 ns | 2093.7500 | 1250.0000 | 312.5000 | 12000608 B |
| List_AutoMapper | 25,195,265.94 ns | 13,222,867.236 ns | 3,433,936.638 ns | 2093.7500 | 1218.7500 | 312.5000 | 13298124 B |
Po benchmarku, ispada da su extension metode brže 3 puta u odnosu na AutoMapper kada je reč o pojedinačnom mapiranju i da zauzimaju istu količinu memorije, dok je mapiranje lista brže nekih ~50% i zauzimaju malo manje memorije.