Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit cee13f6

Browse files
author
Bart Koelman
committed
Tryout: tracking versions in atomic:operations
1 parent 48d8c12 commit cee13f6

File tree

13 files changed

+518
-13
lines changed

13 files changed

+518
-13
lines changed
Lines changed: 14 additions & 0 deletions
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

Lines changed: 41 additions & 2 deletions
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,ITargetedFieldstargetedFields,
30-
ISparseFieldSetCache sparseFieldSetCache)
30+
ILocalIdTracker localIdTracker, IVersionTrackerversionTracker,IResourceGraph resourceGraph, IJsonApiRequest request,
31+
ITargetedFieldstargetedFields,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
}
Lines changed: 94 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 39 additions & 0 deletions
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

Lines changed: 31 additions & 3 deletions
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, IVersionTrackerversionTracker,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

Lines changed: 4 additions & 2 deletions
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

Lines changed: 2 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /