Прежде всего, почему я не могу обновить сложные свойства?

Я работаю над небольшим образцовым проектом, используя Entity Framework 4.1 (сначала код). Мои классы выглядят так:

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; }
}

Я сохранил пару EmployeeTypes ( "первый", "второй" ) в базе данных, и я сохранил Person, у которого есть первый тип. Теперь я хочу изменить Личность. Я знаю, что могу сделать это, загрузив Person, изменив свойства, а затем сохраните. Но то, что я хочу сделать вместо этого, что мне кажется, что это должно сработать, заключается в следующем:

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();

Как ни странно, это изменяет имя FirstName и LastName, но не EmployeeType. Если я получаю новый Контекст и запрашиваю этого Лица, EmployeeType остается установленным как "первым", как это было до того, как этот код запустился.

Что мне нужно сделать, чтобы обновить свойства навигации, а не только скалярные свойства? (Это особенно проблематично, потому что для EmployeeType единственное, что действительно нужно изменить, это внешний ключ в таблице Person, и этот ключ является скалярным свойством.)

(Кстати, я знаю, что могу сделать это, сначала извлекая Person, а затем меняя свойства один за другим, но поскольку я использую привязку к модели в ASP.NET MVC, похоже, этот способ был бы проще, потому что у меня будет обновленный объект человека уже в методе POST.)

Ответ 1

Вы можете попробовать по-другому:

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();

Другой подход заключается в том, чтобы выставлять внешний ключ в вашей организации Person, например:

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; }
}

Это изменит тип отношения между Person и EmployeeType от независимой ассоциации к ассоциации внешних ключей. Вместо назначения свойства навигации назначьте свойство внешнего ключа. Это позволит вам изменить отношение по вашему текущему коду.

Проблема в том, что независимые ассоциации (те, которые не используют свойство внешнего ключа) обрабатываются как отдельный объект в диспетчере состояний/изменении трекера. Таким образом, ваша модификация человека не повлияла на состояние существующего отношения и не установила новое отношение. Я спросил у MSDN, как это сделать с DbContext API, но это возможно, только если вы отбрасываете DbContext в ObjectContext и используете ObjectStateManager и ChangeRelationshipState.

Ответ 2

Попробовав дюжину различных способов сделать это EF-способом, я пришел к выводу, что нет разумного кода EF-кода. Первый способ сделать то, что я пытаюсь сделать. Поэтому я использовал отражение. Я создал этот метод для моего класса, который наследует от 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);
    }
}

Все мои объекты наследуются от абстрактного класса и имеют общее свойство первичного ключа, поэтому он находит существующий объект с тем же ключом, что и тот, который был передан, а затем обновляет существующий объект из нового. SaveChanges необходимо вызвать впоследствии.

Ответ 3

Это работает для коллекций, хотя я чувствую, что должен быть лучший способ.


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();