EF:使用延迟加载的必需属性时,validation失败
鉴于这个非常简单的模型:
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
find的消息是“栏”字段是必需的。 。
但是,如果我通过在SaveChanges
之前添加以下行强制加载Bar
属性:
var bar = foo.Bar;
一切工作正常。 这也适用于如果我删除[Required]
属性。
这真的是预期的行为? 是否有任何解决方法(除了每次我要更新实体时加载每个必需的引用)
我发现下面的post有同样的问题的答案:
造成这个问题的原因是在RC和RTMvalidation中不再延迟加载任何属性。 之所以做出这样的改变,是因为当一次保存大量实体时,会有一些懒惰的加载属性validation会逐一得到它们,这可能会导致很多意外的事务并导致性能下降。
解决方法是在使用.Include()保存或validation之前显式加载所有validation的属性,您可以在此处阅读有关如何执行此操作的更多信息: http : //blogs.msdn.com/b/adonet/archive/2011/01 /31/using-dbcontext-in-ef-feature-ctp5-part-6-loading-related-entities.aspx
我认为这是一个相当糟糕的代理实现。 尽pipe不必要地走对象图和回收延迟加载的属性当然是需要避免的(但在Microsoft的第一个EF版本中显然被忽略了),您不需要去代理包装来validation它是否存在。 第二个想法,我不知道为什么你需要走对象图,无疑,ORM的变化跟踪器知道什么对象需要validation。
我不知道为什么问题存在,但我敢肯定,我不会有这个问题,如果我使用说,NHibernate的。
我的“解决方法” – 我所做的是在EntityTypeConfiguration类中定义关系的Required性质,并删除了Required属性。 这应该使它工作正常。 这意味着你不会validation这种关系,但是它会失败。 不是一个理想的结果。
好的,这里是真正的答案=)
首先一点解释:
如果你有一个属性(比如你的Bar)注意到FK,你也可以在你的模型中有相应的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; }
这就像一个魅力=)
这是一个半可接受的解决方法 :
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); } } }
透明解决方法可以忽略卸载引用上的错误
在您的DbContext
,重写ValidateEntity
方法以删除未加载的引用上的validation错误。
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))); }
优点:
- 透明 ,不会崩溃当您使用inheritance,复杂的types,不需要修改您的模型…
- 只有当validation失败
- 没有反思
- 仅对无效的未加载的引用进行迭代
- 没有无用的数据加载
如果任何人想要一个通用的方法来解决这个问题,在这里你有一个自定义的DbContext基于这些约束找出属性:
- 延迟加载处于打开状态。
- 属性与
virtual
- 具有任何
ValidationAttribute
属性的属性。
检索完这个列表之后,任何有修改的SaveChanges
会自动加载所有的引用和集合,以避免任何意外的exception。
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; }
希望能帮助到你,
我知道这有点晚了,但是,生病发布在这里。 因为我也对此感到非常恼火。 只要告诉EF Include
必填字段。
注意小变化
using (var context = new MyContext()) { var foo = context.Foos.Include("Bar").Find(id); foo.Data = 2; context.SaveChanges(); //Crash here }
由于这在EF 6.1.1中仍然是个问题,我想我会提供另一个可能适合某些人的答案,这取决于他们的确切的模型要求。 总结这个问题:
-
您需要使用代理进行延迟加载。
-
您是懒加载属性被标记为必需。
-
您想修改并保存代理,而不必强制加载惰性引用。
3是不可能与当前的EF代理(他们中的任何一个),这在我看来是一个严重的缺点。
在我的例子中,lazy属性的行为类似于一个值types,所以当我们添加实体并且永远不会改变时,它的值就会被提供。 我可以通过设置setter来保护它,而不是提供一个方法来更新它,也就是说,它必须通过一个构造函数来创build,例如:
var myEntity = new MyEntity(myOtherEntity);
MyEntity有这个属性:
public virtual MyOtherEntity Other { get; protected set; }
所以EF不会对这个属性执行validation,但是我可以确保它在构造函数中不为null。 这是一种情况。
假设您不想以这种方式使用构造函数,仍然可以使用自定义属性来确保validation,例如:
[RequiredForAdd] public virtual MyOtherEntity Other { get; set; }
RequiredForAdd属性是一个从属性不是RequiredAttributeinheritance的自定义属性。 除了基本的东西,它没有任何特性或方法。
在我的数据库上下文类中,我有一个静态构造函数,它可以find所有具有这些属性的属性:
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))); } }
现在我们有一个需要手动检查的属性列表,我们可以重写validation并手动validation它们,并将任何错误添加到从基础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; }
请注意,我只对validation一个添加感兴趣; 如果你想在Modify期间检查,你需要对属性进行强制加载,或者使用Sql命令来检查外键值(不应该已经在上下文中的某处)?
由于Required属性已被删除,EF将创build一个可为空的FK; 为了确保数据库的完整性,您可以在一个Sql脚本中手动修改FK,这个Scripting脚本是在您创build数据库之后运行的。 这将至less赶上修改空问题。
在EF 6.1.2中也有同样的问题。 要解决这个问题,你的class级应该像下面这样:
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”属性是不需要的,因为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; } }