65.9K
CodeProject is changing. Read more.
Home

C# Extension Method to Convert List into a Delimited Text String

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.61/5 (9 votes)

Oct 13, 2015

CPOL
viewsIcon

34903

A simple C# extension method to convert List into a delimited text string. Ideal for creating CSV files!

Introduction

This tip shows you a quick C# extension method which can be called to convert a list of objects into a delimited text string. This may be ideal for converting a list of objects into a string for a CSV file with a line per object and a field per public property.

Background

I needed to use this today, so I wanted to share with anyone who could also make use of it.

Using the Code

You call the extension like this and it returns a string formatted with line and field delimiters.

myList.ToDelimitedText(delimiter, includeHeader, trimTrailingNewLineIfExists);

I have compiled a short suite of tests to show the extension method in use...

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Gists.Extensions.ListOfTExtentions;

namespace Gists_Tests.ExtensionTests.ListOfTExtentionTests
{
    [TestClass]
    public class ListOfT_ToDelimitedTextTests
    {
        #region Mock Data

        private class ComplexObject
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public bool Active { get; set; }
        }

        #endregion

        #region Tests

        [TestMethod]
        public void ToDelimitedText_ReturnsCorrectNumberOfRows()
        {
            // ARRANGE
            var itemList = new List<ComplexObject>
            {
                new ComplexObject {Id = 1, Name = "Sid", Active = true},
                new ComplexObject {Id = 2, Name = "James", Active = false},
                new ComplexObject {Id = 3, Name = "Ted", Active = true}
            };
            const string delimiter = ",";
            const int expectedRowCount = 3;
            const bool includeHeader = false;
            const bool trimTrailingNewLineIfExists = true;

            // ACT
            string result = itemList.ToDelimitedText
		(delimiter, includeHeader, trimTrailingNewLineIfExists);
            var lines = result.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
            var actualRowCount = lines.Length;

            // ASSERT
            Assert.AreEqual(expectedRowCount, actualRowCount);
        }

        [TestMethod]
        public void ToDelimitedText_IncludesHeaderRow_WhenSet()
        {
            // ARRANGE
            var itemList = new List<ComplexObject>
            {
                new ComplexObject {Id = 1, Name = "Sid", Active = true},
                new ComplexObject {Id = 2, Name = "James", Active = false},
                new ComplexObject {Id = 3, Name = "Ted", Active = true}
            };
            const string delimiter = ",";
            const int expectedRowCount = 4;
            const bool includeHeader = true;
            const bool trimTrailingNewLineIfExists = true;
            const string expectedHeader = @"Id,Name,Active";
            
            // ACT
            string result = itemList.ToDelimitedText
		(delimiter, includeHeader, trimTrailingNewLineIfExists);
            var lines = result.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
            int actualRowCount = lines.Length;
            string actualFirstRow = lines[0];
            
            // ASSERT
            Assert.AreEqual(expectedRowCount, actualRowCount);
            Assert.AreEqual(expectedHeader, actualFirstRow);
        }

        [TestMethod]
        public void ToDelimitedText_ReturnsCorrectNumberOfProperties()
        {
            // ARRANGE
            var itemList = new List<ComplexObject>
            {
                new ComplexObject {Id = 1, Name = "Sid", Active = true}
            };
            const string delimiter = ",";
            const int expectedPropertyCount = 3;

            // ACT
            string result = itemList.ToDelimitedText(delimiter);
            var lines = result.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
            var properties = lines.First().Split(delimiter.ToCharArray());
            var actualPropertyCount = properties.Length;

            // ASSERT
            Assert.AreEqual(expectedPropertyCount, actualPropertyCount);
        }

        [TestMethod]
        public void ToDelimitedText_RemovesTrailingNewLine_WhenSet()
        {
            // ARRANGE
            var itemList = new List<ComplexObject>
            {
                new ComplexObject {Id = 1, Name = "Sid", Active = true},
                new ComplexObject {Id = 2, Name = "James", Active = false},
                new ComplexObject {Id = 3, Name = "Ted", Active = true},
            };
            const string delimiter = ",";
            const bool includeHeader = false;
            const bool trimTrailingNewLineIfExists = true;

            // ACT
            string result = itemList.ToDelimitedText
		(delimiter, includeHeader,trimTrailingNewLineIfExists);
            bool endsWithNewLine = result.EndsWith(Environment.NewLine);

            // ASSERT
            Assert.IsFalse(endsWithNewLine);
        }

        [TestMethod]
        public void ToDelimitedText_IncludesTrailingNewLine_WhenNotSet()
        {
            // ARRANGE
            var itemList = new List<ComplexObject>
            {
                new ComplexObject {Id = 1, Name = "Sid", Active = true},
                new ComplexObject {Id = 2, Name = "James", Active = false},
                new ComplexObject {Id = 3, Name = "Ted", Active = true},
            };
            const string delimiter = ",";

            // ACT
            string result = itemList.ToDelimitedText(delimiter);
            bool endsWithNewLine = result.EndsWith(Environment.NewLine);

            // ASSERT
            Assert.IsTrue(endsWithNewLine);
        }

        #endregion
    }
}

And the actual extension method code can be seen below:

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;

namespace Gists.Extensions.ListOfTExtentions
{
    public static class ListOfTExtentions
    {
        /// <summary>
        /// Converts this instance to delimited text.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="instance">The instance.</param>
        /// <param name="delimiter">The delimiter.</param>
        /// <param name="includeHeader">
        /// if set to <c>true</c> then the header row is included.
        /// </param>
        /// <param name="trimTrailingNewLineIfExists">
        /// If set to <c>true</c> then trim trailing new line if it exists.
        /// </param>
        /// <returns></returns>
        public static string ToDelimitedText<T>(this List<T> instance,
            string delimiter,
            bool includeHeader = false,
            bool trimTrailingNewLineIfExists = false)
            where T : class, new()
        {
            int itemCount = instance.Count;
            if (itemCount == 0) return string.Empty;

            var properties = GetPropertiesOfType<T>();
            int propertyCount = properties.Length;
            var outputBuilder = new StringBuilder();

            AddHeaderIfRequired(outputBuilder, includeHeader, properties, propertyCount, delimiter);

            for (int itemIndex = 0; itemIndex < itemCount; itemIndex++)
            {
                T listItem = instance[itemIndex];
                AppendListItemToOutputBuilder
                (outputBuilder, listItem, properties, propertyCount, delimiter);

                AddNewLineIfRequired(trimTrailingNewLineIfExists, itemIndex, itemCount, outputBuilder);
            }

            var output = outputBuilder.ToString();
            return output;
        }

        private static void AddHeaderIfRequired(StringBuilder outputBuilder,
            bool includeHeader,
            PropertyInfo[] properties,
            int propertyCount,
            string delimiter)
        {
            if (!includeHeader) return;

            for (int propertyIndex = 0; propertyIndex < properties.Length; propertyIndex += 1)
            {
                var property = properties[propertyIndex];
                var propertyName = property.Name;
                outputBuilder.Append(propertyName);

                AddDelimiterIfRequired(outputBuilder, propertyCount, delimiter, propertyIndex);
            }
            outputBuilder.Append(Environment.NewLine);
        }

        private static void AddDelimiterIfRequired
        (StringBuilder outputBuilder, int propertyCount, string delimiter,
            int propertyIndex)
        {
            bool isLastProperty = (propertyIndex + 1 == propertyCount);
            if (!isLastProperty)
            {
                outputBuilder.Append(delimiter);
            }
        }

        private static void AddNewLineIfRequired
        (bool trimTrailingNewLineIfExists, int itemIndex, int itemCount,
            StringBuilder outputBuilder)
        {
            bool isLastItem = (itemIndex + 1 == itemCount);
            if (!isLastItem || !trimTrailingNewLineIfExists)
            {
                outputBuilder.Append(Environment.NewLine);
            }
        }

        private static void AppendListItemToOutputBuilder<T>(StringBuilder outputBuilder,
            T listItem,
            PropertyInfo[] properties,
            int propertyCount,
            string delimiter)
            where T : class, new()
        {

            for (int propertyIndex = 0; propertyIndex < properties.Length; propertyIndex += 1)
            {
                var property = properties[propertyIndex];
                var propertyValue = property.GetValue(listItem);
                outputBuilder.Append(propertyValue);

                AddDelimiterIfRequired(outputBuilder, propertyCount, delimiter, propertyIndex);
            }
        }

        private static PropertyInfo[] GetPropertiesOfType<T>() where T : class, new()
        {
            Type itemType = typeof(T);
            var properties = itemType.GetProperties
            (BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.Public);
            return properties;
        }
    }
}

History

  • 2015-10-16 Updated to include header row as per suggestion
  • 2015-10-13 Initial draft