Automapper oferuje coś takiego jak profile. Dokumentacja nie mówi nam o nich na dzień dzisiejszy zbyt wiele (link) (dla leniwych – nie mówi NIC:) ).
Po czymś o nazwie "profil" spodziewałem się możliwości utworzenia mniejszych "podkonfiguracji" charakterystycznych dla konkretnego scenariusza. Na przykład gdybym chciał mapowanie int->string mieć wspólne dla całej aplikacji, to nie umieszczałbym go w żadnym profilu. Natomiast wspomniane kiedyś mapowanie z czasu UTC na czas lokalny mógłbym mieć w profilu o nazwie "ClientSpecificProfile", żeby tylko podczas wysyłania danych do klienta następowała konwersja.
Porobiłem sobie profile, porejestrowałem, podefiniowałem mapowania… przyszło do wywołania metody Map()… i pojawiło się zonczysko. Metoda Map() nie ma przeciążenia przyjmującego nazwę profilu, który powinien być użyty! WTF?
Po niedługiej inwestygacji wszystko okazało się jasne. Profile to nic innego jak sposób na zgrupowanie w jednej klasie mapowań powiązanych ze sobą w jakiś sposób, ale i tak wspólnych dla całej aplikacji. Nie jest to żaden "scope". Korzystając ze statycznej fasady Mapper zawsze odwołujemy się do jednej i tej samej konfiguracji, definiowanej jednokrotnie dla całego systemu.
Ale jak temu zaradzić? Nadal chciałem mieć takie coś jak było zaplanowane. Rozwiązaniem okazało się pominięcie wspomnianej fasady i ręczne zakodowanie tego co ona robi, tyle że więcej niż raz. Efektem jest taka klasa:
1: public static class ObjectMapper 2: { 3: public static IMappingEngine Server { get; private set; } 4: public static IMappingEngine Client { get; private set; } 5: 6: public static void Configure() 7: { 8: ServerToClient(); 9: ClientToServer(); 10: } 11: 12: private static void ServerToClient() 13: { 14: var serverToClientConfig = new AutoMapper.Configuration(new TypeMapFactory(), MapperRegistry.AllMappers()); 15: 16: serverToClientConfig.ConstructServicesUsing(type => AppFacade.UnityContainer.Resolve(type)); 17: 18: serverToClientConfig.AddProfile<DateTimeProfile>(); 19: serverToClientConfig.AddProfile<UsersProfile>(); 20: //... more profiles 21: 22: serverToClientConfig.Seal(); 23: 24: ObjectMapper.Server = new MappingEngine(serverToClientConfig); 25: }
Teraz nigdzie nie używam Mapper, do wszystko odwołuję się poprzez własną fasadę ObjectMapper i udostępnione przez nią konfiguracje mapowań charakterystyczne dla odpowiednich scenariuszy. Wykonywane kroki konfiguracyjne bezczelnie zerżnąłem z oryginalnej implementacji za pomocą Reflectora.
A profil może wyglądać na przykład tak:
1: public class DateTimeProfile : Profile 2: { 3: protected override void Configure() 4: { 5: CreateMap<DateTime, DateTime>().ConvertUsing<UtcToLocalTimeConverter>(); 6: } 7: 8: public class UtcToLocalTimeConverter : TypeConverter<DateTime, DateTime> 9: { 10: private readonly IConfigProvider _configProvider; 11: 12: public UtcToLocalTimeConverter(IConfigProvider configProvider) 13: { 14: _configProvider = configProvider; 15: } 16: 17: protected override DateTime ConvertCore(DateTime source) 18: { 19: string targetTimeZoneName = _configProvider.TargetimeZone; 20: 21: TimeZoneInfo targetTimeZone = TimeZoneInfo.FindSystemTimeZoneById(targetTimeZoneName); 22: 23: return TimeZoneInfo.ConvertTimeFromUtc(source, targetTimeZone); 24: } 25: } 26: }
Po krótkiej konfuzji idea profili okazała się bardzo fajna, pozwalają na logiczne rozdzielenie definicji niepowiązanych ze sobą mapowań… tylko może nazwa nie jest najszczęśliwiej dobrana:) (na przykład MapRegistry bardziej by mi osobiście pasowało).
A nie lepiej(latwiej?) uzyc jakiegos DI do stworzenia odpowiednich profili?
@Assassin:
Tzn. jak? Definicje mapowań muszę gdzieś ręcznie wklepać.
Czy chodzi o wstrzykiwanie odpowiednich IMappingEngine w odpowiednie miejsca? Jeśli tak to moim zdaniem raczej nie ma znaczenia czy dostane IMappingEngine jako zalezność czy statycznie się do konkretnego odwołam, takiego czegoś i tak raczej nie ma sensu testować.