Skip to content

Commit db5bf23

Browse files
committed
fix: merge
2 parents de82fee + 35a2f54 commit db5bf23

File tree

9 files changed

+66
-45
lines changed

9 files changed

+66
-45
lines changed

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,15 @@ public class DefaultEntityRepository<TEntity>
2222
public DefaultEntityRepository(
2323
IJsonApiContext jsonApiContext,
2424
IDbContextResolver contextResolver,
25-
ResourceDefinition<TEntity> resourceDefinition = null
26-
)
25+
ResourceDefinition<TEntity> resourceDefinition = null)
2726
: base(jsonApiContext, contextResolver, resourceDefinition)
2827
{ }
2928

3029
public DefaultEntityRepository(
3130
ILoggerFactory loggerFactory,
3231
IJsonApiContext jsonApiContext,
33-
IDbContextResolver contextResolver,
34-
ResourceDefinition<TEntity> resourceDefinition = null
35-
)
32+
IDbContextResolver contextResolver,
33+
ResourceDefinition<TEntity> resourceDefinition = null)
3634
: base(loggerFactory, jsonApiContext, contextResolver, resourceDefinition)
3735
{ }
3836
}
@@ -55,8 +53,7 @@ public class DefaultEntityRepository<TEntity, TId>
5553
public DefaultEntityRepository(
5654
IJsonApiContext jsonApiContext,
5755
IDbContextResolver contextResolver,
58-
ResourceDefinition<TEntity> resourceDefinition = null
59-
)
56+
ResourceDefinition<TEntity> resourceDefinition = null)
6057
{
6158
_context = contextResolver.GetContext();
6259
_dbSet = contextResolver.GetDbSet<TEntity>();
@@ -69,8 +66,7 @@ public DefaultEntityRepository(
6966
ILoggerFactory loggerFactory,
7067
IJsonApiContext jsonApiContext,
7168
IDbContextResolver contextResolver,
72-
ResourceDefinition<TEntity> resourceDefinition = null
73-
)
69+
ResourceDefinition<TEntity> resourceDefinition = null)
7470
{
7571
_context = contextResolver.GetContext();
7672
_dbSet = contextResolver.GetDbSet<TEntity>();
@@ -151,14 +147,15 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity)
151147
foreach (var relationshipAttr in _jsonApiContext.RelationshipsToUpdate?.Keys)
152148
{
153149
var trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, entity, out bool wasAlreadyTracked);
154-
// LoadInverseRelationships(trackedRelationshipValue, relationshipAttribute)
150+
LoadInverseRelationships(trackedRelationshipValue, relationshipAttr);
155151
if (wasAlreadyTracked)
156152
{
157153
/// We only need to reassign the relationship value to the to-be-added
158154
/// entity when we're using a different instance (because this different one
159155
/// was already tracked) than the one assigned to the to-be-created entity.
160156
AssignRelationshipValue(entity, trackedRelationshipValue, relationshipAttr);
161-
} else if (relationshipAttr is HasManyThroughAttribute throughAttr)
157+
}
158+
else if (relationshipAttr is HasManyThroughAttribute throughAttr)
162159
{
163160
/// even if we don't have to reassign anything because of already tracked
164161
/// entities, we still need to assign the "through" entities in the case of many-to-many.
@@ -171,21 +168,41 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity)
171168
return entity;
172169
}
173170

171+
/// <summary>
172+
/// Loads the inverse relationships to prevent foreign key constraints from being violated
173+
/// to support implicit removes, see https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/502.
174+
/// </summary>
175+
private void LoadInverseRelationships(object trackedRelationshipValue, RelationshipAttribute relationshipAttr)
176+
{
177+
if (relationshipAttr.InverseNavigation == null) return;
178+
if (relationshipAttr is HasOneAttribute hasOneAttr)
179+
{
180+
_context.Entry((IIdentifiable)trackedRelationshipValue).Reference(hasOneAttr.InverseNavigation).Load();
181+
}
182+
else if (relationshipAttr is HasManyAttribute hasManyAttr)
183+
{
184+
foreach (IIdentifiable relationshipValue in (IList)trackedRelationshipValue)
185+
{
186+
_context.Entry((IIdentifiable)trackedRelationshipValue).Reference(hasManyAttr.InverseNavigation).Load();
187+
}
188+
}
189+
}
190+
174191

175192
/// <inheritdoc />
176193
public void DetachRelationshipPointers(TEntity entity)
177194
{
178195

179196
foreach (var relationshipAttr in _jsonApiContext.RelationshipsToUpdate.Keys)
180197
{
181-
if (relationshipAttr is HasOneAttribute hasOneAttr)
198+
if (relationshipAttr is HasOneAttribute hasOneAttr)
182199
{
183200
var relationshipValue = GetEntityResourceSeparationValue(entity, hasOneAttr) ?? (IIdentifiable)hasOneAttr.GetValue(entity);
184201
if (relationshipValue == null) continue;
185202
_context.Entry(relationshipValue).State = EntityState.Detached;
186203

187204
}
188-
else
205+
else
189206
{
190207
IEnumerable<IIdentifiable> relationshipValueList = (IEnumerable<IIdentifiable>)relationshipAttr.GetValue(entity);
191208
/// This adds support for resource-entity separation in the case of one-to-many.
@@ -224,7 +241,7 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity updatedEntity)
224241
{
225242
LoadCurrentRelationships(oldEntity, relationshipAttr);
226243
var trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, updatedEntity, out bool wasAlreadyTracked);
227-
// LoadInverseRelationships(trackedRelationshipValue, relationshipAttribute)
244+
LoadInverseRelationships(trackedRelationshipValue, relationshipAttr);
228245
AssignRelationshipValue(oldEntity, trackedRelationshipValue, relationshipAttr);
229246
}
230247
await _context.SaveChangesAsync();
@@ -241,26 +258,27 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity updatedEntity)
241258
private object GetTrackedRelationshipValue(RelationshipAttribute relationshipAttr, TEntity entity, out bool wasAlreadyAttached)
242259
{
243260
wasAlreadyAttached = false;
244-
if (relationshipAttr is HasOneAttribute hasOneAttribute)
261+
if (relationshipAttr is HasOneAttribute hasOneAttr)
245262
{
246263
/// This adds support for resource-entity separation in the case of one-to-one.
247-
var relationshipValue = GetEntityResourceSeparationValue(entity, hasOneAttribute) ?? (IIdentifiable)hasOneAttribute.GetValue(entity);
248-
if (relationshipValue == null)
249-
return null;
250-
return GetTrackedHasOneRelationshipValue(relationshipValue, hasOneAttribute, ref wasAlreadyAttached);
264+
var relationshipValue = GetEntityResourceSeparationValue(entity, hasOneAttr) ?? (IIdentifiable)hasOneAttr.GetValue(entity);
265+
if (relationshipValue == null)
266+
return null;
267+
return GetTrackedHasOneRelationshipValue(relationshipValue, hasOneAttr, ref wasAlreadyAttached);
251268
}
252269
else
253270
{
254271
IEnumerable<IIdentifiable> relationshipValueList = (IEnumerable<IIdentifiable>)relationshipAttr.GetValue(entity);
255272
/// This adds support for resource-entity separation in the case of one-to-many.
256273
/// todo: currently there is no support for many to many relations.
257-
if (relationshipAttr is HasManyAttribute hasMany)
258-
relationshipValueList = GetEntityResourceSeparationValue(entity, hasMany) ?? relationshipValueList;
274+
if (relationshipAttr is HasManyAttribute hasMany)
275+
relationshipValueList = GetEntityResourceSeparationValue(entity, hasMany) ?? relationshipValueList;
259276
if (relationshipValueList == null) return null;
260277
return GetTrackedManyRelationshipValue(relationshipValueList, relationshipAttr, ref wasAlreadyAttached);
261278
}
262279
}
263280

281+
// helper method used in GetTrackedRelationshipValue. See comments there.
264282
private IList GetTrackedManyRelationshipValue(IEnumerable<IIdentifiable> relationshipValueList, RelationshipAttribute relationshipAttr, ref bool wasAlreadyAttached)
265283
{
266284
if (relationshipValueList == null) return null;
@@ -272,7 +290,7 @@ private IList GetTrackedManyRelationshipValue(IEnumerable<IIdentifiable> relatio
272290
/// this will point to the Resource type in the case of entity resource
273291
/// separation. We should consider to store entity type on
274292
/// the relationship attribute too.
275-
entityType = pointer.GetType();
293+
entityType = entityType ?? pointer.GetType();
276294
var tracked = AttachOrGetTracked(pointer);
277295
if (tracked != null) _wasAlreadyAttached = true;
278296
return Convert.ChangeType(tracked ?? pointer, entityType);
@@ -281,9 +299,9 @@ private IList GetTrackedManyRelationshipValue(IEnumerable<IIdentifiable> relatio
281299
return (IList)trackedPointerCollection;
282300
}
283301

284-
private IIdentifiable GetTrackedHasOneRelationshipValue(IIdentifiable relationshipValue, HasOneAttribute hasOneAttribute, ref bool wasAlreadyAttached)
302+
// helper method used in GetTrackedRelationshipValue. See comments there.
303+
private IIdentifiable GetTrackedHasOneRelationshipValue(IIdentifiable relationshipValue, HasOneAttribute hasOneAttr, ref bool wasAlreadyAttached)
285304
{
286-
287305
var tracked = AttachOrGetTracked(relationshipValue);
288306
if (tracked != null) wasAlreadyAttached = true;
289307
return tracked ?? relationshipValue;
@@ -298,7 +316,7 @@ public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute
298316
// of the property...
299317
var typeToUpdate = (relationship is HasManyThroughAttribute hasManyThrough)
300318
? hasManyThrough.ThroughType
301-
: relationship.DependentType;
319+
: relationship.Type;
302320

303321
var genericProcessor = _genericProcessorFactory.GetProcessor<IGenericProcessor>(typeof(GenericProcessor<>), typeToUpdate);
304322
await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds);
@@ -346,7 +364,7 @@ public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string
346364
: $"{internalRelationshipPath}.{relationship.RelationshipPath}";
347365

348366
if (i < relationshipChain.Length)
349-
entity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.DependentType);
367+
entity = _jsonApiContext.ResourceGraph.GetContextEntity(relationship.Type);
350368
}
351369

352370
return entities.Include(internalRelationshipPath);
@@ -357,9 +375,7 @@ public virtual async Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> en
357375
{
358376
if (pageNumber >= 0)
359377
{
360-
// the IQueryable returned from the hook executor is sometimes consumed here.
361-
// In this case, it does not support .ToListAsync(), so we use the method below.
362-
return await this.ToListAsync(entities.PageForward(pageSize, pageNumber));
378+
return await entities.PageForward(pageSize, pageNumber).ToListAsync();
363379
}
364380

365381
// since EntityFramework does not support IQueryable.Reverse(), we need to know the number of queried entities
@@ -369,9 +385,10 @@ public virtual async Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> en
369385
int virtualFirstIndex = numberOfEntities - pageSize * Math.Abs(pageNumber);
370386
int numberOfElementsInPage = Math.Min(pageSize, virtualFirstIndex + pageSize);
371387

372-
return await ToListAsync(entities
388+
return await entities
373389
.Skip(virtualFirstIndex)
374-
.Take(numberOfElementsInPage));
390+
.Take(numberOfElementsInPage)
391+
.ToListAsync();
375392
}
376393

377394
/// <inheritdoc />

src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ public static void AddJsonApiInternals(
159159
services.AddScoped<IControllerContext, Services.ControllerContext>();
160160
services.AddScoped<IDocumentBuilderOptionsProvider, DocumentBuilderOptionsProvider>();
161161

162+
162163
if (jsonApiOptions.EnableResourceHooks)
163164
{
164165
services.AddSingleton(typeof(IHooksDiscovery<>), typeof(HooksDiscovery<>));
@@ -168,6 +169,7 @@ public static void AddJsonApiInternals(
168169
}
169170

170171
services.AddScoped<IInverseRelationships, InverseRelationships>();
172+
171173
}
172174

173175
private static void AddOperationServices(IServiceCollection services)

src/JsonApiDotNetCore/Models/HasManyAttribute.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ public class HasManyAttribute : RelationshipAttribute
2525
///
2626
/// </example>
2727
public HasManyAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string mappedBy = null, string inverseNavigationProperty = null)
28-
: base(publicName, documentLinks, canInclude, mappedBy, inverseNavigationProperty)
29-
{ }
28+
: base(publicName, documentLinks, canInclude, mappedBy)
29+
{
30+
InverseNavigation = inverseNavigationProperty;
31+
}
3032

3133
/// <summary>
3234
/// Sets the value of the property identified by this attribute

src/JsonApiDotNetCore/Models/HasOneAttribute.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ public class HasOneAttribute : RelationshipAttribute
2929
///
3030
/// </example>
3131
public HasOneAttribute(string publicName = null, Link documentLinks = Link.All, bool canInclude = true, string withForeignKey = null, string mappedBy = null, string inverseNavigationProperty = null)
32-
: base(publicName, documentLinks, canInclude, mappedBy, inverseNavigationProperty)
32+
33+
: base(publicName, documentLinks, canInclude, mappedBy)
3334
{
3435
_explicitIdentifiablePropertyName = withForeignKey;
36+
InverseNavigation = inverseNavigationProperty;
3537
}
3638

3739
private readonly string _explicitIdentifiablePropertyName;

src/JsonApiDotNetCore/Models/RelationshipAttribute.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,17 @@ namespace JsonApiDotNetCore.Models
66
{
77
public abstract class RelationshipAttribute : Attribute
88
{
9-
protected RelationshipAttribute(string publicName, Link documentLinks, bool canInclude, string mappedBy, string inverseNavigationProperty = null)
9+
protected RelationshipAttribute(string publicName, Link documentLinks, bool canInclude, string mappedBy)
1010
{
1111
PublicRelationshipName = publicName;
1212
DocumentLinks = documentLinks;
1313
CanInclude = canInclude;
1414
EntityPropertyName = mappedBy;
15-
InverseNavigation = inverseNavigationProperty;
1615
}
1716

1817
public string PublicRelationshipName { get; internal set; }
1918
public string InternalRelationshipName { get; internal set; }
20-
19+
public string InverseNavigation { get; internal set; }
2120

2221
/// <summary>
2322
/// The related entity type. This does not necessarily match the navigation property type.
@@ -57,8 +56,6 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI
5756
public bool CanInclude { get; }
5857
public string EntityPropertyName { get; }
5958

60-
public string InverseNavigation { get ; internal set;}
61-
6259
public bool TryGetHasOne(out HasOneAttribute result)
6360
{
6461
if (IsHasOne)

test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/RepositoryOverrideTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public async Task Total_Record_Count_Included()
5959
// assert
6060
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
6161
foreach(var item in deserializedBody)
62-
Assert.Equal(person.Id, item.OwnerId);
62+
Assert.Equal(person.Id, item.Owner.Id);
6363
}
6464

6565
[Fact]

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -664,8 +664,8 @@ public async Task Updating_ToOne_Relationship_With_Implicit_Remove()
664664

665665
// Assert
666666
var body = await response.Content.ReadAsStringAsync();
667-
// this test currently fails, will be adressed in next PR
668-
//Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}");
667+
668+
Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}");
669669

670670
}
671671
}

test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public async Task Can_Filter_By_Relationship_Id()
114114
// Assert
115115
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
116116
Assert.NotEmpty(deserializedBody);
117-
Assert.Contains(deserializedBody, (i) => i.OwnerId == person.Id);
117+
Assert.Contains(deserializedBody, (i) => i.Owner.Id == person.Id);
118118
}
119119

120120
[Fact]
@@ -435,7 +435,8 @@ public async Task Can_Get_TodoItem_WithOwner()
435435
// Assert
436436
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
437437
var deserializedBody = (TodoItem)_fixture.GetService<IJsonApiDeSerializer>().Deserialize(body);
438-
Assert.Equal(person.Id, deserializedBody.OwnerId);
438+
439+
Assert.Equal(person.Id, deserializedBody.Owner.Id);
439440
Assert.Equal(todoItem.Id, deserializedBody.Id);
440441
Assert.Equal(todoItem.Description, deserializedBody.Description);
441442
Assert.Equal(todoItem.Ordinal, deserializedBody.Ordinal);

test/ResourceEntitySeparationExampleTests/Acceptance/AddTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public async Task Can_Create_Course_With_Department_Id()
9494
Assert.Equal(course.Number, data.Number);
9595
Assert.Equal(course.Title, data.Title);
9696
Assert.Equal(course.Description, data.Description);
97-
Assert.Equal(department.Id, data.DepartmentId);
97+
Assert.Equal(department.Id, data.Department.Id);
9898
}
9999

100100
[Fact]

0 commit comments

Comments
 (0)