Click here to Skip to main content
15,884,976 members
Articles / Web Development / ASP.NET
Tip/Trick

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

Rate me:
Please Sign up or sign in to vote.
4.61/5 (9 votes)
13 Oct 2015CPOL 33.4K   26   16
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.

C#
myList.ToDelimitedText(delimiter, includeHeader, trimTrailingNewLineIfExists);

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

C#
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:

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United Kingdom United Kingdom
Duane has worked in a commercial software development environment for 9 years, with all but three for a global fashion retailer.

He is proficient in ASP.Net, MVC, C#, HTML, CSS, JavaScript, SQL Server TSQL.

Comments and Discussions

 
SuggestionSuggestion Pin
Afzaal Ahmad Zeeshan15-Oct-15 23:14
professionalAfzaal Ahmad Zeeshan15-Oct-15 23:14 
GeneralMy vote of 5 Pin
Farhad Reza15-Oct-15 20:45
Farhad Reza15-Oct-15 20:45 
GeneralRe: My vote of 5 Pin
dibley197315-Oct-15 20:51
dibley197315-Oct-15 20:51 
SuggestionCSV is too complex for extension methods Pin
sx200814-Oct-15 10:00
sx200814-Oct-15 10:00 
GeneralRe: CSV is too complex for extension methods Pin
dibley197314-Oct-15 19:58
dibley197314-Oct-15 19:58 
SuggestionPossible addition Pin
Gennady Oster14-Oct-15 0:07
Gennady Oster14-Oct-15 0:07 
AnswerRe: Possible addition Pin
dibley197314-Oct-15 1:49
dibley197314-Oct-15 1:49 
GeneralRe: Possible addition Pin
cspitzer14-Oct-15 9:53
cspitzer14-Oct-15 9:53 
GeneralRe: Possible addition Pin
dibley197314-Oct-15 19:59
dibley197314-Oct-15 19:59 
QuestionPossible refactor Pin
Jaguire13-Oct-15 18:50
Jaguire13-Oct-15 18:50 
AnswerRe: Possible refactor Pin
dibley197313-Oct-15 20:28
dibley197313-Oct-15 20:28 
AnswerRe: Possible refactor Pin
Alexandr Stefek14-Oct-15 6:25
Alexandr Stefek14-Oct-15 6:25 
GeneralRe: Possible refactor Pin
Jaguire14-Oct-15 10:06
Jaguire14-Oct-15 10:06 
I agree that is a lot crammed into one line. I decomposed it a bit and came up with this. It works great and does qualifying too. I didn't test it with thousands of items in a list but for my inital testing it seems fairly snappy.
C#
public static class ListExtentions
{
    public static string ToCsv<T>(this List<T> list, string delimiter = ",") where T : class, new()
    {
        return list.Aggregate(string.Empty, (csv, item) => csv + item.ToCsvRow());
    }

    private static string ToCsvRow<T>(this T item)
    {
        return item.GetType()
                   .GetProperties(BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.Public)
                   .Aggregate(string.Empty, (row, property) => row == string.Empty ? property.GetCsvValue(item) : $"{row},{property.GetCsvValue(item)}")
                   + Environment.NewLine;
    }

    private static string GetCsvValue<T>(this PropertyInfo property, T item)
    {
        return $"\"{property.GetValue(item).ToString().Replace("\"", string.Empty)}\"";
    }
}

I see what you mean about getting property info outside the aggregate. Prereflect. lol
GeneralRe: Possible refactor Pin
GerVenson20-Oct-15 22:04
professionalGerVenson20-Oct-15 22:04 
GeneralRe: Possible refactor Pin
dibley197321-Oct-15 2:37
dibley197321-Oct-15 2:37 
AnswerRe: Possible refactor Pin
John Brett14-Oct-15 21:44
John Brett14-Oct-15 21:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.