Skip to content

Commit dd71cb5

Browse files
Fix the Dollar prefixed columns during serialization
1 parent d6d0f83 commit dd71cb5

2 files changed

Lines changed: 172 additions & 12 deletions

File tree

src/Core/Services/MetadataProviders/Converters/DatabaseObjectConverter.cs

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ namespace Azure.DataApiBuilder.Core.Services.MetadataProviders.Converters
1717
public class DatabaseObjectConverter : JsonConverter<DatabaseObject>
1818
{
1919
private const string TYPE_NAME = "TypeName";
20+
private const string DOLLAR_CHAR = "$";
21+
private const string ESCAPED_DOLLARCHAR = "_$";
2022

2123
public override DatabaseObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
2224
{
@@ -29,6 +31,18 @@ public override DatabaseObject Read(ref Utf8JsonReader reader, Type typeToConver
2931

3032
DatabaseObject objA = (DatabaseObject)JsonSerializer.Deserialize(document, concreteType, options)!;
3133

34+
foreach (PropertyInfo prop in objA.GetType().GetProperties())
35+
{
36+
if (IsSourceDefinitionProperty(prop))
37+
{
38+
SourceDefinition? sourceDef = (SourceDefinition?)prop.GetValue(objA);
39+
if (sourceDef is not null)
40+
{
41+
UnescapeDollaredColumns(sourceDef);
42+
}
43+
}
44+
}
45+
3246
return objA;
3347
}
3448
}
@@ -42,28 +56,85 @@ public override void Write(Utf8JsonWriter writer, DatabaseObject value, JsonSeri
4256

4357
writer.WriteStartObject();
4458

45-
// Add TypeName property in DatabaseObject object that we are serializing based on its type. (DatabaseTable, DatabaseView)
46-
// We add this property to differentiate between them in the dictionary. This extra property gets used in deserialization above.
47-
// for example if object is DatabaseTable then we need to add
48-
// "TypeName": "Azure.DataApiBuilder.Config.DatabasePrimitives.DatabaseTable, Azure.DataApiBuilder.Config",
59+
// Add TypeName property in DatabaseObject object that we are serializing based on its type.
4960
writer.WriteString(TYPE_NAME, GetTypeNameFromType(value.GetType()));
5061

5162
// Add other properties of DatabaseObject
5263
foreach (PropertyInfo prop in value.GetType().GetProperties())
5364
{
54-
// Skip the TypeName property, as it has been handled above
5565
if (prop.Name == TYPE_NAME)
5666
{
5767
continue;
5868
}
5969

6070
writer.WritePropertyName(prop.Name);
61-
JsonSerializer.Serialize(writer, prop.GetValue(value), options);
71+
object? propVal = prop.GetValue(value);
72+
Type propType = prop.PropertyType;
73+
74+
// enforcing that we only escape columns for properties whose type is exactly SourceDefinition(DatabaseTables)
75+
if (IsSourceDefinitionProperty(prop) && propVal is SourceDefinition sourceDef && propVal.GetType() == typeof(SourceDefinition))
76+
{
77+
EscapeDollaredColumns(sourceDef);
78+
}
79+
80+
JsonSerializer.Serialize(writer, propVal, propType, options);
6281
}
6382

6483
writer.WriteEndObject();
6584
}
6685

86+
private static bool IsSourceDefinitionProperty(PropertyInfo prop)
87+
{
88+
// Only return true for properties whose type is exactly SourceDefinition (not subclasses)
89+
return prop.PropertyType == typeof(SourceDefinition);
90+
}
91+
92+
/// <summary>
93+
/// Escapes column keys that start with '$' to '_$' for serialization.
94+
/// </summary>
95+
private static void EscapeDollaredColumns(SourceDefinition sourceDef)
96+
{
97+
if (sourceDef.Columns is null || sourceDef.Columns.Count == 0)
98+
{
99+
return;
100+
}
101+
102+
List<string> keysToEscape = sourceDef.Columns.Keys
103+
.Where(k => k.StartsWith(DOLLAR_CHAR, StringComparison.Ordinal))
104+
.ToList();
105+
106+
foreach (string key in keysToEscape)
107+
{
108+
ColumnDefinition col = sourceDef.Columns[key];
109+
sourceDef.Columns.Remove(key);
110+
string newKey = ESCAPED_DOLLARCHAR + key[1..];
111+
sourceDef.Columns[newKey] = col;
112+
}
113+
}
114+
115+
/// <summary>
116+
/// Unescapes column keys that start with '_$' to '$' for deserialization.
117+
/// </summary>
118+
private static void UnescapeDollaredColumns(SourceDefinition sourceDef)
119+
{
120+
if (sourceDef.Columns is null || sourceDef.Columns.Count == 0)
121+
{
122+
return;
123+
}
124+
125+
List<string> keysToUnescape = sourceDef.Columns.Keys
126+
.Where(k => k.StartsWith(ESCAPED_DOLLARCHAR, StringComparison.Ordinal))
127+
.ToList();
128+
129+
foreach (string key in keysToUnescape)
130+
{
131+
ColumnDefinition col = sourceDef.Columns[key];
132+
sourceDef.Columns.Remove(key);
133+
string newKey = DOLLAR_CHAR + key[2..];
134+
sourceDef.Columns[newKey] = col;
135+
}
136+
}
137+
67138
private static Type GetTypeFromName(string typeName)
68139
{
69140
Type? type = Type.GetType(typeName);

src/Service.Tests/UnitTests/SerializationDeserializationTests.cs

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,96 @@ public void TestDictionaryDatabaseObjectSerializationDeserialization()
276276
VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseTable.TableDefinition, _databaseTable.TableDefinition, "FirstName");
277277
}
278278

279-
private void InitializeObjects()
279+
/// <summary>
280+
/// Validates serialization and deserilization of Dictionary containing DatabaseTable
281+
/// The table will have dollar sign prefix ($) in the column name
282+
/// this is how we serialize and deserialize metadataprovider.EntityToDatabaseObject dict.
283+
/// </summary>
284+
[TestMethod]
285+
public void TestDictionaryDatabaseObjectSerializationDeserialization_WithDollarColumn()
280286
{
287+
InitializeObjects(true);
288+
289+
_options = new()
290+
{
291+
Converters = {
292+
new DatabaseObjectConverter(),
293+
new TypeConverter()
294+
},
295+
ReferenceHandler = ReferenceHandler.Preserve,
296+
};
297+
298+
Dictionary<string, DatabaseObject> dict = new() { { "person", _databaseTable } };
299+
300+
string serializedDict = JsonSerializer.Serialize(dict, _options);
301+
Dictionary<string, DatabaseObject> deserializedDict = JsonSerializer.Deserialize<Dictionary<string, DatabaseObject>>(serializedDict, _options)!;
302+
303+
DatabaseTable deserializedDatabaseTable = (DatabaseTable)deserializedDict["person"];
304+
305+
Assert.AreEqual(deserializedDatabaseTable.SourceType, _databaseTable.SourceType);
306+
Assert.AreEqual(deserializedDatabaseTable.FullName, _databaseTable.FullName);
307+
deserializedDatabaseTable.Equals(_databaseTable);
308+
VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseTable.SourceDefinition, _databaseTable.SourceDefinition, "$FirstName");
309+
VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseTable.TableDefinition, _databaseTable.TableDefinition, "$FirstName");
310+
}
311+
312+
/// <summary>
313+
/// Validates serialization and deserilization of Dictionary containing DatabaseView
314+
/// The table will have dollar sign prefix ($) in the column name
315+
/// this is how we serialize and deserialize metadataprovider.EntityToDatabaseObject dict.
316+
/// </summary>
317+
[TestMethod]
318+
public void TestDatabaseViewSerializationDeserialization_WithDollarColumn()
319+
{
320+
InitializeObjects(true);
321+
322+
TestTypeNameChanges(_databaseView, "DatabaseView");
323+
324+
// Test to catch if there is change in number of properties/fields
325+
// Note: On Addition of property make sure it is added in following object creation _databaseView and include in serialization
326+
// and deserialization test.
327+
int fields = typeof(DatabaseView).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Length;
328+
Assert.AreEqual(fields, 6);
329+
330+
string serializedDatabaseView = JsonSerializer.Serialize(_databaseView, _options);
331+
DatabaseView deserializedDatabaseView = JsonSerializer.Deserialize<DatabaseView>(serializedDatabaseView, _options)!;
332+
333+
Assert.AreEqual(deserializedDatabaseView.SourceType, _databaseView.SourceType);
334+
deserializedDatabaseView.Equals(_databaseView);
335+
VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseView.SourceDefinition, _databaseView.SourceDefinition, "$FirstName");
336+
VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseView.ViewDefinition, _databaseView.ViewDefinition, "$FirstName");
337+
}
338+
339+
/// <summary>
340+
/// Validates serialization and deserilization of Dictionary containing DatabaseStoredProcedure
341+
/// The table will have dollar sign prefix ($) in the column name
342+
/// this is how we serialize and deserialize metadataprovider.EntityToDatabaseObject dict.
343+
/// </summary>
344+
[TestMethod]
345+
public void TestDatabaseStoredProcedureSerializationDeserialization_WithDollarColumn()
346+
{
347+
InitializeObjects(true);
348+
349+
TestTypeNameChanges(_databaseStoredProcedure, "DatabaseStoredProcedure");
350+
351+
// Test to catch if there is change in number of properties/fields
352+
// Note: On Addition of property make sure it is added in following object creation _databaseStoredProcedure and include in serialization
353+
// and deserialization test.
354+
int fields = typeof(DatabaseStoredProcedure).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Length;
355+
Assert.AreEqual(fields, 6);
356+
357+
string serializedDatabaseSP = JsonSerializer.Serialize(_databaseStoredProcedure, _options);
358+
DatabaseStoredProcedure deserializedDatabaseSP = JsonSerializer.Deserialize<DatabaseStoredProcedure>(serializedDatabaseSP, _options)!;
359+
360+
Assert.AreEqual(deserializedDatabaseSP.SourceType, _databaseStoredProcedure.SourceType);
361+
deserializedDatabaseSP.Equals(_databaseStoredProcedure);
362+
VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseSP.SourceDefinition, _databaseStoredProcedure.SourceDefinition, "$FirstName", true);
363+
VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseSP.StoredProcedureDefinition, _databaseStoredProcedure.StoredProcedureDefinition, "$FirstName", true);
364+
}
365+
366+
private void InitializeObjects(bool generateDollaredColumn = false)
367+
{
368+
string columnName = generateDollaredColumn ? "$FirstName" : "FirstName";
281369
_options = new()
282370
{
283371
// ObjectConverter behavior different in .NET8 most likely due to
@@ -289,10 +377,11 @@ private void InitializeObjects()
289377
new DatabaseObjectConverter(),
290378
new TypeConverter()
291379
}
380+
292381
};
293382

294383
_columnDefinition = GetColumnDefinition(typeof(string), DbType.String, true, false, false, new string("John"), false);
295-
_sourceDefinition = GetSourceDefinition(false, false, new List<string>() { "FirstName" }, _columnDefinition);
384+
_sourceDefinition = GetSourceDefinition(false, false, new List<string>() { columnName }, _columnDefinition);
296385

297386
_databaseTable = new DatabaseTable()
298387
{
@@ -311,10 +400,10 @@ private void InitializeObjects()
311400
{
312401
IsInsertDMLTriggerEnabled = false,
313402
IsUpdateDMLTriggerEnabled = false,
314-
PrimaryKey = new List<string>() { "FirstName" },
403+
PrimaryKey = new List<string>() { columnName },
315404
},
316405
};
317-
_databaseView.ViewDefinition.Columns.Add("FirstName", _columnDefinition);
406+
_databaseView.ViewDefinition.Columns.Add(columnName, _columnDefinition);
318407

319408
_parameterDefinition = new()
320409
{
@@ -331,10 +420,10 @@ private void InitializeObjects()
331420
SourceType = EntitySourceType.StoredProcedure,
332421
StoredProcedureDefinition = new()
333422
{
334-
PrimaryKey = new List<string>() { "FirstName" },
423+
PrimaryKey = new List<string>() { columnName },
335424
}
336425
};
337-
_databaseStoredProcedure.StoredProcedureDefinition.Columns.Add("FirstName", _columnDefinition);
426+
_databaseStoredProcedure.StoredProcedureDefinition.Columns.Add(columnName, _columnDefinition);
338427
_databaseStoredProcedure.StoredProcedureDefinition.Parameters.Add("Id", _parameterDefinition);
339428
}
340429

0 commit comments

Comments
 (0)