Transform W tym  artykule zobaczymy jak zintegrować AutoMapper  z ASP.NET CORE dla .NET 5, chociaż bądźmy szczerzy możesz skorzystać z tej biblioteki w każdym projekcie w C#.

Co to jest AutoMapper i jakie one problemy rozwiązuje ?

Zaczynamy zabawę mój czytelniku

Co to jest AutoMapper ?

AutoMapper pomaga Ci skopiować dane z jednego obiektu do drugiego. Szczerze to był taki moment w mojej karierze programisty, w którym nie wiedziałem, że taka biblioteka istnieje.

Stworzyłem więc klasę statyczną, która miała metodę generyczną do przypisywania pewnych wartości, które się powtarzały we wszystkich klasach dziedziczących po StatusInfo

public class TransformToResponse<T> where T : StatuInfo, new()
{
    public static T AddStatusOutput(ResponseDataDetail result)
    {
        var transformed = new T();

        transformed.StatusOutput = new StatusOutput()
        {
            HostName = result.StatusOutput.HostName,
            LoginId = result.StatusOutput.LoginId,
            MessagesOutput = new List<MessageOutput>(),
        };

        foreach (var message in result.StatusOutput.MessagesOutput)
        {
            var newmessage = new MessageOutput();

            newmessage.MessageCode = message.MessageCode;
            newmessage.Text = message.Text;
            newmessage.AdditionalInfo = message.AdditionalInfo;
            transformed.StatusOutput.MessagesOutput.Add(newmessage);
        }

        return transformed;
    }
}

Stworzyłem też kiedyś metodę, która po prostu przypisywała wartości z jednego obiektu do drugiego.

private User TranslateUser(UserViewModel userView)
{
    return new User()
    {
        FirstName = userView.FirstName,
LastName = userView.LastName,
Login = userView.Login,
Role = userView.Role,
UserId = userView.UserId
}; }

Napisałem metodę, która przy użyciu refleksji, która  kopiuje do właściwości jednego obiektu do drugiego obiektu, jeśli typy i nazwy się zgadzają.

public static T2 ConvertToTheSameProperties<T1,T2>(T1  employee) where T2 : new()
{
    var prop = employee.GetType().GetProperties();
    var prop2 = typeof(T2).GetProperties();

    T2 u = new T2();

    foreach (var propertyInfo in prop)
    {

       bool propValue = propertyInfo.PropertyType == typeof (string) ||
                     propertyInfo.PropertyType == typeof (int) ||
                     propertyInfo.PropertyType == typeof (long) ||
                     propertyInfo.PropertyType == typeof (Guid) ||
                     propertyInfo.PropertyType == typeof(bool) ||
                     propertyInfo.PropertyType == typeof(bool?);

        foreach (var info in prop2)
        {

            if (propertyInfo.Name == info.Name && 
                info.CanWrite && 
                propertyInfo.CanRead)
            {

                if (propValue && propertyInfo.PropertyType == info.PropertyType)

                    info.SetValue(u, propertyInfo.GetValue(employee));
            }
        }
    }

    return u;
}

Działa ona także tylko dla typów : bool, int, long, Guid, string.

Te rozwiązania wyglądają w porządku, dopóki nie zdasz sobie sprawy, że do rozwiązania tego problemu powstała biblioteka, która istnieje już od wielu wielu lat.

W swojej aplikacji będziesz miał sytuacje, w której encję, które służą do komunikacją z bazą danych, a encję, które wysyłasz do klienta będą się pokrywać, chociaż nie będą takie same.

Możesz oczywiście zadać sobie pytanie, dlaczego to wszystko jest tak rozbite. Dobre praktyki mówią, że powinieneś mieć jednej klasy do wszystkiego we wszystkich warstwach.

Ma to dać elastyczność na zmiany kodu tak aby zmiana definicji klasy, którą wyciągasz z bazy automatycznie nie miała wpływu na to, co widać na stronie internetowej. Kto wie gdzie też ta klasa się serializuję albo deserializuje na XML bądź JSON.

Jest jednak problem. W którymś momencie musimy mapować te obiekty między sobą.

Wyobraź sobie, że mamy takie dwie klasy. 

Employee

public class Employee
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Login { get; set; }
    public string OrganizationCompanyName { get; set; }
    public int OrganizationId { get; set; }
    public string Role { get; set; }
    public string RootOrganizationCompanyName { get; set; }
    public int RootOrganizationId { get; set; }
    public string Type { get; set; }
    public int EmployeeId { get; set; }
}

oraz EmployeeDto, który jest obiektem służącym do komunikacji z bazą danych

public class EmployeeDto
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Login { get; set; }
    public string OrganizationCompanyName { get; set; }
    public int OrganizationId { get; set; }
    public string Role { get; set; }
    public string RootOrganizationCompanyName { get; set; }
    public int RootOrganizationId { get; set; }
    public string Type { get; set; }
    public int EmployeeId { get; set; }
}

Teraz gdy masz te dwa obiekty to przydałoby się mieć metodę, która z mapuje wartości jednej klasy na drugą. W zależności od liczby tych parametrów pisanie takiej metody może być irytujące.

private static Employee Map(EmployeeDto em)
{
    return new Employee()
    {
        //insert code here
    };
}

Co, jeśli masz 200 takich klas. 200 klas znaczy, że trzeba napisać 100 maperów  do nich.

Zobaczmy jak nam może pomóc w tym wszystkim automapper.

Jak AutoMapper działa ?

Jak się domyślasz korzysta on z refleksji. Widziałeś wcześniej moją banalną metodę do przypisywania jednego właściwości do drugiej .

AutoMapper potrafi to wszystko zrobić także, ale lepiej. 

Utwórzmy projekt ASP.NET CORE

Aby to miało jakiś konkret utworzy projekt ASP.NET CORE Web API w wersji .NET 5.

Utwórzmy projekt ASP.NET CORE

Zanim zrobimy cokolwiek warto dodać paczkę NuGet. 

Zanim zrobimy cokolwiek warto dodać paczkę NuGet.

Znajdź AutoMapper.Extensions.Microsoft.DependencyInjections. Ta paczka da nam potrzebną logikę wstrzykiwania zależności do ASP.NET CORE.

Znajdź AutoMapper.Extensions.Microsoft.DependencyInjections

Możesz także to wszystko zrobić korzystając "Package Manager Console". Wpisz tylko to polecenie

Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection

W klasie "startup.cs" do metody  "ConfigureServices" dodaj następujący kod.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

    //...
}

Teraz AutoMapper będzie skanował tą bibliotekę oraz projekt i będzie szukał klasy, która dziedziczy po "Profile".

Kod "AppDomain.CurrentDomain.GetAssemblies()" daje tablice wszystkich bibliotek, które znajdują się w projekcie.

Pora powiedzieć AutoMapper jak ma mapować klasy. Musimy dziedziczyć po Profile aby taką klasę utworzyć.

public class AutoMapperProfile : Profile
{
    public AutoMapperProfile()
    {
        CreateMap<EmployeeDto, Employee>();
    }
}

Mamy więc zmapowane obiekty co dalej trzeba zrobić?

Utworzyłem kontroler i dodałem do niego symulacje pobrania danych do obiektu DTO.

IMaper, który zostanie wstrzyknięty przez AutoMapper zajmie mapowaniem.

[ApiController]
[Route("[controller]")]
public class EmployeeController : Controller
{
    private readonly IMapper _mapper;

    public Employee Index()
    {
        EmployeeDto eDTO = new EmployeeDto()
        {
            EmployeeId = 121,
            FirstName = "Damian",
            LastName = "Morfeus",
            Login = "frankodomanamiga",
            OrganizationCompanyName = "LichyDzwig",
            OrganizationId = 34,
            Role = "Sprzątacz podłogi pietra 34",
            RootOrganizationCompanyName = "Dobra Winda",
            RootOrganizationId = 12,
            Type = "Sprzątacz"
        };

        return _mapper.Map<Employee>(eDTO);
} }

Skoro mówimy o wstrzyknięciu to oczywiście musi skorzystać z konstruktora. Wszystko inne zostanie zrobione przez AutoMapper który został przez z nas skonfigurowany wcześniej.

[ApiController]
[Route("[controller]")]
public class EmployeeController : Controller
{
    private readonly IMapper _mapper;
    
    public EmployeeController(IMapper
        mapper)
    {
        _mapper = mapper;
    }

Jeżeli uruchomisz kod to zobaczysz, że wszystko działa tak jak trzeba.

Aplikacja działa

Co, jeśli nazwy właściwości są róźne

Wracamy znowu do naszych klas. Dodałem tym razem dwie właściwości, które nazywają się inaczej.

W Employee mamy CurrentCity i HomePhone.

public class Employee
{
    public string CurrentCity { get; set; }
    public string HomePhone { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Login { get; set; }
    public string OrganizationCompanyName { get; set; }
    public int OrganizationId { get; set; }
    public string Role { get; set; }
    public string RootOrganizationCompanyName { get; set; }
    public int RootOrganizationId { get; set; }
    public string Type { get; set; }
    public int EmployeeId { get; set; }
}

W EmployeeDto mamy City i Phone.

public class EmployeeDto
{
    public string City { get; set; }
    public string Phone { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Login { get; set; }
    public string OrganizationCompanyName { get; set; }
    public int OrganizationId { get; set; }
    public string Role { get; set; }
    public string RootOrganizationCompanyName { get; set; }
    public int RootOrganizationId { get; set; }
    public string Type { get; set; }
    public int EmployeeId { get; set; }
}

AutoMapper co prawda działa na refleksji, ale oczywiście nie umie nam czytać w myślach.

public Employee Index()
{
    EmployeeDto eDTO = new EmployeeDto()
    {
        EmployeeId = 121,
        FirstName = "Damian",
        LastName = "Morfeus",
        Login = "frankodomanamiga",
        OrganizationCompanyName = "LichyDzwig",
        OrganizationId = 34,
        Role = "Sprzątacz podłogi piętra 34",
        RootOrganizationCompanyName = "Dobra Winda",
        RootOrganizationId = 12,
        Type = "Sprzątacz",
        City = "White UnderWoods",
        Phone = "646555777"
    };

    return _mapper.Map<Employee>(eDTO);
}

Dlatego w tym wypadku mapowanie nie zadziała.

brak mapowania JSON

Trzeba powiedzieć AutoMapperowi jak dana właściwość jest mapowana na co.

public class AutoMapperProfile : Profile
{
    public AutoMapperProfile()
    {
        CreateMap<EmployeeDto, Employee>()
           .ForMember(dest => dest.CurrentCity, 
            opt => opt.MapFrom(src => src.City))
           .ForMember(dest => dest.HomePhone, 
            opt => opt.MapFrom(src => src.Phone));
    }
}

Co, jeśli nasze klasy zawierają kolejne klasy, które wymagają mapowania.

Oto klasy, które definiują adres.

public class AdressDto
{
    public string CityCode { get; set; }
    public string Street { get; set; }
    public int StreetNumber { get; set; }
    public string Country { get; set; }
}

public class Adress
{
    public string CityCode { get; set; }
    public string OnStreet { get; set; }
    public string Country { get; set; }
}

Adresy są zawarte w naszych klasach.

public class Employee
{
    public Adress Adress { get; set; }

    public string CurrentCity { get; set; }
    public string HomePhone { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Login { get; set; }
    public string OrganizationCompanyName { get; set; }
    public int OrganizationId { get; set; }
    public string Role { get; set; }
    public string RootOrganizationCompanyName { get; set; }
    public int RootOrganizationId { get; set; }
    public string Type { get; set; }
    public int EmployeeId { get; set; }
}

public class EmployeeDto
{

    public AdressDto Adress { get; set; }
    public string City { get; set; }
    public string Phone { get; set; }

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Login { get; set; }
    public string OrganizationCompanyName { get; set; }
    public int OrganizationId { get; set; }
    public string Role { get; set; }
    public string RootOrganizationCompanyName { get; set; }
    public int RootOrganizationId { get; set; }
    public string Type { get; set; }
    public int EmployeeId { get; set; }
}

Do naszej definicji mapowania musimy dodać po prostu kolejną funkcje mapującą.

public AutoMapperProfile()
{
    CreateMap<EmployeeDto, Employee>()
       .ForMember(dest => dest.CurrentCity, opt => opt.MapFrom(src => src.City))
       .ForMember(dest => dest.HomePhone, opt => opt.MapFrom(src => src.Phone));

    CreateMap<AdressDto, Adress>()
        .ForMember(dest => dest.OnStreet,
        opt => opt.MapFrom
        (src => src.Street + " " + src.StreetNumber));
}

Dodatkowo utrudniłem sobie zadanie i jak widzisz teraz mapuje dwie właściwości do jednej. Kto powiedział, że tak nie można robić.

Mapowanie warunkowe

Co, jeśli bym chciał dodać więcej warunków.

Do pracownika dodałem dwie właściwości. Jednak określa warunek logiczny czy pracownik jest starszy niż 30 lat. Druga właściwość to po prostu komentarz

public class Employee
{
public string Comment { get; set; } public bool isOver30 { get; set; } public Adress Adress { get; set; } public string CurrentCity { get; set; } public string HomePhone { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Login { get; set; } public string OrganizationCompanyName { get; set; } public int OrganizationId { get; set; } public string Role { get; set; } public string RootOrganizationCompanyName { get; set; } public int RootOrganizationId { get; set; } public string Type { get; set; } public int EmployeeId { get; set; } } public class EmployeeDto { public int Age { get; set; } public AdressDto Adress { get; set; } public string City { get; set; } public string Phone { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Login { get; set; } public string OrganizationCompanyName { get; set; } public int OrganizationId { get; set; } public string Role { get; set; } public string RootOrganizationCompanyName { get; set; } public int RootOrganizationId { get; set; } public string Type { get; set; } public int EmployeeId { get; set; } }

Chce aby dla wieku większego niż 30 pewna właściwość była ustawiona na prawdę. Dla wieku powyżej 55 chce dodać komentarz "Zaraz emerytura". 

CreateMap<EmployeeDto, Employee>()
   .ForMember(dest => dest.CurrentCity,
        opt => opt.MapFrom(src => src.City))
   .ForMember(dest => dest.HomePhone,
        opt => opt.MapFrom(src => src.Phone))
   .ForMember(dest => dest.isOver30, opt =>
        opt.MapFrom(src => src.Age > 30 ? true : false))
   .ForMember(dest => dest.Comment, opt =>
        opt.MapFrom(src => src.Age > 55 ? "Zaraz emerytura" : ""));

Zobaczmy czy wszystkie mapowania działają.

public Employee Index()
{
    EmployeeDto eDTO = new EmployeeDto()
    {
        Age = 61,
        Adress = new AdressDto()
        {
            CityCode = "11250-50",
            Country = "Poland",
            Street = "Polonozea ",
            StreetNumber = 128
        },
        EmployeeId = 121,
        FirstName = "Damian",
        LastName = "Morfeus",
        Login = "frankodomanamiga",
        OrganizationCompanyName = "LichyDzwig",
        OrganizationId = 34,
        Role = "Sprzątacz podłogi piętra 34",
        RootOrganizationCompanyName = "Dobra Winda",
        RootOrganizationId = 12,
        Type = "Sprzątacz",
        City = "White UnderWoods",
        Phone = "646555777"
    };

    return _mapper.Map<Employee>(eDTO);
}

Jak widzisz mapowanie działa poprawnie.

JSON automapper działa

Podsumowanie

Jak widzisz AutoMapper jest bardzo łatwy w użyciu. Dla zaawansowanych przykładów prawdopobnie wciąż będziesz chciał napisać swoje metody transformujące.

Te metody mogą być użyte z AutoMapperem więc nie nie tracisz.

W końcu fajnie jest mieć tą logikę zawiniętą w interfejs "IMapper" niż po rozrzucaną po całym kodzie. Miłego mapowania.

Kod można pobrać tutaj : GitHub - PanNiebieski/example-FluentValidation-NLog-AutoMapper-SwaggerUI-ASPNETCORE5