Skip to content

Commit 32522d0

Browse files
committed
handle attaching HasOne relationships
1 parent 9dd7719 commit 32522d0

File tree

8 files changed

+121
-3
lines changed

8 files changed

+121
-3
lines changed

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,19 @@ public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshi
8484

8585
public virtual async Task<TEntity> CreateAsync(TEntity entity)
8686
{
87-
AttachHasManyPointers();
87+
AttachRelationships();
8888
_dbSet.Add(entity);
8989

9090
await _context.SaveChangesAsync();
9191
return entity;
9292
}
9393

94+
protected virtual void AttachRelationships()
95+
{
96+
AttachHasManyPointers();
97+
AttachHasOnePointers();
98+
}
99+
94100
/// <summary>
95101
/// This is used to allow creation of HasMany relationships when the
96102
/// dependent side of the relationship already exists.
@@ -107,6 +113,18 @@ private void AttachHasManyPointers()
107113
}
108114
}
109115

116+
/// <summary>
117+
/// This is used to allow creation of HasOne relationships when the
118+
/// independent side of the relationship already exists.
119+
/// </summary>
120+
private void AttachHasOnePointers()
121+
{
122+
var relationships = _jsonApiContext.HasOneRelationshipPointers.Get();
123+
foreach (var relationship in relationships)
124+
if (_context.Entry(relationship.Value).State == EntityState.Detached && _context.EntityIsTracked(relationship.Value) == false)
125+
_context.Entry(relationship.Value).State = EntityState.Unchanged;
126+
}
127+
110128
public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
111129
{
112130
var oldEntity = await GetAsync(id);

src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using System;
2+
using System.Linq;
3+
using JsonApiDotNetCore.Internal;
4+
using JsonApiDotNetCore.Models;
25
using Microsoft.EntityFrameworkCore;
36

47
namespace JsonApiDotNetCore.Extensions
@@ -8,5 +11,23 @@ public static class DbContextExtensions
811
[Obsolete("This is no longer required since the introduction of context.Set<T>", error: false)]
912
public static DbSet<T> GetDbSet<T>(this DbContext context) where T : class
1013
=> context.Set<T>();
14+
15+
/// <summary>
16+
/// Determines whether or not EF is already tracking an entity of the same Type and Id
17+
/// </summary>
18+
public static bool EntityIsTracked(this DbContext context, IIdentifiable entity)
19+
{
20+
if (entity == null)
21+
throw new ArgumentNullException(nameof(entity));
22+
23+
var trackedEntries = context.ChangeTracker
24+
.Entries()
25+
.FirstOrDefault(entry =>
26+
entry.Entity.GetType() == entity.GetType()
27+
&& ((IIdentifiable)entry.Entity).StringId == entity.StringId
28+
);
29+
30+
return trackedEntries != null;
31+
}
1132
}
1233
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using JsonApiDotNetCore.Models;
2+
using System;
3+
using System.Collections.Generic;
4+
5+
namespace JsonApiDotNetCore.Request
6+
{
7+
/// <summary>
8+
/// Stores information to set relationships for the request resource.
9+
/// These relationships must already exist and should not be re-created.
10+
///
11+
/// The expected use case is POST-ing or PATCH-ing
12+
/// an entity with HasOne relationships:
13+
/// <code>
14+
/// {
15+
/// "data": {
16+
/// "type": "photos",
17+
/// "attributes": {
18+
/// "title": "Ember Hamster",
19+
/// "src": "http://example.com/images/productivity.png"
20+
/// },
21+
/// "relationships": {
22+
/// "photographer": {
23+
/// "data": { "type": "people", "id": "2" }
24+
/// }
25+
/// }
26+
/// }
27+
/// }
28+
/// </code>
29+
/// </summary>
30+
public class HasOneRelationshipPointers
31+
{
32+
private Dictionary<Type, IIdentifiable> _hasOneRelationships = new Dictionary<Type, IIdentifiable>();
33+
34+
/// <summary>
35+
/// Add the relationship to the list of relationships that should be
36+
/// set in the repository layer.
37+
/// </summary>
38+
public void Add(Type dependentType, IIdentifiable entity)
39+
=> _hasOneRelationships[dependentType] = entity;
40+
41+
/// <summary>
42+
/// Get all the models that should be associated
43+
/// </summary>
44+
public Dictionary<Type, IIdentifiable> Get() => _hasOneRelationships;
45+
}
46+
}

src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,11 @@ private object SetHasOneRelationship(object entity,
241241
var includedRelationshipObject = GetIncludedRelationship(rio, included, relationshipAttr);
242242
if (includedRelationshipObject != null)
243243
relationshipAttr.SetValue(entity, includedRelationshipObject);
244+
245+
// we need to store the fact that this relationship was included in the payload
246+
// for EF, the repository will use these pointers to make ensure we don't try to
247+
// create resources if they already exist, we just need to create the relationship
248+
_jsonApiContext.HasOneRelationshipPointers.Add(attr.Type, includedRelationshipObject);
244249
}
245250

246251
return entity;

src/JsonApiDotNetCore/Services/IJsonApiContext.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,31 @@ public interface IJsonApiRequest : IJsonApiApplication, IUpdateRequest, IQueryRe
8181
/// </summary>
8282
HasManyRelationshipPointers HasManyRelationshipPointers { get; }
8383

84+
/// <summary>
85+
/// Stores information to set relationships for the request resource.
86+
/// These relationships must already exist and should not be re-created.
87+
///
88+
/// The expected use case is POST-ing or PATCH-ing
89+
/// an entity with HasOne relationships:
90+
/// <code>
91+
/// {
92+
/// "data": {
93+
/// "type": "photos",
94+
/// "attributes": {
95+
/// "title": "Ember Hamster",
96+
/// "src": "http://example.com/images/productivity.png"
97+
/// },
98+
/// "relationships": {
99+
/// "photographer": {
100+
/// "data": { "type": "people", "id": "2" }
101+
/// }
102+
/// }
103+
/// }
104+
/// }
105+
/// </code>
106+
/// </summary>
107+
HasOneRelationshipPointers HasOneRelationshipPointers { get; }
108+
84109
/// <summary>
85110
/// If the request is a bulk json:api v1.1 operations request.
86111
/// This is determined by the `

src/JsonApiDotNetCore/Services/JsonApiContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public JsonApiContext(
5353
public Dictionary<string, object> DocumentMeta { get; set; }
5454
public bool IsBulkOperationRequest { get; set; }
5555
public HasManyRelationshipPointers HasManyRelationshipPointers { get; } = new HasManyRelationshipPointers();
56+
public HasOneRelationshipPointers HasOneRelationshipPointers { get; } = new HasOneRelationshipPointers();
5657

5758
public IJsonApiContext ApplyContext<T>(object controller)
5859
{

test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,11 +294,12 @@ public async Task Can_Post_TodoItem()
294294

295295
// Act
296296
var response = await _fixture.Client.SendAsync(request);
297-
var body = await response.Content.ReadAsStringAsync();
298-
var deserializedBody = (TodoItem)_fixture.GetService<IJsonApiDeSerializer>().Deserialize(body);
299297

300298
// Assert
301299
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
300+
var body = await response.Content.ReadAsStringAsync();
301+
var deserializedBody = (TodoItem)_fixture.GetService<IJsonApiDeSerializer>().Deserialize(body);
302+
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
302303
Assert.Equal(todoItem.Description, deserializedBody.Description);
303304
Assert.Equal(todoItem.CreatedDate.ToString("G"), deserializedBody.CreatedDate.ToString("G"));
304305
Assert.Null(deserializedBody.AchievedDate);

test/UnitTests/Serialization/JsonApiDeSerializerTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ public void Sets_Attribute_Values_On_Included_HasOne_Relationships()
477477
jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary<AttrAttribute, object>());
478478
jsonApiContextMock.Setup(m => m.RelationshipsToUpdate).Returns(new Dictionary<RelationshipAttribute, object>());
479479
jsonApiContextMock.Setup(m => m.HasManyRelationshipPointers).Returns(new HasManyRelationshipPointers());
480+
jsonApiContextMock.Setup(m => m.HasOneRelationshipPointers).Returns(new HasOneRelationshipPointers());
480481

481482
var jsonApiOptions = new JsonApiOptions();
482483
jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions);

0 commit comments

Comments
 (0)