Do dzieła!

Ach święta, czas jedzenie i nie policzalnych kalorii. A gdy ktoś ma szczęście, to także czas błogiego programowania bez żadnych zobowiązań. To także czas kiedy można przysiąść i poczytać. Udało mi znaleźć chwilę i posiedziałem, czytałem i czytałem i nie mogłem przestać, bo ciągle nie miałem rozwiązania swojego problemu. Chciałem zaimplementować “Owin Identity” w asp mvc, a w internetach chciałem znaleźć rozwiązanie podane na talerzu. Spędziłem cały dzień wpisując coraz to różne i kombinacje słów “owin, identity, mvc, asp” i wiecie co, nie znalazłem niczego więcej niż w ciągu pierwszych trzech minut. Ciągle chciałem znaleźć te talerz i rozwiązanie na nim. Koniec końców, przeczytałem to co było, włączyłem VS z przykładowym kodem gdzie jest wykorzystany OWIN oraz logowanie i zacząłem właściwą pracę poznawczą – programowanie. Miałem jeden projekt wzorcowy, oraz osobny gdzie chciałem to ponownie zaimplementować. Wykorzystując starodawną metodę kopiowania i zrzynania działającego kodu, przenosiłem powoli funkcjonalność z przykładu do celu, aż wreszcie miałem podstawową funkcjonalność. Moja radość nie trwała wiecznie, kod trzymałem na ramdrive, żeby się to wszystko szybciej kręciło. Chwilę po tym jak skończyłem naukę musiałem zrobić reboot, jakie było moje zaskoczenia gdy się zorientowałem kilka godzin mojej pracy poszło się. Może nie wyglądało to aż tak tragicznie:

A wiecie czemu, bo zostało mi w głowie z więcej niż z samego czytania blogów czy tutoriali. I wiecie co zacznę robić za chwilę? Napiszę to jeszcze raz i jeszcze raz, a każdym razem będzie to zrobione lepiej i lepiej.
Przestań liczyć na to, że znajdziesz idealne rozwiązanie swojego problemu na sieci, napisz je sam. Jeśli nie uda się za pierwszym razem, to na pewno za drugim, albo za trzecim, a już na pewno na czwartym. A jak nie to na pewno będziesz wiedzieć co zrobiłeś nie tak i następnym razem zrobisz to lepiej.
TL;DR;
Przestań czytać, zacznij pisać kod.

O jej, zapomniałem hasła.

Co się stało to się nie odstanie i hasło się zapomniało. Co możemy z tym zrobić? Trzeba mieć wcześniej ustaloną formę komunikacji, może email, może sms, może adres pocztowy.
Jeśli mamy coś takiego, to spoko, jesteśmy uratowani i nasz użytkownik też. Teraz już pozostało już tylko kilka kroków które należy wykonać, aby użytkownik mógł bezpiecznie zmienić swoje zapomniane hasło, na nowe które zapomni kiedy indziej.
Robimy tak, przynajmniej ja tak robię:
Grzecznie prosimy o podanie nam adresu na który mamy przysłać (mając nadzieję, że chociaż tyle pamięta).
Po tym jak poda nam adres, warto sprawdzić czy mamy taki adres. Jeśli go posiadamy, generujemy jednorazowy token, który zostanie przypisany do właśnie tego przypomnienia i tego użytkownika.
Ja robię to tak (co nie oznacza, że jest to jedyny słuszny sposób czy najlepszy):

.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 ViewModelResult<RequestResetPasswordViewModel> RequestResetPassword(string emailAddress, string baseUrl)
   2:  {
   3:      var userToResetPassword = this.database.Users.Get(x => x.Email == emailAddress).FirstOrDefault();
   4:      if (userToResetPassword == null)
   5:      {
   6:          var result = new ViewModelResult<RequestResetPasswordViewModel>
   7:                           {
   8:                               Success = false,
   9:                               ViewModel = null,
  10:                               ErrorMessage =
  11:                                   Translations
  12:                                   .AccoutService_ResetPassword_Email_NotExists
  13:                           };
  14:          return result;
  15:      }
  16:      else
  17:      {
  18:          var token = Guid.NewGuid().ToString().Replace("-", string.Empty);
  19:          var passwordResetRequest = new PasswordResetRequest
  20:                                         {
  21:                                             Created = DateTime.Now,
  22:                                             Token = token,
  23:                                             Used = false,
  24:                                             UserId = userToResetPassword.Id
  25:                                         };
  26:          this.database.PasswordResetRequests.Insert(passwordResetRequest);
  27:          this.emailService.SendPasswordRequestEmail(
  28:              userToResetPassword.Email,
  29:              userToResetPassword.DisplayName,
  30:              baseUrl + token);
  31:          this.database.Save();
  32:          var result = new ViewModelResult<RequestResetPasswordViewModel>
  33:                     {
  34:                         Success = true,
  35:                         ViewModel =
  36:                             new RequestResetPasswordViewModel
  37:                                 {
  38:                                     Message =
  39:                                         Translations
  40:                                         .AccoutService_ResetPassword_EmailSend
  41:                                 }
  42:                     };
  43:          return result;
  44:      }
  45:  }

Co jest powyżej? Szukam gościa z podanego email’a, jeśli go znajdę, to generuje token. W moim przypadku jest to guid bez kresek (może warto to przemyśleć). Później taką informację zapisuje do bazy, a potem korzystając z dostępu do serwisu mailowego wysyłam informację.
W baseUrl ukrywa się podstawowy link go którego zostanie doklejony token – średnio ładne, jestem otwarty na propozycje.
Produkcja mail jest raczej prosta:

   1:  public void SendPasswordRequestEmail(string email, string displayName, string url)
   2:  {
   3:      var smtpClient = new SmtpClient(this.communicationConfiguration.Host, this.communicationConfiguration.Port);
   4:   
   5:      smtpClient.EnableSsl = this.communicationConfiguration.MailSsl;
   6:      smtpClient.Credentials = new NetworkCredential(
   7:          this.communicationConfiguration.HostUser,
   8:          this.communicationConfiguration.HostPassword);
   9:   
  10:   
  11:      var from = new MailAddress(
  12:          this.communicationConfiguration.PasswordResetRequestAddress,
  13:          this.communicationConfiguration.PasswordResetRequestName);
  14:   
  15:      var to = new MailAddress(email, displayName);
  16:   
  17:      var bodyFormat = Translations.EmailsService_PasswordResetRequestBody_DisplayName_Url;
  18:      var subject = Translations.EmailsService_PasswordResetRequestSubject;
  19:      var body = string.Format(bodyFormat, displayName, url);
  20:   
  21:      var message = new MailMessage(from, to);
  22:   
  23:      message.Subject = subject;
  24:      message.Body = body;
  25:      smtpClient.Send(message);
  26:  }

Zwykły plain text do wysłania informacji, część ludzi ładnie wysyła piękne maile, ja tego na razie nie robię. Pewnie tak będzie, jak już zdobędę miliony użytkowników.

To w zasadzie pierwszy etap, kolejna część do obsługa takiego żądania. Ja, nowicjusz robię to tak:

   1:  [HttpGet]
   2:  public ActionResult ResetPassword(string token)
   3:  {
   4:      var serviceResult = this.accountService.IsRequestTokenValid(token);
   5:      var viewModel = new ResetPasswordViewModel2();
   6:      viewModel.Token = token;
   7:      this.ModelState.AddModelError(string.Empty, serviceResult.ErrorMessage);
   8:   
   9:      return this.View("ResetPassword", viewModel);
  10:  }

Wybaczcie mi ResetPasswordViewModel2 – coś testowałem i mi zostało – oops!
Zwykły get do kontrolera, co ważne należy pamiętać żeby dostęp do tej metody był możliwy także dla nie zalogowanych użytkowników.
Sprawdzenie poprawności tokena wygląda tak:

   1:  public MessagesResult IsRequestTokenValid(string token)
   2:  {
   3:      var passwordResetRequest = this.database.PasswordResetRequests.Get(x => x.Token == token).FirstOrDefault();
   4:   
   5:      if (passwordResetRequest != null && passwordResetRequest.Used == false)
   6:      {
   7:          return new MessagesResult { Success = true };
   8:      }
   9:      else
  10:      {
  11:          return new MessagesResult
  12:                     {
  13:                         Success = false,
  14:                         ErrorMessage = Translations.AccoutService_HandlePasswordReset_TokenNotExists
  15:                     };
  16:      }
  17:  }

Opcje z przetrzymywaniem tokena są różne, ja aktualnie postanowiłem trzymać token w bazie i po użyciu oznaczać go jako użyty. Można też go usuwać, można go też trzymać w tabeli użytkowników jako dodatkowe pole. Tysiąc programistów, tysiąc problemów, tysiąc rozwiązań.

Jeśli token jest poprawny renderowany jest widok i czekamy aż pan zapominalski wprowadzi nowe hasło, po tym jak się zdecyduje zostanie obsłużony w taki sposób:

   1:  [HttpPost]
   2:  [ValidateAntiForgeryToken]
   3:  public ActionResult ResetPassword(ResetPasswordViewModel2 model)
   4:  {
   5:      if (ModelState.IsValid)
   6:      {
   7:          var serviceResult = this.accountService.HandleResetPassword(model);
   8:          if (serviceResult.Success)
   9:          {
  10:              return this.RedirectToAction("Login", "Account");
  11:          }
  12:          this.ModelState.AddModelError(string.Empty, serviceResult.ErrorMessage);
  13:      }
  14:   
  15:      return this.View("ResetPassword", model);
  16:  }

Oraz serwis, który wykonuje właściwą pracę:

   1:  public MessagesResult HandleResetPassword(ResetPasswordViewModel2 resetPasswordViewModel)
   2:  {
   3:      if (resetPasswordViewModel.NewPassword != resetPasswordViewModel.ConfirmNewPassword)
   4:      {
   5:          return new MessagesResult
   6:                     {
   7:                         Success = false,
   8:                         ErrorMessage =
   9:                             Translations.AccoutService_HandlePasswordReset_PasswordDoesNotMatch
  10:                     };
  11:      }
  12:   
  13:      var passwordResetRequest =
  14:          this.database.PasswordResetRequests.Get(x => x.Token == resetPasswordViewModel.Token && x.Used == false)
  15:              .FirstOrDefault();
  16:   
  17:      if (passwordResetRequest == null)
  18:      {
  19:          return new MessagesResult
  20:                     {
  21:                         Success = false,
  22:                         ErrorMessage = Translations.AccoutService_HandlePasswordReset_TokenNotExists
  23:                     };
  24:      }
  25:   
  26:      var user = this.database.Users.Get(x => x.Id == passwordResetRequest.UserId).First();
  27:      var hash = this.hashProvider.Get(resetPasswordViewModel.NewPassword);
  28:      
  29:      user.Hash = hash;
  30:      passwordResetRequest.Used = true;
  31:   
  32:      this.database.Users.Update(user);
  33:      this.database.PasswordResetRequests.Update(passwordResetRequest);
  34:   
  35:      this.database.Save();
  36:   
  37:      return new MessagesResult { Success = true };
  38:  }

Nie pamiętam czemu mam powtórzoną walidację pól view-modelu, pamiętam że miałem poważny powód żeby tak zrobić – deal with it!
Ale do rzeczy, jeśli podane hasła się zgadzają i token jest poprawny, generowany jest nowy hash dla hasła i jest ono zapisywane dla użytkownika, z którym powiązany jest token. Po wszystkim w moim przypadku MUSZĘ pamiętać o oznaczeniu tokena jako użytego.
Będzie?!

Code coverage bywa zdradliwy

Od kilku dni wdrażam w życie dopisanie testów do strony nad którą pracuje, bo lepiej późno niż wcale. W zabawie tej korzystam z dodatkowego narzędzia jakim jest ncrunch. Trochę więcej o nim na blogach Paweł i Arek Bardzo ciekawa sprawa, ale warto mieć dobry sprzęt, aby w pełni korzystać ze wszystkich funkcjonalności. Mi do gustu przypadła funkcjonalność mierzenia pokrycia kodu testami. Pomyślałem sobie – “spoko“, nie będę się musiał martwić, czy mam wszystko przetestowane – i właśnie o tej pułapce parę słów. Załóżmy taki kod, który będzie zmanipulowany na potrzeby przykładu:

public class UnderTest
    {
        private int someInternalVariable;
        public void Foo(int a)
        {
            if (a == 1 || a == 3)
            {
                this.someInternalVariable = a;
            }
        }
    }

A następnie taki test, który sprawdza czy zmienna została przypisana

[TestFixture]
    public class Test
    {
        [Test]
        public void T001()
        {
            var sut = new UnderTest();
            sut.Foo(5);
            Assert.AreEqual(0, sut.SomeVariable);
        }

        [Test]
        public void T001_With_Params()
        {
            var sut = new UnderTest();
            sut.Foo(1);
            Assert.AreEqual(1, sut.SomeVariable);
        }
    }

Patrząc teraz na metryki, wynik cieszy oko i aż prosi żeby pochwalić się światu, że testy mamy w jednym palcu, architektura u nas to kozak, bo właśnie wykręciliśmy 100% pokrycia kodu. Aaale potem kątem oka dostrzegamy pułapkę! O nie, a co z przypadkiem gdy a będzie 3, przecież nie mamy takiego testu – jak to możliwe, skoro cyferki mówią 100%, a serce mówi że brakuje. No tak dzieci, bo musicie pamiętać, że code coverage bywa zdradliwy i nigdy, przenigdy nie powinno się tylko na nim opierać wyników naszego testowania. . A to że kusi, to że menagierowie mówią i chcą żeby było przynajmniej 100/80/60/x procent to wiecie, oni są pragnienie, to my jesteśmy oranżada.

Autofacu czytaj szablony z configa – please.

Czyżbym wrócił do formy z blogiem?

Niektórzy może pamiętają, a inni nie, ale walczę z takim tam sobie prywatnym projekcikiem. Otóż nastał tam etap refaktoringu i właśnie wtedy postanowiłem, że trzeba wreszcie przenieść rejestrację komponentów w IOC z kodu do configa. Używam Autofac, więc zerknełem w dokumentację i dociągnełem ‘Autofac.Configuration’ z NuGeta, jest to potrzebne aby móc grzebać z configu. Zaraz potem wziełem się do roboty,wycinek wygląda tak:
Trzeba dodać informację o nowej sekcji:

   1:  <section name="autofac" type="Autofac.Configuration.SectionHandler, Autofac.Configuration" />

A potem w sekcji autofac lecimy w taki sposób:

.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:  <autofac>
   2:      <components>
   3:          <component type="ToBeImplemented.Services.AboutService, ToBeImplemented.Services" 
   4:                     service="ToBeImplemented.Contracts.IAboutService, ToBeImplemented" />
   5:      </components>
   6:  </autofac>

type: określa jaka klasę z pełnym namespace, oraz podaje nazwę assembly
service: określa interfejs z pełnym namespace, oraz podaje nazwę assembly.
To oznacza w kodzie:

   1:  /*** ToBeImplemented project ***/
   2:  namespace ToBeImplemented.Contracts
   3:  {
   4:      public interface IAboutService
   5:      {
   6:          /* definition goes here */
   7:      }
   8:  }
   9:   
  10:  /*** ToBeImplemented.Services project ***/
  11:  namespace ToBeImplemented.Services
  12:  {
  13:      public class AboutService : IAboutService
  14:      {
  15:          /* implemenetation goes here*/
  16:      }
  17:  }

Wygląda to prosto? Mam nadzieję że tak, bo to jest proste i prosta sytuacja. Życie oczywiście nie jest takie, dlatego natrafiłem na taki problem

   1:  namespace ToBeImplemented.Services
   2:  {
   3:      using System.Collections.Generic;
   4:   
   5:      using ToBeImplemented.ViewModels;
   6:   
   7:      public class TagViewModelEqualityComparer : IEqualityComparer<TagViewModel>
   8:      {
   9:         /* code goes here */
  10:      }
  11:  }

Szablony, jak ich nie kochać? Idea jest piękna, jeden kod, wiele zastosowań. A teraz wracając do do autofaca i w zasadzie podstaw w .net i programowaniu i idei tego wpisu na blogu.
Jakiego typu jest IEqualityComparer<TagViewModel>? Hę? Bo ja się pomyliłem, zapomniałem o tym, czym naprawdę są szablony. Może sprawdzimy na takim przykładzie

   1:  using System;
   2:  using System.Collections.Generic;
   3:   
   4:  namespace ConsoleApplication1
   5:  {
   6:      class Program
   7:      {
   8:          static void Main(string[] args)
   9:          {
  10:              Console.WriteLine(typeof(IEqualityComparer<int>));
  11:              Console.WriteLine(typeof(TagViewModelEqualityComparer));
  12:          }
  13:      }
  14:   
  15:      public class TagViewModel
  16:      {
  17:          /* code goes here */
  18:      }
  19:   
  20:      public class TagViewModelEqualityComparer : IEqualityComparer<TagViewModel>
  21:      {
  22:          /* implementation goes here */
  23:      }
  24:  }

Co będzie na ekranie?

System.Collections.Generic.IEqualityComparer`1[ConsoleApplication1.TagViewModel]
ConsoleApplication1.TagViewModelEqualityComparer

Draniu! I teraz do mnie: jak mogłem o tym zapomnieć! Czyli, gdy w config kiedyś zechce zarejestrować klasę korzystającą z templejtów lub będącą templejtem to muszę pamiętać, że nie implementuje templejta, tylko jego konkretną implementacje, w moim przypadku wygląda to tak:

   1:  <component type="ToBeImplemented.Services.TagEqualityComparer, ToBeImplemented.Services"
   2:             service="System.Collections.Generic.IEqualityComparer`1[[ToBeImplemented.Model.Tag, ToBeImplemented.Model]], mscorlib" />

Od teraz będzie działać dobrze. I żeby nie było, chciałem tylko podzielić się wrażeniami z templejtów. Oczywiście autofac to dużo więcej możliwości i problemów, ale mnie ostatnio spotkał tylko ten jeden (kłamczuch ze mnie).
Po dokładne szczegóły dotyczące użytkownia autofaca zapraszam na ich strone projektu, na SO albo może ktoś mnie zapytać jeśli się wstydzi czytać i pisać po angielsku.
I to tyle. Mam nadzieję, że zaoszczędziłem dzięki temu komuś trochę czasu.

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.