Search code examples
c#.net-coremany-to-manyautomapperautomapping

.Net Core Automapper with many to many relationship problem


I am new with Automapper and I have a problem with mapping my classes which have many-to-many relationships. I think I need something like nested mapping but the documentation does not seem for me clearly. I know that I am doing something wrong with AutoMapper configuration but I can't figure out what's wrong.

When I am adding a new book to the database I would like to receive information about the book and the authors who are written the book. (expected JSON result is below)

I have the following classes:

    public class Book
    {
        public int BookId { get; set; }
        public string Title { get; set; }
        public string ISBN { get; set; }
        public string Category { get; set; }
        public int PublisherId { get; set; }
        public Publisher Publisher { get; set; }
        public ICollection<BookAuthor> BooksAuthors { get; set; }
}

    public class Author
    {
        public int AuthorId { get; set; }
        public string AuthorName { get; set; }
        public string AuthorLastName { get; set; }
        public ICollection<BookAuthor> BooksAuthors { get; set; }
    }

    public class BookAuthor
    {
        public int BookId { get; set; }
        public Book Book { get; set; }

        public int AuthorId { get; set; }
        public Author Author { get; set; }
    }

DTOs

    public class BookForNewDto
    {
        public string Title { get; set; }
        public string ISBN { get; set; }
        public string Category { get; set; }
        public int PublisherId { get; set; }
        public ICollection<AuthorForNewBookDto> Authors { get; set; }
    }

    public class AuthorForNewBookDto
    {
        public int AuthorId { get; set; }
    }

BookController (AddBook)

        public async Task<IActionResult> AddBook([FromBody] BookForNewDto bookForNewDto)
        {
            var bookToCreate = _mapper.Map<Book>(bookForNewDto);

            var bookToReturn = _mapper.Map<BookForNewDto>(bookToCreate);

            var bookToAdd = await _context.Books.AddAsync(bookToCreate);

            await _context.SaveChangesAsync();

            var bookIdToAdd = bookToCreate.BookId;
            var authorIdToAdd = bookForNewDto.Authors.Select(aa => aa.AuthorId).ToList();

            foreach (var item in authorIdToAdd)
            {
                var bookAuthorToAdd = new BookAuthor()
                {
                    BookId = bookIdToAdd,
                    AuthorId = item
                };
                var newBookAuthor = await _context.BooksAuthors.AddAsync(bookAuthorToAdd);
                await _context.SaveChangesAsync();
            }

            return Ok(bookToReturn);
        }

AutoMapper Profiler // I attached only the section which concerns Adding a new book.

public class AutoMapping : Profile
    {
        public AutoMapping()
        {
               
            CreateMap<BookForNewDto, Book>();
            CreateMap<Book, BookForNewDto>()
                .ForMember(dto => dto.PublisherId, opt => opt.MapFrom(x => x.PublisherId))
                .ForMember(dto => dto.Authors, c => c.MapFrom(c => c.BooksAuthors));

            CreateMap<BookForNewDto, AuthorForNewBookDto>()
                .ForMember(dto => dto.AuthorId, opt => opt.MapFrom(x => x.Authors.Select(aaa => aaa.AuthorId)));
        }

My question is how do I configure my AutoMapper Profiler to get the JSON result below? I have a problem with the authors. I'm still getting an empty list.

{
    "title": "sample title",
    "isbn": "123-41-5-12311",
    "category": "test",
    "publisherId": 1,
    "authors": [
            {
                "authorId": 43
            },
            {
                "authorId": 45
            },
            {
                "authorId": 134
            },
}

Solution

  • You have to remember that automapper relies on property names to identify which "automap" can be done. They need to be identical if you want Automapper to automap them. In your case with this line of code:

    var bookToCreate = _mapper.Map<Book>(bookForNewDto);
    

    you are trying to map a BookForNewDto entity to a Book one. What you are missing is the fact that BookForNewDto has a property called Authors and Book has a property called BooksAuthors. Automapper will never know that Authors and BooksAuthors is what it should map. So you need to specify this configuration with 1 line of code in your Automapper configuration:

    CreateMap<BookForNewDto, Book>() (this line is already present)
                        .ForMember(dto=>dto.BooksAuthors, opt => opt.MapFrom(x => x.Authors));
    

    Now if you debug your code you'll see that the ICollection BooksAuthors has been mapped from your BookForNewDto.

    Next mapping you need to add is also the AuthorForNewBookDto and BookAuthor since they are present in your 2 entities: remember that if you don't specify it, automapper will set null to your values.

    Here the final automapper configuration:

    CreateMap<BookForNewDto, Book>()
                    .ForMember(dto=>dto.BooksAuthors, opt => opt.MapFrom(x => x.Authors));
    CreateMap<Book, BookForNewDto>()
                    .ForMember(dto => dto.PublisherId, opt => opt.MapFrom(x => x.PublisherId))
                    .ForMember(dto => dto.Authors, c => c.MapFrom(c => c.BooksAuthors));
    
    CreateMap<BookForNewDto, AuthorForNewBookDto>()
                   .ForMember(dto => dto.AuthorId, opt => opt.MapFrom(x => x.Authors.Select(aaa => aaa.AuthorId)));
    CreateMap<AuthorForNewBookDto, BookAuthor>();
    CreateMap<BookAuthor, AuthorForNewBookDto>();