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.