Skip to content

Commit e53ccec

Browse files
author
Bart Koelman
committed
Added extra validations of the resource graph and unified unit tests
1 parent b88d39e commit e53ccec

File tree

6 files changed

+447
-244
lines changed

6 files changed

+447
-244
lines changed

src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public class ResourceGraphBuilder
2121
{
2222
private readonly IJsonApiOptions _options;
2323
private readonly ILogger<ResourceGraphBuilder> _logger;
24-
private readonly HashSet<ResourceType> _resourceTypes = new();
24+
private readonly Dictionary<Type, ResourceType> _resourceTypesByClrType = new();
2525
private readonly TypeLocator _typeLocator = new();
2626

2727
public ResourceGraphBuilder(IJsonApiOptions options, ILoggerFactory loggerFactory)
@@ -38,12 +38,27 @@ public ResourceGraphBuilder(IJsonApiOptions options, ILoggerFactory loggerFactor
3838
/// </summary>
3939
public IResourceGraph Build()
4040
{
41-
var resourceGraph = new ResourceGraph(_resourceTypes);
41+
HashSet<ResourceType> resourceTypes = _resourceTypesByClrType.Values.ToHashSet();
4242

43-
foreach (RelationshipAttribute relationship in _resourceTypes.SelectMany(resourceType => resourceType.Relationships))
43+
if (!resourceTypes.Any())
44+
{
45+
_logger.LogWarning("The resource graph is empty.");
46+
}
47+
48+
var resourceGraph = new ResourceGraph(resourceTypes);
49+
50+
foreach (RelationshipAttribute relationship in resourceTypes.SelectMany(resourceType => resourceType.Relationships))
4451
{
4552
relationship.LeftType = resourceGraph.GetResourceType(relationship.LeftClrType!);
46-
relationship.RightType = resourceGraph.GetResourceType(relationship.RightClrType!);
53+
ResourceType? rightType = resourceGraph.FindResourceType(relationship.RightClrType!);
54+
55+
if (rightType == null)
56+
{
57+
throw new InvalidConfigurationException($"Resource type '{relationship.LeftClrType}' depends on " +
58+
$"'{relationship.RightClrType}', which was not added to the resource graph.");
59+
}
60+
61+
relationship.RightType = rightType;
4762
}
4863

4964
return resourceGraph;
@@ -123,7 +138,7 @@ public ResourceGraphBuilder Add(Type resourceClrType, Type? idClrType = null, st
123138
{
124139
ArgumentGuard.NotNull(resourceClrType, nameof(resourceClrType));
125140

126-
if (_resourceTypes.Any(resourceType => resourceType.ClrType == resourceClrType))
141+
if (_resourceTypesByClrType.ContainsKey(resourceClrType))
127142
{
128143
return this;
129144
}
@@ -139,7 +154,10 @@ public ResourceGraphBuilder Add(Type resourceClrType, Type? idClrType = null, st
139154
}
140155

141156
ResourceType resourceType = CreateResourceType(effectivePublicName, resourceClrType, effectiveIdType);
142-
_resourceTypes.Add(resourceType);
157+
158+
AssertNoDuplicatePublicName(resourceType, effectivePublicName);
159+
160+
_resourceTypesByClrType.Add(resourceClrType, resourceType);
143161
}
144162
else
145163
{
@@ -155,6 +173,8 @@ private ResourceType CreateResourceType(string publicName, Type resourceClrType,
155173
IReadOnlyCollection<RelationshipAttribute> relationships = GetRelationships(resourceClrType);
156174
IReadOnlyCollection<EagerLoadAttribute> eagerLoads = GetEagerLoads(resourceClrType);
157175

176+
AssertNoDuplicatePublicName(attributes, relationships);
177+
158178
var linksAttribute = (ResourceLinksAttribute?)resourceClrType.GetCustomAttribute(typeof(ResourceLinksAttribute));
159179

160180
return linksAttribute == null
@@ -165,7 +185,7 @@ private ResourceType CreateResourceType(string publicName, Type resourceClrType,
165185

166186
private IReadOnlyCollection<AttrAttribute> GetAttributes(Type resourceClrType)
167187
{
168-
var attributes = new List<AttrAttribute>();
188+
var attributesByName = new Dictionary<string, AttrAttribute>();
169189

170190
foreach (PropertyInfo property in resourceClrType.GetProperties())
171191
{
@@ -181,7 +201,7 @@ private IReadOnlyCollection<AttrAttribute> GetAttributes(Type resourceClrType)
181201
Capabilities = _options.DefaultAttrCapabilities
182202
};
183203

184-
attributes.Add(idAttr);
204+
IncludeField(attributesByName, idAttr);
185205
continue;
186206
}
187207

@@ -200,15 +220,20 @@ private IReadOnlyCollection<AttrAttribute> GetAttributes(Type resourceClrType)
200220
attribute.Capabilities = _options.DefaultAttrCapabilities;
201221
}
202222

203-
attributes.Add(attribute);
223+
IncludeField(attributesByName, attribute);
204224
}
205225

206-
return attributes;
226+
if (attributesByName.Count == 1)
227+
{
228+
_logger.LogWarning($"Type '{resourceClrType}' does not contain any attributes.");
229+
}
230+
231+
return attributesByName.Values;
207232
}
208233

209234
private IReadOnlyCollection<RelationshipAttribute> GetRelationships(Type resourceClrType)
210235
{
211-
var relationships = new List<RelationshipAttribute>();
236+
var relationshipsByName = new Dictionary<string, RelationshipAttribute>();
212237
PropertyInfo[] properties = resourceClrType.GetProperties();
213238

214239
foreach (PropertyInfo property in properties)
@@ -222,11 +247,11 @@ private IReadOnlyCollection<RelationshipAttribute> GetRelationships(Type resourc
222247
relationship.LeftClrType = resourceClrType;
223248
relationship.RightClrType = GetRelationshipType(relationship, property);
224249

225-
relationships.Add(relationship);
250+
IncludeField(relationshipsByName, relationship);
226251
}
227252
}
228253

229-
return relationships;
254+
return relationshipsByName.Values;
230255
}
231256

232257
private void SetPublicName(ResourceFieldAttribute field, PropertyInfo property)
@@ -269,6 +294,51 @@ private IReadOnlyCollection<EagerLoadAttribute> GetEagerLoads(Type resourceClrTy
269294
return attributes;
270295
}
271296

297+
private static void IncludeField<TField>(Dictionary<string, TField> fieldsByName, TField field)
298+
where TField : ResourceFieldAttribute
299+
{
300+
if (fieldsByName.TryGetValue(field.PublicName, out var existingField))
301+
{
302+
throw CreateExceptionForDuplicatePublicName(field.Property.DeclaringType!, existingField, field);
303+
}
304+
305+
fieldsByName.Add(field.PublicName, field);
306+
}
307+
308+
private void AssertNoDuplicatePublicName(ResourceType resourceType, string effectivePublicName)
309+
{
310+
var (existingClrType, _) = _resourceTypesByClrType.FirstOrDefault(type => type.Value.PublicName == resourceType.PublicName);
311+
312+
if (existingClrType != null)
313+
{
314+
throw new InvalidConfigurationException(
315+
$"Resource '{existingClrType}' and '{resourceType.ClrType}' both use public name '{effectivePublicName}'.");
316+
}
317+
}
318+
319+
private void AssertNoDuplicatePublicName(IReadOnlyCollection<AttrAttribute> attributes, IReadOnlyCollection<RelationshipAttribute> relationships)
320+
{
321+
IEnumerable<(AttrAttribute attribute, RelationshipAttribute relationship)> query =
322+
from attribute in attributes
323+
from relationship in relationships
324+
where attribute.PublicName == relationship.PublicName
325+
select (attribute, relationship);
326+
327+
(AttrAttribute? duplicateAttribute, RelationshipAttribute? duplicateRelationship) = query.FirstOrDefault();
328+
329+
if (duplicateAttribute != null && duplicateRelationship != null)
330+
{
331+
throw CreateExceptionForDuplicatePublicName(duplicateAttribute.Property.DeclaringType!, duplicateAttribute, duplicateRelationship);
332+
}
333+
}
334+
335+
private static InvalidConfigurationException CreateExceptionForDuplicatePublicName(Type containingClrType, ResourceFieldAttribute existingField,
336+
ResourceFieldAttribute field)
337+
{
338+
return new InvalidConfigurationException(
339+
$"Properties '{containingClrType}.{existingField.Property.Name}' and '{containingClrType}.{field.Property.Name}' both use public name '{field.PublicName}'.");
340+
}
341+
272342
[AssertionMethod]
273343
private static void AssertNoInfiniteRecursion(int recursionDepth)
274344
{

test/JsonApiDotNetCoreTests/JsonApiDotNetCoreTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<PackageReference Include="Macross.Json.Extensions" Version="2.0.0" />
1919
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(AspNetCoreVersion)" />
2020
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="$(EFCoreVersion)" />
21+
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="$(EFCoreVersion)" />
2122
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
2223
</ItemGroup>
2324
</Project>

0 commit comments

Comments
 (0)