Skip to content

Commit d401aa0

Browse files
authored
Improvements to nested serialization (#65)
Define nested serialization rules for objects; update patch notes for 3.1.3 release.
1 parent 892fb46 commit d401aa0

5 files changed

Lines changed: 155 additions & 59 deletions

File tree

CSVFile.nuspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
<description>Tiny and fast CSV and TSV parsing library (40KB) with zero dependencies. Compatible with DotNetFramework (2.0 onwards) and DotNetCore.</description>
1414
<icon>docs/icons8-spreadsheet-96.png</icon>
1515
<releaseNotes>
16-
July 18, 2023
16+
August 5, 2024
1717

18-
* Add serialization options for arrays
18+
* Add serialization options for arrays and objects
1919
</releaseNotes>
2020
<readme>docs/README.md</readme>
2121
<copyright>Copyright 2006 - 2024</copyright>

csharp-csv-reader.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{E92F982D
1515
icons8-spreadsheet-96.png = icons8-spreadsheet-96.png
1616
LICENSE = LICENSE
1717
README.md = README.md
18+
.github\workflows\nuget-publish.yml = .github\workflows\nuget-publish.yml
1819
EndProjectSection
1920
EndProject
2021
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "src.net50", "src\net50\src.net50.csproj", "{C78A66F7-113D-452A-989B-306CD6534E7B}"

src/CSV.cs

Lines changed: 76 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -358,68 +358,94 @@ internal static string ItemsToCsv(IEnumerable items, CSVSettings settings, char[
358358

359359
// Special cases for other types of serialization
360360
string s;
361-
var itemType = item.GetType();
362-
var interfaces = itemType.GetInterfaces();
363-
bool isEnumerable = false;
364-
if (itemType != typeof(string))
361+
if (item is string)
365362
{
366-
foreach (var itemInterface in interfaces)
367-
{
368-
if (itemInterface == typeof(IEnumerable))
369-
{
370-
isEnumerable = true;
371-
}
372-
}
373-
}
374-
375-
if (item is DateTime)
363+
s = item as string;
364+
}
365+
else if (item is DateTime)
376366
{
377367
s = ((DateTime)item).ToString(settings.DateTimeFormat);
378368
}
379-
else if (isEnumerable)
369+
else
380370
{
381-
IEnumerable enumerable = item as IEnumerable;
382-
s = string.Empty;
383-
switch (settings.NestedArrayBehavior)
371+
var itemType = item.GetType();
372+
var interfaces = itemType.GetInterfaces();
373+
bool isEnumerable = false;
374+
if (itemType != typeof(string))
384375
{
385-
case ArrayOptions.ToString:
386-
s = item.ToString();
387-
break;
388-
case ArrayOptions.CountItems:
389-
// from https://stackoverflow.com/questions/3546051/how-to-invoke-system-linq-enumerable-count-on-ienumerablet-using-reflection
390-
if (enumerable != null)
376+
foreach (var itemInterface in interfaces)
377+
{
378+
if (itemInterface == typeof(IEnumerable))
391379
{
392-
int enumerableCount = 0;
393-
var iter = enumerable.GetEnumerator();
394-
using (iter as IDisposable)
380+
isEnumerable = true;
381+
}
382+
}
383+
}
384+
385+
// Treat enumerables as a simple class of objects that can be unrolled
386+
if (isEnumerable)
387+
{
388+
IEnumerable enumerable = item as IEnumerable;
389+
s = string.Empty;
390+
switch (settings.NestedArrayBehavior)
391+
{
392+
case ArrayOptions.ToString:
393+
s = item.ToString();
394+
break;
395+
case ArrayOptions.CountItems:
396+
if (enumerable != null)
395397
{
396-
while (iter.MoveNext())
398+
int enumerableCount = 0;
399+
var iter = enumerable.GetEnumerator();
400+
using (iter as IDisposable)
397401
{
398-
enumerableCount++;
402+
while (iter.MoveNext())
403+
{
404+
enumerableCount++;
405+
}
399406
}
407+
s = enumerableCount.ToString();
400408
}
401-
402-
s = enumerableCount.ToString();
403-
}
404-
405-
break;
406-
case ArrayOptions.TreatAsNull:
407-
if (settings.AllowNull)
408-
{
409-
s = settings.NullToken;
410-
}
411-
break;
412-
case ArrayOptions.RecursiveSerialization:
413-
if (enumerable != null)
414-
{
415-
s = ItemsToCsv(enumerable, settings, riskyChars, forceQualifierTypes);
416-
}
417-
break;
409+
break;
410+
case ArrayOptions.TreatAsNull:
411+
if (settings.AllowNull)
412+
{
413+
s = settings.NullToken;
414+
}
415+
else
416+
{
417+
s = string.Empty;
418+
}
419+
break;
420+
case ArrayOptions.RecursiveSerialization:
421+
if (enumerable != null)
422+
{
423+
s = ItemsToCsv(enumerable, settings, riskyChars, forceQualifierTypes);
424+
}
425+
else
426+
{
427+
s = string.Empty;
428+
}
429+
break;
430+
}
431+
}
432+
else if (itemType.IsClass && settings.NestedObjectBehavior == ObjectOptions.RecursiveSerialization)
433+
{
434+
var nestedItems = new List<object>();
435+
foreach (var field in itemType.GetFields())
436+
{
437+
nestedItems.Add(field.GetValue(item));
438+
}
439+
foreach (var prop in itemType.GetProperties())
440+
{
441+
nestedItems.Add(prop.GetValue(item, null));
442+
}
443+
s = ItemsToCsv(nestedItems, settings, riskyChars, forceQualifierTypes);
444+
}
445+
else
446+
{
447+
s = item.ToString();
418448
}
419-
}
420-
else
421-
{
422-
s = item.ToString();
423449
}
424450

425451
// Check if this item requires qualifiers

src/CSVSettings.cs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace CSVFile
1515
public enum ArrayOptions
1616
{
1717
/// <summary>
18-
/// Use built-in string conversion, which renders arrays as `MyObject[]`
18+
/// Use built-in string conversion, which renders arrays as `MyNamespace.MyObject[]`
1919
/// </summary>
2020
ToString,
2121

@@ -29,6 +29,22 @@ public enum ArrayOptions
2929
/// </summary>
3030
CountItems,
3131

32+
/// <summary>
33+
/// Serialize child arrays recursively using the same settings
34+
/// </summary>
35+
RecursiveSerialization,
36+
}
37+
38+
/// <summary>
39+
/// Defines the behavior of CSV Serialization when a nested object (class) is encountered
40+
/// </summary>
41+
public enum ObjectOptions
42+
{
43+
/// <summary>
44+
/// Use built-in string conversion, which renders as `MyNamespace.MyObject`
45+
/// </summary>
46+
ToString,
47+
3248
/// <summary>
3349
/// Serialize child objects recursively using the same settings
3450
/// </summary>
@@ -160,9 +176,14 @@ public class CSVSettings
160176
public string DateTimeFormat { get; set; } = "o";
161177

162178
/// <summary>
163-
/// The behavior to use when serializing a column of an array type
179+
/// The behavior to use when serializing a column that is an array or enumerable type
180+
/// </summary>
181+
public ArrayOptions NestedArrayBehavior { get; set; } = ArrayOptions.ToString;
182+
183+
/// <summary>
184+
/// The behavior to use when serializing a column that is a class
164185
/// </summary>
165-
public ArrayOptions NestedArrayBehavior = ArrayOptions.TreatAsNull;
186+
public ObjectOptions NestedObjectBehavior { get; set; } = ObjectOptions.ToString;
166187

167188
/// <summary>
168189
/// Standard comma-separated value (CSV) file settings

tests/SerializationTest.cs

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ public class TestClassThree
5454
public List<Guid> NullableList { get; set; }
5555
}
5656

57+
public class TestClassFour
58+
{
59+
public string Name { get; set; }
60+
public TestClassTwo Details { get; set; }
61+
}
62+
5763
[Test]
5864
public void TestObjectSerialization()
5965
{
@@ -136,10 +142,7 @@ public void TestNullSerialization()
136142
}
137143

138144
/// <summary>
139-
/// Arrays and child objects aren't well suited for complex serialization within a CSV file.
140-
/// However, we have options:
141-
/// * ToString just converts it to "MyClass[]"
142-
/// * CountItems just produces the number of elements in the array
145+
/// Tests that validate whether we can serialize arrays within arrays
143146
/// </summary>
144147
[Test]
145148
public void TestArraySerialization()
@@ -186,6 +189,51 @@ public void TestArraySerialization()
186189
+ $"Test,\"a,b,c\",\"1,2,3\",\"True,False,True,False\",,NULL{Environment.NewLine}", recursiveCsv);
187190
}
188191

192+
/// <summary>
193+
/// Tests that validate whether we can serialize objects within arrays
194+
/// </summary>
195+
[Test]
196+
public void TestNestedObjectSerialization()
197+
{
198+
var list = new List<TestClassFour>();
199+
list.Add(new TestClassFour()
200+
{
201+
Name = "Non-Null Test",
202+
Details = new TestClassTwo()
203+
{
204+
FirstColumn = "Hello World!",
205+
SecondColumn = 42,
206+
ThirdColumn = EnumTestType.Third,
207+
}
208+
});
209+
list.Add(new TestClassFour()
210+
{
211+
Name = "Null Test",
212+
Details = null
213+
});
214+
215+
// Serialize to a CSV string using ToString
216+
// This was the default behavior in CSVFile 3.1.2 and earlier - it's pretty ugly!
217+
var options = new CSVSettings()
218+
{
219+
HeaderRowIncluded = true,
220+
NestedObjectBehavior = ObjectOptions.ToString,
221+
NullToken = "NULL",
222+
AllowNull = true,
223+
};
224+
var toStringCsv = CSV.Serialize(list, options);
225+
Assert.AreEqual($"Name,Details{Environment.NewLine}"
226+
+ $"Non-Null Test,CSVTestSuite.SerializationTest+TestClassTwo{Environment.NewLine}"
227+
+ $"Null Test,NULL{Environment.NewLine}", toStringCsv);
228+
229+
// Serialize to a CSV string using counts
230+
options.NestedObjectBehavior = ObjectOptions.RecursiveSerialization;
231+
var recursiveCsv = CSV.Serialize(list, options);
232+
Assert.AreEqual($"Name,Details{Environment.NewLine}"
233+
+ $"Non-Null Test,\"Hello World!,42,Third\"{Environment.NewLine}"
234+
+ $"Null Test,NULL{Environment.NewLine}", recursiveCsv);
235+
}
236+
189237
[Test]
190238
public void TestCaseInsensitiveDeserializer()
191239
{

0 commit comments

Comments
 (0)