Entity Framework Code First - Why can't I update complex properties this way?
I'm working on a small sample project using Entity Framework 4.1 (code first). My classes look like this:
public class Context : DbContext
{
public IDbSet<Person> People { get; set; }
public IDbSet<EmployeeType> EmployeeTypes { get; set; }
}
public class Person
{
[Key]
public int Key { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
virtual public EmployeeType EmployeeType { get; set; }
}
public class EmployeeType
{
[Key]
public int Key { get; set; }
public string Text { get; set; }
virtual public ICollection<Person> People { get; set; }
}
I've saved a couple EmployeeTypes ("first", "second") to the database, and I've saved a Person who has the first type. Now I want to modify the Person. I know I can do this by loading the Person, changing properties, and then saving. But what I want to do instead, which seems to me like it ought to work, is this:
var c = new Context();
var e = c.EmployeeTypes.Single(x => x.Text.Equals("second"));
var 开发者_开发百科p = new Person {
Key = originalKey, // same key
FirstName = "NewFirst", // new first name
LastName = "NewLast", // new last name
EmployeeType = e }; // new employee type
c.Entry(p).State = EntityState.Modified;
c.SaveChanges();
Oddly, this changes FirstName and LastName but not EmployeeType. If I get a new Context and request this Person, the EmployeeType remains set to "first" as it was before this code ran.
What do I need to do to get the navigation properties to update, and not just the scalar properties? (This is especially puzzling because for EmployeeType, the only thing that actually needs to change is the foreign key in the Person table, and that key is a scalar property.)
(By the way, I know I can do this by retrieving the Person first, then changing properties one-by-one, but as I'm using model binding in ASP.NET MVC, it seems like this way would be easier because I'll have the updated person object already in my POST method.)
You can try it different way:
var c = new Context();
var e = c.EmployeeTypes.Single(x => x.Text.Equals("second"));
var p = new Person {
Key = originalKey, // same key
FirstName = "NewFirst", // new first name
LastName = "NewLast"}; // new last name
c.People.Attach(p); // Attach person first so that changes are tracked
c.Entry(p).Reference(e => e.EmployeeType).Load();
p.EmployeeType = e; // Now context should know about the change
c.Entry(p).State = EntityState.Modified;
c.SaveChanges();
Other approach is exposing foreign key in your Person
entity like:
public class Person
{
[Key]
public int Key { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
[ForeignKey("EmployeeType")]
public int EmployeeTypeKey { get; set; }
public virtual EmployeeType EmployeeType { get; set; }
}
This will change the type of relation between Person
and EmployeeType
from Independent association to Foreign key association. Instead of assigning the navigation property assign the foreign key property. This will allow you to modify relation by your current code.
Problem is that independent associations (those don't using foreign key property) are handled as separate object in state manager / change tracker. So your modification of the person didn't affect state of the existing relation neither set the new relation. I asked on MSDN how to do it with DbContext API but it is possible only if you cast DbContext
to ObjectContext
and use ObjectStateManager
and ChangeRelationshipState
.
After trying a dozen different ways to do it the EF way, I concluded that there isn't a reasonable EF Code First way to do what I'm trying to do. So I used reflection. I created this method for my class that inherits from DbContext
:
public void UpdateFrom<T>(T updatedItem) where T : KeyedItem
{
var originalItem = Set<T>().Find(updatedItem.Key);
var props = updatedItem.GetType().GetProperties(
BindingFlags.Public | BindingFlags.Instance);
foreach (var prop in props)
{
var value = prop.GetValue(updatedItem, null);
prop.SetValue(originalItem, value, null);
}
}
All my objects inherit from an abstract class and have a primary key property in common, so this finds the existing object with the same key as the one passed in, and then updates the existing object's from the new one. SaveChanges
needs to be called afterwards.
This works for collections, although i feel like there's got to be a better way.
var properties = typeof(TEntity).GetProperties();
foreach (var property in properties)
{
if (property.GetCustomAttributes(typeof(OneToManyAttribute), false).Length > 0)
{
dynamic collection = db.Entry(e).Collection(property.Name).CurrentValue;
foreach (var item in collection)
{
if(item.GetType().IsSubclassOf(typeof(Entity)))
{
if (item.Id == 0)
{
db.Entry(item).State = EntityState.Added;
}
else
{
db.Entry(item).State = EntityState.Modified;
}
}
}
}
}
db.Entry(e).State = EntityState.Modified;
db.SaveChanges();
精彩评论