Skip to content

Commit 7376baf

Browse files
committed
fix: reattachment issue
1 parent b616f64 commit 7376baf

File tree

4 files changed

+122
-30
lines changed

4 files changed

+122
-30
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,43 @@
11
using System;
2+
using System.Linq;
3+
using System.Threading.Tasks;
24
using JsonApiDotNetCore.Controllers;
5+
using JsonApiDotNetCore.Data;
36
using JsonApiDotNetCore.Services;
47
using JsonApiDotNetCoreExample.Models;
8+
using Microsoft.AspNetCore.Mvc;
9+
using Microsoft.EntityFrameworkCore;
510
using Microsoft.Extensions.Logging;
611

712
namespace JsonApiDotNetCoreExample.Controllers
813
{
914
public class TodoCollectionsController : JsonApiController<TodoItemCollection, Guid>
1015
{
16+
17+
readonly IDbContextResolver _dbResolver;
18+
1119
public TodoCollectionsController(
20+
IDbContextResolver contextResolver,
1221
IJsonApiContext jsonApiContext,
1322
IResourceService<TodoItemCollection, Guid> resourceService,
1423
ILoggerFactory loggerFactory)
1524
: base(jsonApiContext, resourceService, loggerFactory)
16-
{ }
25+
{
26+
_dbResolver = contextResolver;
27+
28+
}
29+
30+
[HttpPatch("{id}")]
31+
public override async Task<IActionResult> PatchAsync(Guid id, [FromBody] TodoItemCollection entity)
32+
{
33+
if (entity.Name == "PRE-ATTACH-TEST")
34+
{
35+
var targetTodoId = entity.TodoItems.First().Id;
36+
var todoItemContext = _dbResolver.GetDbSet<TodoItem>();
37+
await todoItemContext.Where(ti => ti.Id == targetTodoId).FirstOrDefaultAsync();
38+
}
39+
return await base.PatchAsync(id, entity);
40+
}
41+
1742
}
1843
}

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

+9-25
Original file line numberDiff line numberDiff line change
@@ -336,15 +336,14 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
336336

337337
foreach (var relationship in _jsonApiContext.RelationshipsToUpdate)
338338
{
339-
340339
if ((relationship.Key.TypeId as Type).IsAssignableFrom(typeof(HasOneAttribute)))
341340
{
342341
relationship.Key.SetValue(oldEntity, relationship.Value);
343342
}
344343
if ((relationship.Key.TypeId as Type).IsAssignableFrom(typeof(HasManyAttribute)))
345344
{
346345
await _context.Entry(oldEntity).Collection(relationship.Key.InternalRelationshipName).LoadAsync();
347-
var value = CheckForSelfReferingUpdate((IEnumerable<object>)relationship.Value, oldEntity);
346+
var value = PreventReattachment((IEnumerable<object>)relationship.Value);
348347
relationship.Key.SetValue(oldEntity, value);
349348
}
350349
}
@@ -354,37 +353,22 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
354353
}
355354

356355
/// <summary>
357-
/// In case a relationship is updated where the parent type and related
358-
/// are equal, we need to make sure we don't reattach the parent entity
359-
/// as this will cause entity tracking errors.
356+
/// We need to make sure we're not re-attaching entities when assigning
357+
/// new relationship values. Entities may have been loaded in the change
358+
/// tracker anywhere in the application beyond the control of
359+
/// JsonApiDotNetCore.
360360
/// </summary>
361361
/// <returns>The interpolated related entity collection</returns>
362362
/// <param name="relatedEntities">Related entities.</param>
363-
/// <param name="oldEntity">Old entity.</param>
364-
object CheckForSelfReferingUpdate(IEnumerable<object> relatedEntities, TEntity oldEntity)
363+
object PreventReattachment(IEnumerable<object> relatedEntities)
365364
{
366365
var relatedType = TypeHelper.GetTypeOfList(relatedEntities.GetType());
367-
var list = new List<TEntity>();
368-
bool refersSelf = false;
369-
if (relatedType == typeof(TEntity))
370-
{
371-
foreach (TEntity e in relatedEntities)
372-
{
373-
if (oldEntity.StringId == e.StringId)
374-
{
375-
list.Add(oldEntity);
376-
refersSelf = true;
377-
}
378-
else
379-
{
380-
list.Add(e);
381-
}
382-
}
383-
}
384-
return refersSelf ? list : relatedEntities;
366+
var replaced = relatedEntities.Cast<IIdentifiable>().Select(entity => _context.GetTrackedEntity(entity) ?? entity);
367+
return TypeHelper.ConvertCollection(replaced, relatedType);
385368

386369
}
387370

371+
388372
/// <inheritdoc />
389373
public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
390374
{

src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs

+14-4
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,30 @@ public static IQueryable<object> Set(this DbContext context, Type t)
2828
/// Determines whether or not EF is already tracking an entity of the same Type and Id
2929
/// </summary>
3030
public static bool EntityIsTracked(this DbContext context, IIdentifiable entity)
31+
{
32+
return GetTrackedEntity(context, entity) != null;
33+
}
34+
35+
/// <summary>
36+
/// Determines whether or not EF is already tracking an entity of the same Type and Id
37+
/// and returns that entity.
38+
/// </summary>
39+
public static IIdentifiable GetTrackedEntity(this DbContext context, IIdentifiable entity)
3140
{
3241
if (entity == null)
3342
throw new ArgumentNullException(nameof(entity));
34-
43+
3544
var trackedEntries = context.ChangeTracker
3645
.Entries()
37-
.FirstOrDefault(entry =>
38-
entry.Entity.GetType() == entity.GetType()
46+
.FirstOrDefault(entry =>
47+
entry.Entity.GetType() == entity.GetType()
3948
&& ((IIdentifiable)entry.Entity).StringId == entity.StringId
4049
);
4150

42-
return trackedEntries != null;
51+
return (IIdentifiable)trackedEntries?.Entity;
4352
}
4453

54+
4555
/// <summary>
4656
/// Gets the current transaction or creates a new one.
4757
/// If a transaction already exists, commit, rollback and dispose

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs

+73
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,79 @@ public async Task Can_Update_ToMany_Relationship_By_Patching_Resource()
293293
Assert.Equal(2, updatedTodoItems.Count);
294294
}
295295

296+
[Fact]
297+
public async Task Can_Update_ToMany_Relationship_By_Patching_Resource_When_Targets_Already_Attached()
298+
{
299+
// arrange
300+
var todoCollection = new TodoItemCollection();
301+
todoCollection.TodoItems = new List<TodoItem>();
302+
var person = _personFaker.Generate();
303+
var todoItem = _todoItemFaker.Generate();
304+
todoCollection.Owner = person;
305+
todoCollection.Name = "PRE-ATTACH-TEST";
306+
todoCollection.TodoItems.Add(todoItem);
307+
_context.TodoItemCollections.Add(todoCollection);
308+
_context.SaveChanges();
309+
310+
var newTodoItem1 = _todoItemFaker.Generate();
311+
var newTodoItem2 = _todoItemFaker.Generate();
312+
_context.AddRange(new TodoItem[] { newTodoItem1, newTodoItem2 });
313+
_context.SaveChanges();
314+
315+
var builder = new WebHostBuilder()
316+
.UseStartup<Startup>();
317+
318+
var server = new TestServer(builder);
319+
var client = server.CreateClient();
320+
321+
var content = new
322+
{
323+
data = new
324+
{
325+
type = "todo-collections",
326+
id = todoCollection.Id,
327+
attributes = new
328+
{
329+
name = todoCollection.Name
330+
},
331+
relationships = new Dictionary<string, object>
332+
{
333+
{ "todo-items", new
334+
{
335+
data = new object[]
336+
{
337+
new { type = "todo-items", id = $"{newTodoItem1.Id}" },
338+
new { type = "todo-items", id = $"{newTodoItem2.Id}" }
339+
}
340+
341+
}
342+
},
343+
}
344+
}
345+
};
346+
347+
var httpMethod = new HttpMethod("PATCH");
348+
var route = $"/api/v1/todo-collections/{todoCollection.Id}";
349+
var request = new HttpRequestMessage(httpMethod, route);
350+
351+
string serializedContent = JsonConvert.SerializeObject(content);
352+
request.Content = new StringContent(serializedContent);
353+
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
354+
355+
// Act
356+
var response = await client.SendAsync(request);
357+
_context = _fixture.GetService<AppDbContext>();
358+
var updatedTodoItems = _context.TodoItemCollections.AsNoTracking()
359+
.Where(tic => tic.Id == todoCollection.Id)
360+
.Include(tdc => tdc.TodoItems).SingleOrDefault().TodoItems;
361+
362+
363+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
364+
/// we are expecting two, not three, because the request does
365+
/// a "complete replace".
366+
Assert.Equal(2, updatedTodoItems.Count);
367+
}
368+
296369
[Fact]
297370
public async Task Can_Update_ToMany_Relationship_By_Patching_Resource_With_Overlap()
298371
{

0 commit comments

Comments
 (0)