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.

Do ludzi, śmiało ku nowej przygodzie.

Pora wyjść z
piwnicy, a przynajmniej wystawić głowę.
Jakiś czas temu
miałem chęć napisać webowej aplikacji, której głównym założeniem była idea
ułatwienia współpracy pomiędzy ludźmi (oraz poznanie asp mvc). Chciałem
uproszczenia tego w jaki sposób ludzi znajdują ludzi, z którymi mogą działać
razem. Wyszedłem z założenia że są w nas chęci do pracy, ale brak pomysłu oraz
sytuacje odwrotne, gdy jest plan a brakuje rąk do pracy.  Może być też tak, że brakuje środków i nie
mam na myśli pieniędzy, tylko jakieś rekwizyty, czy specjalne umiejętności. (To
w wersji 2.0), czasem też ktoś ma pomysł, ale chciałby się poradzić, czy to co
wymyślił ma sens, albo czy już ktoś tego nie zrobił wcześniej. Właśnie dlatego
wszystkiego powstał “to be implemented” (wiem, że google miało coś
podobnego, o czym dowiedziałem się kilka dni temu).
Na portalu każde
może napisać czego szuka, albo co chciałby stworzyć, może być tak, że nie chce
pisać aplikacji, tylko szuka już gotowego rozwiązania mając nadzieję, że takie
istnieje. Albo chciałby napisać coś, ale nie wie spotka się to pozytywnym przyjęciem,
etc.
Idealnie sytuacja:
strona jest dla wszystkich ludzi, z dowolnej branży, nie tylko dla devów, ale
wiecie jak jest. Tak czy siak, jeśli ktoś znajdzie ktoś czas i chęci to proszę
o parę kliknięć na stronie i podzielenie się wrażeniami.
Technicznie:
Uwagi chętnie
przyjmuje tutaj, na bucket, na trello, czy w innej formie.
Przykłady pomysłów,
dla tych którym nie chciało się wejść na stronę:
1.Szukam aplikacji
która umożliwia mi włączenie / wyłączenie dodatkowego monitora. Np. WIN+1
włączenie/wyłączenie lewego monitora, WIN+2 włączenie/wyłączenie prawego, WIN+3
włączenie/wyłączenie obu zewnętrznych monitorów.
1a. Albo jakiś soft,
który potrafi przełączać także inne urządzenia, np. mam podpiętą kartę muzyczną
na USB i mam wewnętrzną, chciałbym łatwo się przełączać pomiędzy nimi, jakaś
kombinacja klawiszy mile widziana.
2. Wymyśliłem taką
webową aplikacje, do dzielenia się pomysłami, ale nie jestem grafikiem, w
związku z tym chętnie przyjmę pomoc w sprawie designu, ktoś chciałby pomów?
Strona jest już w sporej części zrobiona, ale byłoby miło gdyby wyglądała
lepiej.
3. Ukradzione od
kolegi Damiana: w VS chciałbym przy pomocy jakiejś magicznej kombinacji
klawiszy np. ALT+CTRL+(LEFT|RIGHT) móc obejrzeć historię zmian w pliku, a już
fantastycznie gdyby zasięg był tylko w ramach metody w której jestem.
4. Mam trochę czasu
po pracy i chętnie popisałbym coś w MVC, jeśli ktoś szuka programisty, to
chętnie dołączę do zespołu i pomogę. Mogę zaoferować swój czas i umiejętności w
asp mvc, trochę javascript, czy inne skrypty.

ps.
Aktualnie prowadzę mały refaktoring, także kolejne funkcjonalności czekają.
ps1.
Jeśli macie pomysły to także chętnie przyjmę
ps2.
Wiem już o ‘http://www.builditwith.me/’

Własna konfiguracja w app.config

Jeśli to czytacie, to znaczy że zdążyłem jeszcze z wpisem przed nowym rokiem.  Udało mi się napisać jeszcze jednego posta przed nowym rokiem – jestem królem świata!

Dzisiaj o wykorzystaniu app.config dla własnych celów. Skąd taki pomysł? Otóż jak każdy pewnie ma lub miał w życiu, czasem mieć taką potrzebę aby zmieniać zachowanie aplikacji inaczej niż przez jej przekompilowanie. Wtedy właśnie z pomocą przychodzą pliki .txt, .xml, .config czy dawno dawno temu .ini. Można napisać własne system do obsługi konfiguracji, własne klasy serializujące i deserializujące, można pisać cudne rzeczy. Ale po co, skoro można skorzystać z tego co oferuje app.config. Też tak pomyślałem i rozpętało się piekło. Bo nie da się ot tak dowalić do app.configu swojego kawałka xmla. Trzba to wszystko dokładnie, ale to dokładnie opisać. O ile wytrzymacie ten wpis, to zaraz wszystko wyjaśnię.

Zacznę od początku na prostych obiektach. Np. gdybym chciał w pliku zdefiniować wygląd jakiegoś prostego obiektu, np.

.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:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <configuration>
   3:      <MyConfiguration>
   4:          <Rectangle Color="Red" Width="10" Height="5" Fill="False"/>
   5:      </MyConfiguration>
   6:  </configuration>

Wiecie co się stanie przy próbie uruchomienia takiego prostego kodu jak ten:

.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:  static void Main(string[] args)
   2:  {
   3:      var cfg = ConfigurationManager.AppSettings;
   4:  }

Aby konfiguracja zadziałała, trzeba dodać informację do app.cofig o naszej sekcji konfiguracyjnej.

   1:  <configSections>
   2:      <section name="MyConfiguration" type="AppConfigExample.MyConfiguration, AppConfigExample"/>
   3:  </configSections>

W części type podajemy gdzie znajdzie się klasa opisująca naszą konfigurację. Wpis musi zawierać pełny namespace, następnie należy podać także assembly w który się ona znajduje. Tutaj wszystko mam w jednym assembly.
Skoro trzeba podać klasę to trzeba ją także gdzieś i jakoś zdefiniować i podziedziczyć:

   1:  public class MyConfiguration : ConfigurationSection
   2:  {
   3:  }

A następnie spróbować się do niej dostać:

   1:  var cfg = ConfigurationManager.GetSection("MyConfiguration");

Niestety to nadal za mało (powoli buduje nastrój grozy):

Lubię .net i jego wyjątki za czytelne błędy i opisy problemów, lubię też google czy SO za pomoc w rozwiązywaniu tych wyjątków. Chwila szukania i już wiem co należy dodać, aby móc odczytać mój kwadracik z konfiguracji:

   1:  public class MyConfiguration : ConfigurationSection
   2:  {
   3:      [ConfigurationProperty("Rectangle")]
   4:      public Rectangle Rectangle
   5:      {
   6:          get { return ((Rectangle)(base["Rectangle"])); }
   7:      }
   8:  }

Skoro wspominamy tutaj o klasie Rectangle to należy ją także jakoś zdefiniować i znowu dziedziczenie:

   1:  public class Rectangle: ConfigurationElement
   2:  {
   3:      [ConfigurationProperty("Color")]
   4:      public ConsoleColor Color
   5:      {
   6:          get { return (ConsoleColor)base["Color"]; }
   7:          set { base["Color"] = value; }
   8:      }
   9:   
  10:      [ConfigurationProperty("Width")]
  11:      public int Width
  12:      {
  13:          get { return (int)base["Width"]; }
  14:          set { base["Width"] = value; }
  15:      }
  16:   
  17:      [ConfigurationProperty("Height")]
  18:      public int Height
  19:      {
  20:          get { return (int)base["Height"]; }
  21:          set { base["Height"] = value; }
  22:      }
  23:   
  24:      [ConfigurationProperty("Fill")]
  25:      public bool Fill
  26:      {
  27:          get { return (bool)base["Fill"]; }
  28:          set { base["Fill"] = value; }
  29:      }
  30:  }

Gdy mamy taki kod możemy spokojnie spróbować swoich sił i uruchomić coś takiego:

   1:  var cfg = ConfigurationManager.GetSection("MyConfiguration") as MyConfiguration;
   2:   
   3:  var oc = Console.ForegroundColor;
   4:  Console.ForegroundColor = cfg.Rectangle.Color;
   5:   
   6:  Console.WriteLine("Width:{0}, Height:{1}, Filled:{2}", cfg.Rectangle.Width, cfg.Rectangle.Height,
   7:                    cfg.Rectangle.Fill);
   8:   
   9:  Console.ForegroundColor = oc;
Co powinno dać taki wynik:

Chyba nie macie dosyć? Nie warto zaprzestawać na tylko jednym takim obiekcie, skoro na świecie jest tyle pięknych prostokątów, załóżmy taki przypadek:

   1:  <MyConfiguration>
   2:      <Rectangle Color="Red" Width="10" Height="5" Fill="False"/>
   3:      <Rectangle Color="Green" Width="12" Height="6" Fill="False"/>
   4:      <Rectangle Color="Black" Width="14" Height="3" Fill="True"/>
   5:      <Rectangle Color="White" Width="15" Height="2" Fill="True"/>
   6:      <Rectangle Color="Gray" Width="16" Height="10" Fill="False"/>
   7:  </MyConfiguration>

Co się stanie po uruchomieniu aplikacji z taką konfiguracją?

No racja racja, przecież to kolekcja prostokątów, powinna wyglądać inaczej, bo skąd biedny komputerek może wiedzieć o który obiekt dokładnie nam chodzi.

Macie tak czasami, że wy wiecie że się nie uda czegoś zrobić, a komputer jeszcze walczy? Np. klikniecie przez przypadek na napędzie CD, tam nie ma płyty, wszyscy to wiedzą, ale nie, 15 sekund trzeba czekać zanim komputerek też się dowie. Sieć nie działa, wszyscy wiedzą, a komp walczy. Najgorsze jest to że się nie uczy. Skoro nie było płyty, i napęd się nie wysunął i nie wsunął, to płyty nadal tam nie będzie. Ale kliknij znowu na ikonę z CD i znowu czekasz, bo może jednak jest. Ech takie szybkie i takie głupie.

Wracając do naszego problemu, jak zrobić żeby ten biedny komputerek poradził sobie z wieloma prostokącikami:

   1:  <Rectangles>
   2:      <Rectangle Color="Red" Width="10" Height="5" Fill="False"/>
   3:      <Rectangle Color="Green" Width="12" Height="6" Fill="False"/>
   4:      <Rectangle Color="Black" Width="14" Height="3" Fill="True"/>
   5:      <Rectangle Color="White" Width="15" Height="2" Fill="True"/>
   6:      <Rectangle Color="Gray" Width="16" Height="10" Fill="False"/>
   7:  </Rectangles>

Ale wtedy znowu będzie marudzić, że unrecognized element i że “Rectangles” to ten unrecognized. Trzeba więc zaktualizować informację z klasie MyConfiguration na taki:

   1:  public class MyConfiguration : ConfigurationSection
   2:  {
   3:      [ConfigurationProperty("Rectangles")]
   4:      public Rectangles Rectangles
   5:      {
   6:          get { return ((Rectangles)(base["Rectangles"])); }
   7:      }
   8:  }

Oraz zdefiniować odpowiednio klasę Rectangles. Możecie spróbować samodzielnie ją zdefiniować i zobaczyć jaki poleci wyjątek, lub też przeczytać dalej i zobaczyć jak robią to inni:

   1:  [ConfigurationCollection(typeof(Rectangle))]
   2:  public class Rectangles : ConfigurationElementCollection
   3:  {
   4:      protected override ConfigurationElement CreateNewElement()
   5:      {
   6:          return new Rectangle();
   7:      }
   8:   
   9:      protected override object GetElementKey(ConfigurationElement element)
  10:      {
  11:          return element.GetHashCode();
  12:      }
  13:  }

Linijka 11 nie wygląda najlepiej. A poza tym może spowodować błąd (podczas domyślnej implementacji GetHashCode), aby go uniknąć należy napisać własne GetHashCode w klasie Rectangle lub dołożyć jakiś klucz, co później ułatwi dostęp do wybranych elementów z kolekcji.
Klasę Rectangle należy uaktualnić o następujące pole:

   1:  [ConfigurationProperty("Id", IsKey = true, IsRequired = true)]
   2:  public int Id
   3:  {
   4:      get { return (int)base["Id"]; }
   5:      set { base["Id"] = value; }
   6:  }

Natomiast metoda GetElementKey wyglądać teraz będzie tak:

   1:  protected override object GetElementKey(ConfigurationElement element)
   2:  {
   3:      return ((Rectangle)element).Name;
   4:  }

Kolekcja elementów w app.config wygląda teraz tak:

   1:  <Rectangles>
   2:      <add Id="1" Color="Red" Width="10" Height="5" Fill="False"/>
   3:      <add Id="2" Color="Green" Width="12" Height="6" Fill="False"/>
   4:      <add Id="3" Color="Black" Width="14" Height="3" Fill="True"/>
   5:      <add Id="4" Color="White" Width="15" Height="2" Fill="True"/>
   6:      <add Id="5" Color="Gray" Width="16" Height="10" Fill="False"/>
   7:  </Rectangles>

I znowu coś nie cool, to idiotyczne add – bleh, brzydal. Gdzie się podziało ładne Rectangle? Ponownie zachęcam do sprawdzenia wyjątku jaki leci, gdy zamiast add będzie Rectangle. Na szczęście i to da się napisać lepiej, wystarczy ponownie zaktualizować informację o klasie Rectangles, brakuje tam małej informacji w atrybutach klasy:

   1:  [ConfigurationCollection(typeof(Rectangle), AddItemName = "Rectangle")]
   2:  public class Rectangles : ConfigurationElementCollection { /* unicorns here */ }

Od teraz znowu zamiast add możemy stosować Rectangle i uruchomić taki kod:

   1:  var cfg = ConfigurationManager.GetSection("MyConfiguration") as MyConfiguration;
   2:  var oc = Console.ForegroundColor;
   3:   
   4:  foreach (Rectangle rectangle in cfg.Rectangles)
   5:  {
   6:      Console.ForegroundColor = rectangle.Color;
   7:      Console.WriteLine("Width:{0}, Height:{1}, Filled:{2}", rectangle.Width, rectangle.Height,rectangle.Fill);
   8:      Console.ForegroundColor = oc;
   9:  }

Co według mojego działania da taki wynik:

A ja wybrać ten jeden w swoim rodzaju Rectangl? Skoro mamy jego Id to powinniśmy z niego jakoś skorzystać. Może linq? Potrzebujemy tylko IEnumerable w klasie Rectangles i będzie działać, a więc:

   1:  [ConfigurationCollection(typeof(Rectangle), AddItemName = "Rectangle")]
   2:  public class Rectangles: ConfigurationElementCollection, IEnumerable<Rectangle>
   3:  {
   4:      { /* dragons here */ }
   5:      public new IEnumerator<Rectangle> GetEnumerator()
   6:      {
   7:          int count = base.Count;
   8:          for (int i = 0; i < count; i++)
   9:          {
  10:              yield return base.BaseGet(i) as Rectangle;
  11:          }
  12:      }
  13:  }

I można już napisać coś takiego w kodzie:

   1:  Rectangle rect = cfg.Rectangles.Single(r => r.Id == 2);
   2:   
   3:  Console.ForegroundColor = rect.Color;
   4:  Console.WriteLine("Width:{0}, Height:{1}, Filled:{2}", rect.Width, rect.Height, rect.Fill);

Od teraz wszyscy mogą zazdrościć wam wiedzy o tym, jak lepiej wykorzystywać app.config w swoich aplikacjach.

A teraz cały kod z wpisu:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Configuration;
   4:  using System.Linq;
   5:  using System.Text;
   6:   
   7:  namespace AppConfigExample
   8:  {
   9:      class Program
  10:      {
  11:          static void Main(string[] args)
  12:          {
  13:              var cfg = ConfigurationManager.GetSection("MyConfiguration") as MyConfiguration;
  14:              var oc = Console.ForegroundColor;
  15:   
  16:              foreach (Rectangle rectangle in cfg.Rectangles)
  17:              {
  18:                  Console.ForegroundColor = rectangle.Color;
  19:                  Console.WriteLine("Width:{0}, Height:{1}, Filled:{2}", rectangle.Width, rectangle.Height, rectangle.Fill);
  20:              }
  21:   
  22:              Console.WriteLine("****************************************");
  23:              Rectangle rect = cfg.Rectangles.Single(r => r.Id == 2);
  24:   
  25:              Console.ForegroundColor = rect.Color;
  26:              Console.WriteLine("Width:{0}, Height:{1}, Filled:{2}", rect.Width, rect.Height, rect.Fill);
  27:   
  28:   
  29:   
  30:              Console.ForegroundColor = oc;
  31:          }
  32:      }
  33:   
  34:   
  35:      public class MyConfiguration : ConfigurationSection
  36:      {
  37:          [ConfigurationProperty("Rectangles")]
  38:          public Rectangles Rectangles
  39:          {
  40:              get { return ((Rectangles)(base["Rectangles"])); }
  41:          }
  42:      }
  43:   
  44:      [ConfigurationCollection(typeof(Rectangle), AddItemName = "Rectangle")]
  45:      public class Rectangles : ConfigurationElementCollection, IEnumerable<Rectangle>
  46:      {
  47:          protected override ConfigurationElement CreateNewElement()
  48:          {
  49:              return new Rectangle();
  50:          }
  51:   
  52:          protected override object GetElementKey(ConfigurationElement element)
  53:          {
  54:              return ((Rectangle)element).Id;
  55:          }
  56:   
  57:          public new IEnumerator<Rectangle> GetEnumerator()
  58:          {
  59:              int count = base.Count;
  60:              for (int i = 0; i < count; i++)
  61:              {
  62:                  yield return base.BaseGet(i) as Rectangle;
  63:              }
  64:          }
  65:      }
  66:   
  67:      public class Rectangle : ConfigurationElement
  68:      {
  69:          [ConfigurationProperty("Id", IsKey = true, IsRequired = true)]
  70:          public int Id
  71:          {
  72:              get { return (int)base["Id"]; }
  73:              set { base["Id"] = value; }
  74:          }
  75:   
  76:          [ConfigurationProperty("Color")]
  77:          public ConsoleColor Color
  78:          {
  79:              get { return (ConsoleColor)base["Color"]; }
  80:              set { base["Color"] = value; }
  81:          }
  82:   
  83:          [ConfigurationProperty("Width")]
  84:          public int Width
  85:          {
  86:              get { return (int)base["Width"]; }
  87:              set { base["Width"] = value; }
  88:          }
  89:   
  90:          [ConfigurationProperty("Height")]
  91:          public int Height
  92:          {
  93:              get { return (int)base["Height"]; }
  94:              set { base["Height"] = value; }
  95:          }
  96:   
  97:          [ConfigurationProperty("Fill")]
  98:          public bool Fill
  99:          {
 100:              get { return (bool)base["Fill"]; }
 101:              set { base["Fill"] = value; }
 102:          }
 103:      }
 104:  }

Oraz app.config:

   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <configuration>
   3:      <configSections>
   4:          <section name="MyConfiguration" type="AppConfigExample.MyConfiguration, AppConfigExample"/>
   5:      </configSections>
   6:      <MyConfiguration>
   7:          <Rectangles>
   8:              <Rectangle Id="1" Color="Red" Width="10" Height="5" Fill="False"/>
   9:              <Rectangle Id="2" Color="Green" Width="12" Height="6" Fill="False"/>
  10:              <Rectangle Id="3" Color="Black" Width="14" Height="3" Fill="True"/>
  11:              <Rectangle Id="4" Color="White" Width="15" Height="2" Fill="True"/>
  12:              <Rectangle Id="5" Color="Gray" Width="16" Height="10" Fill="False"/>
  13:          </Rectangles>
  14:      </MyConfiguration>
  15:  </configuration>

To by było na tyle, bierzcie i korzystajcie z tego, bo to dobre jest.
Pozostaje mi tylko życzyć dobrego w nadchodzącym nowym roku!
Tej!

Czym się różni właściwość klasy od pola klasy?

Wracając jeszcze do filozofowania o kodzie, dziś trochę o cechach klas, a dokładniej: pola i właściwości. Zanim wymyślono idee właściwości dostęp do pól odbywał się na dwa sposoby. Pole w klasie (np. name) mogło być publiczne i każdy miotał nim jak szatan, druga opcja to dostęp kontrolowany przez parę metod typu GetName i SetName. Umożliwiały one kontrolowanie tego kto i na jakich zasadach może korzystać z cech wewnętrznych klasy.
Później, aby pominąć pisanie GetName/SetName, wymyślono właściwości, które w sprytny sposób, miały pozbyć się z kodu klienta getów i setów, czyniąc kod bardziej czytelnym. Jednocześnie pozostawić możliwość kontrolowanie kto i jak ma dostęp do pola, poprzez “ukrytą” implementację tychże get i set. Wygląda więc to tak, że klient korzysta z naszego pola w klasie, które naprawdę jest jest właściwością opakowującą to pole. Zresztą każdy wie jak wygląda i działa właściwość w C#.
Teraz czas na właściwą filozofię, czym się różni właściwość klasy od pola w klasie?

  1. Właściwość może zostać tak zaimplementowana, aby była tylko do zapisu lub tylko do odczytu. Pole w klasie jest najczęściej implementowane czytalne/pisalne (uwielbiam nowo mowę). Czasem tworzy się stałe. (ot czasami tak trzeba).
  2. Właściwość podczas zapisu lub odczytu może nas poczęstować wybornym wyjątkiem, ta sama operacja wykonana na polu nigdy.
  3. Właściwość nie może zostać przekazana jako ref lub out argument metody, pole klasy tak.
  4. Czas wykonania właściwości może być czasem bardzo długi, np. podczas wykonywania synchronizacji wątków, leniwej inicjalizacji właściwości, etc. Dostęp do pola zawsze jest operacją natychmiastową.
  5. Dostęp do właściwości może powodować efekty uboczne, np. w cache, każdy dostęp może zmniejszać czas życia keszu. Dostęp do pola nigdy nie ma takiej możliwości.
  6. Odczytanie właściwości klasy czasem może się wymagać zwiększania zapotrzebowania na pamięć RAM, potrzebę na przeliczenie pewnych zależności, czy wykonanie dodatkowych operacji, może także zwrócić nie pełny wynik. Dostęp do pola, nie wymaga dodatkowych środków, oraz zwraca oryginalną zawartość tego pola.

Nasuwa się pytanie, czy wy będąc klientem klasy X, która oferuje dostęp do właściwości, oczekujecie, że dostęp do właściwości Name będzie:

  • trwał strasznie długo
  • każdy operacja odczytu zmienia, zwraca inną wartość
  • dostaniecie tylko spółgłoski
  • zanim dostaniecie wynik, zużycie pamięci wzrośnie o kilkanaście mega

Takich rzeczy można oczekiwać od metod, to one powinny robić coś skomplikowanego, dostęp do pola powinien być możliwie szybki i prosty. Warto o tym pamiętać, pisząc swój kawałek kodu. Może trzeba wrócić do poczciwego GetName(), GetNameConsonantsOnly(),
GetNameVowelsOnly(), GetNameUsing5MbRamMore() ?

Ot takie tam żywcem zerżnięte informacje z książki którą czytam i pomyślałem, że warto o tym napisać.

ps.
DateTime.Now 😉

Metody rozszerzające – testowanie i porządek w api.

Jak każdemu porządnemu developerowi zdarza mi się czasem napisać testy. Jak każdemu porządnemu developerowi, czasem zdarza mi się wykorzystać mechanizm metod rozszerzających (jeśli nie wiesz o czym mówię sprawdź na msdn). Jak każdy prawdziwy developer, chciałem przetestować logikę, która była wykorzystywana w jednej z takich metod. W zasadzie to nie w samej metodzie, chciałem sprawdzić czy zostanie wywołana z wartościami, które są dla mnie ważne.
Zacznę od metod rozszerzających, a testowanie przyjdzie samo.

Tak wygląda interfejs udostępniony przez samą (bez metod rozszerzających) klasę List:

Aby przewinąć listę na sam dół wystarczą trzy wciśnięcia Page Down. Jak widać jedyny import, z którego korzystam to System.Collections.Generic, wymagany aby móc skorzystać z List

To się stanie gdy dodam System.Linq: (obrazek się troszkę nie mieści)

Pojawi się nowa funkcjonalność, zdefiniowana w System.Linq, która pozwala robić cuda z listą i innymi IEnumerable (jak również innymi obiektami). Po imporcie aby przewinąć listę metod, trzeba osiem razy walnąć w Page Down.

Co zrobić, aby lista nowych funkcjonalności nie zaśmiecała dostępnego api? Można to wszystko ukryć przy pomocy jednej klasy, która przekierowuje żądania dalej, do właściwej części, gdzie zdefiniowane będą rozszerzenia
Normalnie implementacja rozszerzeń może wyglądać jakoś tak:

.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:  namespace ConsoleApplication
   2:  {
   3:   
   4:      class Program
   5:      {
   6:          static void Main(string[] args)
   7:          {
   8:              MyClass mc = new MyClass();
   9:          }
  10:      }
  11:   
  12:      public static class MyClassExtension
  13:      {
  14:          public static void Foo0(this MyClass target) { }
  15:          public static void Foo1(this MyClass target) { }
  16:          public static void Foo2(this MyClass target) { }
  17:          public static void Foo3(this MyClass target) { }
  18:          public static void Foo4(this MyClass target) { }
  19:          public static void Foo5(this MyClass target) { }
  20:          public static void Foo6(this MyClass target) { }
  21:          public static void Foo7(this MyClass target) { }
  22:          public static void Foo8(this MyClass target) { }
  23:          public static void Foo9(this MyClass target) { }
  24:      }
  25:   
  26:      public class MyClass
  27:      {
  28:      }
  29:  }

Po takiej implementacji otrzymujemy cztery metody podstawowe, oraz dodatkowy dziesięć nowych z rozszerzenia w MyClassExtension.

Można trochę to wszystko uprościć dodając odpowiednią warstwę abstrakcji (abstrakcja was uwolni, zapamiętajcie to). Opis poniżej:

   1:  namespace ConsoleApplication
   2:  {
   3:      using System;
   4:   
   5:      class Program
   6:      {
   7:          static void Main(string[] args)
   8:          {
   9:              MyClass mc = new MyClass();
  10:              mc.ExtensionService().Foo0();
  11:          }
  12:      }
  13:   
  14:      public static class MyClassExtension
  15:      {
  16:          public static Func<MyClass, IMyClassExtensionService> ExtensionFactory { get; set; }
  17:   
  18:          static MyClassExtension()
  19:          {
  20:              ExtensionFactory = target=> new MyClassExtensionReleaseVersion(target);
  21:          }
  22:   
  23:          public static IMyClassExtensionService ExtensionService(this MyClass target)
  24:          {
  25:              return ExtensionFactory(target);
  26:          }
  27:      }
  28:   
  29:      public interface IMyClassExtensionService
  30:      {
  31:          void Foo0();
  32:          void Foo1();
  33:          void Foo2();
  34:          void Foo3();
  35:          void Foo4();
  36:          void Foo5();
  37:          void Foo6();
  38:          void Foo7();
  39:          void Foo8();
  40:          void Foo9();
  41:      }
  42:   
  43:      public class MyClassExtensionReleaseVersion : IMyClassExtensionService
  44:      {
  45:          private MyClass myclass;
  46:   
  47:          public MyClassExtensionReleaseVersion(MyClass target)
  48:          {
  49:              this.myclass = target;
  50:          }
  51:   
  52:          public void Foo0() { }
  53:          public void Foo1() { }
  54:          public void Foo2() { }
  55:          public void Foo3() { }
  56:          public void Foo4() { }
  57:          public void Foo5() { }
  58:          public void Foo6() { }
  59:          public void Foo7() { }
  60:          public void Foo8() { }
  61:          public void Foo9() { }
  62:      }
  63:   
  64:      public class MyClass
  65:      {
  66:      }
  67:  }

Tak się to prezentuje teraz:

Zacznę od linijki 29 gdzie zdefiniowany został interfejs ze wszystkimi metodami, które mają działać jako rozszerzenie dla klasy MyClass. Linia 43 to implementacja tego interfejsu, warto zauważyć że klasa ta nie jest statyczna, oraz nie posiada statycznych pól, a metody nie przyjmują parametrów. W konstruktorze otrzymuje ona obiekt, na rzecz którego ma działać. Najlepsze zostawiam na koniec, linijka 14, klasa udostępniająca rozszerzenie dla MyClass. W ExtensionService (23) wykorzystywana jest właściwość ExtensionFactory, który jest zwyczajną metodą wytwórczą, zwracającą nowy obiekt MyClassExtensionReleaseVersion, do którego przesyła obiekt, na rzecz którego ma zostać wywołana metoda rozszerzająca. Mam nadzieję, że kod tłumaczy to prościej niż ja. Linia 10 to nowy sposób na wywołanie metod rozszerzających. 
Właściwość ExtensionFactory została zdefiniowana jako publiczna, co umożliwia jego zmianę w razie potrzeby testowania. Tak samo obiekt rozszerzający jest deklarowany poprzez interfejs, w związku z czym w testach można wykorzystać np. MyClassExtensionMockVersion i sprawdzać czy klasa zachowuje się tak jak tego oczekujemy.

Ja skorzystałem z tego rozwiązania podczas testowania nawigacji w projekcie, który korzysta z PRISMa, tam do zmiany widoków wykorzystywany jest interfejs IRegionManager i kilka metod rozszerzających ten interfejs. Chciałem sprawdzić, czy po wykonaniu operacji X jedna z moich klas zarząda zmiany widoku. Jedynym sposobem, na sprawdzenie parametrów przesłanych w query było stworzenie i podstawienie własnej implementacji metod rozszerzających. Na początku testów mam taki zapis:

   1:  Capture.Common.Prism.PrismExtensions.ServiceFactory = p => new NavigationServiceMock(p);

Tak wygląda mój lipny serwis, który działa zamiast tego z Prism:

   1:  public class NavigationServiceMock : INavigation
   2:  {
   3:      private IRegionManager p;
   4:   
   5:      public NavigationServiceMock(IRegionManager p)
   6:      {
   7:          this.p = p;
   8:      }
   9:   
  10:      public IRegionManager AddToRegion(string regionName, object view)
  11:      {
  12:          throw new NotImplementedException();
  13:      }
  14:   
  15:      public IRegionManager RegisterViewWithRegion(string regionName, Func<object> getContentDelegate)
  16:      {
  17:          throw new NotImplementedException();
  18:      }
  19:   
  20:      public IRegionManager RegisterViewWithRegion(string regionName, Type viewType)
  21:      {
  22:          throw new NotImplementedException();
  23:      }
  24:   
  25:      public void RequestNavigate(string regionName, string source)
  26:      {
  27:          throw new NotImplementedException();
  28:      }
  29:   
  30:      public void RequestNavigate(string regionName, Uri source)
  31:      {
  32:          LoadTradeViewModelTest.NavigationServiceMock_RequestedNavigatedTargetRegionName = regionName;
  33:          LoadTradeViewModelTest.NavigationServiceMock_ReqestedNavigatedTargetUri = source.OriginalString;
  34:      }
  35:   
  36:      public void RequestNavigate(string regionName, string source, Action<NavigationResult> navigationCallback)
  37:      {
  38:          throw new NotImplementedException();
  39:      }
  40:   
  41:      public void RequestNavigate(string regionName, Uri source, Action<NavigationResult> navigationCallback)
  42:      {
  43:          throw new NotImplementedException();
  44:      }
  45:  }

Najważniejsze w tym wszystkim to RequestNavigate (30) gdzie zapisuje żądany region i uri. Później w teście porównuje czy są takie jak oczekiwałem. Wszystkie NotImplementedException są nie używane w testach, dlatego też mogły pozostać w takiej domyślnie oferowanej przez VS formie.
Normalna implementacja przekazuje wszystkie parametry do oryginalnej implementacji.

Pojawia się pewien problem, przy próbie stworzenie podobnego rozwiązania dla klas generycznych. Ten problem to kompilator, który cały czas twierdzi że metody rozszerzające muszą być w klasach statycznych, nie generycznych. Same metody mogą być generyczne. Aby to obejść na szybko wymyśliłem coś takiego

   1:  namespace ConsoleApplication
   2:  {
   3:      using System;
   4:      using System.Collections.Generic;
   5:   
   6:      class Program
   7:      {
   8:          static void Main(string[] args)
   9:          {
  10:              List<int> l = new List<int>();
  11:              l.GetExtension().Foo1();
  12:          }
  13:      }
  14:   
  15:      public static class ExtensionsFactory<T>
  16:      {
  17:          public static Func<List<T>, IListExtensionService> Factory { get; set; }
  18:      }
  19:   
  20:      public static class ListExtension
  21:      {
  22:          public static IListExtensionService GetExtension<T>(this List<T> target)
  23:          {
  24:              if (ExtensionsFactory<T>.Factory == null)
  25:              {
  26:                  ExtensionsFactory<T>.Factory = o => new ListExtensionSerivce<T>(o);
  27:              }
  28:   
  29:              return ExtensionsFactory<T>.Factory(target);
  30:          }
  31:      }
  32:   
  33:      public interface IListExtensionService
  34:      {
  35:          void Foo1();
  36:      }
  37:   
  38:      public class ListExtensionSerivce<T> : IListExtensionService
  39:      {
  40:          private List<T> list;
  41:   
  42:          public ListExtensionSerivce(List<T> target)
  43:          {
  44:              this.list = target;
  45:          }
  46:   
  47:          public void Foo1()
  48:          {
  49:              Console.WriteLine("foo and the list");
  50:          }
  51:      }
  52:  }

Cały trik w tym przypadku, to przeniesienie odpowiedzialności na wytwarzanie obiektów z ListExtension do ExtensionsFactory. A podczas wywołania metody, należny się upewnić że fabryka już istnieje.

Myślę że wystarczy mojego wymądrzania się na dziś. Podoba się? Nie podoba się? Macie inne sposoby? Pytania? Uwagi? Cokolwiek?

ps
Mam nadzieję, że więcej wyjaśniłem niż zakręciłem.
ps2
Jak już wszystko napiszę, to czytam czy jest ok, a potem i tak mam lekkiego stresa, że coś głupiego napisałem.