Skip to content

Commit 4a29199

Browse files
author
Bart Koelman
committed
Resource inheritance: return derived types
1 parent f0fce11 commit 4a29199

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1521
-148
lines changed

src/JsonApiDotNetCore.Annotations/Configuration/ResourceType.cs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public sealed class ResourceType
1111
{
1212
private readonly Dictionary<string, ResourceFieldAttribute> _fieldsByPublicName = new();
1313
private readonly Dictionary<string, ResourceFieldAttribute> _fieldsByPropertyName = new();
14+
private readonly Lazy<IReadOnlySet<ResourceType>> _lazyAllConcreteDerivedTypes;
1415

1516
/// <summary>
1617
/// The publicly exposed resource name.
@@ -28,22 +29,35 @@ public sealed class ResourceType
2829
public Type IdentityClrType { get; }
2930

3031
/// <summary>
31-
/// Exposed resource attributes and relationships. See https://jsonapi.org/format/#document-resource-object-fields.
32+
/// The base resource type, in case this is a derived type.
33+
/// </summary>
34+
public ResourceType? BaseType { get; internal set; }
35+
36+
/// <summary>
37+
/// The resource types that directly derive from this one.
38+
/// </summary>
39+
public IReadOnlySet<ResourceType> DirectlyDerivedTypes { get; internal set; } = new HashSet<ResourceType>();
40+
41+
/// <summary>
42+
/// Exposed resource attributes and relationships. See https://jsonapi.org/format/#document-resource-object-fields. When using resource inheritance, this
43+
/// includes the attributes and relationships from base types.
3244
/// </summary>
3345
public IReadOnlyCollection<ResourceFieldAttribute> Fields { get; }
3446

3547
/// <summary>
36-
/// Exposed resource attributes. See https://jsonapi.org/format/#document-resource-object-attributes.
48+
/// Exposed resource attributes. See https://jsonapi.org/format/#document-resource-object-attributes. When using resource inheritance, this includes the
49+
/// attributes from base types.
3750
/// </summary>
3851
public IReadOnlyCollection<AttrAttribute> Attributes { get; }
3952

4053
/// <summary>
41-
/// Exposed resource relationships. See https://jsonapi.org/format/#document-resource-object-relationships.
54+
/// Exposed resource relationships. See https://jsonapi.org/format/#document-resource-object-relationships. When using resource inheritance, this
55+
/// includes the relationships from base types.
4256
/// </summary>
4357
public IReadOnlyCollection<RelationshipAttribute> Relationships { get; }
4458

4559
/// <summary>
46-
/// Related entities that are not exposed as resource relationships.
60+
/// Related entities that are not exposed as resource relationships. When using resource inheritance, this includes the eager-loads from base types.
4761
/// </summary>
4862
public IReadOnlyCollection<EagerLoadAttribute> EagerLoads { get; }
4963

@@ -100,6 +114,29 @@ public ResourceType(string publicName, Type clrType, Type identityClrType, IRead
100114
_fieldsByPublicName.Add(field.PublicName, field);
101115
_fieldsByPropertyName.Add(field.Property.Name, field);
102116
}
117+
118+
_lazyAllConcreteDerivedTypes = new Lazy<IReadOnlySet<ResourceType>>(ResolveAllConcreteDerivedTypes, LazyThreadSafetyMode.PublicationOnly);
119+
}
120+
121+
private IReadOnlySet<ResourceType> ResolveAllConcreteDerivedTypes()
122+
{
123+
var allConcreteDerivedTypes = new HashSet<ResourceType>();
124+
AddConcreteDerivedTypes(this, allConcreteDerivedTypes);
125+
126+
return allConcreteDerivedTypes;
127+
}
128+
129+
private static void AddConcreteDerivedTypes(ResourceType resourceType, ISet<ResourceType> allConcreteDerivedTypes)
130+
{
131+
foreach (ResourceType derivedType in resourceType.DirectlyDerivedTypes)
132+
{
133+
if (!derivedType.ClrType.IsAbstract)
134+
{
135+
allConcreteDerivedTypes.Add(derivedType);
136+
}
137+
138+
AddConcreteDerivedTypes(derivedType, allConcreteDerivedTypes);
139+
}
103140
}
104141

105142
public AttrAttribute GetAttributeByPublicName(string publicName)
@@ -161,6 +198,14 @@ public RelationshipAttribute GetRelationshipByPropertyName(string propertyName)
161198
: null;
162199
}
163200

201+
/// <summary>
202+
/// Returns all directly and indirectly non-abstract resource types that derive from this resource type.
203+
/// </summary>
204+
public IReadOnlySet<ResourceType> GetAllConcreteDerivedTypes()
205+
{
206+
return _lazyAllConcreteDerivedTypes.Value;
207+
}
208+
164209
public override string ToString()
165210
{
166211
return PublicName;

src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,15 @@ public IResourceGraph Build()
4343

4444
var resourceGraph = new ResourceGraph(resourceTypes);
4545

46-
foreach (RelationshipAttribute relationship in resourceTypes.SelectMany(resourceType => resourceType.Relationships))
46+
SetRelationshipTypes(resourceGraph);
47+
SetDirectlyDerivedTypes(resourceGraph);
48+
49+
return resourceGraph;
50+
}
51+
52+
private static void SetRelationshipTypes(ResourceGraph resourceGraph)
53+
{
54+
foreach (RelationshipAttribute relationship in resourceGraph.GetResourceTypes().SelectMany(resourceType => resourceType.Relationships))
4755
{
4856
relationship.LeftType = resourceGraph.GetResourceType(relationship.LeftClrType!);
4957
ResourceType? rightType = resourceGraph.FindResourceType(relationship.RightClrType!);
@@ -56,8 +64,33 @@ public IResourceGraph Build()
5664

5765
relationship.RightType = rightType;
5866
}
67+
}
5968

60-
return resourceGraph;
69+
private static void SetDirectlyDerivedTypes(ResourceGraph resourceGraph)
70+
{
71+
Dictionary<ResourceType, HashSet<ResourceType>> directlyDerivedTypesPerBaseType = new();
72+
73+
foreach (ResourceType resourceType in resourceGraph.GetResourceTypes())
74+
{
75+
ResourceType? baseType = resourceGraph.FindResourceType(resourceType.ClrType.BaseType!);
76+
77+
if (baseType != null)
78+
{
79+
resourceType.BaseType = baseType;
80+
81+
if (!directlyDerivedTypesPerBaseType.ContainsKey(baseType))
82+
{
83+
directlyDerivedTypesPerBaseType[baseType] = new HashSet<ResourceType>();
84+
}
85+
86+
directlyDerivedTypesPerBaseType[baseType].Add(resourceType);
87+
}
88+
}
89+
90+
foreach ((ResourceType baseType, HashSet<ResourceType> directlyDerivedTypes) in directlyDerivedTypesPerBaseType)
91+
{
92+
baseType.DirectlyDerivedTypes = directlyDerivedTypes;
93+
}
6194
}
6295

6396
public ResourceGraphBuilder Add(DbContext dbContext)

src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using JetBrains.Annotations;
44
using JsonApiDotNetCore.AtomicOperations;
55
using JsonApiDotNetCore.Configuration;
6+
using JsonApiDotNetCore.Errors;
67
using JsonApiDotNetCore.Middleware;
78
using JsonApiDotNetCore.Queries.Expressions;
89
using JsonApiDotNetCore.Queries.Internal;
@@ -172,15 +173,37 @@ private ResourceObjectTreeNode GetOrCreateTreeNode(IIdentifiable resource, Resou
172173
{
173174
if (!_resourceToTreeNodeCache.TryGetValue(resource, out ResourceObjectTreeNode? treeNode))
174175
{
175-
ResourceObject resourceObject = ConvertResource(resource, resourceType, kind);
176-
treeNode = new ResourceObjectTreeNode(resource, resourceType, resourceObject);
176+
// In case of resource inheritance, prefer the derived resource type over the base type.
177+
ResourceType effectiveResourceType = GetEffectiveResourceType(resource, resourceType);
178+
179+
ResourceObject resourceObject = ConvertResource(resource, effectiveResourceType, kind);
180+
treeNode = new ResourceObjectTreeNode(resource, effectiveResourceType, resourceObject);
177181

178182
_resourceToTreeNodeCache.Add(resource, treeNode);
179183
}
180184

181185
return treeNode;
182186
}
183187

188+
private static ResourceType GetEffectiveResourceType(IIdentifiable resource, ResourceType declaredType)
189+
{
190+
Type runtimeResourceType = resource.GetType();
191+
192+
if (declaredType.ClrType == runtimeResourceType)
193+
{
194+
return declaredType;
195+
}
196+
197+
ResourceType? derivedType = declaredType.GetAllConcreteDerivedTypes().FirstOrDefault(type => type.ClrType == runtimeResourceType);
198+
199+
if (derivedType == null)
200+
{
201+
throw new InvalidConfigurationException($"Type '{runtimeResourceType}' does not exist in the resource graph.");
202+
}
203+
204+
return derivedType;
205+
}
206+
184207
protected virtual ResourceObject ConvertResource(IIdentifiable resource, ResourceType resourceType, EndpointKind kind)
185208
{
186209
bool isRelationship = kind == EndpointKind.Relationship;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using JetBrains.Annotations;
2+
using JsonApiDotNetCore.Resources;
3+
using JsonApiDotNetCore.Resources.Annotations;
4+
5+
namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.Models;
6+
7+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
8+
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance")]
9+
public sealed class BicycleLight : Identifiable<long>
10+
{
11+
[Attr]
12+
public string Color { get; set; } = null!;
13+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using JetBrains.Annotations;
2+
using JsonApiDotNetCore.Resources.Annotations;
3+
4+
namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.Models;
5+
6+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
7+
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance")]
8+
public class Bike : Vehicle
9+
{
10+
[Attr]
11+
public override bool RequiresDriverLicense { get; set; }
12+
13+
[Attr]
14+
public int GearCount { get; set; }
15+
16+
[HasOne]
17+
public Box? CargoBox { get; set; }
18+
19+
[HasMany]
20+
public ISet<BicycleLight> Lights { get; set; } = new HashSet<BicycleLight>();
21+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using JetBrains.Annotations;
2+
using JsonApiDotNetCore.Resources;
3+
using JsonApiDotNetCore.Resources.Annotations;
4+
5+
namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.Models;
6+
7+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
8+
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance")]
9+
public sealed class Box : Identifiable<long>
10+
{
11+
[Attr]
12+
public decimal Width { get; set; }
13+
14+
[Attr]
15+
public decimal Height { get; set; }
16+
17+
[Attr]
18+
public decimal Depth { get; set; }
19+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using JetBrains.Annotations;
2+
using JsonApiDotNetCore.Resources.Annotations;
3+
4+
namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.Models;
5+
6+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
7+
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance")]
8+
public sealed class Car : MotorVehicle
9+
{
10+
[Attr]
11+
public int SeatCount { get; set; }
12+
13+
[HasMany]
14+
public ISet<GenericFeature> Features { get; set; } = new HashSet<GenericFeature>();
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using JetBrains.Annotations;
2+
using JsonApiDotNetCore.Resources.Annotations;
3+
4+
namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.Models;
5+
6+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
7+
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance")]
8+
public sealed class CarbonWheel : Wheel
9+
{
10+
[Attr]
11+
public bool HasTube { get; set; }
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using JetBrains.Annotations;
2+
using JsonApiDotNetCore.Resources.Annotations;
3+
4+
namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.Models;
5+
6+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
7+
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance")]
8+
public sealed class ChromeWheel : Wheel
9+
{
10+
[Attr]
11+
public string? PaintColor { get; set; }
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using JetBrains.Annotations;
2+
using JsonApiDotNetCore.Resources;
3+
using JsonApiDotNetCore.Resources.Annotations;
4+
5+
namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.Models;
6+
7+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
8+
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance")]
9+
public sealed class Cylinder : Identifiable<long>
10+
{
11+
[Attr]
12+
public int SparkPlugCount { get; set; }
13+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using JetBrains.Annotations;
2+
using JsonApiDotNetCore.Resources.Annotations;
3+
4+
namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.Models;
5+
6+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
7+
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance")]
8+
public sealed class DieselEngine : Engine
9+
{
10+
[Attr]
11+
public override bool IsHydrocarbonBased { get; set; }
12+
13+
[Attr]
14+
public string? SerialCode { get; set; }
15+
16+
[Attr]
17+
public decimal Viscosity { get; set; }
18+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using JetBrains.Annotations;
2+
using JsonApiDotNetCore.Resources;
3+
using JsonApiDotNetCore.Resources.Annotations;
4+
5+
namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.Models;
6+
7+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
8+
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance")]
9+
public abstract class Engine : Identifiable<long>
10+
{
11+
[Attr]
12+
public abstract bool IsHydrocarbonBased { get; set; }
13+
14+
[Attr]
15+
public decimal Capacity { get; set; }
16+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using JetBrains.Annotations;
2+
using JsonApiDotNetCore.Resources.Annotations;
3+
4+
namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.Models;
5+
6+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
7+
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance")]
8+
public sealed class GasolineEngine : Engine
9+
{
10+
[Attr]
11+
public override bool IsHydrocarbonBased { get; set; }
12+
13+
[Attr]
14+
public string? SerialCode { get; set; }
15+
16+
[Attr]
17+
public decimal Volatility { get; set; }
18+
19+
[HasMany]
20+
public ISet<Cylinder> Cylinders { get; set; } = new HashSet<Cylinder>();
21+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using JetBrains.Annotations;
2+
using JsonApiDotNetCore.Resources;
3+
using JsonApiDotNetCore.Resources.Annotations;
4+
5+
namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.Models;
6+
7+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
8+
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance")]
9+
public sealed class GenericFeature : Identifiable<long>
10+
{
11+
[Attr]
12+
public string Description { get; set; } = null!;
13+
14+
[HasMany]
15+
public ISet<GenericProperty> Properties { get; set; } = new HashSet<GenericProperty>();
16+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using JetBrains.Annotations;
2+
using JsonApiDotNetCore.Resources;
3+
using JsonApiDotNetCore.Resources.Annotations;
4+
5+
namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance.Models;
6+
7+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
8+
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.ResourceInheritance")]
9+
public abstract class GenericProperty : Identifiable<long>
10+
{
11+
[Attr]
12+
public string Name { get; set; } = null!;
13+
}

0 commit comments

Comments
 (0)