Entity Framework, Automapper oraz projekcja

Wprowadzenie

Od wielu lat używam viewmodeli do przekazywania danych do widoku – niezależnie od tego, czy to aplikacja desktopowa w WPF, czy webowa w ASP.NET MVC. Do tego jeszcze Automapper do mapowania danych na viewmodele. Niestety mapowanie z wykorzystaniem metody Map obiektów z Entity Frameworka bardzo często ma swoje negatywne konsekwencje (na ogół pobieramy zbyt dużo danych do aplikacji). Jednym z rozwiązań tego problemu jest skorzystanie z metody ProjectTo z Automappera, która w większości przypadków może rozwiązać ten problem. Ale niestety ma też i swoje ograniczenia, o których dowiesz się w tym wpisie.

Problem z metodą Map

Korzystanie z metody Map bardzo często powoduje wczytanie zbyt dużej liczby danych do aplikacji. Po pierwsze, Entity Framework pobiera dane dla całego obiektu, mimo że później w widoku używamy tylko jednej czy dwóch właściwości. Po drugie, gdy wyświetlamy jeszcze dane z powiązanego obiektu, możemy doświadczyć w takiej sytuacji problemu N + 1 zapytań. Lazy Loading pobierze po prostu dane w dodatkowych zapytaniach do bazy danych. A to powoduje spadek wydajności wyświetlania widoku.

Warto zobaczyć ten problem na przykładzie. Poniżej znajdują się dwie klasy (Category oraz Product), których użyję. Obie klasy są powiązane relacją jeden do wielu (produkt znajduje się w jakiejś kategorii):

W przykładzie będziemy chcieli zmapować listę produktów na listę ProductViewModel. Sam viewmodel wygląda tak:

Warto zauważyć, że właściwość Category w viewmodelu jest typu string. Podczas mapowania będziemy chcieli do tej właściwości skopiować wartość z właściwości Name z obiektu kategorii, aby później w widoku produktu wyświetlić jej nazwę.

Zobaczmy, jak zachowa się metoda Map w tym przykładzie:

W metodzie MapTest w pierwszej kolejności konfiguruję i tworzę instancję Automappera. Tak jak wspomniałem, dla właściwości Category definiujemy mapowanie z właściwości Name obiektu Category. Na końcu metody pobieramy wszystkie produkty z bazy i mapujemy na listę ProductViewModel.

W przykładzie skorzystałem z profilera do Entity Framework, w którym ładnie widać, co wykonało się na bazie danych:

automapper projection efprof products with map method

Widać, że w pierwszej kolejności wykonało się zapytanie zwracające listę produktów, a później dodatkowe zapytania zwracające kategorie. Warto też zaznaczyć, że w przypadku dodatkowych zapytań baza danych zwróciła wszystkie dane kategorii:

automapper projection efprof categories with map method

W tym przypadku moglibyśmy zredukować liczbę zapytań poprzez skorzystanie z metody Include, ale dalej w takiej sytuacji Entity Framework pobierałby wszystkie dane do aplikacji.

Darmowy kurs Visual Studio

Pracując z setkami programistów, zauważyłem, że większość osób nie pracuje efektywnie w Visual Studio. W skrajnych przypadkach korzystali z kopiowania z wykorzystaniem menu Edit. Wiem, że to dziwne, ale naprawdę niektórzy tak pracują. Dlatego postanowiłem stworzyć kurs Visual Studio – aby pomóc koleżankom i kolegom w efektywniejszej pracy.

Przygotowałem 20 lekcji e-mail, w których pokażę Ci, w jaki sposób pracować efektywniej i szybciej w Visual Studio. Poznasz dodatki, bez których nie wyobrażam sobie pracy w tym IDE.

Po więcej informacji zapraszam na dedykowaną stronę kursu: Darmowy Kurs Visual Studio.

Quiz C#

Ostatnio przygotowałem również quiz C#, w którym możesz sprawdzić swoją wiedzę. Podejmiesz wyzwanie?

Projekcja w Automapperze

Jednym z rozwiązań tego problemu jest skorzystanie z projekcji, która jest wspierana przez Automapper. Użycie jej w Automapperze sprowadza się do skorzystania z metody ProjectTo zamiast Map. Widać to na kodzie poniżej:

Główna różnica znajduje się w linijce 19, w której właśnie zamiast metody Map użyłem ProjectTo.

Tak mała zmiana kodu bardzo wpływa na to, co wykonuje się po stronie bazy danych:

automapper projection efprof products with projectto method

W tym przypadku widać, że wszystkie dane zostały pobrane jednym zapytaniem (bez konieczności używania metody Include). Dodatkowo zapytanie zwraca tylko te dane, które znajdują się w docelowym viewmodelu. Bez pobierania zbędnych danych. A przypomnę, że w przykładzie zmieliłem tylko jedną linijkę kodu!

Jak to działa? W przypadku metody ProjectTo Automapper buduje na obiekcie IQueryable wywołanie metody Select, w której znajduje się mapowanie danych na viewmodel. W praktyce wykonuje się coś bardzo podobnego do:

Ograniczenia projekcji

Niestety korzystanie z metody ProjectTo ma swoje ograniczenia. Głównym ograniczeniem jest to, co daje nam provider, z którego korzystamy. Wszystko przez to, że to on wykonuje nasze mapowanie. Nie możemy więc skorzystać z większości bardziej zaawansowanych rzeczy z Automappera.

Nie możemy na przykład skorzystać z opisywanego jakiś czas temu na blogu lokalizowania enumów. Przy próbie wykonania czegoś takiego otrzymamy wyjątek. Który, jak widać niżej, nie mówi tak naprawdę, co się stało:

automapper projection exception during mapping enum

Informacja o tym, co jest, a co nie jest wspierane, znajduje się w dokumentacji Automappera poświęconej działaniu projekcji – http://docs.automapper.org/en/stable/Queryable-Extensions.html.

Przykład

Na githubie znajduje się projekt, którego używałem podczas pracy nad tym wpisem – https://github.com/danielplawgo/AutomapperProjection. Po pobraniu przykładu należy w app.config ustawić poprawnego connection stringa do bazy. Podczas tworzenia struktury bazy danych przykład generuje dane testowe z wykorzystaniem biblioteki Bogus.

Podsumowanie

Należy bardzo uważać podczas korzystania z Automappera w aplikacji. Bardzo łatwo jest nieświadomie wczytywać dużo danych do aplikacji podczas mapowania obiektów na viewmodele. Jednym z sposobów ograniczenia tego problemu jest zastąpienie metody Map metodą ProjectTo. Ale. jak to widać w powyższym przykładzie, nie zawsze jest to możliwe. Wtedy trzeba skorzystać z jakiegoś innego rozwiązania, które postaram się opisać w jednym w przyszłych wpisów.

1 thought on “Entity Framework, Automapper oraz projekcja

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *