Skip to content
This repository was archived by the owner on Dec 24, 2022. It is now read-only.

Commit 69085b6

Browse files
committed
Add support for serializing CSV with excluded default values
1 parent c53744d commit 69085b6

File tree

4 files changed

+112
-16
lines changed

4 files changed

+112
-16
lines changed

src/ServiceStack.Text/CsvWriter.cs

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
5+
using System.Reflection;
56
using ServiceStack.Text.Common;
67

78
namespace ServiceStack.Text
@@ -144,6 +145,7 @@ public class CsvWriter<T>
144145
public static List<string> Headers { get; set; }
145146

146147
internal static List<GetMemberDelegate<T>> PropertyGetters;
148+
internal static List<PropertyInfo> PropertyInfos;
147149

148150
private static readonly WriteObjectDelegate OptimizedWriter;
149151

@@ -163,12 +165,15 @@ internal static void Reset()
163165
Headers = new List<string>();
164166

165167
PropertyGetters = new List<GetMemberDelegate<T>>();
168+
PropertyInfos = new List<PropertyInfo>();
166169
foreach (var propertyInfo in TypeConfig<T>.Properties)
167170
{
168171
if (!propertyInfo.CanRead || propertyInfo.GetGetMethod(nonPublic:true) == null) continue;
169172
if (!TypeSerializer.CanCreateFromString(propertyInfo.PropertyType)) continue;
170173

171174
PropertyGetters.Add(propertyInfo.CreateGetter<T>());
175+
PropertyInfos.Add(propertyInfo);
176+
172177
var propertyName = propertyInfo.Name;
173178
var dcsDataMemberName = propertyInfo.GetDataMemberName();
174179
if (dcsDataMemberName != null)
@@ -257,6 +262,7 @@ public static void WriteObjectRow(TextWriter writer, object record)
257262
public static void Write(TextWriter writer, IEnumerable<T> records)
258263
{
259264
if (writer == null) return; //AOT
265+
if (records == null) return;
260266

261267
if (typeof(T) == typeof(Dictionary<string, string>) || typeof(T) == typeof(IDictionary<string, string>))
262268
{
@@ -277,10 +283,56 @@ public static void Write(TextWriter writer, IEnumerable<T> records)
277283
return;
278284
}
279285

280-
if (!CsvConfig<T>.OmitHeaders && Headers.Count > 0)
286+
var recordsList = records.ToList();
287+
288+
var headers = Headers;
289+
var propGetters = PropertyGetters;
290+
var treatAsSingleRow = typeof(T).IsValueType || typeof(T) == typeof(string);
291+
292+
if (!treatAsSingleRow && JsConfig.ExcludeDefaultValues)
293+
{
294+
var hasValues = new bool[headers.Count];
295+
var defaultValues = new object[headers.Count];
296+
for (var i = 0; i < PropertyInfos.Count; i++)
297+
{
298+
defaultValues[i] = PropertyInfos[i].PropertyType.GetDefaultValue();
299+
}
300+
301+
foreach (var record in recordsList)
302+
{
303+
for (var i = 0; i < propGetters.Count; i++)
304+
{
305+
var propGetter = propGetters[i];
306+
var value = propGetter(record);
307+
308+
if (value != null && !value.Equals(defaultValues[i]))
309+
hasValues[i] = true;
310+
}
311+
}
312+
313+
if (hasValues.Any(x => x == false))
314+
{
315+
var newHeaders = new List<string>();
316+
var newGetters = new List<GetMemberDelegate<T>>();
317+
318+
for (int i = 0; i < hasValues.Length; i++)
319+
{
320+
if (hasValues[i])
321+
{
322+
newHeaders.Add(headers[i]);
323+
newGetters.Add(propGetters[i]);
324+
}
325+
}
326+
327+
headers = newHeaders;
328+
propGetters = newGetters;
329+
}
330+
}
331+
332+
if (!CsvConfig<T>.OmitHeaders && headers.Count > 0)
281333
{
282334
var ranOnce = false;
283-
foreach (var header in Headers)
335+
foreach (var header in headers)
284336
{
285337
CsvWriter.WriteItemSeperatorIfRanOnce(writer, ref ranOnce);
286338

@@ -289,25 +341,23 @@ public static void Write(TextWriter writer, IEnumerable<T> records)
289341
writer.Write(CsvConfig.RowSeparatorString);
290342
}
291343

292-
if (records == null) return;
293-
294-
if (typeof(T).IsValueType || typeof(T) == typeof(string))
344+
if (treatAsSingleRow)
295345
{
296-
var singleRow = GetSingleRow(records, typeof(T));
346+
var singleRow = GetSingleRow(recordsList, typeof(T));
297347
WriteRow(writer, singleRow);
298348
return;
299349
}
300350

301-
var row = new string[Headers.Count];
302-
foreach (var record in records)
351+
var row = new string[headers.Count];
352+
foreach (var record in recordsList)
303353
{
304-
for (var i = 0; i < PropertyGetters.Count; i++)
354+
for (var i = 0; i < propGetters.Count; i++)
305355
{
306-
var propertyGetter = PropertyGetters[i];
307-
var value = propertyGetter(record) ?? "";
356+
var propGetter = propGetters[i];
357+
var value = propGetter(record) ?? "";
308358

309-
var strValue = value is string
310-
? (string)value
359+
var strValue = value is string s
360+
? s
311361
: TypeSerializer.SerializeToString(value).StripQuotes();
312362

313363
row[i] = strValue;

tests/Northwind.Common/DataModel/Northwind.models.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ public override int GetHashCode()
523523
public class OrderDetail
524524
: IHasStringId, IEquatable<OrderDetail>
525525
{
526-
public string Id { get { return this.OrderId + "/" + this.ProductId; } }
526+
public string Id => this.OrderId + "/" + this.ProductId;
527527

528528
[Index]
529529
[Alias("OrderID")]

tests/ServiceStack.Text.Tests/CsvSerializerTests.cs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using System.Collections.Generic;
44
using System.Globalization;
55
using System.IO;
6-
using System.Threading;
6+
using System.Linq;
77
using Northwind.Common.DataModel;
88
using NUnit.Framework;
99
using ServiceStack.Text.Tests.Support;
@@ -390,6 +390,52 @@ public void Can_serialize_fields_with_double_quotes()
390390
fromCsv = person.ToCsv().FromCsv<Person>();
391391
Assert.That(fromCsv, Is.EqualTo(person));
392392
}
393+
394+
public Order Clone(Order o) => new Order {
395+
Id = o.Id,
396+
CustomerId = o.CustomerId,
397+
EmployeeId = o.EmployeeId,
398+
OrderDate = o.OrderDate,
399+
RequiredDate = o.RequiredDate,
400+
ShippedDate = o.ShippedDate,
401+
ShipVia = o.ShipVia,
402+
Freight = o.Freight,
403+
ShipName = o.ShipName,
404+
ShipAddress = o.ShipAddress,
405+
ShipCity = o.ShipCity,
406+
ShipRegion = o.ShipRegion,
407+
ShipPostalCode = o.ShipPostalCode,
408+
ShipCountry = o.ShipCountry,
409+
};
410+
411+
[Test]
412+
public void Can_only_serialize_NonDefaultValues()
413+
{
414+
using var scope = JsConfig.With(new Config {
415+
ExcludeDefaultValues = true
416+
});
417+
418+
var orders = NorthwindData.Orders.Take(5).Map(Clone);
419+
orders.ForEach(x => {
420+
//non-default min values
421+
x.RequiredDate = DateTime.MinValue;
422+
x.ShipVia = 0;
423+
424+
//default values
425+
x.ShippedDate = null;
426+
x.EmployeeId = default;
427+
x.Freight = default;
428+
x.ShipPostalCode = null;
429+
x.ShipCountry = null;
430+
});
431+
432+
var csv = orders.ToCsv();
433+
// csv.Print();
434+
var headers = csv.LeftPart('\r');
435+
headers.Print();
436+
Assert.That(headers, Is.EquivalentTo(
437+
"Id,CustomerId,OrderDate,RequiredDate,ShipVia,ShipName,ShipAddress,ShipCity,ShipRegion"));
438+
}
393439

394440
[Test]
395441
public void serialize_Category()

tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<GenerateAssemblyCopyrightAttribute>false</GenerateAssemblyCopyrightAttribute>
1414
<GenerateAssemblyVersionAttribute>false</GenerateAssemblyVersionAttribute>
1515
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
16-
<LangVersion>7.2</LangVersion>
16+
<LangVersion>latest</LangVersion>
1717
</PropertyGroup>
1818
<PropertyGroup>
1919
<DebugType Condition="'$(TargetFramework)' != '' AND '$(TargetFramework)' != 'netcoreapp2.1'">Full</DebugType>

0 commit comments

Comments
 (0)