EF: проверка не обновляется при использовании ленивых, обязательных свойств - PullRequest
69 голосов
/ 18 мая 2011

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

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 обязательно. .

Однако, если я принудительно загружу свойство Bar, добавив следующую строку перед SaveChanges:

var bar = foo.Bar;

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

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

Ответы [ 8 ]

54 голосов
/ 18 мая 2011

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

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

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

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

44 голосов
/ 26 мая 2011

Хорошо, вот реальный ответ =)

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

если у вас есть свойство (например, 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; }

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

7 голосов
/ 10 июня 2016

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

В вашем DbContext, переопределите ValidateEntity метод для удаления ошибки проверки для ссылок, которые не загружены.*

Плюсы:

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

Вот полу-приемлемый обходной путь :

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 голосов
/ 30 августа 2013

Если кому-то нужен общий подход для решения этой проблемы, здесь у вас есть пользовательский 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;
} 

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

2 голосов
/ 11 июня 2015

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

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

using (var context = new MyContext())
{
    var foo = context.Foos.Include("Bar").Find(id);
    foo.Data = 2;
    context.SaveChanges(); //Crash here
}
0 голосов
/ 19 марта 2015

Просто была такая же проблема в 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 было обнуляемым, вы должны написать:

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

    public int? BarId { get; set; }

    public virtual Bar Bar { get; set; }
}
0 голосов
/ 02 октября 2014

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...