EF: ошибка проверки при обновлении при использовании лениво загруженных обязательных свойств



учитывая эту чрезвычайно простую модель:



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:



var bar = foo.Bar;


все работает нормально. Это также работает, если я удалить .



это действительно ожидаемое поведение? Существуют ли какие-либо обходные пути (помимо загрузки каждой необходимой ссылки каждый раз, когда я хочу обновить объект)

645   8  

8 ответов:

нашел следующий пост это был ответ на такую же проблему:

причина этой проблемы заключается в том, что в Проверка RC и RTM больше не ленивая загружает любые свойства. Причина этого изменение было сделано потому, что при сохранении много лиц одновременно ленивая проверка загруженных свойств получил бы их один за другим потенциально вызывая много неожиданного сделки и калечение спектакль.

в обходной путь заключается в явной загрузке все проверенные свойства перед сохранением или проверка с помощью .Включать вас можно подробнее, как это сделать здесь: http://blogs.msdn.com/b/adonet/archive/2011/01/31/using-dbcontext-in-ef-feature-ctp5-part-6-loading-related-entities.aspx

мой взгляд на это - это довольно дерьмовая реализация прокси. Хотя излишне ходить по графу объектов и восстанавливать лениво загруженные свойства, естественно, что-то должно быть избегаемый (но, по-видимому, упущенный из виду в первом воплощении EF от Microsoft), вам не нужно идти без проксирования оболочки, чтобы проверить, что она существует. Во-вторых, я не уверен, почему вам нужно идти по графу объектов в любом случае, конечно, трекер изменений ORM знает, какие объекты требуют проверки.

Я не уверен, почему проблема существует, но я уверен, что у меня не было бы этой проблемы, если бы я использовал, скажем, NHibernate.

мой 'обходной путь' - что Я сделал это определить требуемый характер отношений в классе EntityTypeConfiguration и удалил необходимый атрибут. Это должно заставить его работать нормально. Это означает, что вы не будете проверять связь, но она не сможет выполнить обновление. Не идеальный результат.

ОК, Вот реальный ответ =)

сначала небольшое объяснение

если у вас есть собственность (например,Bar) отмечая FK (ForeignKey), вы также можете иметь соответствующее поле FK в своей модели, поэтому, если нам нужен только FK, а не фактический Bar нам не нужно, чтобы перейти к базе данных:

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

теперь, чтобы ответить на ваш вопрос, что вы можете сделать, чтобы Bar как Required - это флаг BarId свойство по мере необходимости, но только не :

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

это работает как шарм =)

прозрачный обходной путь, чтобы игнорировать ошибку на выгрузке ссылок

в своем 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)));
    }

плюсы :

  • прозрачный и не рухнет, когда вы используете наследование, сложные типы, не требует модификации на вашей модели...
  • только при сбое проверки
  • нет отражение
  • итерации только на недопустимых выгруженных ссылках
  • нет бесполезной загрузки данных

здесь полу-приемлемая работа-вокруг:

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, который обнаруживает свойства на основе этих ограничений:

  • ленивая нагрузка включена.
  • свойства virtual
  • свойства, имеющие какие-либо .

после получения этого списка, на любую 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;
} 

надеюсь, это поможет,

Я знаю, что уже немного поздно... Тем не менее, я опубликую это здесь. Так как я тоже был ужасно раздражен этим. Просто скажи эф 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, я подумал, что дам еще один ответ, который может подойти некоторым людям, в зависимости от их точных требований к модели. Подводя итог вопросу:

  1. вам нужно использовать прокси для ленивой загрузки.

  2. свойство, которое вы лениво загружаете, помечено как обязательное.

  3. вы хотите изменить и сохранить прокси без принудительной загрузки ленивых ссылки на литературу.

3 Невозможно с текущими прокси EF (любой из них), что является серьезным недостатком, на мой взгляд.

в моем случае свойство lazy ведет себя как тип значения, поэтому его значение предоставляется при добавлении сущности и никогда не изменяется. Я могу обеспечить это, сделав его сеттер защищенным и не предоставляя метод для его обновления, то есть он должен быть создан с помощью конструктора, например:

var myEntity = new MyEntity(myOtherEntity);

MyEntity это свойство:

public virtual MyOtherEntity Other { get; protected set; }

поэтому EF не будет выполнять проверку этого свойства, но я могу гарантировать, что он не является нулевым в конструкторе. Это один из сценариев.

если вы не хотите использовать конструктор таким образом, вы все еще можете обеспечить проверку с помощью настраиваемого атрибута, например:

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

атрибут RequiredForAdd является пользовательским атрибутом, который наследуется от атрибута не 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 для проверки значения внешнего ключа (разве это не должно быть где-то в контексте)?

поскольку необходимый атрибут был удален, EF создаст nullable FK; чтобы обеспечить целостность БД, вы можете вручную изменить FKs в сценарии Sql, который вы запускаете для своей базы данных после ее создания. Это, по крайней мере, поймает изменение с нулевыми проблемами.

просто была такая же проблема в 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" не нужен, потому что свойство Bar уже требуется, поскольку свойство BarId не может быть обнулено.

поэтому, если вы хотите, чтобы свойство Bar было nullable, вам нужно будет написать:

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

    public int? BarId { get; set; }

    public virtual Bar Bar { get; set; }
}

Comments

    Ничего не найдено.