Skip to content

Feat/#39: Deeply Nested Inclusions #378

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Sep 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d1777cf
add acceptance test for deeply nested inclusions
jaredcnance Aug 14, 2018
3528e1f
first pass at deeply nested inclusion implementation
jaredcnance Aug 14, 2018
1b80f05
fix(JsonApiDeserializer): NullReferenceException
jaredcnance Aug 15, 2018
d930c5b
fix(DocumentBuilder): handle HasMany relationships
jaredcnance Aug 15, 2018
8847403
fix(JsonApiDeSerializer): allow empty collection
jaredcnance Aug 15, 2018
bea642d
work on adding tests for different scenarios
NullVoxPopuli Aug 29, 2018
4af95d1
assert included counts
NullVoxPopuli Aug 29, 2018
36c5eb6
these tests pass -- should probably add more specific tests though
NullVoxPopuli Aug 29, 2018
c7887b8
try to assert some more complicated stuff, and check specific include…
NullVoxPopuli Aug 29, 2018
b02b3ec
I think I found a bug in the current implementation?
NullVoxPopuli Aug 29, 2018
41ba2ed
omg. im dumb
NullVoxPopuli Aug 29, 2018
bda10b8
hasMany.hasMany
NullVoxPopuli Aug 29, 2018
daa61ab
some more assertions
NullVoxPopuli Aug 29, 2018
1a367fa
this is a big one
NullVoxPopuli Aug 29, 2018
82cfa71
more assertions... and then commented out, because I don't know what …
NullVoxPopuli Aug 29, 2018
a0b6080
this one is tricky
NullVoxPopuli Aug 30, 2018
909c4c6
it passes!
NullVoxPopuli Aug 30, 2018
a8031c6
Merge pull request #389 from NullVoxPopuli/feature/#39-implementation
jaredcnance Sep 4, 2018
54798b9
Merge pull request #390 from NullVoxPopuli/feature/#39-implementation…
jaredcnance Sep 4, 2018
b0e4fa1
Merge pull request #393 from NullVoxPopuli/feature/#39-implementation…
jaredcnance Sep 4, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 48 additions & 16 deletions src/JsonApiDotNetCore/Builders/DocumentBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,24 +192,62 @@ private RelationshipData GetRelationshipData(RelationshipAttribute attr, Context
return relationshipData;
}

private List<DocumentData> GetIncludedEntities(List<DocumentData> included, ContextEntity contextEntity, IIdentifiable entity)
private List<DocumentData> GetIncludedEntities(List<DocumentData> included, ContextEntity rootContextEntity, IIdentifiable rootResource)
{
contextEntity.Relationships.ForEach(r =>
if(_jsonApiContext.IncludedRelationships != null)
{
if (!RelationshipIsIncluded(r.PublicRelationshipName)) return;
foreach(var relationshipName in _jsonApiContext.IncludedRelationships)
{
var relationshipChain = relationshipName.Split('.');

var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, r.InternalRelationshipName);
var contextEntity = rootContextEntity;
var entity = rootResource;
included = IncludeRelationshipChain(included, rootContextEntity, rootResource, relationshipChain, 0);
}
}

if (navigationEntity is IEnumerable hasManyNavigationEntity)
foreach (IIdentifiable includedEntity in hasManyNavigationEntity)
included = AddIncludedEntity(included, includedEntity);
else
included = AddIncludedEntity(included, (IIdentifiable)navigationEntity);
});
return included;
}

private List<DocumentData> IncludeRelationshipChain(
List<DocumentData> included, ContextEntity parentEntity, IIdentifiable parentResource, string[] relationshipChain, int relationshipChainIndex)
{
var requestedRelationship = relationshipChain[relationshipChainIndex];
var relationship = parentEntity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == requestedRelationship);
var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(parentResource, relationship.InternalRelationshipName);
if (navigationEntity is IEnumerable hasManyNavigationEntity)
{
foreach (IIdentifiable includedEntity in hasManyNavigationEntity)
{
included = AddIncludedEntity(included, includedEntity);
included = IncludeSingleResourceRelationships(included, includedEntity, relationship, relationshipChain, relationshipChainIndex);
}
}
else
{
included = AddIncludedEntity(included, (IIdentifiable)navigationEntity);
included = IncludeSingleResourceRelationships(included, (IIdentifiable)navigationEntity, relationship, relationshipChain, relationshipChainIndex);
}

return included;
}

private List<DocumentData> IncludeSingleResourceRelationships(
List<DocumentData> included, IIdentifiable navigationEntity, RelationshipAttribute relationship, string[] relationshipChain, int relationshipChainIndex)
{
if(relationshipChainIndex < relationshipChain.Length)
{
var nextContextEntity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type);
var resource = (IIdentifiable)navigationEntity;
// recursive call
if(relationshipChainIndex < relationshipChain.Length - 1)
included = IncludeRelationshipChain(included, nextContextEntity, resource, relationshipChain, relationshipChainIndex + 1);
}

return included;
}


private List<DocumentData> AddIncludedEntity(List<DocumentData> entities, IIdentifiable entity)
{
var includedEntity = GetIncludedEntity(entity);
Expand Down Expand Up @@ -245,12 +283,6 @@ private DocumentData GetIncludedEntity(IIdentifiable entity)
return data;
}

private bool RelationshipIsIncluded(string relationshipName)
{
return _jsonApiContext.IncludedRelationships != null &&
_jsonApiContext.IncludedRelationships.Contains(relationshipName);
}

private List<ResourceIdentifierObject> GetRelationships(IEnumerable<object> entities)
{
var objType = entities.GetElementType();
Expand Down
36 changes: 27 additions & 9 deletions src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,20 +203,38 @@ public virtual async Task<bool> DeleteAsync(TId id)
/// <inheritdoc />
public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relationshipName)
{
if(string.IsNullOrWhiteSpace(relationshipName)) throw new JsonApiException(400, "Include parameter must not be empty if provided");

var relationshipChain = relationshipName.Split('.');

// variables mutated in recursive loop
// TODO: make recursive method
string internalRelationshipPath = null;
var entity = _jsonApiContext.RequestEntity;
var relationship = entity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == relationshipName);
if (relationship == null)
for(var i = 0; i < relationshipChain.Length; i++)
{
throw new JsonApiException(400, $"Invalid relationship {relationshipName} on {entity.EntityName}",
$"{entity.EntityName} does not have a relationship named {relationshipName}");
}
var requestedRelationship = relationshipChain[i];
var relationship = entity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == requestedRelationship);
if (relationship == null)
{
throw new JsonApiException(400, $"Invalid relationship {requestedRelationship} on {entity.EntityName}",
$"{entity.EntityName} does not have a relationship named {requestedRelationship}");
}

if (!relationship.CanInclude)
{
throw new JsonApiException(400, $"Including the relationship {relationshipName} on {entity.EntityName} is not allowed");
if (relationship.CanInclude == false)
{
throw new JsonApiException(400, $"Including the relationship {requestedRelationship} on {entity.EntityName} is not allowed");
}

internalRelationshipPath = (internalRelationshipPath == null)
? relationship.InternalRelationshipName
: $"{internalRelationshipPath}.{relationship.InternalRelationshipName}";

if(i < relationshipChain.Length)
entity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type);
}

return entities.Include(relationship.InternalRelationshipName);
return entities.Include(internalRelationshipPath);
}

/// <inheritdoc />
Expand Down
5 changes: 5 additions & 0 deletions src/JsonApiDotNetCore/Models/RelationshipAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ public bool TryGetHasMany(out HasManyAttribute result)

public abstract void SetValue(object entity, object newValue);

public object GetValue(object entity) => entity
?.GetType()
.GetProperty(InternalRelationshipName)
.GetValue(entity);

public override string ToString()
{
return base.ToString() + ":" + PublicRelationshipName;
Expand Down
13 changes: 13 additions & 0 deletions src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,19 @@ private object SetHasOneRelationship(object entity,
SetHasOneForeignKeyValue(entity, attr, foreignKeyProperty, rio);
SetHasOneNavigationPropertyValue(entity, attr, rio, included);

// recursive call ...
if(included != null)
{
var navigationPropertyValue = attr.GetValue(entity);
var contextGraphEntity = _jsonApiContext.ContextGraph.GetContextEntity(attr.Type);
if(navigationPropertyValue != null && contextGraphEntity != null)
{
var includedResource = included.SingleOrDefault(r => r.Type == rio.Type && r.Id == rio.Id);
if(includedResource != null)
SetRelationships(navigationPropertyValue, contextGraphEntity, includedResource.Relationships, included);
}
}

return entity;
}

Expand Down
4 changes: 0 additions & 4 deletions src/JsonApiDotNetCore/Services/QueryParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,6 @@ protected virtual List<SortQuery> ParseSortParameters(string value)

protected virtual List<string> ParseIncludedRelationships(string value)
{
const string NESTED_DELIMITER = ".";
if (value.Contains(NESTED_DELIMITER))
throw new JsonApiException(400, "Deeply nested relationships are not supported");

return value
.Split(QueryConstants.COMMA)
.ToList();
Expand Down
Loading