Skip to content

Commit 5f618d2

Browse files
author
Bart Koelman
committed
Unified error messages about mismatches in 'id' and 'lid' values
1 parent 05a639d commit 5f618d2

File tree

5 files changed

+84
-116
lines changed

5 files changed

+84
-116
lines changed

src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs

-22
This file was deleted.

src/JsonApiDotNetCore/Serialization/RequestAdapters/RelationshipDataAdapter.cs

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using System.Collections.Generic;
44
using System.Linq;
55
using JsonApiDotNetCore.Configuration;
6-
using JsonApiDotNetCore.Middleware;
76
using JsonApiDotNetCore.Resources;
87
using JsonApiDotNetCore.Resources.Annotations;
98
using JsonApiDotNetCore.Serialization.Objects;

src/JsonApiDotNetCore/Serialization/RequestAdapters/ResourceIdentityAdapter.cs

+68-77
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Net;
33
using JsonApiDotNetCore.Configuration;
4-
using JsonApiDotNetCore.Errors;
54
using JsonApiDotNetCore.Middleware;
65
using JsonApiDotNetCore.Resources;
76
using JsonApiDotNetCore.Resources.Annotations;
@@ -34,75 +33,7 @@ protected ResourceIdentityAdapter(IResourceGraph resourceGraph, IResourceFactory
3433
ArgumentGuard.NotNull(state, nameof(state));
3534

3635
ResourceContext resourceContext = ConvertType(identity, requirements, state);
37-
38-
if (state.Request.Kind != EndpointKind.AtomicOperations)
39-
{
40-
AssertHasNoLid(identity, state);
41-
}
42-
43-
AssertNoIdWithLid(identity, state);
44-
45-
if (requirements.IdConstraint == JsonElementConstraint.Required)
46-
{
47-
AssertHasIdOrLid(identity, state);
48-
}
49-
else if (requirements.IdConstraint == JsonElementConstraint.Forbidden)
50-
{
51-
AssertHasNoId(identity, state);
52-
}
53-
54-
if (requirements.IdValue != null && identity.Id != null && identity.Id != requirements.IdValue)
55-
{
56-
using (state.Position.PushElement("id"))
57-
{
58-
if (state.Request.Kind == EndpointKind.AtomicOperations)
59-
{
60-
throw new DeserializationException(state.Position, "Resource ID mismatch between 'ref.id' and 'data.id' element.",
61-
$"Expected resource with ID '{requirements.IdValue}' in 'data.id', instead of '{identity.Id}'.");
62-
}
63-
64-
throw new JsonApiException(new ErrorObject(HttpStatusCode.Conflict)
65-
{
66-
Title = "Resource ID mismatch between request body and endpoint URL.",
67-
Detail = $"Expected resource ID '{requirements.IdValue}', instead of '{identity.Id}'.",
68-
Source = new ErrorSource
69-
{
70-
Pointer = state.Position.ToSourcePointer()
71-
}
72-
});
73-
}
74-
}
75-
76-
if (requirements.LidValue != null && identity.Lid != null && identity.Lid != requirements.LidValue)
77-
{
78-
using (state.Position.PushElement("lid"))
79-
{
80-
throw new DeserializationException(state.Position, "Resource local ID mismatch between 'ref.lid' and 'data.lid' element.",
81-
$"Expected resource with local ID '{requirements.LidValue}' in 'data.lid', instead of '{identity.Lid}'.");
82-
}
83-
}
84-
85-
if (requirements.IdValue != null && identity.Lid != null)
86-
{
87-
using (state.Position.PushElement("lid"))
88-
{
89-
throw new DeserializationException(state.Position, "Resource identity mismatch between 'ref.id' and 'data.lid' element.",
90-
$"Expected resource with ID '{requirements.IdValue}' in 'data.id', instead of '{identity.Lid}' in 'data.lid'.");
91-
}
92-
}
93-
94-
if (requirements.LidValue != null && identity.Id != null)
95-
{
96-
using (state.Position.PushElement("id"))
97-
{
98-
throw new DeserializationException(state.Position, "Resource identity mismatch between 'ref.lid' and 'data.id' element.",
99-
$"Expected resource with local ID '{requirements.LidValue}' in 'data.lid', instead of '{identity.Id}' in 'data.id'.");
100-
}
101-
}
102-
103-
IIdentifiable resource = _resourceFactory.CreateInstance(resourceContext.ResourceType);
104-
AssignStringId(identity, resource, state);
105-
resource.LocalId = identity.Lid;
36+
IIdentifiable resource = CreateResource(identity, requirements, resourceContext.ResourceType, state);
10637

10738
return (resource, resourceContext);
10839
}
@@ -148,6 +79,34 @@ private static void AssertIsCompatibleResourceType(ResourceContext actual, Resou
14879
}
14980
}
15081

82+
private IIdentifiable CreateResource(IResourceIdentity identity, ResourceIdentityRequirements requirements, Type resourceType,
83+
RequestAdapterState state)
84+
{
85+
if (state.Request.Kind != EndpointKind.AtomicOperations)
86+
{
87+
AssertHasNoLid(identity, state);
88+
}
89+
90+
AssertNoIdWithLid(identity, state);
91+
92+
if (requirements.IdConstraint == JsonElementConstraint.Required)
93+
{
94+
AssertHasIdOrLid(identity, requirements, state);
95+
}
96+
else if (requirements.IdConstraint == JsonElementConstraint.Forbidden)
97+
{
98+
AssertHasNoId(identity, state);
99+
}
100+
101+
AssertSameIdValue(identity, requirements.IdValue, state);
102+
AssertSameLidValue(identity, requirements.LidValue, state);
103+
104+
IIdentifiable resource = _resourceFactory.CreateInstance(resourceType);
105+
AssignStringId(identity, resource, state);
106+
resource.LocalId = identity.Lid;
107+
return resource;
108+
}
109+
151110
private static void AssertHasNoLid(IResourceIdentity identity, RequestAdapterState state)
152111
{
153112
if (identity.Lid != null)
@@ -165,14 +124,25 @@ private static void AssertNoIdWithLid(IResourceIdentity identity, RequestAdapter
165124
}
166125
}
167126

168-
private static void AssertHasIdOrLid(IResourceIdentity identity, RequestAdapterState state)
127+
private static void AssertHasIdOrLid(IResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state)
169128
{
170-
if (identity.Id == null && identity.Lid == null)
129+
string message = null;
130+
131+
if (requirements.IdValue != null && identity.Id == null)
132+
{
133+
message = "The 'id' element is required.";
134+
}
135+
else if (requirements.LidValue != null && identity.Lid == null)
171136
{
172-
string message = state.Request.Kind == EndpointKind.AtomicOperations
173-
? "The 'id' or 'lid' element is required."
174-
: "The 'id' element is required.";
137+
message = "The 'lid' element is required.";
138+
}
139+
else if (identity.Id == null && identity.Lid == null)
140+
{
141+
message = state.Request.Kind == EndpointKind.AtomicOperations ? "The 'id' or 'lid' element is required." : "The 'id' element is required.";
142+
}
175143

144+
if (message != null)
145+
{
176146
throw new ModelConversionException(state.Position, message, null);
177147
}
178148
}
@@ -186,18 +156,39 @@ private static void AssertHasNoId(IResourceIdentity identity, RequestAdapterStat
186156
}
187157
}
188158

189-
private void AssignStringId(IResourceIdentity identity, IIdentifiable resource, RequestAdapterState state)
159+
private static void AssertSameIdValue(IResourceIdentity identity, string expected, RequestAdapterState state)
190160
{
191-
if (identity.Id != null)
161+
if (expected != null && identity.Id != expected)
192162
{
193163
using IDisposable _ = state.Position.PushElement("id");
194164

165+
throw new ModelConversionException(state.Position, "Conflicting 'id' values found.", $"Expected '{expected}' instead of '{identity.Id}'.",
166+
HttpStatusCode.Conflict);
167+
}
168+
}
169+
170+
private static void AssertSameLidValue(IResourceIdentity identity, string expected, RequestAdapterState state)
171+
{
172+
if (expected != null && identity.Lid != expected)
173+
{
174+
using IDisposable _ = state.Position.PushElement("lid");
175+
176+
throw new ModelConversionException(state.Position, "Conflicting 'lid' values found.", $"Expected '{expected}' instead of '{identity.Lid}'.",
177+
HttpStatusCode.Conflict);
178+
}
179+
}
180+
181+
private void AssignStringId(IResourceIdentity identity, IIdentifiable resource, RequestAdapterState state)
182+
{
183+
if (identity.Id != null)
184+
{
195185
try
196186
{
197187
resource.StringId = identity.Id;
198188
}
199189
catch (FormatException exception)
200190
{
191+
using IDisposable _ = state.Position.PushElement("id");
201192
throw new DeserializationException(state.Position, null, exception.Message);
202193
}
203194
}

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs

+14-14
Original file line numberDiff line numberDiff line change
@@ -1136,14 +1136,14 @@ public async Task Cannot_update_on_resource_ID_mismatch_between_ref_and_data()
11361136
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync<Document>(route, requestBody);
11371137

11381138
// Assert
1139-
httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity);
1139+
httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict);
11401140

11411141
responseDocument.Errors.Should().HaveCount(1);
11421142

11431143
ErrorObject error = responseDocument.Errors[0];
1144-
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
1145-
error.Title.Should().Be("Failed to deserialize request body: Resource ID mismatch between 'ref.id' and 'data.id' element.");
1146-
error.Detail.Should().Be($"Expected resource with ID '{performerId1}' in 'data.id', instead of '{performerId2}'.");
1144+
error.StatusCode.Should().Be(HttpStatusCode.Conflict);
1145+
error.Title.Should().Be("Failed to deserialize request body: Conflicting 'id' values found.");
1146+
error.Detail.Should().Be($"Expected '{performerId1}' instead of '{performerId2}'.");
11471147
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id");
11481148
}
11491149

@@ -1184,14 +1184,14 @@ public async Task Cannot_update_on_resource_local_ID_mismatch_between_ref_and_da
11841184
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync<Document>(route, requestBody);
11851185

11861186
// Assert
1187-
httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity);
1187+
httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict);
11881188

11891189
responseDocument.Errors.Should().HaveCount(1);
11901190

11911191
ErrorObject error = responseDocument.Errors[0];
1192-
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
1193-
error.Title.Should().Be("Failed to deserialize request body: Resource local ID mismatch between 'ref.lid' and 'data.lid' element.");
1194-
error.Detail.Should().Be("Expected resource with local ID 'local-1' in 'data.lid', instead of 'local-2'.");
1192+
error.StatusCode.Should().Be(HttpStatusCode.Conflict);
1193+
error.Title.Should().Be("Failed to deserialize request body: Conflicting 'lid' values found.");
1194+
error.Detail.Should().Be("Expected 'local-1' instead of 'local-2'.");
11951195
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/lid");
11961196
}
11971197

@@ -1240,9 +1240,9 @@ public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_da
12401240

12411241
ErrorObject error = responseDocument.Errors[0];
12421242
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
1243-
error.Title.Should().Be("Failed to deserialize request body: Resource identity mismatch between 'ref.id' and 'data.lid' element.");
1244-
error.Detail.Should().Be($"Expected resource with ID '{performerId}' in 'data.id', instead of 'local-1' in 'data.lid'.");
1245-
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/lid");
1243+
error.Title.Should().Be("Failed to deserialize request body: The 'id' element is required.");
1244+
error.Detail.Should().BeNull();
1245+
error.Source.Pointer.Should().Be("/atomic:operations[0]/data");
12461246
}
12471247

12481248
[Fact]
@@ -1290,9 +1290,9 @@ public async Task Cannot_update_on_mixture_of_local_ID_and_ID_between_ref_and_da
12901290

12911291
ErrorObject error = responseDocument.Errors[0];
12921292
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
1293-
error.Title.Should().Be("Failed to deserialize request body: Resource identity mismatch between 'ref.lid' and 'data.id' element.");
1294-
error.Detail.Should().Be($"Expected resource with local ID 'local-1' in 'data.lid', instead of '{performerId}' in 'data.id'.");
1295-
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id");
1293+
error.Title.Should().Be("Failed to deserialize request body: The 'lid' element is required.");
1294+
error.Detail.Should().BeNull();
1295+
error.Source.Pointer.Should().Be("/atomic:operations[0]/data");
12961296
}
12971297

12981298
[Fact]

test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -998,8 +998,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
998998

999999
ErrorObject error = responseDocument.Errors[0];
10001000
error.StatusCode.Should().Be(HttpStatusCode.Conflict);
1001-
error.Title.Should().Be("Resource ID mismatch between request body and endpoint URL.");
1002-
error.Detail.Should().Be($"Expected resource ID '{existingWorkItems[1].StringId}', instead of '{existingWorkItems[0].StringId}'.");
1001+
error.Title.Should().Be("Failed to deserialize request body: Conflicting 'id' values found.");
1002+
error.Detail.Should().Be($"Expected '{existingWorkItems[1].StringId}' instead of '{existingWorkItems[0].StringId}'.");
10031003
error.Source.Pointer.Should().Be("/data/id");
10041004
}
10051005

0 commit comments

Comments
 (0)