Skip to content

Commit 6741efd

Browse files
committed
fix(JsonApiDeserializer): if hasOne is nullable allow it to be set null
1 parent fa6ee6b commit 6741efd

File tree

7 files changed

+395
-275
lines changed

7 files changed

+395
-275
lines changed

Diff for: src/JsonApiDotNetCore/Models/HasOneAttribute.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,14 @@ public HasOneAttribute(string publicName, Link documentLinks = Link.All, bool ca
3939
? $"{InternalRelationshipName}Id"
4040
: _explicitIdentifiablePropertyName;
4141

42+
/// <summary>
43+
/// Sets the value of the property identified by this attribute
44+
/// </summary>
45+
/// <param name="entity">The target object</param>
46+
/// <param name="newValue">The new property value</param>
4247
public override void SetValue(object entity, object newValue)
4348
{
44-
var propertyName = (newValue.GetType() == Type)
49+
var propertyName = (newValue?.GetType() == Type)
4550
? InternalRelationshipName
4651
: IdentifiablePropertyName;
4752

Diff for: src/JsonApiDotNetCore/Models/RelationshipAttribute.cs

+22
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,28 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI
1919
public Link DocumentLinks { get; } = Link.All;
2020
public bool CanInclude { get; }
2121

22+
public bool TryGetHasOne(out HasOneAttribute result)
23+
{
24+
if (IsHasOne)
25+
{
26+
result = (HasOneAttribute)this;
27+
return true;
28+
}
29+
result = null;
30+
return false;
31+
}
32+
33+
public bool TryGetHasMany(out HasManyAttribute result)
34+
{
35+
if (IsHasMany)
36+
{
37+
result = (HasManyAttribute)this;
38+
return true;
39+
}
40+
result = null;
41+
return false;
42+
}
43+
2244
public abstract void SetValue(object entity, object newValue);
2345

2446
public override string ToString()

Diff for: src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs

+6-4
Original file line numberDiff line numberDiff line change
@@ -207,15 +207,17 @@ private object SetHasOneRelationship(object entity,
207207

208208
var rio = (ResourceIdentifierObject)relationshipData.ExposedData;
209209

210-
if (rio == null) return entity;
211-
212-
var newValue = rio.Id;
213-
214210
var foreignKey = attr.IdentifiablePropertyName;
215211
var entityProperty = entityProperties.FirstOrDefault(p => p.Name == foreignKey);
216212
if (entityProperty == null)
217213
throw new JsonApiException(400, $"{contextEntity.EntityType.Name} does not contain a foreign key property '{foreignKey}' for has one relationship '{attr.InternalRelationshipName}'");
218214

215+
// e.g. PATCH /articles
216+
// {... { "relationships":{ "Owner": { "data" :null } } } }
217+
if (rio == null && Nullable.GetUnderlyingType(entityProperty.PropertyType) == null)
218+
throw new JsonApiException(400, $"Cannot set required relationship identifier '{attr.IdentifiablePropertyName}' to null.");
219+
220+
var newValue = rio?.Id ?? null;
219221
var convertedValue = TypeHelper.ConvertType(newValue, entityProperty.PropertyType);
220222

221223
_jsonApiContext.RelationshipsToUpdate[relationshipAttr] = convertedValue;

Diff for: src/JsonApiDotNetCore/Services/EntityResourceService.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public virtual async Task UpdateRelationshipsAsync(TId id, string relationshipNa
133133
.Relationships
134134
.FirstOrDefault(r => r.InternalRelationshipName == relationshipName);
135135

136-
var relationshipIds = relationships.Select(r => r.Id?.ToString());
136+
var relationshipIds = relationships.Select(r => r?.Id?.ToString());
137137

138138
await _entities.UpdateRelationshipsAsync(entity, relationship, relationshipIds);
139139
}

Diff for: test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs

+95
Original file line numberDiff line numberDiff line change
@@ -127,5 +127,100 @@ public async Task Can_Update_ToOne_Relationship_ThroughLink()
127127
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
128128
Assert.NotNull(todoItemsOwner);
129129
}
130+
131+
[Fact]
132+
public async Task Can_Delete_Relationship_By_Patching_Resource()
133+
{
134+
// arrange
135+
var person = _personFaker.Generate();
136+
var todoItem = _todoItemFaker.Generate();
137+
todoItem.Owner = person;
138+
139+
_context.People.Add(person);
140+
_context.TodoItems.Add(todoItem);
141+
_context.SaveChanges();
142+
143+
var builder = new WebHostBuilder()
144+
.UseStartup<Startup>();
145+
146+
var server = new TestServer(builder);
147+
var client = server.CreateClient();
148+
149+
var content = new
150+
{
151+
data = new
152+
{
153+
type = "todo-items",
154+
relationships = new
155+
{
156+
owner = new
157+
{
158+
data = (object)null
159+
}
160+
}
161+
}
162+
};
163+
164+
var httpMethod = new HttpMethod("PATCH");
165+
var route = $"/api/v1/todo-items/{todoItem.Id}";
166+
var request = new HttpRequestMessage(httpMethod, route);
167+
request.Content = new StringContent(JsonConvert.SerializeObject(content));
168+
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
169+
170+
// Act
171+
var response = await client.SendAsync(request);
172+
173+
// Assert
174+
var todoItemResult = _context.TodoItems
175+
.AsNoTracking()
176+
.Include(t => t.Owner)
177+
.Single(t => t.Id == todoItem.Id);
178+
179+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
180+
Assert.Null(todoItemResult.Owner);
181+
}
182+
183+
[Fact]
184+
public async Task Can_Delete_Relationship_By_Patching_Relationship()
185+
{
186+
// arrange
187+
var person = _personFaker.Generate();
188+
var todoItem = _todoItemFaker.Generate();
189+
todoItem.Owner = person;
190+
191+
_context.People.Add(person);
192+
_context.TodoItems.Add(todoItem);
193+
_context.SaveChanges();
194+
195+
var builder = new WebHostBuilder()
196+
.UseStartup<Startup>();
197+
198+
var server = new TestServer(builder);
199+
var client = server.CreateClient();
200+
201+
var content = new
202+
{
203+
data = (object)null
204+
};
205+
206+
var httpMethod = new HttpMethod("PATCH");
207+
var route = $"/api/v1/todo-items/{todoItem.Id}/relationships/owner";
208+
var request = new HttpRequestMessage(httpMethod, route);
209+
210+
request.Content = new StringContent(JsonConvert.SerializeObject(content));
211+
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
212+
213+
// Act
214+
var response = await client.SendAsync(request);
215+
216+
// Assert
217+
var todoItemResult = _context.TodoItems
218+
.AsNoTracking()
219+
.Include(t => t.Owner)
220+
.Single(t => t.Id == todoItem.Id);
221+
222+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
223+
Assert.Null(todoItemResult.Owner);
224+
}
130225
}
131226
}

0 commit comments

Comments
 (0)