Skip to content

Commit ecfec2d

Browse files
author
Bart Koelman
committed
Tryout: tracking versions in atomic:operations
1 parent fd03010 commit ecfec2d

File tree

13 files changed

+518
-13
lines changed

13 files changed

+518
-13
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using JsonApiDotNetCore.Configuration;
2+
using JsonApiDotNetCore.Resources;
3+
4+
namespace JsonApiDotNetCore.AtomicOperations
5+
{
6+
public interface IVersionTracker
7+
{
8+
bool RequiresVersionTracking();
9+
10+
void CaptureVersions(ResourceType resourceType, IIdentifiable resource);
11+
12+
string? GetVersion(ResourceType resourceType, string stringId);
13+
}
14+
}

src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs

+41-2
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,21 @@ public class OperationsProcessor : IOperationsProcessor
1919
private readonly IOperationProcessorAccessor _operationProcessorAccessor;
2020
private readonly IOperationsTransactionFactory _operationsTransactionFactory;
2121
private readonly ILocalIdTracker _localIdTracker;
22+
private readonly IVersionTracker _versionTracker;
2223
private readonly IResourceGraph _resourceGraph;
2324
private readonly IJsonApiRequest _request;
2425
private readonly ITargetedFields _targetedFields;
2526
private readonly ISparseFieldSetCache _sparseFieldSetCache;
2627
private readonly LocalIdValidator _localIdValidator;
2728

2829
public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccessor, IOperationsTransactionFactory operationsTransactionFactory,
29-
ILocalIdTracker localIdTracker, IResourceGraph resourceGraph, IJsonApiRequest request, ITargetedFields targetedFields,
30-
ISparseFieldSetCache sparseFieldSetCache)
30+
ILocalIdTracker localIdTracker, IVersionTracker versionTracker, IResourceGraph resourceGraph, IJsonApiRequest request,
31+
ITargetedFields targetedFields, ISparseFieldSetCache sparseFieldSetCache)
3132
{
3233
ArgumentGuard.NotNull(operationProcessorAccessor, nameof(operationProcessorAccessor));
3334
ArgumentGuard.NotNull(operationsTransactionFactory, nameof(operationsTransactionFactory));
3435
ArgumentGuard.NotNull(localIdTracker, nameof(localIdTracker));
36+
ArgumentGuard.NotNull(versionTracker, nameof(versionTracker));
3537
ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph));
3638
ArgumentGuard.NotNull(request, nameof(request));
3739
ArgumentGuard.NotNull(targetedFields, nameof(targetedFields));
@@ -40,6 +42,7 @@ public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccesso
4042
_operationProcessorAccessor = operationProcessorAccessor;
4143
_operationsTransactionFactory = operationsTransactionFactory;
4244
_localIdTracker = localIdTracker;
45+
_versionTracker = versionTracker;
4346
_resourceGraph = resourceGraph;
4447
_request = request;
4548
_targetedFields = targetedFields;
@@ -108,11 +111,15 @@ public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccesso
108111
cancellationToken.ThrowIfCancellationRequested();
109112

110113
TrackLocalIdsForOperation(operation);
114+
RefreshVersionsForOperation(operation);
111115

112116
_targetedFields.CopyFrom(operation.TargetedFields);
113117
_request.CopyFrom(operation.Request);
114118

115119
return await _operationProcessorAccessor.ProcessAsync(operation, cancellationToken);
120+
121+
// Ideally we'd take the versions from response here and update the version cache, but currently
122+
// not all resource service methods return data. Therefore this is handled elsewhere.
116123
}
117124

118125
protected void TrackLocalIdsForOperation(OperationContainer operation)
@@ -148,5 +155,37 @@ private void AssignStringId(IIdentifiable resource)
148155
resource.StringId = _localIdTracker.GetValue(resource.LocalId, resourceType);
149156
}
150157
}
158+
159+
private void RefreshVersionsForOperation(OperationContainer operation)
160+
{
161+
if (operation.Request.PrimaryResourceType!.IsVersioned)
162+
{
163+
string? requestVersion = operation.Resource.GetVersion();
164+
165+
if (requestVersion == null)
166+
{
167+
string? trackedVersion = _versionTracker.GetVersion(operation.Request.PrimaryResourceType, operation.Resource.StringId!);
168+
operation.Resource.SetVersion(trackedVersion);
169+
170+
((JsonApiRequest)operation.Request).PrimaryVersion = trackedVersion;
171+
}
172+
}
173+
174+
foreach (var rightResource in operation.GetSecondaryResources())
175+
{
176+
ResourceType rightResourceType = _resourceGraph.GetResourceType(rightResource.GetType());
177+
178+
if (rightResourceType.IsVersioned)
179+
{
180+
string? requestVersion = rightResource.GetVersion();
181+
182+
if (requestVersion == null)
183+
{
184+
string? trackedVersion = _versionTracker.GetVersion(rightResourceType, rightResource.StringId!);
185+
rightResource.SetVersion(trackedVersion);
186+
}
187+
}
188+
}
189+
}
151190
}
152191
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using JsonApiDotNetCore.Configuration;
4+
using JsonApiDotNetCore.Middleware;
5+
using JsonApiDotNetCore.Resources;
6+
using JsonApiDotNetCore.Resources.Annotations;
7+
8+
namespace JsonApiDotNetCore.AtomicOperations
9+
{
10+
public sealed class VersionTracker : IVersionTracker
11+
{
12+
private static readonly CollectionConverter CollectionConverter = new();
13+
14+
private readonly ITargetedFields _targetedFields;
15+
private readonly IJsonApiRequest _request;
16+
private readonly Dictionary<string, string> _versionPerResource = new();
17+
18+
public VersionTracker(ITargetedFields targetedFields, IJsonApiRequest request)
19+
{
20+
ArgumentGuard.NotNull(targetedFields, nameof(targetedFields));
21+
ArgumentGuard.NotNull(request, nameof(request));
22+
23+
_targetedFields = targetedFields;
24+
_request = request;
25+
}
26+
27+
public bool RequiresVersionTracking()
28+
{
29+
if (_request.Kind != EndpointKind.AtomicOperations)
30+
{
31+
return false;
32+
}
33+
34+
return _request.PrimaryResourceType!.IsVersioned || _targetedFields.Relationships.Any(relationship => relationship.RightType.IsVersioned);
35+
}
36+
37+
public void CaptureVersions(ResourceType resourceType, IIdentifiable resource)
38+
{
39+
if (_request.Kind == EndpointKind.AtomicOperations)
40+
{
41+
if (resourceType.IsVersioned)
42+
{
43+
string? leftVersion = resource.GetVersion();
44+
SetVersion(resourceType, resource.StringId!, leftVersion);
45+
}
46+
47+
foreach (var relationship in _targetedFields.Relationships)
48+
{
49+
if (relationship.RightType.IsVersioned)
50+
{
51+
CaptureVersionsInRelationship(resource, relationship);
52+
}
53+
}
54+
}
55+
}
56+
57+
private void CaptureVersionsInRelationship(IIdentifiable resource, RelationshipAttribute relationship)
58+
{
59+
object? afterRightValue = relationship.GetValue(resource);
60+
ICollection<IIdentifiable> afterRightResources = CollectionConverter.ExtractResources(afterRightValue);
61+
62+
foreach (var rightResource in afterRightResources)
63+
{
64+
string? rightVersion = rightResource.GetVersion();
65+
SetVersion(relationship.RightType, rightResource.StringId!, rightVersion);
66+
}
67+
}
68+
69+
private void SetVersion(ResourceType resourceType, string stringId, string? version)
70+
{
71+
string key = GetKey(resourceType, stringId);
72+
73+
if (version == null)
74+
{
75+
_versionPerResource.Remove(key);
76+
}
77+
else
78+
{
79+
_versionPerResource[key] = version;
80+
}
81+
}
82+
83+
public string? GetVersion(ResourceType resourceType, string stringId)
84+
{
85+
string key = GetKey(resourceType, stringId);
86+
return _versionPerResource.TryGetValue(key, out string? version) ? version : null;
87+
}
88+
89+
private string GetKey(ResourceType resourceType, string stringId)
90+
{
91+
return $"{resourceType.PublicName}::{stringId}";
92+
}
93+
}
94+
}

src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs

+1
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ private void AddOperationsLayer()
278278
_services.AddScoped<IOperationsProcessor, OperationsProcessor>();
279279
_services.AddScoped<IOperationProcessorAccessor, OperationProcessorAccessor>();
280280
_services.AddScoped<ILocalIdTracker, LocalIdTracker>();
281+
_services.AddScoped<IVersionTracker, VersionTracker>();
281282
}
282283

283284
public void Dispose()

src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs

+5
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ QueryLayer WrapLayerForSecondaryEndpoint<TId>(QueryLayer secondaryLayer, Resourc
4848
/// </summary>
4949
QueryLayer ComposeForUpdate<TId>(TId id, ResourceType primaryResourceType);
5050

51+
/// <summary>
52+
/// Builds a query that retrieves the primary resource, along with the subset of versioned targeted relationships, after a create/update/delete request.
53+
/// </summary>
54+
QueryLayer ComposeForGetVersionsAfterWrite<TId>(TId id, ResourceType primaryResourceType, TopFieldSelection fieldSelection);
55+
5156
/// <summary>
5257
/// Builds a query for each targeted relationship with a filter to match on its right resource IDs.
5358
/// </summary>

src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs

+39
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,45 @@ public QueryLayer ComposeForUpdate<TId>(TId id, ResourceType primaryResourceType
401401
return primaryLayer;
402402
}
403403

404+
public QueryLayer ComposeForGetVersionsAfterWrite<TId>(TId id, ResourceType primaryResourceType, TopFieldSelection fieldSelection)
405+
{
406+
ArgumentGuard.NotNull(primaryResourceType, nameof(primaryResourceType));
407+
408+
// @formatter:wrap_chained_method_calls chop_always
409+
// @formatter:keep_existing_linebreaks true
410+
411+
IImmutableSet<IncludeElementExpression> includeElements = _targetedFields.Relationships
412+
.Where(relationship => relationship.RightType.IsVersioned)
413+
.Select(relationship => new IncludeElementExpression(relationship))
414+
.ToImmutableHashSet();
415+
416+
// @formatter:keep_existing_linebreaks restore
417+
// @formatter:wrap_chained_method_calls restore
418+
419+
AttrAttribute primaryIdAttribute = GetIdAttribute(primaryResourceType);
420+
421+
QueryLayer primaryLayer = new(primaryResourceType)
422+
{
423+
Include = includeElements.Any() ? new IncludeExpression(includeElements) : IncludeExpression.Empty,
424+
Filter = CreateFilterByIds(id.AsArray(), primaryIdAttribute, null)
425+
};
426+
427+
if (fieldSelection == TopFieldSelection.OnlyIdAttribute)
428+
{
429+
primaryLayer.Projection = new Dictionary<ResourceFieldAttribute, QueryLayer?>
430+
{
431+
[primaryIdAttribute] = null
432+
};
433+
434+
foreach (var include in includeElements)
435+
{
436+
primaryLayer.Projection.Add(include.Relationship, null);
437+
}
438+
}
439+
440+
return primaryLayer;
441+
}
442+
404443
/// <inheritdoc />
405444
public IEnumerable<(QueryLayer, RelationshipAttribute)> ComposeForGetTargetedSecondaryResourceIds(IIdentifiable primaryResource)
406445
{

src/JsonApiDotNetCore/Services/JsonApiResourceService.cs

+31-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Threading;
88
using System.Threading.Tasks;
99
using JetBrains.Annotations;
10+
using JsonApiDotNetCore.AtomicOperations;
1011
using JsonApiDotNetCore.Configuration;
1112
using JsonApiDotNetCore.Diagnostics;
1213
using JsonApiDotNetCore.Errors;
@@ -35,11 +36,12 @@ public class JsonApiResourceService<TResource, TId> : IResourceService<TResource
3536
private readonly TraceLogWriter<JsonApiResourceService<TResource, TId>> _traceWriter;
3637
private readonly IJsonApiRequest _request;
3738
private readonly IResourceChangeTracker<TResource> _resourceChangeTracker;
39+
private readonly IVersionTracker _versionTracker;
3840
private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor;
3941

4042
public JsonApiResourceService(IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer,
4143
IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request,
42-
IResourceChangeTracker<TResource> resourceChangeTracker, IResourceDefinitionAccessor resourceDefinitionAccessor)
44+
IResourceChangeTracker<TResource> resourceChangeTracker, IVersionTracker versionTracker, IResourceDefinitionAccessor resourceDefinitionAccessor)
4345
{
4446
ArgumentGuard.NotNull(repositoryAccessor, nameof(repositoryAccessor));
4547
ArgumentGuard.NotNull(queryLayerComposer, nameof(queryLayerComposer));
@@ -48,6 +50,7 @@ public JsonApiResourceService(IResourceRepositoryAccessor repositoryAccessor, IQ
4850
ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory));
4951
ArgumentGuard.NotNull(request, nameof(request));
5052
ArgumentGuard.NotNull(resourceChangeTracker, nameof(resourceChangeTracker));
53+
ArgumentGuard.NotNull(versionTracker, nameof(versionTracker));
5154
ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor));
5255

5356
_repositoryAccessor = repositoryAccessor;
@@ -56,6 +59,7 @@ public JsonApiResourceService(IResourceRepositoryAccessor repositoryAccessor, IQ
5659
_options = options;
5760
_request = request;
5861
_resourceChangeTracker = resourceChangeTracker;
62+
_versionTracker = versionTracker;
5963
_resourceDefinitionAccessor = resourceDefinitionAccessor;
6064
_traceWriter = new TraceLogWriter<JsonApiResourceService<TResource, TId>>(loggerFactory);
6165
}
@@ -234,7 +238,8 @@ private async Task RetrieveResourceCountForNonPrimaryEndpointAsync(TId id, HasMa
234238
throw;
235239
}
236240

237-
TResource resourceFromDatabase = await GetPrimaryResourceByIdAsync(resourceForDatabase.Id, TopFieldSelection.WithAllAttributes, cancellationToken);
241+
TResource resourceFromDatabase =
242+
await GetPrimaryResourceAfterWriteAsync(resourceForDatabase.Id, TopFieldSelection.WithAllAttributes, cancellationToken);
238243

239244
_resourceChangeTracker.SetFinallyStoredAttributeValues(resourceFromDatabase);
240245

@@ -413,7 +418,7 @@ protected async Task AssertRightResourcesExistAsync(object? rightValue, Cancella
413418
throw;
414419
}
415420

416-
TResource afterResourceFromDatabase = await GetPrimaryResourceByIdAsync(id, TopFieldSelection.WithAllAttributes, cancellationToken);
421+
TResource afterResourceFromDatabase = await GetPrimaryResourceAfterWriteAsync(id, TopFieldSelection.WithAllAttributes, cancellationToken);
417422

418423
_resourceChangeTracker.SetFinallyStoredAttributeValues(afterResourceFromDatabase);
419424

@@ -451,6 +456,11 @@ public virtual async Task SetRelationshipAsync(TId leftId, string relationshipNa
451456
AssertIsNotResourceVersionMismatch(exception);
452457
throw;
453458
}
459+
460+
if (_versionTracker.RequiresVersionTracking())
461+
{
462+
await GetPrimaryResourceAfterWriteAsync(leftId, TopFieldSelection.OnlyIdAttribute, cancellationToken);
463+
}
454464
}
455465

456466
/// <inheritdoc />
@@ -527,6 +537,24 @@ protected async Task<TResource> GetPrimaryResourceByIdAsync(TId id, TopFieldSele
527537
return primaryResources.SingleOrDefault();
528538
}
529539

540+
private async Task<TResource> GetPrimaryResourceAfterWriteAsync(TId id, TopFieldSelection fieldSelection, CancellationToken cancellationToken)
541+
{
542+
AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType);
543+
544+
if (_versionTracker.RequiresVersionTracking())
545+
{
546+
QueryLayer queryLayer = _queryLayerComposer.ComposeForGetVersionsAfterWrite(id, _request.PrimaryResourceType, fieldSelection);
547+
IReadOnlyCollection<TResource> primaryResources = await _repositoryAccessor.GetAsync<TResource>(queryLayer, cancellationToken);
548+
TResource? primaryResource = primaryResources.SingleOrDefault();
549+
AssertPrimaryResourceExists(primaryResource);
550+
551+
_versionTracker.CaptureVersions(_request.PrimaryResourceType, primaryResource);
552+
return primaryResource;
553+
}
554+
555+
return await GetPrimaryResourceByIdAsync(id, fieldSelection, cancellationToken);
556+
}
557+
530558
protected async Task<TResource> GetPrimaryResourceForUpdateAsync(TId id, CancellationToken cancellationToken)
531559
{
532560
AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType);

test/DiscoveryTests/PrivateResourceService.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using JetBrains.Annotations;
2+
using JsonApiDotNetCore.AtomicOperations;
23
using JsonApiDotNetCore.Configuration;
34
using JsonApiDotNetCore.Middleware;
45
using JsonApiDotNetCore.Queries;
@@ -14,8 +15,9 @@ public sealed class PrivateResourceService : JsonApiResourceService<PrivateResou
1415
{
1516
public PrivateResourceService(IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer,
1617
IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request,
17-
IResourceChangeTracker<PrivateResource> resourceChangeTracker, IResourceDefinitionAccessor resourceDefinitionAccessor)
18-
: base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker,
18+
IResourceChangeTracker<PrivateResource> resourceChangeTracker, IVersionTracker versionTracker,
19+
IResourceDefinitionAccessor resourceDefinitionAccessor)
20+
: base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, versionTracker,
1921
resourceDefinitionAccessor)
2022
{
2123
}

test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using FluentAssertions;
2+
using JsonApiDotNetCore.AtomicOperations;
23
using JsonApiDotNetCore.Configuration;
34
using JsonApiDotNetCore.Middleware;
45
using JsonApiDotNetCore.Queries;
@@ -36,6 +37,7 @@ public ServiceDiscoveryFacadeTests()
3637
_services.AddScoped(_ => new Mock<ITargetedFields>().Object);
3738
_services.AddScoped(_ => new Mock<IResourceGraph>().Object);
3839
_services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>));
40+
_services.AddScoped(_ => new Mock<IVersionTracker>().Object);
3941
_services.AddScoped(_ => new Mock<IResourceFactory>().Object);
4042
_services.AddScoped(_ => new Mock<IPaginationContext>().Object);
4143
_services.AddScoped(_ => new Mock<IQueryLayerComposer>().Object);

0 commit comments

Comments
 (0)