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:
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ę.”
.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”