Opisz bibliotekę wartą poznania i napisz dlaczego to właśnie AutoMapper

Automapper – czytając kilka komentarzy pod poprzednim postem wywołuje nie małe emocje. Ja jednak nadal uważam go za dobre i warte poznania narzędzie. Dodatkowo obiecałem, że napiszę więcej niż parę słów o nim, także do dzieła!

Automapper autorem jest Jimmy Bogard i jeśli miałbym opisać jego funkcjonalność swoimi słowami to działałby tak:

“To narzędzie pozwala na (prawie) bezbolesne mapowanie klas,
zapomnijcie o ręcznym przepisywanie wartości z Foo.A do Goo.A – pozwólcie tę pracę wykonać komputerowi.
Jeśli nie wierzycie w czary, to automapper jest jedną z tych bibliotek która przywróci wam wiarę.”
Jarosław Stadnicki dla jstadnicki.blogspot.com
Wracając do czarno-białej rzeczywistości (lub biało czarnej, w zależności od theme w vs). Zanim poznałem AM, gdy musiałem mapować jedną klasę na drugą, korzystałem z metod statycznych, konstruktorów kopiujących, lub innych domowych rozwiązań, jednak zawsze kończyło się to na pisaniu kodu samodzielnie. Pole po polu, właściwość po właściwości. Czasem prowadziło to połączenia ze sobą dwóch światów (dopiero teraz to do mnie dotarło):

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  public class Book
   2:  {
   3:      public Book(BookVM source)
   4:      {
   5:          this.Id = source.Id;
   6:          this.Title = source.Title;
   7:   
   8:      }
   9:      public long Id { get; set; }
  10:      public string Title { get; set; }
  11:  }
  12:   
  13:  public class BookVM
  14:  {
  15:      public BookVM(Book source)
  16:      {
  17:          this.Id = source.Id;
  18:          this.Title = source.Title;
  19:      }
  20:   
  21:      public long Id { get; set; }
  22:      public string Title { get; set; }
  23:  }

Ten sposób powoduje, ze obie klasy muszą o sobie wiedzieć, a skoro tak to, po co je rozdzielać, po co mapować, po co w ogóle dwie osobne klasy, źle!.

Dzięki AM można rozdzielić te dwie klasy i korzystać z nich zupełnie nie zależnie:

   1:  namespace AutoMapper
   2:  {
   3:      class Program
   4:      {
   5:          static void Main(string[] args)
   6:          {
   7:              // initialize AM
   8:              Mapper.CreateMap<Book, BookVM>();
   9:              Mapper.CreateMap<BookVM, Book>();
  10:   
  11:              // somewhere in code
  12:              var src = new Book { Id = 3, Title = "See sharp in 24" };
  13:              var dst = Mapper.Map<BookVM>(src);
  14:   
  15:          }
  16:      }
  17:   
  18:      public class Book
  19:      {
  20:          public long Id { get; set; }
  21:          public string Title { get; set; }
  22:      }
  23:   
  24:      public class BookVM
  25:      {
  26:          public long Id { get; set; }
  27:          public string Title { get; set; }
  28:      }
  29:  }

Jak zauważycie wszędzie podaje mapowanie obu kierunkach, a korzystam tylko z jednego. Nie jest to konieczne, powstało to na wypadek gdybym napisał jakiś kod który będzie z tego korzystać i zostało. W przypadku powyżej linijka 8 jest zbędna i można się jej spokojnie pozbyć. Podobnie będzie w przykładach poniżej.

Efekt działania programu i wartość dst:

Raz zdefiniowane mapowanie można wykorzystać później, jeśli jakaś klasa będzie zawierać nasze książki AM skorzysta już z wcześniej zdefiniowanych reguł:

   1:  using System.Collections.Generic;
   2:   
   3:  namespace AutoMapper
   4:  {
   5:      class Program
   6:      {
   7:          static void Main(string[] args)
   8:          {
   9:              // initialize AM
  10:              Mapper.CreateMap<Book, BookVM>();
  11:              Mapper.CreateMap<BookVM, Book>();
  12:              Mapper.CreateMap<Person, PersonViewModel>();
  13:              Mapper.CreateMap<PersonViewModel, Person>();
  14:              
  15:              //somewhere later in code
  16:              var p = new Person
  17:                          {
  18:                              Id = 1,
  19:                              FirstName = "Jarek",
  20:                              Books =
  21:                                  new List<Book>
  22:                                      {
  23:                                          new Book { Id = 1, Title = "Automapper in 24" },
  24:                                          new Book { Id = 2, Title = "Dependency Injection in 24" },
  25:                                          new Book { Id = 3, Title = "See sharp in 24" },
  26:                                      }
  27:                          };
  28:              var vm = Mapper.Map<PersonViewModel>(p);
  29:          }
  30:      }
  31:   
  32:      public class Person
  33:      {
  34:          public long Id { get; set; }
  35:          public string FirstName { get; set; }
  36:          public List<Book> Books { get; set; }
  37:      }
  38:   
  39:      public class PersonViewModel
  40:      {
  41:          public long Id { get; set; }
  42:          public string FirstName { get; set; }
  43:          public List<BookVM> Books { get; set; }
  44:      }
  45:   
  46:      public class Book
  47:      {
  48:          public long Id { get; set; }
  49:          public string Title { get; set; }
  50:      }
  51:   
  52:      public class BookVM
  53:      {
  54:          public long Id { get; set; }
  55:          public string Title { get; set; }
  56:      }
  57:  }

Po wykonaniu linii 28 vm będzie wyglądać tak:

Jeśli któreś z pól nazywa się inaczej, można machnąć jedno linijkową instrukcję, która wyjaśni że Name to FirstName, np.

   1:  public class Book
   2:  {
   3:      public long Id { get; set; }
   4:      public string Title { get; set; }
   5:  }
   6:   
   7:  public class BookVM
   8:  {
   9:      public long Id { get; set; }
  10:      public string Title { get; set; }
  11:  }
  12:      
  13:  public class Person
  14:  {
  15:      public long Id { get; set; }
  16:      public string FirstName { get; set; }
  17:      public List<Book> Books { get; set; }
  18:  }
  19:   
  20:  public class PersonViewModel
  21:  {
  22:      public long Id { get; set; }
  23:      public string Name { get; set; }
  24:      public List<BookVM> Books { get; set; }
  25:  }
  26:   
  27:  static void Main(string[] args)
  28:  {
  29:      // initialize AM
  30:      Mapper.CreateMap<Book, BookVM>();
  31:      Mapper.CreateMap<BookVM, Book>();
  32:   
  33:      Mapper.CreateMap<Person, PersonViewModel>()
  34:          .ForMember(d => d.Name, o => o.MapFrom(s => s.FirstName));
  35:      Mapper.CreateMap<PersonViewModel, Person>()
  36:          .ForMember(d => d.FirstName, o => o.MapFrom(s => s.Name));
  37:   
  38:      //somewhere later in code
  39:      var p = new Person { FirstName = "jarek", Id = 1 };
  40:      var x = Mapper.Map<PersonViewModel>(p);            
  41:  }

Zmienna x będzie miała taką wartość (co mam nadzieję już nikogo nie będzie dziwić)

Konwertować możemy także tylko część właściwości, np. z klasy person można wyciągnąć tylko kolekcję książek. Oczywiście można to zrobić na dwa sposoby, ręcznie (bleh) i AM (yeah):

   1:  static void Main(string[] args)
   2:  {
   3:      // initialize AM
   4:      Mapper.CreateMap<Book, BookVM>();
   5:      Mapper.CreateMap<BookVM, Book>();
   6:   
   7:      Mapper.CreateMap<Person, PersonViewModel>()
   8:          .ForMember(d => d.Name, o => o.MapFrom(s => s.FirstName));
   9:      Mapper.CreateMap<PersonViewModel, Person>()
  10:          .ForMember(d => d.FirstName, o => o.MapFrom(s => s.Name));
  11:   
  12:      Mapper.CreateMap<Person, List<BookVM>>()
  13:          .ConvertUsing(x => Mapper.Map<List<BookVM>>(x.Books));
  14:   
  15:     //somewhere later in code
  16:      var p = new Person
  17:                  {
  18:                      Id = 1,
  19:                      FirstName = "Jarek",
  20:                      Books =
  21:                          new List<Book>
  22:                              {
  23:                                  new Book { Id = 1, Title = "Automapper in 24" },
  24:                                  new Book { Id = 2, Title = "Dependency Injection in 24" },
  25:                                  new Book { Id = 3, Title = "See sharp in 24" },
  26:                              }
  27:                  };
  28:   
  29:      var books = Mapper.Map<List<BookVM>>(p);
  30:   
  31:  }

W book będzie to czego oczywiście oczekujemy:

Najważniejsza część kodu w tym przypadku ukryła się w linijkach 12 i 13.

W przypadku gdy typy są do siebie trochę mniej pasujące? Np. coś co nancy dostarcza “z pudełka”, czyli string rozdzielony separatorami do kolekcji obiektów – hę? Prosta sprawa żeby mieć tak samo bez nancy:

   1:  using System.Collections.Generic;
   2:   
   3:  namespace AutoMapper
   4:  {
   5:      using System;
   6:      using System.Linq;
   7:   
   8:      class Program
   9:      {
  10:          static void Main(string[] args)
  11:          {
  12:              // initialize AM
  13:              Mapper.CreateMap<string, List<BookVM>>()
  14:                  .ConvertUsing<StringToBookViewModel>();
  15:   
  16:              string booksnames = "Lessie, Pinokio, Guliwer";
  17:              var booksvm = Mapper.Map<List<BookVM>>(booksnames);
  18:          }
  19:      }
  20:   
  21:      public class StringToBookViewModel : ITypeConverter<string, List<BookVM>>
  22:      {
  23:          public List<BookVM> Convert(ResolutionContext context)
  24:          {
  25:              var source = context.SourceValue as string;
  26:              var titles = source.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries);
  27:              var result = new List<BookVM>();
  28:              titles.ToList().ForEach(x => result.Add(new BookVM { Title = x, Id = -1 }));
  29:              return result;
  30:          }
  31:      }
  32:   
  33:      
  34:      public class BookVM
  35:      {
  36:          public long Id { get; set; }
  37:          public string Title { get; set; }
  38:      }
  39:  }

Nie będzie dla nikogo zaskoczeniem taki widok:

Jeśli ktoś się zastanawia co daje string na obiekty, to może warto się zastanowić nad np. obsługą tagów. Gdy ktoś dodaje listę tagów do posta, wpisu, książki, etc, dostajemy od użytkownika ciąg stringów, taki konwerter jak powyżej może podmienić pojedyncze stringi na pełnoprawne obiekty z poprawnym ID – jak? W poprzednim wpisie podałem sposób jak połączyć moc DependencyInjection oraz Automappera, wystarczy dla takiego konwertera dostarczyć repozytorium i w trakcie konwersji można sprawdzić czy dany tag istnieje czy nie; i wreszcie ustawić id obiektu na poprawną wartość.

Z dobroci, które oferuje AM jest także możliwość zdefiniowania zachowania dla obiektów które są nullem, dzięki czemu można pozbyć się uciążliwej ifologi z kodu:

   1:  using System.Collections.Generic;
   2:   
   3:  namespace AutoMapper
   4:  {
   5:      using System;
   6:      using System.Linq;
   7:   
   8:      class Program
   9:      {
  10:          static void Main(string[] args)
  11:          {
  12:              // initialize AM
  13:              Mapper.CreateMap<Book, BookVM>();
  14:              Mapper.CreateMap<BookVM, Book>();
  15:   
  16:              Mapper.CreateMap<Person, PersonViewModel>()
  17:                  .ForMember(d => d.Name, o => o.MapFrom(s => s.FirstName))
  18:                  .ForMember(d => d.Books, o => o.NullSubstitute(new List<BookVM>()));
  19:   
  20:              Mapper.CreateMap<PersonViewModel, Person>()
  21:                  .ForMember(d => d.FirstName, o => o.MapFrom(s => s.Name));
  22:   
  23:              var p = new Person { FirstName = "jarek", Id = 1 };
  24:              var x = Mapper.Map<PersonViewModel>(p);
  25:          }
  26:      }
  27:   
  28:   
  29:      public class Person
  30:      {
  31:          public long Id { get; set; }
  32:          public string FirstName { get; set; }
  33:          public List<Book> Books { get; set; }
  34:      }
  35:   
  36:      public class PersonViewModel
  37:      {
  38:          public long Id { get; set; }
  39:          public string Name { get; set; }
  40:          public List<BookVM> Books { get; set; }
  41:      }
  42:   
  43:      public class Book
  44:      {
  45:          public long Id { get; set; }
  46:          public string Title { get; set; }
  47:      }
  48:   
  49:      public class BookVM
  50:      {
  51:          public long Id { get; set; }
  52:          public string Title { get; set; }
  53:      }
  54:  }

Dzięki NullSubstitute zamiast nulla w książkach mamy pustą kolekcję po której możemy spokojnie iterować.

Dopiero teraz (wow!) zauważyłem, że AM domyślnie stosuje taką strategię. Propopnuje wrócić do trzeciego obrazka i sprawdzić. Obiekt klasy Person ma null na kolekcji książek, ale już PersonViewModel ma pustą kolekcję książek. Całkiem możliwe że Jimmy też nie lubi ifologi. Tak czy siak, warto znać opcję NullSubstitute.

Przykłady powyżej to najczęściej używane konstrukcje używane przeze mnie w moich projektach domowych jak i komercyjnych, jak do tej pory rozwiązywały one wszystkie nasze problemy. Co więcej jeśli czasem coś nie działało jak powinno zawsze mogliśmy w najgorszym wypadku wrócić do ITypeConverter i napisać własne mapowanie od a do z, jednak nadal zachowując standardy AM.
Mogę się założyć, że są jeszcze cuda, które umożliwia AM a o których nie wiem, ale nawet teraz jest to jedno lepszych narzędzi, z których na pewne nie przestanę korzystać.
Jeśli macie uwagi lub pytania, chętnie wdam się w dyskusje.

2 thoughts on “Opisz bibliotekę wartą poznania i napisz dlaczego to właśnie AutoMapper

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.