fbpx
devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
3 minut

Automapper i mapowanie jako NHibernate.Load()


20.04.2011

Dość dawno już temu pokazałem jak można użyć Automapper do mapowania kolekcji bez powodowania ciągnięcia ich zawartości z bazy: "AutoMapper, NHibernate, lazy loading oraz problem select n+1". Dzisiaj wrócę na chwilę do tematu Automappera i NH.

Spójrzmy na klasy:

  1:  public class User
  2:  {
  3:      public virtual int Id { get; set; }
  4:      public virtual string Email { get; set; }
  5:      public virtual Country Country { get; set; }
  6:  }
  7:  
  8:  public class Country
  9:  {
 10:      public virtual int Id { get; set; }
 11:      public virtual string Name { get; set; }
 12:  }

Oraz na dane, które mają nam wystarczyć do utworzenia nowego użytkownika w systemie:

  1:  public class CreateUserRequest
  2:  {
  3:      public string Email { get; set; }
  4:      public int CountryId { get; set; }
  5:  }

Całkiem standardowa (choć oczywiście niesamowicie wykastrowana z jakiejkolwiek złożoności) sytuacja.

W tak banalnym przypadku nie ma problemu z ręcznym utworzeniem klasy User na postawie otrzymanego żądania, ale w bardziej skomplikowanych scenariuszach ręczne mielenie takich instrukcji jest najzwyczajniej w świecie żmudne i głupie, i tu właśnie z pomocą przychodzi Automapper. Chcę móc napisać coś takiego:

  1:  User newUser = Mapper.Map<CreateUserRequest, User>(request);

Wszyscy użytkownicy NH znają zapewne (albo: powinni znać) różnicę między session.Get() i session.Load() (a jak nie znają to odsyłam do Ayende). Przy operacjach tego typu pod User.Country zdecydowanie chciałbym wstawić:

  1:  session.Load<Country>(request.CountryId)

Jednak… pisać takie coś przy wszystkich mapowaniach (albo dla wszystkich mapowań robić osobne "rezolwery") to robota – jak ręczne mapowanie – trochę żmudna i trochę głupia.

Przy trzecim z kolei mapowaniu obok szarej komórki zajarzyła się żaróweczka, której drżące światło za chwil kilka oświetliło taki twór:

  1:  public class LoadingEntityResolver<TEntity> : ValueResolver<int, TEntity>
  2:      where TEntity: IMyEntity
  3:  {
  4:      private readonly ISession _session;
  5:  
  6:      public LoadingEntityResolver(ISession session)
  7:      {
  8:          _session = session;
  9:      }
 10:  
 11:      protected override TEntity ResolveCore(int source)
 12:      {
 13:          return _session.Load<TEntity>(source);
 14:      }
 15:  }
[Uwaga 1: IMyEntity to interfejs implementowany przez wszystkie moje encje, każda z nich ma Id typu int] [Uwaga 2: o tym jak wstrzyknąć sesję do Resolvera można poczytać w innym moim poście: "Automapper a Dependency Injection"]

Teraz w konfiguracji mapowania wystarczy podać ten typ jako custom resolver i reszta zrobi się sama:

  1:  Mapper.CreateMap<CreateUserRequest, User>()
  2:      .ForMember(dst => dst.Country, _ => _.ResolveUsing<LoadingEntityResolver<Country>>().FromMember(src => src.CountryId))
  3:      ;

Coolers.

0 0 votes
Article Rating
3 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
General
General
13 years ago

Ostatnio wyprodukowało mi się sporo takich mappingów, gdzie w ViewModelu miałem np. MyEntityId i musiałem to mapować w mojej encji na MyEntity (tak jak u Ciebie CountryId na Country). Po kilku takich mappingach stwierdziłem, że to bezsensowny monkey code. Zatem zrobiłem sobie klasę o nazwie EntityViewModel, której używałem w ViewModelu w taki sposób:

public class EnityViewModel
{
public long Id {get;set;}
}

public class CreateUserRequest
{
public string Email { get; set; }
public EnityViewModel Country { get; set; }
}

Teraz jedyne co trzeba zrobić, to dla każdej encji stworzyć taki prosty mapping (można to też jakoś zautomatyzować):
Mapper.CreateMap<Country, EntityViewModel>();
Mapper.CreateMap<EntityViewModel, Country>(); // w drugą stronę

Teraz jeżeli użyjesz mappingu skonfigurowanego tak:
Mapper.CreateMap<CreateUserRequest, User>();

to jako wynik metody Mapper.Map(request, user) otrzymasz encję User, gdzie właściwość Company będzie miała tylko ustawione Company.Id. Zakładając, że w konfiguracji mappingu dla CreateUserRequest.Company masz Cascade = None, to NHibernate przy zapisywaniu będzie wiedział co ma zrobić.

Mam nadzieję, że się przyda:)
Krzysiek

General
General
13 years ago

* mała poprawka:

[…]
otrzymasz encję User, gdzie właściwość Country będzie miała tylko ustawione Country.Id. Zakładając, że w konfiguracji mappingu dla User.Country masz Cascade = None, to NHibernate przy zapisywaniu będzie wiedział co ma zrobić.

procent
13 years ago

@General:
Prawda, ciekawe podejście do problemu. Dzięki.

Kurs Gita

Zaawansowany frontend

Szkolenie z Testów

Szkolenie z baz danych

Książka

Zobacz również