Mapper - mapowanie z/do viewmodeli

0

Cześć,

Zastanawiam się czy nie da się tego zrobić prościej i czy rzeczywiście mapowanie z/do viewmodeli musi wyglądać tak:

            //Mapping company entity to company details view model
            CreateMap<Company, CompanyDetailsViewModel>()
                .ForMember(dto => dto.Id, opt => opt.MapFrom(src => src.Id))
                .ForMember(dto => dto.Name, opt => opt.MapFrom(src => src.Name))
                .ForMember(dto => dto.City, opt => opt.MapFrom(src => src.Address.City))
                .ForMember(dto => dto.Voivodeship, opt => opt.MapFrom(src => src.Address.City.Voivodeship))
                .ForMember(dto => dto.PostalCode, opt => opt.MapFrom(src => src.Address.PostalCode))
                .ForMember(dto => dto.Street, opt => opt.MapFrom(src => src.Address.Street))
                .ForMember(dto => dto.ApartmentNumber, opt => opt.MapFrom(src => src.Address.ApartmentNumber))
                .ForMember(dto => dto.StreetNumber, opt => opt.MapFrom(src => src.Address.StreetNumber))
                .ForMember(dto => dto.Workplaces, opt => opt.MapFrom(src => src.Workplaces));

            //Mapping company details view model to company entity
            CreateMap<CompanyDetailsViewModel, Company>()
                .ForMember(dto => dto.Id, opt => opt.MapFrom(src => src.Id))
                .ForMember(dto => dto.Name, opt => opt.MapFrom(src => src.Name))
                .ForPath(dto => dto.Address.PostalCode, opt => opt.MapFrom(src => src.PostalCode))
                .ForPath(dto => dto.Address.City, opt => opt.MapFrom(src => src.City))
                .ForPath(dto => dto.Address.Street, opt => opt.MapFrom(src => src.Street))
                .ForPath(dto => dto.Address.StreetNumber, opt => opt.MapFrom(src => src.StreetNumber))
                .ForPath(dto => dto.Address.ApartmentNumber, opt => opt.MapFrom(src => src.ApartmentNumber))
                .ForPath(dto => dto.Address.City.Voivodeship, opt => opt.MapFrom(src => src.Voivodeship))
                .ForMember(dto => dto.Workplaces, opt => opt.MapFrom(src => src.Workplaces));
  1. Czy jeśli stworzę kolejny model np. CompanySimplyViewModel, to będę musiał z/do niego mapować w podobny sposób, właściwość po właściwości?
  2. Mój obiekt Company entity ma w bazie danych podane tylko Name oraz Id na starcie, ma też klucz obcy do tabeli Address. W momencie "aktualizacji" danych firmy, gdy użytkownik wprowadzi jakąś włąsciwość, która jest mapowana do tabeli Address, to czyEF Core będzie wiedział, że jeśli klucz obcy w tabeli Companies dla adresu jest pusty, to ma go utworzyć, a jeśli już jest to go tylko zaktualizować? czy to wszystko muszę mu wskazać w klasie repozytorium?
4

AutoMapper potrafi rozpoznać te same nazwy właściwości:

 .ForMember(dto => dto.Name, opt => opt.MapFrom(src => src.Name))

Takich mapowań nie musisz wpisywać "ręcznie", zadziałają z automatu :) wystarczy samo stworzenie mapy

      .ForPath(dto => dto.Address.PostalCode, opt => opt.MapFrom(src => src.PostalCode))

Dla takiego przypadku jeżeli w DTO właściwość nazwiesz AddressPostalCode, też powinien złapać automatycznie:)

0

Mam problem ze zmapowaniem wybranych wartości z listboxa (właściwość IEnumerable<int> AuthorsIds) do klasy entity. Model wypełniany jest prawidłowo, ale przy mapowaniu wybrane Ids są "gubione.

Zbudowowałem takie mapowania

            CreateMap<BookDetailsViewModel, Book>().ForMember(dto => dto.Authors, opt => opt.MapFrom(src => src.Authors))
                .AfterMap((src, dto) =>
                {
                    foreach (var entityBookAuthor in dto.Authors)
                    {
                        entityBookAuthor.Book = dto;
                    }
                });

            CreateMap<DropdownModel, BookAuthor>()
                .ForMember(dto => dto.Author, opt => opt.MapFrom(src => src));

Poniżej klasy obiektów:

    public class DropdownModel
    {
        public int Id { get; set; }
        public string DisplayName { get; set; }
    }

    public class BookAuthor
    {
        public int BookId { get; set; }
        public virtual Book Book { get; set; }
        public int AuthorId { get; set; }
        public virtual Author Author { get; set; }
    }

    public class Book
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public virtual ICollection<BookAuthor> Authors { get; set; }
        public string PublishingHouse { get; set; }
        public int NumberOfPages { get; set; }
        public int ISBN { get; set; }
        public Book()
        {
            Authors = new Collection<BookAuthor>();
        }
    }

    public class BookDetailsViewModel
    {
        public string Title { get; set; }
        public string PublishingHouse { get; set; }
        public int NumberOfPages { get; set; }
        public int ISBN { get; set; }
        public IEnumerable<int> AuthorsIds { get; set; }
        public IEnumerable<DropdownModel> Authors {get;set;}
    }
1

ja to jednak bym zrezygnował z automappera - może konstruktory czy coś

0

@boska_cebula: potrzebuję IEnumerable<int> AuthorsIds aby przechwycić wybrane wartości przez użytkownika. Mój Model.Authors to DropdownModel, a model.AuthorsIds to jedna z właściwości BookDetailsViewModel:

    public class BookDetailsViewModel
    {
        public string Title { get; set; }
        public string PublishingHouse { get; set; }
        public int NumberOfPages { get; set; }
        public int ISBN { get; set; }
        public ICollection<int> AuthorsIds { get; set; }
        public IEnumerable<DropdownModel> Authors {get;set;}
    }
        @Html.DropDownListFor(model => model.AuthorsIds, new SelectList(Model.Authors, "Id", "DisplayName", "Id"),
    "-- Wybierz autora lub autorów --", new { @class = "form-control select", @multiple = true, @id="authors" })
3
CreateMap<BookDetailsViewModel, Book>()
  .ForMember(dto => dto.Authors, opt => opt.MapFrom(src => src.Authors.Where(a => src.AuthorsIds.Contains(a.Id))))

powinno wystarczyć.

Tak właściwie to nie rozumiem dlaczego tak uparłeś się na Automappera - komercyjnie ostatni raz użyłem go ~3 lata temu. Ta biblioteka tylko zaciemnia kod i jak widać na Twoim przykładzie powoduje więcej problemów niż rozwiązuje, a tu serio wystarczy przykładowo prosty konstruktor

EDIT:

class Program
    {
        static void Main(string[] args)
        {
            var vm = new BookDetailsViewModel
            {
                Id = 105,
                Title = "test title",
                PublishingHouse = "ph test",
                NumberOfPages = 102,
                ISBN = 123,
                AuthorsIds = new[] { 1, 3, 4 },
                Authors = new[]
                {
                    new DropdownModel { Id = 1, DisplayName = "author 1" },
                    new DropdownModel { Id = 2, DisplayName = "author 2" },
                    new DropdownModel { Id = 3, DisplayName = "author 3" },
                    new DropdownModel { Id = 4, DisplayName = "author 4" },
                    new DropdownModel { Id = 5, DisplayName = "author 5" }
                }
            };

            var mapper = GetMapper();
            var book = mapper.Map<Book>(vm);
        }

        static Mapper GetMapper()
        {
            var config = new MapperConfiguration(cfg =>
                {
                    cfg.CreateMap<BookDetailsViewModel, Book>()
                       .ForMember(dto => dto.Authors, opt => opt.MapFrom(src => src.Authors.Where(a => src.AuthorsIds.Contains(a.Id))))
                       .ForMember(dto => dto.AuthorsManyToMany, opt => opt.MapFrom(src => src.Authors.Where(a => src.AuthorsIds.Contains(a.Id)).Select(x => new BookAuthor { BookId = src.Id, AuthorId = x.Id })));
                    cfg.CreateMap<DropdownModel, Author>()
                       .ForMember(e => e.AuthorId, opt => opt.MapFrom(src => src.Id));
                });

            return new Mapper(config);
        }
    }

    public class BookAuthor
    {
        public int BookId { get; set; }
        public virtual Book Book { get; set; }
        public int AuthorId { get; set; }
        public virtual Author Author { get; set; }
    }

    public class Author
    {
        public int AuthorId { get; set; }
        public string DisplayName { get; set; }
    }

    public class Book
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string PublishingHouse { get; set; }
        public int NumberOfPages { get; set; }
        public int ISBN { get; set; }
        public virtual ICollection<BookAuthor> AuthorsManyToMany { get; set; } = new List<BookAuthor>();
        public virtual ICollection<Author> Authors { get; set; }
    }

    public class BookDetailsViewModel
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string PublishingHouse { get; set; }
        public int NumberOfPages { get; set; }
        public int ISBN { get; set; }
        public IEnumerable<int> AuthorsIds { get; set; }
        public IEnumerable<DropdownModel> Authors { get; set; } = new List<DropdownModel>();
    }

    public class DropdownModel
    {
        public int Id { get; set; }
        public string DisplayName { get; set; }
    }

tak działa - przy okazji masz błąd w Book

0

Ostatecznie i tak zrobiłem to konstruktorem, nie wiem co jest nie halo. Poniżej mój kod, który nie działa:

            CreateMap<BookDetailsViewModel, Book>()
                .ForMember(dto => dto.Authors, opt => opt
                .MapFrom(src => src.Authors.Where(a => src.AuthorsIds.Contains(a.Id))
                .Select(x => new BookAuthor { BookId = src.Id, AuthorId = x.Id })));

            CreateMap<DropdownModel, BookAuthor>()
                .ForMember(dto => dto.AuthorId, opt => opt.MapFrom(src => src.Id));

    public class Author
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string SecondName { get; set; }
        public string Nationality { get; set; }
        public virtual ICollection<BookAuthor> Books { get; set; } = new List<BookAuthor>();
        public Author()
        {
            Books = new Collection<BookAuthor>();
        }
    }
    public class Book
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public virtual ICollection<BookAuthor> Authors { get; set; } = new List<BookAuthor>();
        public string PublishingHouse { get; set; }
        public int NumberOfPages { get; set; }
        public int ISBN { get; set; }
        public Book()
        {
            Authors = new Collection<BookAuthor>();
        }
    }
    public class BookDetailsViewModel
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string PublishingHouse { get; set; }
        public int NumberOfPages { get; set; }
        public int ISBN { get; set; }
        public IEnumerable<int> AuthorsIds { get; set; }
        public IEnumerable<DropdownModel> Authors {get;set;} = new List<DropdownModel>();
    }
    public class DropdownModel
    {
        public int Id { get; set; }
        public string DisplayName { get; set; }
    }
    public class BookAuthor
    {
        public int BookId { get; set; }
        public virtual Book Book { get; set; }
        public int AuthorId { get; set; }
        public virtual Author Author { get; set; }
    }

Poniżej zwykłe mapowanie, które działa poprawnie:

        public void AddBook(BookDetailsViewModel model)
        {
            //Book book = _mapper.Map<Book>(model);
            Book book = new Book();

            foreach (var id in model.AuthorsIds)
            {
                book.Authors.Add(new BookAuthor()
                {
                    AuthorId = id,
                    Author = _authorRepository.GetAuthorById(id),
                    Book = book
                });
            }

            book.ISBN = model.ISBN;
            book.NumberOfPages = model.NumberOfPages;
            book.PublishingHouse = model.PublishingHouse;
            book.Title = model.Title;
            _bookRepository.AddBook(book);
        }
1
boska_cebula napisał(a):

Tak właściwie to nie rozumiem dlaczego tak uparłeś się na Automappera - komercyjnie ostatni raz użyłem go ~3 lata temu. Ta biblioteka tylko zaciemnia kod i jak widać na Twoim przykładzie powoduje więcej problemów niż rozwiązuje, a tu serio wystarczy przykładowo prosty konstruktor

Ja bym nie przesadzał, umiejętnie użyty AutoMapper pozwala pozbyć się tysięcy linijek małpiego kodu. W sytuacji, gdy zgodność między obiektami jest na poziomie ponad 75% nie ma sensu go nie używać, a od 50% da się już uzasadnić jego używanie (no, zależy jeszcze ile tych obiektów jest).
Tyle, że to raczej nie ma zastosowania do aplikacji z GUI czy nawet CRUDów. Ale w takich chyba najczęściej AutoMapper jest używany.

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