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.

Dependency injection i automapper

Od dłuższego czasu korzystam z automappera, jest to genialne rozwiązanie gdy trzeba mapować jeden obiekt na drugi. Gdy w klasach, z i do której chcemy mapować wszystkie typy właściwości/pól i ich nazwy się zgadzają nie trzeba robić praktycznie nic, gdy pojawiają się inne nazwy, wystarczy tylko wskazać że pole właściwość W w klasie A to właściwość X w klasie B i tyle, raz i spokój do końca projektu. Gdy typy się nie zgadzają, można skorzystać z konwerterów, zarówno na poziomie właściwości jak i całych klas. Jeśli nie wiesz o czym mówię wróć 🙂 tutaj za kilka dnia, a ja obiecuje że popełnie wpis o automapperze. Opis – Automappera. Ale nie o tym chciałem napisać. Jeśli ktoś nie korzysta to polecam, jeśli potrzeba pomocy to proszę dać znać, a teraz do rzeczy.

Czasem konwertery aby dobrze wykonać swoją robotę potrzebują innej klasy, oczywiści można ją stworzyć w konstruktorze, ale to nie będzie ładne rozwiązanie, wszystko przez mocne wiązania, które ostatnio wyszły z mody. Na szczęście wymyślono wstrzykiwanie zależności, a automapper nie przeszkadza w skorzystaniu z tej idei. Co zrobić, aby to zadziałało?

Wystarczy taki kawałek kodu dodać do inicjalizacji automappera

.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 static void Initialize(IDependencyResolver current)
   2:  {
   3:      Mapper.Initialize(cfg => cfg.ConstructServicesUsing(t => current.GetService(t)));
   4:      // here goes other initialization of automapper          
   5:  }

Pewnie się zapytacie skąd i jak jest wołane? Ja bym się pytał. Więc tak to robię ja, w Application_Start w global.asax.cs

   1:  ModelMapper.Initialize(DependencyResolver.Current);

Potem gdy chcemy skorzystać z jakiegoś konwertera, na liście parametrów w kontruktorze można podać to czego potrzbujemy i czekać aż framework zrobic swoją magię:

   1:  public class Id2ConceptStatistics : TypeConverter<long, ConceptStatistics>
   2:  {
   3:      private readonly IUnitOfWork unitOfWork;
   4:   
   5:      public Id2ConceptStatistics(IUnitOfWork unitOfWork)
   6:      {
   7:          this.unitOfWork = unitOfWork;
   8:      }
   9:   
  10:      protected override ConceptStatistics ConvertCore(long id)
  11:      {
  12:          var conceptStatistics = this.unitOfWork.ConceptStatistics.GetByID(id);
  13:          var result = conceptStatistics ?? new ConceptStatistics();
  14:          return result;
  15:      }
  16:  }

Powyżej przykład na to jak z long (który jest id) można zmapować na pełno prawną klasę, która jest zagnieżdżona gdzieś w innej klasie.

To tyle.