简体   繁体   中英

Force Entity Framework Core to reload some fields after SaveChanges is called

I have a project using Entity Framework Core to insert/update records in a table in Postgres and I have cases where the numeric precision of some values in C# is higher than the numeric precision of Postgres.

For example, I might have the number the number 0.99878 in memory on a property of type double in C#, but when I save this value it might be stored in Posgres as 0.9988 instead, and that's totally fine and expected.

Entity Framework doesn't know about what Postgres did when storing this number, and everything seems fine, but in reality the number I have in-memory is incorrect unless I manually tell EF to refresh the entity.

I can't increase the precision in the database , but I would like to avoid having to manually refresh entities after every SaveChanges

How can I tell Entity Framework Core to automatically refresh some fields after SaveChanges ?

I tried setting .ValueGeneratedOnAddOrUpdate but that ignores the value that I have in memory.

Silently losing precision is certainly a problem. For my own use case, involving tracking currency, I found that behaviour to be unacceptable. To keep code repetition to a minimum I introduced a validation attribute, forced every decimal in the model to have it, used it to define the column type in the database, and re-validate every modified object before saving.

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

I asked in the Entity Framework opensource repository and this feature does not exist (at least today with the current .NET 5 release) and was added to the backlog for consideration.

Support store-generated values that are both written to and also read back from the database in the same command

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM