web-dev-qa-db-fra.com

EF: échec de validation lors de la mise à jour lors de l'utilisation de propriétés obligatoires chargées paresseusement

Compte tenu de ce modèle extrêmement 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; }
}

Le programme suivant échoue:

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
}

Avec un DbEntityValidationException. Le message trouvé dans EntityValidationErrors est le champ Bar est obligatoire..

Cependant, si je force le chargement de la propriété Bar en ajoutant la ligne suivante avant SaveChanges:

var bar = foo.Bar;

Tout fonctionne bien. Cela fonctionne également si je supprime le [Required] attribut.

Est-ce vraiment le comportement attendu? Existe-t-il des solutions de contournement (outre le chargement de chaque référence requise chaque fois que je souhaite mettre à jour une entité)

69
Diego Mijelshon

J'ai trouvé le post suivant qui avait une réponse au même problème:

La cause de ce problème est que dans RC et la validation RTM ne charge plus paresseusement aucune propriété. les obtiendrait un par un, ce qui pourrait entraîner de nombreuses transactions inattendues et des performances paralysantes.

La solution consiste à charger explicitement toutes les propriétés validées avant d'enregistrer ou de valider à l'aide de .Include (), vous pouvez en savoir plus sur la façon de procéder ici: http://blogs.msdn.com/b/adonet/archive /2011/01/31/using-dbcontext-in-ef-feature-ctp5-part-6-loading-related-entities.aspx

Je pense que c'est une implémentation de proxy assez merdique. Bien que parcourir inutilement le graphique de l'objet et récupérer les propriétés chargées paresseusement soit naturellement quelque chose à éviter (mais apparemment ignoré dans la première incarnation de EF par Microsoft), vous ne devriez pas avoir à aller sans procuration d'un wrapper pour valider son existence. À la réflexion, je ne sais pas pourquoi vous devez quand même parcourir le graphique des objets, le suivi des modifications de l'ORM sait sûrement quels objets nécessitent une validation.

Je ne sais pas pourquoi le problème existe, mais je suis sûr que je n'aurais pas ce problème si j'utilisais disons, NHibernate.

Ma "solution de contournement" - Ce que j'ai fait est de définir la nature requise de la relation dans une classe EntityTypeConfiguration et de supprimer l'attribut requis. Cela devrait le faire fonctionner correctement. Cela signifie que vous ne validerez pas la relation, mais la mise à jour échouera. Pas un résultat idéal.

54
Xhalent

Ok, voici la vraie réponse =)

D'abord une petite explication

si vous avez une propriété (comme votre Bar) notant un FK (ForeignKey), vous pouvez également avoir le champ FK correspondant dans votre modèle, donc si nous n'avons besoin que du FK et non du réel Bar nous n'en avons pas besoin pour accéder à la base de données:

[ForeignKey("BarId")]
public virtual Bar Bar { get; set; }
public int BarId { get; set; }

Maintenant, pour répondre à votre question, ce que vous pouvez faire pour que Bar soit Required est de marquer la propriété BarId comme requis, mais pas le Bar lui-même:

[ForeignKey("BarId")]
public virtual Bar Bar { get; set; }
[Required] //this makes the trick
public int BarId { get; set; }

cela fonctionne comme un charme =)

44
Diego Garber

Solution de contournement transparente pour ignorer l'erreur sur les références non chargées

Dans votre DbContext, remplacez la méthode ValidateEntity pour supprimer l'erreur de validation sur les références qui ne sont pas chargées.

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

Avantages :

  • Transparent et ne plantera pas lorsque vous utilisez l'héritage, les types complexes, ne nécessite pas de modification sur votre modèle ...
  • Uniquement en cas d'échec de la validation
  • Pas de réflexion
  • Itère uniquement sur les références non chargées non valides
  • Pas de chargement de données inutile
7
Guillaume

Voici un solution de contournement semi-acceptable :

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);
        }
    }
}
5
friism

Si quelqu'un veut une approche générale pour résoudre ce problème, vous avez ici un DbContext personnalisé qui découvre les propriétés en fonction de ces contraintes:

  • Lazy Load est activée.
  • Propriétés avec virtual
  • Propriétés ayant n'importe quel attribut ValidationAttribute.

Après avoir récupéré cette liste, sur tout SaveChanges dans lequel vous avez quelque chose à modifier, il chargera automatiquement toutes les références et collections en évitant toute exception inattendue.

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
}

IEntity<T> est:

public interface IEntity<T>
{
    T Id { get; set; }
}

Ces extensions ont été utilisées dans ce code:

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

J'espère que ça aide,

4
Gorka Lerchundi Osa

Je sais qu'il est un peu tard ... Cependant, je vais poster ça ici. Depuis que moi aussi je me suis terriblement ennuyé avec ça. Dites simplement à EF de Include le champ requis.

Notez le PETIT changement

using (var context = new MyContext())
{
    var foo = context.Foos.Include("Bar").Find(id);
    foo.Data = 2;
    context.SaveChanges(); //Crash here
}
2
Aiden Strydom

Juste eu le même problème dans EF 6.1.2. Pour résoudre ce problème, votre classe devrait ressembler à ceci:

public class Foo {
    public int Id { get; set; }
    public int Data { get; set; }

    public int BarId { get; set; }

    public virtual Bar Bar { get; set; }

}

Comme vous pouvez le voir, l'attribut "Obligatoire" n'est pas nécessaire, car la propriété Bar est déjà requise car la propriété BarId n'est pas annulable.

Donc, si vous vouliez que la propriété Bar soit nullable, vous devriez écrire:

public class Foo {
    public int Id { get; set; }
    public int Data { get; set; }

    public int? BarId { get; set; }

    public virtual Bar Bar { get; set; }
}
0
André Lourenço

Étant donné que c'est toujours un problème dans EF 6.1.1, j'ai pensé fournir une autre réponse qui pourrait convenir à certaines personnes, en fonction de leurs exigences de modèle exactes. Pour résumer le problème:

  1. Vous devez utiliser un proxy pour le chargement paresseux.

  2. La propriété que vous chargez paresseusement est marquée Obligatoire.

  3. Vous souhaitez modifier et enregistrer le proxy sans avoir à forcer le chargement des références paresseuses.

3 n'est pas possible avec les proxys EF actuels (l'un ou l'autre), ce qui est une grave lacune à mon avis.

Dans mon cas, la propriété paresseuse se comporte comme un type de valeur, sa valeur est donc fournie lorsque nous ajoutons l'entité et qu'elle n'est jamais modifiée. Je peux appliquer cela en protégeant son setter et en ne fournissant pas de méthode pour le mettre à jour, c'est-à-dire qu'il doit être créé via un constructeur, par exemple:

var myEntity = new MyEntity(myOtherEntity);

MyEntity a cette propriété:

public virtual MyOtherEntity Other { get; protected set; }

Donc EF n'effectuera pas de validation sur cette propriété mais je peux m'assurer qu'elle n'est pas nulle dans le constructeur. C'est un scénario.

En supposant que vous ne souhaitez pas utiliser le constructeur de cette manière, vous pouvez toujours garantir la validation à l'aide d'un attribut personnalisé, tel que:

[RequiredForAdd]
public virtual MyOtherEntity Other { get; set; }

L'attribut RequiredForAdd est un attribut personnalisé qui hérite de l'attribut pas RequiredAttribute. Il n'a pas de propriétés ou de méthodes autres que celles de base.

Dans ma classe DB Context, j'ai un constructeur statique qui trouve toutes les propriétés avec ces attributs:

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

Maintenant que nous avons une liste de propriétés que nous devons vérifier manuellement, nous pouvons remplacer la validation et les valider manuellement, en ajoutant des erreurs à la collection retournée par le validateur de 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;
}

Notez que je ne souhaite que valider pour un Add; si vous souhaitez également vérifier lors de la modification, vous devez soit effectuer le chargement forcé de la propriété, soit utiliser une commande SQL pour vérifier la valeur de la clé étrangère (cela ne devrait-il pas déjà être quelque part dans le contexte)?

Étant donné que l'attribut requis a été supprimé, EF créera un FK nullable; pour vous assurer de l'intégrité de la base de données, vous pouvez modifier les FK manuellement dans un script SQL que vous exécutez sur votre base de données après sa création. Cela va au moins attraper le Modify avec des problèmes nuls.

0
Rob Kent