Skip to content

Commit b46d981

Browse files
committed
feat: added support for updating cyclic relations
1 parent 89d55b6 commit b46d981

File tree

5 files changed

+238
-2
lines changed

5 files changed

+238
-2
lines changed

JsonApiDotnetCore.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ Global
204204
{09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|x86.ActiveCfg = Release|Any CPU
205205
{09C0C8D8-B721-4955-8889-55CB149C3B5C}.Release|x86.Build.0 = Release|Any CPU
206206
{DF9BFD82-D937-4907-B0B4-64670417115F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
207+
{DF9BFD82-D937-4907-B0B4-64670417115F}.Debug|Any CPU.Build.0 = Debug|Any CPU
207208
EndGlobalSection
208209
GlobalSection(SolutionProperties) = preSolution
209210
HideSolutionNode = FALSE

src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
4040

4141
modelBuilder.Entity<ArticleTag>()
4242
.HasKey(bc => new { bc.ArticleId, bc.TagId });
43+
44+
45+
modelBuilder.Entity<TodoItem>()
46+
.HasOne(t => t.DependentTodoItem);
47+
48+
modelBuilder.Entity<TodoItem>()
49+
.HasMany(t => t.ChildrenTodoItems)
50+
.WithOne(t => t.ParentTodoItem)
51+
.HasForeignKey(t => t.ParentTodoItemId);
52+
53+
4354
}
4455

4556
public DbSet<TodoItem> TodoItems { get; set; }

src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using JsonApiDotNetCore.Models;
34

45
namespace JsonApiDotNetCoreExample.Models
@@ -30,7 +31,7 @@ public TodoItem()
3031
public DateTime? UpdatedDate { get; set; }
3132

3233

33-
34+
3435
public int? OwnerId { get; set; }
3536
public int? AssigneeId { get; set; }
3637
public Guid? CollectionId { get; set; }
@@ -43,5 +44,19 @@ public TodoItem()
4344

4445
[HasOne("collection")]
4546
public virtual TodoItemCollection Collection { get; set; }
47+
48+
public virtual int? DependentTodoItemId { get; set; }
49+
[HasOne("dependent-on-todo")]
50+
public virtual TodoItem DependentTodoItem { get; set; }
51+
52+
53+
54+
55+
// cyclical structure
56+
public virtual int? ParentTodoItemId {get; set;}
57+
[HasOne("parent-todo")]
58+
public virtual TodoItem ParentTodoItem { get; set; }
59+
[HasMany("children-todos")]
60+
public virtual List<TodoItem> ChildrenTodoItems { get; set; }
4661
}
4762
}

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,14 +330,39 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
330330
if ((relationship.Key.TypeId as Type).IsAssignableFrom(typeof(HasManyAttribute)))
331331
{
332332
await _context.Entry(oldEntity).Collection(relationship.Key.InternalRelationshipName).LoadAsync();
333-
relationship.Key.SetValue(oldEntity, relationship.Value);
333+
var value = CheckForSelfReferingUpdate((IEnumerable<object>)relationship.Value, oldEntity);
334+
relationship.Key.SetValue(oldEntity, value);
334335
}
335336
}
336337
}
337338
await _context.SaveChangesAsync();
338339
return oldEntity;
339340
}
340341

342+
object CheckForSelfReferingUpdate(IEnumerable<object> relatedEntities, TEntity oldEntity)
343+
{
344+
var entity = relatedEntities.FirstOrDefault();
345+
var list = new List<TEntity>();
346+
bool refersSelf = false;
347+
if (entity?.GetType() == typeof(TEntity))
348+
{
349+
foreach (TEntity e in relatedEntities)
350+
{
351+
if (oldEntity.StringId == e.StringId)
352+
{
353+
list.Add(oldEntity);
354+
refersSelf = true;
355+
}
356+
else
357+
{
358+
list.Add(e);
359+
}
360+
}
361+
}
362+
return (refersSelf ? list : relatedEntities);
363+
364+
}
365+
341366
/// <inheritdoc />
342367
public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
343368
{

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,190 @@ public UpdatingRelationshipsTests(TestFixture<TestStartup> fixture)
4141

4242
}
4343

44+
[Fact]
45+
public async Task Can_Update_Cyclic_ToMany_Relationship_By_Patching_Resource()
46+
{
47+
// Arrange
48+
var todoItem = _todoItemFaker.Generate();
49+
var strayTodoItem = _todoItemFaker.Generate();
50+
_context.TodoItems.Add(todoItem);
51+
_context.TodoItems.Add(strayTodoItem);
52+
_context.SaveChanges();
53+
54+
55+
var builder = new WebHostBuilder()
56+
.UseStartup<Startup>();
57+
58+
var server = new TestServer(builder);
59+
var client = server.CreateClient();
60+
61+
// Act
62+
var content = new
63+
{
64+
data = new
65+
{
66+
type = "todo-items",
67+
id = todoItem.Id,
68+
relationships = new Dictionary<string, object>
69+
{
70+
{ "children-todos", new
71+
{
72+
data = new object[]
73+
{
74+
new { type = "todo-items", id = $"{todoItem.Id}" },
75+
new { type = "todo-items", id = $"{strayTodoItem.Id}" }
76+
}
77+
78+
}
79+
}
80+
}
81+
}
82+
};
83+
84+
var httpMethod = new HttpMethod("PATCH");
85+
var route = $"/api/v1/todo-items/{todoItem.Id}";
86+
var request = new HttpRequestMessage(httpMethod, route);
87+
88+
string serializedContent = JsonConvert.SerializeObject(content);
89+
request.Content = new StringContent(serializedContent);
90+
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
91+
92+
93+
// Act
94+
var response = await client.SendAsync(request);
95+
var body = await response.Content.ReadAsStringAsync();
96+
_context = _fixture.GetService<AppDbContext>();
97+
98+
var updatedTodoItem = _context.TodoItems.AsNoTracking()
99+
.Where(ti => ti.Id == todoItem.Id)
100+
.Include(ti => ti.ChildrenTodoItems).First();
101+
102+
updatedTodoItem.ChildrenTodoItems.Any((ti) => ti.Id == todoItem.Id);
103+
Assert.Contains(updatedTodoItem.ChildrenTodoItems, (ti) => ti.Id == todoItem.Id);
104+
}
105+
106+
[Fact]
107+
public async Task Can_Update_Cyclic_ToOne_Relationship_By_Patching_Resource()
108+
{
109+
// Arrange
110+
var todoItem = _todoItemFaker.Generate();
111+
var strayTodoItem = _todoItemFaker.Generate();
112+
_context.TodoItems.Add(todoItem);
113+
_context.SaveChanges();
114+
115+
116+
var builder = new WebHostBuilder()
117+
.UseStartup<Startup>();
118+
119+
var server = new TestServer(builder);
120+
var client = server.CreateClient();
121+
122+
// Act
123+
var content = new
124+
{
125+
data = new
126+
{
127+
type = "todo-items",
128+
id = todoItem.Id,
129+
relationships = new Dictionary<string, object>
130+
{
131+
{ "dependent-on-todo", new
132+
{
133+
data = new { type = "todo-items", id = $"{todoItem.Id}" }
134+
}
135+
}
136+
}
137+
}
138+
};
139+
140+
var httpMethod = new HttpMethod("PATCH");
141+
var route = $"/api/v1/todo-items/{todoItem.Id}";
142+
var request = new HttpRequestMessage(httpMethod, route);
143+
144+
string serializedContent = JsonConvert.SerializeObject(content);
145+
request.Content = new StringContent(serializedContent);
146+
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
147+
148+
149+
// Act
150+
var response = await client.SendAsync(request);
151+
var body = await response.Content.ReadAsStringAsync();
152+
_context = _fixture.GetService<AppDbContext>();
153+
154+
155+
var updatedTodoItem = _context.TodoItems.AsNoTracking()
156+
.Where(ti => ti.Id == todoItem.Id)
157+
.Include(ti => ti.DependentTodoItem).First();
158+
159+
Assert.Equal(todoItem.Id, updatedTodoItem.DependentTodoItemId);
160+
}
161+
162+
[Fact]
163+
public async Task Can_Update_Both_Cyclic_ToOne_And_ToMany_Relationship_By_Patching_Resource()
164+
{
165+
// Arrange
166+
var todoItem = _todoItemFaker.Generate();
167+
var strayTodoItem = _todoItemFaker.Generate();
168+
_context.TodoItems.Add(todoItem);
169+
_context.TodoItems.Add(strayTodoItem);
170+
_context.SaveChanges();
171+
172+
173+
var builder = new WebHostBuilder()
174+
.UseStartup<Startup>();
175+
176+
var server = new TestServer(builder);
177+
var client = server.CreateClient();
178+
179+
// Act
180+
var content = new
181+
{
182+
data = new
183+
{
184+
type = "todo-items",
185+
id = todoItem.Id,
186+
relationships = new Dictionary<string, object>
187+
{
188+
{ "dependent-on-todo", new
189+
{
190+
data = new { type = "todo-items", id = $"{todoItem.Id}" }
191+
}
192+
},
193+
{ "children-todos", new
194+
{
195+
data = new object[]
196+
{
197+
new { type = "todo-items", id = $"{todoItem.Id}" },
198+
new { type = "todo-items", id = $"{strayTodoItem.Id}" }
199+
}
200+
}
201+
}
202+
}
203+
}
204+
};
205+
206+
var httpMethod = new HttpMethod("PATCH");
207+
var route = $"/api/v1/todo-items/{todoItem.Id}";
208+
var request = new HttpRequestMessage(httpMethod, route);
209+
210+
string serializedContent = JsonConvert.SerializeObject(content);
211+
request.Content = new StringContent(serializedContent);
212+
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
213+
214+
215+
// Act
216+
var response = await client.SendAsync(request);
217+
var body = await response.Content.ReadAsStringAsync();
218+
_context = _fixture.GetService<AppDbContext>();
219+
220+
221+
var updatedTodoItem = _context.TodoItems.AsNoTracking()
222+
.Where(ti => ti.Id == todoItem.Id)
223+
.Include(ti => ti.ParentTodoItem).First();
224+
225+
Assert.Equal(todoItem.Id, updatedTodoItem.ParentTodoItemId);
226+
}
227+
44228
[Fact]
45229
public async Task Can_Update_ToMany_Relationship_By_Patching_Resource()
46230
{

0 commit comments

Comments
 (0)