EF: Validation failing on update when using lazy-loaded, required properties

后端 未结 8 2020
栀梦
栀梦 2020-12-04 15:03

Given this extremely simple model:

public class MyContext : BaseContext
{
    public DbSet Foos { get; set; }
    public DbSet Bars { g         


        
相关标签:
8条回答
  • 2020-12-04 15:56

    Here's a 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);
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-04 15:56

    If anyone wants a general approach to solve this problem, here you have a custom DbContext which finds out properties based on these constraints:

    • Lazy Load is ON.
    • Properties with virtual
    • Properties having any ValidationAttribute attribute.

    After retrieving this list, on any SaveChanges in which have something to modify it will load all references and collections automatically avoiding any unexpected 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
    }
    

    Where IEntity<T> is:

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

    These extensions were used in this 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;
    } 
    

    Hope it helps,

    0 讨论(0)
提交回复
热议问题