O mnieBlogGitHub

AutoMapper to zło - czy na pewno?

01 July, 2020 - 5 min read

Jakiś czas temu przeczytałem artykuł pn. "AutoMapper to zło". Autor w skrócie odradza używanie AutoMappera, z co najmniej dwóch powodów:

  • fakt, że mapowanie między różnymi typami mamy "gratis", zachęca do nadmiernego tworzenia kolejnych "warstw" modeli, co często nie jest potrzebne
  • niestandardowa logika mapowania jest potencjalnym źródłem poważnych i trudnych do wychwycenia błędów.

O ile daruję autorowi clickbaitowy tytuł wpisu, to jednak pozwolę sobie nie zgodzić się z tak postawioną tezą: że AutoMapper to biblioteka, która więcej szkodzi, niż daje pożytku.

Zacznijmy od początku - co sami autorzy AutoMappera o nim mówią:

AutoMapper is a simple little library built to solve a deceptively complex problem - getting rid of code that mapped one object to another. This type of code is rather dreary and boring to write, so why not invent a tool to do it for us?

W skrócie chodzi więc o to, by nie musieć pisać ręcznie kodu, który przekopiowuje dane z obiektu typu A do obiektu typu B. Kropka. Nie ma tu mowy o żadnych warstwach, DTOsach itp. - po prostu mamy obiekt typu A i chcemy go "przekonwertować" na typ B. Powinniśmy więc najpierw zadać sobie pytanie, kiedy z takimi sytuacjami możemy się spotkać, a dopiero potem - czy AutoMapper jest właściwym do tego narzędziem.

Gdy myślę o użyciu AutoMappera - a więc o sytuacjach konwersji danych z typu A na typ B - to przychodzą mi do głowy następujące przykłady:

  • chcemy odseparować / odróżnić model API od modelu, którego używamy w aplikacji,
  • chcemy przenieść dane pomiędzy typami, które o sobie nic nie wiedzą
  • checemy uprościć model zwracany do klienta, bo ten bazodanowy - ze względu na użycie ORMa - jest trochę niewygodny.

Poniżej opiszę te przypadki.

Jak odseparować model API od modelu aplikacji?

Wyobraźmy sobie, że mamy prostą REST-owo-CRUD-ową nakładkę na bazę danych. Mamy tam encję Product. Dopóki jest ona prosta, to po prostu w endpointach przyjmujemy i zwracamy dokładnie tę samą klasę Student, której używamy w modelu bazdanowym (np. używając EF Core).

Co jeśli chcemy jednak zapisywać też informacje "audytowe", tj. kto i kiedy zmodyfikował encję. Dodajemy pola LastModifiedDate i LastModifiedBy. Jeśli chcemy, by np. na stronie te dane były wyświetlane, to świetnie, od razu dostajemy je gratis, bo przecież używamy tej samej klasy Student. Ale co z endpointami od aktualizacji? Czy checmy, aby klient mógł "zapostować" na endpoint /students JSONa z dowolnie wypełnionymi polami LastModifiedDate i LastModifiedDate? Raczej nie, to by mogło pozowlić komuś podszyć się pod innego użytkownika albo antydatować zmianę. Możemy to oczywiście obsłużyć/zabezpieczyć na poziomie np. kontrolera, tj. zawsze odpowiednio nadpisywać te pola, jeśli robimy aktualizację. No ale wówczas po co te pola w modelu API? Jeśli opisujemy nasze API za pomocą np. Swaggera, to w wygenerowanej dokumentacji będą one uwzględnione - jak powiedzieć klientowi, że "nie, te pola są nieistotne, możesz je zignorować". Trochę słabo. :/ W końcu, skoro używamy języka typowanego (C#), to typy powinny opisywać dokładnie to, co się dzieje. W takiej sytuacji AutoMapper może być pomocą - wprowadzamy typy StudentViewApiModel, StudentUpsertApiModel i możemy z nich / do nich mapować naszą klasę Student.

Jak przenieść dane pomiędzy typami, które o sobie nic nie wiedzą?

Weźmy teraz na tapetę inną sytuację - mamy trzy projekty:

  • Students.Domain - logika biznesowa
  • Students.WebApi - Web API dla tej logiki
  • Students.FunctionApp - cloudowa funkcja, która też operuje na tej logice (nie wiem, czyta z kolejki, uruchamia się w jakichś odstępach czasowych itp.) Oczywiście dwa ostatnie projekty zależą od pierwszego. Jeślibyśmy chcieli korzystać wszędzie z klasy Student pochodzącej z projektu Students.Domain, to możemy natrafić na schody: jakiś problem typu opisany w poprzedniej części. Załóżmy zatem, że zdecydowaliśmy się wprowadzić oddzielny typ - powiedzmy StudentQueueModel - np. w projekcie Students.FunctionApp i chcemy jakoś przekonwertować dane z tego typu na ten domenowy. Klasa Student nic nie wie o klasie StudentQueueModel, nie możemy zrobić więc metody Student FromQueueModel(StudentQueueModel model). Możemy w drugą stronę - w klasie StudentQueueModel zrobić metodę Student ToDomainModel() i pisać mapowanie ręcznie. Albo - możemy też użyć AutoMappera.

Jak uprościć model, który mamy "w środku", zanim wyślemy go do klienta, "na zewnątrz"?

Może być tak, że biblioteka ORM wymaga od nas pewnych dodatkowych pól lub konstrukcji, których nie chcielibyśmy wystawiać klientowi. (Ja natrafiłem na taki problem z podkolekcją elementu typu prostego dla EF Core: zamiast ICollection<string> musiałem stworzyć typ SubItem, z unikalnym kluczem i wartością typu string - i jego użyć w kolekcji ICollection<SubItem>.) Może być tak, że jakieś dane ściągamy z zewnętrznego serwisu - albo w ogóle nasze API jest tylko nakładką na jakiś stary system - i ten oryginalny format jest niestrawny lub niewygodny dla klienta. Ot, choćby wartości logiczne są zapisane jako string (np. "true"). Mapowanie możemy napisać ręcznie - ale możemy też użyć AutoMappera.

Podsumowanie

Moim zdaniem istnieją sytuacje, w których albo chcemy mieć dodatkową warstwę modelu, albo wręcz taką wprost mamy narzuconą z zewnątrz (ciągniemy dane ze starego systemu). Wówczas - AutoMapper może być pomocny, bo nie tylko przyspiesza development, ale zabezpiecza przed błędami (nie trzeba pisać mapowania dla nowego pola - o ile występuje w obu typach mapowania, źródłowym i docelowym). Oczywiście, jak każde narzędzie, także i AutoMapper, może być źle użyty lub nadużyty: ale to już jest opowiedziane znanym powiedzeniem, mówiącym, że dla trzymającego tylko młotek wszystko wydaje się gwoździem. Wydaje mi się więc, że autor rzeczonego artykułu wylewa dziecko z kąpielą. Jeśli czasami zdarza się, że dodajemy niepotrzebne warstwy modeli i mapowania między nimi, to nie znaczy, że narzędzie do mapowania jest złe - to znaczy, że podjęliśmy złą decyzję i niepotrzebnie doprowadziliśmy do sytuacji, kiedy mapowanie jest w ogóle użyte.

© 2020, Built with Gatsby