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