Co to jest AutoMapper i dlaczego warto go znać

13

W każdej aplikacji klient-serwer następuje komunikacja. Masło maślane – gdyby nie było komunikacji, nie byłoby aplikacji klient-serwer. Pomiędzy klientem i serwerem muszą być przesyłane jakieś dane. Szymon pisał jakiś czas temu o tym dlaczego warto wyrzucić ze swojej architektury DTOs, czyli Data Transfer Objects.

Ja natomiast przedstawię pokrótce narzędzie, które pozwoli bardzo efektywnie WYKORZYSTAĆ koncept DTOs. Dane tak czy siak przesłać w jedną i drugą stronę trzeba a nie zawsze opłaca się budowanie dwóch modeli domeny jak to sugeruje Szymon.

W dalszej części posta będę posługiwał się klasami:

  1:  public class User
  2:  {
  3:  	public int Id { get; set; }
  4:  	public string FirstName { get; set; }
  5:  	public string LastName { get; set; }
  6:  	public Address Address { get; set; }
  7:  
  8:  	public void Register()
  9:  	{
 10:  		//....
 11:  	}
 12:  
 13:  	// some more domain logic...
 14:  }
 15:  
 16:  public class Address
 17:  {
 18:  	public string Street { get; set; }
 19:  	public string City { get; set; }
 20:  	public string Country { get; set; }
 21:  }

One, mimo swej karłowatej postaci, reprezentują naszą logikę wykonywaną po stronie serwera. Zakładam, że nie chcielibyśmy przesyłać ich po kablu do klientów – koniec końców klasy te mają służyć wykonywaniu operacji na danych, a nie ich prezentacji.

Do przesyłania danych posłużymy się wspomnianymi wcześniej Data Transfer Objects, czyli na przykład:

  1:  [DataContract]
  2:  public class UserDto
  3:  {
  4:  	[DataMember]
  5:  	public int Id { get; set; }
  6:  	[DataMember]
  7:  	public string FirstName { get; set; }
  8:  	[DataMember]
  9:  	public string LastName { get; set; }
 10:  }

Zauważcie atrybuty WCF – one jasno określają cel powstania tej klasy, czyli reprezentację danych gotowych do transmisji GDZIEŚ.

Przyjrzyjmy się kilku sposobom tworzenia i wypełniania takich obiektów

1. Manualne muskanie właściwości, czyli "będę doktorem"

Niczego prostszego nie da się chyba wymyślić. Z drugiej strony: ciężko również wymyślić coś bardziej errorogennego (brudne myśli na bok!) i trudniejszego w utrzymaniu.

  1:  UserDto dto = new UserDto();
  2:  dto.Id = user.Id;
  3:  dto.FirstName = user.FirstName;
  4:  dto.LastName = user.LastName;

2. Generyczny przepisywacz wartości, czyli "no place to hide"

Pewnie wielu z was zdarzyło się pisać coś takiego. Ja pisałem to przynajmniej dwukrotnie. Działać… owszem działa. W ograniczonym zakresie, ale jednak.

Wyglądać może toto mniej więcej tak:

  1:  public static void CopyPropertiesTo(this object source, object target)
  2:  {
  3:  	Type sourceType = source.GetType();
  4:  	Type targetType = target.GetType();
  5:  
  6:  	foreach (var sourceProp in sourceType.GetProperties())
  7:  	{
  8:  		var targetProp = targetType.GetProperty(sourceProp.Name);
  9:  		if (targetProp != null)
 10:  		{
 11:  			targetProp.SetValue(target, sourceProp.GetValue(source, null), null);
 12:  		}
 13:  	}
 14:  }

A korzysta się z tego tak:

  1:  UserDto dto = new UserDto();
  2:  user.CopyPropertiesTo(dto);

Zdecydowanie lepiej niż ręczne przepisywanie, ale…

Co by się stało w obu tych przypadkach, gdybyśmy chcieli dodać coś takiego:

  1:  [DataContract]
  2:  public class UserDto
  3:  {
  4:  	//...
  5:  	//...
  6:  	[DataMember]
  7:  	public string AddressStreet { get; set; }
  8:  }

Generyczny przepisywacz leży i kwiczy jak Lach na Siczy.

Natomiast w przypadku pierwszym odpowiedź jest banalna – za każdym razem, gdy zmieni się coś w DTO – idziemy do odpowiedniego miejsca i dopisujemy odpowiednie instrukcje przypisania. Tak jak małpa w ZOO regularnie łazi do wiadra w którym co jakiś czas znajduje banana.

Bycie małpą nie może być fajne (chyba że jest to evil monkey), więc zobaczmy co oferuje zapowiadany…

3. AutoMapper

Po kolei:

Najpierw definiujemy mapowania, których będziemy potrzebować (jest to krok wymagany). Robimy to raz, przy starcie aplikacji, na przykład wraz z inicjalizacją NHibernatowej SessionFactory czy ASPNETMVCowych routów:

  1:  Mapper.CreateMap<User, UserDto>();

A potem mapujemy do woli:

  1:  var dto = Mapper.Map<User, UserDto>(user);

Należy zauważyć, że AutoMapper jest "inteligentny". Przedstawiony scenariusz z AddressStreet będzie obsłużony tak jak sobie tego życzę: odpowiednia nazwa właściwości zdecyduje o tym, że pobrana zostanie wartość Street ze składowej Address. Widzicie jakie daje to możliwości rozbudowy i modyfikacji definicji DTO bez ingerencji w mapowania?

Mało tego, ta jedna definicja pozwala na mapowanie również kolekcji danych obiektów:

  1:  var dtos = Mapper.Map<User[], UserDto[]>(users);

A jeśli chcemy ręcznie ingerować w jakieś mapowanie, na przykład zawrzeć w naszym obiekcie DTO coś takiego:

  1:  [DataContract]
  2:  public class UserDto
  3:  {
  4:  	//...
  5:  	//...
  6:  	[DataMember]
  7:  	public string FullName { get; set; }
  8:  }

to oczywiście AutoMapper nam na to pozwoli:

  1:  Mapper.CreateMap<User, UserDto>()
  2:  	.ForMember(dest => dest.FullName,
  3:  	opts => opts.MapFrom(src => src.FirstName + " " + src.LastName));

A gdy dojdziemy do wniosku, że wysyłanie gdziekolwiek ID użytkowników jest złym pomysłem i nie chcemy nigdy mieć tej wartości w DTO (z jakiegokolwiek powodu, a ostatnio dwukrotnie spotkałem się z takim podejściem) – nic prostszego:

  1:  Mapper.CreateMap<User, UserDto>()
  2:  	.ForMember(dest => dest.Id, opts => opts.Ignore())

Możemy też dodać instrukcję dającą nam 100% gwarancji, że AutoMapper będzie umiał wypełnić WSZYSTKIE wartości w naszych obiektach DTO. Gdybyśmy do UserDto dodali:

  1:  [DataContract]
  2:  public class UserDto
  3:  {
  4:  	//...
  5:  	//...
  6:  	[DataMember]
  7:  	public int Age { get; set; }
  8:  }

wówczas wartość ta zawsze byłaby pusta: w końcu w User nie mamy takiej właściwości. Wystarczy jednak jedna linijka po definiowaniu całej konfiguracji:

  1:  Mapper.AssertConfigurationIsValid();

i zawsze w podobnym przypadku prosto w nasze zdziwione twarze poleci wyjątek AutoMapperConfigurationException. Warto o tym wiedzieć.

Kilka uwag końcowych:

  • ręczne mapowanie jest GŁUPIE
  • ta prosta biblioteczka może wyeliminować masę GŁUPICH czynności i GŁUPIEGO kodu
  • nie dajmy się ponieść; z mapowaniem jest jak ze wszystkim innym i łatwo przesadzić, w szczególności należy pamiętać, że ten mechanizm powstał po to aby mapować domenę na DTO; stosowanie takich mapowań w innych sytuacjach może świadczyć o potrzebie ponownej analizy kodu źródłowego i ewentualnych zmian w przyjętych rozwiązaniach architektonicznych (bardzo enigmatycznie brzmiące zdanie, ale wbrew pozorom ma sens:) )

Zachęcam do samodzielnego ściągnięcia tego narzędzia (jedna dllka na CodePlex) i pobawienia się nim chwilę. Nie ma tu nic szczególnie skomplikowanego, ale nie zaszkodzi również zerknąć do minidokumentacji ("General features") oraz  kodu źródłowego, a w szczególności do testów jednostkowych pełniących rolę sampli.

Nie przegap kolejnych postów!

Dołącz do ponad 9000 programistów w devstyle newsletter!

Tym samym wyrażasz zgodę na otrzymanie informacji marketingowych z devstyle.pl (doh...). Powered by ConvertKit
Share.

About Author

Programista, trener, prelegent, pasjonat, blogger. Autor podcasta programistycznego: DevTalk.pl. Jeden z liderów Białostockiej Grupy .NET i współorganizator konferencji Programistok. Od 2008 Microsoft MVP w kategorii .NET. Więcej informacji znajdziesz na stronie O autorze. Napisz do mnie ze strony Kontakt. Dodatkowo: Twitter, Facebook, YouTube.

13 Comments

  1. Bardzo przydatne, szczególnie w wyświetlaniu danych w widokach MVC, czyli Model -> ViewModel. Niestety nie sprawdza się za bardzo w drugą stronę, gdyż nie koniecznie wszystkie właściwości chciałbym, aby bezwiednie dostawały dane z modelu widoku, gdyż chciałbym w obiekcie domeny reagować na zmiany. Tu często modyfikacje odbywają się poprzez odpowiednie metody, które oprócz zapisania danych wykonują jeszcze jakieś operacje.

    Gdyby było inaczej to tak naprawdę doszedł bym do enigmatycznego modelu danych, czego nie chcę.

    Podsumowując AutoMapper jest fajny, ale tylko do niektórych zastosować i masz rację pisząc, że nie można dać się ponieść, bo można się zapędzić w kozi róg. :)

  2. Jak z każdą technologią tego typu nie można dać się ponieść. Jednak trzeba przyznać, że mapowanie na DTO jest teraz dużo prostsze. W drugą stronę – owszem mogą być drobne kłopoty. Brakuje mi tylko takiego mapowania ad-hoc, tj. coś w stylu: Mapper.Map<TSource, TDestination>(source).ForMember(…). Chyba, że słabo szukałem :).

  3. @dario-g & @biz:
    AutoMapper jest tylko do mapowania domena->DTO – takie bylo sluszne zalozenie tworcy. Mapowanie w druga strone jest po prostu niebezpieczne, dlatego tez (mimo requestow od uzytkownikow) nie zostanie zaimplementowane ‘both-ways-mapping’.

  4. Widziałem ten przykład z DynamicMapping – prawie o to mi chodzi, tylko to jest mało elastyczne. Brakuje mi tu możliwości ręcznego ingerowania w mapowanie, tak jak przy CreateMap. No nic w końcu są źródła, coś pokombinuje.

  5. @matma:
    To, że w obie strony potrafi mapować to wiem, ale bardzo często mapowanie DTO -> Model jest bardzo niepożądane. Często w modelu mam właściwości, któe są READONLY dlatego, bo chcę, aby ich modyfikacja odbywała się poprzez specjalne metody, które wykonują dodatkowe operacje.

    @procent:
    Jakie było dokładne założenie to nie wiem. Widziałem w kilku przykładach J.Palermo (w Jego ekipie pracuje Jimmy Bogard http://www.lostechies.com/blogs/jimmy_bogard/default.aspx twórca AutoMappera) mapowanie w obie strony i model faktycznie sprowadzony do funkcjonalności DTO.