Cloning for dummies – z pomocą AutoMappera

Taki poboczny projekt jak VodSearcher skłania do eksperymentowania w zasadzie na każdym etapie i w każdym jego elemencie. Dla mnie spotkanie z nowymi technologiami samo w sobie jest eksperymentem, jednak od początku tkwiła w tym przedsięwzięciu również chęć stworzenia aplikacji o „zdrowej” architekturze. To – jak na pewno wiecie – jest chyba najtrudniejszym elementem programowania. Każdy więc drobiazg przepoczwarzał się w niekończący się rajd przez blogi, tutoriale i repozytoria githuba w poszukiwaniu Świętego Graala Tego Czy Owego. Nie muszę mówić jak rozwijające były te poszukiwania – nawet jeśli graale wciąż pozostają nieodkryte. Pewnie będzie to skutkowało również zmianami w różnych częściach systemu – gdy tylko odkryję lepsze sposoby na organizację funkcjonalności i komponentów.

Dygresje te wzięły się z ostatnich poszukiwań: dotyczących modeli w aplikacji i ich przekazywania i wykorzystywania w poszczególnych warstwach. I rodzących się w związku z tym pytań:

  • czy potrzebne mi są obiekty typu DTO?
  • czy DTO i ViewModel to to samo?
  • czy ViewModel powinien siedzieć w warstwie UI czy logiki biznesowej?

Póki co – pozostanę przy poniższych założeniach:

  • będę używał obiektów DTO (proste obiekty POCO) w warstwie logiki biznesowej – będą służyć przesyłaniu danych do warstwy UI
  • obiekty ViewModel będą używane tylko w warstwie UI – i będą korzystać z przesłanych przez serwisy obiektów DTO
  • jeśli okaże się, że potrzebna jest implementacja jakichś zachowań w widokach – będę mógł to zrobić w ViewModelu i utrzymać separację warstw i ich odpowiedzialności
  • jeśli okaże się, że obiekty DTO powodują niepotrzebne komplikacje nie dając nic w zamian – spróbuję innego podejścia

No i tutaj właśnie dotykamy sedna, czyli właściwego tematu niniejszego posta. Na prostym przykładzie opiszę kolejną użyteczną bibliotekę, która pomoże w pracy z kilkoma warstwami modeli. W praktyce bowiem zobaczymy, że nasz obiekt DTO często będzie się niewiele (lub wcale) różnił od ViewModela, który z niego będzie korzystał; a obiekt DTO będzie przecież tworzony z modeli Entity Framework. Staniemy zatem przed koniecznością przeniesienia danych z jednego obiektu do drugiego.

Lenistwo motorem postępu – nie będziemy zatem ręcznie dłubać i przepisywać właściwości z obiektu DTO do obiektu ViewModel. Wykorzystam bibliotekę AutoMapper – do tego właśnie stworzoną. Nie muszę pisać jak ją dodać do naszego projektu – NuGet zawsze pomocny:) Dodajemy ją do projektu VodSearcher – tam będziemy wykonywać mapowanie DTO na ViewModel.

Na potrzeby przykładu przyjmiemy klasę MovieDto jak źródło danych, a klasę MovieViewModel jako obiekt docelowy:

public class MovieDto
{
    public string Title { get; set; }
    public int ProductionYear { get; set; }
}

public class MovieViewModel
{
    public string Title { get; set; }
    public int ProductionYear { get; set; }
}

Widzimy, że rzeczywiście są one bliźniacze – a i samo przepisanie właściwości jest banalne. W przypadku bardziej skomplikowanych obiektów rodzi się jednak ryzyko pomyłek i błędów. Jak zatem wykorzystać AutoMappera?

Opiera się on na definicji mapowań z określeniem typu źródłowego i docelowego. Najprostsza konfiguracja i definicja takiego mapowania wygląda tak:

var config = new MapperConfiguration(cfg => cfg.CreateMap<MovieDto, MovieViewModel>());

a jej wykonanie tak:

var mapper = config.CreateMapper();
MovieDto dto = new MovieDto();
dto.Title = "Casablanca";
dto.ProductionYear = 1942;
MovieViewModel viewModel = mapper.Map<MovieViewModel>(dto);

AutoMapper przeniesie wartości właściwości o tych samych nazwach z jednego obiektu do drugiego. Podobnie będzie, jeśli z tak zdefiniowanym mapowaniem będziemy chcieli zmapować listę obiektów:

List<MovieDto> dtoList = new List<MovieDto>();
dtoList.Add(dto);
var viewModelList = mapper.Map<IEnumerable<MovieDto>,IEnumerable<MovieViewModel>>(dtoList);

Opcji, których można użyć podczas definiowania mapowania jest duużo – konwencje nazewnicze, mapowania warunkowe, konwersja typów, formatowania itd… Pokażę jeszcze jedną – która już przydała mi się podczas pracy. Na liście filmów chciałem wyświetlać dla każdego filmu listę gatunków, do których jest przypisany. Obiekt MovieDto rozszerzyłem więc o właściwość:

ICollection<string> MovieGenres { get; set; }

ale w obiekcie MovieViewModel chciałbym mieć listę gatunków w formie ciągu znaków oddzialanych przecinkiem – stąd taka właściwość:

public string MovieGenres { get; set; }

Definicja mapowanie będzie wtedy wyglądała tak:

var config = new MapperConfiguration(cfg => cfg.CreateMap<MovieDto, MovieViewModel>()
.ForMember(dest => dest.MovieGenres,
m => m.MapFrom(src => string.Join(", ", src.MovieGenres))));

a poniżej wywołanie:

var mapper = config.CreateMapper();
MovieDto dto = new MovieDto();
dto.Title = "Casablanca";
dto.ProductionYear = 1942;
dto.MovieGenres = new List<string>();
dto.MovieGenres.Add("romance");
dto.MovieGenres.Add("war");
MovieViewModel viewModel = mapper.Map<MovieViewModel>(dto);

I mamy dokładnie to o co chodziło:)

Znów – będę korzystał z AutoMappera i informował o ciekawych przypadkach użycia. Zobaczę również, czy zauważalny będzie jego wpływ na wydajność (lecz umówmy się: skala projektu raczej na razie nie daje podstaw by się o to martwić:).

Marcin

4 Comments

  • Marcin Kwiecień 19, 2016 at 6:00 am

    Dzięki za tutorial. Chciałem poznać Automappera ale jakoś zawsze było mi pod górę :) a w sumie jest dość prosty i przydatny.

    Reply
    • Marcin Kwiecień 22, 2016 at 6:42 am

      Dziękuję! To na razie tylko proste przykłady – będę się dzielił następnymi doświadczeniami.

      Reply
  • Dawid K Kwiecień 20, 2016 at 1:47 am

    Tak z ciekawości – w jakiej sytuacji ViewModele mogłyby w ogóle istnieć w logice biznesowej, a nie w GUI?

    Reply
    • Marcin Kwiecień 22, 2016 at 6:41 am

      Może niezbyt ściśle napisałem – chodzi mi o sytuację (spotykaną w tutorialach całkiem często), w której ViewModel=DomainModel, albo i ViewModel=DomainModel=DataModel i ten jeden model jest używany w kilku warstwach (w tym przekazywany do widoku). Co, jak już pisałeś, nie jest najlepszą praktyką :) Dzięki za czujność! :)

      Reply

Dodaj komentarz