Skip to content

Commit c154330

Browse files
author
Bart Koelman
committed
Basic plumbing of version through the pipeline
1 parent 15f5923 commit c154330

21 files changed

+152
-15
lines changed

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

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using JetBrains.Annotations;
5+
using JsonApiDotNetCore.Resources;
56
using JsonApiDotNetCore.Resources.Annotations;
67

78
namespace JsonApiDotNetCore.Configuration
@@ -30,6 +31,11 @@ public sealed class ResourceType
3031
/// </summary>
3132
public Type IdentityClrType { get; }
3233

34+
/// <summary>
35+
/// When <c>true</c>, this resource type uses optimistic concurrency.
36+
/// </summary>
37+
public bool IsVersioned => ClrType.IsOrImplementsInterface<IVersionedIdentifiable>();
38+
3339
/// <summary>
3440
/// Exposed resource attributes and relationships. See https://jsonapi.org/format/#document-resource-object-fields.
3541
/// </summary>

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

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

211211
TResource? newResource = await _create.CreateAsync(resource, cancellationToken);
212212

213-
string resourceId = (newResource ?? resource).StringId!;
214-
string locationUrl = $"{HttpContext.Request.Path}/{resourceId}";
213+
TResource resultResource = newResource ?? resource;
214+
string? resourceVersion = resultResource.GetVersion();
215+
string locationUrl = $"{HttpContext.Request.Path}/{resultResource.StringId}{(resourceVersion != null ? $";v~{resourceVersion}" : null)}";
215216

216217
if (newResource == null)
217218
{
@@ -225,6 +226,9 @@ public virtual async Task<IActionResult> PostAsync([FromBody] TResource resource
225226
/// <summary>
226227
/// Adds resources to a to-many relationship. Example: <code><![CDATA[
227228
/// POST /articles/1/revisions HTTP/1.1
229+
/// ]]></code> Example:
230+
/// <code><![CDATA[
231+
/// POST /articles/1;v~8/revisions HTTP/1.1
228232
/// ]]></code>
229233
/// </summary>
230234
/// <param name="id">
@@ -266,6 +270,9 @@ public virtual async Task<IActionResult> PostRelationshipAsync(TId id, string re
266270
/// Updates the attributes and/or relationships of an existing resource. Only the values of sent attributes are replaced. And only the values of sent
267271
/// relationships are replaced. Example: <code><![CDATA[
268272
/// PATCH /articles/1 HTTP/1.1
273+
/// ]]></code> Example:
274+
/// <code><![CDATA[
275+
/// PATCH /articles/1;v~8 HTTP/1.1
269276
/// ]]></code>
270277
/// </summary>
271278
public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] TResource resource, CancellationToken cancellationToken)
@@ -299,7 +306,13 @@ public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] TResource
299306
/// PATCH /articles/1/relationships/author HTTP/1.1
300307
/// ]]></code> Example:
301308
/// <code><![CDATA[
309+
/// PATCH /articles/1;v~8/relationships/author HTTP/1.1
310+
/// ]]></code> Example:
311+
/// <code><![CDATA[
302312
/// PATCH /articles/1/relationships/revisions HTTP/1.1
313+
/// ]]></code> Example:
314+
/// <code><![CDATA[
315+
/// PATCH /articles/1;v~8/relationships/revisions HTTP/1.1
303316
/// ]]></code>
304317
/// </summary>
305318
/// <param name="id">
@@ -339,6 +352,9 @@ public virtual async Task<IActionResult> PatchRelationshipAsync(TId id, string r
339352
/// <summary>
340353
/// Deletes an existing resource. Example: <code><![CDATA[
341354
/// DELETE /articles/1 HTTP/1.1
355+
/// ]]></code> Example:
356+
/// <code><![CDATA[
357+
/// DELETE /articles/1;v~8 HTTP/1.1
342358
/// ]]></code>
343359
/// </summary>
344360
public virtual async Task<IActionResult> DeleteAsync(TId id, CancellationToken cancellationToken)
@@ -361,6 +377,9 @@ public virtual async Task<IActionResult> DeleteAsync(TId id, CancellationToken c
361377
/// <summary>
362378
/// Removes resources from a to-many relationship. Example: <code><![CDATA[
363379
/// DELETE /articles/1/relationships/revisions HTTP/1.1
380+
/// ]]></code> Example:
381+
/// <code><![CDATA[
382+
/// DELETE /articles/1;v~8/relationships/revisions HTTP/1.1
364383
/// ]]></code>
365384
/// </summary>
366385
/// <param name="id">

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

+12
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,18 @@ public override async Task<IActionResult> GetAsync(CancellationToken cancellatio
5050
}
5151

5252
/// <inheritdoc />
53+
// The {version} parameter is allowed, but ignored. It occurs in rendered links, because POST/PATCH/DELETE use it.
5354
[HttpGet("{id}")]
55+
[HttpGet("{id};v~{version}")]
5456
[HttpHead("{id}")]
57+
[HttpHead("{id};v~{version}")]
5558
public override async Task<IActionResult> GetAsync(TId id, CancellationToken cancellationToken)
5659
{
5760
return await base.GetAsync(id, cancellationToken);
5861
}
5962

6063
/// <inheritdoc />
64+
// No {version} parameter, because it does not occur in rendered links.
6165
[HttpGet("{id}/{relationshipName}")]
6266
[HttpHead("{id}/{relationshipName}")]
6367
public override async Task<IActionResult> GetSecondaryAsync(TId id, string relationshipName, CancellationToken cancellationToken)
@@ -66,8 +70,11 @@ public override async Task<IActionResult> GetSecondaryAsync(TId id, string relat
6670
}
6771

6872
/// <inheritdoc />
73+
// The {version} parameter is allowed, but ignored. It occurs in rendered links, because POST/PATCH/DELETE use it.
6974
[HttpGet("{id}/relationships/{relationshipName}")]
75+
[HttpGet("{id};v~{version}/relationships/{relationshipName}")]
7076
[HttpHead("{id}/relationships/{relationshipName}")]
77+
[HttpHead("{id};v~{version}/relationships/{relationshipName}")]
7178
public override async Task<IActionResult> GetRelationshipAsync(TId id, string relationshipName, CancellationToken cancellationToken)
7279
{
7380
return await base.GetRelationshipAsync(id, relationshipName, cancellationToken);
@@ -82,6 +89,7 @@ public override async Task<IActionResult> PostAsync([FromBody] TResource resourc
8289

8390
/// <inheritdoc />
8491
[HttpPost("{id}/relationships/{relationshipName}")]
92+
[HttpPost("{id};v~{version}/relationships/{relationshipName}")]
8593
public override async Task<IActionResult> PostRelationshipAsync(TId id, string relationshipName, [FromBody] ISet<IIdentifiable> rightResourceIds,
8694
CancellationToken cancellationToken)
8795
{
@@ -90,13 +98,15 @@ public override async Task<IActionResult> PostRelationshipAsync(TId id, string r
9098

9199
/// <inheritdoc />
92100
[HttpPatch("{id}")]
101+
[HttpPatch("{id};v~{version}")]
93102
public override async Task<IActionResult> PatchAsync(TId id, [FromBody] TResource resource, CancellationToken cancellationToken)
94103
{
95104
return await base.PatchAsync(id, resource, cancellationToken);
96105
}
97106

98107
/// <inheritdoc />
99108
[HttpPatch("{id}/relationships/{relationshipName}")]
109+
[HttpPatch("{id};v~{version}/relationships/{relationshipName}")]
100110
public override async Task<IActionResult> PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object? rightValue,
101111
CancellationToken cancellationToken)
102112
{
@@ -105,13 +115,15 @@ public override async Task<IActionResult> PatchRelationshipAsync(TId id, string
105115

106116
/// <inheritdoc />
107117
[HttpDelete("{id}")]
118+
[HttpDelete("{id};v~{version}")]
108119
public override async Task<IActionResult> DeleteAsync(TId id, CancellationToken cancellationToken)
109120
{
110121
return await base.DeleteAsync(id, cancellationToken);
111122
}
112123

113124
/// <inheritdoc />
114125
[HttpDelete("{id}/relationships/{relationshipName}")]
126+
[HttpDelete("{id};v~{version}/relationships/{relationshipName}")]
115127
public override async Task<IActionResult> DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet<IIdentifiable> rightResourceIds,
116128
CancellationToken cancellationToken)
117129
{

Diff for: src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs

+6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ public interface IJsonApiRequest
1919
/// </summary>
2020
string? PrimaryId { get; }
2121

22+
/// <summary>
23+
/// The version of the primary resource for this request, when using optimistic concurrency. This would be "abc" in "/blogs/123;v~abc/author". This is
24+
/// <c>null</c> when not using optimistic concurrency, and before and after processing operations in an atomic:operations request.
25+
/// </summary>
26+
string? PrimaryVersion { get; }
27+
2228
/// <summary>
2329
/// The primary resource type for this request. This would be "blogs" in "/blogs", "/blogs/123" or "/blogs/123/author". This is <c>null</c> before and
2430
/// after processing operations in an atomic:operations request.

Diff for: src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs

+6
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ private static void SetupResourceRequest(JsonApiRequest request, ResourceType pr
226226
request.IsReadOnly = httpRequest.Method == HttpMethod.Get.Method || httpRequest.Method == HttpMethod.Head.Method;
227227
request.PrimaryResourceType = primaryResourceType;
228228
request.PrimaryId = GetPrimaryRequestId(routeValues);
229+
request.PrimaryVersion = GetPrimaryRequestVersion(routeValues);
229230

230231
string? relationshipName = GetRelationshipNameForSecondaryRequest(routeValues);
231232

@@ -277,6 +278,11 @@ private static void SetupResourceRequest(JsonApiRequest request, ResourceType pr
277278
return routeValues.TryGetValue("id", out object? id) ? (string?)id : null;
278279
}
279280

281+
private static string? GetPrimaryRequestVersion(RouteValueDictionary routeValues)
282+
{
283+
return routeValues.TryGetValue("version", out object? id) ? (string?)id : null;
284+
}
285+
280286
private static string? GetRelationshipNameForSecondaryRequest(RouteValueDictionary routeValues)
281287
{
282288
return routeValues.TryGetValue("relationshipName", out object? routeValue) ? (string?)routeValue : null;

Diff for: src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ public sealed class JsonApiRequest : IJsonApiRequest
1414
/// <inheritdoc />
1515
public string? PrimaryId { get; set; }
1616

17+
/// <inheritdoc />
18+
public string? PrimaryVersion { get; set; }
19+
1720
/// <inheritdoc />
1821
public ResourceType? PrimaryResourceType { get; set; }
1922

@@ -42,6 +45,7 @@ public void CopyFrom(IJsonApiRequest other)
4245

4346
Kind = other.Kind;
4447
PrimaryId = other.PrimaryId;
48+
PrimaryVersion = other.PrimaryVersion;
4549
PrimaryResourceType = other.PrimaryResourceType;
4650
SecondaryResourceType = other.SecondaryResourceType;
4751
Relationship = other.Relationship;
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
namespace JsonApiDotNetCore.Resources
2+
{
3+
/// <summary>
4+
/// Defines the basic contract for a JSON:API resource that uses optimistic concurrency. All resource classes must implement
5+
/// <see cref="IVersionedIdentifiable{TId, TVersion}" />.
6+
/// </summary>
7+
public interface IVersionedIdentifiable : IIdentifiable
8+
{
9+
/// <summary>
10+
/// The value for element 'version' in a JSON:API request or response.
11+
/// </summary>
12+
string? Version { get; set; }
13+
}
14+
15+
/// <summary>
16+
/// When implemented by a class, indicates to JsonApiDotNetCore that the class represents a JSON:API resource that uses optimistic concurrency.
17+
/// </summary>
18+
/// <typeparam name="TId">
19+
/// The resource identifier type.
20+
/// </typeparam>
21+
/// <typeparam name="TVersion">
22+
/// The database vendor-specific type that is used to store the concurrency token.
23+
/// </typeparam>
24+
public interface IVersionedIdentifiable<TId, TVersion> : IIdentifiable<TId>, IVersionedIdentifiable
25+
{
26+
/// <summary>
27+
/// The concurrency token, which is used to detect if the resource was modified by another user since the moment this resource was last retrieved.
28+
/// </summary>
29+
TVersion ConcurrencyToken { get; set; }
30+
}
31+
}

Diff for: src/JsonApiDotNetCore/Resources/IdentifiableExtensions.cs

+17
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,22 @@ public static object GetTypedId(this IIdentifiable identifiable)
3535

3636
return propertyValue!;
3737
}
38+
39+
public static string? GetVersion(this IIdentifiable identifiable)
40+
{
41+
ArgumentGuard.NotNull(identifiable, nameof(identifiable));
42+
43+
return identifiable is IVersionedIdentifiable versionedIdentifiable ? versionedIdentifiable.Version : null;
44+
}
45+
46+
public static void SetVersion(this IIdentifiable identifiable, string? version)
47+
{
48+
ArgumentGuard.NotNull(identifiable, nameof(identifiable));
49+
50+
if (identifiable is IVersionedIdentifiable versionedIdentifiable)
51+
{
52+
versionedIdentifiable.Version = version;
53+
}
54+
}
3855
}
3956
}

Diff for: src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs

+11
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public sealed class ResourceObjectConverter : JsonObjectConverter<ResourceObject
2020
private static readonly JsonEncodedText TypeText = JsonEncodedText.Encode("type");
2121
private static readonly JsonEncodedText IdText = JsonEncodedText.Encode("id");
2222
private static readonly JsonEncodedText LidText = JsonEncodedText.Encode("lid");
23+
private static readonly JsonEncodedText VersionText = JsonEncodedText.Encode("version");
2324
private static readonly JsonEncodedText MetaText = JsonEncodedText.Encode("meta");
2425
private static readonly JsonEncodedText AttributesText = JsonEncodedText.Encode("attributes");
2526
private static readonly JsonEncodedText RelationshipsText = JsonEncodedText.Encode("relationships");
@@ -86,6 +87,11 @@ public override ResourceObject Read(ref Utf8JsonReader reader, Type typeToConver
8687
resourceObject.Lid = reader.GetString();
8788
break;
8889
}
90+
case "version":
91+
{
92+
resourceObject.Version = reader.GetString();
93+
break;
94+
}
8995
case "attributes":
9096
{
9197
if (resourceType != null)
@@ -240,6 +246,11 @@ public override void Write(Utf8JsonWriter writer, ResourceObject value, JsonSeri
240246
writer.WriteString(LidText, value.Lid);
241247
}
242248

249+
if (value.Version != null)
250+
{
251+
writer.WriteString(VersionText, value.Version);
252+
}
253+
243254
if (!value.Attributes.IsNullOrEmpty())
244255
{
245256
writer.WritePropertyName(AttributesText);

Diff for: src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ public sealed class AtomicReference : IResourceIdentity
2121
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2222
public string? Lid { get; set; }
2323

24+
[JsonPropertyName("version")]
25+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
26+
public string? Version { get; set; }
27+
2428
[JsonPropertyName("relationship")]
2529
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2630
public string? Relationship { get; set; }

Diff for: src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ public interface IResourceIdentity
55
public string? Type { get; }
66
public string? Id { get; }
77
public string? Lid { get; }
8+
public string? Version { get; }
89
}
910
}

Diff for: src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ public sealed class ResourceIdentifierObject : IResourceIdentity
2222
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2323
public string? Lid { get; set; }
2424

25+
[JsonPropertyName("version")]
26+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
27+
public string? Version { get; set; }
28+
2529
[JsonPropertyName("meta")]
2630
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2731
public IDictionary<string, object?>? Meta { get; set; }

Diff for: src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ public sealed class ResourceObject : IResourceIdentity
2222
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2323
public string? Lid { get; set; }
2424

25+
[JsonPropertyName("version")]
26+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
27+
public string? Version { get; set; }
28+
2529
[JsonPropertyName("attributes")]
2630
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2731
public IDictionary<string, object?>? Attributes { get; set; }

Diff for: src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicOperationObjectAdapter.cs

+1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ private WriteOperationKind ConvertOperationCode(AtomicOperationObject atomicOper
111111
if (refResult != null)
112112
{
113113
state.WritableRequest!.PrimaryId = refResult.Resource.StringId;
114+
state.WritableRequest.PrimaryVersion = refResult.Resource.GetVersion();
114115
state.WritableRequest.PrimaryResourceType = refResult.ResourceType;
115116
state.WritableRequest.Relationship = refResult.Relationship;
116117
state.WritableRequest.IsCollection = refResult.Relationship is HasManyAttribute;

Diff for: src/JsonApiDotNetCore/Serialization/Request/Adapters/RelationshipDataAdapter.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ private static SingleOrManyData<ResourceIdentifierObject> ToIdentifierData(Singl
4444
{
4545
Type = resourceObject.Type,
4646
Id = resourceObject.Id,
47-
Lid = resourceObject.Lid
47+
Lid = resourceObject.Lid,
48+
Version = resourceObject.Version
4849
});
4950
}
5051
else if (data.SingleValue != null)
@@ -53,7 +54,8 @@ private static SingleOrManyData<ResourceIdentifierObject> ToIdentifierData(Singl
5354
{
5455
Type = data.SingleValue.Type,
5556
Id = data.SingleValue.Id,
56-
Lid = data.SingleValue.Lid
57+
Lid = data.SingleValue.Lid,
58+
Version = data.SingleValue.Version
5759
};
5860
}
5961

Diff for: src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataInOperationsRequestAdapter.cs

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ protected override (IIdentifiable resource, ResourceType resourceType) ConvertRe
2121

2222
state.WritableRequest!.PrimaryResourceType = resourceType;
2323
state.WritableRequest.PrimaryId = resource.StringId;
24+
state.WritableRequest.PrimaryVersion = resource.GetVersion();
2425

2526
return (resource, resourceType);
2627
}

Diff for: src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs

+2
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ private IIdentifiable CreateResource(IResourceIdentity identity, ResourceIdentit
105105
IIdentifiable resource = _resourceFactory.CreateInstance(resourceClrType);
106106
AssignStringId(identity, resource, state);
107107
resource.LocalId = identity.Lid;
108+
resource.SetVersion(identity.Version);
109+
108110
return resource;
109111
}
110112

0 commit comments

Comments
 (0)