Przejdź do treści

DevStyle - Strona Główna
Co to jest AutoMapper i dlaczego warto go znać

Co to jest AutoMapper i dlaczego warto go znać

Maciej Aniserowicz

4 listopada 2009

Backend

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.

Zobacz również