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.