EmptyResult na zły sposób

Programując internety gdy wysyła się jakieś żądanie na serwer nie można założyć, że poleceni się po prostu wykona. Operacja void nie istnieje. Tzn można, ale to zła praktyka, można przecież wysłać żądanie i nie sprawdzić czy w ogóle doszło ono na serwer. Ale nie o to chodzi, mój przypadek polegał na tym, że wysyłać na serwer żądanie i chciałem tylko sprawdzić czy serwer to dostał czy nie. W moim przypadku wynik w ogóle nie był ważny. Naiwnie pomyślałem sobie, że wystarczy zwrócić (oczywiście w .net asp mvc) new EmptyResult() a na kliencie sprawdzić czy długość wyniku jest zero. Jak można się domyślić nie było by tego wpisu gdyby rzeczywistość nagięła się do mojego wyobrażenia.
Otóż pozytywny wynik operacji oraz długość wyniku była by zero gdybym wysłał request i ustawił dataType na script lub html. Ale nie w moim przypadku ja chciałem json. I co? Otóż biblioteka, która wrapowala żądania też była sprytna i czasem wysyłała żądanie json a czasem xml. I teraz operacja czasem działała gorzej a czasem jeszcze gorzej. Nie wiem czemu ubzdurałem sobie że EmptyResult coś zwróci i nie powinienem sprawdzić statusu odpowiedzi, zamiast zawartości PUSTEGO WYNIKU.

Ale jeśli jesteście ciekawi co odpowiada serwer na różne żądanie popatrzcie na ten przykładowy kod:

.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 BlogController : Controller
   2:  {
   3:      [HttpGet]
   4:      public ActionResult Index()
   5:      {
   6:          return this.View("Index");
   7:      }
   8:   
   9:      [HttpGet]
  10:      public ActionResult CallMeToGetSuccessEmptyResult()
  11:      {
  12:          return new EmptyResult();
  13:      }
  14:  }

Następnie widok:

.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:   
   2:  <script src="~/Scripts/jquery-1.10.2.js"></script>
   3:  <div class="row">
   4:      @Html.ActionLink("By xml", "CallMeToGetSuccessEmptyResult", "Blog", null, new { @class = "btn btn-default test-button", id = "demo-xml", data_type_of_data = "xml", data_anchor_id = "#demo-container-xml" })
   5:      @Html.ActionLink("By json", "CallMeToGetSuccessEmptyResult", "Blog", null, new { @class = "btn btn-default test-button", id = "demo-json", data_type_of_data = "json", data_anchor_id = "#demo-container-json" })
   6:      @Html.ActionLink("By script", "CallMeToGetSuccessEmptyResult", "Blog", null, new { @class = "btn btn-default test-button", id = "demo-script", data_type_of_data = "script", data_anchor_id = "#demo-container-script" })
   7:      @Html.ActionLink("By html", "CallMeToGetSuccessEmptyResult", "Blog", null, new { @class = "btn btn-default test-button", id = "demo-html", data_type_of_data = "html", data_anchor_id = "#demo-container-html" })
   8:  </div>
   9:   
  10:  <div class="row">
  11:   
  12:      <div class="col-md-3">XML<div id="demo-container-xml"></div></div>
  13:      <div class="col-md-3">JSON<div id="demo-container-json"></div></div>
  14:      <div class="col-md-3">SCRIPT<div id="demo-container-script"></div></div>
  15:      <div class="col-md-3">HTML<div id="demo-container-html"></div></div>
  16:  </div>
  17:   
  18:  <script type="text/javascript">
  19:   
  20:      var showJson = function(data, status, anchor) {
  21:          $(anchor).html('');
  22:          var htmlToSet = "status: " + status + "<br/>";
  23:          if (data != null) {
  24:              htmlToSet += "data lenght: " + data.length;
  25:          } else {
  26:              htmlToSet += "data is null";
  27:          }
  28:              $(anchor).html( htmlToSet );
  29:      };
  30:   
  31:      var demoResult = function(e) {
  32:          e.preventDefault();
  33:          var href = $(e.target);
  34:          var data_type = href.data("type-of-data");
  35:          var anchor = href.data("anchor-id");
  36:          var ajaxOptions = {
  37:              url: '@Url.Action("CallMeToGetSuccessEmptyResult", "Blog")',
  38:              dataType: data_type,
  39:              success: function(data, status) {
  40:                  showJson(data, status, anchor);
  41:              },
  42:              error:function(data, status) {
  43:                  showJson(data, status, anchor);
  44:              }
  45:          };
  46:   
  47:          $.ajax(ajaxOptions);
  48:      };
  49:   
  50:      $(document).ready(function() {
  51:          $(".test-button").on("click", demoResult);
  52:      });
  53:   
  54:  </script>

I teraz na obrazu wyniki działania aplikaji:

Czyli nie zawsze EmptyResult to tylko “”, czasem to także parseerror gdy oczekujemy json. Na przyszłość polecam zastanowić się co się powinno sprawdzić, gdy nie chcemy nic sprawdzać. Oraz jak chce to sprawdzić.
Jak zawsze, chętnie popełnię kolejne błędy za was w następnym odcinku.

A jak Pan klei stringi?

Człowiek idzie na rozmowę o pracę i pytają go o różne rzeczy, najczęściej pytają o to czego i tak nie będzie korzystać w tej pracy. Np, ile piłek golfowych mieści się w autobusie, czy ile jest okien w wąchocku, chociaż w takimi rzeczami lubi się parać HR. Nasi pytają o wzorce, gdzie najczęściej pada odpowiedź singleton i/lub fabryka. Wcześniej może zapytają o różnicę pomiędzy value type i reference type i kolejny klasyk to konkatenacja stringów. Każdy kto był chociaż na jednej rozmowie odpowie StringBuilder.
Jak często zdarza się wam łączyć taką ilość stringów, że tworzycie StringBuildera, żeby rzeczywiście było szybciej?. Ja chyba nigdy nie byłem (albo już nie pamiętam) w takiej sytuacji, ale zawsze mnie o to pytają. Po jednej z takich rozmów zacząłem rozmyślać co robię w takich sytuacjach, gdy mam kilka stringów do sklejenia, czy korzystam z łączenia stringów i o matko alokuje tworze ich kilka w pamięci raz na kilka sekund czy może tworze owego magicznego wszech potężnego Buildera, aby uratował mnie przy klejeniu “Ala ma 1 kota, koloru szarego“. Uświadomiłem sobie, że praktycznie zawsze korzystam ze string.Format. I co teraz? A jeśli on jest źle zaimplementowany i chłopaki z mikrosoftu kleją stringi jak przedszkolaki? Nie mogłem spać po nocy i musiałem sprawdzić. Dot peek na ratunek, sprawdziłem i jest pięknie, jestem uratowany, jest bohater, w środku siedzi StringBuilder i robi swoje. Także jeśli jesteście ciekawi co jest szybsze czy string.Format czy nowy StringBuilder to sprawdźcie sami. Ja w każdym bądź razie od teraz będę na rozmowach mówić, że korzystam ze string.Format().
Tyle, ot taki krótki wpis.

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.

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.