Skip to content

Commit 149af4d

Browse files
author
Bart Koelman
committed
Wire-up callbacks for serialization
1 parent bf1072b commit 149af4d

16 files changed

+326
-157
lines changed

benchmarks/Serialization/JsonApiSerializerBenchmarks.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ public JsonApiSerializerBenchmarks()
3636

3737
var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, new ResourceObjectBuilderSettings());
3838

39-
_jsonApiSerializer = new ResponseSerializer<BenchmarkResource>(metaBuilder, linkBuilder,
40-
includeBuilder, fieldsToSerialize, resourceObjectBuilder, options);
39+
IResourceDefinitionAccessor resourceDefinitionAccessor = new Mock<IResourceDefinitionAccessor>().Object;
40+
41+
_jsonApiSerializer = new ResponseSerializer<BenchmarkResource>(metaBuilder, linkBuilder, includeBuilder, fieldsToSerialize, resourceObjectBuilder,
42+
resourceDefinitionAccessor, options);
4143
}
4244

4345
private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resourceGraph)

src/JsonApiDotNetCore/Resources/IResourceDefinition.cs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public interface IResourceDefinition<TResource, TId>
160160
/// <summary>
161161
/// Executes before replacing (overwriting) a to-one relationship.
162162
/// <para>
163-
/// Implementing this method enables to perform validations and change <see cref="rightResourceId" />, before the relationship is updated.
163+
/// Implementing this method enables to perform validations and change <paramref name="rightResourceId" />, before the relationship is updated.
164164
/// </para>
165165
/// </summary>
166166
/// <param name="leftResource">
@@ -269,8 +269,8 @@ Task OnRemoveFromRelationshipAsync(TResource leftResource, HasManyAttribute hasM
269269
/// <param name="resource">
270270
/// The original resource retrieved from the underlying data store (or a freshly instantiated resource in case of a POST resource request), updated with
271271
/// the changes from the incoming request. Exception: In case <paramref name="operationKind" /> is <see cref="OperationKind.DeleteResource" /> or
272-
/// <see cref="OperationKind.AddToRelationship" />, this is an empty object with only the <see cref="Identifiable.Id" /> property set, because for those
273-
/// endpoints no resource is retrieved upfront.
272+
/// <see cref="OperationKind.AddToRelationship" />, this is an empty object with only the <see cref="Identifiable{T}.Id" /> property set, because for
273+
/// those endpoints no resource is retrieved upfront.
274274
/// </param>
275275
/// <param name="operationKind">
276276
/// Identifies from which endpoint this method was called. Possible values: <see cref="OperationKind.CreateResource" />,
@@ -300,5 +300,37 @@ Task OnRemoveFromRelationshipAsync(TResource leftResource, HasManyAttribute hasM
300300
/// Propagates notification that request handling should be canceled.
301301
/// </param>
302302
Task OnWriteSucceededAsync(TResource resource, OperationKind operationKind, CancellationToken cancellationToken);
303+
304+
/// <summary>
305+
/// Executes after a resource has been deserialized from an incoming request body.
306+
/// </summary>
307+
/// <para>
308+
/// Implementing this method enables to change the incoming resource before it enters an ASP.NET Controller Action method.
309+
/// </para>
310+
/// <para>
311+
/// Changing attributes on <paramref name="resource" /> from this method may break detection of side effects on resource POST/PATCH requests, because
312+
/// side effect detection considers any changes done from this method to be part of the incoming request body. So setting additional attributes from this
313+
/// method (that were not sent by the client) are not considered side effects, resulting in incorrectly reporting that there were no side effects.
314+
/// </para>
315+
/// <param name="resource">
316+
/// The deserialized resource.
317+
/// </param>
318+
void OnDeserialize(TResource resource);
319+
320+
/// <summary>
321+
/// Executes before a (primary or included) resource is serialized into an outgoing response body.
322+
/// </summary>
323+
/// <para>
324+
/// Implementing this method enables to change the returned resource, for example scrub sensitive data or transform returned attribute values.
325+
/// </para>
326+
/// <para>
327+
/// Changing attributes on <paramref name="resource" /> from this method may break detection of side effects on resource POST/PATCH requests. What this
328+
/// means is that if side effects were detected before, this is not re-evaluated after running this method, so it may incorrectly report side effects if
329+
/// they were undone by this method.
330+
/// </para>
331+
/// <param name="resource">
332+
/// The serialized resource.
333+
/// </param>
334+
void OnSerialize(TResource resource);
303335
}
304336
}

src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,15 @@ Task OnWritingAsync<TResource>(TResource resource, OperationKind operationKind,
9595
/// </summary>
9696
Task OnWriteSucceededAsync<TResource>(TResource resource, OperationKind operationKind, CancellationToken cancellationToken)
9797
where TResource : class, IIdentifiable;
98+
99+
/// <summary>
100+
/// Invokes <see cref="IResourceDefinition{TResource,TId}.OnDeserialize" /> for the specified resource.
101+
/// </summary>
102+
void OnDeserialize(IIdentifiable resource);
103+
104+
/// <summary>
105+
/// Invokes <see cref="IResourceDefinition{TResource,TId}.OnSerialize" /> for the specified resource.
106+
/// </summary>
107+
void OnSerialize(IIdentifiable resource);
98108
}
99109
}

src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,16 @@ public virtual Task OnWriteSucceededAsync(TResource resource, OperationKind oper
162162
return Task.CompletedTask;
163163
}
164164

165+
/// <inheritdoc />
166+
public virtual void OnDeserialize(TResource resource)
167+
{
168+
}
169+
170+
/// <inheritdoc />
171+
public virtual void OnSerialize(TResource resource)
172+
{
173+
}
174+
165175
/// <summary>
166176
/// This is an alias type intended to simplify the implementation's method signature. See <see cref="CreateSortExpressionFromLambda" /> for usage
167177
/// details.

src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,24 @@ public async Task OnWriteSucceededAsync<TResource>(TResource resource, Operation
173173
await resourceDefinition.OnWriteSucceededAsync(resource, operationKind, cancellationToken);
174174
}
175175

176+
/// <inheritdoc />
177+
public void OnDeserialize(IIdentifiable resource)
178+
{
179+
ArgumentGuard.NotNull(resource, nameof(resource));
180+
181+
dynamic resourceDefinition = ResolveResourceDefinition(resource.GetType());
182+
resourceDefinition.OnDeserialize((dynamic)resource);
183+
}
184+
185+
/// <inheritdoc />
186+
public void OnSerialize(IIdentifiable resource)
187+
{
188+
ArgumentGuard.NotNull(resource, nameof(resource));
189+
190+
dynamic resourceDefinition = ResolveResourceDefinition(resource.GetType());
191+
resourceDefinition.OnSerialize((dynamic)resource);
192+
}
193+
176194
protected virtual object ResolveResourceDefinition(Type resourceType)
177195
{
178196
ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType);

src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,28 @@ public sealed class AtomicOperationsResponseSerializer : BaseSerializer, IJsonAp
2020
private readonly IMetaBuilder _metaBuilder;
2121
private readonly ILinkBuilder _linkBuilder;
2222
private readonly IFieldsToSerialize _fieldsToSerialize;
23+
private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor;
2324
private readonly IJsonApiRequest _request;
2425
private readonly IJsonApiOptions _options;
2526

2627
/// <inheritdoc />
2728
public string ContentType { get; } = HeaderConstants.AtomicOperationsMediaType;
2829

2930
public AtomicOperationsResponseSerializer(IResourceObjectBuilder resourceObjectBuilder, IMetaBuilder metaBuilder, ILinkBuilder linkBuilder,
30-
IFieldsToSerialize fieldsToSerialize, IJsonApiRequest request, IJsonApiOptions options)
31+
IFieldsToSerialize fieldsToSerialize, IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiRequest request, IJsonApiOptions options)
3132
: base(resourceObjectBuilder)
3233
{
3334
ArgumentGuard.NotNull(metaBuilder, nameof(metaBuilder));
3435
ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder));
3536
ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize));
37+
ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor));
3638
ArgumentGuard.NotNull(request, nameof(request));
3739
ArgumentGuard.NotNull(options, nameof(options));
3840

3941
_metaBuilder = metaBuilder;
4042
_linkBuilder = linkBuilder;
4143
_fieldsToSerialize = fieldsToSerialize;
44+
_resourceDefinitionAccessor = resourceDefinitionAccessor;
4245
_request = request;
4346
_options = options;
4447
}
@@ -79,6 +82,8 @@ private AtomicResultObject SerializeOperation(OperationContainer operation)
7982
_request.CopyFrom(operation.Request);
8083
_fieldsToSerialize.ResetCache();
8184

85+
_resourceDefinitionAccessor.OnSerialize(operation.Resource);
86+
8287
Type resourceType = operation.Resource.GetType();
8388
IReadOnlyCollection<AttrAttribute> attributes = _fieldsToSerialize.GetAttributes(resourceType);
8489
IReadOnlyCollection<RelationshipAttribute> relationships = _fieldsToSerialize.GetRelationships(resourceType);

src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,14 @@ private void ProcessChain(object related, IList<RelationshipAttribute> inclusion
135135

136136
private void ProcessRelationship(IIdentifiable parent, IList<RelationshipAttribute> inclusionChain)
137137
{
138-
// get the resource object for parent.
139-
ResourceObject resourceObject = GetOrBuildResourceObject(parent);
138+
ResourceObject resourceObject = TryGetBuiltResourceObjectFor(parent);
139+
140+
if (resourceObject == null)
141+
{
142+
_resourceDefinitionAccessor.OnSerialize(parent);
143+
144+
resourceObject = BuildCachedResourceObjectFor(parent);
145+
}
140146

141147
if (!inclusionChain.Any())
142148
{
@@ -188,23 +194,25 @@ protected override RelationshipEntry GetRelationshipData(RelationshipAttribute r
188194
};
189195
}
190196

191-
/// <summary>
192-
/// Gets the resource object for <paramref name="parent" /> by searching the included list. If it was not already built, it is constructed and added to
193-
/// the inclusion list.
194-
/// </summary>
195-
private ResourceObject GetOrBuildResourceObject(IIdentifiable parent)
197+
private ResourceObject TryGetBuiltResourceObjectFor(IIdentifiable resource)
196198
{
197-
Type type = parent.GetType();
198-
string resourceName = ResourceContextProvider.GetResourceContext(type).PublicName;
199-
ResourceObject entry = _included.SingleOrDefault(ro => ro.Type == resourceName && ro.Id == parent.StringId);
199+
Type resourceType = resource.GetType();
200+
ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(resourceType);
200201

201-
if (entry == null)
202-
{
203-
entry = Build(parent, _fieldsToSerialize.GetAttributes(type), _fieldsToSerialize.GetRelationships(type));
204-
_included.Add(entry);
205-
}
202+
return _included.SingleOrDefault(resourceObject => resourceObject.Type == resourceContext.PublicName && resourceObject.Id == resource.StringId);
203+
}
204+
205+
private ResourceObject BuildCachedResourceObjectFor(IIdentifiable resource)
206+
{
207+
Type resourceType = resource.GetType();
208+
IReadOnlyCollection<AttrAttribute> attributes = _fieldsToSerialize.GetAttributes(resourceType);
209+
IReadOnlyCollection<RelationshipAttribute> relationships = _fieldsToSerialize.GetRelationships(resourceType);
206210

207-
return entry;
211+
ResourceObject resourceObject = Build(resource, attributes, relationships);
212+
213+
_included.Add(resourceObject);
214+
215+
return resourceObject;
208216
}
209217
}
210218
}

src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class RequestDeserializer : BaseDeserializer, IJsonApiDeserializer
2626
private readonly IHttpContextAccessor _httpContextAccessor;
2727
private readonly IJsonApiRequest _request;
2828
private readonly IJsonApiOptions _options;
29+
private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor;
2930

3031
public RequestDeserializer(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory, ITargetedFields targetedFields,
3132
IHttpContextAccessor httpContextAccessor, IJsonApiRequest request, IJsonApiOptions options)
@@ -40,6 +41,10 @@ public RequestDeserializer(IResourceContextProvider resourceContextProvider, IRe
4041
_httpContextAccessor = httpContextAccessor;
4142
_request = request;
4243
_options = options;
44+
45+
#pragma warning disable 612 // Method is obsolete
46+
_resourceDefinitionAccessor = resourceFactory.GetResourceDefinitionAccessor();
47+
#pragma warning restore 612
4348
}
4449

4550
/// <inheritdoc />
@@ -59,6 +64,11 @@ public object Deserialize(string body)
5964

6065
object instance = DeserializeBody(body);
6166

67+
if (instance is IIdentifiable resource)
68+
{
69+
_resourceDefinitionAccessor.OnDeserialize(resource);
70+
}
71+
6272
AssertResourceIdIsNotTargeted(_targetedFields);
6373

6474
return instance;
@@ -204,6 +214,8 @@ private OperationContainer ParseForCreateOrUpdateResourceOperation(AtomicOperati
204214

205215
IIdentifiable primaryResource = ParseResourceObject(operation.SingleData);
206216

217+
_resourceDefinitionAccessor.OnDeserialize(primaryResource);
218+
207219
request.PrimaryId = primaryResource.StringId;
208220
_request.CopyFrom(request);
209221

src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,30 @@ public class ResponseSerializer<TResource> : BaseSerializer, IJsonApiSerializer
3131
private readonly ILinkBuilder _linkBuilder;
3232
private readonly IIncludedResourceObjectBuilder _includedBuilder;
3333
private readonly IFieldsToSerialize _fieldsToSerialize;
34+
private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor;
3435
private readonly IJsonApiOptions _options;
3536
private readonly Type _primaryResourceType;
3637

3738
/// <inheritdoc />
3839
public string ContentType { get; } = HeaderConstants.MediaType;
3940

4041
public ResponseSerializer(IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder,
41-
IFieldsToSerialize fieldsToSerialize, IResourceObjectBuilder resourceObjectBuilder, IJsonApiOptions options)
42+
IFieldsToSerialize fieldsToSerialize, IResourceObjectBuilder resourceObjectBuilder, IResourceDefinitionAccessor resourceDefinitionAccessor,
43+
IJsonApiOptions options)
4244
: base(resourceObjectBuilder)
4345
{
4446
ArgumentGuard.NotNull(metaBuilder, nameof(metaBuilder));
4547
ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder));
4648
ArgumentGuard.NotNull(includedBuilder, nameof(includedBuilder));
4749
ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize));
50+
ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor));
4851
ArgumentGuard.NotNull(options, nameof(options));
4952

5053
_metaBuilder = metaBuilder;
5154
_linkBuilder = linkBuilder;
5255
_includedBuilder = includedBuilder;
5356
_fieldsToSerialize = fieldsToSerialize;
57+
_resourceDefinitionAccessor = resourceDefinitionAccessor;
5458
_options = options;
5559
_primaryResourceType = typeof(TResource);
5660
}
@@ -92,6 +96,11 @@ private string SerializeErrorDocument(ErrorDocument errorDocument)
9296
/// </remarks>
9397
internal string SerializeSingle(IIdentifiable resource)
9498
{
99+
if (resource != null)
100+
{
101+
_resourceDefinitionAccessor.OnSerialize(resource);
102+
}
103+
95104
IReadOnlyCollection<AttrAttribute> attributes = _fieldsToSerialize.GetAttributes(_primaryResourceType);
96105
IReadOnlyCollection<RelationshipAttribute> relationships = _fieldsToSerialize.GetRelationships(_primaryResourceType);
97106

@@ -119,6 +128,11 @@ internal string SerializeSingle(IIdentifiable resource)
119128
/// </remarks>
120129
internal string SerializeMany(IReadOnlyCollection<IIdentifiable> resources)
121130
{
131+
foreach (IIdentifiable resource in resources)
132+
{
133+
_resourceDefinitionAccessor.OnSerialize(resource);
134+
}
135+
122136
IReadOnlyCollection<AttrAttribute> attributes = _fieldsToSerialize.GetAttributes(_primaryResourceType);
123137
IReadOnlyCollection<RelationshipAttribute> relationships = _fieldsToSerialize.GetRelationships(_primaryResourceType);
124138

0 commit comments

Comments
 (0)