Ręcznie pisane mapowania

0

Chcę pobrać z bazy kilku pierwszych użytkowników, których nick zawiera danego stringa:

var users = await dbContext.Users
                .Where(u => u.UserName.Contains(query.UserName))
                .Take(query.Limit)
                .Select(u => userMapper.UserShortInfo(u))
                .ToListAsync(cancellationToken);

UserShortInfoDto jest potrzebne w kilku innych DTO, dlatego mam metodę mapującą:

public UserShortInfoDto UserShortInfo(User user) => new UserShortInfoDto
        {
            Id = user.Id,
            UserName = user.UserName,
            AvatarUrl = GetAvatarUrl(user.Id)
        };

Do selekta przekazuję Func<User, UserShortInfoDto>, a powinienem Expression<Func<User, UserShortInfoDto>>. Tak więc napisałem osobną metodę zwracająca Expression:

public Expression<Func<User, UserShortInfoDto>> UserShortInfoExpr() => u =>
            new UserShortInfoDto
            {
                Id = u.Id,
                UserName = u.UserName,
                AvatarUrl = GetAvatarUrl(u.Id)
            };

Tylko że to oznacza duplikację kodu - te dwie metody robią prawie to samo (i podobna duplikacja wystąpi nie raz). Nie mogę zwrócić stąd u => UserShortInfo(u), bo kompilator zrozumie, że do mapowania potrzebny jest cały User, i EF pobierze wszystkie kolumny. Jak sobie z tym poradzić?

EDIT: W AutoMapperze jest chyba coś takiego jak ProjectTo, ale sądząc po wpisach na tym forum i artykułach takich jak ten nie jest on już modny w świecie .NETu. ;(

1

Wygląda na problem X-Y, ale jak bardzo Ci zależy, to w metodzie zwracającej Func po prostu skompiluj Expression: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/expression-trees/how-to-execute-expression-trees

0

O, faktycznie zapomniałem, że tak można. Jakie byłoby rozwiązanie problemu X? Czy chodzi o to, że serwery, na których hostowane są teraz aplikacje, mają tyle RAMu, a samo mapowanie przebiega tak szybko, że można je wykonywać osobno?

0

Jak potrzebujesz z bazy UserShortInfo, to pewnie ten typ mógłbyś wyciągać, a potem jechałbyś automapperem i nie miał takich problemów. Bez większej ilości kodu trudno wyczuć.

0

Właściwie to chciałem zaimplementować live search po username - to zapytanie jest wykonywane w tym kontekście. Co sądzisz o tym artykule na temat AutoMappera? https://cezarypiatek.github.io/post/why-i-dont-use-automapper/

2

Nie mam nic przeciwko używaniu automappera, zaoszczędza masę czasu. Jak ktoś upycha logikę w mapowaniu, to niech się nie dziwi, że kompilator tego nie wykrywa, to nie ten język, żeby w trakcie kompilacji dowodzić poprawności kodu. Jak masz testy do bardziej zaawansowanych mapowań, to nie ma problemu. Osobiście zazwyczaj robię jakiś generyczny interfejs IMapper<TFrom, TTo>, potem mam podstawową implementację opartą o automappera, a jeżeli potrzebuję bardziej skomplikowanej, to wtedy piszę dedykowany kod i po sprawie.

Załóżmy, że nie używasz automappera i piszesz własną funkcję konwertującą, która robi coś ponad zwykłe przepisanie pól, czy wtedy nie testujesz tego? A jeżeli testujesz, to czy masz pretensje do kompilatora, że nie wyłapał błędów sam, tylko musiałeś pisać testy jednostkowe? Tak samo z automapperem, jak masz bardziej skomplikowane mapowanie, to powinien być test do tego.

A co w sytuacji, gdy robisz trywialne przepisanie, ale na przykład masz literówkę w nazwie właściwości? To wtedy można napisać generyczny test, który używa AutoFixture do stworzenia jednego obiektu, mapuje go na drugi obiekt i z powrotem, a potem FluentValidatorem sprawdza, czy obiekty są dokładnie takie same. Jedna metoda, jedna klasa testowa, a potem tylko test case dla wszystkich par typów, które chcesz mapować. A co zrobisz, gdy nie masz automappera? Okej, masz wsparcie kompilatora przy trywialnych przepisaniach, ale znowu musisz każdą taką metodę zaklepać ręcznie. Jak masz trzy deteosy na krzyż, to można i tak, ale jak jest tego więcej (a w typowym crudzie masz pewnie zazwyczaj encję z bazy danych, encję dziedzinową, potem model widoku, więc już trzy typy i cztery mapowania), to przecież szkoda czasu.

0

Nie powiem, ma sens to, co piszesz. Chociaż z tym marnowaniem czasu to chyba nie do końca prawda, bo ogólnie przeciwnicy AutoMappera wspominają o czymś takim jak MappingGenerator: https://marketplace.visualstudio.com/items?itemName=54748ff9-45fc-43c2-8ec5-cf7912bc3b84.mappinggenerator (napisał go autor artykułu) ;)

2

No ale jeżeli możesz wygenerować mapowanie, to zapewne automapper wystarczy tak samo, więc nie widzę jakiegoś dużego zysku (poza wydajnościowym). No i to podejście wywali się, jak dojdzie nowa właściwość (bo zapewne zapomni się zaktualizować mapping), a automapper z testami będzie działał dalej.

0
Afish napisał(a):

Wygląda na problem X-Y, ale jak bardzo Ci zależy, to w metodzie zwracającej Func po prostu skompiluj Expression: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/expression-trees/how-to-execute-expression-trees

Nie da się jakoś podstawić Func do Expression<Func>?

taki pseudo code

public static Func<User, UserShortInfoDto> GetFunc()
{
    return new Func<User, UserShortInfoDto>(x => new UserShortInfoDto { Name = x.SecondName });
}

public static Expression<Func<User, UserShortInfoDto>> Map(User user)
{
    return Expression.Lambda<Func<User, UserShortInfoDto>>(Expression.Call(GetFunc().Method.Invoke(user));
}

1 użytkowników online, w tym zalogowanych: 0, gości: 1