Skip to content

Bug/Regression doing a query with a jsonb column as string with 2.1.* #2049

Description

@lucax88x

Ciao!

I've reproduced the issue there:

https://github.com/lucax88x/dapper-bug

So, if you have a postgresql db, and you use a jsonb column, and you use netwonsoft as deserializer such as:

NpgsqlConnection.GlobalTypeMapper.UseJsonNet(settings: settings);

and perform a a query like

 await conn.QueryFirstOrDefaultAsync<string>("select \"Content\" from public.\"Posts\"");

will throw an exception with

 ---> System.InvalidCastException: Object must implement IConvertible.
   at System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider)
   at Dapper.SqlMapper.GetValue[T](DbDataReader reader, Type effectiveType, Object val) in /_/Dapper/SqlMapper.cs:line 1366
   --- End of inner exception stack trace ---
   at Dapper.SqlMapper.ThrowDataException(Exception ex, Int32 index, IDataReader reader, Object value) in /_/Dapper/SqlMapper.cs:line 3928
   at Dapper.SqlMapper.QueryRowAsync[T](IDbConnection cnn, Row row, Type effectiveType, CommandDefinition command) in /_/Dapper/SqlMapper.Async.cs:line 496

This happens only with 2.1.*, if I revert to 2.0.151 everything works perfectly.

This is probably the PR that breaks it.

ps: without json.net it also work perfectly, it's just a combination of json.net and jsonb.

Activity

  1. mgravell commented on Mar 5, 2024

    @mgravell
    Member

    I imagine that what's happening here is that npgsql is doing the deserialize, and giving us something that isn't a string (but rather: a rich object) - and we're expecting it to be a string. This is probably related to Dapper (vanilla) using the untyped data read API in some cases. Dapper.AOT does a better job of preferring typed data, so I imagine that Dapper.AOT would explicitly request a string, and receive a string. Any chance you can try the same query with Dapper AOT enabled? Or provide a full repro?

  2. lucax88x commented on Mar 5, 2024

    @lucax88x
    Author

    There's a github link with a full repro on top.

    Clone the repo, and run the tests, it will fire up a db with test containers and run the query.

    I can try with AOT tomorrow!

  3. mgravell commented on Mar 6, 2024

    @mgravell
    Member
  4. mgravell commented on Mar 6, 2024

    @mgravell
    Member

    apologies, I overlooked the repro; taking a peek

  5. mgravell commented on Mar 6, 2024

    @mgravell
    Member

    nit: await using var conn = new NpgsqlConnection(_connectionString);, but: great repro, thanks - looking

  6. mgravell commented on Mar 6, 2024

    @mgravell
    Member

    I'm a little confused... it... works? (I haven't changed anything except the connection string)

    image

    and at the command-line:

       09:39:49  DapperBug.Tests  main   8.0.200   13.796s   
    ➜ dotnet test
      Determining projects to restore...
      All projects are up-to-date for restore.
      DapperBug -> C:\Code\dapper-bug\DapperBug\bin\Debug\net8.0\DapperBug.dll
      DapperBug.Tests -> C:\Code\dapper-bug\DapperBug.Tests\bin\Debug\net8.0\DapperBug.Tests.dll
    Test run for C:\Code\dapper-bug\DapperBug.Tests\bin\Debug\net8.0\DapperBug.Tests.dll (.NETCoreApp,Version=v8.0)
    Microsoft (R) Test Execution Command Line Tool Version 17.9.0 (x64)
    Copyright (c) Microsoft Corporation.  All rights reserved.
    
    Starting test execution, please wait...
    A total of 1 test files matched the specified pattern.
    
    Passed!  - Failed:     0, Passed:     1, Skipped:     0, Total:     1, Duration: < 1 ms - DapperBug.Tests.dll (net8.0)

    What should I be seeing?

  7. mgravell commented on Mar 6, 2024

    @mgravell
    Member

    it is possible that this can be fixed with a one-liner in SqlMapper.cs L217:

    -                [typeof(string)] = DbType.String,
    +                [typeof(string)] = new(DbType.String, TypeMapEntryFlags.SetType | TypeMapEntryFlags.UseGetFieldValue),

    can check as soon as we can repro

    on a per-consumer-project basis, we can check this with

    SqlMapper.AddTypeMap(typeof(string), DbType.String, true);
  8. lucax88x commented on Mar 6, 2024

    @lucax88x
    Author

    I'm a little confused... it... works? (I haven't changed anything except the connection string)

    image

    and at the command-line:

       09:39:49  DapperBug.Tests  main   8.0.200   13.796s   
    ➜ dotnet test
      Determining projects to restore...
      All projects are up-to-date for restore.
      DapperBug -> C:\Code\dapper-bug\DapperBug\bin\Debug\net8.0\DapperBug.dll
      DapperBug.Tests -> C:\Code\dapper-bug\DapperBug.Tests\bin\Debug\net8.0\DapperBug.Tests.dll
    Test run for C:\Code\dapper-bug\DapperBug.Tests\bin\Debug\net8.0\DapperBug.Tests.dll (.NETCoreApp,Version=v8.0)
    Microsoft (R) Test Execution Command Line Tool Version 17.9.0 (x64)
    Copyright (c) Microsoft Corporation.  All rights reserved.
    
    Starting test execution, please wait...
    A total of 1 test files matched the specified pattern.
    
    Passed!  - Failed:     0, Passed:     1, Skipped:     0, Total:     1, Duration: < 1 ms - DapperBug.Tests.dll (net8.0)
    

    What should I be seeing?

    I'm a little confused... it... works? (I haven't changed anything except the connection string)

    image

    and at the command-line:

       09:39:49  DapperBug.Tests  main   8.0.200   13.796s   
    ➜ dotnet test
      Determining projects to restore...
      All projects are up-to-date for restore.
      DapperBug -> C:\Code\dapper-bug\DapperBug\bin\Debug\net8.0\DapperBug.dll
      DapperBug.Tests -> C:\Code\dapper-bug\DapperBug.Tests\bin\Debug\net8.0\DapperBug.Tests.dll
    Test run for C:\Code\dapper-bug\DapperBug.Tests\bin\Debug\net8.0\DapperBug.Tests.dll (.NETCoreApp,Version=v8.0)
    Microsoft (R) Test Execution Command Line Tool Version 17.9.0 (x64)
    Copyright (c) Microsoft Corporation.  All rights reserved.
    
    Starting test execution, please wait...
    A total of 1 test files matched the specified pattern.
    
    Passed!  - Failed:     0, Passed:     1, Skipped:     0, Total:     1, Duration: < 1 ms - DapperBug.Tests.dll (net8.0)
    

    What should I be seeing?

    Wow.

    This is extremely weird, I lost two hours for that.

    So, stay with me:
    update the git repo
    run the compose from the root
    docker compose -f docker-compose.yml up

    now, you have 2 tests from the solution
    -CreateTables
    -RunAloneNotTogetherWithCreateTables

    1. Try to run CreateTables ALONE so you have the structure.
    2. Now run the RunAloneNotTogetherWithCreateTable ALONE and it should give the Exception above (now reproduced)

    But, if for some reason, you try to run both of them together, CreateTables will fail because table is already there (which is OK), but the RunAloneNotTogetherWithCreateTables will be green (WTF?)!

    Something must happening with assembly...

    Gonna try with your map.

  9. lucax88x commented on Mar 6, 2024

    @lucax88x
    Author
    SqlMapper.AddTypeMap(typeof(string), DbType.String, true);

    I can confirm it works.

  10. mgravell commented on Mar 6, 2024

    @mgravell
    Member

    great; that's a workaround, and we'll get the code fixed for next release

  11. mgravell commented on Mar 6, 2024

    @mgravell
    Member
    Npgsql.PostgresException : 3D000: database "test" does not exist
    

    so... I don't think it is connecting to the containerized database, but... I think we understand the problem well enough now

  12. lucax88x commented on Mar 6, 2024

    @lucax88x
    Author

    Create the db in the container :)

  13. lucax88x commented on Mar 6, 2024

    @lucax88x
    Author

    thanks for the quick answer btw.

  14. mgravell commented on Mar 6, 2024

    @mgravell
    Member

    a couple of subtle behaviour changes with exception messages, but : within tolerance (and in some cases, clearer)

  15. lucax88x commented on Mar 14, 2024

    @lucax88x
    Author

    @mgravell

    I don't know where the problem is, but I'm noticing the same bug using npgsql 8 and dapper 2.0.151.

    I did another repro using a online postgresql.

    https://gist.github.com/lucax88x/69c6deb14b80c856271534de5c10e1ad

    dapper + json.net
    dapper without json.net

    npgsql with json.net
    ngpsql without json.net

    Without the workaround (the SqlMapper one) the one with dapper and json.net fails.

    So I think the regression didn't come with 2.1 but rather from 8.0 of npgsql.

  16. mgravell commented on Mar 14, 2024

    @mgravell
    Member
  17. mgravell commented on Mar 14, 2024

    @mgravell
    Member

    This change will almost certainly be reverted; it broke other scenarios, where string is handled more flexibly. I'll check more options.

  18. lucax88x commented on Mar 15, 2024

    @lucax88x
    Author

    It looks like we still haven't shipped NuGet packages for this - will fix However: IIRC npgsql now marks this global API as deprecated, preferring a column-specific configuration, so I'm sure they'd say "we already fixed this: the fix is to stop using that API" (apologies if I'm misrepresenting this - I don't do much npgsql)
    …
    On Thu, 14 Mar 2024, 12:51 Luca Trazzi, @.***> wrote: @mgravell https://github.com/mgravell I don't know where the problem is, but I'm noticing the same bug using npgsql 8 and dapper 2.0.151. I did another repro using a online postgresql. https://gist.github.com/lucax88x/69c6deb14b80c856271534de5c10e1ad dapper + json.net dapper without json.net npgsql with json.net ngpsql without json.net Without the workaround (the SqlMapper one) the one with dapper and json.net fails. So I think the regression didn't come with 2.1 but rather from 8.0 of npgsql. — Reply to this email directly, view it on GitHub <#2049 (comment)> or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAEHMAFKXOWA7YXJ27DRADYYGMMJBFKMF2HI4TJMJ2XIZLTSOBKK5TBNR2WLJDUOJ2WLJDOMFWWLO3UNBZGKYLEL5YGC4TUNFRWS4DBNZ2F6YLDORUXM2LUPGBKK5TBNR2WLJDUOJ2WLJDOMFWWLLTXMF2GG2C7MFRXI2LWNF2HTAVFOZQWY5LFUVUXG43VMWSG4YLNMWVXI2DSMVQWIX3UPFYGLLDTOVRGUZLDORPXI6LQMWWES43TOVSUG33NNVSW45FGORXXA2LDOOJIFJDUPFYGLKTSMVYG643JORXXE6NFOZQWY5LFU4YTMMJTGM2DLAVEOR4XAZNFNFZXG5LFUV3GC3DVMWVDEMJXGAYDEMJRGUZ2O5DSNFTWOZLSUZRXEZLBORSQ . You are receiving this email because you were mentioned. Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub .

    if you check the second repro I'm not using the global one, so they didn't fix, also because the problem is occuring only with dapper

  19. mgravell commented on Mar 15, 2024

    @mgravell
    Member

    @roji can I summon your wisdom here? Problem is JSON deserialize kicking in when using the untyped read API, but actually wanting a string; there are long reasons (fixed in AOT) why typed read is a PITA. Anything more I can do the detect this problem before it happens?

  20. roji commented on Mar 15, 2024

    @roji

    Hey @mgravell... Is this the repro (from the OP)? That's odd - by default, Npgsql should absolutely return a string for an untyped read of a PG jsonb.

    If I I tried to repro this without Dapper (similar to the gist in #2049 (comment)) and couldn't (see below) - is it possible to get an ADO.NET-only code sample that shows what Dapper is doing when stuff fails?

    Re NpgsqlConnection.GlobalTypeMapper being obsolete, that shouldn't have anything to do with this; we now recommend using the newer DbDataSource API to create connections, and to configure plugins such as Json.NET there, so:

    var builder = new NpgsqlDataSourceBuilder("Host=localhost;Username=test;Password=test");
    builder.UseJsonNet();
    await using var dataSource = builder.Build();
    
    await using var conn = await dataSource.OpenConnectionAsync();
    Attempted ADO-only repro
    #pragma warning disable CS0618 // Type or member is obsolete
    NpgsqlConnection.GlobalTypeMapper.UseJsonNet();
    #pragma warning restore CS0618 // Type or member is obsolete
    
    await using var conn = new NpgsqlConnection("Host=localhost;Username=test;Password=test");
    await conn.OpenAsync();
    
    Console.WriteLine("Creating and seeding data");
    
    await conn.ExecuteAsync("""
                            drop table if exists posts;
                            create table public.posts
                            (
                                postid  integer generated by default as identity
                            constraint "PK_posts"
                            primary key,
                                title   text  not null,
                            content jsonb not null
                                );
    
                            insert into public.posts (postid, title, content) values (1, 'title', '{"Prop1": "2", "Prop2": 2}');
                            """);
    
    await using (var command = new NpgsqlCommand("select content from public.posts", conn))
    await using (var reader = await command.ExecuteReaderAsync())
    {
        while (await reader.ReadAsync())
        {
            Console.WriteLine($"Content typed: {reader.GetFieldValue<string>(0)}");
            Console.WriteLine($"Content untyped: {reader.GetValue(0)}");
        }
    }
    
    // The following indeed throws:
    _ = await conn.QueryFirstOrDefaultAsync<string>("select content from public.posts");
  21. lucax88x commented on Mar 15, 2024

    @lucax88x
    Author
  22. roji commented on Mar 15, 2024

    @roji

    @lucax88x the ADO tests in that gist pass, only dapper_should_work_with_json_net seems to fail.

  23. added a commit that references this issue on Apr 12, 2024
  24. mgravell commented on Apr 12, 2024

    @mgravell
    Member

    crossref #2070

  25. added a commit that references this issue on Apr 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions