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;

Все работает отлично. Это также работает, если я удаляю атрибут [Required].

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

Ответ 1

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

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

Ответ 2

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

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

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

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

Теперь, чтобы ответить на ваш вопрос, вы можете сделать Bar как Required, чтобы BarId свойство BarId как требуется, но не сам Bar:

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

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

Ответ 3

Прозрачное обходное решение для игнорирования ошибки при разгруженных ссылках

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

Плюсы:

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

Ответ 4

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

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

Ответ 5

Если кому-то нужен общий подход для решения этой проблемы, здесь у вас есть настраиваемый DbContext, который обнаруживает свойства на основе этих ограничений:

  • Lazy Load включен.
  • Свойства с 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;
} 

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

Ответ 6

Я знаю это немного поздно... Однако, плохой пост это здесь. Так как я тоже ужасно раздражался этим. Просто сообщите EF Include обязательное поле.

Обратите внимание на МАЛЕНЬКИЕ изменения

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

Ответ 7

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

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

  • У вас есть ленивая загрузка.

  • Вы хотите изменить и сохранить прокси-сервер без необходимости принудительно загружать ленивые ссылки.

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

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

var myEntity = new MyEntity(myOtherEntity);

MyEntity обладает этим свойством:

public virtual MyOtherEntity Other { get; protected set; }

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

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

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

Атрибут RequiredForAdd является настраиваемым атрибутом, который наследует от атрибута не обязательно атрибут. Он не имеет свойств или методов, кроме его базовых.

В моем классе 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;
}

Обратите внимание, что меня интересует только проверка на Add; если вы хотите также проверить во время Modify, вам нужно либо выполнить силовую нагрузку для свойства, либо использовать команду Sql для проверки значения внешнего ключа (не должно быть уже где-то в контексте)?

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

Ответ 8

Просто такая же проблема в 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; }

}

Как вы можете видеть, атрибут "Обязательный" не требуется, поскольку свойство Bar уже требуется, поскольку свойство BarId не равно NULL.

Итак, если вы хотите, чтобы свойство Bar было равно NULL, вам нужно было бы написать:

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

    public int? BarId { get; set; }

    public virtual Bar Bar { get; set; }
}

Ответ 9

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

using (var context = new MyContext())
{
    var foo = new Foo { Bar = null};  //changed Bar = new Bar() to Bar = null
    context.Foos.Add(foo);
    context.SaveChanges();
    id = foo.Id;
}