Skip to content

Commit 59bf942

Browse files
committed
don't reload individual relationships, just detach and then fetch
1 parent d7d16bd commit 59bf942

File tree

5 files changed

+44
-66
lines changed

5 files changed

+44
-66
lines changed

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 23 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshi
8585
{
8686
_logger.LogDebug($"[JADN] GetAndIncludeAsync({id}, {relationshipName})");
8787

88-
var includedSet = await IncludeAsync(Get(), relationshipName);
88+
var includedSet = Include(Get(), relationshipName);
8989
var result = await includedSet.SingleOrDefaultAsync(e => e.Id.Equals(id));
9090

9191
return result;
@@ -108,6 +108,28 @@ protected virtual void AttachRelationships()
108108
AttachHasOnePointers();
109109
}
110110

111+
/// <inheritdoc />
112+
public void DetachRelationshipPointers(TEntity entity)
113+
{
114+
foreach (var hasOneRelationship in _jsonApiContext.HasOneRelationshipPointers.Get())
115+
{
116+
_context.Entry(hasOneRelationship.Value).State = EntityState.Detached;
117+
}
118+
119+
foreach (var hasManyRelationship in _jsonApiContext.HasManyRelationshipPointers.Get())
120+
{
121+
foreach (var pointer in hasManyRelationship.Value)
122+
{
123+
_context.Entry(pointer).State = EntityState.Detached;
124+
}
125+
126+
// HACK: detaching has many relationships doesn't appear to be sufficient
127+
// the navigation property actually needs to be nulled out, otherwise
128+
// EF adds duplicate instances to the collection
129+
hasManyRelationship.Key.SetValue(entity, null);
130+
}
131+
}
132+
111133
/// <summary>
112134
/// This is used to allow creation of HasMany relationships when the
113135
/// dependent side of the relationship already exists.
@@ -178,7 +200,6 @@ public virtual async Task<bool> DeleteAsync(TId id)
178200
}
179201

180202
/// <inheritdoc />
181-
[Obsolete("Use IncludeAsync")]
182203
public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relationshipName)
183204
{
184205
var entity = _jsonApiContext.RequestEntity;
@@ -197,52 +218,6 @@ public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string
197218
return entities.Include(relationship.InternalRelationshipName);
198219
}
199220

200-
/// <inheritdoc />
201-
public virtual async Task<IQueryable<TEntity>> IncludeAsync(IQueryable<TEntity> entities, string relationshipName)
202-
{
203-
var entity = _jsonApiContext.RequestEntity;
204-
var relationship = entity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == relationshipName);
205-
if (relationship == null)
206-
{
207-
throw new JsonApiException(400, $"Invalid relationship {relationshipName} on {entity.EntityName}",
208-
$"{entity.EntityName} does not have a relationship named {relationshipName}");
209-
}
210-
211-
if (!relationship.CanInclude)
212-
{
213-
throw new JsonApiException(400, $"Including the relationship {relationshipName} on {entity.EntityName} is not allowed");
214-
}
215-
216-
await ReloadPointerAsync(relationship);
217-
218-
return entities.Include(relationship.InternalRelationshipName);
219-
}
220-
221-
/// <summary>
222-
/// Ensure relationships on the provided entity have been fully loaded from the database.
223-
/// </summary>
224-
/// <remarks>
225-
/// The only known case when this should be called is when a POST request is
226-
/// sent with an ?include query.
227-
///
228-
/// See https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/343
229-
/// </remarks>
230-
private async Task ReloadPointerAsync(RelationshipAttribute relationshipAttr)
231-
{
232-
if (relationshipAttr.IsHasOne && _jsonApiContext.HasOneRelationshipPointers.Get().TryGetValue(relationshipAttr, out var pointer))
233-
{
234-
await _context.Entry(pointer).ReloadAsync();
235-
}
236-
237-
if (relationshipAttr.IsHasMany && _jsonApiContext.HasManyRelationshipPointers.Get().TryGetValue(relationshipAttr, out var pointers))
238-
{
239-
foreach (var hasManyPointer in pointers)
240-
{
241-
await _context.Entry(hasManyPointer).ReloadAsync();
242-
}
243-
}
244-
}
245-
246221
/// <inheritdoc />
247222
public virtual async Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> entities, int pageSize, int pageNumber)
248223
{

src/JsonApiDotNetCore/Data/IEntityReadRepository.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ public interface IEntityReadRepository<TEntity, in TId>
2121
/// </summary>
2222
IQueryable<TEntity> Get();
2323

24-
[Obsolete("Use IncludeAsync")]
25-
IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relationshipName);
26-
2724
/// <summary>
2825
/// Include a relationship in the query
2926
/// </summary>
@@ -32,7 +29,7 @@ public interface IEntityReadRepository<TEntity, in TId>
3229
/// _todoItemsRepository.GetAndIncludeAsync(1, "achieved-date");
3330
/// </code>
3431
/// </example>
35-
Task<IQueryable<TEntity>> IncludeAsync(IQueryable<TEntity> entities, string relationshipName);
32+
IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relationshipName);
3633

3734
/// <summary>
3835
/// Apply a filter to the provided queryable

src/JsonApiDotNetCore/Data/IEntityRepository.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public interface IEntityRepository<TEntity>
88
{ }
99

1010
public interface IEntityRepository<TEntity, in TId>
11-
: IEntityReadRepository<TEntity, TId>,
11+
: IEntityReadRepository<TEntity, TId>,
1212
IEntityWriteRepository<TEntity, TId>
1313
where TEntity : class, IIdentifiable<TId>
1414
{ }

src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,19 @@ public interface IEntityWriteRepository<TEntity, in TId>
1919
Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds);
2020

2121
Task<bool> DeleteAsync(TId id);
22+
23+
/// <summary>
24+
/// Ensures that any relationship pointers created during a POST or PATCH
25+
/// request are detached from the DbContext.
26+
/// This allows the relationships to be fully loaded from the database.
27+
///
28+
/// </summary>
29+
/// <remarks>
30+
/// The only known case when this should be called is when a POST request is
31+
/// sent with an ?include query.
32+
///
33+
/// See https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/343
34+
/// </remarks>
35+
void DetachRelationshipPointers(TEntity entity);
2236
}
2337
}

src/JsonApiDotNetCore/Services/EntityResourceService.cs

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ public virtual async Task<TResource> CreateAsync(TResource resource)
8181
// been requested
8282
// https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/343
8383
if (ShouldIncludeRelationships())
84+
{
85+
_entities.DetachRelationshipPointers(entity);
8486
return await GetWithRelationshipsAsync(entity.Id);
87+
}
8588

8689
return MapOut(entity);
8790
}
@@ -98,7 +101,7 @@ public virtual async Task<IEnumerable<TResource>> GetAsync()
98101
entities = ApplySortAndFilterQuery(entities);
99102

100103
if (ShouldIncludeRelationships())
101-
entities = await IncludeRelationshipsAsync(entities, _jsonApiContext.QuerySet.IncludedRelationships);
104+
entities = IncludeRelationships(entities, _jsonApiContext.QuerySet.IncludedRelationships);
102105

103106
if (_jsonApiContext.Options.IncludeTotalRecordCount)
104107
_jsonApiContext.PageManager.TotalRecords = await _entities.CountAsync(entities);
@@ -224,7 +227,6 @@ protected virtual IQueryable<TEntity> ApplySortAndFilterQuery(IQueryable<TEntity
224227
return entities;
225228
}
226229

227-
[Obsolete("Use IncludeRelationshipsAsync")]
228230
protected IQueryable<TEntity> IncludeRelationships(IQueryable<TEntity> entities, List<string> relationships)
229231
{
230232
_jsonApiContext.IncludedRelationships = relationships;
@@ -235,23 +237,13 @@ protected IQueryable<TEntity> IncludeRelationships(IQueryable<TEntity> entities,
235237
return entities;
236238
}
237239

238-
protected virtual async Task<IQueryable<TEntity>> IncludeRelationshipsAsync(IQueryable<TEntity> entities, List<string> relationships)
239-
{
240-
_jsonApiContext.IncludedRelationships = relationships;
241-
242-
foreach (var r in relationships)
243-
entities = await _entities.IncludeAsync(entities, r);
244-
245-
return entities;
246-
}
247-
248240
private async Task<TResource> GetWithRelationshipsAsync(TId id)
249241
{
250242
var query = _entities.Get().Where(e => e.Id.Equals(id));
251243

252244
foreach (var r in _jsonApiContext.QuerySet.IncludedRelationships)
253245
{
254-
query = await _entities.IncludeAsync(query, r);
246+
query = _entities.Include(query, r);
255247
}
256248

257249
var value = await _entities.FirstOrDefaultAsync(query);

0 commit comments

Comments
 (0)