2

I have read several posts about this (e.g. Why can't EF handle two properties with same foreign key, but separate references/instances?) issue but none provided a satisfying solution.

Using EF6, inserting an object with two navigation properties fails, when the properties have the same type, represent the same entry (i.e. have the same primary key value) but are two different object instances.

Setup:

public abstract class EntityBase
{
    public int Id { get; set; }
    public bool IsTransient => Id <= 0;
}

public class Person : EntityBase
{
    public string Name { get; set; }
}

public class Task : EntityBase
{
    public int CreatorId { get; set; }
    public int ReceiverId { get; set; }
    public string Text { get; set; }

    public Person Creator { get; set; }
    public Person Receiver { get; set; }
}

When I try to insert a new Task like the following, an Exception occurs when creator and receiver are the same Person (i.e. have the same Id value). Exception is: "... because more than one entity of type 'Person' have the same primary key value.".

public void AddTask(string text, Person creator, Person receiver)
{
    using (var context = new MyEntities())
    {
        Task task = new Task()
        {
            Text = text,
            Creator = creator,
            Receiver = receiver
        };
        context.Task.Add(task);
        context.Entry(creator).State = EntityState.Unchanged;
        context.Entry(receiver).State = EntityState.Unchanged; // Exception is raised here

        context.SaveChanges();
    }
}

The exception can be avoided using the following code:

public void AddTask(string text, Person creator, Person receiver)
{
    using (var context = new MyEntities())
    {
        Task task = new Task()
        {
            Text = text,
            Creator = creator,
            Receiver = receiver
        };
        context.Task.Add(task);
        context.Entry(creator).State = EntityState.Unchanged;
        if (creator.Id == receiver.Id)
            task.Receiver = creator;
        else
            context.Entry(receiver).State = EntityState.Unchanged;

        context.SaveChanges();
    }
}

Question: Is there any convenient way two avoid the exception except from checking the Id values of the navigation properties before insert?

This is a simple example given here. There are much more complex object graphs to be inserted where a manual check like shown here is quite cumbersome.

EDIT: The exception message is (german):

System.InvalidOperationException: Fehler beim Speichern oder Übernehmen der Änderungen, weil mehrere Entitäten des Typs 'Person' den gleichen Primärschlüsselwert aufweisen. Stellen Sie sicher, dass explizit festgelegte Primärschlüsselwerte eindeutig sind. Die von der Datenbank generierten Primärschlüssel müssen in der Datenbank und im Entity Framework-Modell ordnungsgemäß konfiguriert sein. Verwenden Sie den Entity Designer für die Database First-/Model First-Konfiguration. Verwenden Sie die Fluent-API 'HasDatabaseGeneratedOption' oder 'DatabaseGeneratedAttribute' für die Code First-Konfiguration.

The reason why I explicitly set the EntityState is because I'm working with detached objects. If I wouldn't set the state of the Person object it would be inserted as a new entry. To avoid this I mark it as Unchanged.

Abid
  • 565
  • 1
  • 5
  • 15
  • I have encountered a setup like this many times but never ran in this particular problem. Can you post the full exception? – Stefan Oct 04 '18 at 15:55
  • And can you explain why you are using `context.Entry(creator).State = EntityState.Unchanged;` to mark the state? If the entities are tracked it shouldn't be necessary. – Stefan Oct 04 '18 at 15:56
  • @Stefan Please see my edits. – Abid Oct 06 '18 at 12:35
  • Ha, yes, that's what's surprises me; it seems that marking as `unchanged` still lead to inserting in the database. Anyhow; I think you are pushing the ORM to hard, keep in mind that it's an full blown repository. Keeping a long lived Db context etc. can lead to some annoying maintainability issues. I know this doesn't help you much in this case but I would suggest to keep your ORM (entityframework) calls "short" and isolated. For an example with domain models (which might help you) see: https://stackoverflow.com/questions/23648832/viewmodels-in-mvc-mvvm-seperation-of-layers-best-practices – Stefan Oct 07 '18 at 08:23
  • Thanks for detailed clarification, but actually I think that using detached objects like I do results in NOT keeping _a long lived Db context_ and also keeping the ORM calls short ... – Abid Oct 07 '18 at 09:20
  • When re-using the creator and receiver later on, your question suggests otherwise ;-) ... nevertheless, I'll hope you will work it out. – Stefan Oct 07 '18 at 09:38
  • No offence, I mark you answer as accepted as it seems to be the only solution. – Abid Oct 08 '18 at 12:44

1 Answers1

0

You could just set the Id's:

using (var context = new MyEntities())
{
    Task task = new Task()
    {
        Text = text,
        CreatorId = creator.Id,
        ReceiverId = receiver.Id
    };
    context.Task.Add(task);
    context.SaveChanges();
}
Stefan
  • 17,448
  • 11
  • 60
  • 79
  • Yes but then I have to load each object again after saving it as I need the navigation properties in the view afterwards. So thats not optimal. – Abid Oct 04 '18 at 15:26