5

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!

soomon
  • 396
  • 2
  • 18
  • Can you provide a complete repro? The only thing I can think is that you're doing server-side arithmetic and the least-significant digits are being lost to preserver the 28 more significant ones, which can happen with DECIMAL in TSQL. – David Browne - Microsoft Jul 24 '20 at 19:18
  • I have added a screenshot to illustrate that right before I invoke context.SaveChanges, everything looks good. – soomon Jul 24 '20 at 20:02
  • Yes, but in a simple test everything worked fine for me. So try to pare it down to a minimal repro, and you'll either find the issue or have something to add to your question. – David Browne - Microsoft Jul 24 '20 at 20:03
  • I could send you a link to the repo, but I could not find a way to send a private message here. Can send it on linkedin if that is ok with you. – soomon Jul 24 '20 at 20:49
  • It's better if you can simplify the scenario down to a repro that can be posted in the question body, or at least posted publicly. I wouldn't be able to look at your actual code without an non-disclosure agreement. – David Browne - Microsoft Jul 24 '20 at 21:12
  • I have uploaded a minimal version to github: https://github.com/soomon/decimal-issue . Thanks so much for taking a look! – soomon Jul 24 '20 at 21:37

3 Answers3

3

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 :/

soomon
  • 396
  • 2
  • 18
1

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.

Preben Huybrechts
  • 5,853
  • 2
  • 27
  • 63
  • I tried to add "property.SetPrecision(10);" after "property.SetColumnType("decimal(38, 10)");", but it didn't help :/ My EF Core version is 5.0.0-preview.7.20365.15 – soomon Jul 24 '20 at 19:44
0

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).

lauxjpn
  • 4,749
  • 1
  • 20
  • 40
  • hi. thanks for taking a look! I added the table create script in my post. please note that I use EF Core to handle database and table management. As inserting the data without EF Core works, I think the database is fine and it is an issue with my DB Context – soomon Jul 25 '20 at 07:37
  • The `CREATE TABLE` script looks fine. Please enable EF Core logging (at least at the information level) and update your question with the SQL that is being generated (and logged) by EF Core and is containing the `INSERT` statement. – lauxjpn Jul 25 '20 at 10:40