EF:使用延迟加载的必需属性时更新验证失败

Posted

技术标签:

【中文标题】EF:使用延迟加载的必需属性时更新验证失败【英文标题】:EF: Validation failing on update when using lazy-loaded, required properties 【发布时间】:2011-08-27 16:02:47 【问题描述】:

鉴于这个极其简单的模型:

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; 

以下程序失败:

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

使用DbEntityValidationException。在EntityValidationErrors 中找到的消息是Bar 字段是必需的。

但是,如果我通过在SaveChanges 之前添加以下行来强制加载Bar 属性:

var bar = foo.Bar;

一切正常。如果我删除 [Required] 属性,这也有效。

这真的是预期的行为吗?是否有任何解决方法(除了每次我想更新实体时加载每个所需的引用)

【问题讨论】:

我昨天刚刚碰到这个问题,所以我可以确认你的观察。我正在寻找解决方法。这似乎很不幸。 这不仅仅是导航属性的问题。我已经在 MSDN 上抱怨过:social.msdn.microsoft.com/Forums/en-US/adodotnetentityframework/… 说实话,我认为 EF 代理由于所有这些围绕可空性的问题而被破坏和危险。请参阅此处的问题:entityframework.codeplex.com/workitem/1571 还有一个问题是无法将未加载的引用设置为 null(因为它已经为 null/未加载)。基本上,代理在 EF 中不起作用,即使更改跟踪也表现出相同的行为。这种情况令人震惊,每个人都必须编写解决方法来解决基本的日常情况。 【参考方案1】:

好的,这是真正的答案 =)

先稍微解释一下

如果您有一个属性(如您的 Bar)注明 FK (ForeignKey),您也可以在模型中拥有相应的 FK 字段,因此如果我们只需要 FK 而不是实际的 Bar 我们不需要它去数据库:

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

现在,为了回答您的问题,您可以做些什么来使 Bar 成为 Required 是根据需要标记 BarId 属性,而不是 Bar 本身:

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

这就像一个魅力 =)

【讨论】:

不错的答案(赞成)。我的 FK 的名称与属性相同,所以我必须使用 [Required, Column("Bar"), ForeignKey("Bar")] public int? BarId get; set; ,这很难看,因为我实际上是在破解我的域模型以满足 EF 的怪异。 这样做的问题是,当创建一个新的 Foo() 时,您需要设置 Bar 和 BarId 属性,如果您只设置 Bar 属性,那么您将无法通过 BarId 所需的验证。此外,BarId 需要为 null 才能使所需的属性起作用。 这对我有用。我认为 BarId 应该可以为空,以反映 Bar 尚未设置,此外我认为 [Required] 在标量属性上毫无意义。 @Xhalent,您可以在 Bar 属性中设置 BarId。 感谢您的回答!我不需要 [Required] 属性,但我的模型中没有 ForeignKey (Id) - 现在它就像一个魅力! (我使用的是 EF5) 但是如果你删除Foo,它不会级联删除到Bar。当您从上下文和 SaveChanges 中删除 Foo 时,Bar 在删除之前设置为 null,然后您会收到此错误:“遇到无效数据。缺少必需的关系。检查 StateEntries 以确定违反约束的来源。”但是 StateEntries 中没有任何内容可以指示问题。【参考方案2】:

透明的解决方法以忽略卸载引用上的错误

在您的 DbContext 中,覆盖 ValidateEntity 方法以消除未加载引用的验证错误。

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

优点:

透明并且在您使用继承时不会崩溃,复杂类型,不需要对您的模型进行修改... 仅当验证失败时 无反射 仅对无效的卸载引用进行迭代 没有无用的数据加载

【讨论】:

在我看来这是解决这个问题的最佳方案。简单,避免了往返数据库的开销。 这确实是一个很棒的解决方案,应该标记为答案。【参考方案3】:

我知道这有点晚了......但是,我把这个贴在这里。因为我也对此非常恼火。只需将必填字段告诉 EF 至 Include

注意变化

using (var context = new MyContext())

    var foo = context.Foos.Include("Bar").Find(id);
    foo.Data = 2;
    context.SaveChanges(); //Crash here

【讨论】:

这对我有用,而其他人没有。添加简单,易于理解。【参考方案4】:

在 EF 6.1.2 中遇到了同样的问题。要解决这个问题,您的课程应该如下所示:

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

    public int BarId  get; set; 

    public virtual Bar Bar  get; set; 


如您所见,不需要“Required”属性,因为 BarId 属性不可为空,因此 Bar 属性已经是必需的。

因此,如果您希望 Bar 属性可以为空,则必须编写:

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

    public int? BarId  get; set; 

    public virtual Bar Bar  get; set; 

【讨论】:

【参考方案5】:

由于这在 EF 6.1.1 中仍然是一个问题,我想我会提供另一个可能适合某些人的答案,具体取决于他们的确切模型要求。总结一下这个问题:

    您需要使用代理进行延迟加载。

    你延迟加载的属性标记为必填。

    您想要修改和保存代理,而不必强制加载惰性引用。

当前的 EF 代理(其中任何一个)都无法实现 3,这在我看来是一个严重的缺陷。

在我的例子中,惰性属性的行为类似于值类型,因此它的值是在我们添加实体时提供的,并且从不更改。我可以通过使其 setter 受保护而不提供更新它的方法来强制执行此操作,也就是说,它必须通过构造函数创建,例如:

var myEntity = new MyEntity(myOtherEntity);

MyEntity 有这个属性:

public virtual MyOtherEntity Other  get; protected set; 

所以 EF 不会对此属性执行验证,但我可以确保它在构造函数中不为空。这是一种情况。

假设你不想以那种方式使用构造函数,你仍然可以使用自定义属性来确保验证,例如:

[RequiredForAdd]
public virtual MyOtherEntity Other  get; set; 

RequiredForAdd 属性是继承自notRequiredAttribute 属性的自定义属性。除了基本属性或方法之外,它没有任何属性或方法。

在我的 DB Context 类中,我有一个静态构造函数,它可以找到所有具有这些属性的属性:

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

现在我们有了一个需要手动检查的属性列表,我们可以覆盖验证并手动验证它们,将任何错误添加到从基本验证器返回的集合中:

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;

请注意,我只对验证添加感兴趣;如果您还想在 Modify 期间进行检查,则需要对属性执行强制加载或使用 Sql 命令来检查外键值(不应该已经在上下文中的某处)?

由于Required 属性已被移除,EF 将创建一个可为空的FK;为了确保您的数据库完整性,您可以在创建数据库后在针对数据库运行的 Sql 脚本中手动更改 FK。这至少会捕获 Modify with null 问题。

【讨论】:

【参考方案6】:

如果有人想要解决这个问题的通用方法,这里有一个自定义 DbContext,它可以根据这些约束找出属性:

延迟加载已开启。 virtual 的属性 具有任何ValidationAttribute 属性的属性。

检索此列表后,在任何需要修改的 SaveChanges 上,它将自动加载所有引用和集合,以避免任何意外异常。

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&lt;T&gt; 在哪里:

public interface IEntity<T>

    T Id  get; set; 

在这段代码中使用了这些扩展:

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;
 

希望对你有帮助,

【讨论】:

【参考方案7】:

这是semi-acceptable work-around:

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

【讨论】:

是的,这或多或少是我这些天在做的事情。我什至创建了 OSS project 和 Nuget package 并以此为特色。 此代码是否适用于继承?我有三个继承级别,我得到一个空 ref,我认为这是因为 property.Name 不属于基本类型。 @RobKent 我当然也想知道,因为我遇到了和你完全相同的问题。有谁知道吗?【参考方案8】:

我发现following post 对同样的问题有答案:

这个问题的原因是在 RC 和 RTM 验证不再懒惰 加载任何属性。这个原因 进行更改是因为保存时 很多实体同时拥有 延迟加载属性验证 可能会一一获得 造成很多意外 交易和瘫痪 性能。

解决方法是显式加载 保存前所有经过验证的属性 或使用 .Include() 进行验证,您 可以在此处阅读有关如何执行此操作的更多信息: http://blogs.msdn.com/b/adonet/archive/2011/01/31/using-dbcontext-in-ef-feature-ctp5-part-6-loading-related-entities.aspx

我认为这是一个非常糟糕的代理实现。虽然不必要地遍历对象图和检索延迟加载的属性自然是要避免的(但显然在 Microsoft 的第一个 EF 化身中被忽略了),但您不必取消代理包装器来验证它是否存在。再想一想,我不确定你为什么需要遍历对象图,当然 ORM 的更改跟踪器知道哪些对象需要验证。

我不确定为什么会出现这个问题,但我确信如果我使用 NHibernate 就不会遇到这个问题。

我的“解决方法” - 我所做的是在 EntityTypeConfiguration 类中定义关系的必需性质,并删除了必需属性。这应该使它工作正常。这意味着您不会验证关系,但更新会失败。不是一个理想的结果。

【讨论】:

我最终写了一个通用的LoadAllReferences 方法。我对 EF 感到非常失望。 感谢您的回答。这是我很长时间以来见过的最愚蠢的错误。怎么会有人认为这对于 ORM 来说是可以接受的? 我对学习这个感到失望。难道不是通过删除虚拟来使所有必需的导航属性非惰性的另一种解决方法吗? @CarlG。如果您将所有引用设为非惰性引用,那么您最终会从数据库中检索未确定数量的对象,其中任何数量的对象实际上都需要用于特定的工作单元。这就是延迟加载可用的原因。 是的,框架肯定知道 Bar 字段没有更改,因此不需要检查。 EF是个笑话。我希望我没有选择它,现在切换为时已晚,但我再也不会使用它了。

以上是关于EF:使用延迟加载的必需属性时更新验证失败的主要内容,如果未能解决你的问题,请参考以下文章

EF 延迟加载

EF6基础系列(九)---预先加载延迟加载显示加载

EF的延迟加载LazyLoad

EF延迟加载

延迟加载实体,正在加载引用但实体属性不是

实体框架急切加载不返回数据,延迟加载有