Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

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

0.00/5 (No votes)
13 Oct 2015 1  
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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here