Skip to content

Commit 7e16929

Browse files
committed
fix: support include on nested resources route
1 parent a801d95 commit 7e16929

File tree

6 files changed

+100
-8
lines changed

6 files changed

+100
-8
lines changed

src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ public TodoItem()
3838
public DateTimeOffset? OffsetDate { get; set; }
3939

4040
public int? OwnerId { get; set; }
41+
4142
public int? AssigneeId { get; set; }
43+
4244
public Guid? CollectionId { get; set; }
4345

4446
[HasOne]
@@ -49,6 +51,7 @@ public TodoItem()
4951

5052
[HasOne]
5153
public virtual Person OneToOnePerson { get; set; }
54+
5255
public virtual int? OneToOnePersonId { get; set; }
5356

5457
[HasMany]
@@ -59,13 +62,16 @@ public TodoItem()
5962

6063
// cyclical to-one structure
6164
public virtual int? DependentOnTodoId { get; set; }
65+
6266
[HasOne]
6367
public virtual TodoItem DependentOnTodo { get; set; }
6468

6569
// cyclical to-many structure
6670
public virtual int? ParentTodoId {get; set;}
71+
6772
[HasOne]
6873
public virtual TodoItem ParentTodo { get; set; }
74+
6975
[HasMany]
7076
public virtual List<TodoItem> ChildrenTodos { get; set; }
7177
}

src/JsonApiDotNetCore/Controllers/JsonApiController.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ namespace JsonApiDotNetCore.Controllers
99
{
1010
public class JsonApiController<T, TId> : BaseJsonApiController<T, TId> where T : class, IIdentifiable<TId>
1111
{
12-
1312
/// <param name="jsonApiOptions"></param>
1413
/// <param name="resourceService"></param>
1514
/// <param name="loggerFactory"></param>

src/JsonApiDotNetCore/QueryParameterServices/Common/QueryParameterService.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@ public abstract class QueryParameterService
2020
protected QueryParameterService(IResourceGraph resourceGraph, ICurrentRequest currentRequest)
2121
{
2222
_resourceGraph = resourceGraph;
23-
_requestResource = currentRequest.GetRequestResource();
23+
if (currentRequest.RequestRelationship != null)
24+
{
25+
_requestResource= resourceGraph.GetResourceContext(currentRequest.RequestRelationship.RightType);
26+
}
27+
else
28+
{
29+
_requestResource = currentRequest.GetRequestResource();
30+
}
2431
}
2532

2633
protected QueryParameterService() { }

src/JsonApiDotNetCore/Services/DefaultResourceService.cs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,10 @@ public virtual async Task<TResource> GetRelationshipsAsync(TId id, string relati
134134

135135
// TODO: it would be better if we could distinguish whether or not the relationship was not found,
136136
// vs the relationship not being set on the instance of T
137-
var entityQuery = _repository.Include(_repository.Get(id), new RelationshipAttribute[] { relationship });
138-
var entity = await _repository.FirstOrDefaultAsync(entityQuery);
137+
138+
var query = _repository.Get(id);
139+
query = ApplyInclude(query, chainPrefix: new List<RelationshipAttribute> { relationship });
140+
var entity = await _repository.FirstOrDefaultAsync(query);
139141
if (entity == null) // this does not make sense. If the parent entity is not found, this error is thrown?
140142
throw new JsonApiException(404, $"Relationship '{relationshipName}' not found.");
141143

@@ -246,12 +248,31 @@ protected virtual IQueryable<TResource> ApplyFilter(IQueryable<TResource> entiti
246248
/// </summary>
247249
/// <param name="entities"></param>
248250
/// <returns></returns>
249-
protected virtual IQueryable<TResource> ApplyInclude(IQueryable<TResource> entities)
251+
protected virtual IQueryable<TResource> ApplyInclude(IQueryable<TResource> entities, IEnumerable<RelationshipAttribute> chainPrefix = null)
250252
{
251253
var chains = _includeService.Get();
252-
if (chains != null && chains.Any())
253-
foreach (var r in chains)
254-
entities = _repository.Include(entities, r.ToArray());
254+
bool hasInclusionChain = chains.Any();
255+
256+
if (chains == null)
257+
{
258+
throw new Exception();
259+
}
260+
261+
if (chainPrefix != null && !hasInclusionChain)
262+
{
263+
hasInclusionChain = true;
264+
chains.Add(new List<RelationshipAttribute>());
265+
}
266+
267+
268+
if (hasInclusionChain)
269+
{
270+
foreach (var inclusionChain in chains)
271+
{
272+
inclusionChain.InsertRange(0, chainPrefix);
273+
entities = _repository.Include(entities, inclusionChain.ToArray());
274+
}
275+
}
255276

256277
return entities;
257278
}

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec
1616
{
17+
1718
public class CreatingDataTests : FunctionalTestCollection<StandardApplicationFactory>
1819
{
1920
private readonly Faker<TodoItem> _todoItemFaker;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Net;
4+
using System.Threading.Tasks;
5+
using Bogus;
6+
using JsonApiDotNetCoreExample.Models;
7+
using JsonApiDotNetCoreExampleTests.Helpers.Models;
8+
using Microsoft.EntityFrameworkCore;
9+
using Xunit;
10+
using Person = JsonApiDotNetCoreExample.Models.Person;
11+
12+
namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec
13+
{
14+
public class NestedResourceTests : FunctionalTestCollection<StandardApplicationFactory>
15+
{
16+
private readonly Faker<TodoItem> _todoItemFaker;
17+
private readonly Faker<Person> _personFaker;
18+
private readonly Faker<Passport> _passportFaker;
19+
20+
public NestedResourceTests(StandardApplicationFactory factory) : base(factory)
21+
{
22+
_todoItemFaker = new Faker<TodoItem>()
23+
.RuleFor(t => t.Description, f => f.Lorem.Sentence())
24+
.RuleFor(t => t.Ordinal, f => f.Random.Number())
25+
.RuleFor(t => t.CreatedDate, f => f.Date.Past());
26+
_personFaker = new Faker<Person>()
27+
.RuleFor(t => t.FirstName, f => f.Name.FirstName())
28+
.RuleFor(t => t.LastName, f => f.Name.LastName());
29+
_passportFaker = new Faker<Passport>()
30+
.RuleFor(t => t.SocialSecurityNumber, f => f.Random.Number());
31+
}
32+
33+
[Fact]
34+
public async Task Include_NestedResource_CanInclude()
35+
{
36+
// Arrange
37+
var assignee = _dbContext.Add(_personFaker.Generate()).Entity;
38+
var todo = _dbContext.Add(_todoItemFaker.Generate()).Entity;
39+
var owner = _dbContext.Add(_personFaker.Generate()).Entity;
40+
var passport = _dbContext.Add(_passportFaker.Generate()).Entity;
41+
_dbContext.SaveChanges();
42+
todo.AssigneeId = assignee.Id;
43+
todo.OwnerId = owner.Id;
44+
owner.PassportId = passport.Id;
45+
_dbContext.SaveChanges();
46+
47+
// Act
48+
var (body, response) = await Get($"/api/v1/people/{assignee.Id}/assignedTodoItems?include=owner.passport");
49+
50+
// Assert
51+
AssertEqualStatusCode(HttpStatusCode.OK, response);
52+
var resultTodoItem = _deserializer.DeserializeList<TodoItemClient>(body).Data.SingleOrDefault();
53+
Assert.Equal(todo.Id, resultTodoItem.Id);
54+
Assert.Equal(todo.Owner.Id, resultTodoItem.Owner.Id);
55+
Assert.Equal(todo.Owner.Passport.Id, resultTodoItem.Owner.Passport.Id);
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)