Skip to content

Commit 0c79d35

Browse files
authored
Merge pull request #1641 from json-api-dotnet/resource-inheritance-fixes
Resource inheritance fixes
2 parents 55f671c + cfe159b commit 0c79d35

26 files changed

+302
-118
lines changed

Directory.Build.props

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodingGuidelines.ruleset</CodeAnalysisRuleSet>
1111
<RunSettingsFilePath>$(MSBuildThisFileDirectory)tests.runsettings</RunSettingsFilePath>
1212
<JsonApiDotNetCoreVersionPrefix>5.6.1</JsonApiDotNetCoreVersionPrefix>
13+
<NuGetAuditMode>direct</NuGetAuditMode>
14+
<NoWarn>$(NoWarn);NETSDK1215</NoWarn>
1315
</PropertyGroup>
1416

1517
<PropertyGroup>

JsonApiDotNetCore.sln.DotSettings

+1
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,7 @@ $left$ = $right$;</s:String>
668668
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=D29C3A091CD9E74BBFDF1B8974F5A977/SearchPattern/@EntryValue">if ($argument$ is null) throw new ArgumentNullException(nameof($argument$));</s:String>
669669
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=D29C3A091CD9E74BBFDF1B8974F5A977/Severity/@EntryValue">WARNING</s:String>
670670
<s:Boolean x:Key="/Default/UserDictionary/Words/=Accurize/@EntryIndexedValue">True</s:Boolean>
671+
<s:Boolean x:Key="/Default/UserDictionary/Words/=accurized/@EntryIndexedValue">True</s:Boolean>
671672
<s:Boolean x:Key="/Default/UserDictionary/Words/=appsettings/@EntryIndexedValue">True</s:Boolean>
672673
<s:Boolean x:Key="/Default/UserDictionary/Words/=Assignee/@EntryIndexedValue">True</s:Boolean>
673674
<s:Boolean x:Key="/Default/UserDictionary/Words/=Contoso/@EntryIndexedValue">True</s:Boolean>

src/Examples/DapperExample/Repositories/DapperRepository.cs

+6-5
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ public sealed partial class DapperRepository<TResource, TId> : IResourceReposito
103103
private readonly SqlCaptureStore _captureStore;
104104
private readonly ILoggerFactory _loggerFactory;
105105
private readonly ILogger<DapperRepository<TResource, TId>> _logger;
106-
private readonly CollectionConverter _collectionConverter = new();
107106
private readonly ParameterFormatter _parameterFormatter = new();
108107
private readonly DapperFacade _dapperFacade;
109108

@@ -270,12 +269,12 @@ private async Task ApplyTargetedFieldsAsync(TResource resourceFromRequest, TReso
270269

271270
if (relationship is HasManyAttribute hasManyRelationship)
272271
{
273-
HashSet<IIdentifiable> rightResourceIds = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance);
272+
HashSet<IIdentifiable> rightResourceIds = CollectionConverter.Instance.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance);
274273

275274
await _resourceDefinitionAccessor.OnSetToManyRelationshipAsync(leftResource, hasManyRelationship, rightResourceIds, writeOperation,
276275
cancellationToken);
277276

278-
return _collectionConverter.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType);
277+
return CollectionConverter.Instance.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType);
279278
}
280279

281280
return rightValue;
@@ -464,7 +463,9 @@ public async Task AddToToManyRelationshipAsync(TResource? leftResource, [Disallo
464463
leftPlaceholderResource.Id = leftId;
465464

466465
await _resourceDefinitionAccessor.OnAddToRelationshipAsync(leftPlaceholderResource, relationship, rightResourceIds, cancellationToken);
467-
relationship.SetValue(leftPlaceholderResource, _collectionConverter.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType));
466+
467+
relationship.SetValue(leftPlaceholderResource,
468+
CollectionConverter.Instance.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType));
468469

469470
await _resourceDefinitionAccessor.OnWritingAsync(leftPlaceholderResource, WriteOperationKind.AddToRelationship, cancellationToken);
470471

@@ -500,7 +501,7 @@ public async Task RemoveFromToManyRelationshipAsync(TResource leftResource, ISet
500501
var relationship = (HasManyAttribute)_targetedFields.Relationships.Single();
501502

502503
await _resourceDefinitionAccessor.OnRemoveFromRelationshipAsync(leftResource, relationship, rightResourceIds, cancellationToken);
503-
relationship.SetValue(leftResource, _collectionConverter.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType));
504+
relationship.SetValue(leftResource, CollectionConverter.Instance.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType));
504505

505506
await _resourceDefinitionAccessor.OnWritingAsync(leftResource, WriteOperationKind.RemoveFromRelationship, cancellationToken);
506507

src/Examples/DapperExample/Repositories/ResourceChangeDetector.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ namespace DapperExample.Repositories;
1212
/// </summary>
1313
internal sealed class ResourceChangeDetector
1414
{
15-
private readonly CollectionConverter _collectionConverter = new();
1615
private readonly IDataModelService _dataModelService;
1716

1817
private Dictionary<string, object?> _currentColumnValues = [];
@@ -69,7 +68,7 @@ private Dictionary<RelationshipAttribute, HashSet<IIdentifiable>> CaptureRightRe
6968
foreach (RelationshipAttribute relationship in ResourceType.Relationships)
7069
{
7170
object? rightValue = relationship.GetValue(resource);
72-
HashSet<IIdentifiable> rightResources = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance);
71+
HashSet<IIdentifiable> rightResources = CollectionConverter.Instance.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance);
7372

7473
relationshipValues[relationship] = rightResources;
7574
}

src/JsonApiDotNetCore.Annotations/CollectionConverter.cs

+29-5
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,24 @@ internal sealed class CollectionConverter
1515
typeof(IEnumerable<>)
1616
];
1717

18+
public static CollectionConverter Instance { get; } = new();
19+
20+
private CollectionConverter()
21+
{
22+
}
23+
1824
/// <summary>
1925
/// Creates a collection instance based on the specified collection type and copies the specified elements into it.
2026
/// </summary>
2127
/// <param name="source">
2228
/// Source to copy from.
2329
/// </param>
2430
/// <param name="collectionType">
25-
/// Target collection type, for example: typeof(List{Article}) or typeof(ISet{Person}).
31+
/// Target collection type, for example: <code><![CDATA[
32+
/// typeof(List<Article>)
33+
/// ]]></code> or <code><![CDATA[
34+
/// typeof(ISet<Person>)
35+
/// ]]></code>.
2636
/// </param>
2737
public IEnumerable CopyToTypedCollection(IEnumerable source, Type collectionType)
2838
{
@@ -41,7 +51,12 @@ public IEnumerable CopyToTypedCollection(IEnumerable source, Type collectionType
4151
}
4252

4353
/// <summary>
44-
/// Returns a compatible collection type that can be instantiated, for example IList{Article} -> List{Article} or ISet{Article} -> HashSet{Article}
54+
/// Returns a compatible collection type that can be instantiated, for example: <code><![CDATA[
55+
/// IList<Article> -> List<Article>
56+
/// ]]></code> or
57+
/// <code><![CDATA[
58+
/// ISet<Article> -> HashSet<Article>
59+
/// ]]></code>.
4560
/// </summary>
4661
private Type ToConcreteCollectionType(Type collectionType)
4762
{
@@ -80,7 +95,12 @@ public IReadOnlyCollection<IIdentifiable> ExtractResources(object? value)
8095
}
8196

8297
/// <summary>
83-
/// Returns the element type if the specified type is a generic collection, for example: IList{string} -> string or IList -> null.
98+
/// Returns the element type if the specified type is a generic collection, for example: <code><![CDATA[
99+
/// IList<string> -> string
100+
/// ]]></code> or
101+
/// <code><![CDATA[
102+
/// IList -> null
103+
/// ]]></code>.
84104
/// </summary>
85105
public Type? FindCollectionElementType(Type? type)
86106
{
@@ -96,8 +116,12 @@ public IReadOnlyCollection<IIdentifiable> ExtractResources(object? value)
96116
}
97117

98118
/// <summary>
99-
/// Indicates whether a <see cref="HashSet{T}" /> instance can be assigned to the specified type, for example IList{Article} -> false or ISet{Article} ->
100-
/// true.
119+
/// Indicates whether a <see cref="HashSet{T}" /> instance can be assigned to the specified type, for example:
120+
/// <code><![CDATA[
121+
/// IList<Article> -> false
122+
/// ]]></code> or <code><![CDATA[
123+
/// ISet<Article> -> true
124+
/// ]]></code>.
101125
/// </summary>
102126
public bool TypeCanContainHashSet(Type collectionType)
103127
{

src/JsonApiDotNetCore.Annotations/Resources/Annotations/HasManyAttribute.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ private bool EvaluateIsManyToMany()
5959
{
6060
if (InverseNavigationProperty != null)
6161
{
62-
Type? elementType = CollectionConverter.FindCollectionElementType(InverseNavigationProperty.PropertyType);
62+
Type? elementType = CollectionConverter.Instance.FindCollectionElementType(InverseNavigationProperty.PropertyType);
6363
return elementType != null;
6464
}
6565

@@ -103,14 +103,14 @@ public void AddValue(object resource, IIdentifiable resourceToAdd)
103103
ArgumentGuard.NotNull(resourceToAdd);
104104

105105
object? rightValue = GetValue(resource);
106-
List<IIdentifiable> rightResources = CollectionConverter.ExtractResources(rightValue).ToList();
106+
List<IIdentifiable> rightResources = CollectionConverter.Instance.ExtractResources(rightValue).ToList();
107107

108108
if (!rightResources.Exists(nextResource => nextResource == resourceToAdd))
109109
{
110110
rightResources.Add(resourceToAdd);
111111

112112
Type collectionType = rightValue?.GetType() ?? Property.PropertyType;
113-
IEnumerable typedCollection = CollectionConverter.CopyToTypedCollection(rightResources, collectionType);
113+
IEnumerable typedCollection = CollectionConverter.Instance.CopyToTypedCollection(rightResources, collectionType);
114114
base.SetValue(resource, typedCollection);
115115
}
116116
}

src/JsonApiDotNetCore.Annotations/Resources/Annotations/HasOneAttribute.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ private bool EvaluateIsOneToOne()
5757
{
5858
if (InverseNavigationProperty != null)
5959
{
60-
Type? elementType = CollectionConverter.FindCollectionElementType(InverseNavigationProperty.PropertyType);
60+
Type? elementType = CollectionConverter.Instance.FindCollectionElementType(InverseNavigationProperty.PropertyType);
6161
return elementType == null;
6262
}
6363

src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs

-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ namespace JsonApiDotNetCore.Resources.Annotations;
1212
[PublicAPI]
1313
public abstract class RelationshipAttribute : ResourceFieldAttribute
1414
{
15-
private protected static readonly CollectionConverter CollectionConverter = new();
16-
1715
// This field is definitely assigned after building the resource graph, which is why its public equivalent is declared as non-nullable.
1816
private ResourceType? _rightType;
1917

src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors;
1010
public class SetRelationshipProcessor<TResource, TId> : ISetRelationshipProcessor<TResource, TId>
1111
where TResource : class, IIdentifiable<TId>
1212
{
13-
private readonly CollectionConverter _collectionConverter = new();
1413
private readonly ISetRelationshipService<TResource, TId> _service;
1514

1615
public SetRelationshipProcessor(ISetRelationshipService<TResource, TId> service)
@@ -40,7 +39,7 @@ public SetRelationshipProcessor(ISetRelationshipService<TResource, TId> service)
4039

4140
if (relationship is HasManyAttribute)
4241
{
43-
IReadOnlyCollection<IIdentifiable> rightResources = _collectionConverter.ExtractResources(rightValue);
42+
IReadOnlyCollection<IIdentifiable> rightResources = CollectionConverter.Instance.ExtractResources(rightValue);
4443
return rightResources.ToHashSet(IdentifiableComparer.Instance);
4544
}
4645

src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs

+1-3
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,6 @@ private sealed class ArrayIndexerSegment(
319319
Func<Type, int, Type?>? getCollectionElementTypeCallback)
320320
: ModelStateKeySegment(modelType, isInComplexType, nextKey, sourcePointer, parent, getCollectionElementTypeCallback)
321321
{
322-
private static readonly CollectionConverter CollectionConverter = new();
323-
324322
public int ArrayIndex { get; } = arrayIndex;
325323

326324
public Type GetCollectionElementType()
@@ -333,7 +331,7 @@ private Type GetDeclaredCollectionElementType()
333331
{
334332
if (ModelType != typeof(string))
335333
{
336-
Type? elementType = CollectionConverter.FindCollectionElementType(ModelType);
334+
Type? elementType = CollectionConverter.Instance.FindCollectionElementType(ModelType);
337335

338336
if (elementType != null)
339337
{

src/JsonApiDotNetCore/Queries/Parsing/IncludeParser.cs

+27-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ private static ReadOnlyCollection<IncludeTreeNode> LookupRelationshipName(string
122122
{
123123
// Depending on the left side of the include chain, we may match relationships anywhere in the resource type hierarchy.
124124
// This is compensated for when rendering the response, which substitutes relationships on base types with the derived ones.
125-
IReadOnlySet<RelationshipAttribute> relationships = parent.Relationship.RightType.GetRelationshipsInTypeOrDerived(relationshipName);
125+
HashSet<RelationshipAttribute> relationships = GetRelationshipsInConcreteTypes(parent.Relationship.RightType, relationshipName);
126126

127127
if (relationships.Count > 0)
128128
{
@@ -140,6 +140,32 @@ private static ReadOnlyCollection<IncludeTreeNode> LookupRelationshipName(string
140140
return children.AsReadOnly();
141141
}
142142

143+
private static HashSet<RelationshipAttribute> GetRelationshipsInConcreteTypes(ResourceType resourceType, string relationshipName)
144+
{
145+
HashSet<RelationshipAttribute> relationshipsToInclude = [];
146+
147+
foreach (RelationshipAttribute relationship in resourceType.GetRelationshipsInTypeOrDerived(relationshipName))
148+
{
149+
if (!relationship.LeftType.ClrType.IsAbstract)
150+
{
151+
relationshipsToInclude.Add(relationship);
152+
}
153+
154+
IncludeRelationshipsFromConcreteDerivedTypes(relationship, relationshipsToInclude);
155+
}
156+
157+
return relationshipsToInclude;
158+
}
159+
160+
private static void IncludeRelationshipsFromConcreteDerivedTypes(RelationshipAttribute relationship, HashSet<RelationshipAttribute> relationshipsToInclude)
161+
{
162+
foreach (ResourceType derivedType in relationship.LeftType.GetAllConcreteDerivedTypes())
163+
{
164+
RelationshipAttribute relationshipInDerived = derivedType.GetRelationshipByPublicName(relationship.PublicName);
165+
relationshipsToInclude.Add(relationshipInDerived);
166+
}
167+
}
168+
143169
private static void AssertRelationshipsFound(HashSet<RelationshipAttribute> relationshipsFound, string relationshipName,
144170
IReadOnlyCollection<IncludeTreeNode> parents, int position)
145171
{

src/JsonApiDotNetCore/Queries/QueryLayerComposer.cs

+2-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ namespace JsonApiDotNetCore.Queries;
1414
[PublicAPI]
1515
public class QueryLayerComposer : IQueryLayerComposer
1616
{
17-
private readonly CollectionConverter _collectionConverter = new();
1817
private readonly IQueryConstraintProvider[] _constraintProviders;
1918
private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor;
2019
private readonly IJsonApiOptions _options;
@@ -213,7 +212,7 @@ private IImmutableSet<IncludeElementExpression> ProcessIncludeSet(IImmutableSet<
213212
foreach (IncludeElementExpression includeElement in includeElementsEvaluated)
214213
{
215214
parentLayer.Selection ??= new FieldSelection();
216-
FieldSelectors selectors = parentLayer.Selection.GetOrCreateSelectors(parentLayer.ResourceType);
215+
FieldSelectors selectors = parentLayer.Selection.GetOrCreateSelectors(includeElement.Relationship.LeftType);
217216

218217
if (!selectors.ContainsField(includeElement.Relationship))
219218
{
@@ -413,7 +412,7 @@ public QueryLayer ComposeForUpdate<TId>([DisallowNull] TId id, ResourceType prim
413412
foreach (RelationshipAttribute relationship in _targetedFields.Relationships)
414413
{
415414
object? rightValue = relationship.GetValue(primaryResource);
416-
HashSet<IIdentifiable> rightResourceIds = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance);
415+
HashSet<IIdentifiable> rightResourceIds = CollectionConverter.Instance.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance);
417416

418417
if (rightResourceIds.Count > 0)
419418
{

src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryClauseBuilderContext.cs

+12
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public QueryClauseBuilderContext(Expression source, ResourceType resourceType, T
6161
ArgumentGuard.NotNull(lambdaScopeFactory);
6262
ArgumentGuard.NotNull(lambdaScope);
6363
ArgumentGuard.NotNull(queryableBuilder);
64+
AssertSameType(source.Type, resourceType);
6465

6566
Source = source;
6667
ResourceType = resourceType;
@@ -72,6 +73,17 @@ public QueryClauseBuilderContext(Expression source, ResourceType resourceType, T
7273
State = state;
7374
}
7475

76+
private static void AssertSameType(Type sourceType, ResourceType resourceType)
77+
{
78+
Type? sourceElementType = CollectionConverter.Instance.FindCollectionElementType(sourceType);
79+
80+
if (sourceElementType != resourceType.ClrType)
81+
{
82+
throw new InvalidOperationException(
83+
$"Internal error: Mismatch between expression type '{sourceElementType?.Name}' and resource type '{resourceType.ClrType.Name}'.");
84+
}
85+
}
86+
7587
public QueryClauseBuilderContext WithSource(Expression source)
7688
{
7789
ArgumentGuard.NotNull(source);

src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryableBuilder.cs

+10
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public virtual Expression ApplyQuery(QueryLayer layer, QueryableBuilderContext c
3535
{
3636
ArgumentGuard.NotNull(layer);
3737
ArgumentGuard.NotNull(context);
38+
AssertSameType(layer.ResourceType, context.ElementType);
3839

3940
Expression expression = context.Source;
4041

@@ -66,6 +67,15 @@ public virtual Expression ApplyQuery(QueryLayer layer, QueryableBuilderContext c
6667
return expression;
6768
}
6869

70+
private static void AssertSameType(ResourceType resourceType, Type elementType)
71+
{
72+
if (elementType != resourceType.ClrType)
73+
{
74+
throw new InvalidOperationException(
75+
$"Internal error: Mismatch between query layer type '{resourceType.ClrType.Name}' and query element type '{elementType.Name}'.");
76+
}
77+
}
78+
6979
protected virtual Expression ApplyInclude(Expression source, IncludeExpression include, ResourceType resourceType, QueryableBuilderContext context)
7080
{
7181
ArgumentGuard.NotNull(source);

0 commit comments

Comments
 (0)