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

时间:2011-05-18 00:06:16

标签: entity-framework ef-code-first

鉴于这个非常简单的模型:

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
}

DbEntityValidationExceptionEntityValidationErrors中的消息是条形字段是必需的。

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

var bar = foo.Bar;

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

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

8 个答案:

答案 0 :(得分:54)

我发现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

我对此的看法是,这是一个非常糟糕的代理实现。虽然不必要地走对象图并重新启动延迟加载的属性自然是有待避免的(但在微软的EF的第一个版本中显然被忽略了),你不必需要去代理包装器来验证它是否存在。再想一想,我不知道为什么你还需要走对象图,当然ORM的变化跟踪器知道哪些对象需要验证。

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

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

答案 1 :(得分:44)

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

首先做一点解释

如果你有一个属性(比如你的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; }

这就像魅力=)

答案 2 :(得分:7)

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

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 :(得分:5)

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

答案 4 :(得分:4)

如果有人想要一般方法来解决这个问题,那么你有一个自定义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<T>的位置:

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

希望它有所帮助,

答案 5 :(得分:2)

我知道有点晚了...但是,这里发帖不好。因为我也非常恼火。只需告诉EF Include必填字段。

请注意 SMALL 更改

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

答案 6 :(得分:0)

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

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

  2. 您延迟加载的属性标记为必需。

  3. 您希望修改并保存代理,而无需强制加载延迟引用。

  4. 使用当前的EF代理(其中任何一个)都无法使用

    3,这在我看来是一个严重的缺点。

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

    var myEntity = new MyEntity(myOtherEntity);
    

    MyEntity有这个属性:

    public virtual MyOtherEntity Other { get; protected set; }
    

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

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

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

    RequiredForAdd属性是继承自Attribute 而非RequiredAttribute 的自定义属性。除了基础之外,它没有任何属性或方法。

    在我的数据库上下文类中,我有一个静态构造函数,它可以找到具有这些属性的所有属性:

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

    请注意,我只对验证添加感兴趣;如果你想在修改期间检查,你需要为属性强制加载或使用Sql命令检查外键值(不应该已经在上下文中的某个地方)?

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

答案 7 :(得分:0)

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

}

正如您所看到的,&#34;必需&#34;不需要属性,因为Bar属性已经是必需的,因为BarId属性不可为空。

因此,如果您希望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; }
}