From 1ce156a0fcf75c1ce3a7d07e051c5259f1939761 Mon Sep 17 00:00:00 2001 From: Christian Jacob Date: Tue, 11 May 2021 20:39:57 +0200 Subject: [PATCH 1/5] Updated target frameworks, added NewtonSoft.Json, added support for JsonPropertyAttribute, fixed l10n issue in tests regarding DateTime --- src/CsvSerializer/CsvSerializer.csproj | 3 +- src/CsvSerializer/Properties/AssemblyInfo.cs | 2 +- src/CsvSerializer/Serializer.cs | 410 +++++++++--------- src/CsvSerializerTests/BasicFunctionality.cs | 26 +- .../CsvSerializerTests.csproj | 9 +- src/CsvSerializerTests/TestObjects.cs | 13 + 6 files changed, 252 insertions(+), 211 deletions(-) diff --git a/src/CsvSerializer/CsvSerializer.csproj b/src/CsvSerializer/CsvSerializer.csproj index c0db8e8..3e4f9ec 100644 --- a/src/CsvSerializer/CsvSerializer.csproj +++ b/src/CsvSerializer/CsvSerializer.csproj @@ -1,7 +1,7 @@  - netstandard1.3 + netstandard2.1 CsvSerializer CsvSerializer 1.6.1 @@ -31,6 +31,7 @@ + diff --git a/src/CsvSerializer/Properties/AssemblyInfo.cs b/src/CsvSerializer/Properties/AssemblyInfo.cs index 5970572..0e6d8d9 100644 --- a/src/CsvSerializer/Properties/AssemblyInfo.cs +++ b/src/CsvSerializer/Properties/AssemblyInfo.cs @@ -11,7 +11,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Mindfire Technology")] [assembly: AssemblyProduct("CsvSerializer")] -[assembly: AssemblyCopyright("Copyright © Mindfite Technology")] +[assembly: AssemblyCopyright("Copyright © Mindfire Technology")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] diff --git a/src/CsvSerializer/Serializer.cs b/src/CsvSerializer/Serializer.cs index 9fc9494..29fc197 100644 --- a/src/CsvSerializer/Serializer.cs +++ b/src/CsvSerializer/Serializer.cs @@ -1,174 +1,182 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using CsvSerializer.Csv; - -namespace CsvSerializer -{ - public class Serializer : ISerializer - { - public CsvSettings Settings { get; set; } - - public Stack Serializers { get; protected set; } - - public Serializer() - { - Settings = new CsvSettings(); - Serializers = - new Stack - { - //new DefaultCsvSerializer() - }; - } - - public void Serialize(Stream output, object value, bool leaveStreamOpen = true) - { - if (output == null) - throw new ArgumentNullException("output"); - - if (value == null) - throw new ArgumentNullException("value"); - - - var columnList = GetColumnList(value); - - using (var csv = new CsvBuilder(Settings, output, leaveStreamOpen)) - { - // Setup Columns - csv.AddColumns(columnList); - - // Write out row data - foreach (object rowObject in EnumerateRows(value)) - { - var row = csv.AddRow(); - PopulateRowData(row, rowObject); - } - } - } - - private void PopulateRowData(Row row, object rowObject) - { - foreach (var cell in row.Values) - { - cell.Value = GetValue(cell, rowObject); - } - } - - private string GetValue(Cell cell, object rowObject) - { - //cell.Column.Info.GetValue(rowObject, null).ToString(); - object o = ResolveObjectPathOrNull(cell.Column.ObjectPath, rowObject, cell.Column.ObjectIndex); - if (o == null) - return string.Empty; - else - return o.ToString(); - } - - private object ResolveObjectPathOrNull(string path, object value, int? rownum = null) - { - string[] heirarchy = path.Split('.'); - foreach (string part in heirarchy) - { - var prop = value.GetType().GetProperty(part); - value = prop.GetValue(value, null); - - if (value is IEnumerable && !(value is string) && rownum != null) - value = ((IEnumerable)value).GetObjectAtIndex(rownum.Value); - - if (value == null) - return null; - } - - return value; - } - - protected IEnumerable GetProperties(object value, bool recursive, string prefix = "", int? colnum = null) - { - if (prefix == null) - prefix = string.Empty; - - foreach (var prop in value.Properties()) - { - string name = (prefix + "." + prop.Name).TrimStart('.'); - string simpleName = (!Settings.ShowFullNamePath ? string.Empty : - prefix + (colnum == null ? string.Empty : colnum.ToString()) + Settings.NamePathDelimeter) + prop.Name; - if (simpleName.StartsWith(Settings.NamePathDelimeter)) - simpleName = simpleName.Substring(Settings.NamePathDelimeter.Length); - - var attributes = prop.GetCustomAttributes(true); - - // Check for ignore - if ((attributes.Any(n => n.TypeName() == "CsvIgnoreAttribute")) || - (Settings.UseSerializerAttributes && attributes.Any(n => n.TypeName() == "NonSerializedAttribute")) || - (Settings.UseXmlAttributes && attributes.Any(n => n.TypeName() == "XmlIgnoreAttribute")) || - (Settings.UseJsonAttributes && attributes.Any(n => n.TypeName() == "JsonIgnoreAttribute"))) - continue; - - // Check for collection - object propValue = prop.GetValue(value, null); - if (Settings.ConvertChildCollectionsToRows && propValue is IEnumerable && !(propValue is string)) - { - // The row names are index based Address1, Address2, etc. - int rownum = 1; - foreach (object child in EnumerateRows(propValue)) - foreach (var pd in GetProperties(child, true, name, rownum++)) - yield return pd; - } - else if (!IsClrType(prop.PropertyType) && recursive && propValue != null) - { - foreach (var pd in GetProperties(propValue, true, name)) - yield return pd; - } - else - { - yield return new PropertyData - { - Info = prop, - Name = simpleName, - ObjectPath = name, - ObjectIndex = colnum, - PropertyValue = propValue - }; - } - } - } - - protected class PropertyData - { - public PropertyInfo Info { get; set; } - public string Name { get; set; } - public string ObjectPath { get; set; } - public int? ObjectIndex { get; set; } - public object PropertyValue { get; set; } - } - - protected List GetColumnList(object value) - { - var result = new List(); - - // Is the base collection Enumerable? If so, ignore it and use a child as the prototype. - if (value is IEnumerable) - { - var enumerator = ((IEnumerable)value).GetEnumerator(); - if (enumerator.MoveNext() && enumerator.Current != null) - value = enumerator.Current; - } - - return GetProperties(value, true).Select(n => new Column - { - Name = n.Name, - ObjectPath = n.ObjectPath, - ObjectIndex = n.ObjectIndex, - Info = n.Info - }).ToList(); - } - - private bool IsClrType(Type type) - { +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using CsvSerializer.Csv; +using Newtonsoft.Json; + +namespace CsvSerializer +{ + public class Serializer : ISerializer + { + public CsvSettings Settings { get; set; } + + public Stack Serializers { get; protected set; } + + public Serializer() + { + Settings = new CsvSettings(); + Serializers = + new Stack + { + //new DefaultCsvSerializer() + }; + } + + public void Serialize(Stream output, object value, bool leaveStreamOpen = true) + { + if (output == null) + throw new ArgumentNullException("output"); + + if (value == null) + throw new ArgumentNullException("value"); + + + var columnList = GetColumnList(value); + + using (var csv = new CsvBuilder(Settings, output, leaveStreamOpen)) + { + // Setup Columns + csv.AddColumns(columnList); + + // Write out row data + foreach (object rowObject in EnumerateRows(value)) + { + var row = csv.AddRow(); + PopulateRowData(row, rowObject); + } + } + } + + private void PopulateRowData(Row row, object rowObject) + { + foreach (var cell in row.Values) + { + cell.Value = GetValue(cell, rowObject); + } + } + + private string GetValue(Cell cell, object rowObject) + { + //cell.Column.Info.GetValue(rowObject, null).ToString(); + object o = ResolveObjectPathOrNull(cell.Column.ObjectPath, rowObject, cell.Column.ObjectIndex); + if (o == null) + return string.Empty; + else + return o.ToString(); + } + + private object ResolveObjectPathOrNull(string path, object value, int? rownum = null) + { + string[] heirarchy = path.Split('.'); + foreach (string part in heirarchy) + { + var prop = value.GetType().GetProperty(part); + value = prop.GetValue(value, null); + + if (value is IEnumerable && !(value is string) && rownum != null) + value = ((IEnumerable)value).GetObjectAtIndex(rownum.Value); + + if (value == null) + return null; + } + + return value; + } + + protected IEnumerable GetProperties(object value, bool recursive, string prefix = "", int? colnum = null) + { + if (prefix == null) + prefix = string.Empty; + + foreach (var prop in value.Properties()) + { + string name = (prefix + "." + prop.Name).TrimStart('.'); + string simpleName = (!Settings.ShowFullNamePath ? string.Empty : + prefix + (colnum == null ? string.Empty : colnum.ToString()) + Settings.NamePathDelimeter) + prop.Name; + if (simpleName.StartsWith(Settings.NamePathDelimeter)) + simpleName = simpleName.Substring(Settings.NamePathDelimeter.Length); + + var attributes = prop.GetCustomAttributes(true); + + // Check for ignore + if ((attributes.Any(n => n.TypeName() == "CsvIgnoreAttribute")) || + (Settings.UseSerializerAttributes && attributes.Any(n => n.TypeName() == "NonSerializedAttribute")) || + (Settings.UseXmlAttributes && attributes.Any(n => n.TypeName() == "XmlIgnoreAttribute")) || + (Settings.UseJsonAttributes && attributes.Any(n => n.TypeName() == "JsonIgnoreAttribute"))) + continue; + + // Check for JsonPropertyAttribute + if (Settings.UseJsonAttributes && attributes.Any(n => n.TypeName() == "JsonPropertyAttribute")) + { + var attribute = (JsonPropertyAttribute)attributes.First(n => n.TypeName() == "JsonPropertyAttribute"); + simpleName = attribute.PropertyName; + } + + // Check for collection + object propValue = prop.GetValue(value, null); + if (Settings.ConvertChildCollectionsToRows && propValue is IEnumerable && !(propValue is string)) + { + // The row names are index based Address1, Address2, etc. + int rownum = 1; + foreach (object child in EnumerateRows(propValue)) + foreach (var pd in GetProperties(child, true, name, rownum++)) + yield return pd; + } + else if (!IsClrType(prop.PropertyType) && recursive && propValue != null) + { + foreach (var pd in GetProperties(propValue, true, name)) + yield return pd; + } + else + { + yield return new PropertyData + { + Info = prop, + Name = simpleName, + ObjectPath = name, + ObjectIndex = colnum, + PropertyValue = propValue + }; + } + } + } + + protected class PropertyData + { + public PropertyInfo Info { get; set; } + public string Name { get; set; } + public string ObjectPath { get; set; } + public int? ObjectIndex { get; set; } + public object PropertyValue { get; set; } + } + + protected List GetColumnList(object value) + { + var result = new List(); + + // Is the base collection Enumerable? If so, ignore it and use a child as the prototype. + if (value is IEnumerable) + { + var enumerator = ((IEnumerable)value).GetEnumerator(); + if (enumerator.MoveNext() && enumerator.Current != null) + value = enumerator.Current; + } + + return GetProperties(value, true).Select(n => new Column + { + Name = n.Name, + ObjectPath = n.ObjectPath, + ObjectIndex = n.ObjectIndex, + Info = n.Info + }).ToList(); + } + + private bool IsClrType(Type type) + { if (type == typeof(string) || type == typeof(decimal) || type == typeof(decimal?) || @@ -194,33 +202,33 @@ private bool IsClrType(Type type) type == typeof(float) || type == typeof(float?) || type == typeof(double) || - type == typeof(double?)) - return true; - else - return false; - } - - private ICsvCustomSerializer GetSerializer(PropertyInfo prop) - { - return null; - } - - private IOutputFormatter GetFormatter(PropertyInfo prop) - { - return null; - } - - private IEnumerable EnumerateRows(object value) - { - if (value is IEnumerable) - return (IEnumerable)value; - else - return EmumerateOne(value); - } - - private IEnumerable EmumerateOne(object value) - { - yield return value; - } - } -} + type == typeof(double?)) + return true; + else + return false; + } + + private ICsvCustomSerializer GetSerializer(PropertyInfo prop) + { + return null; + } + + private IOutputFormatter GetFormatter(PropertyInfo prop) + { + return null; + } + + private IEnumerable EnumerateRows(object value) + { + if (value is IEnumerable) + return (IEnumerable)value; + else + return EmumerateOne(value); + } + + private IEnumerable EmumerateOne(object value) + { + yield return value; + } + } +} diff --git a/src/CsvSerializerTests/BasicFunctionality.cs b/src/CsvSerializerTests/BasicFunctionality.cs index 88efad3..1c34ae4 100644 --- a/src/CsvSerializerTests/BasicFunctionality.cs +++ b/src/CsvSerializerTests/BasicFunctionality.cs @@ -154,7 +154,7 @@ public void FlattenObjectAsColumns() Customer = new Person { FirstName = "Nate", LastName = "Zaugg" } }; var ms = new MemoryStream(); - string expected = "Id,OrderDate,Customer.FirstName,Customer.LastName,Subtotal,Tax,Total\r\nOrder/12,6/1/2015 12:00:00 AM,Nate,Zaugg,0,0,0\r\n"; + string expected = $"Id,OrderDate,Customer.FirstName,Customer.LastName,Subtotal,Tax,Total\r\nOrder/12,{new DateTime(2015, 06, 01)},Nate,Zaugg,0,0,0\r\n"; string actual; @@ -184,7 +184,7 @@ public void FlattenCollectionsAsColumns() order.Add(new OrderItem { Id = "2", Name = "Xoom Tablet", ShortDescription = "I like Xoom tab", Qty = 1, PricePerQty = 100, LineTotal = 100 }); var ms = new MemoryStream(); - string expected = "Id,OrderDate,Customer.FirstName,Customer.LastName,Subtotal,Tax,Total,Items1.Name,Items1.ShortDescription,Items1.PricePerQty,Items1.Qty,Items1.LineTotal,Items1.TimeShipped,Items1.DiscountAmount,Items2.Name,Items2.ShortDescription,Items2.PricePerQty,Items2.Qty,Items2.LineTotal,Items2.TimeShipped,Items2.DiscountAmount\r\nOrder/12,6/1/2015 12:00:00 AM,Nate,Zaugg,300,22,322,Galaxy S5,My phone is nice!,200,1,200,,,Xoom Tablet,I like Xoom tab,100,1,100,,\r\n"; + string expected = $"Id,OrderDate,Customer.FirstName,Customer.LastName,Subtotal,Tax,Total,Items1.Name,Items1.ShortDescription,Items1.PricePerQty,Items1.Qty,Items1.LineTotal,Items1.TimeShipped,Items1.DiscountAmount,Items2.Name,Items2.ShortDescription,Items2.PricePerQty,Items2.Qty,Items2.LineTotal,Items2.TimeShipped,Items2.DiscountAmount\r\nOrder/12,{new DateTime(2015, 06, 01)},Nate,Zaugg,300,22,322,Galaxy S5,My phone is nice!,200,1,200,,,Xoom Tablet,I like Xoom tab,100,1,100,,\r\n"; string actual; @@ -262,8 +262,8 @@ public void AssertNullableValuesSerialize() sb.Append("Id,OrderDate,Customer.FirstName,Customer.LastName,Subtotal,Tax,Total,"); sb.Append("Items1.Name,Items1.ShortDescription,Items1.PricePerQty,Items1.Qty,Items1.LineTotal,Items1.TimeShipped,Items1.DiscountAmount,"); sb.Append("Items2.Name,Items2.ShortDescription,Items2.PricePerQty,Items2.Qty,Items2.LineTotal,Items2.TimeShipped,Items2.DiscountAmount\r\n"); - sb.Append("Order/13,4/7/2017 12:00:00 AM,Zach,Thurston,935,21,956,"); - sb.Append("iPhone 7,128 GB Jet Black,850,1,775,4/7/2017 12:03:00 PM,75,"); + sb.Append($"Order/13,{new DateTime(2017, 04, 07)},Zach,Thurston,935,21,956,"); + sb.Append($"iPhone 7,128 GB Jet Black,850,1,775,{new DateTime(2017, 04, 07, 12, 3, 00)},75,"); sb.Append("AirPods,Wireless earbuds,160,1,160,,\r\n"); string actual; @@ -276,6 +276,24 @@ public void AssertNullableValuesSerialize() // Assert Assert.AreEqual(expected, actual); } + + [TestMethod] + public void AssertHeaderPrintedUsingJsonPropertyAttribute() + { + // Arrange + var serializer = new Serializer(); + var person = new RestPerson { FirstName = "Nate \"D\"", LastName = "Zaugg" }; + var ms = new MemoryStream(); + string expected = "first_name,last_name\r\n\"Nate \"\"D\"\"\",Zaugg\r\n"; + string actual; + + // Act + serializer.Serialize(ms, person); + actual = Encoding.UTF8.GetString(ms.ToArray()); + + // Assert + Assert.AreEqual(expected, actual); + } } } diff --git a/src/CsvSerializerTests/CsvSerializerTests.csproj b/src/CsvSerializerTests/CsvSerializerTests.csproj index e5aeafd..ae5c4f1 100644 --- a/src/CsvSerializerTests/CsvSerializerTests.csproj +++ b/src/CsvSerializerTests/CsvSerializerTests.csproj @@ -1,7 +1,7 @@  - netcoreapp1.0 + netcoreapp3.1 false @@ -13,17 +13,18 @@ + - + - + - + diff --git a/src/CsvSerializerTests/TestObjects.cs b/src/CsvSerializerTests/TestObjects.cs index 69f086a..4fbd2b6 100644 --- a/src/CsvSerializerTests/TestObjects.cs +++ b/src/CsvSerializerTests/TestObjects.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using CsvSerializer; +using Newtonsoft.Json; namespace CsvSerializerTests { @@ -15,6 +16,18 @@ public class Person public string LastName { get; set; } } + public class RestPerson + { + [JsonIgnore] + public string Id { get; set; } + + [JsonProperty("first_name")] + public string FirstName { get; set; } + + [JsonProperty("last_name")] + public string LastName { get; set; } + } + public class Order { From 892cd5b9095c769d170bc30e5e01c483100092e2 Mon Sep 17 00:00:00 2001 From: Christian Jacob Date: Tue, 11 May 2021 21:08:14 +0200 Subject: [PATCH 2/5] Added CsvSerializationException, added support for JsonRequiredAttribute --- .../CsvSerializationException.cs | 17 +++++++++++ src/CsvSerializer/Serializer.cs | 11 ++++++++ src/CsvSerializerTests/BasicFunctionality.cs | 28 ++++++++++++++++++- src/CsvSerializerTests/TestObjects.cs | 1 + 4 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/CsvSerializer/CsvSerializationException.cs diff --git a/src/CsvSerializer/CsvSerializationException.cs b/src/CsvSerializer/CsvSerializationException.cs new file mode 100644 index 0000000..7695720 --- /dev/null +++ b/src/CsvSerializer/CsvSerializationException.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CsvSerializer +{ + public class CsvSerializationException : Exception + { + public CsvSerializationException(string message) : base(message) + { + } + + public CsvSerializationException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/src/CsvSerializer/Serializer.cs b/src/CsvSerializer/Serializer.cs index 29fc197..2a2feb7 100644 --- a/src/CsvSerializer/Serializer.cs +++ b/src/CsvSerializer/Serializer.cs @@ -116,6 +116,17 @@ protected IEnumerable GetProperties(object value, bool recursive, simpleName = attribute.PropertyName; } + // Check for JsonRequiredAttribute + if (Settings.UseJsonAttributes && attributes.Any(n => n.TypeName() == "JsonRequiredAttribute")) + { + object propertyValue = prop.GetValue(value, null); + + if (propertyValue == null) + { + throw new CsvSerializationException($"Cannot write a null value for property '{prop.Name}'. Property requires a value.", null); + } + } + // Check for collection object propValue = prop.GetValue(value, null); if (Settings.ConvertChildCollectionsToRows && propValue is IEnumerable && !(propValue is string)) diff --git a/src/CsvSerializerTests/BasicFunctionality.cs b/src/CsvSerializerTests/BasicFunctionality.cs index 1c34ae4..e2d6b49 100644 --- a/src/CsvSerializerTests/BasicFunctionality.cs +++ b/src/CsvSerializerTests/BasicFunctionality.cs @@ -294,6 +294,32 @@ public void AssertHeaderPrintedUsingJsonPropertyAttribute() // Assert Assert.AreEqual(expected, actual); } - } + [TestMethod] + public void ThrowExceptionIfRequiredPropertyIsNull() + { + // Arrange + var serializer = new Serializer(); + var person = new RestPerson { FirstName = "Nate \"D\"" }; + var ms = new MemoryStream(); + string expected = "Cannot write a null value for property 'LastName'. Property requires a value."; + string actual; + + // Act + try + { + serializer.Serialize(ms, person); + + // Assert + Assert.Fail("Expected CsvSerializationException to occur."); + } + catch (CsvSerializationException ex ) + { + actual = ex.Message; + + // Assert + Assert.AreEqual(expected, actual); + } + } + } } diff --git a/src/CsvSerializerTests/TestObjects.cs b/src/CsvSerializerTests/TestObjects.cs index 4fbd2b6..e200489 100644 --- a/src/CsvSerializerTests/TestObjects.cs +++ b/src/CsvSerializerTests/TestObjects.cs @@ -24,6 +24,7 @@ public class RestPerson [JsonProperty("first_name")] public string FirstName { get; set; } + [JsonRequired] [JsonProperty("last_name")] public string LastName { get; set; } } From 52be12960d482510cc663f04293fc8c9124e68bb Mon Sep 17 00:00:00 2001 From: Christian Jacob Date: Tue, 11 May 2021 21:42:47 +0200 Subject: [PATCH 3/5] Added support for StringLengthAttribute --- src/CsvSerializer/CsvSerializer.csproj | 1 + src/CsvSerializer/CsvSettings.cs | 4 + src/CsvSerializer/Serializer.cs | 477 +++++++------- src/CsvSerializerTests/BasicFunctionality.cs | 651 ++++++++++--------- src/CsvSerializerTests/TestObjects.cs | 2 + 5 files changed, 591 insertions(+), 544 deletions(-) diff --git a/src/CsvSerializer/CsvSerializer.csproj b/src/CsvSerializer/CsvSerializer.csproj index 3e4f9ec..0747dbb 100644 --- a/src/CsvSerializer/CsvSerializer.csproj +++ b/src/CsvSerializer/CsvSerializer.csproj @@ -32,6 +32,7 @@ + diff --git a/src/CsvSerializer/CsvSettings.cs b/src/CsvSerializer/CsvSettings.cs index e4da213..c8c8af6 100644 --- a/src/CsvSerializer/CsvSettings.cs +++ b/src/CsvSerializer/CsvSettings.cs @@ -43,6 +43,9 @@ public class CsvSettings /// Specifies if attribute tags like [NonSerialized] or [DataMember(Name="value")] public bool UseSerializerAttributes { get; set; } + /// Specifies if attribute tags like [StringLength] + public bool UserDataAnnotationAttributes { get; set; } + /// /// Indicates if a collection is detected as a property on the object to be serialized, the collection will be converted to rows. /// E.g. If there is a Person with 3 addresses, there will be columns for Person.Address1.City, Person.Address2.City, and Person.Address3.City. @@ -73,6 +76,7 @@ public CsvSettings() UseXmlAttributes = true; UseJsonAttributes = true; UseSerializerAttributes = true; + UserDataAnnotationAttributes = true; ConvertChildCollectionsToRows = true; FlattenHeirarchicalStructuresWithEmptyRows = true; } diff --git a/src/CsvSerializer/Serializer.cs b/src/CsvSerializer/Serializer.cs index 2a2feb7..a489e6c 100644 --- a/src/CsvSerializer/Serializer.cs +++ b/src/CsvSerializer/Serializer.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Reflection; @@ -10,236 +11,248 @@ namespace CsvSerializer { - public class Serializer : ISerializer - { - public CsvSettings Settings { get; set; } - - public Stack Serializers { get; protected set; } - - public Serializer() - { - Settings = new CsvSettings(); - Serializers = - new Stack - { - //new DefaultCsvSerializer() - }; - } - - public void Serialize(Stream output, object value, bool leaveStreamOpen = true) - { - if (output == null) - throw new ArgumentNullException("output"); - - if (value == null) - throw new ArgumentNullException("value"); - - - var columnList = GetColumnList(value); - - using (var csv = new CsvBuilder(Settings, output, leaveStreamOpen)) - { - // Setup Columns - csv.AddColumns(columnList); - - // Write out row data - foreach (object rowObject in EnumerateRows(value)) - { - var row = csv.AddRow(); - PopulateRowData(row, rowObject); - } - } - } - - private void PopulateRowData(Row row, object rowObject) - { - foreach (var cell in row.Values) - { - cell.Value = GetValue(cell, rowObject); - } - } - - private string GetValue(Cell cell, object rowObject) - { - //cell.Column.Info.GetValue(rowObject, null).ToString(); - object o = ResolveObjectPathOrNull(cell.Column.ObjectPath, rowObject, cell.Column.ObjectIndex); - if (o == null) - return string.Empty; - else - return o.ToString(); - } - - private object ResolveObjectPathOrNull(string path, object value, int? rownum = null) - { - string[] heirarchy = path.Split('.'); - foreach (string part in heirarchy) - { - var prop = value.GetType().GetProperty(part); - value = prop.GetValue(value, null); - - if (value is IEnumerable && !(value is string) && rownum != null) - value = ((IEnumerable)value).GetObjectAtIndex(rownum.Value); - - if (value == null) - return null; - } - - return value; - } - - protected IEnumerable GetProperties(object value, bool recursive, string prefix = "", int? colnum = null) - { - if (prefix == null) - prefix = string.Empty; - - foreach (var prop in value.Properties()) - { - string name = (prefix + "." + prop.Name).TrimStart('.'); - string simpleName = (!Settings.ShowFullNamePath ? string.Empty : - prefix + (colnum == null ? string.Empty : colnum.ToString()) + Settings.NamePathDelimeter) + prop.Name; - if (simpleName.StartsWith(Settings.NamePathDelimeter)) - simpleName = simpleName.Substring(Settings.NamePathDelimeter.Length); - - var attributes = prop.GetCustomAttributes(true); - - // Check for ignore - if ((attributes.Any(n => n.TypeName() == "CsvIgnoreAttribute")) || - (Settings.UseSerializerAttributes && attributes.Any(n => n.TypeName() == "NonSerializedAttribute")) || - (Settings.UseXmlAttributes && attributes.Any(n => n.TypeName() == "XmlIgnoreAttribute")) || - (Settings.UseJsonAttributes && attributes.Any(n => n.TypeName() == "JsonIgnoreAttribute"))) - continue; - - // Check for JsonPropertyAttribute - if (Settings.UseJsonAttributes && attributes.Any(n => n.TypeName() == "JsonPropertyAttribute")) - { - var attribute = (JsonPropertyAttribute)attributes.First(n => n.TypeName() == "JsonPropertyAttribute"); - simpleName = attribute.PropertyName; - } - - // Check for JsonRequiredAttribute - if (Settings.UseJsonAttributes && attributes.Any(n => n.TypeName() == "JsonRequiredAttribute")) - { - object propertyValue = prop.GetValue(value, null); - - if (propertyValue == null) - { - throw new CsvSerializationException($"Cannot write a null value for property '{prop.Name}'. Property requires a value.", null); - } - } - - // Check for collection - object propValue = prop.GetValue(value, null); - if (Settings.ConvertChildCollectionsToRows && propValue is IEnumerable && !(propValue is string)) - { - // The row names are index based Address1, Address2, etc. - int rownum = 1; - foreach (object child in EnumerateRows(propValue)) - foreach (var pd in GetProperties(child, true, name, rownum++)) - yield return pd; - } - else if (!IsClrType(prop.PropertyType) && recursive && propValue != null) - { - foreach (var pd in GetProperties(propValue, true, name)) - yield return pd; - } - else - { - yield return new PropertyData - { - Info = prop, - Name = simpleName, - ObjectPath = name, - ObjectIndex = colnum, - PropertyValue = propValue - }; - } - } - } - - protected class PropertyData - { - public PropertyInfo Info { get; set; } - public string Name { get; set; } - public string ObjectPath { get; set; } - public int? ObjectIndex { get; set; } - public object PropertyValue { get; set; } - } - - protected List GetColumnList(object value) - { - var result = new List(); - - // Is the base collection Enumerable? If so, ignore it and use a child as the prototype. - if (value is IEnumerable) - { - var enumerator = ((IEnumerable)value).GetEnumerator(); - if (enumerator.MoveNext() && enumerator.Current != null) - value = enumerator.Current; - } - - return GetProperties(value, true).Select(n => new Column - { - Name = n.Name, - ObjectPath = n.ObjectPath, - ObjectIndex = n.ObjectIndex, - Info = n.Info - }).ToList(); - } - - private bool IsClrType(Type type) - { - if (type == typeof(string) || - type == typeof(decimal) || - type == typeof(decimal?) || - type == typeof(Array) || - type == typeof(DateTime) || - type == typeof(DateTime?) || - type == typeof(char) || - type == typeof(char?) || - type == typeof(byte) || - type == typeof(byte?) || - type == typeof(short) || - type == typeof(short?) || - type == typeof(ushort) || - type == typeof(ushort?) || - type == typeof(int) || - type == typeof(int?) || - type == typeof(uint) || - type == typeof(uint?) || - type == typeof(long) || - type == typeof(long?) || - type == typeof(ulong) || - type == typeof(ulong?) || - type == typeof(float) || - type == typeof(float?) || - type == typeof(double) || - type == typeof(double?)) - return true; - else - return false; - } - - private ICsvCustomSerializer GetSerializer(PropertyInfo prop) - { - return null; - } - - private IOutputFormatter GetFormatter(PropertyInfo prop) - { - return null; - } - - private IEnumerable EnumerateRows(object value) - { - if (value is IEnumerable) - return (IEnumerable)value; - else - return EmumerateOne(value); - } - - private IEnumerable EmumerateOne(object value) - { - yield return value; - } - } + public class Serializer : ISerializer + { + public CsvSettings Settings { get; set; } + + public Stack Serializers { get; protected set; } + + public Serializer() + { + Settings = new CsvSettings(); + Serializers = + new Stack + { + //new DefaultCsvSerializer() + }; + } + + public void Serialize(Stream output, object value, bool leaveStreamOpen = true) + { + if (output == null) + throw new ArgumentNullException("output"); + + if (value == null) + throw new ArgumentNullException("value"); + + + var columnList = GetColumnList(value); + + using (var csv = new CsvBuilder(Settings, output, leaveStreamOpen)) + { + // Setup Columns + csv.AddColumns(columnList); + + // Write out row data + foreach (object rowObject in EnumerateRows(value)) + { + var row = csv.AddRow(); + PopulateRowData(row, rowObject); + } + } + } + + private void PopulateRowData(Row row, object rowObject) + { + foreach (var cell in row.Values) + { + cell.Value = GetValue(cell, rowObject); + } + } + + private string GetValue(Cell cell, object rowObject) + { + //cell.Column.Info.GetValue(rowObject, null).ToString(); + object o = ResolveObjectPathOrNull(cell.Column.ObjectPath, rowObject, cell.Column.ObjectIndex); + if (o == null) + return string.Empty; + else + return o.ToString(); + } + + private object ResolveObjectPathOrNull(string path, object value, int? rownum = null) + { + string[] heirarchy = path.Split('.'); + foreach (string part in heirarchy) + { + var prop = value.GetType().GetProperty(part); + value = prop.GetValue(value, null); + + if (value is IEnumerable && !(value is string) && rownum != null) + value = ((IEnumerable)value).GetObjectAtIndex(rownum.Value); + + if (value == null) + return null; + } + + return value; + } + + protected IEnumerable GetProperties(object value, bool recursive, string prefix = "", int? colnum = null) + { + if (prefix == null) + prefix = string.Empty; + + foreach (var prop in value.Properties()) + { + string name = (prefix + "." + prop.Name).TrimStart('.'); + string simpleName = (!Settings.ShowFullNamePath ? string.Empty : + prefix + (colnum == null ? string.Empty : colnum.ToString()) + Settings.NamePathDelimeter) + prop.Name; + if (simpleName.StartsWith(Settings.NamePathDelimeter)) + simpleName = simpleName.Substring(Settings.NamePathDelimeter.Length); + + var attributes = prop.GetCustomAttributes(true); + + // Check for ignore + if ((attributes.Any(n => n.TypeName() == "CsvIgnoreAttribute")) || + (Settings.UseSerializerAttributes && attributes.Any(n => n.TypeName() == "NonSerializedAttribute")) || + (Settings.UseXmlAttributes && attributes.Any(n => n.TypeName() == "XmlIgnoreAttribute")) || + (Settings.UseJsonAttributes && attributes.Any(n => n.TypeName() == "JsonIgnoreAttribute"))) + continue; + + // Check for JsonPropertyAttribute + if (Settings.UseJsonAttributes && attributes.Any(n => n.TypeName() == "JsonPropertyAttribute")) + { + var attribute = (JsonPropertyAttribute)attributes.First(n => n.TypeName() == "JsonPropertyAttribute"); + simpleName = attribute.PropertyName; + } + + // Check for JsonRequiredAttribute + if (Settings.UseJsonAttributes && attributes.Any(n => n.TypeName() == "JsonRequiredAttribute")) + { + object propertyValue = prop.GetValue(value, null); + + if (propertyValue == null) + { + throw new CsvSerializationException($"Cannot write a null value for property '{prop.Name}'. Property requires a value.", null); + } + } + + // Check for StringLengthAttribute + if (Settings.UserDataAnnotationAttributes && attributes.Any(n => n.TypeName() == "StringLengthAttribute")) + { + var attribute = (StringLengthAttribute)attributes.First(n => n.TypeName() == "StringLengthAttribute"); + object propertyValue = prop.GetValue(value, null); + + if (propertyValue.ToString().Length > attribute.MaximumLength) + { + throw new CsvSerializationException(attribute.FormatErrorMessage(prop.Name), null); + } + } + + // Check for collection + object propValue = prop.GetValue(value, null); + if (Settings.ConvertChildCollectionsToRows && propValue is IEnumerable && !(propValue is string)) + { + // The row names are index based Address1, Address2, etc. + int rownum = 1; + foreach (object child in EnumerateRows(propValue)) + foreach (var pd in GetProperties(child, true, name, rownum++)) + yield return pd; + } + else if (!IsClrType(prop.PropertyType) && recursive && propValue != null) + { + foreach (var pd in GetProperties(propValue, true, name)) + yield return pd; + } + else + { + yield return new PropertyData + { + Info = prop, + Name = simpleName, + ObjectPath = name, + ObjectIndex = colnum, + PropertyValue = propValue + }; + } + } + } + + protected class PropertyData + { + public PropertyInfo Info { get; set; } + public string Name { get; set; } + public string ObjectPath { get; set; } + public int? ObjectIndex { get; set; } + public object PropertyValue { get; set; } + } + + protected List GetColumnList(object value) + { + var result = new List(); + + // Is the base collection Enumerable? If so, ignore it and use a child as the prototype. + if (value is IEnumerable) + { + var enumerator = ((IEnumerable)value).GetEnumerator(); + if (enumerator.MoveNext() && enumerator.Current != null) + value = enumerator.Current; + } + + return GetProperties(value, true).Select(n => new Column + { + Name = n.Name, + ObjectPath = n.ObjectPath, + ObjectIndex = n.ObjectIndex, + Info = n.Info + }).ToList(); + } + + private bool IsClrType(Type type) + { + if (type == typeof(string) || + type == typeof(decimal) || + type == typeof(decimal?) || + type == typeof(Array) || + type == typeof(DateTime) || + type == typeof(DateTime?) || + type == typeof(char) || + type == typeof(char?) || + type == typeof(byte) || + type == typeof(byte?) || + type == typeof(short) || + type == typeof(short?) || + type == typeof(ushort) || + type == typeof(ushort?) || + type == typeof(int) || + type == typeof(int?) || + type == typeof(uint) || + type == typeof(uint?) || + type == typeof(long) || + type == typeof(long?) || + type == typeof(ulong) || + type == typeof(ulong?) || + type == typeof(float) || + type == typeof(float?) || + type == typeof(double) || + type == typeof(double?)) + return true; + else + return false; + } + + private ICsvCustomSerializer GetSerializer(PropertyInfo prop) + { + return null; + } + + private IOutputFormatter GetFormatter(PropertyInfo prop) + { + return null; + } + + private IEnumerable EnumerateRows(object value) + { + if (value is IEnumerable) + return (IEnumerable)value; + else + return EmumerateOne(value); + } + + private IEnumerable EmumerateOne(object value) + { + yield return value; + } + } } diff --git a/src/CsvSerializerTests/BasicFunctionality.cs b/src/CsvSerializerTests/BasicFunctionality.cs index e2d6b49..c8d24fa 100644 --- a/src/CsvSerializerTests/BasicFunctionality.cs +++ b/src/CsvSerializerTests/BasicFunctionality.cs @@ -6,320 +6,347 @@ namespace CsvSerializerTests { - [TestClass] - public class BasicFunctionality - { - [TestMethod] - public void IntegrationTestSimpleObject() - { - // Arrange - var serializer = new Serializer(); - var person = new Person { FirstName = "Nate \"D\"", LastName = "Zaugg" }; - var ms = new MemoryStream(); - string expected = "FirstName,LastName\r\n\"Nate \"\"D\"\"\",Zaugg\r\n"; - string actual; - - // Act - serializer.Serialize(ms, person); - actual = Encoding.UTF8.GetString(ms.ToArray()); - - // Assert - Assert.AreEqual(expected, actual); - } - - [TestMethod] - public void IntegrationTestSimpleObjectArray() - { - // Arrange - var serializer = new Serializer(); - var people = new[] - { - new Person{ FirstName = "Nate \"D\"", LastName = "Zaugg" }, - new Person{ FirstName = "James\r\nTheKid", LastName = "King" }, - new Person{ FirstName = "Tiffany", LastName = "Zaugg" }, - }; - var ms = new MemoryStream(); - string expected = "FirstName,LastName\r\n\"Nate \"\"D\"\"\",Zaugg\r\nJames TheKid,King\r\nTiffany,Zaugg\r\n"; - string actual; - - // Act - serializer.Serialize(ms, people); - actual = Encoding.UTF8.GetString(ms.ToArray()); - - // Assert - Assert.AreEqual(expected, actual); - } - - [TestMethod] - public void AssertLinesEndWithCorrectLineEnding() - { - // Arrange - var serializer = new Serializer { Settings = new CsvSettings { NewLineDelimeter = "\n" } }; - var person = new Person { FirstName = "Nate \"D\"", LastName = "Zaugg" }; - var ms = new MemoryStream(); - string expected = "FirstName,LastName\n\"Nate \"\"D\"\"\",Zaugg\n"; - string actual; - - // Act - serializer.Serialize(ms, person); - actual = Encoding.UTF8.GetString(ms.ToArray()); - - // Assert - Assert.AreEqual(expected, actual); - } - - [TestMethod] - public void AssertHeaderPrintsOnlyWhenRequested() - { - // Arrange - var serializer = new Serializer { Settings = new CsvSettings { WriteHeaders = false } }; - var person = new Person { FirstName = "Nate \"D\"", LastName = "Zaugg" }; - var ms = new MemoryStream(); - string expected = "\"Nate \"\"D\"\"\",Zaugg\r\n"; - string actual; - - // Act - serializer.Serialize(ms, person); - actual = Encoding.UTF8.GetString(ms.ToArray()); - - // Assert - Assert.AreEqual(expected, actual); - } - - [TestMethod] - public void AssertValuesWithQuotesReceiveQuotes() - { - // Arrange - var serializer = new Serializer(); - var person = new Person { FirstName = "Nate \"D\"", LastName = "Zaugg" }; - var ms = new MemoryStream(); - string expected = "FirstName,LastName\r\n\"Nate \"\"D\"\"\",Zaugg\r\n"; - string actual; - - // Act - serializer.Serialize(ms, person); - actual = Encoding.UTF8.GetString(ms.ToArray()); - - // Assert - Assert.AreEqual(expected, actual); - } - - [TestMethod] - public void AssertValuesWithCamasReceiveQuotes() - { - // Arrange - var serializer = new Serializer(); - var person = new Person { FirstName = "Nate", LastName = "Dr, Zaugg" }; - var ms = new MemoryStream(); - string expected = "FirstName,LastName\r\nNate,\"Dr, Zaugg\"\r\n"; - - string actual; - - // Act - serializer.Serialize(ms, person); - actual = Encoding.UTF8.GetString(ms.ToArray()); - - // Assert - Assert.AreEqual(expected, actual); - } - - [TestMethod] - public void AssertValuesWithLineEndingsDoTheRightThing() - { - // Arrange - var serializer = new Serializer { Settings = new CsvSettings { RemoveLineBreaksInFields = false } }; - var person = new Person { FirstName = "Nate", LastName = "Dr\r\n Zaugg" }; - var ms = new MemoryStream(); - string expected = "FirstName,LastName\r\nNate,\"Dr\r\n Zaugg\"\r\n"; - - string actual; - - // Act - serializer.Serialize(ms, person); - actual = Encoding.UTF8.GetString(ms.ToArray()); - - // Assert - Assert.AreEqual(expected, actual); - } - - [TestMethod] - public void FlattenObjectAsColumns() - { - // Arrange - var serializer = new Serializer(); - var order = new Order - { - Id = "Order/12", - OrderDate = new DateTime(2015, 06, 01), - Customer = new Person { FirstName = "Nate", LastName = "Zaugg" } - }; - var ms = new MemoryStream(); - string expected = $"Id,OrderDate,Customer.FirstName,Customer.LastName,Subtotal,Tax,Total\r\nOrder/12,{new DateTime(2015, 06, 01)},Nate,Zaugg,0,0,0\r\n"; - - string actual; - - // Act - serializer.Serialize(ms, order); - actual = Encoding.UTF8.GetString(ms.ToArray()); - - // Assert - Assert.AreEqual(expected, actual); - } - - [TestMethod] - public void FlattenCollectionsAsColumns() - { - // Arrange - var serializer = new Serializer(); - var order = new Order - { - Id = "Order/12", - OrderDate = new DateTime(2015, 06, 01), - Customer = new Person { FirstName = "Nate", LastName = "Zaugg" }, - Subtotal = 300, - Tax = 22, - Total = 322 - }; - order.Add(new OrderItem { Id = "1", Name = "Galaxy S5", ShortDescription = "My phone is nice!", Qty = 1, PricePerQty = 200, LineTotal = 200 }); - order.Add(new OrderItem { Id = "2", Name = "Xoom Tablet", ShortDescription = "I like Xoom tab", Qty = 1, PricePerQty = 100, LineTotal = 100 }); - - var ms = new MemoryStream(); - string expected = $"Id,OrderDate,Customer.FirstName,Customer.LastName,Subtotal,Tax,Total,Items1.Name,Items1.ShortDescription,Items1.PricePerQty,Items1.Qty,Items1.LineTotal,Items1.TimeShipped,Items1.DiscountAmount,Items2.Name,Items2.ShortDescription,Items2.PricePerQty,Items2.Qty,Items2.LineTotal,Items2.TimeShipped,Items2.DiscountAmount\r\nOrder/12,{new DateTime(2015, 06, 01)},Nate,Zaugg,300,22,322,Galaxy S5,My phone is nice!,200,1,200,,,Xoom Tablet,I like Xoom tab,100,1,100,,\r\n"; - - string actual; - - // Act - serializer.Serialize(ms, order); - actual = Encoding.UTF8.GetString(ms.ToArray()); - - // Assert - Assert.AreEqual(expected, actual); - } - - [TestMethod] - public void AssertNumberOfRowsDoesNotChangeWithType() - { - // Note: We need to serialize base on the declairing type rather than the reflected type. - // Example: Two classes: Person -> Parent. If we have IEnumerable and one is a parent, - // then we need to serialize the same way (Person) for all objects. - } - - [TestMethod] - public void AssertOutputFormatterWorkingCorrectly() - { - } - - [TestMethod] - public void AssertChildObjectsSerialize() - { - } - - [TestMethod] - public void AssertChildObjectsNamedCorrectly() - { - } - - [TestMethod] - public void TestSingleObjectEnumeration() - { - } - - [TestMethod] - public void TestEnumerableObjectEnumeration() - { - } - - [TestMethod] - public void TestEnumValuesSerializeCorrectly() - { - } - - [TestMethod] - public void TestInfinateRecursionNotHappening() - { - // Note: Parent <---> Child is always a bad idea! But we should make sure we don't be stupid because someone else is - } - - [TestMethod] - public void AssertNullableValuesSerialize() - { - // Arrange - var serializer = new Serializer(); - var order = new Order - { - Id = "Order/13", - OrderDate = new DateTime(2017, 04, 07), - Customer = new Person { FirstName = "Zach", LastName = "Thurston" }, - Subtotal = 935, - Tax = 21, - Total = 956 - }; - order.Add(new OrderItem { Id = "1", Name = "iPhone 7", ShortDescription = "128 GB Jet Black", Qty = 1, PricePerQty = 850, LineTotal = 775, DiscountAmount = 75, TimeShipped = new DateTime(2017, 04, 07, 12, 3, 00) }); - order.Add(new OrderItem { Id = "2", Name = "AirPods", ShortDescription = "Wireless earbuds", Qty = 1, PricePerQty = 160, LineTotal = 160 }); - - var ms = new MemoryStream(); - var sb = new StringBuilder(); - sb.Append("Id,OrderDate,Customer.FirstName,Customer.LastName,Subtotal,Tax,Total,"); - sb.Append("Items1.Name,Items1.ShortDescription,Items1.PricePerQty,Items1.Qty,Items1.LineTotal,Items1.TimeShipped,Items1.DiscountAmount,"); - sb.Append("Items2.Name,Items2.ShortDescription,Items2.PricePerQty,Items2.Qty,Items2.LineTotal,Items2.TimeShipped,Items2.DiscountAmount\r\n"); - sb.Append($"Order/13,{new DateTime(2017, 04, 07)},Zach,Thurston,935,21,956,"); - sb.Append($"iPhone 7,128 GB Jet Black,850,1,775,{new DateTime(2017, 04, 07, 12, 3, 00)},75,"); - sb.Append("AirPods,Wireless earbuds,160,1,160,,\r\n"); - - string actual; - string expected = sb.ToString(); - - // Act - serializer.Serialize(ms, order); - actual = Encoding.UTF8.GetString(ms.ToArray()); - - // Assert - Assert.AreEqual(expected, actual); - } - - [TestMethod] - public void AssertHeaderPrintedUsingJsonPropertyAttribute() - { - // Arrange - var serializer = new Serializer(); - var person = new RestPerson { FirstName = "Nate \"D\"", LastName = "Zaugg" }; - var ms = new MemoryStream(); - string expected = "first_name,last_name\r\n\"Nate \"\"D\"\"\",Zaugg\r\n"; - string actual; - - // Act - serializer.Serialize(ms, person); - actual = Encoding.UTF8.GetString(ms.ToArray()); - - // Assert - Assert.AreEqual(expected, actual); - } - - [TestMethod] - public void ThrowExceptionIfRequiredPropertyIsNull() - { - // Arrange - var serializer = new Serializer(); - var person = new RestPerson { FirstName = "Nate \"D\"" }; - var ms = new MemoryStream(); - string expected = "Cannot write a null value for property 'LastName'. Property requires a value."; - string actual; - - // Act - try + [TestClass] + public class BasicFunctionality + { + [TestMethod] + public void IntegrationTestSimpleObject() + { + // Arrange + var serializer = new Serializer(); + var person = new Person { FirstName = "Nate \"D\"", LastName = "Zaugg" }; + var ms = new MemoryStream(); + string expected = "FirstName,LastName\r\n\"Nate \"\"D\"\"\",Zaugg\r\n"; + string actual; + + // Act + serializer.Serialize(ms, person); + actual = Encoding.UTF8.GetString(ms.ToArray()); + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void IntegrationTestSimpleObjectArray() + { + // Arrange + var serializer = new Serializer(); + var people = new[] { - serializer.Serialize(ms, person); + new Person{ FirstName = "Nate \"D\"", LastName = "Zaugg" }, + new Person{ FirstName = "James\r\nTheKid", LastName = "King" }, + new Person{ FirstName = "Tiffany", LastName = "Zaugg" }, + }; + var ms = new MemoryStream(); + string expected = "FirstName,LastName\r\n\"Nate \"\"D\"\"\",Zaugg\r\nJames TheKid,King\r\nTiffany,Zaugg\r\n"; + string actual; + + // Act + serializer.Serialize(ms, people); + actual = Encoding.UTF8.GetString(ms.ToArray()); + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void AssertLinesEndWithCorrectLineEnding() + { + // Arrange + var serializer = new Serializer { Settings = new CsvSettings { NewLineDelimeter = "\n" } }; + var person = new Person { FirstName = "Nate \"D\"", LastName = "Zaugg" }; + var ms = new MemoryStream(); + string expected = "FirstName,LastName\n\"Nate \"\"D\"\"\",Zaugg\n"; + string actual; + + // Act + serializer.Serialize(ms, person); + actual = Encoding.UTF8.GetString(ms.ToArray()); + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void AssertHeaderPrintsOnlyWhenRequested() + { + // Arrange + var serializer = new Serializer { Settings = new CsvSettings { WriteHeaders = false } }; + var person = new Person { FirstName = "Nate \"D\"", LastName = "Zaugg" }; + var ms = new MemoryStream(); + string expected = "\"Nate \"\"D\"\"\",Zaugg\r\n"; + string actual; + + // Act + serializer.Serialize(ms, person); + actual = Encoding.UTF8.GetString(ms.ToArray()); + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void AssertValuesWithQuotesReceiveQuotes() + { + // Arrange + var serializer = new Serializer(); + var person = new Person { FirstName = "Nate \"D\"", LastName = "Zaugg" }; + var ms = new MemoryStream(); + string expected = "FirstName,LastName\r\n\"Nate \"\"D\"\"\",Zaugg\r\n"; + string actual; + + // Act + serializer.Serialize(ms, person); + actual = Encoding.UTF8.GetString(ms.ToArray()); + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void AssertValuesWithCamasReceiveQuotes() + { + // Arrange + var serializer = new Serializer(); + var person = new Person { FirstName = "Nate", LastName = "Dr, Zaugg" }; + var ms = new MemoryStream(); + string expected = "FirstName,LastName\r\nNate,\"Dr, Zaugg\"\r\n"; + + string actual; + + // Act + serializer.Serialize(ms, person); + actual = Encoding.UTF8.GetString(ms.ToArray()); + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void AssertValuesWithLineEndingsDoTheRightThing() + { + // Arrange + var serializer = new Serializer { Settings = new CsvSettings { RemoveLineBreaksInFields = false } }; + var person = new Person { FirstName = "Nate", LastName = "Dr\r\n Zaugg" }; + var ms = new MemoryStream(); + string expected = "FirstName,LastName\r\nNate,\"Dr\r\n Zaugg\"\r\n"; + + string actual; + + // Act + serializer.Serialize(ms, person); + actual = Encoding.UTF8.GetString(ms.ToArray()); + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void FlattenObjectAsColumns() + { + // Arrange + var serializer = new Serializer(); + var order = new Order + { + Id = "Order/12", + OrderDate = new DateTime(2015, 06, 01), + Customer = new Person { FirstName = "Nate", LastName = "Zaugg" } + }; + var ms = new MemoryStream(); + string expected = $"Id,OrderDate,Customer.FirstName,Customer.LastName,Subtotal,Tax,Total\r\nOrder/12,{new DateTime(2015, 06, 01)},Nate,Zaugg,0,0,0\r\n"; + + string actual; + + // Act + serializer.Serialize(ms, order); + actual = Encoding.UTF8.GetString(ms.ToArray()); + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void FlattenCollectionsAsColumns() + { + // Arrange + var serializer = new Serializer(); + var order = new Order + { + Id = "Order/12", + OrderDate = new DateTime(2015, 06, 01), + Customer = new Person { FirstName = "Nate", LastName = "Zaugg" }, + Subtotal = 300, + Tax = 22, + Total = 322 + }; + order.Add(new OrderItem { Id = "1", Name = "Galaxy S5", ShortDescription = "My phone is nice!", Qty = 1, PricePerQty = 200, LineTotal = 200 }); + order.Add(new OrderItem { Id = "2", Name = "Xoom Tablet", ShortDescription = "I like Xoom tab", Qty = 1, PricePerQty = 100, LineTotal = 100 }); + + var ms = new MemoryStream(); + string expected = $"Id,OrderDate,Customer.FirstName,Customer.LastName,Subtotal,Tax,Total,Items1.Name,Items1.ShortDescription,Items1.PricePerQty,Items1.Qty,Items1.LineTotal,Items1.TimeShipped,Items1.DiscountAmount,Items2.Name,Items2.ShortDescription,Items2.PricePerQty,Items2.Qty,Items2.LineTotal,Items2.TimeShipped,Items2.DiscountAmount\r\nOrder/12,{new DateTime(2015, 06, 01)},Nate,Zaugg,300,22,322,Galaxy S5,My phone is nice!,200,1,200,,,Xoom Tablet,I like Xoom tab,100,1,100,,\r\n"; + + string actual; + + // Act + serializer.Serialize(ms, order); + actual = Encoding.UTF8.GetString(ms.ToArray()); + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void AssertNumberOfRowsDoesNotChangeWithType() + { + // Note: We need to serialize base on the declairing type rather than the reflected type. + // Example: Two classes: Person -> Parent. If we have IEnumerable and one is a parent, + // then we need to serialize the same way (Person) for all objects. + } + + [TestMethod] + public void AssertOutputFormatterWorkingCorrectly() + { + } + + [TestMethod] + public void AssertChildObjectsSerialize() + { + } + + [TestMethod] + public void AssertChildObjectsNamedCorrectly() + { + } + + [TestMethod] + public void TestSingleObjectEnumeration() + { + } + + [TestMethod] + public void TestEnumerableObjectEnumeration() + { + } + + [TestMethod] + public void TestEnumValuesSerializeCorrectly() + { + } + + [TestMethod] + public void TestInfinateRecursionNotHappening() + { + // Note: Parent <---> Child is always a bad idea! But we should make sure we don't be stupid because someone else is + } + + [TestMethod] + public void AssertNullableValuesSerialize() + { + // Arrange + var serializer = new Serializer(); + var order = new Order + { + Id = "Order/13", + OrderDate = new DateTime(2017, 04, 07), + Customer = new Person { FirstName = "Zach", LastName = "Thurston" }, + Subtotal = 935, + Tax = 21, + Total = 956 + }; + order.Add(new OrderItem { Id = "1", Name = "iPhone 7", ShortDescription = "128 GB Jet Black", Qty = 1, PricePerQty = 850, LineTotal = 775, DiscountAmount = 75, TimeShipped = new DateTime(2017, 04, 07, 12, 3, 00) }); + order.Add(new OrderItem { Id = "2", Name = "AirPods", ShortDescription = "Wireless earbuds", Qty = 1, PricePerQty = 160, LineTotal = 160 }); + + var ms = new MemoryStream(); + var sb = new StringBuilder(); + sb.Append("Id,OrderDate,Customer.FirstName,Customer.LastName,Subtotal,Tax,Total,"); + sb.Append("Items1.Name,Items1.ShortDescription,Items1.PricePerQty,Items1.Qty,Items1.LineTotal,Items1.TimeShipped,Items1.DiscountAmount,"); + sb.Append("Items2.Name,Items2.ShortDescription,Items2.PricePerQty,Items2.Qty,Items2.LineTotal,Items2.TimeShipped,Items2.DiscountAmount\r\n"); + sb.Append($"Order/13,{new DateTime(2017, 04, 07)},Zach,Thurston,935,21,956,"); + sb.Append($"iPhone 7,128 GB Jet Black,850,1,775,{new DateTime(2017, 04, 07, 12, 3, 00)},75,"); + sb.Append("AirPods,Wireless earbuds,160,1,160,,\r\n"); + + string actual; + string expected = sb.ToString(); + + // Act + serializer.Serialize(ms, order); + actual = Encoding.UTF8.GetString(ms.ToArray()); + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void AssertHeaderPrintedUsingJsonPropertyAttribute() + { + // Arrange + var serializer = new Serializer(); + var person = new RestPerson { FirstName = "Nate \"D\"", LastName = "Zaugg" }; + var ms = new MemoryStream(); + string expected = "first_name,last_name\r\n\"Nate \"\"D\"\"\",Zaugg\r\n"; + string actual; + + // Act + serializer.Serialize(ms, person); + actual = Encoding.UTF8.GetString(ms.ToArray()); + + // Assert + Assert.AreEqual(expected, actual); + } + + [TestMethod] + public void ThrowExceptionIfRequiredPropertyIsNull() + { + // Arrange + var serializer = new Serializer(); + var person = new RestPerson { FirstName = "Nate \"D\"" }; + var ms = new MemoryStream(); + string expected = "Cannot write a null value for property 'LastName'. Property requires a value."; + string actual; + + // Act + try + { + serializer.Serialize(ms, person); + + // Assert + Assert.Fail("Expected CsvSerializationException to occur."); + } + catch (CsvSerializationException ex) + { + actual = ex.Message; + + // Assert + Assert.AreEqual(expected, actual); + } + } + + [TestMethod] + public void ThrowExceptionIfStringLengthExceeded() + { + // Arrange + var serializer = new Serializer(); + var person = new RestPerson { FirstName = "Christian", LastName = "Jacob" }; + var ms = new MemoryStream(); + string expected = "The field FirstName must be a string with a maximum length of 7."; + string actual; + + // Act + try + { + serializer.Serialize(ms, person); - // Assert - Assert.Fail("Expected CsvSerializationException to occur."); - } - catch (CsvSerializationException ex ) + // Assert + Assert.Fail("Expected CsvSerializationException to occur."); + } + catch (CsvSerializationException ex) { - actual = ex.Message; + actual = ex.Message; - // Assert - Assert.AreEqual(expected, actual); - } - } - } + // Assert + Assert.AreEqual(expected, actual); + } + } + } } diff --git a/src/CsvSerializerTests/TestObjects.cs b/src/CsvSerializerTests/TestObjects.cs index e200489..e7f89c2 100644 --- a/src/CsvSerializerTests/TestObjects.cs +++ b/src/CsvSerializerTests/TestObjects.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using CsvSerializer; @@ -22,6 +23,7 @@ public class RestPerson public string Id { get; set; } [JsonProperty("first_name")] + [StringLength(7)] public string FirstName { get; set; } [JsonRequired] From 921ea646057de2d126207d39851ba183e73934f4 Mon Sep 17 00:00:00 2001 From: Christian Jacob Date: Tue, 11 May 2021 21:46:47 +0200 Subject: [PATCH 4/5] Update README.md Added JsonRequiredAttribute, StringLengthAttribute and UseDataAnnotationAttributes --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 11c3b91..c8da4aa 100644 --- a/README.md +++ b/README.md @@ -60,10 +60,14 @@ The delimeter between each part of a path. E.g. "Person.Address.Line1". Default: Specifies if attribute tags like [XmlIgnore] or [XmlElement(Name="value")] should be observed. Default: true (bool) ### UseJsonAttributes -Specifies if attribute tags like [JsonIgnore] or [JsonProperty("value")] should be observed. Default: true (bool) +Specifies if attribute tags like [JsonIgnore] or [JsonProperty("value")] or [JsonRequired] should be observed. Default: true (bool) ### UseSerializerAttributes -Specifies if attribute tags like [NonSerialized] or [DataMember(Name="value")]. Default: true (bool) +Specifies if attribute tags like [NonSerialized] or [DataMember(Name="value")] should be observed. Default: true (bool) + +### UseDataAnnotationAttributes +Specifies if attribute tags like [StringLength] should be observed. Default: true (bool) + ### ConvertChildCollectionsToRows Indicates if a collection is detected as a property on the object to be serialized, the collection will be converted to rows. From 59d28542201b7302bf783e9f5317aac1858fa3a6 Mon Sep 17 00:00:00 2001 From: Christian Jacob Date: Wed, 12 May 2021 09:04:01 +0200 Subject: [PATCH 5/5] Changed back to .NET Standard 1.4 due to compatibility issues and fixed Unit Tests --- src/CsvSerializer/CsvSerializer.csproj | 2 +- src/CsvSerializerTests/BasicFunctionality.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CsvSerializer/CsvSerializer.csproj b/src/CsvSerializer/CsvSerializer.csproj index 0747dbb..8a605ff 100644 --- a/src/CsvSerializer/CsvSerializer.csproj +++ b/src/CsvSerializer/CsvSerializer.csproj @@ -1,7 +1,7 @@  - netstandard2.1 + netstandard1.4 CsvSerializer CsvSerializer 1.6.1 diff --git a/src/CsvSerializerTests/BasicFunctionality.cs b/src/CsvSerializerTests/BasicFunctionality.cs index c8d24fa..8b2fc32 100644 --- a/src/CsvSerializerTests/BasicFunctionality.cs +++ b/src/CsvSerializerTests/BasicFunctionality.cs @@ -282,9 +282,9 @@ public void AssertHeaderPrintedUsingJsonPropertyAttribute() { // Arrange var serializer = new Serializer(); - var person = new RestPerson { FirstName = "Nate \"D\"", LastName = "Zaugg" }; + var person = new RestPerson { FirstName = "Nate", LastName = "Zaugg" }; var ms = new MemoryStream(); - string expected = "first_name,last_name\r\n\"Nate \"\"D\"\"\",Zaugg\r\n"; + string expected = "first_name,last_name\r\nNate,Zaugg\r\n"; string actual; // Act @@ -300,7 +300,7 @@ public void ThrowExceptionIfRequiredPropertyIsNull() { // Arrange var serializer = new Serializer(); - var person = new RestPerson { FirstName = "Nate \"D\"" }; + var person = new RestPerson { FirstName = "Nate" }; var ms = new MemoryStream(); string expected = "Cannot write a null value for property 'LastName'. Property requires a value."; string actual;