@@ -62,37 +62,60 @@ public override void Write(Utf8JsonWriter writer, DatabaseObject value, JsonSeri
6262 // "TypeName": "Azure.DataApiBuilder.Config.DatabasePrimitives.DatabaseTable, Azure.DataApiBuilder.Config",
6363 writer . WriteString ( TYPE_NAME , GetTypeNameFromType ( value . GetType ( ) ) ) ;
6464
65- // Track SourceDefinition objects we've already escaped to avoid double-escaping
66- // (SourceDefinition and TableDefinition/ViewDefinition/StoredProcedureDefinition can reference the same object )
67- HashSet < SourceDefinition > escapedSourceDefs = new HashSet < SourceDefinition > ( ) ;
65+ // Track SourceDefinition objects and their original Columns dictionaries
66+ // to restore them after serialization (avoiding permanent mutation )
67+ Dictionary < SourceDefinition , Dictionary < string , ColumnDefinition > > originalColumnsDictionaries = new ( ) ;
6868
69- // Add other properties of DatabaseObject
70- foreach ( PropertyInfo prop in value . GetType ( ) . GetProperties ( ) )
69+ try
7170 {
72- // Skip the TypeName property, as it has been handled above
73- if ( prop . Name == TYPE_NAME )
71+ // Add other properties of DatabaseObject
72+ foreach ( PropertyInfo prop in value . GetType ( ) . GetProperties ( ) )
7473 {
75- continue ;
76- }
74+ // Skip the TypeName property, as it has been handled above
75+ if ( prop . Name == TYPE_NAME )
76+ {
77+ continue ;
78+ }
7779
78- writer . WritePropertyName ( prop . Name ) ;
79- object ? propVal = prop . GetValue ( value ) ;
80+ writer . WritePropertyName ( prop . Name ) ;
81+ object ? propVal = prop . GetValue ( value ) ;
8082
81- // Only escape columns for properties whose type(derived type) is SourceDefinition.
82- // Use HashSet to avoid double-escaping when multiple properties reference the same SourceDefinition object.
83- if ( IsSourceDefinitionOrDerivedClassProperty ( prop ) && propVal is SourceDefinition sourceDef )
84- {
85- if ( ! escapedSourceDefs . Contains ( sourceDef ) )
83+ // Only escape columns for properties whose type(derived type) is SourceDefinition.
84+ // Temporarily replace dictionary contents with escaped version, then restore after serialization.
85+ if ( IsSourceDefinitionOrDerivedClassProperty ( prop ) && propVal is SourceDefinition sourceDef )
8686 {
87- EscapeDollaredColumns ( sourceDef ) ;
88- escapedSourceDefs . Add ( sourceDef ) ;
87+ if ( ! originalColumnsDictionaries . ContainsKey ( sourceDef ) )
88+ {
89+ // Store original dictionary contents
90+ originalColumnsDictionaries [ sourceDef ] = new Dictionary < string , ColumnDefinition > ( sourceDef . Columns , sourceDef . Columns . Comparer ) ;
91+
92+ // Replace dictionary contents with escaped version
93+ Dictionary < string , ColumnDefinition > escapedColumns = CreateEscapedColumnsDictionary ( sourceDef . Columns ) ;
94+ sourceDef . Columns . Clear ( ) ;
95+ foreach ( var kvp in escapedColumns )
96+ {
97+ sourceDef . Columns [ kvp . Key ] = kvp . Value ;
98+ }
99+ }
89100 }
101+
102+ JsonSerializer . Serialize ( writer , propVal , options ) ;
90103 }
91104
92- JsonSerializer . Serialize ( writer , propVal , options ) ;
105+ writer . WriteEndObject ( ) ;
106+ }
107+ finally
108+ {
109+ // Always restore original Columns dictionary contents to avoid mutating the input object
110+ foreach ( var kvp in originalColumnsDictionaries )
111+ {
112+ kvp . Key . Columns . Clear ( ) ;
113+ foreach ( var colKvp in kvp . Value )
114+ {
115+ kvp . Key . Columns [ colKvp . Key ] = colKvp . Value ;
116+ }
117+ }
93118 }
94-
95- writer . WriteEndObject ( ) ;
96119 }
97120
98121 private static bool IsSourceDefinitionOrDerivedClassProperty ( PropertyInfo prop )
@@ -102,52 +125,56 @@ private static bool IsSourceDefinitionOrDerivedClassProperty(PropertyInfo prop)
102125 }
103126
104127 /// <summary>
105- /// Escapes column keys that start with '$' or 'DAB_ESCAPE$' for serialization.
128+ /// Creates a new dictionary with escaped column keys for serialization.
106129 /// Uses a double-encoding approach to handle edge cases:
107130 /// 1. First escapes columns starting with 'DAB_ESCAPE$' to 'DAB_ESCAPE$DAB_ESCAPE$...'
108131 /// 2. Then escapes columns starting with '$' to 'DAB_ESCAPE$...'
109132 /// This ensures that even if a column is named 'DAB_ESCAPE$xyz', it will be properly handled.
133+ /// Returns a new dictionary without modifying the original.
110134 /// </summary>
111- private static void EscapeDollaredColumns ( SourceDefinition sourceDef )
135+ private static Dictionary < string , ColumnDefinition > CreateEscapedColumnsDictionary ( Dictionary < string , ColumnDefinition > originalColumns )
112136 {
113- if ( sourceDef . Columns is null || sourceDef . Columns . Count == 0 )
137+ if ( originalColumns is null || originalColumns . Count == 0 )
114138 {
115- return ;
139+ return new Dictionary < string , ColumnDefinition > ( StringComparer . InvariantCultureIgnoreCase ) ;
116140 }
117141
142+ // Create a new dictionary with the same comparer as the original
143+ Dictionary < string , ColumnDefinition > escapedColumns = new ( originalColumns . Comparer ) ;
144+
118145 // Step 1: Escape columns that start with the escape sequence itself
119146 // This prevents collision when a column name already contains 'DAB_ESCAPE$'
120- List < string > keysStartingWithEscapeSequence = sourceDef . Columns . Keys
121- . Where ( k => k . StartsWith ( ESCAPED_DOLLARCHAR , StringComparison . Ordinal ) )
122- . ToList ( ) ;
123-
124- foreach ( string key in keysStartingWithEscapeSequence )
147+ foreach ( var kvp in originalColumns )
125148 {
126- ColumnDefinition col = sourceDef . Columns [ key ] ;
127- sourceDef . Columns . Remove ( key ) ;
128- string newKey = ESCAPED_DOLLARCHAR + key ;
129- sourceDef . Columns [ newKey ] = col ;
130- }
149+ string key = kvp . Key ;
150+ ColumnDefinition value = kvp . Value ;
131151
132- // Step 2: Escape columns that start with '$'
133- List < string > keysToEscape = sourceDef . Columns . Keys
134- . Where ( k => k . StartsWith ( DOLLAR_CHAR , StringComparison . Ordinal ) )
135- . ToList ( ) ;
136-
137- foreach ( string key in keysToEscape )
138- {
139- ColumnDefinition col = sourceDef . Columns [ key ] ;
140- sourceDef . Columns . Remove ( key ) ;
141- string newKey = ESCAPED_DOLLARCHAR + key [ 1 ..] ;
142- sourceDef . Columns [ newKey ] = col ;
152+ if ( key . StartsWith ( ESCAPED_DOLLARCHAR , StringComparison . Ordinal ) )
153+ {
154+ // Double-escape: DAB_ESCAPE$FirstName -> DAB_ESCAPE$DAB_ESCAPE$FirstName
155+ escapedColumns [ ESCAPED_DOLLARCHAR + key ] = value ;
156+ }
157+ else if ( key . StartsWith ( DOLLAR_CHAR , StringComparison . Ordinal ) )
158+ {
159+ // Escape dollar: $FirstName -> DAB_ESCAPE$FirstName
160+ escapedColumns [ ESCAPED_DOLLARCHAR + key . Substring ( 1 ) ] = value ;
161+ }
162+ else
163+ {
164+ // No escaping needed
165+ escapedColumns [ key ] = value ;
166+ }
143167 }
168+
169+ return escapedColumns ;
144170 }
145171
146172 /// <summary>
147173 /// Unescapes column keys for deserialization using reverse double-encoding:
148174 /// 1. First unescapes columns starting with 'DAB_ESCAPE$DAB_ESCAPE$' to 'DAB_ESCAPE$...'
149175 /// 2. Then unescapes columns starting with 'DAB_ESCAPE$' to '$...'
150176 /// This ensures proper reconstruction of original column names even in edge cases.
177+ /// Modifies the dictionary in-place during deserialization.
151178 /// </summary>
152179 private static void UnescapeDollaredColumns ( SourceDefinition sourceDef )
153180 {
@@ -156,32 +183,39 @@ private static void UnescapeDollaredColumns(SourceDefinition sourceDef)
156183 return ;
157184 }
158185
159- // Step 1: Unescape columns that were double-escaped (originally started with 'DAB_ESCAPE$')
160- string doubleEscapeSequence = ESCAPED_DOLLARCHAR + ESCAPED_DOLLARCHAR ;
161- List < string > doubleEscapedKeys = sourceDef . Columns . Keys
162- . Where ( k => k . StartsWith ( doubleEscapeSequence , StringComparison . Ordinal ) )
163- . ToList ( ) ;
186+ // Create a new dictionary with unescaped keys
187+ Dictionary < string , ColumnDefinition > unescapedColumns = new ( sourceDef . Columns . Comparer ) ;
164188
165- foreach ( string key in doubleEscapedKeys )
189+ foreach ( var kvp in sourceDef . Columns )
166190 {
167- ColumnDefinition col = sourceDef . Columns [ key ] ;
168- sourceDef . Columns . Remove ( key ) ;
169- // Remove the first 'DAB_ESCAPE$' prefix
170- string newKey = key . Substring ( ESCAPED_DOLLARCHAR . Length ) ;
171- sourceDef . Columns [ newKey ] = col ;
172- }
191+ string key = kvp . Key ;
192+ ColumnDefinition value = kvp . Value ;
173193
174- // Step 2: Unescape columns that start with 'DAB_ESCAPE$' (originally started with '$')
175- List < string > keysToUnescape = sourceDef . Columns . Keys
176- . Where ( k => k . StartsWith ( ESCAPED_DOLLARCHAR , StringComparison . Ordinal ) )
177- . ToList ( ) ;
194+ // Step 1: Unescape columns that were double-escaped (originally started with 'DAB_ESCAPE$')
195+ string doubleEscapeSequence = ESCAPED_DOLLARCHAR + ESCAPED_DOLLARCHAR ;
196+ if ( key . StartsWith ( doubleEscapeSequence , StringComparison . Ordinal ) )
197+ {
198+ // Remove the first 'DAB_ESCAPE$' prefix: DAB_ESCAPE$DAB_ESCAPE$FirstName -> DAB_ESCAPE$FirstName
199+ unescapedColumns [ key . Substring ( ESCAPED_DOLLARCHAR . Length ) ] = value ;
200+ }
201+ // Step 2: Unescape columns that start with 'DAB_ESCAPE$' (originally started with '$')
202+ else if ( key . StartsWith ( ESCAPED_DOLLARCHAR , StringComparison . Ordinal ) )
203+ {
204+ // Add back the '$' prefix: DAB_ESCAPE$FirstName -> $FirstName
205+ unescapedColumns [ DOLLAR_CHAR + key . Substring ( ESCAPED_DOLLARCHAR . Length ) ] = value ;
206+ }
207+ else
208+ {
209+ // No unescaping needed
210+ unescapedColumns [ key ] = value ;
211+ }
212+ }
178213
179- foreach ( string key in keysToUnescape )
214+ // Replace the dictionary contents with the unescaped version
215+ sourceDef . Columns . Clear ( ) ;
216+ foreach ( var kvp in unescapedColumns )
180217 {
181- ColumnDefinition col = sourceDef . Columns [ key ] ;
182- sourceDef . Columns . Remove ( key ) ;
183- string newKey = DOLLAR_CHAR + key [ ESCAPED_DOLLARCHAR . Length ..] ;
184- sourceDef . Columns [ newKey ] = col ;
218+ sourceDef . Columns [ kvp . Key ] = kvp . Value ;
185219 }
186220 }
187221
0 commit comments