Skip to content

Commit 6d2ccf5

Browse files
authored
Merge pull request #355 from json-api-dotnet/fix/#354
Fix/#354: Null reference exception when fetching relationships with compound name
2 parents f3272b0 + 46f1e2d commit 6d2ccf5

File tree

5 files changed

+240
-47
lines changed

5 files changed

+240
-47
lines changed

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

+17-3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public DefaultEntityRepository(
4848
_genericProcessorFactory = _jsonApiContext.GenericProcessorFactory;
4949
}
5050

51+
/// </ inheritdoc>
5152
public virtual IQueryable<TEntity> Get()
5253
{
5354
if (_jsonApiContext.QuerySet?.Fields != null && _jsonApiContext.QuerySet.Fields.Count > 0)
@@ -56,21 +57,25 @@ public virtual IQueryable<TEntity> Get()
5657
return _dbSet;
5758
}
5859

60+
/// </ inheritdoc>
5961
public virtual IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQuery filterQuery)
6062
{
6163
return entities.Filter(_jsonApiContext, filterQuery);
6264
}
6365

66+
/// </ inheritdoc>
6467
public virtual IQueryable<TEntity> Sort(IQueryable<TEntity> entities, List<SortQuery> sortQueries)
6568
{
6669
return entities.Sort(sortQueries);
6770
}
6871

72+
/// </ inheritdoc>
6973
public virtual async Task<TEntity> GetAsync(TId id)
7074
{
7175
return await Get().SingleOrDefaultAsync(e => e.Id.Equals(id));
7276
}
7377

78+
/// </ inheritdoc>
7479
public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshipName)
7580
{
7681
_logger.LogDebug($"[JADN] GetAndIncludeAsync({id}, {relationshipName})");
@@ -80,6 +85,7 @@ public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshi
8085
return result;
8186
}
8287

88+
/// </ inheritdoc>
8389
public virtual async Task<TEntity> CreateAsync(TEntity entity)
8490
{
8591
AttachRelationships();
@@ -102,9 +108,9 @@ protected virtual void AttachRelationships()
102108
private void AttachHasManyPointers()
103109
{
104110
var relationships = _jsonApiContext.HasManyRelationshipPointers.Get();
105-
foreach(var relationship in relationships)
111+
foreach (var relationship in relationships)
106112
{
107-
foreach(var pointer in relationship.Value)
113+
foreach (var pointer in relationship.Value)
108114
{
109115
_context.Entry(pointer).State = EntityState.Unchanged;
110116
}
@@ -123,6 +129,7 @@ private void AttachHasOnePointers()
123129
_context.Entry(relationship.Value).State = EntityState.Unchanged;
124130
}
125131

132+
/// </ inheritdoc>
126133
public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
127134
{
128135
var oldEntity = await GetAsync(id);
@@ -141,12 +148,14 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
141148
return oldEntity;
142149
}
143150

151+
/// </ inheritdoc>
144152
public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
145153
{
146154
var genericProcessor = _genericProcessorFactory.GetProcessor<IGenericProcessor>(typeof(GenericProcessor<>), relationship.Type);
147155
await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds);
148156
}
149157

158+
/// </ inheritdoc>
150159
public virtual async Task<bool> DeleteAsync(TId id)
151160
{
152161
var entity = await GetAsync(id);
@@ -161,11 +170,12 @@ public virtual async Task<bool> DeleteAsync(TId id)
161170
return true;
162171
}
163172

173+
/// </ inheritdoc>
164174
public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relationshipName)
165175
{
166176
var entity = _jsonApiContext.RequestEntity;
167177
var relationship = entity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == relationshipName);
168-
if (relationship == null)
178+
if (relationship == null)
169179
{
170180
throw new JsonApiException(400, $"Invalid relationship {relationshipName} on {entity.EntityName}",
171181
$"{entity.EntityName} does not have a relationship named {relationshipName}");
@@ -178,6 +188,7 @@ public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string
178188
return entities.Include(relationship.InternalRelationshipName);
179189
}
180190

191+
/// </ inheritdoc>
181192
public virtual async Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> entities, int pageSize, int pageNumber)
182193
{
183194
if (pageNumber >= 0)
@@ -198,20 +209,23 @@ public virtual async Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> en
198209
.ToListAsync();
199210
}
200211

212+
/// </ inheritdoc>
201213
public async Task<int> CountAsync(IQueryable<TEntity> entities)
202214
{
203215
return (entities is IAsyncEnumerable<TEntity>)
204216
? await entities.CountAsync()
205217
: entities.Count();
206218
}
207219

220+
/// </ inheritdoc>
208221
public async Task<TEntity> FirstOrDefaultAsync(IQueryable<TEntity> entities)
209222
{
210223
return (entities is IAsyncEnumerable<TEntity>)
211224
? await entities.FirstOrDefaultAsync()
212225
: entities.FirstOrDefault();
213226
}
214227

228+
/// </ inheritdoc>
215229
public async Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entities)
216230
{
217231
return (entities is IAsyncEnumerable<TEntity>)

src/JsonApiDotNetCore/Data/IEntityReadRepository.cs

+46-3
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,75 @@
66

77
namespace JsonApiDotNetCore.Data
88
{
9-
public interface IEntityReadRepository<TEntity>
10-
: IEntityReadRepository<TEntity, int>
11-
where TEntity : class, IIdentifiable<int>
9+
public interface IEntityReadRepository<TEntity>
10+
: IEntityReadRepository<TEntity, int>
11+
where TEntity : class, IIdentifiable<int>
1212
{ }
1313

1414
public interface IEntityReadRepository<TEntity, in TId>
1515
where TEntity : class, IIdentifiable<TId>
1616
{
17+
/// <summary>
18+
/// The base GET query. This is a good place to apply rules that should affect all reads,
19+
/// such as authorization of resources.
20+
/// </summary>
1721
IQueryable<TEntity> Get();
1822

23+
/// <summary>
24+
/// Include a relationship in the query
25+
/// </summary>
26+
/// <example>
27+
/// <code>
28+
/// _todoItemsRepository.GetAndIncludeAsync(1, "achieved-date");
29+
/// </code>
30+
/// </example>
1931
IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relationshipName);
2032

33+
/// <summary>
34+
/// Apply a filter to the provided queryable
35+
/// </summary>
2136
IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQuery filterQuery);
2237

38+
/// <summary>
39+
/// Apply a sort to the provided queryable
40+
/// </summary>
2341
IQueryable<TEntity> Sort(IQueryable<TEntity> entities, List<SortQuery> sortQueries);
2442

43+
/// <summary>
44+
/// Paginate the provided queryable
45+
/// </summary>
2546
Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> entities, int pageSize, int pageNumber);
2647

48+
/// <summary>
49+
/// Get the entity by id
50+
/// </summary>
2751
Task<TEntity> GetAsync(TId id);
2852

53+
/// <summary>
54+
/// Get the entity with the specified id and include the relationship.
55+
/// </summary>
56+
/// <param name="id">The entity id</param>
57+
/// <param name="relationshipName">The exposed relationship name</param>
58+
/// <example>
59+
/// <code>
60+
/// _todoItemsRepository.GetAndIncludeAsync(1, "achieved-date");
61+
/// </code>
62+
/// </example>
2963
Task<TEntity> GetAndIncludeAsync(TId id, string relationshipName);
3064

65+
/// <summary>
66+
/// Count the total number of records
67+
/// </summary>
3168
Task<int> CountAsync(IQueryable<TEntity> entities);
3269

70+
/// <summary>
71+
/// Get the first element in the collection, return the default value if collection is empty
72+
/// </summary>
3373
Task<TEntity> FirstOrDefaultAsync(IQueryable<TEntity> entities);
3474

75+
/// <summary>
76+
/// Convert the collection to a materialized list
77+
/// </summary>
3578
Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entities);
3679
}
3780
}

src/JsonApiDotNetCore/Internal/ContextGraph.cs

+41-1
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,45 @@ namespace JsonApiDotNetCore.Internal
66
{
77
public interface IContextGraph
88
{
9-
object GetRelationship<TParent>(TParent entity, string relationshipName);
9+
/// <summary>
10+
/// Gets the value of the navigation property, defined by the relationshipName,
11+
/// on the provided instance.
12+
/// </summary>
13+
/// <param name="resource">The resource instance</param>
14+
/// <param name="propertyName">The navigation property name.</param>
15+
/// <example>
16+
/// <code>
17+
/// _graph.GetRelationship(todoItem, nameof(TodoItem.Owner));
18+
/// </code>
19+
/// </example>
20+
object GetRelationship<TParent>(TParent resource, string propertyName);
21+
22+
/// <summary>
23+
/// Get the internal navigation property name for the specified public
24+
/// relationship name.
25+
/// </summary>
26+
/// <param name="relationshipName">The public relationship name specified by a <see cref="HasOneAttribute" /> or <see cref="HasManyAttribute" /></param>
27+
/// <example>
28+
/// <code>
29+
/// _graph.GetRelationshipName&lt;TodoItem&gt;("achieved-date");
30+
/// // returns "AchievedDate"
31+
/// </code>
32+
/// </example>
1033
string GetRelationshipName<TParent>(string relationshipName);
34+
35+
/// <summary>
36+
/// Get the resource metadata by the DbSet property name
37+
/// </summary>
1138
ContextEntity GetContextEntity(string dbSetName);
39+
40+
/// <summary>
41+
/// Get the resource metadata by the resource type
42+
/// </summary>
1243
ContextEntity GetContextEntity(Type entityType);
44+
45+
/// <summary>
46+
/// Was built against an EntityFrameworkCore DbContext ?
47+
/// </summary>
1348
bool UsesDbContext { get; }
1449
}
1550

@@ -40,14 +75,18 @@ internal ContextGraph(List<ContextEntity> entities, bool usesDbContext, List<Val
4075
Instance = this;
4176
}
4277

78+
/// </ inheritdoc>
4379
public bool UsesDbContext { get; }
4480

81+
/// </ inheritdoc>
4582
public ContextEntity GetContextEntity(string entityName)
4683
=> Entities.SingleOrDefault(e => string.Equals(e.EntityName, entityName, StringComparison.OrdinalIgnoreCase));
4784

85+
/// </ inheritdoc>
4886
public ContextEntity GetContextEntity(Type entityType)
4987
=> Entities.SingleOrDefault(e => e.EntityType == entityType);
5088

89+
/// </ inheritdoc>
5190
public object GetRelationship<TParent>(TParent entity, string relationshipName)
5291
{
5392
var parentEntityType = entity.GetType();
@@ -62,6 +101,7 @@ public object GetRelationship<TParent>(TParent entity, string relationshipName)
62101
return navigationProperty.GetValue(entity);
63102
}
64103

104+
/// </ inheritdoc>
65105
public string GetRelationshipName<TParent>(string relationshipName)
66106
{
67107
var entityType = typeof(TParent);

0 commit comments

Comments
 (0)