@@ -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 < 2 )
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 {
0 commit comments