jmoreno
jmoreno

Reputation: 13571

EF 6 SaveChanges with multiple references to same (changed) object

I have a class with two references to the same class. When updating the main class, I may also update the referenced class. When I have two references to the same (modified) object, I get an InvalidOperationException:

Attaching an entity of type 'ns.entity' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.

Simple example:

public class Example {
    public int OldestFriendId {get; set;}
    public int BestFriendId {get; set;}
    public virtual Friend Oldest {get; set; }
    public virtual Friend Best {get; set; }

 }

If while updating Example, I want to update the Middle name of my Oldest/Best friend, it works as long as they aren't the same. But if they are the same, then I get the above exception.

I can't figure out how to get this to work. I've tried setting references to null, saving them independently of the parent class, setting all references in them to null (EF is automatically creating two list of Examples in Friend).

How can I save an object that has changed when there are multiple references to it?

UPDATE: not yet working the way I want, but I have had some progress after removing the list of Examples from Friend. Also, the update is the result of a POST. Still investigating...

As sample code was asked for...this is from a post on a web app, no change was actually made

public ActionResult SaveEdit(int id, [Bind(Include = "OldestFriendId, BestFrinedId, Oldest, Best")] Example example)
{
    if (ModelState.IsValid)
        {
            using (((WindowsIdentity)ControllerContext.HttpContext.User.Identity).Impersonate())
            {
                using (var _db = new exampleEntities())
                {
                  //example.Best= example.Oldest; // this line would allow the update to work.

                  //next line is where the exception occurs
                   _db.Entry(example).State = EntityState.Modified;
                   _db.SaveChanges();
                 }
            }
        }
}

The EditorFor template:

@model Testing.Friend

<div class="col-md-10">
    @Html.HiddenFor(model => model.FriendId)
    @Html.EditorFor(model => model.FirstName)
    @Html.EditorFor(model => model.LastName)
</div>

The Edit view for Example

@model Testing.Example

@using (Html.BeginForm())
{
@Html.AntiForgeryToken()

<div class="form-horizontal">
    <h4>Example</h4>
    <hr />
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
    @Html.HiddenFor(model => model.ExampleId)

    <div class="form-group">
        @Html.LabelFor(model => model.OldestFriendId, "OldestFriendId", htmlAttributes: new { @class = "control-label col-md-2" })
        @Html.HiddenFor(model => model.OldestFriendId)
        @Html.EditorFor(model => model.Oldest)
    </div>

    <div class="form-group">
        @Html.LabelFor(model => model.BestFriendId, "BestFriendId", htmlAttributes: new { @class = "control-label col-md-2" })
        @Html.HiddenFor(model => model.BestFriendId)
        @Html.EditorFor(model=> model.Best)
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Save" class="btn btn-default" />
        </div>
    </div>
</div>

}

Upvotes: 3

Views: 3175

Answers (3)

jmoreno
jmoreno

Reputation: 13571

My solution to the problem was to stop binding the whole object, and bind to the individual objects.

public ActionResult SaveEdit(int id, [Bind(Include = "OldestFriendId, BestFrinedId")] Example example,
     Bind(Prefix="Oldest", Include = "FriendId, FirstName, MiddleName, LastName")] Friend oldest, 
     Bind(Prefix="Best", Include = "FriendId, FirstName, MiddleName, LastName")] Friend best) {
if (ModelState.IsValid)
    {
        using (((WindowsIdentity)ControllerContext.HttpContext.User.Identity).Impersonate())
        {
            using (var _db = new exampleEntities())
            {
               // do whatever processing you want on best and/or oldest
               example.BestFriendId = best.FriendId;
               example.OldestFriendId = oldest.FriendId;

               _db.Entry(example).State = EntityState.Modified;
               _db.SaveChanges();
             }
        }
    }
}

Upvotes: 1

Michal Ciechan
Michal Ciechan

Reputation: 13898

EDIT

The most likely cause is because when you retrieve the object back, it deserializes the 2 friends as 2 completely different objects (even when they are the same). Same problem as below, but rather than EF deserializing into 2 objects, ASP.NET MVC is doing it.

What you will have to do is something like the following:

  1. Check if the 2 Friend ID's are the same (as ID is the PK). If not continue as normal
  2. If they have the same ID, check if the 2 friend objects are the same.
  3. If they are the same go to step 5.
  4. Combine the changes together, however you want to deal with conflicts.
  5. Set one of the Freinds to the same as the other Friend reference, e.g. Best = Oldest
  6. SaveChanges()

Original Answer

My guess is that this is the classic problem of Include when you are retrieving the data.

When you do

 Context.Examples.Include(x => x.Oldest).Include(x => x.Best).ToList()

What is happening is EF will create TWO objects of friend(Oldest and Best), even if they point to the same record. This is a known problem with include.

So when you go to save after update, EF sees them as 2 separate entities with the same key (and data) and complains.

If this is the case you have a couple of options:

  • Retrieve a list of all Friends for the current example and then the Example without the include
  • Let EF use LazyLoading and load the Friends when and as you need them.

Upvotes: 2

phil soady
phil soady

Reputation: 11348

EDIT: Replaced with full sample code This example works for me. I think is does what you are trying.

using System;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Migrations;

namespace Ef6Test {
public class Program {
    public static void Main(string[] args) {
        ExecDb1();

    }

    private static void ExecDb1() {
        Database.SetInitializer(new MigrateDatabaseToLatestVersion<Ef6Ctx, Ef6MigConf>());
        WhichDb.DbName = "MSAMPLEDB";
        WhichDb.ConnType = ConnType.CtxViaDbConn;
        var sqlConn = GetSqlConn4DBName(WhichDb.DbName);
        var context = new Ef6Ctx(sqlConn);
        context.Database.Initialize(true);
        Console.WriteLine(WhichDb.DbName, context.Database.Exists() );
        AddJunk(context);

    }

    public static class WhichDb {
        public static string DbName { get; set; }
        public static string ConnectionName { get; set; }
        public static ConnType ConnType { get; set; }
    }

    public enum ConnType {
        CtxViaDbConn,
        CtxViaConnectionName
    }

    private static void AddJunk(DbContext context) {
        var friend = new Friend();
        friend.Name = "Fred";
        friend.Phone = "555-1232424";
        context.Set<Friend>().Add(friend);
        context.SaveChanges();

        // break here and check db content.

        var eg = new Example();
        eg.Best = friend;  // set them equal
        eg.Oldest = friend;
        friend.Name = "Fredie"; // change the name of the fly
        friend.Phone = "555-99999"; // and phone is also different
        context.Set<Example>().Add(eg); Add the new example
        context.SaveChanges();

        // result... 2 records.
        // The original friend record should be chnaged
    }

    public static DbConnection GetSqlConn4DBName(string dbName) {
        var sqlConnFact = new SqlConnectionFactory(
            "Data Source=localhost; Integrated Security=True; MultipleActiveResultSets=True");
        var sqlConn = sqlConnFact.CreateConnection(dbName);
        return sqlConn;
    }
}

public class MigrationsContextFactory : IDbContextFactory<Ef6Ctx> {
    public Ef6Ctx Create() {
        switch (Program.WhichDb.ConnType) {
            case Program.ConnType.CtxViaDbConn:
                var sqlConn = Program.GetSqlConn4DBName(Program.WhichDb.DbName); //
                return new Ef6Ctx(sqlConn);

            case Program.ConnType.CtxViaConnectionName:
                return new Ef6Ctx(Program.WhichDb.ConnectionName);
            default:
                throw new ArgumentOutOfRangeException();
        }
    }
}

public class Ef6MigConf : DbMigrationsConfiguration<Ef6Ctx> {
    public Ef6MigConf() {
        AutomaticMigrationsEnabled = true;
        AutomaticMigrationDataLossAllowed = true;
    }
}



public class Friend {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Phone { get; set; }

}

public class Example
{
    public int Id { get; set; }
    public int? BestFriendId { get; set; }
    public int? OldestFriendId { get; set; }
    public virtual Friend Best { get; set; }
    public virtual Friend Oldest { get; set; }


 }

   public class Ef6Ctx : DbContext {
    public Ef6Ctx(DbConnection dbConn) : base(dbConn, true) { }

    public Ef6Ctx(string connectionName) : base(connectionName) { }

    public DbSet<Friend> Friends { get; set; }


    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {


        modelBuilder.Entity<Example>()
                    .HasOptional(t=>t.Best)
                    .WithMany()
                    .HasForeignKey(x=>x.BestFriendId);

        modelBuilder.Entity<Example>()
                    .HasOptional(t => t.Oldest)
                    .WithMany()
                    .HasForeignKey(x => x.OldestFriendId);

        }
 }

}

Upvotes: 0

Related Questions