Skip to content

Commit

Permalink
Resource inheritance: sorting on derived fields
Browse files Browse the repository at this point in the history
  • Loading branch information
Bart Koelman committed Mar 21, 2022
1 parent afa13c0 commit 5b9a64d
Show file tree
Hide file tree
Showing 20 changed files with 1,285 additions and 118 deletions.
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

0 comments on commit 5b9a64d

Please sign in to comment.