entity framework - errors - EF: Fallo de validación en la actualización cuando se utilizan propiedades requeridas cargadas perezosas
ex entityvalidationerrors (8)
Dado este modelo extremadamente simple:
public class MyContext : BaseContext
{
public DbSet<Foo> Foos { get; set; }
public DbSet<Bar> Bars { get; set; }
}
public class Foo
{
public int Id { get; set; }
public int Data { get; set; }
[Required]
public virtual Bar Bar { get; set; }
}
public class Bar
{
public int Id { get; set; }
}
El siguiente programa falla:
object id;
using (var context = new MyContext())
{
var foo = new Foo { Bar = new Bar() };
context.Foos.Add(foo);
context.SaveChanges();
id = foo.Id;
}
using (var context = new MyContext())
{
var foo = context.Foos.Find(id);
foo.Data = 2;
context.SaveChanges(); //Crash here
}
Con una DbEntityValidationException
. El mensaje que se encuentra en EntityValidationErrors
es El campo de la barra es obligatorio. .
Sin embargo, si SaveChanges
carga de la propiedad de Bar
agregando la siguiente línea antes de SaveChanges
:
var bar = foo.Bar;
Todo funciona bien Esto también funciona si elimino el atributo [Required]
.
¿Es este realmente el comportamiento esperado? ¿Hay alguna solución (además de cargar cada referencia requerida cada vez que deseo actualizar una entidad)
Aquí hay una solución temporal aceptable :
var errors = this.context.GetValidationErrors();
foreach (DbEntityValidationResult result in errors) {
Type baseType = result.Entry.Entity.GetType().BaseType;
foreach (PropertyInfo property in result.Entry.Entity.GetType().GetProperties()) {
if (baseType.GetProperty(property.Name).GetCustomAttributes(typeof(RequiredAttribute), true).Any()) {
property.GetValue(result.Entry.Entity, null);
}
}
}
Dado que esto todavía es un problema en EF 6.1.1, pensé que podría proporcionar otra respuesta que pueda adaptarse a algunas personas, dependiendo de los requisitos exactos de su modelo. Para resumir el problema:
Necesita usar un proxy para la carga diferida.
La propiedad de carga lenta está marcada como Requerido.
Desea modificar y guardar el proxy sin tener que forzar las referencias perezosas.
3 no es posible con los proxies actuales de EF (ninguno de ellos), lo cual es una falla seria en mi opinión.
En mi caso, la propiedad floja se comporta como un tipo de valor por lo que su valor se proporciona cuando agregamos la entidad y nunca cambiamos. Puedo aplicar esto haciendo que su setter esté protegido y no proporcione un método para actualizarlo, es decir, debe ser creado a través de un constructor, por ejemplo:
var myEntity = new MyEntity(myOtherEntity);
MyEntity tiene esta propiedad:
public virtual MyOtherEntity Other { get; protected set; }
Entonces, EF no realizará la validación en esta propiedad, pero puedo asegurarme de que no sea nula en el constructor. Ese es un escenario.
Suponiendo que no desea utilizar el constructor de esa manera, aún puede garantizar la validación utilizando un atributo personalizado, como por ejemplo:
[RequiredForAdd]
public virtual MyOtherEntity Other { get; set; }
El atributo RequiredForAdd es un atributo personalizado que hereda de Attribute no RequiredAttribute . No tiene propiedades o métodos aparte de sus bases.
En mi clase de contexto DB tengo un constructor estático que encuentra todas las propiedades con esos atributos:
private static readonly List<Tuple<Type, string>> validateOnAddList = new List<Tuple<Type, string>>();
static MyContext()
{
FindValidateOnAdd();
}
private static void FindValidateOnAdd()
{
validateOnAddList.Clear();
var modelType = typeof (MyEntity);
var typeList = modelType.Assembly.GetExportedTypes()
.Where(t => t.Namespace.NotNull().StartsWith(modelType.Namespace.NotNull()))
.Where(t => t.IsClass && !t.IsAbstract);
foreach (var type in typeList)
{
validateOnAddList.AddRange(type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(pi => pi.CanRead)
.Where(pi => !(pi.GetIndexParameters().Length > 0))
.Where(pi => pi.GetGetMethod().IsVirtual)
.Where(pi => pi.GetCustomAttributes().Any(attr => attr is RequiredForAddAttribute))
.Where(pi => pi.PropertyType.IsClass && pi.PropertyType != typeof (string))
.Select(pi => new Tuple<Type, string>(type, pi.Name)));
}
}
Ahora que tenemos una lista de propiedades que debemos verificar manualmente, podemos anular la validación y validarlas manualmente, agregando cualquier error a la colección devuelta desde el validador base:
protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
{
return CustomValidateEntity(entityEntry, items);
}
private DbEntityValidationResult CustomValidateEntity(DbEntityEntry entry, IDictionary<object, object> items)
{
var type = ObjectContext.GetObjectType(entry.Entity.GetType());
// Always use the default validator.
var result = base.ValidateEntity(entry, items);
// In our case, we only wanted to validate on Add and our known properties.
if (entry.State != EntityState.Added || !validateOnAddList.Any(t => t.Item1 == type))
return result;
var propertiesToCheck = validateOnAddList.Where(t => t.Item1 == type).Select(t => t.Item2);
foreach (var name in propertiesToCheck)
{
var realProperty = type.GetProperty(name);
var value = realProperty.GetValue(entry.Entity, null);
if (value == null)
{
logger.ErrorFormat("Custom validation for RequiredForAdd attribute validation exception. {0}.{1} is null", type.Name, name);
result.ValidationErrors.Add(new DbValidationError(name, string.Format("RequiredForAdd validation exception. {0}.{1} is required.", type.Name, name)));
}
}
return result;
}
Tenga en cuenta que solo estoy interesado en validar un Add; si también desea verificar durante la Modificación, deberá hacer la carga forzada de la propiedad o usar un comando Sql para verificar el valor de la clave externa (¿no debería estar ya en algún lugar del contexto)?
Debido a que el atributo Requerido se ha eliminado, EF creará un FK que se puede anular; Para garantizar la integridad de la base de datos, puede modificar los FK manualmente en un script Sql que ejecute en su base de datos después de que se haya creado. Esto al menos captará la modificación con problemas nulos.
Encontré la siguiente publicación que tenía una respuesta para el mismo problema:
La causa de este problema es que en la validación de RC y RTM ya no se aplican cargas perezosas a ninguna propiedad. La razón por la cual se realizó este cambio es porque al guardar muchas entidades a la vez que tienen la validación de propiedades cargadas perezosas las obtendría una por una, lo que podría ocasionar muchas transacciones inesperadas y un rendimiento paralizante.
La solución consiste en cargar explícitamente todas las propiedades validadas antes de guardarlas o validarlas mediante .Include (), puede leer más sobre cómo hacerlo aquí: http://blogs.msdn.com/b/adonet/archive/2011/01/31/using-dbcontext-in-ef-feature-ctp5-part-6-loading-related-entities.aspx
Mi opinión sobre esto es que es una implementación incorrecta de proxy. Aunque caminar innecesariamente el gráfico de objetos y recuperar propiedades cargadas de forma lenta es, naturalmente, algo que debe evitarse (pero aparentemente pasado por alto en la primera encarnación de EF de Microsoft), no debería tener que ir desprocesando un contenedor para validar que existe. Pensándolo bien, no estoy seguro de por qué tienes que ir caminando por el gráfico de objetos de todos modos, seguramente el rastreador de cambios del ORM sabe qué objetos requieren validación.
No estoy seguro de por qué existe el problema, pero estoy seguro de que no estaría teniendo este problema si estuviera usando decir, NHibernate.
Mi ''solución'' - Lo que he hecho es definir la naturaleza Requerido de la relación en una clase EntityTypeConfiguration, y eliminé el atributo Requerido. Esto debería hacer que funcione bien. Significa que no validará la relación, pero fallará la actualización. No es un resultado ideal.
Ok, aquí está la respuesta real =)
Primero una pequeña explicación:
si tiene una propiedad (como su barra) marcando un FK, también puede tener el campo FK correspondiente en su modelo, de modo que si solo necesitamos el FK y no la "barra" real, no lo necesitamos para ir a la base de datos :
[ForeignKey("BarId")]
public virtual Bar Bar { get; set; }
public int BarId { get; set; }
Ahora, para responder a su pregunta, lo que puede hacer para que la barra sea REQUERIDA es marcar la propiedad BarId según sea necesario, pero no la barra en sí misma:
[ForeignKey("BarId")]
public virtual Bar Bar { get; set; }
[Required] //this makes the trick
public int BarId { get; set; }
esto funciona como un encanto =)
Sé que es un poco tarde ... Sin embargo, publico esto aquí. Desde que yo también me enojé horriblemente con esto. Solo dile a EF que Include
el campo requerido.
Observe el PEQUEÑO cambio
using (var context = new MyContext())
{
var foo = context.Foos.Include("Bar").Find(id);
foo.Data = 2;
context.SaveChanges(); //Crash here
}
Si alguien quiere un enfoque general para resolver este problema, aquí tiene un DbContext personalizado que descubre propiedades basadas en estas restricciones:
- Lazy Lazy está activada.
- Propiedades con
virtual
- Propiedades que tienen cualquier atributo
ValidationAttribute
.
Después de recuperar esta lista, en cualquier SaveChanges
en el que tenga algo que modificar cargará todas las referencias y colecciones automáticamente, evitando cualquier excepción inesperada.
public abstract class ExtendedDbContext : DbContext
{
public ExtendedDbContext(string nameOrConnectionString)
: base(nameOrConnectionString)
{
}
public ExtendedDbContext(DbConnection existingConnection, bool contextOwnsConnection)
: base(existingConnection, contextOwnsConnection)
{
}
public ExtendedDbContext(ObjectContext objectContext, bool dbContextOwnsObjectContext)
: base(objectContext, dbContextOwnsObjectContext)
{
}
public ExtendedDbContext(string nameOrConnectionString, DbCompiledModel model)
: base(nameOrConnectionString, model)
{
}
public ExtendedDbContext(DbConnection existingConnection, DbCompiledModel model, bool contextOwnsConnection)
: base(existingConnection, model, contextOwnsConnection)
{
}
#region Validation + Lazy Loading Hack
/// <summary>
/// Enumerator which identifies lazy loading types.
/// </summary>
private enum LazyEnum
{
COLLECTION,
REFERENCE,
PROPERTY,
COMPLEX_PROPERTY
}
/// <summary>
/// Defines a lazy load property
/// </summary>
private class LazyProperty
{
public string Name { get; private set; }
public LazyEnum Type { get; private set; }
public LazyProperty(string name, LazyEnum type)
{
this.Name = name;
this.Type = type;
}
}
/// <summary>
/// Concurrenct dictinary which acts as a Cache.
/// </summary>
private ConcurrentDictionary<Type, IList<LazyProperty>> lazyPropertiesByType =
new ConcurrentDictionary<Type, IList<LazyProperty>>();
/// <summary>
/// Obtiene por la caché y si no lo tuviese lo calcula, cachea y obtiene.
/// </summary>
private IList<LazyProperty> GetLazyProperties(Type entityType)
{
return
lazyPropertiesByType.GetOrAdd(
entityType,
innerEntityType =>
{
if (this.Configuration.LazyLoadingEnabled == false)
return new List<LazyProperty>();
return
innerEntityType
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(pi => pi.CanRead)
.Where(pi => !(pi.GetIndexParameters().Length > 0))
.Where(pi => pi.GetGetMethod().IsVirtual)
.Where(pi => pi.GetCustomAttributes().Exists(attr => typeof(ValidationAttribute).IsAssignableFrom(attr.GetType())))
.Select(
pi =>
{
Type propertyType = pi.PropertyType;
if (propertyType.HasGenericInterface(typeof(ICollection<>)))
return new LazyProperty(pi.Name, LazyEnum.COLLECTION);
else if (propertyType.HasGenericInterface(typeof(IEntity<>)))
return new LazyProperty(pi.Name, LazyEnum.REFERENCE);
else
return new LazyProperty(pi.Name, LazyEnum.PROPERTY);
}
)
.ToList();
}
);
}
#endregion
#region DbContext
public override int SaveChanges()
{
// Get all Modified entities
var changedEntries =
this
.ChangeTracker
.Entries()
.Where(p => p.State == EntityState.Modified);
foreach (var entry in changedEntries)
{
foreach (LazyProperty lazyProperty in GetLazyProperties(ObjectContext.GetObjectType(entry.Entity.GetType())))
{
switch (lazyProperty.Type)
{
case LazyEnum.REFERENCE:
entry.Reference(lazyProperty.Name).Load();
break;
case LazyEnum.COLLECTION:
entry.Collection(lazyProperty.Name).Load();
break;
}
}
}
return base.SaveChanges();
}
#endregion
}
Donde IEntity<T>
es:
public interface IEntity<T>
{
T Id { get; set; }
}
Estas extensiones se usaron en este código:
public static bool HasGenericInterface(this Type input, Type genericType)
{
return
input
.GetInterfaces()
.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
}
public static bool Exists<T>(this IEnumerable<T> source, Predicate<T> predicate)
{
foreach (T item in source)
{
if (predicate(item))
return true;
}
return false;
}
Espero eso ayude,
Solo tuve el mismo problema en EF 6.1.2. Para resolver esto, tu clase debería ser como la siguiente:
public class Foo {
public int Id { get; set; }
public int Data { get; set; }
public int BarId { get; set; }
public virtual Bar Bar { get; set; }
}
Como puede ver, el atributo "Requerido" no es necesario, porque la propiedad Bar ya es necesaria ya que la propiedad BarId no puede contener nulos.
Entonces, si quisieras que la propiedad del Bar fuera nulable, tendrías que escribir:
public class Foo {
public int Id { get; set; }
public int Data { get; set; }
public int? BarId { get; set; }
public virtual Bar Bar { get; set; }
}
Solución transparente para ignorar el error en las referencias descargadas
En su DbContext
, anule el método ValidateEntity
para eliminar el error de validación en las referencias que no están cargadas.
private static bool IsReferenceAndNotLoaded(DbEntityEntry entry, string memberName)
{
var reference = entry.Member(memberName) as DbReferenceEntry;
return reference != null && !reference.IsLoaded;
}
protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry,
IDictionary<object, object> items)
{
var result = base.ValidateEntity(entityEntry, items);
if (result.IsValid || entityEntry.State != EntityState.Modified)
{
return result;
}
return new DbEntityValidationResult(entityEntry,
result.ValidationErrors
.Where(e => !IsReferenceAndNotLoaded(entityEntry, e.PropertyName)));
}
Pros:
- Transparente y no se bloqueará cuando use herencia, tipos complejos, no requiere modificación en su modelo ...
- Solo cuando la validación falla
- Sin reflejo
- Itera solo en referencias no cargadas no válidas
- Sin carga de datos inútiles