Skip to content

Redesign conversion between JSON objects and ASP.NET models #1091

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 49 commits into from
Oct 27, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
8534c41
Replaced suppression with fix
Sep 20, 2021
f502628
Fixed: do not duplicate the list of JSON:API errors in meta stack tra…
Sep 20, 2021
afe119d
Breaking: Added option to write request body in meta when unable to r…
Sep 20, 2021
2b47438
Breaking: Added option to allow unknown attribute/relationship keys i…
Sep 22, 2021
2b1c32b
Fixed invalid test
Sep 23, 2021
0192e39
Rewrite of reading the request body and converting it into models.
Sep 29, 2021
d247ba2
Updated error message for unknown attribute/relationship
Sep 29, 2021
8dbafd2
Updated error message for unknown resource type
Sep 29, 2021
d243dc3
Unified error messages about the absense or presense of 'id' and 'lid'
Sep 29, 2021
4c3bf71
Unified error messages about the absense of 'type'
Sep 29, 2021
1700c00
Unified error messages about incompatible types
Sep 29, 2021
d4549e8
Revert the use of different exception, because this way the request b…
Sep 29, 2021
c2e31d3
Unified error messages about mismatches in 'id' and 'lid' values
Sep 29, 2021
5ea961f
Additional unification of error messages
Sep 29, 2021
6244ef7
Unified error messages for failed type conversion
Sep 29, 2021
72f02b0
Fix cibuild
Sep 30, 2021
fe10147
Unified error messages about data presense and its value: null/object…
Sep 30, 2021
2322ac8
Unified remaining error messages
Sep 30, 2021
d254ac9
Sealed types and reduced dependencies
Sep 30, 2021
0923c8b
Fixed broken test on linux
Sep 30, 2021
6e0d287
Adapter renames:
Oct 1, 2021
169acc7
Added missing assertions on request body in error meta
Oct 1, 2021
317f293
Refactorings:
Oct 1, 2021
65b1f03
Enhanced existing tests: Assert on resource type when `included` cont…
Oct 2, 2021
5198265
Fixed the number of resource definition callbacks for sparse fieldsets
Oct 4, 2021
64cc4e2
Refactor: removed OperationContainer.Kind because IJsonApiRequest.Wri…
Oct 4, 2021
1e6d18e
Added test to capture the current behavior to return data:null for vo…
Oct 4, 2021
cab3dc6
Improved tests for includes
Oct 5, 2021
d8510aa
Rewrite of rendering the response body from models
Oct 5, 2021
055b93f
Avoid closure in hot code path to reduce allocations
Oct 6, 2021
a7c6009
Cleanup reader and writer
Oct 6, 2021
428b06b
Removed old code
Oct 6, 2021
05f155e
Fixed: crash in test serializer on assertion failure
Oct 6, 2021
fbdcefb
Removed RequestScopedServiceProvider
Oct 6, 2021
7fcb58e
Use sets for include expressions
Oct 6, 2021
f8d71f2
Fixed: return Content-Length header in HEAD response
Oct 6, 2021
ff763f1
Reorganized JADNC.Serialization namespace
Oct 6, 2021
80fe2b2
Created custom exception for remaining errors
Oct 6, 2021
f6caf3a
Fixed: call ResourceDefinition.OnApplyIncludes for all children, even…
Oct 6, 2021
0efde1b
Renamed ResourceContext to ResourceType and exposed it through relati…
Oct 7, 2021
f7e7da4
Moved logic to build resource graph from DbContext into ResourceGraph…
Oct 8, 2021
3d98057
Opened up ResponseModelAdapter for extensibility
Oct 8, 2021
2e3659f
Check off roadmap entry
Oct 11, 2021
2c4e75c
Review feedback
Oct 20, 2021
7158595
Simplified existing tests
Oct 25, 2021
7c6684f
Added extra test for data:null in relationship
Oct 25, 2021
4a8bfdd
Added test for broken resource linkage
Oct 25, 2021
0f41f85
Ported existing unit tests and changed how included[] is built.
Oct 26, 2021
5dfb6ea
Fixed cibuild
Oct 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Unified error messages about mismatches in 'id' and 'lid' values
  • Loading branch information
Bart Koelman committed Sep 30, 2021
commit c2e31d322fc4fd5e95fabe84816990d06b2773a9
22 changes: 0 additions & 22 deletions src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Serialization.Objects;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Net;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
Expand Down Expand Up @@ -34,75 +33,7 @@ protected ResourceIdentityAdapter(IResourceGraph resourceGraph, IResourceFactory
ArgumentGuard.NotNull(state, nameof(state));

ResourceContext resourceContext = ConvertType(identity, requirements, state);

if (state.Request.Kind != EndpointKind.AtomicOperations)
{
AssertHasNoLid(identity, state);
}

AssertNoIdWithLid(identity, state);

if (requirements.IdConstraint == JsonElementConstraint.Required)
{
AssertHasIdOrLid(identity, state);
}
else if (requirements.IdConstraint == JsonElementConstraint.Forbidden)
{
AssertHasNoId(identity, state);
}

if (requirements.IdValue != null && identity.Id != null && identity.Id != requirements.IdValue)
{
using (state.Position.PushElement("id"))
{
if (state.Request.Kind == EndpointKind.AtomicOperations)
{
throw new DeserializationException(state.Position, "Resource ID mismatch between 'ref.id' and 'data.id' element.",
$"Expected resource with ID '{requirements.IdValue}' in 'data.id', instead of '{identity.Id}'.");
}

throw new JsonApiException(new ErrorObject(HttpStatusCode.Conflict)
{
Title = "Resource ID mismatch between request body and endpoint URL.",
Detail = $"Expected resource ID '{requirements.IdValue}', instead of '{identity.Id}'.",
Source = new ErrorSource
{
Pointer = state.Position.ToSourcePointer()
}
});
}
}

if (requirements.LidValue != null && identity.Lid != null && identity.Lid != requirements.LidValue)
{
using (state.Position.PushElement("lid"))
{
throw new DeserializationException(state.Position, "Resource local ID mismatch between 'ref.lid' and 'data.lid' element.",
$"Expected resource with local ID '{requirements.LidValue}' in 'data.lid', instead of '{identity.Lid}'.");
}
}

if (requirements.IdValue != null && identity.Lid != null)
{
using (state.Position.PushElement("lid"))
{
throw new DeserializationException(state.Position, "Resource identity mismatch between 'ref.id' and 'data.lid' element.",
$"Expected resource with ID '{requirements.IdValue}' in 'data.id', instead of '{identity.Lid}' in 'data.lid'.");
}
}

if (requirements.LidValue != null && identity.Id != null)
{
using (state.Position.PushElement("id"))
{
throw new DeserializationException(state.Position, "Resource identity mismatch between 'ref.lid' and 'data.id' element.",
$"Expected resource with local ID '{requirements.LidValue}' in 'data.lid', instead of '{identity.Id}' in 'data.id'.");
}
}

IIdentifiable resource = _resourceFactory.CreateInstance(resourceContext.ResourceType);
AssignStringId(identity, resource, state);
resource.LocalId = identity.Lid;
IIdentifiable resource = CreateResource(identity, requirements, resourceContext.ResourceType, state);

return (resource, resourceContext);
}
Expand Down Expand Up @@ -148,6 +79,34 @@ private static void AssertIsCompatibleResourceType(ResourceContext actual, Resou
}
}

private IIdentifiable CreateResource(IResourceIdentity identity, ResourceIdentityRequirements requirements, Type resourceType,
RequestAdapterState state)
{
if (state.Request.Kind != EndpointKind.AtomicOperations)
{
AssertHasNoLid(identity, state);
}

AssertNoIdWithLid(identity, state);

if (requirements.IdConstraint == JsonElementConstraint.Required)
{
AssertHasIdOrLid(identity, requirements, state);
}
else if (requirements.IdConstraint == JsonElementConstraint.Forbidden)
{
AssertHasNoId(identity, state);
}

AssertSameIdValue(identity, requirements.IdValue, state);
AssertSameLidValue(identity, requirements.LidValue, state);

IIdentifiable resource = _resourceFactory.CreateInstance(resourceType);
AssignStringId(identity, resource, state);
resource.LocalId = identity.Lid;
return resource;
}

private static void AssertHasNoLid(IResourceIdentity identity, RequestAdapterState state)
{
if (identity.Lid != null)
Expand All @@ -165,14 +124,25 @@ private static void AssertNoIdWithLid(IResourceIdentity identity, RequestAdapter
}
}

private static void AssertHasIdOrLid(IResourceIdentity identity, RequestAdapterState state)
private static void AssertHasIdOrLid(IResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state)
{
if (identity.Id == null && identity.Lid == null)
string message = null;

if (requirements.IdValue != null && identity.Id == null)
{
message = "The 'id' element is required.";
}
else if (requirements.LidValue != null && identity.Lid == null)
{
string message = state.Request.Kind == EndpointKind.AtomicOperations
? "The 'id' or 'lid' element is required."
: "The 'id' element is required.";
message = "The 'lid' element is required.";
}
else if (identity.Id == null && identity.Lid == null)
{
message = state.Request.Kind == EndpointKind.AtomicOperations ? "The 'id' or 'lid' element is required." : "The 'id' element is required.";
}

if (message != null)
{
throw new ModelConversionException(state.Position, message, null);
}
}
Expand All @@ -186,18 +156,39 @@ private static void AssertHasNoId(IResourceIdentity identity, RequestAdapterStat
}
}

private void AssignStringId(IResourceIdentity identity, IIdentifiable resource, RequestAdapterState state)
private static void AssertSameIdValue(IResourceIdentity identity, string expected, RequestAdapterState state)
{
if (identity.Id != null)
if (expected != null && identity.Id != expected)
{
using IDisposable _ = state.Position.PushElement("id");

throw new ModelConversionException(state.Position, "Conflicting 'id' values found.", $"Expected '{expected}' instead of '{identity.Id}'.",
HttpStatusCode.Conflict);
}
}

private static void AssertSameLidValue(IResourceIdentity identity, string expected, RequestAdapterState state)
{
if (expected != null && identity.Lid != expected)
{
using IDisposable _ = state.Position.PushElement("lid");

throw new ModelConversionException(state.Position, "Conflicting 'lid' values found.", $"Expected '{expected}' instead of '{identity.Lid}'.",
HttpStatusCode.Conflict);
}
}

private void AssignStringId(IResourceIdentity identity, IIdentifiable resource, RequestAdapterState state)
{
if (identity.Id != null)
{
try
{
resource.StringId = identity.Id;
}
catch (FormatException exception)
{
using IDisposable _ = state.Position.PushElement("id");
throw new DeserializationException(state.Position, null, exception.Message);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1136,14 +1136,14 @@ public async Task Cannot_update_on_resource_ID_mismatch_between_ref_and_data()
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync<Document>(route, requestBody);

// Assert
httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity);
httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict);

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

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

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

// Assert
httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity);
httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict);

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

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

Expand Down Expand Up @@ -1240,9 +1240,9 @@ public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_da

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

[Fact]
Expand Down Expand Up @@ -1290,9 +1290,9 @@ public async Task Cannot_update_on_mixture_of_local_ID_and_ID_between_ref_and_da

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

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -998,8 +998,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>

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

Expand Down