c# - inicializar - Actualice Entity desde ViewModel en MVC usando AutoMapper
imapper c# (3)
Tengo una Entidad Supplier.cs
y su ViewModel SupplierVm.cs
. Estoy intentando actualizar un proveedor existente, pero recibo la pantalla amarilla de la muerte (YSOD) con el mensaje de error:
La operación falló: no se pudo cambiar la relación porque una o más de las propiedades de clave externa no admiten nulos. Cuando se realiza un cambio en una relación, la propiedad de clave foránea relacionada se establece en un valor nulo. Si la clave externa no admite valores nulos, se debe definir una nueva relación, se debe asignar a la propiedad de clave externa otro valor no nulo o se debe eliminar el objeto no relacionado.
Creo que sé por qué está sucediendo, pero no estoy seguro de cómo solucionarlo . Aquí hay un screencast de lo que está sucediendo. Creo que la razón por la que recibo el error es porque esa relación se pierde cuando AutoMapper hace su trabajo .
CÓDIGO
Estas son las Entidades que creo que son relevantes:
public abstract class Business : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string TaxNumber { get; set; }
public string Description { get; set; }
public string Phone { get; set; }
public string Website { get; set; }
public string Email { get; set; }
public bool IsDeleted { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime? ModifiedOn { get; set; }
public virtual ICollection<Address> Addresses { get; set; } = new List<Address>();
public virtual ICollection<Contact> Contacts { get; set; } = new List<Contact>();
}
public class Supplier : Business
{
public virtual ICollection<PurchaseOrder> PurchaseOrders { get; set; }
}
public class Address : IEntity
{
public Address()
{
CreatedOn = DateTime.UtcNow;
}
public int Id { get; set; }
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
public string Area { get; set; }
public string City { get; set; }
public string County { get; set; }
public string PostCode { get; set; }
public string Country { get; set; }
public bool IsDeleted { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime? ModifiedOn { get; set; }
public int BusinessId { get; set; }
public virtual Business Business { get; set; }
}
public class Contact : IEntity
{
public Contact()
{
CreatedOn = DateTime.UtcNow;
}
public int Id { get; set; }
public string Title { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
public string Department { get; set; }
public bool IsDeleted { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime? ModifiedOn { get; set; }
public int BusinessId { get; set; }
public virtual Business Business { get; set; }
}
Y aquí está mi ViewModel :
public class SupplierVm
{
public SupplierVm()
{
Addresses = new List<AddressVm>();
Contacts = new List<ContactVm>();
PurchaseOrders = new List<PurchaseOrderVm>();
}
public int Id { get; set; }
[Required]
[Display(Name = "Company Name")]
public string Name { get; set; }
[Display(Name = "Tax Number")]
public string TaxNumber { get; set; }
public string Description { get; set; }
public string Phone { get; set; }
public string Website { get; set; }
public string Email { get; set; }
[Display(Name = "Status")]
public bool IsDeleted { get; set; }
public IList<AddressVm> Addresses { get; set; }
public IList<ContactVm> Contacts { get; set; }
public IList<PurchaseOrderVm> PurchaseOrders { get; set; }
public string ButtonText => Id != 0 ? "Update Supplier" : "Add Supplier";
}
La configuración de asignación de mi AutoMapper es así:
cfg.CreateMap<Supplier, SupplierVm>();
cfg.CreateMap<SupplierVm, Supplier>()
.ForMember(d => d.Addresses, o => o.UseDestinationValue())
.ForMember(d => d.Contacts, o => o.UseDestinationValue());
cfg.CreateMap<Contact, ContactVm>();
cfg.CreateMap<ContactVm, Contact>()
.Ignore(c => c.Business)
.Ignore(c => c.CreatedOn);
cfg.CreateMap<Address, AddressVm>();
cfg.CreateMap<AddressVm, Address>()
.Ignore(a => a.Business)
.Ignore(a => a.CreatedOn);
Finalmente, aquí está mi método de edición SupplierController :
[HttpPost]
public ActionResult Edit(SupplierVm supplier)
{
if (!ModelState.IsValid) return View(supplier);
_supplierService.UpdateSupplier(supplier);
return RedirectToAction("Index");
}
Y aquí está el Método UpdateSupplier
en SupplierService.cs
:
public void UpdateSupplier(SupplierVm supplier)
{
var updatedSupplier = _supplierRepository.Find(supplier.Id);
Mapper.Map(supplier, updatedSupplier); // I lose navigational property here
_supplierRepository.Update(updatedSupplier);
_supplierRepository.Save();
}
He leído mucho y, de acuerdo con esta publicación del blog , ¡lo que tengo debería funcionar! También he leído cosas como esta, pero pensé que consultaría con los lectores antes de abandonar AutoMapper para actualizar entidades.
He recibido este problema muchas veces y normalmente es esto:
El FK Id en la referencia padre no coincide con el PK en esa entidad FK. es decir, si tiene una tabla de pedidos y una tabla OrderStatus. Cuando carga ambos en entidades, Order tiene OrderStatusId = 1 y OrderStatus.Id = 1. Si cambia OrderStatusId = 2 pero no actualiza OrderStatus.Id a 2, obtendrá este error. Para solucionarlo, debe cargar el Id de 2 y actualizar la entidad de referencia o simplemente configurar la entidad de referencia OrderStatus en Orden para anular antes de guardar.
No estoy seguro si esto se ajustará a su requerimiento, pero sugeriría seguirlo.
Desde su código seguramente parece que está perdiendo relación durante el mapeo en alguna parte.
Para mí, parece que, como parte de la operación UpdateSupplier, no está actualizando ninguno de los detalles secundarios del proveedor.
Si ese es el caso, sugeriría actualizar solo las propiedades modificadas del SupplierVm al dominio Supplier class. Puede escribir un método separado en el que asignará valores de propiedad de SupplierVm al objeto Proveedor (Esto debería cambiar solo las propiedades que no son secundarias, como Nombre, Descripción, Sitio web, Teléfono, etc.).
Y luego realice db Update. Esto lo salvará de un posible desorden de las entidades rastreadas.
Si está cambiando las entidades secundarias del proveedor, le sugiero que las actualice independientemente de los proveedores porque recuperar un gráfico de objetos completo de la base de datos requeriría muchas consultas para ser ejecutadas y su actualización también ejecutará consultas de actualización innecesarias en la base de datos.
Actualizar las entidades de forma independiente ahorraría muchas operaciones de DB y se agregaría al rendimiento de la aplicación.
Todavía puede utilizar la recuperación de todo el gráfico de objetos si tiene que mostrar todos los detalles sobre el proveedor en una sola pantalla. Para las actualizaciones, no recomendaría la actualización de todo el gráfico de objetos.
Espero que esto ayude a resolver su problema.
La causa
La línea ...
Mapper.Map(supplier, updatedSupplier);
... hace mucho más de lo que parece.
- Durante la operación de mapeo,
updatedSupplier
carga sus colecciones (Addresses
, etc.) perezosamente porque AutoMapper (AM) accede a ellas. Puede verificar esto supervisando las declaraciones de SQL. - AM reemplaza estas colecciones cargadas por las colecciones que mapea desde el modelo de vista. Esto sucede a pesar de la configuración
UseDestinationValue
. (Personalmente, creo que esta configuración es incomprensible).
Este reemplazo tiene algunas consecuencias inesperadas:
- Deja los elementos originales en las colecciones adjuntas al contexto, pero ya no están dentro del alcance del método en el que se encuentra. Los elementos todavía están en las colecciones
Local
(comocontext.Addresses.Local
) pero ahora están privados de su padre, porque EF ha ejecutado correcciones de relaciones . Su estado estáModified
. - Anexa los elementos del modelo de vista al contexto en un estado
Added
. Después de todo, son nuevos en el contexto. Si en este punto esperaría 1Address
encontext.Addresses.Local
, vería 2. Pero solo verá los elementos agregados en el depurador.
Son estos elementos ''Modificados'' sin padres los que causan la excepción. Y si no fuera así, la próxima sorpresa habría sido agregar nuevos elementos a la base de datos mientras esperaba las actualizaciones.
OK, ¿ahora qué?
Entonces, ¿cómo se puede arreglar esto?
R : Intenté reproducir tu situación lo más cerca posible. Para mí, una posible solución consistía en dos modificaciones:
Desactiva la carga diferida. No sé cómo arreglarías esto con tus repositorios, pero en algún lugar debería haber una línea como
context.Configuration.LazyLoadingEnabled = false;
Al hacer esto, solo tendrá los elementos
Added
, no los elementosModified
ocultos.Marque los elementos
Added
comoModified
. Nuevamente, "en algún lugar", ponga líneas comoforeach (var addr in updatedSupplier.Addresses) { context.Entry(addr).State = System.Data.Entity.EntityState.Modified; }
... y así.
B. Otra opción es asignar el modelo de vista a nuevos objetos de entidad ...
var updatedSupplier = Mapper.Map<Supplier>(supplier);
... y márcalo, y todos sus hijos, como Modified
. Sin embargo, esto es bastante "caro" en términos de actualizaciones, ver el siguiente punto.
C. Una mejor solución en mi opinión es sacar AM de la ecuación por completo y pintar el estado manualmente. Siempre tengo cuidado con el uso de AM para escenarios complejos de mapeo. En primer lugar, porque el mapeo en sí está muy alejado del código donde se usa, lo que dificulta la inspección del código. Pero principalmente porque trae sus propias formas de hacer las cosas. No siempre está claro cómo interactúa con otras operaciones delicadas, como el seguimiento de cambios.
Pintar el estado es un procedimiento minucioso. La base podría ser una declaración como ...
context.Entry(updatedSupplier).CurrentValues.SetValues(supplier);
... que copia las propiedades escalares del supplier
a updatedSupplier
si sus nombres coinciden. O puede usar AM (después de todo) para asignar modelos de vistas individuales a sus contrapartes de entidad, pero ignorando las propiedades de navegación.
La opción C le da un control detallado sobre lo que se actualiza, como originalmente se pretendía, en lugar de la actualización exhaustiva de la opción B. En caso de duda, esto puede ayudarlo a decidir qué opción usar.