3
3
using JetBrains . Annotations ;
4
4
using Newtonsoft . Json ;
5
5
using Newtonsoft . Json . Serialization ;
6
+ using SysNotNull = System . Diagnostics . CodeAnalysis . NotNullAttribute ;
6
7
7
8
namespace JsonApiDotNetCore . OpenApi . Client ;
8
9
@@ -23,7 +24,7 @@ protected void SetSerializerSettingsForJsonApi(JsonSerializerSettings settings)
23
24
}
24
25
25
26
/// <inheritdoc />
26
- public IDisposable RegisterAttributesForRequestDocument < TRequestDocument , TAttributesObject > ( TRequestDocument requestDocument ,
27
+ public IDisposable OmitDefaultValuesForAttributesInRequestDocument < TRequestDocument , TAttributesObject > ( TRequestDocument requestDocument ,
27
28
params Expression < Func < TAttributesObject , object ? > > [ ] alwaysIncludedAttributeSelectors )
28
29
where TRequestDocument : class
29
30
{
@@ -43,9 +44,10 @@ public IDisposable RegisterAttributesForRequestDocument<TRequestDocument, TAttri
43
44
}
44
45
}
45
46
46
- _jsonApiJsonConverter . RegisterRequestDocument ( requestDocument , new AttributeNamesContainer ( attributeNames , typeof ( TAttributesObject ) ) ) ;
47
+ _jsonApiJsonConverter . RegisterRequestDocumentForAttributesOmission ( requestDocument ,
48
+ new AttributesObjectInfo ( attributeNames , typeof ( TAttributesObject ) ) ) ;
47
49
48
- return new AttributesRegistrationScope ( _jsonApiJsonConverter , requestDocument ) ;
50
+ return new RequestDocumentRegistrationScope ( _jsonApiJsonConverter , requestDocument ) ;
49
51
}
50
52
51
53
private static Expression RemoveConvert ( Expression expression )
@@ -67,38 +69,38 @@ private static Expression RemoveConvert(Expression expression)
67
69
68
70
private sealed class JsonApiJsonConverter : JsonConverter
69
71
{
70
- private readonly Dictionary < object , AttributeNamesContainer > _alwaysIncludedAttributesPerRequestDocumentInstance = new ( ) ;
71
- private readonly Dictionary < Type , ISet < object > > _requestDocumentInstancesPerRequestDocumentType = new ( ) ;
72
- private bool _isSerializing ;
72
+ private readonly Dictionary < object , AttributesObjectInfo > _attributesObjectInfoByRequestDocument = new ( ) ;
73
+ private readonly Dictionary < Type , ISet < object > > _requestDocumentsByType = new ( ) ;
74
+ private SerializationScope ? _serializationScope ;
73
75
74
76
public override bool CanRead => false ;
75
77
76
- public void RegisterRequestDocument ( object requestDocument , AttributeNamesContainer attributes )
78
+ public void RegisterRequestDocumentForAttributesOmission ( object requestDocument , AttributesObjectInfo attributesObjectInfo )
77
79
{
78
- _alwaysIncludedAttributesPerRequestDocumentInstance [ requestDocument ] = attributes ;
80
+ _attributesObjectInfoByRequestDocument [ requestDocument ] = attributesObjectInfo ;
79
81
80
82
Type requestDocumentType = requestDocument . GetType ( ) ;
81
83
82
- if ( ! _requestDocumentInstancesPerRequestDocumentType . ContainsKey ( requestDocumentType ) )
84
+ if ( ! _requestDocumentsByType . ContainsKey ( requestDocumentType ) )
83
85
{
84
- _requestDocumentInstancesPerRequestDocumentType [ requestDocumentType ] = new HashSet < object > ( ) ;
86
+ _requestDocumentsByType [ requestDocumentType ] = new HashSet < object > ( ) ;
85
87
}
86
88
87
- _requestDocumentInstancesPerRequestDocumentType [ requestDocumentType ] . Add ( requestDocument ) ;
89
+ _requestDocumentsByType [ requestDocumentType ] . Add ( requestDocument ) ;
88
90
}
89
91
90
- public void RemoveAttributeRegistration ( object requestDocument )
92
+ public void RemoveRegistration ( object requestDocument )
91
93
{
92
- if ( _alwaysIncludedAttributesPerRequestDocumentInstance . ContainsKey ( requestDocument ) )
94
+ if ( _attributesObjectInfoByRequestDocument . ContainsKey ( requestDocument ) )
93
95
{
94
- _alwaysIncludedAttributesPerRequestDocumentInstance . Remove ( requestDocument ) ;
96
+ _attributesObjectInfoByRequestDocument . Remove ( requestDocument ) ;
95
97
96
98
Type requestDocumentType = requestDocument . GetType ( ) ;
97
- _requestDocumentInstancesPerRequestDocumentType [ requestDocumentType ] . Remove ( requestDocument ) ;
99
+ _requestDocumentsByType [ requestDocumentType ] . Remove ( requestDocument ) ;
98
100
99
- if ( ! _requestDocumentInstancesPerRequestDocumentType [ requestDocumentType ] . Any ( ) )
101
+ if ( ! _requestDocumentsByType [ requestDocumentType ] . Any ( ) )
100
102
{
101
- _requestDocumentInstancesPerRequestDocumentType . Remove ( requestDocumentType ) ;
103
+ _requestDocumentsByType . Remove ( requestDocumentType ) ;
102
104
}
103
105
}
104
106
}
@@ -107,71 +109,195 @@ public override bool CanConvert(Type objectType)
107
109
{
108
110
ArgumentGuard . NotNull ( objectType ) ;
109
111
110
- return ! _isSerializing && _requestDocumentInstancesPerRequestDocumentType . ContainsKey ( objectType ) ;
112
+ if ( _serializationScope == null )
113
+ {
114
+ return _requestDocumentsByType . ContainsKey ( objectType ) ;
115
+ }
116
+
117
+ return _serializationScope . ShouldConvertAsAttributesObject ( objectType ) ;
111
118
}
112
119
113
120
public override object ReadJson ( JsonReader reader , Type objectType , object ? existingValue , JsonSerializer serializer )
114
121
{
115
- throw new Exception ( "This code should not be reachable." ) ;
122
+ throw new UnreachableCodeException ( ) ;
116
123
}
117
124
118
125
public override void WriteJson ( JsonWriter writer , object ? value , JsonSerializer serializer )
119
126
{
120
127
ArgumentGuard . NotNull ( writer ) ;
128
+ ArgumentGuard . NotNull ( value ) ;
121
129
ArgumentGuard . NotNull ( serializer ) ;
122
130
123
- if ( value ! = null )
131
+ if ( _serializationScope = = null )
124
132
{
125
- if ( _alwaysIncludedAttributesPerRequestDocumentInstance . ContainsKey ( value ) )
126
- {
127
- AttributeNamesContainer attributeNamesContainer = _alwaysIncludedAttributesPerRequestDocumentInstance [ value ] ;
128
- serializer . ContractResolver = new JsonApiDocumentContractResolver ( attributeNamesContainer ) ;
129
- }
133
+ AssertObjectIsRequestDocument ( value ) ;
130
134
131
- try
132
- {
133
- _isSerializing = true ;
134
- serializer . Serialize ( writer , value ) ;
135
- }
136
- finally
135
+ SerializeRequestDocument ( writer , value , serializer ) ;
136
+ }
137
+ else
138
+ {
139
+ AttributesObjectInfo ? attributesObjectInfo = _serializationScope . AttributesObjectInScope ;
140
+
141
+ AssertObjectMatchesSerializationScope ( attributesObjectInfo , value ) ;
142
+
143
+ SerializeAttributesObject ( attributesObjectInfo , writer , value , serializer ) ;
144
+ }
145
+ }
146
+
147
+ private void AssertObjectIsRequestDocument ( object value )
148
+ {
149
+ Type objectType = value . GetType ( ) ;
150
+
151
+ if ( ! _requestDocumentsByType . ContainsKey ( objectType ) )
152
+ {
153
+ throw new UnreachableCodeException ( ) ;
154
+ }
155
+ }
156
+
157
+ private void SerializeRequestDocument ( JsonWriter writer , object value , JsonSerializer serializer )
158
+ {
159
+ _serializationScope = new SerializationScope ( ) ;
160
+
161
+ if ( _attributesObjectInfoByRequestDocument . TryGetValue ( value , out AttributesObjectInfo ? attributesObjectInfo ) )
162
+ {
163
+ _serializationScope . AttributesObjectInScope = attributesObjectInfo ;
164
+ }
165
+
166
+ try
167
+ {
168
+ serializer . Serialize ( writer , value ) ;
169
+ }
170
+ finally
171
+ {
172
+ _serializationScope = null ;
173
+ }
174
+ }
175
+
176
+ private static void AssertObjectMatchesSerializationScope ( [ SysNotNull ] AttributesObjectInfo ? attributesObjectInfo , object value )
177
+ {
178
+ Type objectType = value . GetType ( ) ;
179
+
180
+ if ( attributesObjectInfo == null || ! attributesObjectInfo . MatchesType ( objectType ) )
181
+ {
182
+ throw new UnreachableCodeException ( ) ;
183
+ }
184
+ }
185
+
186
+ private static void SerializeAttributesObject ( AttributesObjectInfo alwaysIncludedAttributes , JsonWriter writer , object value , JsonSerializer serializer )
187
+ {
188
+ AssertRequiredPropertiesAreNotExcluded ( value , alwaysIncludedAttributes , writer ) ;
189
+
190
+ serializer . ContractResolver = new JsonApiAttributeContractResolver ( alwaysIncludedAttributes ) ;
191
+ serializer . Serialize ( writer , value ) ;
192
+ }
193
+
194
+ private static void AssertRequiredPropertiesAreNotExcluded ( object value , AttributesObjectInfo alwaysIncludedAttributes , JsonWriter jsonWriter )
195
+ {
196
+ PropertyInfo [ ] propertyInfos = value . GetType ( ) . GetProperties ( ) ;
197
+
198
+ foreach ( PropertyInfo attributesPropertyInfo in propertyInfos )
199
+ {
200
+ bool isExplicitlyIncluded = alwaysIncludedAttributes . IsAttributeMarkedForInclusion ( attributesPropertyInfo . Name ) ;
201
+
202
+ if ( isExplicitlyIncluded )
137
203
{
138
- _isSerializing = false ;
204
+ return ;
139
205
}
206
+
207
+ AssertRequiredPropertyIsNotIgnored ( value , attributesPropertyInfo , jsonWriter . Path ) ;
208
+ }
209
+ }
210
+
211
+ private static void AssertRequiredPropertyIsNotIgnored ( object value , PropertyInfo attribute , string path )
212
+ {
213
+ JsonPropertyAttribute jsonPropertyForAttribute = attribute . GetCustomAttributes < JsonPropertyAttribute > ( ) . Single ( ) ;
214
+
215
+ if ( jsonPropertyForAttribute . Required != Required . Always )
216
+ {
217
+ return ;
218
+ }
219
+
220
+ bool isPropertyIgnored = DefaultValueEqualsCurrentValue ( attribute , value ) ;
221
+
222
+ if ( isPropertyIgnored )
223
+ {
224
+ throw new JsonSerializationException (
225
+ $ "Ignored property '{ jsonPropertyForAttribute . PropertyName } ' must have a value because it is required. Path '{ path } '.") ;
140
226
}
141
227
}
228
+
229
+ private static bool DefaultValueEqualsCurrentValue ( PropertyInfo propertyInfo , object instance )
230
+ {
231
+ object ? currentValue = propertyInfo . GetValue ( instance ) ;
232
+ object ? defaultValue = GetDefaultValue ( propertyInfo . PropertyType ) ;
233
+
234
+ if ( defaultValue == null )
235
+ {
236
+ return currentValue == null ;
237
+ }
238
+
239
+ return defaultValue . Equals ( currentValue ) ;
240
+ }
241
+
242
+ private static object ? GetDefaultValue ( Type type )
243
+ {
244
+ return type . IsValueType ? Activator . CreateInstance ( type ) : null ;
245
+ }
246
+ }
247
+
248
+ private sealed class SerializationScope
249
+ {
250
+ private bool _isFirstAttemptToConvertAttributes = true ;
251
+ public AttributesObjectInfo ? AttributesObjectInScope { get ; set ; }
252
+
253
+ public bool ShouldConvertAsAttributesObject ( Type type )
254
+ {
255
+ if ( ! _isFirstAttemptToConvertAttributes || AttributesObjectInScope == null )
256
+ {
257
+ return false ;
258
+ }
259
+
260
+ if ( ! AttributesObjectInScope . MatchesType ( type ) )
261
+ {
262
+ return false ;
263
+ }
264
+
265
+ _isFirstAttemptToConvertAttributes = false ;
266
+ return true ;
267
+ }
142
268
}
143
269
144
- private sealed class AttributeNamesContainer
270
+ private sealed class AttributesObjectInfo
145
271
{
146
- private readonly ISet < string > _attributeNames ;
147
- private readonly Type _containerType ;
272
+ private readonly ISet < string > _attributesMarkedForInclusion ;
273
+ private readonly Type _attributesObjectType ;
148
274
149
- public AttributeNamesContainer ( ISet < string > attributeNames , Type containerType )
275
+ public AttributesObjectInfo ( ISet < string > attributesMarkedForInclusion , Type attributesObjectType )
150
276
{
151
- ArgumentGuard . NotNull ( attributeNames ) ;
152
- ArgumentGuard . NotNull ( containerType ) ;
277
+ ArgumentGuard . NotNull ( attributesMarkedForInclusion ) ;
278
+ ArgumentGuard . NotNull ( attributesObjectType ) ;
153
279
154
- _attributeNames = attributeNames ;
155
- _containerType = containerType ;
280
+ _attributesMarkedForInclusion = attributesMarkedForInclusion ;
281
+ _attributesObjectType = attributesObjectType ;
156
282
}
157
283
158
- public bool ContainsAttribute ( string name )
284
+ public bool IsAttributeMarkedForInclusion ( string name )
159
285
{
160
- return _attributeNames . Contains ( name ) ;
286
+ return _attributesMarkedForInclusion . Contains ( name ) ;
161
287
}
162
288
163
- public bool ContainerMatchesType ( Type type )
289
+ public bool MatchesType ( Type type )
164
290
{
165
- return _containerType == type ;
291
+ return _attributesObjectType == type ;
166
292
}
167
293
}
168
294
169
- private sealed class AttributesRegistrationScope : IDisposable
295
+ private sealed class RequestDocumentRegistrationScope : IDisposable
170
296
{
171
297
private readonly JsonApiJsonConverter _jsonApiJsonConverter ;
172
298
private readonly object _requestDocument ;
173
299
174
- public AttributesRegistrationScope ( JsonApiJsonConverter jsonApiJsonConverter , object requestDocument )
300
+ public RequestDocumentRegistrationScope ( JsonApiJsonConverter jsonApiJsonConverter , object requestDocument )
175
301
{
176
302
ArgumentGuard . NotNull ( jsonApiJsonConverter ) ;
177
303
ArgumentGuard . NotNull ( requestDocument ) ;
@@ -182,28 +308,28 @@ public AttributesRegistrationScope(JsonApiJsonConverter jsonApiJsonConverter, ob
182
308
183
309
public void Dispose ( )
184
310
{
185
- _jsonApiJsonConverter . RemoveAttributeRegistration ( _requestDocument ) ;
311
+ _jsonApiJsonConverter . RemoveRegistration ( _requestDocument ) ;
186
312
}
187
313
}
188
314
189
- private sealed class JsonApiDocumentContractResolver : DefaultContractResolver
315
+ private sealed class JsonApiAttributeContractResolver : DefaultContractResolver
190
316
{
191
- private readonly AttributeNamesContainer _attributeNamesContainer ;
317
+ private readonly AttributesObjectInfo _attributesObjectInfo ;
192
318
193
- public JsonApiDocumentContractResolver ( AttributeNamesContainer attributeNamesContainer )
319
+ public JsonApiAttributeContractResolver ( AttributesObjectInfo attributesObjectInfo )
194
320
{
195
- ArgumentGuard . NotNull ( attributeNamesContainer ) ;
321
+ ArgumentGuard . NotNull ( attributesObjectInfo ) ;
196
322
197
- _attributeNamesContainer = attributeNamesContainer ;
323
+ _attributesObjectInfo = attributesObjectInfo ;
198
324
}
199
325
200
326
protected override JsonProperty CreateProperty ( MemberInfo member , MemberSerialization memberSerialization )
201
327
{
202
328
JsonProperty property = base . CreateProperty ( member , memberSerialization ) ;
203
329
204
- if ( _attributeNamesContainer . ContainerMatchesType ( property . DeclaringType ! ) )
330
+ if ( _attributesObjectInfo . MatchesType ( property . DeclaringType ! ) )
205
331
{
206
- if ( _attributeNamesContainer . ContainsAttribute ( property . UnderlyingName ! ) )
332
+ if ( _attributesObjectInfo . IsAttributeMarkedForInclusion ( property . UnderlyingName ! ) )
207
333
{
208
334
property . NullValueHandling = NullValueHandling . Include ;
209
335
property . DefaultValueHandling = DefaultValueHandling . Include ;
0 commit comments