BoardGamesNook · C# · Programowanie

Refactoring: AutoMapper cz. 2

Dzisiaj czas na kontynuację poprzedniego wpisu o AutoMapperze.

Miałam następujący problem: moje mappery działały, aczkolwiek nie umiałam użyć 2 różnych obiektów początkowych (source), żeby zmapować je do jednego końcowego obiektu (destination). Doszłam do tego, że nie można zrobić tego w jednym kroku. Trzeba najpierw zmapować na podstawie jednego obiektu i wynik tego mapowania zmapować na podstawie drugiego obiektu. Kod wywołania wyglądał tak:

//source 1 - boardGame
//source 2 - gamer
//destination - gamerBoardGameViewModel
var gamerBoardGameViewModel = Mapper.Map<GamerBoardGameViewModel>(boardGame);
Mapper.Map(gamer, gamerBoardGameViewModel);

a kod profilu wyglądał tak:

public class GamerBoardGameProfile : Profile
{
    public GamerBoardGameProfile()
    {
        //mapper 1
        CreateMap<BoardGame, GamerBoardGameViewModel>();
        //mapper 2
        CreateMap<Gamer, GamerBoardGameViewModel>()
            .ForMember(dest => dest.GamerId, opt => opt.MapFrom(src => src.Id));
    }
}

Error mapping types Property: Id

Cokolwiek zrobiłam, zawsze wyskakiwał mi powyższy błąd. Po kilkudziesięciu minutach poszukiwań doszłam do tego, że mapper mapował Id (typu string) z obiektu Gamer, na Id (typu int) z obiektu GamerBoardGameViewModel. Typy się nie zgadzały, stąd powyższy błąd. Czemu tak robił? Ponieważ AutoMapper sam mapuje propercje, które mają takie same nazwy (w moim przypadku Id). Próbowałam więc zignorować mapowanie tej propercji przez wywołanie metody Ignore():

CreateMap<Gamer, GamerBoardGameViewModel>()
    .ForMember(dest => dest.Id, opt => opt.Ignore());

albo nawet na sztywno przypisać jakąś wartość:

CreateMap<Gamer, GamerBoardGameViewModel>()
    .ForMember(dest => dest.Id, opt => opt.MapFrom(src => 1));

Niestety nic to nie dało.

Zaczynamy od nowa

Zakomentowałam wszystkie mapery i zaczęłam rozwiązywanie problemu od nowa, w trochę inny sposób. Najpierw dodałam konfigurację mapowania wewnątrz metody Mapper.Initialize():

public static void InitializeAutoMapper()
{
    Mapper.Initialize(cfg =>
    {
        cfg.CreateMap<Gamer, GamerBoardGameViewModel>()
			.ForMember(dest => dest.Id, opt => opt.Ignore());
    });
}

Przetestowałam to i szok, zadziałało. Super! Przeniosłam tę konfigurację z powrotem do Profilu i wywołałam go w Mapper.Initialize():

public static void InitializeAutoMapper()
{
	Mapper.Initialize(cfg =>
	{
		cfg.AddProfile<GamerBoardGameProfile>();
	});
}

Nadal działało. Odkomentowałam pozostałem Profile i… I NIE DZIAŁA.

public static void InitializeAutoMapper()
{
	Mapper.Initialize(cfg =>
	{
		cfg.AddProfile<GamerBoardGameProfile>();
		cfg.AddProfile<GamerProfile>();
		cfg.AddProfile<GamerBoardGameProfile>();
		cfg.AddProfile<GameResultProfile>();
		cfg.AddProfile<BoardGameProfile>();
		cfg.AddProfile<GameTableProfile>();
	});
}

Ale przecież działało!

No ale jak to? Jeden działał, a wszystkie już nie? Jak to możliwe? No i tutaj właśnie zapaliła się żaróweczka nad głową. Wystarczyło sprawdzić dokładnie mapery żeby zauważyć, że w dwóch miejscach miałam taki sam maper (z obiektu X na obiekt Y). Co z tego, że pierwsza konfiguracja mapera była prawidłowa. Podczas deklaracji AutoMappera ta konfiguracja była prawdopodobnie zastępowana przez kolejną (która była już błędna).

Kofiguracja prawidłowa:

public ValidGamerProfile()
{
	CreateMap<Gamer, GamerBoardGameViewModel>()
		.ForMember(dest => dest.Id, opt => opt.Ignore())
		.ForMember(dest => dest.GamerId, opt => opt.MapFrom(src => src.Id));
}

Konfiguracja błędna:

public InvalidGamerProfile()
{
	CreateMap<Gamer, GamerBoardGameViewModel>()
		.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)),
		.ForMember(dest => dest.GamerId, opt => opt.MapFrom(src => src.Id));
}

Wniosek

Dobrym rozwiązaniem, który pozwoli na niepowtarzanie opisanego błędu, jest podział maperów ze względu na używany source. Pozwoli to na uniknięcie niepotrzebnych duplikacji i zmarnowanych godzin. Poniżej kod używany do mapowania do obiektu typu GamerBoardGameViewModel pochodzący z BoardGameProfile (wywoływanego najpierw):

public BoardGameProfile()
{
	CreateMap<BoardGame, GamerBoardGameViewModel>()
		.ForMember(dest => dest.BoardGameId, opt => opt.MapFrom(src => src.Id))
		.ForMember(dest => dest.BoardGameName, opt => opt.MapFrom(src => src.Name))
		.ForMember(dest => dest.ImageUrl, opt => opt.MapFrom(src => src.ImageUrl));
}

oraz z GamerProfile:

public GamerProfile()
{
	CreateMap<Gamer, GamerBoardGameViewModel>()
		.ForMember(dest => dest.Id, opt => opt.Ignore())
		.ForMember(dest => dest.ImageUrl, opt => opt.Ignore())
		.ForMember(dest => dest.BoardGameName, opt => opt.Ignore())
		.ForMember(dest => dest.GamerId, opt => opt.MapFrom(src => src.Id))
		.ForMember(dest => dest.GamerNickname, opt => opt.MapFrom(src => src.Nickname));
}

6 myśli na temat “Refactoring: AutoMapper cz. 2

  1. „Czemu tak robił? Ponieważ AutoMapper sam mapuje propercje, które mają takie same nazwy (w moim przypadku Id). ” Nie do końca tak jest. AutoMapper domyślnie mapuje property używając „StartsWith” co znaczy mniej więcej tyle: mamy klasę A a w niej propertę „Test” oraz klasę B a w niej propertę „TestKolejny” – automapper będzię próbował mapować i może nawet mu się to udać – exception poleci jeśli mamy różne typy i powiedzmy chcemy mapować null do nie nullowalnego obiektu.
    Natomiast mapowanie „ATakiTest” na „TestKolejny” nigdy nie będzię wykonywane. Polecam sprawdzić organoleptycznie na najnowszej wersji AutoMappera

    Polubienie

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj / Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj / Zmień )

Zdjęcie na Google+

Komentujesz korzystając z konta Google+. Wyloguj / Zmień )

Connecting to %s