Skip to content

Commit 11af2d2

Browse files
author
Chris Santero
committed
support updating to-one relationship with null
1 parent f81047a commit 11af2d2

9 files changed

Lines changed: 115 additions & 42 deletions

File tree

JSONAPI.EntityFramework.Tests.TestWebApp/Models/Post.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ public class Post
1717

1818
public DateTimeOffset Created { get; set; }
1919

20-
[Required]
2120
[JsonIgnore]
2221
public string AuthorId { get; set; }
2322

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"posts": [
3+
{
4+
"id": "202",
5+
"links": {
6+
"author": null
7+
}
8+
}
9+
]
10+
}

JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithArrayRelationshipValueResponse.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"id": "{{SOME_GUID}}",
55
"status": "500",
66
"title": "JSONAPI.Json.JsonApiFormatter+BadRequestException",
7-
"detail": "The value of the relationship must be an object.",
7+
"detail": "The value of a to-many relationship must be an object.",
88
"stackTrace": "{{STACK_TRACE}}",
99
"inner": null
1010
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"posts": [
3+
{
4+
"id": "202",
5+
"title": "Post 2",
6+
"content": "Post 2 content",
7+
"created": "2015-02-05T08:10:00+00:00",
8+
"links": {
9+
"author": null,
10+
"comments": [ "104" ],
11+
"tags": [ "302", "303" ]
12+
}
13+
}
14+
]
15+
}

JSONAPI.EntityFramework.Tests/Acceptance/Fixtures/Posts/Responses/PutWithStringRelationshipValueResponse.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"id": "{{SOME_GUID}}",
55
"status": "500",
66
"title": "JSONAPI.Json.JsonApiFormatter+BadRequestException",
7-
"detail": "The value of the relationship must be an object.",
7+
"detail": "The value of a to-one relationship must be an object or null.",
88
"stackTrace": "{{STACK_TRACE}}",
99
"inner": null
1010
}

JSONAPI.EntityFramework.Tests/Acceptance/PostsTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,36 @@ public async Task PutWithToOneUpdate()
182182
}
183183
}
184184

185+
[TestMethod]
186+
[DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")]
187+
[DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")]
188+
[DeploymentItem(@"Acceptance\Data\PostTagLink.csv", @"Acceptance\Data")]
189+
[DeploymentItem(@"Acceptance\Data\Tag.csv", @"Acceptance\Data")]
190+
[DeploymentItem(@"Acceptance\Data\User.csv", @"Acceptance\Data")]
191+
public async Task PutWithNullToOneUpdate()
192+
{
193+
using (var effortConnection = GetEffortConnection())
194+
{
195+
var response = await SubmitPut(effortConnection, "posts/202", @"Acceptance\Fixtures\Posts\Requests\PutWithNullToOneUpdateRequest.json");
196+
197+
response.StatusCode.Should().Be(HttpStatusCode.OK);
198+
await AssertResponseContent(response, @"Acceptance\Fixtures\Posts\Responses\PutWithNullToOneUpdateResponse.json");
199+
200+
using (var dbContext = new TestDbContext(effortConnection, false))
201+
{
202+
var allPosts = dbContext.Posts.ToArray();
203+
allPosts.Length.Should().Be(4);
204+
var actualPost = allPosts.First(t => t.Id == "202");
205+
actualPost.Id.Should().Be("202");
206+
actualPost.Title.Should().Be("Post 2");
207+
actualPost.Content.Should().Be("Post 2 content");
208+
actualPost.Created.Should().Be(new DateTimeOffset(2015, 02, 05, 08, 10, 0, new TimeSpan(0)));
209+
actualPost.AuthorId.Should().BeNull();
210+
actualPost.Tags.Select(t => t.Id).Should().BeEquivalentTo("302", "303");
211+
}
212+
}
213+
}
214+
185215
[TestMethod]
186216
[DeploymentItem(@"Acceptance\Data\Comment.csv", @"Acceptance\Data")]
187217
[DeploymentItem(@"Acceptance\Data\Post.csv", @"Acceptance\Data")]

JSONAPI.EntityFramework.Tests/JSONAPI.EntityFramework.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@
156156
<EmbeddedResource Include="Acceptance\Fixtures\Posts\Responses\PutWithStringRelationshipValueResponse.json" />
157157
<EmbeddedResource Include="Acceptance\Fixtures\Posts\Responses\PutWithArrayRelationshipValueResponse.json" />
158158
<EmbeddedResource Include="Acceptance\Fixtures\Posts\Requests\PutWithArrayRelationshipValueRequest.json" />
159+
<EmbeddedResource Include="Acceptance\Fixtures\Posts\Responses\PutWithNullToOneUpdateResponse.json" />
160+
<EmbeddedResource Include="Acceptance\Fixtures\Posts\Requests\PutWithNullToOneUpdateRequest.json" />
159161
<None Include="App.Config">
160162
<SubType>Designer</SubType>
161163
</None>

JSONAPI.EntityFramework/EntityFrameworkMaterializer.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -348,10 +348,16 @@ private async Task Merge (Type type, object ephemeral, object material)
348348

349349
if (materialKey != ephemeralKey)
350350
{
351-
object[] idParams = ephemeralKey.EntityKeyValues.Select(ekv => ekv.Value).ToArray();
352-
prop.SetValue(material, await GetByIdAsync(prop.PropertyType, idParams), null);
351+
if (ephemeralKey == null)
352+
{
353+
prop.SetValue(material, null, null);
354+
}
355+
else
356+
{
357+
object[] idParams = ephemeralKey.EntityKeyValues.Select(ekv => ekv.Value).ToArray();
358+
prop.SetValue(material, await GetByIdAsync(prop.PropertyType, idParams), null);
359+
}
353360
}
354-
// else,
355361
}
356362
else
357363
{

JSONAPI/Json/JsonApiFormatter.cs

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -798,14 +798,14 @@ private void DeserializeLinkedResources(object obj, Stream readStream, JsonReade
798798
PropertyInfo prop = _modelManager.GetPropertyForJsonKey(objectType, value);
799799
if (prop != null && !prop.PropertyType.CanWriteAsJsonApiAttribute())
800800
{
801-
if (reader.TokenType != JsonToken.StartObject)
802-
throw new BadRequestException("The value of the relationship must be an object.");
803-
804801
//FIXME: We're really assuming they're ICollections...but testing for that doesn't work for some reason. Break prone!
805802
if (prop.PropertyType.GetInterfaces().Contains(typeof(IEnumerable)) && prop.PropertyType.IsGenericType)
806803
{
807804
// Is a hasMany
808805

806+
if (reader.TokenType != JsonToken.StartObject)
807+
throw new BadRequestException("The value of a to-many relationship must be an object.");
808+
809809
JArray ids = null;
810810
string resourceType = null;
811811

@@ -905,50 +905,61 @@ private void DeserializeLinkedResources(object obj, Stream readStream, JsonReade
905905
{
906906
// Is a belongsTo
907907

908-
string id = null;
909-
string resourceType = null;
910-
911-
while (reader.Read())
908+
if (reader.TokenType == JsonToken.StartObject)
912909
{
913-
if (reader.TokenType == JsonToken.EndObject)
914-
break;
910+
string id = null;
911+
string resourceType = null;
915912

916-
// Not sure what else could even go here, but if it's not a property name, throw an error.
917-
if (reader.TokenType != JsonToken.PropertyName)
918-
throw new BadRequestException("Unexpected token: " + reader.TokenType);
913+
while (reader.Read())
914+
{
915+
if (reader.TokenType == JsonToken.EndObject)
916+
break;
919917

920-
var propName = (string)reader.Value;
921-
reader.Read();
918+
// Not sure what else could even go here, but if it's not a property name, throw an error.
919+
if (reader.TokenType != JsonToken.PropertyName)
920+
throw new BadRequestException("Unexpected token: " + reader.TokenType);
922921

923-
if (propName == "id")
924-
{
925-
var idValue = reader.Value;
922+
var propName = (string)reader.Value;
923+
reader.Read();
926924

927-
// The id must be a string.
928-
if (!(idValue is string))
929-
throw new BadRequestException("The value of the `id` property must be a string.");
925+
if (propName == "id")
926+
{
927+
var idValue = reader.Value;
930928

931-
id = (string)idValue;
932-
}
933-
else if (propName == "type")
934-
{
935-
// TODO: we don't do anything with this value yet, but we will need to in order to
936-
// support polymorphic endpoints
937-
resourceType = (string)reader.Value;
929+
// The id must be a string.
930+
if (!(idValue is string))
931+
throw new BadRequestException("The value of the `id` property must be a string.");
932+
933+
id = (string)idValue;
934+
}
935+
else if (propName == "type")
936+
{
937+
// TODO: we don't do anything with this value yet, but we will need to in order to
938+
// support polymorphic endpoints
939+
resourceType = (string)reader.Value;
940+
}
938941
}
939-
}
940942

941-
// The id must be specified.
942-
if (id == null)
943-
throw new BadRequestException("Nothing was specified for the `id` property.");
943+
// The id must be specified.
944+
if (id == null)
945+
throw new BadRequestException("Nothing was specified for the `id` property.");
944946

945-
// The type must be specified.
946-
if (resourceType == null)
947-
throw new BadRequestException("Nothing was specified for the `type` property.");
947+
// The type must be specified.
948+
if (resourceType == null)
949+
throw new BadRequestException("Nothing was specified for the `type` property.");
948950

949-
Type relType = prop.PropertyType;
951+
Type relType = prop.PropertyType;
950952

951-
prop.SetValue(obj, GetById(relType, id));
953+
prop.SetValue(obj, GetById(relType, id));
954+
}
955+
else if (reader.TokenType == JsonToken.Null)
956+
{
957+
prop.SetValue(obj, null);
958+
}
959+
else
960+
{
961+
throw new BadRequestException("The value of a to-one relationship must be an object or null.");
962+
}
952963
}
953964

954965
// Tell the MetadataManager that we deserialized this property

0 commit comments

Comments
 (0)