@@ -40,10 +40,10 @@ public class NpgsqlParameter : DbParameter, IDbDataParameter, ICloneable
4040 internal string TrimmedName { get ; private protected set ; } = PositionalName ;
4141 internal const string PositionalName = "" ;
4242
43- internal PgTypeInfo ? TypeInfo { get ; private set ; }
43+ private protected PgTypeInfo ? TypeInfo { get ; private set ; }
4444
4545 internal PgTypeId PgTypeId { get ; private set ; }
46- internal PgConverter ? Converter { get ; private set ; }
46+ private protected PgConverter ? Converter { get ; private set ; }
4747
4848 internal DataFormat Format { get ; private protected set ; }
4949 private protected Size ? WriteSize { get ; set ; }
@@ -278,7 +278,7 @@ public override object? Value
278278 get => _value ;
279279 set
280280 {
281- if ( value is null || _value ? . GetType ( ) != value . GetType ( ) )
281+ if ( ShouldResetObjectTypeInfo ( value ) )
282282 ResetTypeInfo ( ) ;
283283 else
284284 ResetBindingInfo ( ) ;
@@ -497,6 +497,17 @@ public sealed override string SourceColumn
497497
498498 Type ? GetValueType ( Type staticValueType ) => staticValueType != typeof ( object ) ? staticValueType : Value ? . GetType ( ) ;
499499
500+ internal bool ShouldResetObjectTypeInfo ( object ? value )
501+ {
502+ var currentType = TypeInfo ? . Type ;
503+ if ( currentType is null || value is null )
504+ return false ;
505+
506+ var valueType = value . GetType ( ) ;
507+ // We don't want to reset the type info when the value is a DBNull, we're able to write it out with any type info.
508+ return valueType != typeof ( DBNull ) && currentType != valueType ;
509+ }
510+
500511 internal void GetResolutionInfo ( out PgTypeInfo ? typeInfo , out PgConverter ? converter , out PgTypeId pgTypeId )
501512 {
502513 typeInfo = TypeInfo ;
@@ -540,6 +551,7 @@ _npgsqlDbType is { } npgsqlDbType
540551 pgTypeId = options . ToCanonicalTypeId ( pgType . GetRepresentationalType ( ) ) ;
541552 }
542553
554+ var unspecifiedDBNull = false ;
543555 var valueType = StaticValueType ;
544556 if ( valueType == typeof ( object ) )
545557 {
@@ -551,22 +563,31 @@ _npgsqlDbType is { } npgsqlDbType
551563 }
552564
553565 // We treat object typed DBNull values as default info.
566+ // Unless we don't have a pgTypeId either, at which point we'll use an 'unspecified' PgTypeInfo to help us write a NULL.
554567 if ( valueType == typeof ( DBNull ) )
555568 {
556- valueType = null ;
557- pgTypeId ??= options . ToCanonicalTypeId ( options . UnknownPgType ) ;
569+ if ( pgTypeId is null )
570+ {
571+ unspecifiedDBNull = true ;
572+ typeInfo = options . UnspecifiedDBNullTypeInfo ;
573+ }
574+ else
575+ valueType = null ;
558576 }
559577 }
560578
561- TypeInfo = typeInfo = AdoSerializerHelpers . GetTypeInfoForWriting ( valueType , pgTypeId , options , _npgsqlDbType ) ;
579+ if ( ! unspecifiedDBNull )
580+ typeInfo = AdoSerializerHelpers . GetTypeInfoForWriting ( valueType , pgTypeId , options , _npgsqlDbType ) ;
581+
582+ TypeInfo = typeInfo ;
562583 }
563584
564585 // This step isn't part of BindValue because we need to know the PgTypeId beforehand for things like SchemaOnly with null values.
565586 // We never reuse resolutions for resolvers across executions as a mutable value itself may influence the result.
566587 // TODO we could expose a property on a Converter/TypeInfo to indicate whether it's immutable, at that point we can reuse.
567588 if ( ! previouslyResolved || typeInfo ! . IsResolverInfo )
568589 {
569- ResetBindingInfo ( ) ; // No need for ResetConverterResolution as we'll mutate those fields directly afterwards.
590+ ResetBindingInfo ( ) ;
570591 var resolution = ResolveConverter ( typeInfo ! ) ;
571592 Converter = resolution . Converter ;
572593 PgTypeId = resolution . PgTypeId ;
@@ -735,11 +756,6 @@ public override void ResetDbType()
735756 private protected void ResetTypeInfo ( )
736757 {
737758 TypeInfo = null ;
738- ResetConverterResolution ( ) ;
739- }
740-
741- void ResetConverterResolution ( )
742- {
743759 _asObject = false ;
744760 Converter = null ;
745761 PgTypeId = default ;
0 commit comments