Skip to content

Commit 892fb46

Browse files
authored
Add support for nested serialization of arrays (#64)
Define nested serialization options for arrays. This allows complex object serialization to produce something better than "System.String[]" all over the place.
1 parent ff2b191 commit 892fb46

4 files changed

Lines changed: 158 additions & 7 deletions

File tree

CSVFile.nuspec

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<package >
33
<metadata>
44
<id>CSVFile</id>
5-
<version>3.1.2</version>
5+
<version>3.1.3</version>
66
<title>CSVFile</title>
77
<authors>Ted Spence</authors>
88
<owners>Ted Spence</owners>
@@ -15,10 +15,10 @@
1515
<releaseNotes>
1616
July 18, 2023
1717

18-
* Fix issue with inconsistent handling of embedded newlines in the streaming version of the reader
18+
* Add serialization options for arrays
1919
</releaseNotes>
2020
<readme>docs/README.md</readme>
21-
<copyright>Copyright 2006 - 2023</copyright>
21+
<copyright>Copyright 2006 - 2024</copyright>
2222
<tags>fast csv parser serialization deserialization streaming async</tags>
2323
<repository type="git" url="https://github.com/tspence/csharp-csv-reader" />
2424
<dependencies>

src/CSV.cs

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Home page: https://github.com/tspence/csharp-csv-reader
55
*/
66
using System;
7+
using System.Collections;
78
using System.Collections.Generic;
89
using System.IO;
910
using System.Text;
@@ -339,7 +340,7 @@ internal static string RemoveByteOrderMarker(string rawString)
339340
/// <param name="riskyChars"></param>
340341
/// <param name="forceQualifierTypes"></param>
341342
/// <returns></returns>
342-
internal static string ItemsToCsv(IEnumerable<object> items, CSVSettings settings, char[] riskyChars, Dictionary<Type, int> forceQualifierTypes)
343+
internal static string ItemsToCsv(IEnumerable items, CSVSettings settings, char[] riskyChars, Dictionary<Type, int> forceQualifierTypes)
343344
{
344345
var sb = new StringBuilder();
345346
foreach (var item in items)
@@ -355,12 +356,67 @@ internal static string ItemsToCsv(IEnumerable<object> items, CSVSettings setting
355356
continue;
356357
}
357358

358-
// Is this a date time?
359+
// Special cases for other types of serialization
359360
string s;
361+
var itemType = item.GetType();
362+
var interfaces = itemType.GetInterfaces();
363+
bool isEnumerable = false;
364+
if (itemType != typeof(string))
365+
{
366+
foreach (var itemInterface in interfaces)
367+
{
368+
if (itemInterface == typeof(IEnumerable))
369+
{
370+
isEnumerable = true;
371+
}
372+
}
373+
}
374+
360375
if (item is DateTime)
361376
{
362377
s = ((DateTime)item).ToString(settings.DateTimeFormat);
363378
}
379+
else if (isEnumerable)
380+
{
381+
IEnumerable enumerable = item as IEnumerable;
382+
s = string.Empty;
383+
switch (settings.NestedArrayBehavior)
384+
{
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)
391+
{
392+
int enumerableCount = 0;
393+
var iter = enumerable.GetEnumerator();
394+
using (iter as IDisposable)
395+
{
396+
while (iter.MoveNext())
397+
{
398+
enumerableCount++;
399+
}
400+
}
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;
418+
}
419+
}
364420
else
365421
{
366422
s = item.ToString();
@@ -399,7 +455,10 @@ internal static string ItemsToCsv(IEnumerable<object> items, CSVSettings setting
399455
}
400456

401457
// Subtract the trailing delimiter so we don't inadvertently add an empty column at the end
402-
sb.Length -= 1;
458+
if (sb.Length > 0)
459+
{
460+
sb.Length -= 1;
461+
}
403462
return sb.ToString();
404463
}
405464

src/CSVSettings.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,32 @@
99

1010
namespace CSVFile
1111
{
12+
/// <summary>
13+
/// Defines the behavior of CSV serialization when a nested array is encountered
14+
/// </summary>
15+
public enum ArrayOptions
16+
{
17+
/// <summary>
18+
/// Use built-in string conversion, which renders arrays as `MyObject[]`
19+
/// </summary>
20+
ToString,
21+
22+
/// <summary>
23+
/// Convert any array columns that are array types into nulls (either blanks or null tokens)
24+
/// </summary>
25+
TreatAsNull,
26+
27+
/// <summary>
28+
/// Render the number of items in the array
29+
/// </summary>
30+
CountItems,
31+
32+
/// <summary>
33+
/// Serialize child objects recursively using the same settings
34+
/// </summary>
35+
RecursiveSerialization,
36+
}
37+
1238
/// <summary>
1339
/// Settings to configure how a CSV file is parsed
1440
/// </summary>
@@ -133,11 +159,16 @@ public class CSVSettings
133159
/// </summary>
134160
public string DateTimeFormat { get; set; } = "o";
135161

162+
/// <summary>
163+
/// The behavior to use when serializing a column of an array type
164+
/// </summary>
165+
public ArrayOptions NestedArrayBehavior = ArrayOptions.TreatAsNull;
166+
136167
/// <summary>
137168
/// Standard comma-separated value (CSV) file settings
138169
/// </summary>
139170
public static readonly CSVSettings CSV = new CSVSettings();
140-
171+
141172
/// <summary>
142173
/// Standard comma-separated value (CSV) file settings that permit rendering of NULL values
143174
/// </summary>

tests/SerializationTest.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ public class TestClassTwo
4444
public EnumTestType? ThirdColumn;
4545
}
4646

47+
public class TestClassThree
48+
{
49+
public string Name { get; set; }
50+
public string[] StringArray { get; set; }
51+
public List<int> IntList { get; set; }
52+
public IEnumerable<bool> BoolEnumerable { get; set; }
53+
public List<Guid> GuidList { get; set; }
54+
public List<Guid> NullableList { get; set; }
55+
}
56+
4757
[Test]
4858
public void TestObjectSerialization()
4959
{
@@ -125,6 +135,57 @@ public void TestNullSerialization()
125135
}
126136
}
127137

138+
/// <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
143+
/// </summary>
144+
[Test]
145+
public void TestArraySerialization()
146+
{
147+
var list = new List<TestClassThree>();
148+
list.Add(new TestClassThree()
149+
{
150+
Name = "Test",
151+
StringArray = new [] { "a", "b", "c"},
152+
IntList = new List<int> { 1, 2, 3 },
153+
BoolEnumerable = new [] { true, false, true, false },
154+
GuidList = new List<Guid>(),
155+
});
156+
157+
// Serialize to a CSV string using ToString
158+
// This was the default behavior in CSVFile 3.1.2 and earlier - it's pretty ugly!
159+
var options = new CSVSettings()
160+
{
161+
HeaderRowIncluded = true,
162+
NestedArrayBehavior = ArrayOptions.ToString,
163+
NullToken = "NULL",
164+
AllowNull = true,
165+
};
166+
var toStringCsv = CSV.Serialize(list, options);
167+
Assert.AreEqual($"Name,StringArray,IntList,BoolEnumerable,GuidList,NullableList{Environment.NewLine}"
168+
+ $"Test,System.String[],System.Collections.Generic.List`1[System.Int32],System.Boolean[],System.Collections.Generic.List`1[System.Guid],NULL{Environment.NewLine}", toStringCsv);
169+
170+
// Serialize to a CSV string using counts
171+
options.NestedArrayBehavior = ArrayOptions.CountItems;
172+
var countItemsCsv = CSV.Serialize(list, options);
173+
Assert.AreEqual($"Name,StringArray,IntList,BoolEnumerable,GuidList,NullableList{Environment.NewLine}"
174+
+ $"Test,3,3,4,0,NULL{Environment.NewLine}", countItemsCsv);
175+
176+
// Serialize to a CSV string using counts
177+
options.NestedArrayBehavior = ArrayOptions.TreatAsNull;
178+
var ignoreArraysCsv = CSV.Serialize(list, options);
179+
Assert.AreEqual($"Name,StringArray,IntList,BoolEnumerable,GuidList,NullableList{Environment.NewLine}"
180+
+ $"Test,NULL,NULL,NULL,NULL,NULL{Environment.NewLine}", ignoreArraysCsv);
181+
182+
// And now for the magic: Recursive serialization!
183+
options.NestedArrayBehavior = ArrayOptions.RecursiveSerialization;
184+
var recursiveCsv = CSV.Serialize(list, options);
185+
Assert.AreEqual($"Name,StringArray,IntList,BoolEnumerable,GuidList,NullableList{Environment.NewLine}"
186+
+ $"Test,\"a,b,c\",\"1,2,3\",\"True,False,True,False\",,NULL{Environment.NewLine}", recursiveCsv);
187+
}
188+
128189
[Test]
129190
public void TestCaseInsensitiveDeserializer()
130191
{

0 commit comments

Comments
 (0)