1

I didn't find any relevant answer here so I will trigger you, thanks in advance :

I have a controller with 2 methods of the Edit action, (I simplified it for better understanding):

MrSaleBeta01.Controllers
{
    public class PostsController : Controller
    {
        private MrSaleDB db = new MrSaleDB();  
        ...

        // GET: Posts/Edit/5
        public ActionResult Edit(int? id)
        {
            ...
        }       

        // POST: Posts/Edit/5
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Edit( Post post, int? CategoryIdLevel1, int? CategoryIdLevel2, int? originalCategoryId)
        {       
            ...
            Category cnew = db.Categories.Find(post.CategoryId);

            MoveFromCategory(post, originalCategoryId);
            ...

            db.Entry(post).State = EntityState.Modified;

            db.SaveChanges();

            return RedirectToAction("Index");
        }       

        //move post from his old category (fromCategoryId) to a new one (post.CategoryId):
        //returns true on success, false on failure.
        public bool MoveFromCategory(Post post, int? fromCategoryId)
        {
            try
            {

                if (post.CategoryId == fromCategoryId)
                    return true;

                Category cold = null, cnew = null;
                if (fromCategoryId!=null)                               
                    cold = db.Categories.Find(fromCategoryId);
                if (post.CategoryId != 0)                               
                    cnew = db.Categories.Find(post.CategoryId);

                if (cold != null)
                {
                    cold.Posts.Remove(post);
                }
                if( cnew != null)
                    cnew.Posts.Add(post);

                db.Entry(cold).State = EntityState.Modified;
                db.Entry(cnew).State = EntityState.Modified;
                //db.Entry(p).State = EntityState.Modified;
                //db.SaveChanges();

                return true;
            }
            catch (Exception)
            {
                return false;
                //throw;
            }
        }
    }
}

So, the idea is very default: The first method is called by Get and returns the View of Edit. Then I need to save the changes by sending the post object from the view to the HttpPost Edit method.

My Model is something like that (I simplified it for better understanding):

MrSaleBeta01.Models
{

    public class Post
    {
        public int Id { get; set; }

        [ForeignKey("Category")]
        public virtual int CategoryId { get; set; }               
        public virtual Category Category { get; set; }
    }


    public class Category
    {
        public Category()
        {
            this.Categories = new List<Category>();
            this.Posts = new List<Post>();
        }

        #region Primitive Properties
        public int CategoryId { get; set; }       
        public string Name { get; set; }
        #endregion

        #region Navigation Properties
        public virtual IList<Post> Posts { get; set; }
        #endregion
    }
}

The idea: Every Post needs to have it's Category. Every Category can have multiple Posts or none. (1-N relationship).

The problem:

In the Edit (HttpPost) method, after I update the Category's objects (move the Post from it's category to a different category object. After that I do some other modifications on post object), I get an error in the line of the edit method:

db.Entry(post).State = EntityState.Modified;

saying that:

{"Attaching an entity of type 'MrSaleBeta01.Models.Post' 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."}

The error is beacuse there is a conflict to the line:

cold.Posts.Remove(post);

And even to the line:

cnew.Posts.Add(post);

I tried to use the solution of AsNoTracking() but without success, I also tried to change the line "db.Entry(post).State = EntityState.Modified" line to:

db.As.Attach(post)

but that line is even cannot be compiled.

What am I doing wrong? How can I solve that issue?

ekad
  • 14,436
  • 26
  • 44
  • 46
Dudi
  • 3,069
  • 1
  • 27
  • 23
  • You might have a look at my answer on [ASP.NET MVC - Attaching an entity of type 'MODELNAME' failed because another entity of the same type already has the same primary key value](http://stackoverflow.com/questions/23201907/asp-net-mvc-attaching-an-entity-of-type-modelname-failed-because-another-ent/39557606#39557606). – Murat Yıldız Sep 18 '16 at 12:33

1 Answers1

0

1) You dont have to call .Attach() nor .State = anything.

You have your Entity created as proxy object (cold = db.Categories.Find(fromCategoryId);), its proxy responsibility to track any changes. As exception say, this COULD be your problem.

2) public int CategoryId { get; set; } should be marked with [Key] (i am not sure if convention mark it as primary key, but i doubt it - i think EF conventions take this PK as FK to Category, which could confuse object graph and behave strangely...)

3) Uh, just noticed... Why are you using your FromCategory method at all? I may overlook something, but looks like it just remove Category from collection and add it to another... EF proxy does this automatically for you, right after post.CategoryId = newCatId;

Edit1:

4) Change public virtual IList<Post> Posts { get; set; } to public virtual ICollection<Post> Posts { get; set; }

Edit2:

1) that was created automatically while I scaffold the PostsController according to the Post model. So I guess I need it?

3) It's not just remove Category from collection and add it to another, but remove the post from the collection of posts in one category to another. So I don't think that EF proxy does this automatically.

I am not famillier with ASP, i work with desktop MVP/MVVM, so i am not sure here - but from my point of view, you really dont need to touch EntityState as long as you are using var x = db.Set<X>().Create(); (== db.X.Create();) (NOT var x = new X();) for new entities and db.Set<X>().FetchMeWhatever(); (== db.X.FetchMeWhatever();) for everything else (Otherwise you get only POCO without proxy. From your example, it looks like you are doing it right ;) ).

Then you have entity with proxy (thats why you have your reference properties on model virtual - this new emitted proxy type override them) and this proxy will take care for 1:n, m:n, 1:1 relations for you. I think this is why folks are using mappers (not only EF and not only DB mappers) mainly :) For me it looks like, you are trying to do this manually and it is unnecessary and its just making a mess.

Proxy also take care of change tracking (so as i say, you dont need to set EntityState manually, only in extreme cases - I can not think of any right now... Even with concurrency.)

So my advice is:

  1. Use only ICollection<> for referencing collections
  2. Check and get rid of any var entity = new Entity(); (as i say, looks like you are doing this)
  3. Throw away every db.Entry(x).State = EntityState.whatever; (trust EF and his change tracker)
  4. Set only one side of reference - it doesnt matter if Category.Posts or Post.Category or even Post.CategoryId - and let mapper do the work. Please note that this will work only with proxy types (as i say above) on entities with virtual referencing & id & ICollection<> properties.

Btw, there are 2 types of change tracking, snippet and proxy - snippet have original values in RAM and is comparing them at SaveChanges() time, for proxy tracking, you need to have all your properties marked virtual - and comparing them at x.Prop = "x" time. But thats off-topic ;)

Jan 'splite' K.
  • 1,667
  • 2
  • 27
  • 34
  • 1) that was created automatically while I scaffold the PostsController according to the Post model. So I guess I need it? 2) I think CategoryId is PK in Category by default but I will change it. Thanks. 3) It's not just remove Category from collection and add it to another, but remove the post from the collection of posts in one category to another. So I don't think that EF proxy does this automatically. Am I right? Thanks a lot anyway.. – Dudi May 17 '15 at 15:21
  • Eddited answer to your comment in my answer ( :D sry my engrish...) **and added 4)** - this may be your problem, `IList<>` vs `ICollection<>` – Jan 'splite' K. May 17 '15 at 16:27
  • Thanks @Jan for your detailed answer. I tried to get ride of the "db.Entry(post).State = EntityState.Modified;", however it doesn't do the job for me because I have a line before saying: if ( MoveFromCategory(post, originalCategoryId) == false) { //on failure: save the old category: post.CategoryId = (int) originalCategoryId; } – Dudi May 17 '15 at 17:06
  • ... //db.Entry(post).State = EntityState.Modified; db.SaveChanges(); And the removing of the EntityState line cause it to not save the new post object in the db after doing db.saveChanges() line . (the post in the data base doesn't get post.CategoryId == the new category ID value). – Dudi May 17 '15 at 17:15
  • Hmmm... As i say, i dont know ASP -- look like i was misleading you. Your Post is really POCO, there is no way how `Edit` method caller could know from which context it can create instance for `Post post` parameter. Thats why nothing work for you... I found some basic tutorial about ASP and saw, they are using only `int? id` in their Edit mehotds + [TryUpdateModel](https://msdn.microsoft.com/en-us/library/dd470908(v=vs.108).aspx) method. Maybe you should look at it. – Jan 'splite' K. May 17 '15 at 22:50
  • And [tutorial](http://www.asp.net/mvc/overview/getting-started/getting-started-with-ef-using-mvc/updating-related-data-with-the-entity-framework-in-an-asp-net-mvc-application) – Jan 'splite' K. May 17 '15 at 22:51
  • 1
    Just wanted to know you that you was right! . I solved the problem by changing the IList to be ICollection. Then I followed the Relationship Convention tutorial in this article [link](https://msdn.microsoft.com/en-us/data/jj679962.aspx) to get 2 sides relationship between Category to Post. I used the navigation property as you told me to. It's works! thanks (-; – Dudi May 21 '15 at 17:04
  • I'm glad I could help :) (please, do NOT mark my answer as accepted, becouse it isnt the right answer. You should post your own answer and accept it) – Jan 'splite' K. May 22 '15 at 14:24