2
\$\begingroup\$

I made a dynamic CSV serializer as a learning challenge a while back, I'm hoping to get my code picked apart so I can learn what I can do better.

This is a bit long, so I wouldn't expect anyone to take the time to pick apart the entire thing. If it helps, here is the github page for a download with some instructions.

This is similar to: Dynamic CSV Formatter where I got some code review on an earlier version.

public class CSVSerializer<T>
{
    /// <summary>
    /// Formats and Writes a CSV to your path
    /// </summary>
    /// <param name="path">path and name of your file</param>
    /// <param name="input">Input collection of objects to format</param>
    public void WriteCSV(string path, ICollection<T> input)
    {
        string CSVString;
        CSVString = GetCSVString(input);

        StreamWriter writer = new StreamWriter(path);
        writer.Write(CSVString);
        writer.Flush();
        writer.Close();
    }

    /// <summary>
    /// Formats and Writes a CSV to your path
    /// </summary>
    /// <param name="path">path and name of your file</param>
    /// <param name="input">Input collection of objects to format</param>
    /// <param name="collumnNames">Collection of Column names that matches the names of your properties. Is not case or white space sensative</param>
    public void WriteCSV(string path, ICollection<T> input, ICollection<string> columnNames) 
    {
        string CSVString;
        CSVString = GetCSVString(input, columnNames);

        StreamWriter writer = new StreamWriter(path);
        writer.Write(CSVString);
        writer.Flush();
        writer.Close();
    }

    /// <summary>
    /// Formats and Writes a CSV to your path
    /// </summary>
    /// <param name="path">path and name of your file</param>
    /// <param name="input">Input collection of objects to format</param>
    /// <param name="columnNames">Collection of CustomHeaders that specify what you want your headers to be named</param>
    public void WriteCSV(string path, ICollection<T> input, ICollection<CustomHeader> columnNames)
    {
        string CSVString;
        CSVString = GetCSVString(input, columnNames);

        StreamWriter writer = new StreamWriter(path);
        writer.Write(CSVString);
        writer.Flush();
        writer.Close();
    }

    /// <summary>
    /// Returns a collection of strings, each a CSV row
    /// </summary>
    /// <param name="input"> Input collection of objects to format</param>
    /// <returns></returns>
    public List<string> GetCSVRows(ICollection<T> input)
    {
        List<List<string>> dataStrings = GetCSVDataStrings(input);
        List<string> output = new List<string>();

        foreach (List<string> row in dataStrings)
        {
            output.Add(FormatCSVRow(row, false));
        }
        return output;
    }

    /// <summary>
    /// Returns a collection of strings, each a CSV row. Only returns the collumns you specificed
    /// </summary>
    /// <param name="input"> Input collection of objects to serialize</param>
    /// <param name="columnNames"> Collection of Column names that matches the names of your properties. Is not case or white space sensative</param>
    /// <returns></returns>
    public List<string> GetCSVRows(ICollection<T> input, ICollection<string> columnNames)
    {
        List<List<string>> dataStrings = GetCSVDataStrings(input, columnNames);
        List<string> output = new List<string>();

        foreach (List<string> row in dataStrings)
        {
            output.Add(FormatCSVRow(row, false));
        }
        return output;
    }

    /// <summary>
    /// Returns a collection of strings, each a CSV row. Only returns the collumns you specificed
    /// </summary>
    /// <param name="input">Input collection of objects to serialize</param>
    /// <param name="columnNames">Collection of CustomHeaders that specify what you want your headers to be named</param>
    /// <returns></returns>
    public List<string> GetCSVRows(ICollection<T> input, ICollection<CustomHeader> columnNames)
    {
        List<List<string>> dataStrings = GetCSVDataStrings(input, columnNames);
        List<string> output = new List<string>();

        foreach (List<string> row in dataStrings)
        {
            output.Add(FormatCSVRow(row, false));
        }
        return output;
    }

    /// <summary>
    /// Returns a single string formatted as a CSV based on your input objects
    /// </summary>
    /// <param name="input">Input collection of objects to serialize</param>
    /// <returns></returns>
    public string GetCSVString(ICollection<T> input)
    {
        List<List<string>> dataStrings = GetCSVDataStrings(input);
        string output = "";

        foreach (List<string> row in dataStrings)
        {
            output += FormatCSVRow(row, true);
        }
        return output;
    }

    /// <summary>
    /// Returns a single string formatted as a CSV based on your input objects
    /// </summary>
    /// <param name="input">Input collection of objects to serialize</param>
    /// <param name="collumnNames">Collection of Column names that matches the names of your properties. Is not case or white space sensative</param>
    /// <returns></returns>
    public string GetCSVString(ICollection<T> input, ICollection<string> columnNames)
    {
        List<List<string>> dataStrings = GetCSVDataStrings(input, columnNames);
        string output = "";

        foreach (List<string> row in dataStrings)
        {
            output += FormatCSVRow(row, true);
        }
        return output;
    }

    /// <summary>
    /// Returns a single string formatted as a CSV based on your input objects
    /// </summary>
    /// <param name="input">Input collection of objects to serialize</param>
    /// <param name="columnNames">Collection of CustomHeaders that specify what you want your headers to be named</param>
    /// <returns></returns>
    public string GetCSVString(ICollection<T> input, ICollection<CustomHeader> columnNames)
    {
        List<List<string>> dataStrings = GetCSVDataStrings(input, columnNames);
        string output = "";

        foreach (List<string> row in dataStrings)
        {
            output += FormatCSVRow(row, true);
        }
        return output;
    }

    #region Property Retrieval
    //Retrieves a 2-Dimensional array of strigns that represent the input classes
    private List<List<string>> GetCSVDataStrings(ICollection<T> input, ICollection<string> headers = null)
    {
        List<List<string>> output = new List<List<string>>();
        List<string> validHeaders = new List<string>();
        List<ValidType> properties;
        if (headers != null)
        {
            List<string> cleanHeaders = CleanStringOfCaseAndSpace(headers).ToList();
            properties = SortProperties(FilterProperties(new List<PropertyInfo>(typeof(T).GetProperties()), cleanHeaders), cleanHeaders);
            validHeaders = FilterColumnHeaders(properties, cleanHeaders, headers);
        }
        else
        {
            properties = FilterProperties(new List<PropertyInfo>(typeof(T).GetProperties()));
        }

        if (properties.Count != 0)
        {
            if (headers == null)
            {
                output.Add(GetHeaders(properties));
            }
            else
            {
                output.Add(validHeaders);
            }

            foreach (T item in input)
            {
                output.Add(GetDataRowAsStrings(item, properties));
            }
            return output;
        }
        else
        {
            throw new ArgumentException("There was no valid input to format as a CSV");
        }
    }

    //Retrieves a 2-dimensional array of strings that represents the input classes, rpeserving the header names specified by the user
    private List<List<string>> GetCSVDataStrings(ICollection<T> input, ICollection<CustomHeader> headers)
    {
        List<List<string>> output = new List<List<string>>();

        List<ValidType> properties;
        List<string> cleanHeaders = new List<string>();
        List<string> customHeaders = new List<string>();
        foreach (CustomHeader header in headers)
        {
            cleanHeaders.Add(CleanStringOfCaseAndSpace(header.HeaderPropertyName));
            customHeaders.Add(MakeStringSafe(CleanString(header.HeaderOutputName)));
        }

        //Retrieves and sorts properties that have column names, if the property doesn't have a column name it's filtered out
        properties = SortProperties(FilterProperties(new List<PropertyInfo>(typeof(T).GetProperties()), cleanHeaders), cleanHeaders);

        //Filters out headers that don't have matching properties
        customHeaders = FilterColumnHeaders(properties, headers);

        if (properties.Count != 0)
        {
            output.Add(customHeaders);
            foreach (T item in input)
            {
                output.Add(GetDataRowAsStrings(item, properties));
            }
            return output;
        }
        else
        {
            throw new ArgumentException("There was no valid input to format as a CSV");
        }



    }


    //Converts each type T into a list of  based on it's data
    private List<string> GetDataRowAsStrings(T input, List<ValidType> properties)
    {
        List<string> output = new List<string>();
        foreach (ValidType property in properties)
        {
            if (!property.IsCollection)
            {
                if(property.PropertyInformation.GetValue(input) != null)
                {
                    output.Add(MakeStringSafe(CleanString(property.PropertyInformation.GetValue(input).ToString())));
                }
                else
                {
                    output.Add("");
                }
            }
            else
            {
                if(property.PropertyInformation.GetValue(input) != null)
                {
                    string workingString = FormatMultiItemCSVCell(GetStringDataFromGenericCollection(property.PropertyInformation, input));
                    output.Add(MakeStringSafe(CleanString(workingString)));
                }
                else
                {
                    output.Add("");
                }
            }
        }
        return output;
    }

    //Filters out non accpted Types
    private List<ValidType> FilterProperties(List<PropertyInfo> properties, ICollection<string> cleanColumnNames = null)
    {
        List<ValidType> output = new List<ValidType>();

        foreach (PropertyInfo property in properties)
        {
            ValidType validType;
            if (cleanColumnNames != null) //If there are specific collumn names only add properties that match those names
            {
                if (PropertyNameExistsInArray(property, cleanColumnNames))
                {
                    if (CheckForValidProperty(property, out validType))
                    {
                        validType.IsForcedTry = false;
                        output.Add(validType);
                    }
                    else
                    {
                        validType.IsForcedTry = true;
                        output.Add(validType);
                    }
                }
            }
            else
            {
                if (CheckForValidProperty(property, out validType))
                {
                    output.Add(validType);
                }
            }
        }

        return output;
    }

    //Sorts the properties by the collumn name orders
    private List<ValidType> SortProperties(List<ValidType> properties, ICollection<string> cleanColumnNames)
    {
        foreach(ValidType property in properties)
        {
            for (int i = 0; i < cleanColumnNames.Count; i++)
            {
                if(string.Compare(property.PropertyInformation.Name, cleanColumnNames.ElementAt(i), StringComparison.OrdinalIgnoreCase) == 0)
                {
                    property.SortOrder = i;
                }
            }
        }
        List<ValidType> output = properties.OrderBy(o => o.SortOrder).ToList();
        return output;
    }

    private List<string> FilterColumnHeaders(List<ValidType> properties, ICollection<string> cleanCustomHeaders, ICollection<string> customHeaders)
    {
        List<string> validcolumnNames = new List<string>();
        foreach (ValidType property in properties)
        {
            for(int i = 0; i < customHeaders.Count; i++)
            {
                if (string.Compare(property.PropertyInformation.Name, cleanCustomHeaders.ElementAt(i), StringComparison.OrdinalIgnoreCase) == 0)
                {
                    validcolumnNames.Add(customHeaders.ElementAt(i));
                }
            }
        }
        return validcolumnNames;
    }

    private List<string> FilterColumnHeaders(List<ValidType> properties, ICollection<CustomHeader> cleanCustomHeaders)
    {
        List<string> validcolumnNames = new List<string>();
        foreach(ValidType property in properties)
        {
            foreach(CustomHeader header in cleanCustomHeaders)
            {
                if(string.Compare(property.PropertyInformation.Name, header.HeaderPropertyName, StringComparison.OrdinalIgnoreCase) == 0)
                {
                    validcolumnNames.Add(header.HeaderOutputName);
                }
            }
        }
        return validcolumnNames;
    }

    //Checks if the property is of an acceptable type and outputs a ValidType. Only gets sent properties that exist in the collumn names
    private bool CheckForValidProperty(PropertyInfo property, out ValidType validType)
    {
        if (property.PropertyType.IsGenericType)
        {
            Type interfaceType = property.PropertyType.GetInterface(typeof(ICollection<>).Name);
            if (interfaceType != null)
            {
                if (interfaceType.Name == typeof(ICollection<>).Name)
                {
                    if (property.PropertyType.GenericTypeArguments[0].IsPrimitive || property.PropertyType.GenericTypeArguments[0] == typeof(string))
                    {
                        validType = new ValidType(true, property);
                        return true;
                    }
                }
            }
        }
        else if (property.PropertyType.IsPrimitive || property.PropertyType == typeof(string))
        {
            validType = new ValidType(false, property);
            return true;
        }

        validType = new ValidType(false, property);
        return false;
    }

    //Checks if the property exists in the array, ignores case and whitespaces.
    private bool PropertyNameExistsInArray(PropertyInfo property, ICollection<string> columnNames)
    {
        foreach (string name in columnNames)
        {
            if (string.Compare(property.Name, name, StringComparison.OrdinalIgnoreCase) == 0)
            {
                return true;
            }
        }
        return false;
    }

    //Cleans a list of strigns of cases and spaces. Used to lower extra load of doing it on the fly for each comparison
    private ICollection<string> CleanStringOfCaseAndSpace(ICollection<string> names)
    {
        List<string> output = new List<string>();
        foreach (string name in names)
        {
            output.Add(name.ToLower().Replace(" ", ""));
        }
        return output;
    }

    private string CleanStringOfCaseAndSpace(string input)
    {
        return input.ToLower().Replace(" ", "");
    }

    //Gets the string headers for each applicable type in T
    private List<string> GetHeaders(List<ValidType> properties)
    {
        List<string> propertyStrings = new List<string>();
        foreach (ValidType validType in properties)
        {
            propertyStrings.Add(validType.PropertyInformation.Name);
        }
        return propertyStrings;
    }

    #endregion

    //Takes a list of strings and formats them in a CSV style as a row
    private string FormatCSVRow(List<string> strings, bool lineBreaks)
    {
        string output = string.Join(",", strings);
        if (lineBreaks)
        {
            output += "\n";
        }

        return output;
    }

    private List<string> GetStringDataFromGenericCollection(PropertyInfo info, T item)
    {
        List<string> output = new List<string>();
        IEnumerable collectionObject = (IEnumerable)info.GetValue(item);
        if (collectionObject != null)
        {
            output = collectionObject.Cast<object>().Select(e => e.ToString()).ToList();
        }
        return output;
    }

    //Takes a list of strings a puts them into a single string that be a single CSV item
    private string FormatMultiItemCSVCell(ICollection<string> input)
    {
        string output = "";
        for (int i = 0; i < input.Count; i++)
        {
            if (i == input.Count - 1)
            {
                output += input.ElementAt(i);
            }
            else
            {
                output += input.ElementAt(i) + ", ";
            }
        }
        return output;
    }

    //Cleans a string of any new lines or line breaks
    private string CleanString(string input)
    {
        if (String.IsNullOrEmpty(input))
        {
            return input;
        }
        string lineSeparator = ((char)0x2028).ToString();
        string paragraphSeparator = ((char)0x2029).ToString();

        return input.Replace("\r\n", string.Empty)
                    .Replace("\n", string.Empty)
                    .Replace("\r", string.Empty)
                    .Replace("\t", string.Empty)
                    .Replace(lineSeparator, string.Empty)
                    .Replace(paragraphSeparator, string.Empty);
    }

    //Encases any comma containing strings in quotes and puts a quote infront of any in-string quote
    private string MakeStringSafe(string input)
    {
        bool containsCommasOrSemicolins = input.Contains(",") || input.Contains(";");
        bool containsQuotes = input.Contains("\"");
        string output = input;

        if (containsQuotes)
        {
            int i = 0;
            while((i = output.IndexOf('"', i)) != -1)
            {
                output = output.Insert(i, "\"");
                i+= 3;
                if(i-1 >= output.Length)
                {
                    break;
                }
            }

            return "\"" + output + "\"";
        }
        else if (containsCommasOrSemicolins)
        {
            return "\"" + output + "\"";
        }


        return output;
    }
}

Custom Header Class:

/// <summary>
/// Holds data regarding your custom header text
/// </summary>
public class CustomHeader
{
    public CustomHeader(string headerPropertyName, string headerOutputName)
    {
        HeaderPropertyName = headerPropertyName;
        HeaderOutputName = headerOutputName;
    }
    /// <summary>
    /// The name of your property as it appears in your class
    /// </summary>
    public string HeaderPropertyName { get; set; }
    /// <summary>
    /// The name you wish to see in the output
    /// </summary>
    public string HeaderOutputName { get; set; }
}

Valid Type Class:

/// <summary>
/// Encases internal information regarding the type of a property
/// </summary>
internal class ValidType
{
    public ValidType(bool isCollection, PropertyInfo propertyInfo)
    {
        IsCollection = isCollection;
        PropertyInformation = propertyInfo;
    }

    public bool IsCollection { get; set; }
    /// <summary>
    /// If the type is not supported, but is included as a column header by the user
    /// </summary>
    public bool IsForcedTry { get; set; }
    public PropertyInfo PropertyInformation { get; set; }
    public int SortOrder { get; set; }
}

A copy/pasteable console application to quickly run:

class Program
{
    static void Main(string[] args)
    {

        List<TestObject> testList = new List<TestObject>();
        CSVSerializer<TestObject> serializer = new CSVSerializer<TestObject>();

        for (int i = 0; i < 50; i++)
        {
            TestObject test = new TestObject()
            {
                Calls = 90,
                Name = "Fredrick",
                Times = new List<int>() { 1, 2, 3, 4 }
            };

            testList.Add(test);
        }

        List<string> output = serializer.GetCSVRows(testList);
        string testPaste = "";
        using (StreamWriter writer = new StreamWriter("testoutput.csv"))
        {
            foreach (string item in output)
            {
                writer.WriteLine(item);
                writer.Flush();
                testPaste += item + "\n\b";
                Console.WriteLine(item);
            }
            writer.Close();
        }

        Console.ReadLine();
    }
}

public class TestObject
{
    public int Calls { get; set; }
    public string Name { get; set; }
    public List<int> Times { get; set; }
    public List<string> TestEmptyCollection { get; set; }
    public CSVSerializer<TestObject> TestNonSupportedGeneric { get; set; }
}
\$\endgroup\$
2
\$\begingroup\$

I would recommend using TypeDescriptor instead of directly enumerating PropertyInfo. It helps defining columns at run-time.

\$\endgroup\$
  • \$\begingroup\$ I looked into TypeDescriptor just now and can't see what it would change, I don't think I fully understand what TypeDescriptor can do. Would this change the behavior of the program, the performance, or just make it easier to write/read? \$\endgroup\$ – Douglas Gaskell Jan 28 '16 at 5:53
  • \$\begingroup\$ TypeDescriptor does the same thing your code does - reads PropertyInfo from the reflection API to expose them as PropertyDescriptor array. It just provides an additional indirection level - types might implement ICustomTypeDescriptor and intercept control deciding on the list of properties at run-time. It is a little bit old school, but still in use. \$\endgroup\$ – Dmitry Nogin Jan 28 '16 at 5:59
  • \$\begingroup\$ So if I understand you correctly, an example where TypeDescripter would be useful is when properties are added to a Type at runtime? I'm wondering if my code would miss properties of an object that implements ICustomTypeDescriptor, if so then this is definitely something I should be moving over too. If not, I'll probably look into it when I make a deserializer. \$\endgroup\$ – Douglas Gaskell Jan 28 '16 at 6:05
  • \$\begingroup\$ Yep, ICustomTypeDescriptor overrides reflection information. Here is a good article. \$\endgroup\$ – Dmitry Nogin Jan 28 '16 at 6:12
  • \$\begingroup\$ It is usually used when you defining data binding source with dynamic column set for the grid control in WPF/WinForms/ASP.NET. You might be interested to serialize such kind of things. \$\endgroup\$ – Dmitry Nogin Jan 28 '16 at 6:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.