I've been pulling my hair out for about 2 days now, as I simply cannot get EF to save changes whenever I add a many-to-many entity to an existing entity.
My structure is plain simple:
I have a table called Person
, it has an ID (Primary, identity), and a few other string fields
A table called Keyword
with an ID (Primary, identity) and a string field called Value
and a PersonKeywordRelation
, with a PersonId
, and a KeywordId
field
When I have generated my entities (Database first), I get a class Person
, with an ICollection<Keyword>
- all good, works as expected.
The problem arises when I try to save an existing Person
, with a modified list of keywords. Only the scalar properties (strings) are saved, not my keywords!
I am pretty sure I'm not the only one who have had this problem, (in fact I am entirely sure as I have seen a couple questions on here already, on the same subject, yet I am unable to find a working answer...), mostly for older versions of EF, which is another good reason as to why I started yet another question: Has nothing changed that addresses this issue at all?
Here is my code that does the updating (and creation) of persons. You'll notice my attempt on making EF save changes accordingly.
public void SavePersons(IList<Person> persons)
{
// Create a EF Context
using (var ctx = new MyDbEntities())
{
foreach (var person in persons)
{
// Attach
ctx.Persons.Attach(person);
// Insert or update?
ctx.Entry(person).State = person.Id == 0 ? EntityState.Added : EntityState.Modified;
// Get current keywords before clearing from entity
var keywords = new List<Keyword>(person.Keywords);
// Clear keywords from entity, so we can add fresh ones, hopefully
// EF will have an easier time handling this..
person.Keywords.Clear();
// Add keywords
keywords.ForEach(kw =>
{
ctx.Keywords.Attach(kw);
ctx.Entry(kw).State = EntityState.Modified;
person.Keywords.Add(kw);
});
}
// Save
ctx.SaveChanges();
}
}
Finally.. Finally I can rest! I found the solution! It's not a pretty one, but it works!
Here's the code - sharing is caring.
This is definitely the last time I will work with Entity Framework. Causes more pain & agony than good.
public void SavePersons(IList<Person> persons)
{
// Create a EF Context
using (var ctx = new MyDbEntities())
{
// Iterate
foreach (var person in persons)
{
// Get current keywords
var keywords = new List<Keyword>(person.Keywords).ToList();
// Fetch Person from DB (if its not a NEW entry). Must use Include, else it's not working.
var newPerson = ctx.Persons
.Include("Keywords")
.FirstOrDefault(s => s.Id == person.Id) ?? person;
// Clear keywords of the object, else EF will INSERT them.. Silly.
newPerson.Keywords.Clear();
// Insert or update?
ctx.Entry(newPerson).State = newPerson.Id == 0 ? EntityState.Added : EntityState.Modified;
// Apply new scalar values
if(newPerson.Id != 0)
{
person.Id = newPerson.Id;
ctx.Entry(newPerson).CurrentValues.SetValues(person);
}
// Iterate through all keywords
foreach (var kw in ctx.Keywords)
{
// If the current kw exists in OUR list, add it
// - if not, remove the relation from the DB.
if (keywords.Any(k => k.Id == kw.Id))
{
//ctx.Entry(kw).State = EntityState.Unchanged;
ctx.Keywords.Attach(kw);
newPerson.Keywords.Add(kw);
}
else
newPerson.Keywords.Remove(kw);
}
}
// Save
ctx.SaveChanges();
}
}
Try adding .ToList():
var keywords = new List<Keyword>(person.Keywords).ToList();//generate list sepereate from .Keywords
I'm suspecting that your Keywords list is never populated because you clear it before hydrating it.
So the following is untested but, after you fix my bugs ;) it should hopefully do the trick. i don't know the rest of your code so i've opted for creating clones of the input data and attaching the objects to the context in a specific order.
EDIT: renamed method
// get unique list of Keywords from all Persons
private List<Keyword> getUniqueKeywords(IEnumerable<Person> oxygenThiefs) {
var result = new List<Keyword>();
foreach (var thief in oxygenThiefs) {
foreach (var keyword in thief.Keywords) {
if (!result.Contains(keyword)) {
result.Add(keyword);
}
}
}
return result;
}
// shallow clone of Person
private Person clonePerson(Person target) {
return new Person {
Id = target.Id,
Name = target.Name,
..
..
};
}
public void SavePersons(IList<Person> persons) {
// Create a EF Context
using (var ctx = new MyDbEntities()) {
// add all Keywords to the Context so that they are tracked
foreach (var keyword in getUniqueKeywords(persons)) {
ctx.Keywords.Attach(keyword);
// if value of Keyword has actually changed then uncomment line
// ctx.Entry(keyword).State = EntityState.Modified
}
foreach (var person in persons) {
// hehe
var shallowPerson = clonePerson(person);
// Attach Person
ctx.Persons.Attach(shallowPerson);
// Establish relationship (however shallow and meaningless)
foreach (var keyword in person.Keywords) {
shallowPerson.Keywords.Add(keyword);
}
// Insert or update?
ctx.Entry(shallowPerson).State = person.Id == 0 ? EntityState.Added : EntityState.Modified;
}
// Save
ctx.SaveChanges();
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With