Skip to content

Commit 8542ee7

Browse files
Bart Koelmanbkoelman
Bart Koelman
authored andcommitted
Basic plumbing of version through the pipeline
1 parent 71e5398 commit 8542ee7

21 files changed

+151
-15
lines changed

src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs

Lines changed: 6 additions & 0 deletions
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.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
}

src/JsonApiDotNetCore.Annotations/TypeExtensions.cs

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

src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs

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

src/JsonApiDotNetCore/Controllers/JsonApiController.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,18 @@ public override async Task<IActionResult> GetAsync(CancellationToken cancellatio
4747
}
4848

4949
/// <inheritdoc />
50+
// The {version} parameter is allowed, but ignored. It occurs in rendered links, because POST/PATCH/DELETE use it.
5051
[HttpGet("{id}")]
52+
[HttpGet("{id};v~{version}")]
5153
[HttpHead("{id}")]
54+
[HttpHead("{id};v~{version}")]
5255
public override async Task<IActionResult> GetAsync(TId id, CancellationToken cancellationToken)
5356
{
5457
return await base.GetAsync(id, cancellationToken);
5558
}
5659

5760
/// <inheritdoc />
61+
// No {version} parameter, because it does not occur in rendered links.
5862
[HttpGet("{id}/{relationshipName}")]
5963
[HttpHead("{id}/{relationshipName}")]
6064
public override async Task<IActionResult> GetSecondaryAsync(TId id, string relationshipName, CancellationToken cancellationToken)
@@ -63,8 +67,11 @@ public override async Task<IActionResult> GetSecondaryAsync(TId id, string relat
6367
}
6468

6569
/// <inheritdoc />
70+
// The {version} parameter is allowed, but ignored. It occurs in rendered links, because POST/PATCH/DELETE use it.
6671
[HttpGet("{id}/relationships/{relationshipName}")]
72+
[HttpGet("{id};v~{version}/relationships/{relationshipName}")]
6773
[HttpHead("{id}/relationships/{relationshipName}")]
74+
[HttpHead("{id};v~{version}/relationships/{relationshipName}")]
6875
public override async Task<IActionResult> GetRelationshipAsync(TId id, string relationshipName, CancellationToken cancellationToken)
6976
{
7077
return await base.GetRelationshipAsync(id, relationshipName, cancellationToken);
@@ -79,6 +86,7 @@ public override async Task<IActionResult> PostAsync([FromBody] TResource resourc
7986

8087
/// <inheritdoc />
8188
[HttpPost("{id}/relationships/{relationshipName}")]
89+
[HttpPost("{id};v~{version}/relationships/{relationshipName}")]
8290
public override async Task<IActionResult> PostRelationshipAsync(TId id, string relationshipName, [FromBody] ISet<IIdentifiable> rightResourceIds,
8391
CancellationToken cancellationToken)
8492
{
@@ -87,13 +95,15 @@ public override async Task<IActionResult> PostRelationshipAsync(TId id, string r
8795

8896
/// <inheritdoc />
8997
[HttpPatch("{id}")]
98+
[HttpPatch("{id};v~{version}")]
9099
public override async Task<IActionResult> PatchAsync(TId id, [FromBody] TResource resource, CancellationToken cancellationToken)
91100
{
92101
return await base.PatchAsync(id, resource, cancellationToken);
93102
}
94103

95104
/// <inheritdoc />
96105
[HttpPatch("{id}/relationships/{relationshipName}")]
106+
[HttpPatch("{id};v~{version}/relationships/{relationshipName}")]
97107
public override async Task<IActionResult> PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object? rightValue,
98108
CancellationToken cancellationToken)
99109
{
@@ -102,13 +112,15 @@ public override async Task<IActionResult> PatchRelationshipAsync(TId id, string
102112

103113
/// <inheritdoc />
104114
[HttpDelete("{id}")]
115+
[HttpDelete("{id};v~{version}")]
105116
public override async Task<IActionResult> DeleteAsync(TId id, CancellationToken cancellationToken)
106117
{
107118
return await base.DeleteAsync(id, cancellationToken);
108119
}
109120

110121
/// <inheritdoc />
111122
[HttpDelete("{id}/relationships/{relationshipName}")]
123+
[HttpDelete("{id};v~{version}/relationships/{relationshipName}")]
112124
public override async Task<IActionResult> DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet<IIdentifiable> rightResourceIds,
113125
CancellationToken cancellationToken)
114126
{

src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs

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

src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ private static void SetupResourceRequest(JsonApiRequest request, ResourceType pr
212212
request.IsReadOnly = httpRequest.Method == HttpMethod.Get.Method || httpRequest.Method == HttpMethod.Head.Method;
213213
request.PrimaryResourceType = primaryResourceType;
214214
request.PrimaryId = GetPrimaryRequestId(routeValues);
215+
request.PrimaryVersion = GetPrimaryRequestVersion(routeValues);
215216

216217
string? relationshipName = GetRelationshipNameForSecondaryRequest(routeValues);
217218

@@ -263,6 +264,11 @@ private static void SetupResourceRequest(JsonApiRequest request, ResourceType pr
263264
return routeValues.TryGetValue("id", out object? id) ? (string?)id : null;
264265
}
265266

267+
private static string? GetPrimaryRequestVersion(RouteValueDictionary routeValues)
268+
{
269+
return routeValues.TryGetValue("version", out object? id) ? (string?)id : null;
270+
}
271+
266272
private static string? GetRelationshipNameForSecondaryRequest(RouteValueDictionary routeValues)
267273
{
268274
return routeValues.TryGetValue("relationshipName", out object? routeValue) ? (string?)routeValue : null;

src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs

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

src/JsonApiDotNetCore/Resources/IdentifiableExtensions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,21 @@ public static Type GetClrType(this IIdentifiable identifiable)
4141

4242
return identifiable is IAbstractResourceWrapper abstractResource ? abstractResource.AbstractType : identifiable.GetType();
4343
}
44+
45+
public static string? GetVersion(this IIdentifiable identifiable)
46+
{
47+
ArgumentGuard.NotNull(identifiable, nameof(identifiable));
48+
49+
return identifiable is IVersionedIdentifiable versionedIdentifiable ? versionedIdentifiable.Version : null;
50+
}
51+
52+
public static void SetVersion(this IIdentifiable identifiable, string? version)
53+
{
54+
ArgumentGuard.NotNull(identifiable, nameof(identifiable));
55+
56+
if (identifiable is IVersionedIdentifiable versionedIdentifiable)
57+
{
58+
versionedIdentifiable.Version = version;
59+
}
60+
}
4461
}

src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public sealed class ResourceObjectConverter : JsonObjectConverter<ResourceObject
1818
private static readonly JsonEncodedText TypeText = JsonEncodedText.Encode("type");
1919
private static readonly JsonEncodedText IdText = JsonEncodedText.Encode("id");
2020
private static readonly JsonEncodedText LidText = JsonEncodedText.Encode("lid");
21+
private static readonly JsonEncodedText VersionText = JsonEncodedText.Encode("version");
2122
private static readonly JsonEncodedText MetaText = JsonEncodedText.Encode("meta");
2223
private static readonly JsonEncodedText AttributesText = JsonEncodedText.Encode("attributes");
2324
private static readonly JsonEncodedText RelationshipsText = JsonEncodedText.Encode("relationships");
@@ -84,6 +85,11 @@ public override ResourceObject Read(ref Utf8JsonReader reader, Type typeToConver
8485
resourceObject.Lid = reader.GetString();
8586
break;
8687
}
88+
case "version":
89+
{
90+
resourceObject.Version = reader.GetString();
91+
break;
92+
}
8793
case "attributes":
8894
{
8995
if (resourceType != null)
@@ -238,6 +244,11 @@ public override void Write(Utf8JsonWriter writer, ResourceObject value, JsonSeri
238244
writer.WriteString(LidText, value.Lid);
239245
}
240246

247+
if (value.Version != null)
248+
{
249+
writer.WriteString(VersionText, value.Version);
250+
}
251+
241252
if (!value.Attributes.IsNullOrEmpty())
242253
{
243254
writer.WritePropertyName(AttributesText);

src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs

Lines changed: 4 additions & 0 deletions
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; }

src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ public interface IResourceIdentity
55
public string? Type { get; }
66
public string? Id { get; }
77
public string? Lid { get; }
8+
public string? Version { get; }
89
}

src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ public sealed class ResourceIdentifierObject : 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("meta")]
2529
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2630
public IDictionary<string, object?>? Meta { get; set; }

src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ public sealed class ResourceObject : 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("attributes")]
2529
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
2630
public IDictionary<string, object?>? Attributes { get; set; }

src/JsonApiDotNetCore/Serialization/Request/Adapters/AtomicOperationObjectAdapter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ private WriteOperationKind ConvertOperationCode(AtomicOperationObject atomicOper
107107
if (refResult != null)
108108
{
109109
state.WritableRequest!.PrimaryId = refResult.Resource.StringId;
110+
state.WritableRequest.PrimaryVersion = refResult.Resource.GetVersion();
110111
state.WritableRequest.PrimaryResourceType = refResult.ResourceType;
111112
state.WritableRequest.Relationship = refResult.Relationship;
112113
state.WritableRequest.IsCollection = refResult.Relationship is HasManyAttribute;

src/JsonApiDotNetCore/Serialization/Request/Adapters/RelationshipDataAdapter.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ private static SingleOrManyData<ResourceIdentifierObject> ToIdentifierData(Singl
4141
{
4242
Type = resourceObject.Type,
4343
Id = resourceObject.Id,
44-
Lid = resourceObject.Lid
44+
Lid = resourceObject.Lid,
45+
Version = resourceObject.Version
4546
});
4647
}
4748
else if (data.SingleValue != null)
@@ -50,7 +51,8 @@ private static SingleOrManyData<ResourceIdentifierObject> ToIdentifierData(Singl
5051
{
5152
Type = data.SingleValue.Type,
5253
Id = data.SingleValue.Id,
53-
Lid = data.SingleValue.Lid
54+
Lid = data.SingleValue.Lid,
55+
Version = data.SingleValue.Version
5456
};
5557
}
5658

src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceDataInOperationsRequestAdapter.cs

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

src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ private IIdentifiable CreateResource(IResourceIdentity identity, ResourceIdentit
117117
IIdentifiable resource = _resourceFactory.CreateInstance(resourceClrType);
118118
AssignStringId(identity, resource, state);
119119
resource.LocalId = identity.Lid;
120+
resource.SetVersion(identity.Version);
121+
120122
return resource;
121123
}
122124

0 commit comments

Comments
 (0)