Skip to content

Commit

Permalink
Tryout: tracking versions in atomic:operations
Browse files Browse the repository at this point in the history
  • Loading branch information
Bart Koelman committed Nov 30, 2021
1 parent fd03010 commit ecfec2d
Show file tree
Hide file tree
Showing 13 changed files with 518 additions and 13 deletions.
14 changes: 14 additions & 0 deletions src/JsonApiDotNetCore/AtomicOperations/IVersionTracker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Resources;

namespace JsonApiDotNetCore.AtomicOperations
{
public interface IVersionTracker
{
bool RequiresVersionTracking();

void CaptureVersions(ResourceType resourceType, IIdentifiable resource);

string? GetVersion(ResourceType resourceType, string stringId);
}
}
43 changes: 41 additions & 2 deletions src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,21 @@ public class OperationsProcessor : IOperationsProcessor
private readonly IOperationProcessorAccessor _operationProcessorAccessor;
private readonly IOperationsTransactionFactory _operationsTransactionFactory;
private readonly ILocalIdTracker _localIdTracker;
private readonly IVersionTracker _versionTracker;
private readonly IResourceGraph _resourceGraph;
private readonly IJsonApiRequest _request;
private readonly ITargetedFields _targetedFields;
private readonly ISparseFieldSetCache _sparseFieldSetCache;
private readonly LocalIdValidator _localIdValidator;

public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccessor, IOperationsTransactionFactory operationsTransactionFactory,
ILocalIdTracker localIdTracker, IResourceGraph resourceGraph, IJsonApiRequest request, ITargetedFields targetedFields,
ISparseFieldSetCache sparseFieldSetCache)
ILocalIdTracker localIdTracker, IVersionTracker versionTracker, IResourceGraph resourceGraph, IJsonApiRequest request,
ITargetedFields targetedFields, ISparseFieldSetCache sparseFieldSetCache)
{
ArgumentGuard.NotNull(operationProcessorAccessor, nameof(operationProcessorAccessor));
ArgumentGuard.NotNull(operationsTransactionFactory, nameof(operationsTransactionFactory));
ArgumentGuard.NotNull(localIdTracker, nameof(localIdTracker));
ArgumentGuard.NotNull(versionTracker, nameof(versionTracker));
ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph));
ArgumentGuard.NotNull(request, nameof(request));
ArgumentGuard.NotNull(targetedFields, nameof(targetedFields));
Expand All @@ -40,6 +42,7 @@ public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccesso
_operationProcessorAccessor = operationProcessorAccessor;
_operationsTransactionFactory = operationsTransactionFactory;
_localIdTracker = localIdTracker;
_versionTracker = versionTracker;
_resourceGraph = resourceGraph;
_request = request;
_targetedFields = targetedFields;
Expand Down Expand Up @@ -108,11 +111,15 @@ public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccesso
cancellationToken.ThrowIfCancellationRequested();

TrackLocalIdsForOperation(operation);
RefreshVersionsForOperation(operation);

_targetedFields.CopyFrom(operation.TargetedFields);
_request.CopyFrom(operation.Request);

return await _operationProcessorAccessor.ProcessAsync(operation, cancellationToken);

// Ideally we'd take the versions from response here and update the version cache, but currently
// not all resource service methods return data. Therefore this is handled elsewhere.
}

protected void TrackLocalIdsForOperation(OperationContainer operation)
Expand Down Expand Up @@ -148,5 +155,37 @@ private void AssignStringId(IIdentifiable resource)
resource.StringId = _localIdTracker.GetValue(resource.LocalId, resourceType);
}
}

private void RefreshVersionsForOperation(OperationContainer operation)
{
if (operation.Request.PrimaryResourceType!.IsVersioned)
{
string? requestVersion = operation.Resource.GetVersion();

if (requestVersion == null)
{
string? trackedVersion = _versionTracker.GetVersion(operation.Request.PrimaryResourceType, operation.Resource.StringId!);
operation.Resource.SetVersion(trackedVersion);

((JsonApiRequest)operation.Request).PrimaryVersion = trackedVersion;
}
}

foreach (var rightResource in operation.GetSecondaryResources())
{
ResourceType rightResourceType = _resourceGraph.GetResourceType(rightResource.GetType());

if (rightResourceType.IsVersioned)
{
string? requestVersion = rightResource.GetVersion();

if (requestVersion == null)
{
string? trackedVersion = _versionTracker.GetVersion(rightResourceType, rightResource.StringId!);
rightResource.SetVersion(trackedVersion);
}
}
}
}
}
}
94 changes: 94 additions & 0 deletions src/JsonApiDotNetCore/AtomicOperations/VersionTracker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System.Collections.Generic;
using System.Linq;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;

namespace JsonApiDotNetCore.AtomicOperations
{
public sealed class VersionTracker : IVersionTracker
{
private static readonly CollectionConverter CollectionConverter = new();

private readonly ITargetedFields _targetedFields;
private readonly IJsonApiRequest _request;
private readonly Dictionary<string, string> _versionPerResource = new();

public VersionTracker(ITargetedFields targetedFields, IJsonApiRequest request)
{
ArgumentGuard.NotNull(targetedFields, nameof(targetedFields));
ArgumentGuard.NotNull(request, nameof(request));

_targetedFields = targetedFields;
_request = request;
}

public bool RequiresVersionTracking()
{
if (_request.Kind != EndpointKind.AtomicOperations)
{
return false;
}

return _request.PrimaryResourceType!.IsVersioned || _targetedFields.Relationships.Any(relationship => relationship.RightType.IsVersioned);
}

public void CaptureVersions(ResourceType resourceType, IIdentifiable resource)
{
if (_request.Kind == EndpointKind.AtomicOperations)
{
if (resourceType.IsVersioned)
{
string? leftVersion = resource.GetVersion();
SetVersion(resourceType, resource.StringId!, leftVersion);
}

foreach (var relationship in _targetedFields.Relationships)
{
if (relationship.RightType.IsVersioned)
{
CaptureVersionsInRelationship(resource, relationship);
}
}
}
}

private void CaptureVersionsInRelationship(IIdentifiable resource, RelationshipAttribute relationship)
{
object? afterRightValue = relationship.GetValue(resource);
ICollection<IIdentifiable> afterRightResources = CollectionConverter.ExtractResources(afterRightValue);

foreach (var rightResource in afterRightResources)
{
string? rightVersion = rightResource.GetVersion();
SetVersion(relationship.RightType, rightResource.StringId!, rightVersion);
}
}

private void SetVersion(ResourceType resourceType, string stringId, string? version)
{
string key = GetKey(resourceType, stringId);

if (version == null)
{
_versionPerResource.Remove(key);
}
else
{
_versionPerResource[key] = version;
}
}

public string? GetVersion(ResourceType resourceType, string stringId)
{
string key = GetKey(resourceType, stringId);
return _versionPerResource.TryGetValue(key, out string? version) ? version : null;
}

private string GetKey(ResourceType resourceType, string stringId)
{
return $"{resourceType.PublicName}::{stringId}";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ private void AddOperationsLayer()
_services.AddScoped<IOperationsProcessor, OperationsProcessor>();
_services.AddScoped<IOperationProcessorAccessor, OperationProcessorAccessor>();
_services.AddScoped<ILocalIdTracker, LocalIdTracker>();
_services.AddScoped<IVersionTracker, VersionTracker>();
}

public void Dispose()
Expand Down
5 changes: 5 additions & 0 deletions src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ QueryLayer WrapLayerForSecondaryEndpoint<TId>(QueryLayer secondaryLayer, Resourc
/// </summary>
QueryLayer ComposeForUpdate<TId>(TId id, ResourceType primaryResourceType);

/// <summary>
/// Builds a query that retrieves the primary resource, along with the subset of versioned targeted relationships, after a create/update/delete request.
/// </summary>
QueryLayer ComposeForGetVersionsAfterWrite<TId>(TId id, ResourceType primaryResourceType, TopFieldSelection fieldSelection);

/// <summary>
/// Builds a query for each targeted relationship with a filter to match on its right resource IDs.
/// </summary>
Expand Down
39 changes: 39 additions & 0 deletions src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,45 @@ public QueryLayer ComposeForUpdate<TId>(TId id, ResourceType primaryResourceType
return primaryLayer;
}

public QueryLayer ComposeForGetVersionsAfterWrite<TId>(TId id, ResourceType primaryResourceType, TopFieldSelection fieldSelection)
{
ArgumentGuard.NotNull(primaryResourceType, nameof(primaryResourceType));

// @formatter:wrap_chained_method_calls chop_always
// @formatter:keep_existing_linebreaks true

IImmutableSet<IncludeElementExpression> includeElements = _targetedFields.Relationships
.Where(relationship => relationship.RightType.IsVersioned)
.Select(relationship => new IncludeElementExpression(relationship))
.ToImmutableHashSet();

// @formatter:keep_existing_linebreaks restore
// @formatter:wrap_chained_method_calls restore

AttrAttribute primaryIdAttribute = GetIdAttribute(primaryResourceType);

QueryLayer primaryLayer = new(primaryResourceType)
{
Include = includeElements.Any() ? new IncludeExpression(includeElements) : IncludeExpression.Empty,
Filter = CreateFilterByIds(id.AsArray(), primaryIdAttribute, null)
};

if (fieldSelection == TopFieldSelection.OnlyIdAttribute)
{
primaryLayer.Projection = new Dictionary<ResourceFieldAttribute, QueryLayer?>
{
[primaryIdAttribute] = null
};

foreach (var include in includeElements)
{
primaryLayer.Projection.Add(include.Relationship, null);
}
}

return primaryLayer;
}

/// <inheritdoc />
public IEnumerable<(QueryLayer, RelationshipAttribute)> ComposeForGetTargetedSecondaryResourceIds(IIdentifiable primaryResource)
{
Expand Down
34 changes: 31 additions & 3 deletions src/JsonApiDotNetCore/Services/JsonApiResourceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using JsonApiDotNetCore.AtomicOperations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Diagnostics;
using JsonApiDotNetCore.Errors;
Expand Down Expand Up @@ -35,11 +36,12 @@ public class JsonApiResourceService<TResource, TId> : IResourceService<TResource
private readonly TraceLogWriter<JsonApiResourceService<TResource, TId>> _traceWriter;
private readonly IJsonApiRequest _request;
private readonly IResourceChangeTracker<TResource> _resourceChangeTracker;
private readonly IVersionTracker _versionTracker;
private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor;

public JsonApiResourceService(IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer,
IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request,
IResourceChangeTracker<TResource> resourceChangeTracker, IResourceDefinitionAccessor resourceDefinitionAccessor)
IResourceChangeTracker<TResource> resourceChangeTracker, IVersionTracker versionTracker, IResourceDefinitionAccessor resourceDefinitionAccessor)
{
ArgumentGuard.NotNull(repositoryAccessor, nameof(repositoryAccessor));
ArgumentGuard.NotNull(queryLayerComposer, nameof(queryLayerComposer));
Expand All @@ -48,6 +50,7 @@ public JsonApiResourceService(IResourceRepositoryAccessor repositoryAccessor, IQ
ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory));
ArgumentGuard.NotNull(request, nameof(request));
ArgumentGuard.NotNull(resourceChangeTracker, nameof(resourceChangeTracker));
ArgumentGuard.NotNull(versionTracker, nameof(versionTracker));
ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor));

_repositoryAccessor = repositoryAccessor;
Expand All @@ -56,6 +59,7 @@ public JsonApiResourceService(IResourceRepositoryAccessor repositoryAccessor, IQ
_options = options;
_request = request;
_resourceChangeTracker = resourceChangeTracker;
_versionTracker = versionTracker;
_resourceDefinitionAccessor = resourceDefinitionAccessor;
_traceWriter = new TraceLogWriter<JsonApiResourceService<TResource, TId>>(loggerFactory);
}
Expand Down Expand Up @@ -234,7 +238,8 @@ private async Task RetrieveResourceCountForNonPrimaryEndpointAsync(TId id, HasMa
throw;
}

TResource resourceFromDatabase = await GetPrimaryResourceByIdAsync(resourceForDatabase.Id, TopFieldSelection.WithAllAttributes, cancellationToken);
TResource resourceFromDatabase =
await GetPrimaryResourceAfterWriteAsync(resourceForDatabase.Id, TopFieldSelection.WithAllAttributes, cancellationToken);

_resourceChangeTracker.SetFinallyStoredAttributeValues(resourceFromDatabase);

Expand Down Expand Up @@ -413,7 +418,7 @@ protected async Task AssertRightResourcesExistAsync(object? rightValue, Cancella
throw;
}

TResource afterResourceFromDatabase = await GetPrimaryResourceByIdAsync(id, TopFieldSelection.WithAllAttributes, cancellationToken);
TResource afterResourceFromDatabase = await GetPrimaryResourceAfterWriteAsync(id, TopFieldSelection.WithAllAttributes, cancellationToken);

_resourceChangeTracker.SetFinallyStoredAttributeValues(afterResourceFromDatabase);

Expand Down Expand Up @@ -451,6 +456,11 @@ public virtual async Task SetRelationshipAsync(TId leftId, string relationshipNa
AssertIsNotResourceVersionMismatch(exception);
throw;
}

if (_versionTracker.RequiresVersionTracking())
{
await GetPrimaryResourceAfterWriteAsync(leftId, TopFieldSelection.OnlyIdAttribute, cancellationToken);
}
}

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

private async Task<TResource> GetPrimaryResourceAfterWriteAsync(TId id, TopFieldSelection fieldSelection, CancellationToken cancellationToken)
{
AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType);

if (_versionTracker.RequiresVersionTracking())
{
QueryLayer queryLayer = _queryLayerComposer.ComposeForGetVersionsAfterWrite(id, _request.PrimaryResourceType, fieldSelection);
IReadOnlyCollection<TResource> primaryResources = await _repositoryAccessor.GetAsync<TResource>(queryLayer, cancellationToken);
TResource? primaryResource = primaryResources.SingleOrDefault();
AssertPrimaryResourceExists(primaryResource);

_versionTracker.CaptureVersions(_request.PrimaryResourceType, primaryResource);
return primaryResource;
}

return await GetPrimaryResourceByIdAsync(id, fieldSelection, cancellationToken);
}

protected async Task<TResource> GetPrimaryResourceForUpdateAsync(TId id, CancellationToken cancellationToken)
{
AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType);
Expand Down
6 changes: 4 additions & 2 deletions test/DiscoveryTests/PrivateResourceService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.AtomicOperations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries;
Expand All @@ -14,8 +15,9 @@ public sealed class PrivateResourceService : JsonApiResourceService<PrivateResou
{
public PrivateResourceService(IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer,
IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request,
IResourceChangeTracker<PrivateResource> resourceChangeTracker, IResourceDefinitionAccessor resourceDefinitionAccessor)
: base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker,
IResourceChangeTracker<PrivateResource> resourceChangeTracker, IVersionTracker versionTracker,
IResourceDefinitionAccessor resourceDefinitionAccessor)
: base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, versionTracker,
resourceDefinitionAccessor)
{
}
Expand Down
2 changes: 2 additions & 0 deletions test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FluentAssertions;
using JsonApiDotNetCore.AtomicOperations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries;
Expand Down Expand Up @@ -36,6 +37,7 @@ public ServiceDiscoveryFacadeTests()
_services.AddScoped(_ => new Mock<ITargetedFields>().Object);
_services.AddScoped(_ => new Mock<IResourceGraph>().Object);
_services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>));
_services.AddScoped(_ => new Mock<IVersionTracker>().Object);
_services.AddScoped(_ => new Mock<IResourceFactory>().Object);
_services.AddScoped(_ => new Mock<IPaginationContext>().Object);
_services.AddScoped(_ => new Mock<IQueryLayerComposer>().Object);
Expand Down
Loading

0 comments on commit ecfec2d

Please sign in to comment.