-
Notifications
You must be signed in to change notification settings - Fork 883
Expand file tree
/
Copy pathPgConcreteTypeInfoProvider.cs
More file actions
205 lines (184 loc) · 9.95 KB
/
Copy pathPgConcreteTypeInfoProvider.cs
File metadata and controls
205 lines (184 loc) · 9.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
using System;
using System.Diagnostics.CodeAnalysis;
using Npgsql.Internal.Postgres;
namespace Npgsql.Internal;
[Experimental(NpgsqlDiagnostics.ConvertersExperimental)]
public abstract class PgConcreteTypeInfoProvider
{
private protected PgConcreteTypeInfoProvider() { }
/// <summary>
/// Gets the appropriate type info solely based on PgTypeId.
/// </summary>
public PgConcreteTypeInfo GetDefault(PgTypeId? pgTypeId)
{
var result = GetDefaultCore(pgTypeId);
if (pgTypeId.HasValue && result.PgTypeId != Nullable.GetValueRefOrDefaultRef(in pgTypeId))
ThrowPgTypeIdMismatch(nameof(GetDefaultCore));
return result;
}
/// <summary>
/// Gets the appropriate type info based on the given field context.
/// </summary>
public PgConcreteTypeInfo? GetForField(in ProviderFieldContext context) => GetForFieldCore(context);
/// <summary>
/// Gets the appropriate type info based on the given value and expected type id.
/// </summary>
public PgConcreteTypeInfo? GetForValueAsObject(in ProviderValueContext context, object? value, out object? writeState)
{
writeState = null;
try
{
var result = GetForValueAsObjectCore(context, value, ref writeState);
// Contract: a null return means "fall back to default" and forbids state production —
// the default path has no slot for state. Enforce here so callers can rely on
// "result is null ⇒ writeState is null" instead of disposing defensively.
if (result is null && writeState is not null)
ThrowNullResultWithState(nameof(GetForValueAsObjectCore));
var expected = context.ExpectedPgTypeId;
if (result is not null && expected.HasValue && result.PgTypeId != Nullable.GetValueRefOrDefaultRef(in expected))
ThrowPgTypeIdMismatch(nameof(GetForValueAsObjectCore));
return result;
}
catch
{
// Safety net mirroring PgConverter.Bind's envelope: a Core that produces partial state and
// then throws (or whose result trips post-call validation) leaves writeState referencing an
// orphaned resource. Dispose and null before propagating so every caller sees uniform
// "out param is null on throw" semantics regardless of where in the producer chain it failed.
// Null first, then dispose: a throwing Dispose must not leave the caller's slot pointing
// at a half-disposed object — they'd dispose it again.
(var toDispose, writeState) = (writeState, null);
(toDispose as IDisposable)?.Dispose();
throw;
}
}
/// <summary>
/// Gets the default concrete type info for a given PgTypeId.
/// </summary>
/// <remarks>
/// Implementations should not return new instances of the possible infos that can be returned, instead its expected these are cached once returned.
/// Composing providers depend on this to cache their own infos - wrapping the element info - with the cache key being the element info reference.
/// </remarks>
protected abstract PgConcreteTypeInfo GetDefaultCore(PgTypeId? pgTypeId);
/// <summary>
/// Gets the concrete type info for a given field context.
/// </summary>
/// <remarks>
/// Implementations should not return new instances of the possible infos that can be returned, instead its expected these are cached once returned.
/// Composing providers depend on this to cache their own infos - wrapping the element info - with the cache key being the element info reference.
/// </remarks>
protected virtual PgConcreteTypeInfo? GetForFieldCore(in ProviderFieldContext context) => null;
internal abstract Type TypeToConvert { get; }
/// <summary>
/// Whether dispatched concretes from this provider may have a <see cref="PgTypeInfo.Type"/> that varies along the
/// <see cref="TypeToConvert"/> subtype chain. Defaults to <see langword="false"/>: providers are canonical unless
/// they explicitly opt in. Polymorphic providers that dispatch to varied concretes per call must override to
/// <see langword="true"/>.
/// </summary>
internal virtual bool AllowConcreteVariance => false;
/// <summary>
/// Whether this provider is part of the framework's own resolution mechanism rather than plugin-authored code.
/// Providers are dual-natured — extensible surface plus tier-2 resolution mechanism — and this flag lets the
/// framework label its own composing infrastructure to skip self-validation without affecting the surface that
/// plugins extend.
/// </summary>
/// <remarks>
/// Compare to the cache layer (tier-1): the cache is purely mechanism, not extensible surface, so it doesn't need
/// an analogous flag — the whole class is framework-only by construction.
/// </remarks>
internal bool IsInternalProvider { get; private protected init; }
private protected abstract PgConcreteTypeInfo? GetForValueAsObjectCore(in ProviderValueContext context, object? value, ref object? writeState);
private protected static void ThrowPgTypeIdMismatch(string methodName)
=> throw new InvalidOperationException(
$"'{methodName}' incorrectly returned a different {nameof(PgTypeId)} in its concrete type info than the caller passed in.");
private protected static void ThrowNullResultWithState(string methodName)
=> throw new InvalidOperationException(
$"'{methodName}' returned null (signalling fall-back to default) but also produced write state. Returning null is reserved for delegation; state production requires a non-null result.");
}
[Experimental(NpgsqlDiagnostics.ConvertersExperimental)]
public abstract class PgConcreteTypeInfoProvider<T> : PgConcreteTypeInfoProvider
{
/// <summary>
/// Gets the appropriate type info based on the given value and expected type id.
/// </summary>
public PgConcreteTypeInfo? GetForValue(in ProviderValueContext context, T? value, out object? writeState)
{
writeState = null;
try
{
var result = GetForValueCore(context, value, ref writeState);
if (result is null && writeState is not null)
ThrowNullResultWithState(nameof(GetForValueCore));
var expected = context.ExpectedPgTypeId;
if (result is not null && expected.HasValue && result.PgTypeId != Nullable.GetValueRefOrDefaultRef(in expected))
ThrowPgTypeIdMismatch(nameof(GetForValueCore));
return result;
}
catch
{
// Null first, then dispose: a throwing Dispose must not leave the caller's slot
// pointing at a half-disposed object — they'd dispose it again.
(var toDispose, writeState) = (writeState, null);
(toDispose as IDisposable)?.Dispose();
throw;
}
}
/// <summary>
/// Gets the concrete type info for a given value and expected type id.
/// </summary>
/// <remarks>
/// Implementations should not return new instances of the possible infos that can be returned, instead its expected these are cached once returned.
/// Composing providers depend on this to cache their own infos - wrapping the element info - with the cache key being the element info reference.
/// </remarks>
protected abstract PgConcreteTypeInfo? GetForValueCore(in ProviderValueContext context, T? value, ref object? writeState);
internal sealed override Type TypeToConvert => typeof(T);
// If null was passed while it is not a valid value for T we directly return null.
// This allows concrete info to be produced by falling back to GetDefault afterwards.
private protected sealed override PgConcreteTypeInfo? GetForValueAsObjectCore(in ProviderValueContext context, object? value, ref object? writeState)
=> default(T) is null || value is not null ? GetForValueCore(context, (T?)value, ref writeState) : null;
}
[Experimental(NpgsqlDiagnostics.ConvertersExperimental)]
public readonly struct ProviderValueContext
{
public PgTypeId? ExpectedPgTypeId { get; init; }
public NestedObjectDbNullHandling NestedObjectDbNullHandling { get; init; }
}
/// <summary>
/// The field metadata a provider dispatches on when resolving a concrete type info for a read. The expected
/// PG type id is not carried here — read-side resolution always yields a decided info that knows its own id,
/// so a context copy would be redundant. Providers that need the id at dispatch time read their own posted id.
/// </summary>
[Experimental(NpgsqlDiagnostics.ConvertersExperimental)]
public readonly struct ProviderFieldContext
{
public ProviderFieldContext() { }
public string? Name { get; init; }
public int TypeModifier { get; init; } = -1;
}
[Experimental(NpgsqlDiagnostics.ConvertersExperimental)]
static class PgConcreteTypeInfoProviderExtensions
{
extension(PgConcreteTypeInfoProvider provider)
{
internal PgConcreteTypeInfo? GetForValueAsNestedObject(in ProviderValueContext context, object? value, out object? writeState)
{
writeState = null;
switch (context.NestedObjectDbNullHandling)
{
case NestedObjectDbNullHandling.ExtendedThrowOnNull:
if (value is null)
ThrowHelper.ThrowArgumentNullException("Object-typed value cannot be null, a db null value must be used instead.", nameof(value));
goto case NestedObjectDbNullHandling.Extended;
case NestedObjectDbNullHandling.Extended:
if (value is DBNull)
return null;
goto case NestedObjectDbNullHandling.Default;
case NestedObjectDbNullHandling.Default:
return value is null ? null : provider.GetForValueAsObject(context, value, out writeState);
default:
ThrowHelper.ThrowUnreachableException();
return default;
}
}
}
}