Skip to content

Commit 934f96a

Browse files
Bart Koelmanbkoelman
Bart Koelman
authored andcommitted
Tryout: tracking versions in atomic:operations
1 parent 405dbd2 commit 934f96a

File tree

13 files changed

+511
-12
lines changed

13 files changed

+511
-12
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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+
}

src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs

+40-1
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,21 @@ public class OperationsProcessor : IOperationsProcessor
1515
private readonly IOperationProcessorAccessor _operationProcessorAccessor;
1616
private readonly IOperationsTransactionFactory _operationsTransactionFactory;
1717
private readonly ILocalIdTracker _localIdTracker;
18+
private readonly IVersionTracker _versionTracker;
1819
private readonly IResourceGraph _resourceGraph;
1920
private readonly IJsonApiRequest _request;
2021
private readonly ITargetedFields _targetedFields;
2122
private readonly ISparseFieldSetCache _sparseFieldSetCache;
2223
private readonly LocalIdValidator _localIdValidator;
2324

2425
public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccessor, IOperationsTransactionFactory operationsTransactionFactory,
25-
ILocalIdTracker localIdTracker, IResourceGraph resourceGraph, IJsonApiRequest request, ITargetedFields targetedFields,
26+
ILocalIdTracker localIdTracker, IVersionTracker versionTracker, IResourceGraph resourceGraph, IJsonApiRequest request, ITargetedFields targetedFields,
2627
ISparseFieldSetCache sparseFieldSetCache)
2728
{
2829
ArgumentGuard.NotNull(operationProcessorAccessor, nameof(operationProcessorAccessor));
2930
ArgumentGuard.NotNull(operationsTransactionFactory, nameof(operationsTransactionFactory));
3031
ArgumentGuard.NotNull(localIdTracker, nameof(localIdTracker));
32+
ArgumentGuard.NotNull(versionTracker, nameof(versionTracker));
3133
ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph));
3234
ArgumentGuard.NotNull(request, nameof(request));
3335
ArgumentGuard.NotNull(targetedFields, nameof(targetedFields));
@@ -36,6 +38,7 @@ public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccesso
3638
_operationProcessorAccessor = operationProcessorAccessor;
3739
_operationsTransactionFactory = operationsTransactionFactory;
3840
_localIdTracker = localIdTracker;
41+
_versionTracker = versionTracker;
3942
_resourceGraph = resourceGraph;
4043
_request = request;
4144
_targetedFields = targetedFields;
@@ -104,11 +107,15 @@ public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccesso
104107
cancellationToken.ThrowIfCancellationRequested();
105108

106109
TrackLocalIdsForOperation(operation);
110+
RefreshVersionsForOperation(operation);
107111

108112
_targetedFields.CopyFrom(operation.TargetedFields);
109113
_request.CopyFrom(operation.Request);
110114

111115
return await _operationProcessorAccessor.ProcessAsync(operation, cancellationToken);
116+
117+
// Ideally we'd take the versions from response here and update the version cache, but currently
118+
// not all resource service methods return data. Therefore this is handled elsewhere.
112119
}
113120

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

src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs

+1
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ private void AddOperationsLayer()
275275
_services.AddScoped<IOperationsProcessor, OperationsProcessor>();
276276
_services.AddScoped<IOperationProcessorAccessor, OperationProcessorAccessor>();
277277
_services.AddScoped<ILocalIdTracker, LocalIdTracker>();
278+
_services.AddScoped<IVersionTracker, VersionTracker>();
278279
}
279280

280281
public void Dispose()

src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs

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

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

src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs

+40
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,46 @@ public QueryLayer ComposeForUpdate<TId>(TId id, ResourceType primaryResourceType
394394
return primaryLayer;
395395
}
396396

397+
public QueryLayer ComposeForGetVersionsAfterWrite<TId>(TId id, ResourceType primaryResourceType, TopFieldSelection fieldSelection)
398+
{
399+
ArgumentGuard.NotNull(primaryResourceType, nameof(primaryResourceType));
400+
401+
// @formatter:wrap_chained_method_calls chop_always
402+
// @formatter:keep_existing_linebreaks true
403+
404+
IImmutableSet<IncludeElementExpression> includeElements = _targetedFields.Relationships
405+
.Where(relationship => relationship.RightType.IsVersioned)
406+
.Select(relationship => new IncludeElementExpression(relationship))
407+
.ToImmutableHashSet();
408+
409+
// @formatter:keep_existing_linebreaks restore
410+
// @formatter:wrap_chained_method_calls restore
411+
412+
AttrAttribute primaryIdAttribute = GetIdAttribute(primaryResourceType);
413+
414+
QueryLayer primaryLayer = new(primaryResourceType)
415+
{
416+
Include = includeElements.Any() ? new IncludeExpression(includeElements) : IncludeExpression.Empty,
417+
Filter = CreateFilterByIds(id.AsArray(), primaryIdAttribute, null)
418+
};
419+
420+
if (fieldSelection == TopFieldSelection.OnlyIdAttribute)
421+
{
422+
var primarySelection = new FieldSelection();
423+
FieldSelectors primarySelectors = primarySelection.GetOrCreateSelectors(primaryLayer.ResourceType);
424+
primarySelectors.IncludeAttribute(primaryIdAttribute);
425+
426+
foreach (IncludeElementExpression include in includeElements)
427+
{
428+
primarySelectors.IncludeRelationship(include.Relationship, null);
429+
}
430+
431+
primaryLayer.Selection = primarySelection;
432+
}
433+
434+
return primaryLayer;
435+
}
436+
397437
/// <inheritdoc />
398438
public IEnumerable<(QueryLayer, RelationshipAttribute)> ComposeForGetTargetedSecondaryResourceIds(IIdentifiable primaryResource)
399439
{

src/JsonApiDotNetCore/Services/JsonApiResourceService.cs

+31-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Net;
33
using System.Runtime.CompilerServices;
44
using JetBrains.Annotations;
5+
using JsonApiDotNetCore.AtomicOperations;
56
using JsonApiDotNetCore.Configuration;
67
using JsonApiDotNetCore.Diagnostics;
78
using JsonApiDotNetCore.Errors;
@@ -30,11 +31,12 @@ public class JsonApiResourceService<TResource, TId> : IResourceService<TResource
3031
private readonly TraceLogWriter<JsonApiResourceService<TResource, TId>> _traceWriter;
3132
private readonly IJsonApiRequest _request;
3233
private readonly IResourceChangeTracker<TResource> _resourceChangeTracker;
34+
private readonly IVersionTracker _versionTracker;
3335
private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor;
3436

3537
public JsonApiResourceService(IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext,
3638
IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request, IResourceChangeTracker<TResource> resourceChangeTracker,
37-
IResourceDefinitionAccessor resourceDefinitionAccessor)
39+
IVersionTracker versionTracker, IResourceDefinitionAccessor resourceDefinitionAccessor)
3840
{
3941
ArgumentGuard.NotNull(repositoryAccessor, nameof(repositoryAccessor));
4042
ArgumentGuard.NotNull(queryLayerComposer, nameof(queryLayerComposer));
@@ -43,6 +45,7 @@ public JsonApiResourceService(IResourceRepositoryAccessor repositoryAccessor, IQ
4345
ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory));
4446
ArgumentGuard.NotNull(request, nameof(request));
4547
ArgumentGuard.NotNull(resourceChangeTracker, nameof(resourceChangeTracker));
48+
ArgumentGuard.NotNull(versionTracker, nameof(versionTracker));
4649
ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor));
4750

4851
_repositoryAccessor = repositoryAccessor;
@@ -51,6 +54,7 @@ public JsonApiResourceService(IResourceRepositoryAccessor repositoryAccessor, IQ
5154
_options = options;
5255
_request = request;
5356
_resourceChangeTracker = resourceChangeTracker;
57+
_versionTracker = versionTracker;
5458
_resourceDefinitionAccessor = resourceDefinitionAccessor;
5559
_traceWriter = new TraceLogWriter<JsonApiResourceService<TResource, TId>>(loggerFactory);
5660
}
@@ -226,7 +230,8 @@ private async Task RetrieveResourceCountForNonPrimaryEndpointAsync(TId id, HasMa
226230
throw;
227231
}
228232

229-
TResource resourceFromDatabase = await GetPrimaryResourceByIdAsync(resourceForDatabase.Id, TopFieldSelection.WithAllAttributes, cancellationToken);
233+
TResource resourceFromDatabase =
234+
await GetPrimaryResourceAfterWriteAsync(resourceForDatabase.Id, TopFieldSelection.WithAllAttributes, cancellationToken);
230235

231236
_resourceChangeTracker.SetFinallyStoredAttributeValues(resourceFromDatabase);
232237

@@ -477,7 +482,7 @@ private async Task<TResource> GetForHasManyUpdateAsync(HasManyAttribute hasManyR
477482
throw;
478483
}
479484

480-
TResource afterResourceFromDatabase = await GetPrimaryResourceByIdAsync(id, TopFieldSelection.WithAllAttributes, cancellationToken);
485+
TResource afterResourceFromDatabase = await GetPrimaryResourceAfterWriteAsync(id, TopFieldSelection.WithAllAttributes, cancellationToken);
481486

482487
_resourceChangeTracker.SetFinallyStoredAttributeValues(afterResourceFromDatabase);
483488

@@ -522,6 +527,11 @@ public virtual async Task SetRelationshipAsync(TId leftId, string relationshipNa
522527
AssertIsNotResourceVersionMismatch(exception);
523528
throw;
524529
}
530+
531+
if (_versionTracker.RequiresVersionTracking())
532+
{
533+
await GetPrimaryResourceAfterWriteAsync(leftId, TopFieldSelection.OnlyIdAttribute, cancellationToken);
534+
}
525535
}
526536

527537
/// <inheritdoc />
@@ -614,6 +624,24 @@ protected async Task<TResource> GetPrimaryResourceByIdAsync(TId id, TopFieldSele
614624
return primaryResources.SingleOrDefault();
615625
}
616626

627+
private async Task<TResource> GetPrimaryResourceAfterWriteAsync(TId id, TopFieldSelection fieldSelection, CancellationToken cancellationToken)
628+
{
629+
AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType);
630+
631+
if (_versionTracker.RequiresVersionTracking())
632+
{
633+
QueryLayer queryLayer = _queryLayerComposer.ComposeForGetVersionsAfterWrite(id, _request.PrimaryResourceType, fieldSelection);
634+
IReadOnlyCollection<TResource> primaryResources = await _repositoryAccessor.GetAsync<TResource>(queryLayer, cancellationToken);
635+
TResource? primaryResource = primaryResources.SingleOrDefault();
636+
AssertPrimaryResourceExists(primaryResource);
637+
638+
_versionTracker.CaptureVersions(_request.PrimaryResourceType, primaryResource);
639+
return primaryResource;
640+
}
641+
642+
return await GetPrimaryResourceByIdAsync(id, fieldSelection, cancellationToken);
643+
}
644+
617645
protected async Task<TResource> GetPrimaryResourceForUpdateAsync(TId id, CancellationToken cancellationToken)
618646
{
619647
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, IPaginationContext paginationContext,
1617
IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request, IResourceChangeTracker<PrivateResource> resourceChangeTracker,
17-
IResourceDefinitionAccessor resourceDefinitionAccessor)
18-
: base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, resourceDefinitionAccessor)
18+
IVersionTracker versionTracker, IResourceDefinitionAccessor resourceDefinitionAccessor)
19+
: base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, versionTracker,
20+
resourceDefinitionAccessor)
1921
{
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)