Skip to content

Resource inheritance #1142

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 19 commits into from
Apr 4, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Prev Previous commit
Next Next commit
Resource inheritance: sorting on derived fields
  • Loading branch information
Bart Koelman committed Mar 21, 2022
commit 1b2c75ecec4686722c52f38d0973208b8c87a268
55 changes: 55 additions & 0 deletions src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,61 @@ public IReadOnlySet<ResourceType> GetAllConcreteDerivedTypes()
return _lazyAllConcreteDerivedTypes.Value;
}

internal IReadOnlySet<AttrAttribute> GetAttributesInTypeOrDerived(string publicName)
{
return GetAttributesInTypeOrDerived(this, publicName);
}

private static IReadOnlySet<AttrAttribute> GetAttributesInTypeOrDerived(ResourceType resourceType, string publicName)
{
AttrAttribute? attribute = resourceType.FindAttributeByPublicName(publicName);

if (attribute != null)
{
return attribute.AsHashSet();
}

// Hiding base members using the 'new' keyword instead of 'override' (effectively breaking inheritance) is currently not supported.
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/knowing-when-to-use-override-and-new-keywords
HashSet<AttrAttribute> attributesInDerivedTypes = new();

foreach (AttrAttribute attributeInDerivedType in resourceType.DirectlyDerivedTypes
.Select(derivedType => GetAttributesInTypeOrDerived(derivedType, publicName)).SelectMany(attributesInDerivedType => attributesInDerivedType))
{
attributesInDerivedTypes.Add(attributeInDerivedType);
}

return attributesInDerivedTypes;
}

internal IReadOnlySet<RelationshipAttribute> GetRelationshipsInTypeOrDerived(string publicName)
{
return GetRelationshipsInTypeOrDerived(this, publicName);
}

private static IReadOnlySet<RelationshipAttribute> GetRelationshipsInTypeOrDerived(ResourceType resourceType, string publicName)
{
RelationshipAttribute? relationship = resourceType.FindRelationshipByPublicName(publicName);

if (relationship != null)
{
return relationship.AsHashSet();
}

// Hiding base members using the 'new' keyword instead of 'override' (effectively breaking inheritance) is currently not supported.
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/knowing-when-to-use-override-and-new-keywords
HashSet<RelationshipAttribute> relationshipsInDerivedTypes = new();

foreach (RelationshipAttribute relationshipInDerivedType in resourceType.DirectlyDerivedTypes
.Select(derivedType => GetRelationshipsInTypeOrDerived(derivedType, publicName))
.SelectMany(relationshipsInDerivedType => relationshipsInDerivedType))
{
relationshipsInDerivedTypes.Add(relationshipInDerivedType);
}

return relationshipsInDerivedTypes;
}

public override string ToString()
{
return PublicName;
Expand Down
6 changes: 3 additions & 3 deletions src/JsonApiDotNetCore/Configuration/IResourceGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ ResourceType GetResourceType<TResource>()
/// (TResource resource) => new { resource.Attribute1, resource.Relationship2 }
/// ]]>
/// </param>
IReadOnlyCollection<ResourceFieldAttribute> GetFields<TResource>(Expression<Func<TResource, dynamic?>> selector)
IReadOnlyCollection<ResourceFieldAttribute> GetFields<TResource>(Expression<Func<TResource, object?>> selector)
where TResource : class, IIdentifiable;

/// <summary>
Expand All @@ -68,7 +68,7 @@ IReadOnlyCollection<ResourceFieldAttribute> GetFields<TResource>(Expression<Func
/// (TResource resource) => new { resource.attribute1, resource.Attribute2 }
/// ]]>
/// </param>
IReadOnlyCollection<AttrAttribute> GetAttributes<TResource>(Expression<Func<TResource, dynamic?>> selector)
IReadOnlyCollection<AttrAttribute> GetAttributes<TResource>(Expression<Func<TResource, object?>> selector)
where TResource : class, IIdentifiable;

/// <summary>
Expand All @@ -82,6 +82,6 @@ IReadOnlyCollection<AttrAttribute> GetAttributes<TResource>(Expression<Func<TRes
/// (TResource resource) => new { resource.Relationship1, resource.Relationship2 }
/// ]]>
/// </param>
IReadOnlyCollection<RelationshipAttribute> GetRelationships<TResource>(Expression<Func<TResource, dynamic?>> selector)
IReadOnlyCollection<RelationshipAttribute> GetRelationships<TResource>(Expression<Func<TResource, object?>> selector)
where TResource : class, IIdentifiable;
}
10 changes: 5 additions & 5 deletions src/JsonApiDotNetCore/Configuration/ResourceGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public ResourceType GetResourceType<TResource>()
}

/// <inheritdoc />
public IReadOnlyCollection<ResourceFieldAttribute> GetFields<TResource>(Expression<Func<TResource, dynamic?>> selector)
public IReadOnlyCollection<ResourceFieldAttribute> GetFields<TResource>(Expression<Func<TResource, object?>> selector)
where TResource : class, IIdentifiable
{
ArgumentGuard.NotNull(selector, nameof(selector));
Expand All @@ -100,7 +100,7 @@ public IReadOnlyCollection<ResourceFieldAttribute> GetFields<TResource>(Expressi
}

/// <inheritdoc />
public IReadOnlyCollection<AttrAttribute> GetAttributes<TResource>(Expression<Func<TResource, dynamic?>> selector)
public IReadOnlyCollection<AttrAttribute> GetAttributes<TResource>(Expression<Func<TResource, object?>> selector)
where TResource : class, IIdentifiable
{
ArgumentGuard.NotNull(selector, nameof(selector));
Expand All @@ -109,15 +109,15 @@ public IReadOnlyCollection<AttrAttribute> GetAttributes<TResource>(Expression<Fu
}

/// <inheritdoc />
public IReadOnlyCollection<RelationshipAttribute> GetRelationships<TResource>(Expression<Func<TResource, dynamic?>> selector)
public IReadOnlyCollection<RelationshipAttribute> GetRelationships<TResource>(Expression<Func<TResource, object?>> selector)
where TResource : class, IIdentifiable
{
ArgumentGuard.NotNull(selector, nameof(selector));

return FilterFields<TResource, RelationshipAttribute>(selector);
}

private IReadOnlyCollection<TField> FilterFields<TResource, TField>(Expression<Func<TResource, dynamic?>> selector)
private IReadOnlyCollection<TField> FilterFields<TResource, TField>(Expression<Func<TResource, object?>> selector)
where TResource : class, IIdentifiable
where TField : ResourceFieldAttribute
{
Expand Down Expand Up @@ -157,7 +157,7 @@ private IReadOnlyCollection<TKind> GetFieldsOfType<TResource, TKind>()
return (IReadOnlyCollection<TKind>)resourceType.Fields;
}

private IEnumerable<string> ToMemberNames<TResource>(Expression<Func<TResource, dynamic?>> selector)
private IEnumerable<string> ToMemberNames<TResource>(Expression<Func<TResource, object?>> selector)
{
Expression selectorBody = RemoveConvert(selector.Body);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Queries.Expressions;
public static class SparseFieldSetExpressionExtensions
{
public static SparseFieldSetExpression? Including<TResource>(this SparseFieldSetExpression? sparseFieldSet,
Expression<Func<TResource, dynamic?>> fieldSelector, IResourceGraph resourceGraph)
Expression<Func<TResource, object?>> fieldSelector, IResourceGraph resourceGraph)
where TResource : class, IIdentifiable
{
ArgumentGuard.NotNull(fieldSelector, nameof(fieldSelector));
Expand Down Expand Up @@ -39,7 +39,7 @@ public static class SparseFieldSetExpressionExtensions
}

public static SparseFieldSetExpression? Excluding<TResource>(this SparseFieldSetExpression? sparseFieldSet,
Expression<Func<TResource, dynamic?>> fieldSelector, IResourceGraph resourceGraph)
Expression<Func<TResource, object?>> fieldSelector, IResourceGraph resourceGraph)
where TResource : class, IIdentifiable
{
ArgumentGuard.NotNull(fieldSelector, nameof(fieldSelector));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace JsonApiDotNetCore.Queries.Internal.Parsing;

/// <summary>
/// Indicates how to handle derived types when resolving resource field chains.
/// </summary>
internal enum FieldChainInheritanceRequirement
{
/// <summary>
/// Do not consider derived types when resolving attributes or relationships.
/// </summary>
Disabled,

/// <summary>
/// Consider derived types when resolving attributes or relationships, but fail when multiple matches are found.
/// </summary>
RequireSingleMatch
}
Original file line number Diff line number Diff line change
Expand Up @@ -425,12 +425,14 @@ protected override IImmutableList<ResourceFieldAttribute> OnResolveFieldChain(st
{
if (chainRequirements == FieldChainRequirements.EndsInToMany)
{
return ChainResolver.ResolveToOneChainEndingInToMany(_resourceTypeInScope!, path, _validateSingleFieldCallback);
return ChainResolver.ResolveToOneChainEndingInToMany(_resourceTypeInScope!, path, FieldChainInheritanceRequirement.Disabled,
_validateSingleFieldCallback);
}

if (chainRequirements == FieldChainRequirements.EndsInAttribute)
{
return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceTypeInScope!, path, _validateSingleFieldCallback);
return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceTypeInScope!, path, FieldChainInheritanceRequirement.Disabled,
_validateSingleFieldCallback);
}

if (chainRequirements == FieldChainRequirements.EndsInToOne)
Expand Down
51 changes: 7 additions & 44 deletions src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing;
[PublicAPI]
public class IncludeParser : QueryExpressionParser
{
private static readonly ResourceFieldChainErrorFormatter ErrorFormatter = new();

public IncludeExpression Parse(string source, ResourceType resourceTypeInScope, int? maximumDepth)
{
ArgumentGuard.NotNull(resourceTypeInScope, nameof(resourceTypeInScope));
Expand Down Expand Up @@ -98,7 +100,7 @@ private ICollection<IncludeTreeNode> LookupRelationshipName(string relationshipN
{
// Depending on the left side of the include chain, we may match relationships anywhere in the resource type hierarchy.
// This is compensated for when rendering the response, which substitutes relationships on base types with the derived ones.
ISet<RelationshipAttribute> relationships = GetRelationshipsInTypeOrDerived(parent.Relationship.RightType, relationshipName);
IReadOnlySet<RelationshipAttribute> relationships = parent.Relationship.RightType.GetRelationshipsInTypeOrDerived(relationshipName);

if (relationships.Any())
{
Expand All @@ -116,61 +118,22 @@ private ICollection<IncludeTreeNode> LookupRelationshipName(string relationshipN
return children;
}

private ISet<RelationshipAttribute> GetRelationshipsInTypeOrDerived(ResourceType resourceType, string relationshipName)
{
RelationshipAttribute? relationship = resourceType.FindRelationshipByPublicName(relationshipName);

if (relationship != null)
{
return relationship.AsHashSet();
}

// Hiding base members using the 'new' keyword instead of 'override' (effectively breaking inheritance) is currently not supported.
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/knowing-when-to-use-override-and-new-keywords
HashSet<RelationshipAttribute> relationshipsInDerivedTypes = new();

foreach (ResourceType derivedType in resourceType.DirectlyDerivedTypes)
{
ISet<RelationshipAttribute> relationshipsInDerivedType = GetRelationshipsInTypeOrDerived(derivedType, relationshipName);
relationshipsInDerivedTypes.AddRange(relationshipsInDerivedType);
}

return relationshipsInDerivedTypes;
}

private static void AssertRelationshipsFound(ISet<RelationshipAttribute> relationshipsFound, string relationshipName, ICollection<IncludeTreeNode> parents)
{
if (relationshipsFound.Any())
{
return;
}

var messageBuilder = new StringBuilder();
messageBuilder.Append($"Relationship '{relationshipName}'");

string[] parentPaths = parents.Select(parent => parent.Path).Distinct().Where(path => path != string.Empty).ToArray();

if (parentPaths.Length > 0)
{
messageBuilder.Append($" in '{parentPaths[0]}.{relationshipName}'");
}
string path = parentPaths.Length > 0 ? $"{parentPaths[0]}.{relationshipName}" : relationshipName;

ResourceType[] parentResourceTypes = parents.Select(parent => parent.Relationship.RightType).Distinct().ToArray();

if (parentResourceTypes.Length == 1)
{
messageBuilder.Append($" does not exist on resource type '{parentResourceTypes[0].PublicName}'");
}
else
{
string typeNames = string.Join(", ", parentResourceTypes.Select(type => $"'{type.PublicName}'"));
messageBuilder.Append($" does not exist on any of the resource types {typeNames}");
}

bool hasDerived = parents.Any(parent => parent.Relationship.RightType.DirectlyDerivedTypes.Count > 0);
messageBuilder.Append(hasDerived ? " or any of its derived types." : ".");
bool hasDerivedTypes = parents.Any(parent => parent.Relationship.RightType.DirectlyDerivedTypes.Count > 0);

throw new QueryParseException(messageBuilder.ToString());
string message = ErrorFormatter.GetForNoneFound(ResourceFieldCategory.Relationship, relationshipName, path, parentResourceTypes, hasDerivedTypes);
throw new QueryParseException(message);
}

private static void AssertAtLeastOneCanBeIncluded(ISet<RelationshipAttribute> relationshipsFound, string relationshipName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace JsonApiDotNetCore.Queries.Internal.Parsing;

internal enum ResourceFieldCategory
{
Field,
Attribute,
Relationship
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.Text;
using JsonApiDotNetCore.Configuration;

namespace JsonApiDotNetCore.Queries.Internal.Parsing;

internal sealed class ResourceFieldChainErrorFormatter
{
public string GetForNotFound(ResourceFieldCategory category, string publicName, string path, ResourceType resourceType,
FieldChainInheritanceRequirement inheritanceRequirement)
{
var builder = new StringBuilder();
WriteSource(category, publicName, builder);
WritePath(path, publicName, builder);

builder.Append($" does not exist on resource type '{resourceType.PublicName}'");

if (inheritanceRequirement != FieldChainInheritanceRequirement.Disabled && resourceType.DirectlyDerivedTypes.Any())
{
builder.Append(" or any of its derived types");
}

builder.Append('.');

return builder.ToString();
}

public string GetForMultipleMatches(ResourceFieldCategory category, string publicName, string path)
{
var builder = new StringBuilder();
WriteSource(category, publicName, builder);
WritePath(path, publicName, builder);

builder.Append(" is defined on multiple derived types.");

return builder.ToString();
}

public string GetForWrongFieldType(ResourceFieldCategory category, string publicName, string path, ResourceType resourceType, string expected)
{
var builder = new StringBuilder();
WriteSource(category, publicName, builder);
WritePath(path, publicName, builder);

builder.Append($" must be {expected} on resource type '{resourceType.PublicName}'.");

return builder.ToString();
}

public string GetForNoneFound(ResourceFieldCategory category, string publicName, string path, ICollection<ResourceType> parentResourceTypes,
bool hasDerivedTypes)
{
var builder = new StringBuilder();
WriteSource(category, publicName, builder);
WritePath(path, publicName, builder);

if (parentResourceTypes.Count == 1)
{
builder.Append($" does not exist on resource type '{parentResourceTypes.First().PublicName}'");
}
else
{
string typeNames = string.Join(", ", parentResourceTypes.Select(type => $"'{type.PublicName}'"));
builder.Append($" does not exist on any of the resource types {typeNames}");
}

builder.Append(hasDerivedTypes ? " or any of its derived types." : ".");

return builder.ToString();
}

private static void WriteSource(ResourceFieldCategory category, string publicName, StringBuilder builder)
{
builder.Append($"{category} '{publicName}'");
}

private static void WritePath(string path, string publicName, StringBuilder builder)
{
if (path != publicName)
{
builder.Append($" in '{path}'");
}
}
}
Loading