0

I'm having some trouble with my EF Code First model when saving a relation to a many to many relationship. My Models:

public class Event
{
    public int Id { get; set; }
    public string Name { get; set; }  
    public virtual ICollection<Tag> Tags { get; set; }
}


public class Tag
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Event> Events { get; set; }
}

In my controller, I map one or many TagViewModels into type of Tag, and send it down to my servicelayer for persistence. At this time by inspecting the entities the Tag has both Id and Name (The Id is a hidden field, and the name is a textbox in my view)

The problem occurs when I now try to add the Tag to the Event. Let's take the following scenario:

The Event is already in my database, and let's say it already has the related tags C#, ASP.NET

If I now send the following list of tags to the servicelayer:

ID  Name
1   C#
2   ASP.NET
3   EF4

and add them by first fetching the Event from the DB, so that I have an actual Event from my DbContext, then I simply do

myEvent.Tags.Add

to add the tags.. Problem is that after SaveChanges() my DB now contains this set of tags:

ID  Name
1   C#
2   ASP.NET
3   EF4
4   C#
5   ASP.NET

This, even though my Tags that I save has it's ID set when I save it (although I didn't fetch it from the DB)

Yngve B-Nilsen
  • 9,606
  • 2
  • 36
  • 50
  • Read my answer here: http://stackoverflow.com/questions/3635071/update-relationships-when-saving-changes-of-ef4-poco-objects/3635326#3635326 The situation is still the same for code-first. – Ladislav Mrnka Mar 11 '11 at 09:48
  • 0 down vote I'm confused. Are you saying that you are trying to specify tags for a particular event, but those tags are being added into the TAGS table of the database rather than just creating new rows in the join table? – Julie Lerman Mar 11 '11 at 14:38
  • @Julie - That's exactly right. Since I'm using EF Code first, i don't access the EventTags table directly, but simply add stuff to the someEvent.Tags collection... – Yngve B-Nilsen Mar 11 '11 at 22:41

2 Answers2

2

I think I know what happend in your code. Let me explain my opinion in simple example:

using (var context = new Context())
{
    // Just let assume these are your tags received from view model.
    var csharp = ...;
    var aspnet = ...;
    var ef4 = ...;

    // Now you load Event
    var e = context.Events.Where(e => e.Id == someId);
    // Ups first access to Tag collection triggers lazy loading which
    // is enabled by default so, all current tags are loaded
    e.Tags.Add(csharp);
    e.Tags.Add(aspnet);
    e.Tags.Add(ef4);

    // Now e.Tags.Count == 5 !!! Why?
    context.SaveChanges();
}

The first problem: Because a dynamic proxy created on top of your Event instance uses HashSet for Tags it checks if added entity already exists in the collection. If not, it adds the entity othewise it skips the entity. To be able to do this check correctly YOU MUST IMPLEMENT Equals and GetHashCode in Tag! So because you didn't do it, it takes added tags as new ones with temporary keys and it adds them to Tags table with autogenerated key.

The second problem: Even if you implement Equals and GetHashCode you will solve only duplicity of C# and ASP.NET tags. At the moment the context doesn't track EF4 tag so this tag is still considered as a new one. You must inform the context that EF4 tag exists in DB. So lets Attach all tags to the context before you trigger lazy loading on Tags collection. Attaching entity to the context by default sets its state to Unchanged:

using (var context = new Context())
{
    foreach (var tag in TagsFromView)
    {
        context.Attach(tag);
    }

    // Now you load Event
    var e = context.Events.Where(e => e.Id == someId);
    foreach(var tag in TagsFromView)
    {
        // First access will trigger lazy loading but already
        // attached instances of tags are used
        e.Tags.Add(tag);
    }

    // Now you must delete all tags present in e.Tags and not
    // present in TagsFromView

    context.SaveChanges();
}

This works if you don't create new tags in your view. If you want to do it as well, you mustn't attach new tags to the context. You have to differ between existing tags and new tags (for example new tags can have Id = 0).

Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
  • Excellent answer.. I'll try this tomorrow, and let you know how it works :) – Yngve B-Nilsen Mar 12 '11 at 16:07
  • I accepted this answer because it works in terms of my question. Problem is that it doesn't really solve my problem. Point is that part of the problem isn't explained in my question. So I'll leave this as the accepted answer, and post a new question later :) – Yngve B-Nilsen Mar 14 '11 at 12:58
1

You need to get the tags from the db, otherwise EF will treat them as new items and override the id.

Shiraz Bhaiji
  • 64,065
  • 34
  • 143
  • 252
  • Well, that is actually what I'm wondering.. Are there no way of making EF work this out by itself? If it has an Id set it shouldn't insert a new entity? – Yngve B-Nilsen Mar 11 '11 at 08:16