soomon
soomon

Reputation: 427

Decimal number in MSSQL table gets rounded although precision is set

My c# object has a decimal property:

public decimal LastPrice { get; set; }

While processing my object, the decimal value gets set. For example:

LastPrice = 0.091354;

I modified my DbContext to increase the decimal precision as explained in another stackoverflow post:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{       
        foreach (var property in modelBuilder.Model.GetEntityTypes()
            .SelectMany(t => t.GetProperties())
            .Where(p => p.ClrType == typeof(decimal) || p.ClrType == typeof(decimal?)))
        {
            property.SetColumnType("decimal(38, 10)");
        }        
}

The tables design view in Microsoft Sql Server Management Studio reflects this configuration. Here is the table scripted from SSMS:

CREATE TABLE [dbo].[TradedCurrencyPairs](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [LastPrice] [decimal](38, 10) NOT NULL,
...

When I check the object during debugging as it gets added to my DbContext, it looks good: When I check the object during debugging as it gets added to my DbContext, it looks good

But in the database it ends up as being 0.0000000000.

As far as I can tell, the value is still being rounded as if it would have a precision of 2. A value that should be 0.09232 becomes 0.0900000000. So all decimals still get cut.

I have tried several different Data Annotations:

// [PrecisionAndScale(20, 10)]
//[RegularExpression(@"^\d+\.\d{20,10}$")]
//[Range(0, 99999999999999999999.9999999999)]
[Range(typeof(decimal), "20", "10")]

but they didn't help. Inserting data from SSMS works fine: INSERT INTO TradedCurrencyPairs VALUES ('tzt', 'ttt', 'rrrr', '20120618 10:34:09 AM' , 'hgghghg', 0.123456, 1, 0.123456789, 0.123456, 0.123456); go

My DbModelSnapshot for the column looks like this:

            b.Property<decimal>("LastPrice")
                .HasPrecision(10)
                .HasColumnType("decimal(20, 10)");

I also tried:

TradedCurrencyPair TestPair = new TradedCurrencyPair("one", "two", "Bibox", DateTime.Now, "unknown", 0.1234567890M, 1, 0.1234567890M, 0.1234567890M, 0.1234567890M);
                context.TradedCurrencyPairs.Add(TestPair);
                context.SaveChanges(); 

The result is the same...

Somewhere between setting the value and it ending up in the database, it gets modified :/

here is the SQL Table:

/****** Object:  Table [dbo].[TradedCurrencyPairs]    Script Date: 25/07/2020 09:32:32 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[TradedCurrencyPairs](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [TradedCurrency] [nvarchar](max) NULL,
    [BaseCurrency] [nvarchar](max) NULL,
    [Exchange] [nvarchar](max) NULL,
    [DateTime] [datetime2](7) NOT NULL,
    [TransactionType] [nvarchar](max) NULL,
    [LastPrice] [decimal](20, 10) NOT NULL,
    [ExchangeInternalPairId] [int] NOT NULL,
    [High24h] [decimal](20, 10) NOT NULL,
    [Low24h] [decimal](20, 10) NOT NULL,
    [Volume24h] [decimal](20, 10) NOT NULL,
 CONSTRAINT [PK_TradedCurrencyPairs] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

What DOES work is not using entity framework:

SqlCommand command = new SqlCommand("insert into TradedCurrencyPairs values('one', 'two', 'Bibox', convert(datetime, '18-06-12 10:34:09 PM', 5), 'unknown', 0.1234567890, 1, 0.1234567890, 0.1234567890, 0.1234567890); ", cnn);
This way the decimals do not get modified. So EF causes the issue.

Could anybody please explain to me what I am doing wrong?

Thanks a lot!

Upvotes: 5

Views: 2035

Answers (3)

soomon
soomon

Reputation: 427

When I posted the code from here:

Entity Framework Core - setting the decimal precision and scale to all decimal properties

I was still used to using EF Core 3, so I enabled the code for EF Core 3. I didn't remember I use beta packages and had them updated to EF Core 5 preview.

So using this:

    foreach (var property in modelBuilder.Model.GetEntityTypes()
        .SelectMany(t => t.GetProperties())
        .Where(p => p.ClrType == typeof(decimal) || p.ClrType == typeof(decimal?)))
    {
        
       // EF Core 3
       // property.SetColumnType("decimal(20, 10)");
       // property.SetPrecision(10);
        
        // EF Core 5
        property.SetPrecision(18);
        property.SetScale(6);
    }

instead of this:

    foreach (var property in modelBuilder.Model.GetEntityTypes()
        .SelectMany(t => t.GetProperties())
        .Where(p => p.ClrType == typeof(decimal) || p.ClrType == typeof(decimal?)))
    {
        
        EF Core 3
        property.SetColumnType("decimal(20, 10)");
        property.SetPrecision(10);
        
        // EF Core 5
        // property.SetPrecision(18);
        // property.SetScale(6);
    }

works perfectly.

I'm sorry that I have wasted your time because of my stupid mistake :/

Upvotes: 3

lauxjpn
lauxjpn

Reputation: 5254

Your code (posted here on SO) should generally work. Here is a quick sample program, that shows this:

using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace IssueConsoleTemplate
{
    public class IceCream
    {
        public int IceCreamId { get; set; }
        public string Name { get; set; }
        public decimal PricePerKilogram { get; set; }
    }

    public class Context : DbContext
    {
        public DbSet<IceCream> IceCreams { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder
                .UseSqlServer(@"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=So63079237")
                .UseLoggerFactory(
                    LoggerFactory.Create(
                        b => b
                            .AddConsole()
                            .AddFilter(level => level >= LogLevel.Information)))
                .EnableSensitiveDataLogging()
                .EnableDetailedErrors();
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<IceCream>(
                entity =>
                {
                    entity.Property(e => e.PricePerKilogram)
                        .HasColumnType("decimal(38, 10)");
                    
                    entity.HasData(
                        new IceCream {IceCreamId = 1, Name = "Vanilla", PricePerKilogram = 123456789.123456789M},
                        new IceCream {IceCreamId = 2, Name = "Chocolate", PricePerKilogram = 0.0000001234M});
                });
        }
    }

    internal static class Program
    {
        private static void Main()
        {
            using var context = new Context();

            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            var iceCreams = context.IceCreams
                .OrderBy(i => i.IceCreamId)
                .ToList();
            
            Debug.Assert(iceCreams.Count == 2);
            Debug.Assert(iceCreams[0].PricePerKilogram == 123456789.123456789M);
            Debug.Assert(iceCreams[1].PricePerKilogram == 0.0000001234M);
        }
    }
}

I also took a look at the repo you posted. It works without issues as well. I used the following ExecuteAsync method (not really different from your original one):

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    using (TraderDbContext context = new TraderDbContext())
    {
        context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

        // Works without issues.
        TradedCurrencyPair TestPair = new TradedCurrencyPair("one", "two", "Bibox", DateTime.Now, "unknown", 0.1234567890M, 1, 0.1234567890M, 0.1234567890M, 0.1234567890M);
        context.TradedCurrencyPairs.Add(TestPair);
        context.SaveChanges();
    }
    
    using (TraderDbContext context = new TraderDbContext())
    {
        var tradedCurrencyPair = context.TradedCurrencyPairs.Single();
        Debug.Assert(tradedCurrencyPair.LastPrice == 0.1234567890M);
    }
}

EF Core logs the following SQL, which is correct:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (16ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      CREATE TABLE [TradedCurrencyPairs] (
          [Id] int NOT NULL IDENTITY,
          [TradedCurrency] nvarchar(max) NULL,
          [BaseCurrency] nvarchar(max) NULL,
          [Exchange] nvarchar(max) NULL,
          [DateTime] datetime2 NOT NULL,
          [TransactionType] nvarchar(max) NULL,
          [LastPrice] decimal(20, 10) NOT NULL,
          [ExchangeInternalPairId] int NOT NULL,
          [High24h] decimal(20, 10) NOT NULL,
          [Low24h] decimal(20, 10) NOT NULL,
          [Volume24h] decimal(20, 10) NOT NULL,
          CONSTRAINT [PK_TradedCurrencyPairs] PRIMARY KEY ([Id])
      );

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (79ms) [Parameters=[@p0='two' (Size = 4000), @p1='2020-07-25T04:20:47.8858298+02:00', @p2='Bibox' (Size = 4000), @p3='1', @p4='0.1234567890' (Precision = 10) (Scale = 10), @p5='0.1234567890' (Precision = 10) (Scale = 10), @p6='0.1234567890' (Precision = 10) (Scale = 10), @p7='one' (Size = 4000), @p8='unknown' (Size = 4000), @p9='0.1234567890' (Precision = 10) (Scale = 10)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [TradedCurrencyPairs] ([BaseCurrency], [DateTime], [Exchange], [ExchangeInternalPairId], [High24h], [LastPrice], [Low24h], [TradedCurrency], [TransactionType], [Volume24h])
      VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9);
      SELECT [Id]
      FROM [TradedCurrencyPairs]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

Do you have anything special setup database-side? Triggers, default values or something like that? You might want to post the CREATE TABLE script of the database you are using (generated by SSMS).

Upvotes: 0

Preben Huybrechts
Preben Huybrechts

Reputation: 6111

EF core 5 has support for the precision on the model builder try:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<TradedCurrencyPair>()
        .Property(to => tp.LastPrice)
        .HasPrecision(38, 10);
}

Or you could use SetPrecision in the same way as you are using SetColumnType see github.

Upvotes: 1

Related Questions