Programując w ASP .NET MVC często mamy potrzebę zmapować jeden obiekt na inny, może to być np. encja pobrana z bazy danych, którą trzeba pozbawić niektórych właściwości i przekazać do dalszych warstw naszej aplikacji, lub – w prostszy ujęciu – konwersja modelu na View Model, którym napełnimy widok naszej aplikacji.
Można oczywiście robić to ręcznie, misternie przepisując każda właściwość, np. tak:
var personsManualGen = persons.Select(item => new PersonVm { Age = item.Age, Name = item.Name, OfficeDesc = item.Office.Description, RoundSalary = Convert.ToInt32(item.Salary) }).ToList();
Ale jak mawiał jeden z moich profesorów – podstawową cechą informatyka jest lenistwo, a już programisty w szczególności! Dlatego powstało co najmniej kilka frameworków umożliwiających automatyczne mapowanie jednej klasy na inną.
Projektem obecnie najbardziej dynamicznie rozwijanym AutoMapper.
AutoMapper
Instalacja
Install-Package AutoMapper
Nie chce powielać – skądinąd bardzo dobrej dokumentacji, dlatego pokarzę tylko kilka przykładów oraz skupię się na zbadaniu jego wydajności.
Najprostsze użycie:
var config = new MapperConfiguration(cfg => cfg.CreateMap<Order, OrderDto>()); var mapper = config.CreateMapper(); OrderDto dto = mapper.Map<OrderDto>(order);
Istnieje również sporo dodatkowych możliwości, np.:
Zadania do wykonania przed i po mapowaniu obiektów:
var config = new MapperConfiguration(cfg => { cfg.CreateMap<Source, Dest>() .BeforeMap((src, dest) => src.Value = src.Value + 10) .AfterMap((src, dest) => dest.Name = "John");
Mapowanie warunkowe:
var config = new MapperConfiguration(cfg => { cfg.CreateMap<Foo,Bar>() .ForMember(dest => dest.baz, opt => opt.Condition(src => (src.baz >= 0))); });
Sporo opcji związanych z konwencjami nazewniczymi i konwersją znaków.
Możemy przekazywać parametry do konstruktorów, jeśli nie wynikają one wprost z nazwy:
var config = new MapperConfiguration(cfg => cfg.CreateMap<Source, SourceDto>() .ForCtorParam("valueParamSomeOtherName", opt => opt.MapFrom(src => src.Value)) );
Można też konwertować typy:
var config = new MapperConfiguration(cfg => { cfg.CreateMap<string, int>().ConvertUsing(Convert.ToInt32); cfg.CreateMap<string, DateTime>().ConvertUsing(new DateTimeTypeConverter()); cfg.CreateMap<string, Type>().ConvertUsing<TypeTypeConverter>(); cfg.CreateMap<Source, Destination>(); }); config.AssertConfigurationIsValid();
A co z wydajnością?
Dla testu wykonałem prosta aplikację dostępna na moim GitHubie:
Najpierw tworzę sporo egzemplarzy klasy PersonEntity za pomocą biblioteki Bogus – opisanej w poprzednim artykule:
public class PersonEntity { public string Name { get; set; } public int Age { get; set; } public decimal Salary { get; set; } public Office Office { get; set; } } public class Office { public string Description { get; set; } public DateTime OpeningHours { get; set; } }
Następnie dokonuje mapowania na piechotę:
var personsManualGen = persons.Select(item => new PersonVm { Age = item.Age, Name = item.Name, OfficeDesc = item.Office.Description, RoundSalary = Convert.ToInt32(item.Salary) }).ToList();
I mapowania za pomocą AutoMappera:
var config = new MapperConfiguration(cfg => { cfg.CreateMap<PersonEntity, PersonVm>() .ForMember(dest => dest.OfficeDesc, opt => opt.MapFrom(src => src.Office.Description)) .ForMember(dest => dest.RoundSalary, opt => opt.MapFrom(src => Convert.ToInt32(src.Salary))); }); var mapper = config.CreateMapper(); var personAutoMap = mapper.Map<List<PersonEntity>, List<PersonVm>>(persons);
Wyniki:
Dla 200tys. obiektów:
Dla 1tys. obiektów:
Dla 500tys. obiektów:
Podsumowanie:
AutoMapper jest ewidentnie wolniejszy od zwyczajnego mapowania jednej klasy na drugą, jest jednak szalenie wygodny w użyciu .
Zauważcie, że mapowane obiekty były dość proste, niestety w bardziej złożonych obiektach z większa ilością konwersji – narzut AutoMappera jest jeszcze bardziej widoczny.
W przypadku mapowania niewielkiej ilości obiektów, w sytuacji gdy większość pól posiada takie same nazwy (lub da się je przetłumaczyć wg jakiegoś klucza – np. poprzez dodanie prefixu) – użycie AutoMappera jest bardzo wygodne i przyspiesza istotnie development. Modyfikacja nazw pól czy logiki konwersji sprowadza się do zmiany tylko w jednym miejscu – gdzie przechowujemy konfigurację AutoMappera.
W sytuacjach gdzie mamy wiele złożonych elementów – ja raczej konwertowałbym obiekty na piechotę, ew. przedtem sprawdzając jak radzi sobie AutoMapper.
Jest też kilka sposobów optymalizacji (strojenia) AutoMappera, skupie się na nich w kolejnych artykułach! 🙂
Artykuł w ukazuje się w ramach projektu CyberHome tworzonego w ramach akcji DajSięPoznać.
Jeśli zależy Ci na wydajności polecam biblioteke Mapster 2.0 : https://github.com/eswann/Mapster. Na stronie znajduję się tabelka z porównaniem Mapstera do innych bibliotek pod względem wydajności.
Thx, nie znałem tej biblioteki!