AutoMapper – mapowanie jednej klasy na drugą – wydajność i możliwości

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:

Wynik mojego testu
Wynik testu dla 200k obiektów

Dla 1tys. obiektów:

WynikAutoMapper
Wynik testu dla 1k obiektów

Dla 500tys. obiektów:

Wynik mojego testu dla 500k obiektów
Wynik testu dla 500k 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ć.


2 przemyślenia nt. „AutoMapper – mapowanie jednej klasy na drugą – wydajność i możliwości

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *