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()
{
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;
string result = itemList.ToDelimitedText
(delimiter, includeHeader, trimTrailingNewLineIfExists);
var lines = result.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
var actualRowCount = lines.Length;
Assert.AreEqual(expectedRowCount, actualRowCount);
}
[TestMethod]
public void ToDelimitedText_IncludesHeaderRow_WhenSet()
{
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";
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.AreEqual(expectedRowCount, actualRowCount);
Assert.AreEqual(expectedHeader, actualFirstRow);
}
[TestMethod]
public void ToDelimitedText_ReturnsCorrectNumberOfProperties()
{
var itemList = new List<ComplexObject>
{
new ComplexObject {Id = 1, Name = "Sid", Active = true}
};
const string delimiter = ",";
const int expectedPropertyCount = 3;
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.AreEqual(expectedPropertyCount, actualPropertyCount);
}
[TestMethod]
public void ToDelimitedText_RemovesTrailingNewLine_WhenSet()
{
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;
string result = itemList.ToDelimitedText
(delimiter, includeHeader,trimTrailingNewLineIfExists);
bool endsWithNewLine = result.EndsWith(Environment.NewLine);
Assert.IsFalse(endsWithNewLine);
}
[TestMethod]
public void ToDelimitedText_IncludesTrailingNewLine_WhenNotSet()
{
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 = ",";
string result = itemList.ToDelimitedText(delimiter);
bool endsWithNewLine = result.EndsWith(Environment.NewLine);
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
{
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