Skip to content

Fix sql_variant (object-typed) columns failing to map to value-type members (#2234) - #2246

Open
SENESO wants to merge 1 commit into
DapperLib:mainfrom
SENESO:fix/sql-variant-convert-2234
Open

SENESO wants to merge 1 commit into
DapperLib:mainfrom
SENESO:fix/sql-variant-convert-2234

Conversation

@SENESO

@SENESO SENESO commented Oct 24, 2026

Copy link
Copy Markdown

Fixes #2234.

The bug

With Dapper 2.1.86 and Microsoft.Data.SqlClient, mapping a sql_variant column onto a value-type member throws:

connection.QuerySingle<TestRow>("SELECT SERVERPROPERTY('IsLocalDB') AS IsLocalDB");
// TestRow.IsLocalDB is bool
System.Data.DataException: Error parsing column 0 (IsLocalDB=0 - Int32)
 ---> System.InvalidCastException: Specified cast is not valid.

The same code works in 2.1.79, and casting to int or bit in the SQL works in 2.1.86.

Root cause

SERVERPROPERTY returns sql_variant, and SqlClient reports such a column's field type as System.Object (GetFieldType(0) returns System.Object while the actual value is a boxed Int32 — verified against a live SQL Server). The deserializer therefore has to decide at runtime, from the boxed value, how to convert it to the member type.

The change in #2228 added colType == typeof(object) to the direct-unbox shortcut in LoadReaderValueOrBranchToDBNullLabel (SqlMapper.cs), so for a column reported as object the emitted IL now does a blind unbox.any of the target member type. Unboxing a boxed Int32 as bool is an invalid cast, hence the exception. Before that change, this combination fell through to FlexibleConvertBoxedFromHeadOfStack, which converts the boxed value to the member type.

The fix

For a column reported as object whose target member is a value type, the deserializer now emits a runtime check of the boxed value's type:

Reference-type members keep the previous direct behaviour, type handlers still take priority, and columns with a declared field type are completely unaffected. As a side effect this also fixes the nullable case (bool? over a variant column), which threw even in 2.1.79 because the conversion result was never wrapped into the Nullable<T>.

Tests

New SqlVariantTests (database-free; a stub reader that reports the column as System.Object, exactly like SqlClient does for sql_variant):

  • VariantInt32MapsToBool — the reported scenario (0 → false, 1 → true)
  • VariantInt32MapsToNullableBool — including DBNull → null
  • VariantInt32MapsToBoolStruct — struct rows go through the same code path
  • VariantExactBoxMapsDirectly — exact-type boxes (int, DateTime) still unbox directly
  • VariantConvertibleBoxConverts — Int64 → int, Double → decimal
  • VariantBoxTypeCanVaryPerRow — one query whose rows carry different underlying types
  • DeclaredInt32StillMapsToBool — declared-type columns unchanged

Also verified end-to-end against a local SQL Server 2022 with Microsoft.Data.SqlClient 7.0.1: SERVERPROPERTY('IsLocalDB') → bool fails on current main with the exact error from the issue and maps correctly with this change; sql_variant int → bool/bool?/int and string variants all behave. The Microsoft.Data.SqlClient test classes (Misc, MultiMap, QueryMultiple, Async, TypeHandler, Enum, Decimal, Null, DataReader, DateTimeOnly, Constructor, Parameter — 307 tests) pass, except 3 ParameterTests culture/collation cases that fail identically on unmodified main in the same environment.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQL Server sql_variant mapping regression in 2.1.86: SERVERPROPERTY Int32 to bool throws InvalidCastException

1 participant