From 86538d271425c60218084d88a340f69496ff5fc6 Mon Sep 17 00:00:00 2001 From: "Leandro M. Medeiros" Date: Fri, 25 Sep 2026 12:16:15 +0200 Subject: [PATCH] fix: Regression - Broken mapping of numeric arrays in Postgres (#2231 & #2234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DateOnly/TimeOnly conversion changes introduced in `6d48ef6` excluded `TypeCode.Object` from Dapper’s direct conversion path. As a result, provider-returned PostgreSQL arrays, such as `long[]`, could fall through to `Convert.ChangeType`, breaking mappings to compatible collection types such as `IReadOnlyCollection`. This change preserves the new DateOnly/TimeOnly conversion behavior while restoring direct assignment when the provider value is assignable to the target type. A PostgreSQL regression test has also been added covering `bigint|int|smallint[]` to `IReadOnlyCollection` mapping. Fixes #2231 and #2234 --- Dapper/SqlMapper.cs | 8 ++++-- .../Dapper.Tests/Providers/PostgresqlTests.cs | 25 ++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Dapper/SqlMapper.cs b/Dapper/SqlMapper.cs index 8f3956cab..4ee8c0587 100644 --- a/Dapper/SqlMapper.cs +++ b/Dapper/SqlMapper.cs @@ -3851,13 +3851,17 @@ private static void LoadReaderValueOrBranchToDBNullLabel(ILGenerator il, int ind { TypeCode dataTypeCode = Type.GetTypeCode(colType), unboxTypeCode = Type.GetTypeCode(unboxType); bool hasTypeHandler; + bool providerValueIsAssignable = unboxType.IsAssignableFrom(colType); // note the TypeCode tests are only meaningful for distinct codes: TypeCode.Object // matching TypeCode.Object says nothing (TimeSpan vs TimeOnly?, say), and a direct // unbox there throws; such pairs belong to the flexible-convert path below. A column // reported as plain *object* (sql_variant etc) keeps the direct unbox: the runtime // box is the only truth available there - if ((hasTypeHandler = typeHandlers.ContainsKey(unboxType)) || colType == unboxType - || colType == typeof(object) || unboxType == typeof(object) + if ((hasTypeHandler = typeHandlers.ContainsKey(unboxType)) + || colType == unboxType + || providerValueIsAssignable + || colType == typeof(object) + || unboxType == typeof(object) || (dataTypeCode == unboxTypeCode && dataTypeCode != TypeCode.Object) || (dataTypeCode == Type.GetTypeCode(nullUnderlyingType) && dataTypeCode != TypeCode.Object) || colType == nullUnderlyingType) diff --git a/tests/Dapper.Tests/Providers/PostgresqlTests.cs b/tests/Dapper.Tests/Providers/PostgresqlTests.cs index b9d60f389..77c229075 100644 --- a/tests/Dapper.Tests/Providers/PostgresqlTests.cs +++ b/tests/Dapper.Tests/Providers/PostgresqlTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Data; using System.Data.Common; using System.Linq; using Xunit; @@ -68,6 +67,30 @@ private class DateReadings public DateOnly AsDateOnly { get; set; } public DateTime? AsNullableDateTime { get; set; } } + + private sealed record NumericArraysTable + { + public IReadOnlyCollection Longs { get; set; } = []; + public IReadOnlyCollection Ints { get; set; } = []; + public IReadOnlyCollection Shorts { get; set; } = []; + } + + [FactPostgresql] + public void TestPostgresqlBigintArrayMapsToReadOnlyCollection() + { + using var conn = GetOpenNpgsqlConnection(); + + var row = conn.QuerySingle( + @"SELECT + ARRAY[1001, 1002, 1003]::bigint[] AS ""Longs"" + , ARRAY[2001, 2002, 2003]::int[] AS ""Ints"" + , ARRAY[3001, 3002, 3003]::smallint[] AS ""Shorts"" + ;"); + + Assert.Equal(new [] { 1001L, 1002L, 1003L }, row.Longs.ToArray()); + Assert.Equal(new [] { 2001, 2002, 2003 }, row.Ints.ToArray()); + Assert.Equal(new short[] { 3001, 3002, 3003 }, row.Shorts.ToArray()); + } #endif [FactPostgresql]