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. 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/CsvSerializer.csproj b/src/CsvSerializer/CsvSerializer.csproj index c0db8e8..8a605ff 100644 --- a/src/CsvSerializer/CsvSerializer.csproj +++ b/src/CsvSerializer/CsvSerializer.csproj @@ -1,7 +1,7 @@  - netstandard1.3 + netstandard1.4 CsvSerializer CsvSerializer 1.6.1 @@ -31,6 +31,8 @@ + + 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/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..a489e6c 100644 --- a/src/CsvSerializer/Serializer.cs +++ b/src/CsvSerializer/Serializer.cs @@ -1,226 +1,258 @@ -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) - { - 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; - } - } -} +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +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 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 88efad3..8b2fc32 100644 --- a/src/CsvSerializerTests/BasicFunctionality.cs +++ b/src/CsvSerializerTests/BasicFunctionality.cs @@ -6,276 +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,6/1/2015 12:00:00 AM,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,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 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,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("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); - } - } - + [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", LastName = "Zaugg" }; + var ms = new MemoryStream(); + string expected = "first_name,last_name\r\nNate,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" }; + 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) + { + actual = ex.Message; + + // 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..e7f89c2 100644 --- a/src/CsvSerializerTests/TestObjects.cs +++ b/src/CsvSerializerTests/TestObjects.cs @@ -1,9 +1,11 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using CsvSerializer; +using Newtonsoft.Json; namespace CsvSerializerTests { @@ -15,6 +17,20 @@ public class Person public string LastName { get; set; } } + public class RestPerson + { + [JsonIgnore] + public string Id { get; set; } + + [JsonProperty("first_name")] + [StringLength(7)] + public string FirstName { get; set; } + + [JsonRequired] + [JsonProperty("last_name")] + public string LastName { get; set; } + } + public class Order {