1

Entity model:

public class DocumentType : CodeBase
{
    [Required]
    [MaxLength(100)]
    public string Name { get; set; }

    public TimeSpan? Productiontime { get; set; }

    public bool IsDeliverable { get; set; }

    public virtual ICollection<DocumentTypeRetractRelation> DocumentTypes { get; set; }
    public virtual ICollection<DocumentTypeRetractRelation> RetractDocumentTypes { get; set; }
}

Relation model:

/// <summary>
/// Relationship between document types showing which documenttypes can 
/// retracted when delivering a new document.
/// </summary>
[Table("DocumentTypeRetractRelation")]
public class DocumentTypeRetractRelation
{
    public int DocumentTypeId { get; set; }
    public virtual DocumentType DocumentType { get; set; }

    public int RetractDocumentTypeId { get; set; }
    public virtual DocumentType RetractDocumentType { get; set; }
}

Model builder:

modelBuilder.Entity<DocumentTypeRetractRelation>().HasKey(x => new { x.DocumentTypeId, x.RetractDocumentTypeId });

modelBuilder.Entity<DocumentTypeRetractRelation>()
    .HasOne(x => x.DocumentType)
    .WithMany(x => x.DocumentTypes)
    .HasForeignKey(x => x.DocumentTypeId);

modelBuilder.Entity<DocumentTypeRetractRelation>()
    .HasOne(x => x.RetractDocumentType)
    .WithMany(x => x.RetractDocumentTypes)
    .HasForeignKey(x => x.RetractDocumentTypeId);

Update writer:

    public async Task<DocumentType> UpdateAsync(DocumentTypeUpdateDto documentTypeUpdateDto)
    {
        using (IUnitOfWork uow = UowProvider.CreateUnitOfWork<EntityContext>())
        {
            var documentTypeRepo = uow.GetCustomRepository<IDocumentTypeRepository>();

            var existingDocument = await documentTypeRepo.GetAsync(documentTypeUpdateDto.Id);

            if (existingDocument == null)
                throw new EntityNotFoundException("DocumentType", existingDocument.Id);

            foreach (var retractDocumentTypeId in documentTypeUpdateDto.RetractDocumentTypeIds)
            {
                existingDocument.RetractDocumentTypes.Add(new DocumentTypeRetractRelation()
                {
                    DocumentTypeId = existingDocument.Id,
                    RetractDocumentTypeId = retractDocumentTypeId
                });
            }

            documentTypeRepo.Update(existingDocument);

            await uow.SaveChangesAsync();

            return existingDocument;
        }
    }

When trying to update the existingDocument I get the following error:

The instance of entity type 'DocumentTypeRetractRelation' cannot be tracked because another instance of this type with the same key is already being tracked. When adding new entities, for most key types a unique temporary key value will be created if no key is set (i.e. if the key property is assigned the default value for its type). If you are explicitly setting key values for new entities, ensure they do not collide with existing entities or temporary values generated for other new entities. When attaching existing entities, ensure that only one entity instance with a given key value is attached to the context.

user2963570
  • 381
  • 6
  • 21
  • The exception indicates duplicate `DocumentTypeRetractRelation` **keys**. Check if the `existingDocument` navigation properties are populated. Also the lifetime of the `DbContext` used. – Ivan Stoev Apr 12 '17 at 16:48
  • They are. After the savechanges the id's get replaced by 2 times the same entity which is not the case before the savechanges. – user2963570 Apr 13 '17 at 10:04
  • You'd probably need to load them in advance, then merge with changes instead of blindly adding. – Ivan Stoev Apr 13 '17 at 10:07

1 Answers1

2

The problem is not the self referencing, but applying the many-to-many collection modifications which generate different DocumentTypeRetractRelation objects with the same PK as stated in the exception message.

The correct way currently in EF Core is to make sure RetractDocumentTypes of the existingDocument are loaded (contains the original values), then merge the changes by either using an existing or create new DocumentTypeRetractRelation objects.

Replace the following code

foreach (var retractDocumentTypeId in documentTypeUpdateDto.RetractDocumentTypeIds)
{
    existingDocument.RetractDocumentTypes.Add(new DocumentTypeRetractRelation()
    {
        DocumentTypeId = existingDocument.Id,
        RetractDocumentTypeId = retractDocumentTypeId
    });
}

with

// existingDocument.RetractDocumentTypes should be loaded (either eager or explicit)
existingDocument.RetractDocumentTypes = (
    from retractDocumentTypeId in documentTypeUpdateDto.RetractDocumentTypeIds
    join existingRelation in existingDocument.RetractDocumentTypes
    on retractDocumentTypeId equals existingRelation.RetractDocumentTypeId
    into existingRelations
    select existingRelations.FirstOrDefault() ?? new DocumentTypeRetractRelation()
    {
        DocumentTypeId = existingDocument.Id,
        RetractDocumentTypeId = retractDocumentTypeId
    }).ToList();

This would handle both added, removed and unchanged relations. You can do similar to DocumentTypes.

Actually looking at your model, the above code should be for DocumentTypes collection (since you a receiving the RetractDocumentTypeIds, which in combination with the document Id form the DocumentTypes collection content). So simply replace the RetractDocumentTypes with DocumentTypes.

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343