Skip to content

Commit 0422d51

Browse files
authored
Merge 934f96a into 71e5398
2 parents 71e5398 + 934f96a commit 0422d51

File tree

71 files changed

+6407
-109
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+6407
-109
lines changed

Diff for: JsonApiDotNetCore.sln.DotSettings

+1
Original file line numberDiff line numberDiff line change
@@ -645,5 +645,6 @@ $left$ = $right$;</s:String>
645645
<s:Boolean x:Key="/Default/UserDictionary/Words/=subdirectory/@EntryIndexedValue">True</s:Boolean>
646646
<s:Boolean x:Key="/Default/UserDictionary/Words/=unarchive/@EntryIndexedValue">True</s:Boolean>
647647
<s:Boolean x:Key="/Default/UserDictionary/Words/=Workflows/@EntryIndexedValue">True</s:Boolean>
648+
<s:Boolean x:Key="/Default/UserDictionary/Words/=xmin/@EntryIndexedValue">True</s:Boolean>
648649
<s:Boolean x:Key="/Default/UserDictionary/Words/=xunit/@EntryIndexedValue">True</s:Boolean>
649650
</wpf:ResourceDictionary>
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using JetBrains.Annotations;
22
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Middleware;
34
using JsonApiDotNetCore.Queries;
45
using JsonApiDotNetCore.Repositories;
56
using JsonApiDotNetCore.Resources;
@@ -11,10 +12,10 @@ namespace MultiDbContextExample.Repositories;
1112
public sealed class DbContextARepository<TResource> : EntityFrameworkCoreRepository<TResource, int>
1213
where TResource : class, IIdentifiable<int>
1314
{
14-
public DbContextARepository(ITargetedFields targetedFields, DbContextResolver<DbContextA> dbContextResolver, IResourceGraph resourceGraph,
15-
IResourceFactory resourceFactory, IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory,
16-
IResourceDefinitionAccessor resourceDefinitionAccessor)
17-
: base(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor)
15+
public DbContextARepository(IJsonApiRequest request, ITargetedFields targetedFields, DbContextResolver<DbContextA> dbContextResolver,
16+
IResourceGraph resourceGraph, IResourceFactory resourceFactory, IResourceDefinitionAccessor resourceDefinitionAccessor,
17+
IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory)
18+
: base(request, targetedFields, dbContextResolver, resourceGraph, resourceFactory, resourceDefinitionAccessor, constraintProviders, loggerFactory)
1819
{
1920
}
2021
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using JetBrains.Annotations;
22
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Middleware;
34
using JsonApiDotNetCore.Queries;
45
using JsonApiDotNetCore.Repositories;
56
using JsonApiDotNetCore.Resources;
@@ -11,10 +12,10 @@ namespace MultiDbContextExample.Repositories;
1112
public sealed class DbContextBRepository<TResource> : EntityFrameworkCoreRepository<TResource, int>
1213
where TResource : class, IIdentifiable<int>
1314
{
14-
public DbContextBRepository(ITargetedFields targetedFields, DbContextResolver<DbContextB> dbContextResolver, IResourceGraph resourceGraph,
15-
IResourceFactory resourceFactory, IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory,
16-
IResourceDefinitionAccessor resourceDefinitionAccessor)
17-
: base(targetedFields, dbContextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory, resourceDefinitionAccessor)
15+
public DbContextBRepository(IJsonApiRequest request, ITargetedFields targetedFields, DbContextResolver<DbContextB> dbContextResolver,
16+
IResourceGraph resourceGraph, IResourceFactory resourceFactory, IResourceDefinitionAccessor resourceDefinitionAccessor,
17+
IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory)
18+
: base(request, targetedFields, dbContextResolver, resourceGraph, resourceFactory, resourceDefinitionAccessor, constraintProviders, loggerFactory)
1819
{
1920
}
2021
}

Diff for: src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs

+6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using JetBrains.Annotations;
2+
using JsonApiDotNetCore.Resources;
23
using JsonApiDotNetCore.Resources.Annotations;
34

45
namespace JsonApiDotNetCore.Configuration;
@@ -38,6 +39,11 @@ public sealed class ResourceType
3839
/// </summary>
3940
public IReadOnlySet<ResourceType> DirectlyDerivedTypes { get; internal set; } = new HashSet<ResourceType>();
4041

42+
/// <summary>
43+
/// When <c>true</c>, this resource type uses optimistic concurrency.
44+
/// </summary>
45+
public bool IsVersioned => ClrType.IsOrImplementsInterface<IVersionedIdentifiable>();
46+
4147
/// <summary>
4248
/// Exposed resource attributes and relationships. See https://jsonapi.org/format/#document-resource-object-fields. When using resource inheritance, this
4349
/// includes the attributes and relationships from base types.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using JetBrains.Annotations;
2+
3+
namespace JsonApiDotNetCore.Resources;
4+
5+
/// <summary>
6+
/// Defines the basic contract for a JSON:API resource that uses optimistic concurrency. All resource classes must implement
7+
/// <see cref="IVersionedIdentifiable{TId, TVersion}" />.
8+
/// </summary>
9+
public interface IVersionedIdentifiable : IIdentifiable
10+
{
11+
/// <summary>
12+
/// The value for element 'version' in a JSON:API request or response.
13+
/// </summary>
14+
string? Version { get; set; }
15+
}
16+
17+
/// <summary>
18+
/// When implemented by a class, indicates to JsonApiDotNetCore that the class represents a JSON:API resource that uses optimistic concurrency.
19+
/// </summary>
20+
/// <typeparam name="TId">
21+
/// The resource identifier type.
22+
/// </typeparam>
23+
/// <typeparam name="TVersion">
24+
/// The database vendor-specific type that is used to store the concurrency token.
25+
/// </typeparam>
26+
[PublicAPI]
27+
public interface IVersionedIdentifiable<TId, TVersion> : IIdentifiable<TId>, IVersionedIdentifiable
28+
{
29+
/// <summary>
30+
/// The concurrency token, which is used to detect if the resource was modified by another user since the moment this resource was last retrieved.
31+
/// </summary>
32+
TVersion ConcurrencyToken { get; set; }
33+
34+
/// <summary>
35+
/// Represents a database column where random data is written to on updates, in order to force a concurrency check during relationship updates.
36+
/// </summary>
37+
Guid ConcurrencyValue { get; set; }
38+
}

Diff for: src/JsonApiDotNetCore.Annotations/TypeExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public static bool IsOrImplementsInterface<TInterface>(this Type? source)
1313
/// <summary>
1414
/// Whether the specified source type implements or equals the specified interface. This overload enables to test for an open generic interface.
1515
/// </summary>
16-
private static bool IsOrImplementsInterface(this Type? source, Type interfaceType)
16+
public static bool IsOrImplementsInterface(this Type? source, Type interfaceType)
1717
{
1818
ArgumentGuard.NotNull(interfaceType, nameof(interfaceType));
1919

+13
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+
}

Diff for: 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
}
+91
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+
}

Diff for: 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()

Diff for: src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs

+6
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,12 @@ public ResourceGraphBuilder Add(Type resourceClrType, Type? idClrType = null, st
207207
return this;
208208
}
209209

210+
if (resourceClrType.IsOrImplementsInterface<IVersionedIdentifiable>() && !resourceClrType.IsOrImplementsInterface(typeof(IVersionedIdentifiable<,>)))
211+
{
212+
throw new InvalidConfigurationException(
213+
$"Resource type '{resourceClrType}' implements 'IVersionedIdentifiable', but not 'IVersionedIdentifiable<TId, TVersion>'.");
214+
}
215+
210216
if (resourceClrType.IsOrImplementsInterface<IIdentifiable>())
211217
{
212218
string effectivePublicName = publicName ?? FormatResourceName(resourceClrType);

Diff for: src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs

+21-2
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,9 @@ public virtual async Task<IActionResult> PostAsync([FromBody] TResource resource
206206

207207
TResource? newResource = await _create.CreateAsync(resource, cancellationToken);
208208

209-
string resourceId = (newResource ?? resource).StringId!;
210-
string locationUrl = $"{HttpContext.Request.Path}/{resourceId}";
209+
TResource resultResource = newResource ?? resource;
210+
string? resourceVersion = resultResource.GetVersion();
211+
string locationUrl = $"{HttpContext.Request.Path}/{resultResource.StringId}{(resourceVersion != null ? $";v~{resourceVersion}" : null)}";
211212

212213
if (newResource == null)
213214
{
@@ -221,6 +222,9 @@ public virtual async Task<IActionResult> PostAsync([FromBody] TResource resource
221222
/// <summary>
222223
/// Adds resources to a to-many relationship. Example: <code><![CDATA[
223224
/// POST /articles/1/revisions HTTP/1.1
225+
/// ]]></code> Example:
226+
/// <code><![CDATA[
227+
/// POST /articles/1;v~8/revisions HTTP/1.1
224228
/// ]]></code>
225229
/// </summary>
226230
/// <param name="id">
@@ -262,6 +266,9 @@ public virtual async Task<IActionResult> PostRelationshipAsync(TId id, string re
262266
/// Updates the attributes and/or relationships of an existing resource. Only the values of sent attributes are replaced. And only the values of sent
263267
/// relationships are replaced. Example: <code><![CDATA[
264268
/// PATCH /articles/1 HTTP/1.1
269+
/// ]]></code> Example:
270+
/// <code><![CDATA[
271+
/// PATCH /articles/1;v~8 HTTP/1.1
265272
/// ]]></code>
266273
/// </summary>
267274
public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] TResource resource, CancellationToken cancellationToken)
@@ -295,7 +302,13 @@ public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] TResource
295302
/// PATCH /articles/1/relationships/author HTTP/1.1
296303
/// ]]></code> Example:
297304
/// <code><![CDATA[
305+
/// PATCH /articles/1;v~8/relationships/author HTTP/1.1
306+
/// ]]></code> Example:
307+
/// <code><![CDATA[
298308
/// PATCH /articles/1/relationships/revisions HTTP/1.1
309+
/// ]]></code> Example:
310+
/// <code><![CDATA[
311+
/// PATCH /articles/1;v~8/relationships/revisions HTTP/1.1
299312
/// ]]></code>
300313
/// </summary>
301314
/// <param name="id">
@@ -335,6 +348,9 @@ public virtual async Task<IActionResult> PatchRelationshipAsync(TId id, string r
335348
/// <summary>
336349
/// Deletes an existing resource. Example: <code><![CDATA[
337350
/// DELETE /articles/1 HTTP/1.1
351+
/// ]]></code> Example:
352+
/// <code><![CDATA[
353+
/// DELETE /articles/1;v~8 HTTP/1.1
338354
/// ]]></code>
339355
/// </summary>
340356
public virtual async Task<IActionResult> DeleteAsync(TId id, CancellationToken cancellationToken)
@@ -357,6 +373,9 @@ public virtual async Task<IActionResult> DeleteAsync(TId id, CancellationToken c
357373
/// <summary>
358374
/// Removes resources from a to-many relationship. Example: <code><![CDATA[
359375
/// DELETE /articles/1/relationships/revisions HTTP/1.1
376+
/// ]]></code> Example:
377+
/// <code><![CDATA[
378+
/// DELETE /articles/1;v~8/relationships/revisions HTTP/1.1
360379
/// ]]></code>
361380
/// </summary>
362381
/// <param name="id">

0 commit comments

Comments
 (0)