Skip to content

Commit ad8c4c8

Browse files
author
Bart Koelman
committed
Resource graph validation: fail if field from base type does not exist on derived type
1 parent cb1f020 commit ad8c4c8

File tree

6 files changed

+131
-5
lines changed

6 files changed

+131
-5
lines changed

src/JsonApiDotNetCore/Configuration/ResourceGraph.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public ResourceType GetResourceType(string publicName)
4242

4343
if (resourceType == null)
4444
{
45-
throw new InvalidOperationException($"Resource type '{publicName}' does not exist.");
45+
throw new InvalidOperationException($"Resource type '{publicName}' does not exist in the resource graph.");
4646
}
4747

4848
return resourceType;
@@ -63,7 +63,7 @@ public ResourceType GetResourceType(Type resourceClrType)
6363

6464
if (resourceType == null)
6565
{
66-
throw new InvalidOperationException($"Resource of type '{resourceClrType.Name}' does not exist.");
66+
throw new InvalidOperationException($"Type '{resourceClrType}' does not exist in the resource graph.");
6767
}
6868

6969
return resourceType;

src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public IResourceGraph Build()
4646
SetFieldTypes(resourceGraph);
4747
SetRelationshipTypes(resourceGraph);
4848
SetDirectlyDerivedTypes(resourceGraph);
49+
ValidateFieldsInDerivedTypes(resourceGraph);
4950

5051
return resourceGraph;
5152
}
@@ -105,6 +106,42 @@ private static void SetDirectlyDerivedTypes(ResourceGraph resourceGraph)
105106
}
106107
}
107108

109+
private void ValidateFieldsInDerivedTypes(ResourceGraph resourceGraph)
110+
{
111+
foreach (ResourceType resourceType in resourceGraph.GetResourceTypes())
112+
{
113+
if (resourceType.BaseType != null)
114+
{
115+
ValidateAttributesInDerivedType(resourceType);
116+
ValidateRelationshipsInDerivedType(resourceType);
117+
}
118+
}
119+
}
120+
121+
private static void ValidateAttributesInDerivedType(ResourceType resourceType)
122+
{
123+
foreach (AttrAttribute attribute in resourceType.BaseType!.Attributes)
124+
{
125+
if (resourceType.FindAttributeByPublicName(attribute.PublicName) == null)
126+
{
127+
throw new InvalidConfigurationException($"Attribute '{attribute.PublicName}' from base type " +
128+
$"'{resourceType.BaseType.ClrType}' does not exist in derived type '{resourceType.ClrType}'.");
129+
}
130+
}
131+
}
132+
133+
private static void ValidateRelationshipsInDerivedType(ResourceType resourceType)
134+
{
135+
foreach (RelationshipAttribute relationship in resourceType.BaseType!.Relationships)
136+
{
137+
if (resourceType.FindRelationshipByPublicName(relationship.PublicName) == null)
138+
{
139+
throw new InvalidConfigurationException($"Relationship '{relationship.PublicName}' from base type " +
140+
$"'{resourceType.BaseType.ClrType}' does not exist in derived type '{resourceType.ClrType}'.");
141+
}
142+
}
143+
}
144+
108145
public ResourceGraphBuilder Add(DbContext dbContext)
109146
{
110147
ArgumentGuard.NotNull(dbContext, nameof(dbContext));

src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ private static void RegisterTypeForUnboundInterfaces(IServiceCollection serviceC
111111

112112
if (!seenCompatibleInterface)
113113
{
114-
throw new InvalidConfigurationException($"{implementationType} does not implement any of the expected JsonApiDotNetCore interfaces.");
114+
throw new InvalidConfigurationException($"Type '{implementationType}' does not implement any of the expected JsonApiDotNetCore interfaces.");
115115
}
116116
}
117117

test/JsonApiDotNetCoreTests/UnitTests/ResourceDefinitions/CreateSortExpressionFromLambdaTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ public void Cannot_convert_unexposed_resource_type()
205205
exception.Errors[0].StatusCode.Should().Be(HttpStatusCode.InternalServerError);
206206
exception.Errors[0].Title.Should().StartWith("Invalid lambda expression for sorting from resource definition. It should ");
207207
exception.Errors[0].Detail.Should().StartWith("The lambda expression 'entry => Convert(entry, FileEntry).Content' is invalid. ");
208-
exception.Errors[0].Detail.Should().EndWith("Resource of type 'FileEntry' does not exist.");
208+
exception.Errors[0].Detail.Should().EndWith($"Type '{typeof(FileEntry)}' does not exist in the resource graph.");
209209
exception.Errors[0].Source.Should().BeNull();
210210
}
211211

test/JsonApiDotNetCoreTests/UnitTests/ResourceGraph/ResourceGraphBuilderTests.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,60 @@ public void Cannot_build_graph_with_missing_related_HasMany_resource()
197197
$"depends on '{typeof(ResourceWithAttribute)}', which was not added to the resource graph.");
198198
}
199199

200+
[Fact]
201+
public void Cannot_build_graph_with_different_attribute_name_in_derived_type()
202+
{
203+
// Arrange
204+
var options = new JsonApiOptions();
205+
var builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance);
206+
207+
builder.Add<AbstractBaseResource, int>();
208+
builder.Add<DerivedAlternateAttributeName, int>();
209+
210+
// Act
211+
Action action = () => builder.Build();
212+
213+
// Assert
214+
action.Should().ThrowExactly<InvalidConfigurationException>().WithMessage("Attribute 'baseValue' from base type " +
215+
$"'{typeof(AbstractBaseResource)}' does not exist in derived type '{typeof(DerivedAlternateAttributeName)}'.");
216+
}
217+
218+
[Fact]
219+
public void Cannot_build_graph_with_different_ToOne_relationship_name_in_derived_type()
220+
{
221+
// Arrange
222+
var options = new JsonApiOptions();
223+
var builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance);
224+
225+
builder.Add<AbstractBaseResource, int>();
226+
builder.Add<DerivedAlternateToOneRelationshipName, int>();
227+
228+
// Act
229+
Action action = () => builder.Build();
230+
231+
// Assert
232+
action.Should().ThrowExactly<InvalidConfigurationException>().WithMessage("Relationship 'baseToOne' from base type " +
233+
$"'{typeof(AbstractBaseResource)}' does not exist in derived type '{typeof(DerivedAlternateToOneRelationshipName)}'.");
234+
}
235+
236+
[Fact]
237+
public void Cannot_build_graph_with_different_ToMany_relationship_name_in_derived_type()
238+
{
239+
// Arrange
240+
var options = new JsonApiOptions();
241+
var builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance);
242+
243+
builder.Add<AbstractBaseResource, int>();
244+
builder.Add<DerivedAlternateToManyRelationshipName, int>();
245+
246+
// Act
247+
Action action = () => builder.Build();
248+
249+
// Assert
250+
action.Should().ThrowExactly<InvalidConfigurationException>().WithMessage("Relationship 'baseToMany' from base type " +
251+
$"'{typeof(AbstractBaseResource)}' does not exist in derived type '{typeof(DerivedAlternateToManyRelationshipName)}'.");
252+
}
253+
200254
[Fact]
201255
public void Logs_warning_when_adding_non_resource_type()
202256
{
@@ -405,4 +459,38 @@ public sealed class ResourceWithIdOverride : Identifiable<long>
405459
[Attr(Capabilities = AttrCapabilities.AllowFilter)]
406460
public override long Id { get; set; }
407461
}
462+
463+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
464+
public abstract class AbstractBaseResource : Identifiable<int>
465+
{
466+
[Attr(PublicName = "baseValue")]
467+
public virtual int Value { get; set; }
468+
469+
[HasOne(PublicName = "baseToOne")]
470+
public virtual AbstractBaseResource? BaseToOne { get; set; }
471+
472+
[HasMany(PublicName = "baseToMany")]
473+
public virtual ISet<AbstractBaseResource> BaseToMany { get; set; } = new HashSet<AbstractBaseResource>();
474+
}
475+
476+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
477+
public sealed class DerivedAlternateAttributeName : AbstractBaseResource
478+
{
479+
[Attr(PublicName = "derivedValue")]
480+
public override int Value { get; set; }
481+
}
482+
483+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
484+
public sealed class DerivedAlternateToOneRelationshipName : AbstractBaseResource
485+
{
486+
[HasOne(PublicName = "derivedToOne")]
487+
public override AbstractBaseResource? BaseToOne { get; set; }
488+
}
489+
490+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
491+
public sealed class DerivedAlternateToManyRelationshipName : AbstractBaseResource
492+
{
493+
[HasMany(PublicName = "derivedToMany")]
494+
public override ISet<AbstractBaseResource> BaseToMany { get; set; } = new HashSet<AbstractBaseResource>();
495+
}
408496
}

test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ public void AddResourceService_Throws_If_Type_Does_Not_Implement_Any_Interfaces(
9595
Action action = () => services.AddResourceService<int>();
9696

9797
// Assert
98-
action.Should().ThrowExactly<InvalidConfigurationException>();
98+
action.Should().ThrowExactly<InvalidConfigurationException>()
99+
.WithMessage("Type 'System.Int32' does not implement any of the expected JsonApiDotNetCore interfaces.");
99100
}
100101

101102
[Fact]

0 commit comments

Comments
 (0)