Skip to content

Commit be0f452

Browse files
author
Bart Koelman
committed
Wire-up serialization callbacks
1 parent 509b7e5 commit be0f452

16 files changed

+308
-142
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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,16 @@ public Task OnWriteSucceededAsync(IntResource resource, OperationKind operationK
524524
{
525525
throw new NotImplementedException();
526526
}
527+
528+
public void OnDeserialize(IntResource resource)
529+
{
530+
throw new NotImplementedException();
531+
}
532+
533+
public void OnSerialize(IntResource resource)
534+
{
535+
throw new NotImplementedException();
536+
}
527537
}
528538

529539
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
@@ -600,6 +610,16 @@ public Task OnWriteSucceededAsync(GuidResource resource, OperationKind operation
600610
{
601611
throw new NotImplementedException();
602612
}
613+
614+
public void OnDeserialize(GuidResource resource)
615+
{
616+
throw new NotImplementedException();
617+
}
618+
619+
public void OnSerialize(GuidResource resource)
620+
{
621+
throw new NotImplementedException();
622+
}
603623
}
604624

605625
[UsedImplicitly(ImplicitUseTargetFlags.Members)]

test/UnitTests/Models/ResourceConstructionTests.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using JsonApiDotNetCore.Resources;
66
using JsonApiDotNetCore.Serialization;
77
using Microsoft.AspNetCore.Http;
8+
using Microsoft.Extensions.DependencyInjection;
89
using Microsoft.Extensions.Logging.Abstractions;
910
using Moq;
1011
using Newtonsoft.Json;
@@ -33,7 +34,10 @@ public void When_resource_has_default_constructor_it_must_succeed()
3334

3435
IResourceGraph graph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add<ResourceWithoutConstructor>().Build();
3536

36-
var serializer = new RequestDeserializer(graph, new ResourceFactory(new ServiceContainer()), new TargetedFields(), _mockHttpContextAccessor.Object,
37+
var serviceContainer = new ServiceContainer();
38+
serviceContainer.AddService(typeof(IResourceDefinitionAccessor), new NeverResourceDefinitionAccessor());
39+
40+
var serializer = new RequestDeserializer(graph, new ResourceFactory(serviceContainer), new TargetedFields(), _mockHttpContextAccessor.Object,
3741
_requestMock.Object, options);
3842

3943
var body = new
@@ -63,7 +67,10 @@ public void When_resource_has_default_constructor_that_throws_it_must_fail()
6367

6468
IResourceGraph graph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add<ResourceWithThrowingConstructor>().Build();
6569

66-
var serializer = new RequestDeserializer(graph, new ResourceFactory(new ServiceContainer()), new TargetedFields(), _mockHttpContextAccessor.Object,
70+
var serviceContainer = new ServiceContainer();
71+
serviceContainer.AddService(typeof(IResourceDefinitionAccessor), new NeverResourceDefinitionAccessor());
72+
73+
var serializer = new RequestDeserializer(graph, new ResourceFactory(serviceContainer), new TargetedFields(), _mockHttpContextAccessor.Object,
6774
_requestMock.Object, options);
6875

6976
var body = new
@@ -95,7 +102,10 @@ public void When_resource_has_constructor_with_string_parameter_it_must_fail()
95102

96103
IResourceGraph graph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add<ResourceWithStringConstructor>().Build();
97104

98-
var serializer = new RequestDeserializer(graph, new ResourceFactory(new ServiceContainer()), new TargetedFields(), _mockHttpContextAccessor.Object,
105+
var serviceContainer = new ServiceContainer();
106+
serviceContainer.AddService(typeof(IResourceDefinitionAccessor), new NeverResourceDefinitionAccessor());
107+
108+
var serializer = new RequestDeserializer(graph, new ResourceFactory(serviceContainer), new TargetedFields(), _mockHttpContextAccessor.Object,
99109
_requestMock.Object, options);
100110

101111
var body = new

0 commit comments

Comments
 (0)