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.