11// Licensed to the .NET Foundation under one or more agreements.
22// The .NET Foundation licenses this file to you under the MIT license.
33
4- using System . Collections . Concurrent ;
54using System . IO . Pipelines ;
65using System . Text . Json . Nodes ;
76using Microsoft . AspNetCore . Http ;
@@ -16,7 +15,7 @@ namespace Microsoft.AspNetCore.OpenApi;
1615/// </summary>
1716internal sealed class OpenApiSchemaStore
1817{
19- private readonly ConcurrentDictionary < OpenApiSchemaKey , JsonObject > _schemas = new ( )
18+ private readonly Dictionary < OpenApiSchemaKey , JsonObject > _schemas = new ( )
2019 {
2120 // Pre-populate OpenAPI schemas for well-defined types in ASP.NET Core.
2221 [ new OpenApiSchemaKey ( typeof ( IFormFile ) , null ) ] = new JsonObject
@@ -50,7 +49,8 @@ internal sealed class OpenApiSchemaStore
5049 } ,
5150 } ;
5251
53- private readonly ConcurrentDictionary < OpenApiSchema , string ? > _schemasWithReference = new ( OpenApiSchemaComparer . Instance ) ;
52+ private readonly Dictionary < OpenApiSchema , string ? > _schemasWithReference = new ( OpenApiSchemaComparer . Instance ) ;
53+ private readonly Dictionary < string , int > _referenceIdCounter = new ( ) ;
5454
5555 /// <summary>
5656 /// Resolves the JSON schema for the given type and parameter description.
@@ -60,7 +60,13 @@ internal sealed class OpenApiSchemaStore
6060 /// <returns>A <see cref="JsonObject" /> representing the JSON schema associated with the key.</returns>
6161 public JsonObject GetOrAdd ( OpenApiSchemaKey key , Func < OpenApiSchemaKey , JsonObject > valueFactory )
6262 {
63- return _schemas . GetOrAdd ( key , valueFactory ) ;
63+ if ( _schemas . TryGetValue ( key , out var schema ) )
64+ {
65+ return schema ;
66+ }
67+ var targetSchema = valueFactory ( key ) ;
68+ _schemas . Add ( key , targetSchema ) ;
69+ return targetSchema ;
6470 }
6571
6672 /// <summary>
@@ -75,38 +81,91 @@ public JsonObject GetOrAdd(OpenApiSchemaKey key, Func<OpenApiSchemaKey, JsonObje
7581 /// <param name="schema">The <see cref="OpenApiSchema"/> to add to the schemas-with-references cache.</param>
7682 public void PopulateSchemaIntoReferenceCache ( OpenApiSchema schema )
7783 {
78- _schemasWithReference . AddOrUpdate ( schema , ( _ ) => null , ( schema , _ ) => GetSchemaReferenceId ( schema ) ) ;
84+ AddOrUpdateSchemaByReference ( schema ) ;
7985 if ( schema . AdditionalProperties is not null )
8086 {
81- _schemasWithReference . AddOrUpdate ( schema . AdditionalProperties , ( _ ) => null , ( schema , _ ) => GetSchemaReferenceId ( schema ) ) ;
87+ AddOrUpdateSchemaByReference ( schema . AdditionalProperties ) ;
8288 }
8389 if ( schema . Items is not null )
8490 {
85- _schemasWithReference . AddOrUpdate ( schema . Items , ( _ ) => null , ( schema , _ ) => GetSchemaReferenceId ( schema ) ) ;
91+ AddOrUpdateSchemaByReference ( schema . Items ) ;
8692 }
8793 if ( schema . AllOf is not null )
8894 {
8995 foreach ( var allOfSchema in schema . AllOf )
9096 {
91- _schemasWithReference . AddOrUpdate ( allOfSchema , ( _ ) => null , ( schema , _ ) => GetSchemaReferenceId ( schema ) ) ;
97+ AddOrUpdateSchemaByReference ( allOfSchema ) ;
9298 }
9399 }
94100 if ( schema . AnyOf is not null )
95101 {
96102 foreach ( var anyOfSchema in schema . AnyOf )
97103 {
98- _schemasWithReference . AddOrUpdate ( anyOfSchema , ( _ ) => null , ( schema , _ ) => GetSchemaReferenceId ( schema ) ) ;
104+ AddOrUpdateSchemaByReference ( anyOfSchema ) ;
99105 }
100106 }
101107 if ( schema . Properties is not null )
102108 {
103109 foreach ( var property in schema . Properties . Values )
104110 {
105- _schemasWithReference . AddOrUpdate ( property , ( _ ) => null , ( schema , _ ) => GetSchemaReferenceId ( schema ) ) ;
111+ AddOrUpdateSchemaByReference ( property ) ;
106112 }
107113 }
108114 }
109115
116+ private void AddOrUpdateSchemaByReference ( OpenApiSchema schema )
117+ {
118+ if ( _schemasWithReference . TryGetValue ( schema , out var referenceId ) )
119+ {
120+ // If we've already used this reference ID else where in the document, increment a counter value to the reference
121+ // ID to avoid name collisions. These collisions are most likely to occur when the same .NET type produces a different
122+ // schema in the OpenAPI document because of special annotations provided on it. For example, in the two type definitions
123+ // below:
124+ // public class Todo
125+ // {
126+ // public int Id { get; set; }
127+ // public string Name { get; set; }
128+ // }
129+ // public class Project
130+ // {
131+ // public int Id { get; set; }
132+ // [MinLength(5)]
133+ // public string Title { get; set; }
134+ // }
135+ // The `Title` and `Name` properties are both strings but the `Title` property has a `minLength` annotation
136+ // on it that will materialize into a different schema.
137+ // {
138+ //
139+ // "type": "string",
140+ // "minLength": 5
141+ // }
142+ // {
143+ // "type": "string"
144+ // }
145+ // In this case, although the reference ID based on the .NET type we would use is `string`, the
146+ // two schemas are distinct.
147+ if ( referenceId == null )
148+ {
149+ var targetReferenceId = GetSchemaReferenceId ( schema ) ;
150+ if ( _referenceIdCounter . TryGetValue ( targetReferenceId , out var counter ) )
151+ {
152+ counter ++ ;
153+ _referenceIdCounter [ targetReferenceId ] = counter ;
154+ _schemasWithReference [ schema ] = $ "{ targetReferenceId } { counter } ";
155+ }
156+ else
157+ {
158+ _referenceIdCounter [ targetReferenceId ] = 1 ;
159+ _schemasWithReference [ schema ] = targetReferenceId ;
160+ }
161+ }
162+ }
163+ else
164+ {
165+ _schemasWithReference [ schema ] = null ;
166+ }
167+ }
168+
110169 private static string GetSchemaReferenceId ( OpenApiSchema schema )
111170 {
112171 if ( schema . Extensions . TryGetValue ( OpenApiConstants . SchemaId , out var referenceIdAny )
@@ -118,5 +177,5 @@ private static string GetSchemaReferenceId(OpenApiSchema schema)
118177 throw new InvalidOperationException ( "The schema reference ID must be set on the schema." ) ;
119178 }
120179
121- public ConcurrentDictionary < OpenApiSchema , string ? > SchemasByReference => _schemasWithReference ;
180+ public Dictionary < OpenApiSchema , string ? > SchemasByReference => _schemasWithReference ;
122181}
0 commit comments