66
77namespace Npgsql . Internal ;
88
9- sealed class ObjectConverter : PgStreamingConverter < object >
9+ sealed class LateBindingConverter : PgStreamingConverter < object >
1010{
11- public ObjectConverter ( ) => HandleDbNull = true ;
11+ public LateBindingConverter ( ) => HandleDbNull = true ;
1212
1313 protected override bool IsDbNullValue ( object ? value , object ? writeState )
1414 {
1515 var ( concreteTypeInfo , effectiveState ) = writeState switch
1616 {
1717 PgConcreteTypeInfo info => ( info , ( object ? ) null ) ,
18- WriteState ws => ( ws . ConcreteTypeInfo , ws . EffectiveState ) ,
19- _ => throw new InvalidOperationException ( "writeState cannot be null, LateBoundTypeInfoProvider is expected to pre-populate it with concrete type info." )
18+ LateBindingWriteState ws => ( ws . ConcreteTypeInfo , ws . EffectiveState ) ,
19+ _ => throw new InvalidOperationException ( "writeState cannot be null, LateBindingTypeInfoProvider is expected to pre-populate it with concrete type info." )
2020 } ;
2121
2222 return concreteTypeInfo . Converter . IsDbNullAsObject ( value , effectiveState ) ;
@@ -30,7 +30,7 @@ protected override Size BindValue(in BindContext context, object value, ref obje
3030 var ( concreteTypeInfo , effectiveState ) = writeState switch
3131 {
3232 PgConcreteTypeInfo info => ( info , ( object ? ) null ) ,
33- WriteState state => ( state . ConcreteTypeInfo , state . EffectiveState ) ,
33+ LateBindingWriteState state => ( state . ConcreteTypeInfo , state . EffectiveState ) ,
3434 _ => throw new InvalidOperationException ( "Invalid state" )
3535 } ;
3636
@@ -43,7 +43,7 @@ protected override Size BindValue(in BindContext context, object value, ref obje
4343 // Null the wrapper's EffectiveState before handoff. Inner BindAsObject's framework safety net
4444 // disposes via our local ref on throw and nulls the local; the wrapper would otherwise hold a
4545 // dangling reference to the same object, double-disposing through outer Bind's catch.
46- if ( writeState is WriteState before )
46+ if ( writeState is LateBindingWriteState before )
4747 before . EffectiveState = null ;
4848
4949 var result = concreteTypeInfo . Converter . BindAsObject (
@@ -52,10 +52,10 @@ protected override Size BindValue(in BindContext context, object value, ref obje
5252 ref effectiveState ) ;
5353 if ( effectiveState is not null )
5454 {
55- if ( writeState is WriteState s )
55+ if ( writeState is LateBindingWriteState s )
5656 s . EffectiveState = effectiveState ;
5757 else
58- writeState = new WriteState { ConcreteTypeInfo = concreteTypeInfo , EffectiveState = effectiveState } ;
58+ writeState = new LateBindingWriteState { ConcreteTypeInfo = concreteTypeInfo , EffectiveState = effectiveState } ;
5959 }
6060
6161 return result ;
@@ -72,7 +72,7 @@ async ValueTask Write(bool async, PgWriter writer, object value, CancellationTok
7272 var ( concreteTypeInfo , effectiveState ) = writer . Current . WriteState switch
7373 {
7474 PgConcreteTypeInfo info => ( info , ( object ? ) null ) ,
75- WriteState state => ( state . ConcreteTypeInfo , state . EffectiveState ) ,
75+ LateBindingWriteState state => ( state . ConcreteTypeInfo , state . EffectiveState ) ,
7676 _ => throw new InvalidOperationException ( "Invalid state" )
7777 } ;
7878
@@ -82,37 +82,13 @@ async ValueTask Write(bool async, PgWriter writer, object value, CancellationTok
8282 using var _ = await writer . BeginNestedWrite ( async , writeRequirement , writer . Current . Size . Value , effectiveState , cancellationToken ) . ConfigureAwait( false) ;
8383 await concreteTypeInfo . Converter . WriteAsObject ( async , writer , value , cancellationToken ) . ConfigureAwait( false) ;
8484 }
85-
86- internal sealed class WriteState : IDisposable
87- {
88- public required PgConcreteTypeInfo ConcreteTypeInfo { get ; init ; }
89- public required object ? EffectiveState { get ; set ; }
90- int _disposed ;
91-
92- // EffectiveState may hold a pooled WriteState from the underlying concrete converter
93- // (composite, array, etc.). The outer DisposeWriteState on PgTypeInfo only sees this
94- // wrapper, so the wrapper is responsible for cascading disposal to the inner state.
95- public void Dispose ( )
96- {
97- // Atomic idempotency guard — EffectiveState may be pool-backed; cascading double-dispose
98- // corrupts downstream pools. Atomic catches concurrent disposal too.
99- if ( Interlocked . Exchange ( ref _disposed , 1 ) != 0 )
100- {
101- Debug . Assert ( false , "ObjectConverter.WriteState double-dispose detected — caller violated lifecycle contract." ) ;
102- return ;
103- }
104-
105- if ( EffectiveState is IDisposable disposable )
106- disposable . Dispose ( ) ;
107- }
108- }
10985}
11086
11187// TODO the goal is to allow this provider to return the underlying converter type info, but we're not there yet.
112- // At that point we don't need the ObjectConverter any longer.
113- sealed class LateBoundTypeInfoProvider ( PgSerializerOptions options , PgTypeId typeId ) : PgConcreteTypeInfoProvider < object >
88+ // At that point we don't need the LateBindingConverter any longer.
89+ sealed class LateBindingTypeInfoProvider ( PgSerializerOptions options , PgTypeId typeId ) : PgConcreteTypeInfoProvider < object >
11490{
115- readonly PgConcreteTypeInfo _defaultConcreteTypeInfo = new ( options , new ObjectConverter ( ) , typeId ) ;
91+ readonly PgConcreteTypeInfo _defaultConcreteTypeInfo = new ( options , new LateBindingConverter ( ) , typeId ) ;
11692
11793 protected override PgConcreteTypeInfo GetDefaultCore ( PgTypeId ? pgTypeId )
11894 {
@@ -139,11 +115,35 @@ protected override PgConcreteTypeInfo GetForValueCore(ProviderValueContext conte
139115 // cascades to EffectiveState; PgConcreteTypeInfo (non-IDisposable) is a long-lived cached
140116 // instance so the no-wrapper branch is naturally safe.
141117 writeState = effectiveState is not null
142- ? new ObjectConverter . WriteState { ConcreteTypeInfo = concreteTypeInfo , EffectiveState = effectiveState }
118+ ? new LateBindingWriteState { ConcreteTypeInfo = concreteTypeInfo , EffectiveState = effectiveState }
143119 : concreteTypeInfo ;
144120 if ( ! concreteTypeInfo . SupportsWriting )
145121 AdoSerializerHelpers . ThrowWritingNotSupported ( valueType , options , concreteTypeInfo . PgTypeId , resolved : true ) ;
146122
147123 return GetDefault ( context . ExpectedPgTypeId ) ;
148124 }
149125}
126+
127+ file sealed class LateBindingWriteState : IDisposable
128+ {
129+ public required PgConcreteTypeInfo ConcreteTypeInfo { get ; init ; }
130+ public required object ? EffectiveState { get ; set ; }
131+ int _disposed ;
132+
133+ // EffectiveState may hold a pooled write state from the underlying concrete converter
134+ // (composite, array, etc.). The outer DisposeWriteState on PgTypeInfo only sees this
135+ // wrapper, so the wrapper is responsible for cascading disposal to the inner state.
136+ public void Dispose ( )
137+ {
138+ // Atomic idempotency guard — EffectiveState may be pool-backed; cascading double-dispose
139+ // corrupts downstream pools. Atomic catches concurrent disposal too.
140+ if ( Interlocked . Exchange ( ref _disposed , 1 ) != 0 )
141+ {
142+ Debug . Assert ( false , $ "{ nameof ( LateBindingWriteState ) } double-dispose detected — caller violated lifecycle contract.") ;
143+ return ;
144+ }
145+
146+ if ( EffectiveState is IDisposable disposable )
147+ disposable . Dispose ( ) ;
148+ }
149+ }
0 commit comments