jsdev17
jsdev17

Reputation: 1110

EF Core: How to avoid duplicate entries?

I'm building an app to keep an inventory of books.

When a book is added, I am checking if the book's publisher and author(s) already exist in the database. If the answer is no, then it gets created. But if, for example, a publisher already exists, I fetch it from the DB and set it on the incoming Book object to avoid creating a new entry, basically with the same value but a different id.

However, I am getting an error when attempting to do this:

System.InvalidOperationException: The instance of entity type 'Publisher' cannot be tracked because another instance with the key value '{Id}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.

I've been trying to make sense of this error, but have not found an answer yet (I get the same error when doing the same thing for Authors).

Here's some of the code:

BooksService

public async Task<BookDto> AddBookAsync(BookDto book)
{
    await SetPublisherIfItAlreadyExists(book);
    await SetAnyAuthorsThatMayAlreadyExist(book);
    
    var bookEntity = _mapper.Map<Book>(book);
    var newBook = await _booksRepository.AddBookAsync(bookEntity);
    return _mapper.Map<BookDto>(newBook);
}

private async Task SetPublisherIfItAlreadyExists(BookDto book)
{
    if (book.Publisher?.Name == null) return;
    var publisher = await _publisherRepository.GetPublisherByNameAsync(book.Publisher.Name);
    if (publisher == null) return;
    book.Publisher = _mapper.Map<PublisherDto>(publisher);
}

private async Task SetAnyAuthorsThatMayAlreadyExist(BookDto book)
{
    if (!book.Authors.Any()) return;
    
    var existingAuthors = new List<Author>();
    var nonExistingAuthors = new List<AuthorDto>();

    foreach (var incomingAuthor in book.Authors)
    {
        var author = await _authorsRepository.GetAuthorByNameAsync(incomingAuthor.FirstName, incomingAuthor.LastName);
        if (author != null)
        {
            existingAuthors.Add(author);
        }
        else
        {
            nonExistingAuthors.Add(incomingAuthor);
        }
    }

    if (!existingAuthors.Any()) return;
    var existingAuthorsDtos = _mapper.Map<List<AuthorDto>>(existingAuthors);
    book.Authors = existingAuthorsDtos.Concat(nonExistingAuthors).ToList();
}

PublisherRepository

public async Task<Publisher> GetPublisherByNameAsync(string publisherName)
{
    var publisher = await _dbContext.Publishers.FirstOrDefaultAsync(p => p.Name.Equals(publisherName));
    return publisher;
}

AuthorsRepository

public async Task<Author> GetAuthorByNameAsync(string firstName, string lastName)
{

    var author = await _dbContext.Authors.FirstOrDefaultAsync(a =>
        a.FirstName.ToLower().Equals(firstName.ToLower()) &&
        a.LastName.ToLower().Equals(lastName.ToLower())
    );
    return author;
}

BooksRepository

public async Task<Book> AddBookAsync(Book book)
{
    await _dbContext.Books.AddAsync(book);
    await _dbContext.SaveChangesAsync();
    return book;
}

How the error is triggered (Update)

For simplicity's sake, here's a simplified payload included in an HTTP Request. Assume the DB is empty on the first request

First request

// POST /books
{
   title: "book title",
   publisher: "sunny publications"
   ... // other properties
}

Second request

// POST /books
{
   title: "some other book",
   publisher: "sunny publications"
   ... // other properties
}

This second request causess the described error to be thrown.

What am I missing here? Will appreciate any help!

Upvotes: 1

Views: 1947

Answers (1)

CodeCaster
CodeCaster

Reputation: 151720

You get the publisher if it exists, this starts tracking that entity.

Then you map it to a PublisherDto to assign it to your BookDto, and map the BookDto back to an entity, including the publisher. This creates the second Publisher instance with the same Id.

Don't map and map again, create the entity on the entity.

Upvotes: 4

Related Questions