Skip to content

Commit 7302906

Browse files
committed
Fixed: Do not allow the use of 'lid' when client-generated IDs are required
1 parent 491b003 commit 7302906

File tree

5 files changed

+74
-9
lines changed

5 files changed

+74
-9
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ private ResourceIdentityRequirements CreateRefRequirements(RequestAdapterState s
125125
return new ResourceIdentityRequirements
126126
{
127127
EvaluateIdConstraint = resourceType =>
128-
ResourceIdentityRequirements.DoEvaluateIdConstraint(resourceType, state.Request.WriteOperation, _options.ClientIdGeneration)
128+
ResourceIdentityRequirements.DoEvaluateIdConstraint(resourceType, state.Request.WriteOperation, _options.ClientIdGeneration),
129+
EvaluateAllowLid = resourceType =>
130+
ResourceIdentityRequirements.DoEvaluateAllowLid(resourceType, state.Request.WriteOperation, _options.ClientIdGeneration)
129131
};
130132
}
131133

@@ -135,6 +137,7 @@ private static ResourceIdentityRequirements CreateDataRequirements(AtomicReferen
135137
{
136138
ResourceType = refResult.ResourceType,
137139
EvaluateIdConstraint = refRequirements.EvaluateIdConstraint,
140+
EvaluateAllowLid = refRequirements.EvaluateAllowLid,
138141
IdValue = refResult.Resource.StringId,
139142
LidValue = refResult.Resource.LocalId,
140143
RelationshipName = refResult.Relationship?.PublicName

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections;
2+
using JsonApiDotNetCore.Middleware;
23
using JsonApiDotNetCore.Resources;
34
using JsonApiDotNetCore.Resources.Annotations;
45
using JsonApiDotNetCore.Serialization.Objects;
@@ -71,6 +72,7 @@ private static SingleOrManyData<ResourceIdentifierObject> ToIdentifierData(Singl
7172
{
7273
ResourceType = relationship.RightType,
7374
EvaluateIdConstraint = _ => JsonElementConstraint.Required,
75+
EvaluateAllowLid = _ => state.Request.Kind == EndpointKind.AtomicOperations,
7476
RelationshipName = relationship.PublicName
7577
};
7678

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,20 @@ private static void AssertIsCompatibleResourceType(ResourceType actual, Resource
9696
private IIdentifiable CreateResource(ResourceIdentity identity, ResourceIdentityRequirements requirements, ResourceType resourceType,
9797
RequestAdapterState state)
9898
{
99-
if (state.Request.Kind != EndpointKind.AtomicOperations)
99+
AssertNoIdWithLid(identity, state);
100+
101+
bool allowLid = requirements.EvaluateAllowLid?.Invoke(resourceType) ?? false;
102+
103+
if (!allowLid)
100104
{
101105
AssertHasNoLid(identity, state);
102106
}
103107

104-
AssertNoIdWithLid(identity, state);
105-
106108
JsonElementConstraint? idConstraint = requirements.EvaluateIdConstraint?.Invoke(resourceType);
107109

108110
if (idConstraint == JsonElementConstraint.Required)
109111
{
110-
AssertHasIdOrLid(identity, requirements, state);
112+
AssertHasIdOrLid(identity, requirements, allowLid, state);
111113
}
112114
else if (idConstraint == JsonElementConstraint.Forbidden)
113115
{
@@ -128,7 +130,10 @@ private static void AssertHasNoLid(ResourceIdentity identity, RequestAdapterStat
128130
if (identity.Lid != null)
129131
{
130132
using IDisposable _ = state.Position.PushElement("lid");
131-
throw new ModelConversionException(state.Position, "The 'lid' element is not supported at this endpoint.", null);
133+
134+
throw state.Request.Kind == EndpointKind.AtomicOperations
135+
? new ModelConversionException(state.Position, "The 'lid' element cannot be used because a client-generated ID is required.", null)
136+
: new ModelConversionException(state.Position, "The 'lid' element is not supported at this endpoint.", null);
132137
}
133138
}
134139

@@ -140,7 +145,7 @@ private static void AssertNoIdWithLid(ResourceIdentity identity, RequestAdapterS
140145
}
141146
}
142147

143-
private static void AssertHasIdOrLid(ResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state)
148+
private static void AssertHasIdOrLid(ResourceIdentity identity, ResourceIdentityRequirements requirements, bool allowLid, RequestAdapterState state)
144149
{
145150
string? message = null;
146151

@@ -154,7 +159,7 @@ private static void AssertHasIdOrLid(ResourceIdentity identity, ResourceIdentity
154159
}
155160
else if (identity.Id == null && identity.Lid == null)
156161
{
157-
message = state.Request.Kind == EndpointKind.AtomicOperations ? "The 'id' or 'lid' element is required." : "The 'id' element is required.";
162+
message = allowLid ? "The 'id' or 'lid' element is required." : "The 'id' element is required.";
158163
}
159164

160165
if (message != null)

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ public sealed class ResourceIdentityRequirements
2121
/// </summary>
2222
public Func<ResourceType, JsonElementConstraint?>? EvaluateIdConstraint { get; init; }
2323

24+
/// <summary>
25+
/// When not null, provides a callback to indicate whether the "lid" element can be used instead of the "id" element. Defaults to <c>false</c>.
26+
/// </summary>
27+
public Func<ResourceType, bool>? EvaluateAllowLid { get; init; }
28+
2429
/// <summary>
2530
/// When not null, indicates what the value of the "id" element must be.
2631
/// </summary>
@@ -50,4 +55,15 @@ public sealed class ResourceIdentityRequirements
5055
}
5156
: JsonElementConstraint.Required;
5257
}
58+
59+
internal static bool DoEvaluateAllowLid(ResourceType resourceType, WriteOperationKind? writeOperation, ClientIdGenerationMode globalClientIdGeneration)
60+
{
61+
if (writeOperation == null)
62+
{
63+
return false;
64+
}
65+
66+
ClientIdGenerationMode clientIdGeneration = resourceType.ClientIdGeneration ?? globalClientIdGeneration;
67+
return !(writeOperation == WriteOperationKind.CreateResource && clientIdGeneration == ClientIdGenerationMode.Required);
68+
}
5369
}

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ public async Task Cannot_create_resource_for_missing_client_generated_ID()
175175

176176
ErrorObject error = responseDocument.Errors[0];
177177
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
178-
error.Title.Should().Be("Failed to deserialize request body: The 'id' or 'lid' element is required.");
178+
error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required.");
179179
error.Detail.Should().BeNull();
180180
error.Source.ShouldNotBeNull();
181181
error.Source.Pointer.Should().Be("/atomic:operations[0]/data");
@@ -281,6 +281,45 @@ public async Task Cannot_create_resource_for_incompatible_ID()
281281
error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty());
282282
}
283283

284+
[Fact]
285+
public async Task Cannot_create_resource_with_local_ID()
286+
{
287+
// Arrange
288+
var requestBody = new
289+
{
290+
atomic__operations = new[]
291+
{
292+
new
293+
{
294+
op = "add",
295+
data = new
296+
{
297+
type = "textLanguages",
298+
lid = "new-server-id"
299+
}
300+
}
301+
}
302+
};
303+
304+
const string route = "/operations";
305+
306+
// Act
307+
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync<Document>(route, requestBody);
308+
309+
// Assert
310+
httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity);
311+
312+
responseDocument.Errors.ShouldHaveCount(1);
313+
314+
ErrorObject error = responseDocument.Errors[0];
315+
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
316+
error.Title.Should().Be("Failed to deserialize request body: The 'lid' element cannot be used because a client-generated ID is required.");
317+
error.Detail.Should().BeNull();
318+
error.Source.ShouldNotBeNull();
319+
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/lid");
320+
error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty());
321+
}
322+
284323
[Fact]
285324
public async Task Cannot_create_resource_for_ID_and_local_ID()
286325
{

0 commit comments

Comments
 (0)