Принудительное ядро Entity Framework для перезагрузки некоторых полей после вызова SaveChanges

#c# #entity-framework #entity-framework-core

#c# #entity-framework #entity-framework-core

Вопрос:

У меня есть проект, использующий Entity Framework Core для вставки / обновления записей в таблице в Postgres, и у меня есть случаи, когда числовая точность некоторых значений в C # выше, чем числовая точность Postgres.

Например, у меня может быть число номер 0.99878 в памяти для свойства типа double в C #, но когда я сохраняю это значение, оно может быть сохранено в Posgres as 0.9988 вместо этого, и это совершенно нормально и ожидаемо.

Entity Framework не знает о том, что сделал Postgres при сохранении этого числа, и все кажется прекрасным, но на самом деле число, которое у меня есть в памяти, неверно, если я вручную не укажу EF обновить объект.

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

Как я могу сообщить Entity Framework Core автоматически обновлять некоторые поля после SaveChanges ?

Я попытался установить .ValueGeneratedOnAddOrUpdate , но это игнорирует значение, которое у меня есть в памяти.

Комментарии:

1. ИМХО, используйте десятичный тип, проверяйте / округляйте до той же точности перед сохранением.

2. Спасибо @JeremyLakeman это обходной путь, но у меня есть более одного поля с одинаковой характеристикой, поэтому проверять каждое поле сложнее, чем просить EF снова перезагрузить всю сущность — я просто надеюсь, что есть способ избежать даже этого, и заставить EF перезагрузить поле автоматически

3. @TheGeneral Это не сильно отличается от наличия автоматически сгенерированных полей (из базы данных) или наличия триггера базы данных, который переопределяет значение в зависимости от некоторого условия. Вопрос в том, как мы можем сообщить EF, что сохраненное поле могло измениться после вставки или обновления, чтобы оно могло перезагрузить его автоматически — точно так же, как это делается для полей идентификации.

4. На самом деле вы правы, я отменяю свой комментарий.

Ответ №1:

Молчаливая потеря точности, безусловно, является проблемой. Для моего собственного варианта использования, связанного с отслеживанием валюты, я обнаружил, что такое поведение неприемлемо. Чтобы свести повторение кода к минимуму, я ввел атрибут проверки, принудительно установил его для каждого десятичного числа в модели, использовал его для определения типа столбца в базе данных и повторной проверки каждого измененного объекта перед сохранением.

     [AttributeUsage(AttributeTargets.Property)]
    public class PrecisionAttribute : ValidationAttribute
    {
        public PrecisionAttribute(int precision, int scale)
        {
            Precision = precision;
            Scale = scale;
        }

        public int Precision { get; set; }
        public int Scale { get; set; }
        public override bool IsValid(object value)
        {
            if (value is decimal val)
            {
                if (val == 0)
                    return true;
                var sqlDec = new SqlDecimal(val);
                try
                {
                    return (sqlDec == SqlDecimal.ConvertToPrecScale(sqlDec, Precision, Scale)).Value;
                }
                catch (Exception)
                {
                    return false;
                }
            }
            var dec = value as decimal?;
            return !dec.HasValue || IsValid(dec.Value);
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (IsValid(value))
                return ValidationResult.Success;
            return new ValidationResult($"'{value}' is not a valid {validationContext.DisplayName}", new string[] { validationContext.MemberName });
        }
        
        public void Apply(IMutableProperty property) => property.SetColumnType($"numeric({Precision},{Scale})");
    }

    // ... DbContext ...
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
        //...
            foreach (var table in modelBuilder.Model.GetEntityTypes())
            {
                foreach (var col in table.GetProperties())
                {
                    var precision = col.PropertyInfo?.GetCustomAttribute<PrecisionAttribute>() ?? col.FieldInfo?.GetCustomAttribute<PrecisionAttribute>();
                    if (precision != null)
                        precision.Apply(col);
                    else if(col.ClrType == typeof(decimal) || col.ClrType == typeof(decimal?))
                        throw new Exception($"Decimal column {table.ClrType.Name}.{col.Name} doesn't have a precision!");
                }
            }
        }

        public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
        {
            PreSave();
            return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
        }

        public override int SaveChanges(bool accept) {
            PreSave();
            return base.SaveChanges(accept);
        }

        private void PreSave()
        {
            if (!ChangeTracker.HasChanges())
                return;

            var serviceProvider = this.GetService<IServiceProvider>();
            var items = new Dictionary<object, object>();
            foreach (var entry in ChangeTracker.Entries())
            {
                switch (entry.State)
                {
                    case EntityState.Added:
                    case EntityState.Modified:
                        var context = new ValidationContext(entry.Entity, serviceProvider, items);
                        Validator.ValidateObject(entry.Entity, context, true);
                        break;
                }
            }
        }
  

Комментарии:

1. Спасибо @Jeremy Lakeman. Это, безусловно, интересный подход. В моем случае точность не важна, и у меня слишком много полей, о которых нужно беспокоиться, и они имеют разную точность… Я забочусь только о том, чтобы отображать в пользовательском интерфейсе то, что хранится в базе данных, поэтому все, что я хочу, это автоматическое обновление некоторых полей, чтобы я не возвращал неверные данные в пользовательский интерфейс после сохранения. ps: Я поддержал ваш ответ, но, похоже, у меня пока недостаточно репутации, чтобы это учитывалось:(.

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

3. Это неплохая идея, просто в моем сценарии у меня нет базы данных, и я не хочу знать, какую точность они используют в каждом поле (а затем должны правильно округлять в моем приложении) и постоянно синхронизировать ее с базой данных всякий раз, когдавладельцы базы данных изменяют точность полей — это большая нагрузка

Ответ №2:

Я спросил в репозитории Entity Framework с открытым исходным кодом, и эта функция не существует (по крайней мере, сегодня с текущей версией .NET 5) и была добавлена в список невыполненных работ для рассмотрения.

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

https://github.com/dotnet/efcore/issues/23196#issuecomment-723494947