17

I am using Entity Framework 6.x using the Code First approach on an MVC 5 application. In this particular situation my model (among other things) contains two properties named Latitude and Longitude:

[Required, Range(-90, +90)]
public decimal Latitude { get; set; }

[Required, Range(-180, +180)]
public decimal Longitude { get; set; }

And when I performed the migration I got something like this

CreateTable("ResProperty"), c => new {
        :
    Latitude = c.Decimal(nullable: false, precision: 10, scale: 8),
    Longitude = c.Decimal(nullable: false, precision: 11, scale: 8),
        :
})
... other stuff

so both latitude and longitude have 8 decimal digits. The former with 2 whole numbers (max 90) and the latter with 3 whole numbers (max 180).

After performing the Update-Database command my table's columns are shown as:

Latitude decimal(10,8)
Longitude decimal(11,8)

that seems good to me. Now In my view I have a map and Javascript code that allows the user to reposition the marker. That works fine too. When the marker is repositioned the Latitude and Longitude fields are populated with the updated value which (Javascript) has more than 12 decimal digits. That does not matter AFAIK because my scale is 8 decimals.

After the submit button is pressed and either the Create or Edit POST method is invoked I examine the model instance and I confirmed that the actual values passed in the model to the controller are correct, they have more than enough decimal digits (those that Javascript code place). So the value is correct.

Now... the problem being that after db.SaveChanges() is performed the database gets updated -and I have confirmed that an actual write/update has taken place- but somehow internally the EF disregards my actual values and writes truncated latitude/longitude rounded to ONLY TWO decimal digits, so my Latitude shows in the DB as 09.500000000 all other decimal digits are zeroed because a rounding seems to have taken place.

// Prior to SaveChanges()
Latitude = 9.08521879
Longitude = -79.51658792
// After SaveChanges()
Latitude = 9.08000000
Longitude = -79.51000000

Why is it rounding it if I have given the correct scale and precision and the column has the correct scale and precision as well? why is SaveChanges altering my values?

I found this post (http://weiding331.blogspot.com/2014/01/entity-framework-decimal-value.html) which is the same issue but I don't know how I can fix that (if it does) because I have already performed several migrations and data additions after the table in question was "migrated".

Summarizing

  • The model data type is correct (decimal)
  • The database migration code has the correct precion/scale (lat 10/8 lon 11/8)
  • The SQL database columns have the correct precision/scale (lat 10/8, long 11/8)
  • The values passed in the model have at least 8 decimal digits for both latitude and longitude
  • the actual writing/updating of the value takes place in the database without error, but...
  • The values recorded on the database for these two columns are truncated to TWO decimal digits and show the other least significant decimal digits as zero (0)
Lord of Scripts
  • 3,579
  • 5
  • 41
  • 62
  • Did you specify scale and precision in your model? Take a look at this so question http://stackoverflow.com/questions/3504660/decimal-precision-and-scale-in-ef-code-first? On a different note EF does support spatial data types... – Pawel Oct 16 '14 at 00:46
  • I don't think I could use OnModelCreating anymore because that particular migration was several levels down in my migration list. I added it post mortem and ran Update-Database (without any new migration) but didn't see it solving the problem. Besides the SQL columns on the DB did show the correct precision but EF was truncating it internally during SaveChanges(). – Lord of Scripts Oct 17 '14 at 16:44

2 Answers2

21

EF has a special property for SqlProviderServices (implementation for the SqlClient provider for SQL Server) - TruncateDecimalsToScale. The default value is true so maybe you can change it to false value. For example:

public class DbContextConfiguration : DbConfiguration
    {
        public DbContextConfiguration()
        {
            var now = SqlProviderServices.Instance;
            SqlProviderServices.TruncateDecimalsToScale = false;
            this.SetProviderServices(SqlProviderServices.ProviderInvariantName, SqlProviderServices.Instance);
        }
    }

    [DbConfigurationType(typeof(DbContextConfiguration))]
    public class MyContext : DbContext
    { ... }

More info about that: https://msdn.microsoft.com/en-us/library/system.data.entity.sqlserver.sqlproviderservices.truncatedecimalstoscale%28v=vs.113%29.aspx

  • 1
    This looks like exactly what I am looking for but could you explain how the value is being set here? There is a variable "now" that doesn't seem to be used and the property seems to be set on a static method. Is this the best way of setting this value? I am surprised that such an important feature doesn't have more documentation around it - truncation of decimals is surely a massive issue. Thanks. – Mark007 Oct 08 '16 at 16:41
  • @Mark007 it's a global configuration. Of course also it's not a good place to setup these things but you know it's EF ;) – Adrian Tarnowski Mar 21 '17 at 11:42
  • It worked and solved the issue but I think I need to ILSpy things and see exactly what is happening at some point. – Mark007 Mar 23 '17 at 13:10
  • 3
    Is this considered a bug? I can't see how it isn't...after all the scaling is correct yet it is still truncating inappropriately? – Ross Brasseaux Nov 16 '17 at 23:03
1

Storing spatial data I would recommend DbGeography class that is made for that type of data.

https://learn.microsoft.com/en-us/dotnet/api/system.data.entity.spatial.dbgeography?view=entity-framework-6.2.0


As already mentioned the truncate issue can be solved with SqlProviderServices.TruncateDecimalsToScale = false; as @AdrianTarnowski pointed out. I would however like to show why this is happening and why Entity Framework 6.X truncates decimal values instead of rounding by default.

To test I'm using a basic program like this:

class Program
{
    static void Main(string[] args)
    {
        var dbContext = new ApplicationDbContext();
        dbContext.TestValues.Add(new TestValue()
        {
            Value = 0.0005m
        });
        dbContext.TestValues.Add(new TestValue()
        {
            Value = 0.0001m
        });
        dbContext.TestValues.Add(new TestValue()
        {
            Value = 0.0007m
        });
        dbContext.SaveChanges();
    }
}

public class TestValue
{
    public int Id { get; set; }

    public decimal Value { get; set; }
}

public class DbContextConfiguration : DbConfiguration
{
    public DbContextConfiguration()
    {
        var providerInstance = SqlProviderServices.Instance;
        SqlProviderServices.TruncateDecimalsToScale = true;
        this.SetProviderServices(SqlProviderServices.ProviderInvariantName, SqlProviderServices.Instance);
    }
}

[DbConfigurationType(typeof(DbContextConfiguration))]
public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext() : base("ApplicationContext")
    {
        Database.Log = s => Debug.WriteLine(s);
    }

    public DbSet<TestValue> TestValues { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<TestValue>().Property(x => x.Value).HasPrecision(18, 3);

        base.OnModelCreating(modelBuilder);
    }
}

Default it looks like this: SqlProviderServices.TruncateDecimalsToScale = true;. This is to prevent breaking existing applications that depend on this behavior.

https://learn.microsoft.com/en-us/dotnet/api/system.data.entity.sqlserver.sqlproviderservices.truncatedecimalstoscale?redirectedfrom=MSDN&view=entity-framework-6.2.0#overloads

When TruncateDecimalsToScale is normal (TruncateDecimalsToScale = true;) an insert from entity framework looks like this in the Database.Log from DbContext:

INSERT [dbo].[TestValues]([Value])
VALUES (@0)
SELECT [Id]
FROM [dbo].[TestValues]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()


-- @0: '0,0005' (Type = Decimal, Precision = 18, Scale = 3)

However looking at SQL Server Profiler the actual data that is sent is 0 for every value from above.

exec sp_executesql N'INSERT [dbo].[TestValues]([Value])
VALUES (@0)
SELECT [Id]
FROM [dbo].[TestValues]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()',N'@0 decimal(18,3)',@0=0

Changing to SqlProviderServices.TruncateDecimalsToScale = false; the Database.Log from DbContext looks like this instead:

INSERT [dbo].[TestValues]([Value])
VALUES (@0)
SELECT [Id]
FROM [dbo].[TestValues]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()


-- @0: '0,0005' (Type = Decimal)

Now SQL Server Profiler looks better and have correct values:

exec sp_executesql N'INSERT [dbo].[TestValues]([Value])
VALUES (@0)
SELECT [Id]
FROM [dbo].[TestValues]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()',N'@0 decimal(4,4)',@0=5

Note that EntityFrameworkCore is not affected by this. Here rounding is default.

POC:

class Program
{
    static void Main(string[] args)
    {
        using (var dbContext = new ApplicationDbContext())
        {
            dbContext.TestValues.Add(new TestValue()
            {
                Value = 0.0005m
            });
            dbContext.TestValues.Add(new TestValue()
            {
                Value = 0.0001m
            });
            dbContext.TestValues.Add(new TestValue()
            {
                Value = 0.0007m
            });
            dbContext.SaveChanges();
        }
    }
}

public class TestValue
{
    public int Id { get; set; }

    public decimal Value { get; set; }
}

public class ApplicationDbContext : DbContext
{
    public DbSet<TestValue> TestValues { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer("data source=localhost;initial catalog=;persist security info=True;User Id=;Password=;", providerOptions => providerOptions.CommandTimeout(60));
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<TestValue>().Property(x => x.Value).HasColumnType("decimal(18, 3)");

        base.OnModelCreating(modelBuilder);
    }
}
Ogglas
  • 62,132
  • 37
  • 328
  • 418