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: }
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.
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
* 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ć.
@General:
Prawda, ciekawe podejście do problemu. Dzięki.