69 votes

EF: Validation échoue sur la mise à jour lors de l'utilisation de chargement paresseux, propriétés requises

Donnée très simple, ce modèle:

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 domaine de Bar est nécessaire..

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

var bar = foo.Bar;

Tout fonctionne très bien. Cela fonctionne aussi si je supprime l' [Required] d'attribut.

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

54voto

Xhalent Points 3038

J'ai trouvé le post suivant, qui a eu une réponse pour le même problème:

La cause de ce problème est que, dans RC et RTM, la validation n'est plus paresseux les charges de toutes les propriétés. La raison de ce le changement a été fait est que lors de l'enregistrement beaucoup d'entités à la fois qui ont paresseux propriétés chargées de la validation obtenir un par un, potentiellement causant beaucoup de l'inattendu les transactions et paralysant les performances.

La solution est de charger explicitement le tout validé propriétés avant de les enregistrer ou de la validation par l'aide .Include(), vous pouvez en lire plus sur comment faire ici: http://blogs.msdn.com/b/adonet/archive/2011/01/31/using-dbcontext-in-ef-feature-ctp5-part-6-loading-related-entities.aspx

De mon point de vue sur ce qu'est un assez merdique proxy de mise en œuvre. Alors que unnecesarily de marche de l'objet graphique et retriveing chargement paresseux propriétés est naturellement quelque chose à éviter (mais apparemment oubliée dans Microsoft première incarnation de l'EF), vous ne devriez pas avoir besoin d'aller de l'onu-le proxy d'un wrapper pour s'assurer qu'il existe. En seconde opinion, je ne suis pas sûr pourquoi vous avez besoin de marcher le graphe d'objets de toute façon, sûrement le système de suivi de changement de l'ORM sait ce que les objets nécessitant une validation.

Je ne sais pas pourquoi le problème existe, mais je suis sûr que je ne serais pas à avoir ce problème si j'ai été en utilisant le dire, NHibernate.

Ma "solution de contournement" - de Ce que j'ai fait est de définir la nature de la relation dans un EntityTypeConfiguration classe, et a supprimé l'attribut Obligatoire. Cela devrait bien fonctionner. Cela signifie que vous ne pourrez pas valider la relation, mais ce sera un échec de la mise à jour. Pas un résultat idéal.

44voto

Diego Garber Points 219

Ok, là est la vraie réponse =)

D'abord, une petite explication:

si vous avez une propriété (comme votre Bar) en notant une FK, vous pouvez également avoir le correspondant FK champ dans votre modèle, de sorte que si nous avons seulement besoin de la FK et pas de la "Barre" nous n'avons pas besoin d'aller à 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 rendre la Barre de tel que REQUIS est de marquer le BarId bien que nécessaire, mais pas la Barre elle-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 =)

5voto

friism Points 11330

Voici un semi-acceptable travail autour de:

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

4voto

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

  • Lazy Load est SUR.
  • Propriétés avec virtual
  • Propriétés avoir tout ValidationAttribute d'attribut.

Après la récupération de cette liste, sur n'importe quel SaveChanges qui ont quelque chose à modifier, il va charger toutes les références et les collections é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és 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;
} 

Espérons que cela aide,

0voto

Rob Kent Points 3133

Puisque c'est encore un problème en EF 6.1.1 j'ai pensé que je pourrais fournir une autre réponse qui peut convenir à certaines personnes, en fonction de leur modèle exact exigences. Pour résumer le problème:

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

  2. La propriété vous êtes paresseux chargement est marqué Nécessaire.

  3. Vous voulez modifier et enregistrer le proxy sans avoir à force de charger le paresseux références.

3 n'est pas possible avec le EF procurations (l'un d'eux), ce qui est une grave lacune à mon avis.

Dans mon cas, le paresseux propriété se comporte comme un type de valeur afin que sa valeur est fournie lorsque nous ajoutons de l'entité et n'a jamais changé. Je peux appliquer en sorte que ses setter protégés et de ne pas fournir une méthode de mise à jour, qui est, il doit être créé par le biais d'un constructeur, par exemple:

var myEntity = new MyEntity(myOtherEntity);

MyEntity a cette propriété:

public virtual MyOtherEntity Other { get; protected set; }

Donc EF ne sera pas effectuer une validation de cette propriété, mais je peux vous assurer qu'il n'est pas null dans le constructeur. C'est un scénario.

En supposant que vous ne souhaitez pas utiliser le constructeur de cette façon, vous pouvez toujours s'assurer de la validation à l'aide d'un attribut personnalisé, tels que:

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

Le RequiredForAdd attribut est un attribut personnalisé qui hérite de l'Attribut pas RequiredAttribute. Il n'a pas de propriétés ou méthodes en dehors de sa base.

Dans ma DB Contexte de la classe, 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 dont nous avons besoin pour vérifier manuellement, nous pouvons remplacer la validation et de les valider manuellement, l'ajout de toutes les erreurs de la collection retournée à partir de la base du programme de validation:

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 m'intéresse qu'à valider pour un complément; si vous vouliez vérifier au cours de la Modifier, vous devez soit faire la force de la charge de la propriété ou de l'utilisation d'une commande Sql pour vérifier la valeur de clé étrangère (ne pourrait-il pas déjà quelque part dans le contexte)?

Parce que l'attribut Obligatoire a été supprimé, EF créer un nullable FK; pour vous assurer d'intégrité DB vous pourriez modifier la FKs manuellement dans un script Sql que vous exécutez sur votre base de données après qu'il a été créé. Cela permettra au moins de rattraper le Modifier avec null questions.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X