c# - Entity Framework Code First-¿Por qué no puedo actualizar las propiedades complejas de esta manera?
entity-framework ef-code-first (3)
Después de probar una docena de maneras diferentes de hacerlo a la manera de EF, llegué a la conclusión de que no hay una primera manera razonable de hacer el Código de EF para hacer lo que estoy tratando de hacer. Así que utilicé la reflexión. DbContext
este método para mi clase que hereda de 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);
}
}
Todos mis objetos heredan de una clase abstracta y tienen una propiedad de clave principal en común, por lo que este encuentra el objeto existente con la misma clave que la que se pasa, y luego actualiza el objeto existente del nuevo. SaveChanges
necesita ser llamado después.
Estoy trabajando en un pequeño proyecto de muestra utilizando Entity Framework 4.1 (primero el código). Mis clases se ven así:
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; }
}
He guardado un par de EmployeeTypes ("first", "second") en la base de datos, y he guardado una persona que tiene el primer tipo. Ahora quiero modificar la persona. Sé que puedo hacer esto cargando la Persona, cambiando las propiedades y luego guardando. Pero lo que quiero hacer en cambio, lo que me parece que debería funcionar, es lo siguiente:
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();
Curiosamente, esto cambia Nombre y Apellido, pero no EmployeeType. Si obtengo un Contexto nuevo y solicito a esta Persona, el Tipo de Empleado permanece establecido en "primero" como estaba antes de que se ejecutara este código.
¿Qué necesito hacer para actualizar las propiedades de navegación y no solo las propiedades escalares? (Esto es especialmente desconcertante porque para EmployeeType, lo único que realmente debe cambiar es la clave externa en la tabla de persona, y esa clave es una propiedad escalar).
(Por cierto, sé que puedo hacer esto recuperando la Persona primero, luego cambiando las propiedades una por una, pero como estoy usando el enlace de modelo en ASP.NET MVC, parece que de esta manera sería más fácil porque Tendré el objeto persona actualizado ya en mi método POST.)
Esto funciona para las colecciones, aunque creo que tiene que haber una mejor manera.
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();
Puedes probarlo de otra manera:
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();
Otro enfoque es exponer la clave externa en su entidad Person
como:
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; }
}
Esto cambiará el tipo de relación entre Person
y EmployeeType
de asociación independiente a asociación de clave extranjera. En lugar de asignar la propiedad de navegación, asigne la propiedad de clave externa. Esto te permitirá modificar la relación por tu código actual.
El problema es que las asociaciones independientes (aquellas que no usan propiedades de clave externa) se manejan como un objeto separado en el administrador del estado / rastreador de cambios. Por lo tanto, su modificación de la persona no afectó el estado de la relación existente ni estableció la nueva relación. Pregunté en MSDN cómo hacerlo con la API DbContext, pero solo es posible si DbContext
en ObjectContext
y usa ObjectStateManager
y ChangeRelationshipState
.