Converting a list to a CSV file in C# is a common requirement in software development, especially when exporting data for reports or data sharing. This article provides a step-by-step tutorial on how to convert a list to a CSV file in C#, first with plain .NET code and then with the CsvHelper library. The examples target .NET 6 or later.
Why Convert a List to a CSV File?
CSV (Comma-Separated Values) is a widely used file format for storing tabular data. It's lightweight, human-readable, and easy to process programmatically. Exporting lists to CSV in C# is particularly useful for applications that deal with:
- Data reporting.
- Data interchange between systems.
- Archiving structured information.
Method 1: C# List to CSV File Without a Library
To convert a list to a CSV file in C# without extra packages, we will:
- Create a sample list of objects.
-
Use
StringBuilderto generate CSV content. - Write the CSV content to a file.
Here is the step-by-step implementation.
Step 1: Define a Sample Data Model
The model below has three public properties. Each property becomes one CSV column.
The two string properties are initialized
with string.Empty so the compiler doesn't
warn about non-nullable properties when nullable reference types are enabled,
which is the default in new .NET 6+ projects.
public class Person
{
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
public string Email { get; set; } = string.Empty;
}
Step 2: Create a List of Objects
List<Person> people = new List<Person>
{
new Person { Name = "Nikunj Satasiya", Age = 30, Email = "nikunj.satasiya@example.com" },
new Person { Name = "Mansi Satasiya", Age = 29, Email = "mansi.satasiya@example.com" },
new Person { Name = "Hiren Dobariya", Age = 31, Email = "hiren.dobariya@example.com" }
};
Step 3: Convert the List to CSV Format
The helper below uses reflection to read the public properties of any type,
writes the property names as the header row, and then writes one row per item.
It lives in a static class so it can be
called from anywhere.
The snippets in this article rely on the implicit
using directives that are enabled by default
in .NET 6+ console projects (for example
System.Linq,
System.IO and
System.Collections.Generic). If your project
has implicit usings turned off, add those namespaces yourself.
If you use a top-level Program.cs, C#
requires type declarations such as Person and
CsvExporter to appear after all top-level
statements in that file. The simplest option is to place each class in its own
file.
using System.Globalization;
using System.Reflection;
using System.Text;
public static class CsvExporter
{
private static readonly char[] SpecialCharacters = { ',', '"', '\r', '\n' };
public static string ConvertListToCsv<T>(IEnumerable<T> items)
{
ArgumentNullException.ThrowIfNull(items);
var csvBuilder = new StringBuilder();
var properties = typeof(T)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.GetGetMethod() != null && p.GetIndexParameters().Length == 0)
.ToArray();
csvBuilder.AppendLine(string.Join(",", properties.Select(p => Escape(p.Name))));
foreach (var item in items)
{
if (item == null)
{
continue;
}
var row = properties.Select(p =>
Escape(Convert.ToString(p.GetValue(item), CultureInfo.InvariantCulture)));
csvBuilder.AppendLine(string.Join(",", row));
}
return csvBuilder.ToString();
}
private static string Escape(string? value)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
if (value.IndexOfAny(SpecialCharacters) >= 0)
{
return "\"" + value.Replace("\"", "\"\"") + "\"";
}
return value;
}
}
A few details in this code are worth understanding. The first line of output is the header, built from the property names. Each following line is one item, with every property value converted to text. Null values become empty fields.
The Escape method follows the usual CSV
quoting rules. A field that contains a comma, a double quote or a line break
is wrapped in double quotes, and any double quote inside it is doubled.
Without this step, a value such as
Smith, John would be split into two columns.
Values are converted with
CultureInfo.InvariantCulture. Otherwise a
decimal like 1.5 could be written as
1,5 on a machine with a European culture,
which would break the column layout.
The helper reads only public instance properties that have a public getter.
The
BindingFlags.Public | BindingFlags.Instance
argument and the Where filter enforce this.
Fields, static properties, write-only properties and indexers are skipped,
because reading an indexer or a property without a getter would throw an
exception.
Complex values such as nested objects are written using their
ToString() result, which for most classes is
just the type name. For example, an
Address property would appear as
MyApp.Address. For those cases, map the data
to a flat model first.
The properties are read from the compile-time type
T, not from each object's runtime type. For
example, a List<object> produces no
columns, and a sequence typed as an interface includes only that interface's
properties.
A null sequence throws an
ArgumentNullException, while
null items inside the sequence are skipped.
Reflection also doesn't guarantee a property order. In practice it is normally
the declaration order, but if column order matters, don't rely on it.
Finally, AppendLine uses the platform's
newline (CRLF on Windows, LF on Linux and macOS). Nearly all CSV readers
accept either. RFC 4180 specifies CRLF, so if you need strict compliance on
every platform, append "\r\n" explicitly
instead of using AppendLine.
Step 4: Save the CSV Content to a File
using System.Text;
string csvContent = CsvExporter.ConvertListToCsv(people);
File.WriteAllText("people.csv", csvContent, new UTF8Encoding(true));
Console.WriteLine("CSV file created successfully.");
The file is written as UTF-8 with a byte order mark (BOM). The BOM helps Excel
detect the encoding so that non-English characters display correctly. Some
other tools may show the BOM as an extra character at the start of the file,
so omit it (use new UTF8Encoding(false)) if
the file is meant for systems rather than Excel users.
A relative path such as "people.csv" is
created in the application's current working directory. When you run from
Visual Studio, that is usually the output folder (for example
bin/Debug/net8.0). When you use
dotnet run, it is usually the project folder.
Use a full path if you need the file somewhere specific.
The resulting people.csv looks like this:
Name,Age,Email
Nikunj Satasiya,30,nikunj.satasiya@example.com
Mansi Satasiya,29,mansi.satasiya@example.com
Hiren Dobariya,31,hiren.dobariya@example.com
Method 2: Convert a List to CSV Using CsvHelper
If you prefer using a third-party library, here's how to do it with CsvHelper. It takes care of quoting, escaping and formatting for you.
Step 1: Install CsvHelper
Run the following command in the NuGet Package Manager Console:
Install-Package CsvHelper
Or, if you use the .NET CLI, run
dotnet add package CsvHelper in your project
folder.
Step 2: Write the CSV File
using CsvHelper;
using System.Globalization;
using System.IO;
using System.Text;
using (var writer = new StreamWriter("people.csv", false, new UTF8Encoding(true)))
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(people);
}
Console.WriteLine("CSV file created successfully using CsvHelper.");
WriteRecords writes a header row from the
property names, followed by one row per item. Passing
CultureInfo.InvariantCulture keeps number and
date formatting consistent regardless of the machine's regional settings. For
async code, CsvHelper also provides
WriteRecordsAsync.
Note that a plain StreamWriter writes UTF-8
without a BOM. The example above passes
new UTF8Encoding(true) so the output matches
the manual approach and opens correctly in Excel. Remove it if you don't want
a BOM.
Optional: Protect Against CSV Injection
Recent versions of CsvHelper can guard against formula injection through the
InjectionOptions setting. Older versions used
a SanitizeForInjection setting instead. The
protection is off by default. To enable it, pass a
CsvConfiguration to the writer:
using CsvHelper;
using CsvHelper.Configuration;
using System.Globalization;
using System.IO;
using System.Text;
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
InjectionOptions = InjectionOptions.Escape
};
using (var writer = new StreamWriter("people.csv", false, new UTF8Encoding(true)))
using (var csv = new CsvWriter(writer, config))
{
csv.WriteRecords(people);
}
The available options are None (the default),
Escape,
Strip and
Exception.
Escape follows the OWASP recommendations.
Test it with your own data before relying on it, because legitimate values
such as negative numbers also start with -.
C# Generate CSV from List
This example shows how to generate a CSV string from a list, using the
CsvExporter helper from Method 1, for
scenarios where you don't need to save a file but want to use the CSV data
programmatically:
string csvContent = CsvExporter.ConvertListToCsv(people);
Console.WriteLine(csvContent);
A CSV string is useful when the data goes somewhere other than the disk. For example, you can send it as an email attachment, store it in a database or cloud storage, or return it from a web endpoint as a downloadable file.
C# Convert IEnumerable to CSV
The ConvertListToCsv method already accepts
IEnumerable<T>, and
List<T> implements that interface. This
means the same method works for arrays, LINQ query results and any other
sequence, with no changes:
IEnumerable<Person> adults = people.Where(p => p.Age >= 30);
string adultsCsv = CsvExporter.ConvertListToCsv(adults);
Console.WriteLine(adultsCsv);
The method enumerates the sequence once, so a lazy LINQ query is evaluated at that point.
Plain C# or CsvHelper: Which Should You Use?
For smaller projects, a straightforward approach without additional libraries
may be sufficient. The StringBuilder method
we used earlier is lightweight and works well for flat objects with simple
values.
If you need custom column names, a specific column order, custom date or number formats, or nested objects, a library such as CsvHelper will save you from rebuilding that logic yourself. It is also usually the safer choice for larger or more complex exports.
Export List to CSV in C#: Best Practices
-
Handle Special Characters: Fields containing commas, quotes or line
breaks must be enclosed in quotes, with inner quotes doubled. The
Escapemethod above does this, and CsvHelper does it automatically. - Validate Data: Check for null or invalid values before writing. In the helper above, null values are written as empty fields and null items in the list are skipped.
-
Watch Out for Formula Injection: If the data comes from users and the
file will be opened in Excel, text values starting with
=,+,-or@can be interpreted as formulas. OWASP also lists a leading tab or carriage return. Sanitize such text values, for example by following the OWASP guidance on CSV injection. Apply this to text columns only, because a legitimate negative number also starts with-. If you use CsvHelper, itsInjectionOptionssetting can do this for you, as shown in the CsvHelper section above. -
Mind Encoding and Culture: Use UTF-8 (with a BOM if Excel users are
your audience) and invariant culture for numbers and dates, unless you
deliberately want locale-specific formatting. For dates, prefer an
unambiguous format such as ISO 8601 (
yyyy-MM-dd). Note that the manual helper writes aDateTimein the invariant culture's month-first format (for example01/31/2025 00:00:00), so format dates yourself if you want ISO 8601. CsvHelper's mapping options let you set a format per property. -
Consider Excel's Regional Delimiter: Excel uses the list separator
from the operating system's regional settings. In locales where that
separator is a semicolon, a comma-delimited file may open as a single
column. In that case, import it through Excel's Data > From Text/CSV
option or choose a delimiter that matches your audience. Excel also
recognizes an optional first line of
sep=,that tells it which delimiter to use. This is an Excel-specific convention: other CSV parsers will treat that line as data, and behavior can vary between Excel versions, so use it only for files meant to be opened in Excel. -
Stream Large Exports: The
StringBuilderapproach keeps the entire file in memory. For very large datasets, write rows directly to aStreamWriterinstead. -
Measure Reflection Overhead: Reflection is convenient, but reading
values with
GetValueis slower than direct property access. It is fine for small and medium lists. For high-volume exports, measure first, and if it becomes a bottleneck, write the columns explicitly or use CsvHelper. - Consider Trimming and Native AOT: The reflection-based helper is not trim-safe or Native AOT-safe. When trimming is enabled, the trimmer cannot know which properties will be read at runtime, so it may remove them, which can lead to missing columns and trim warnings at build time. If you publish with trimming or Native AOT, write the columns explicitly instead. CsvHelper also relies on reflection, so check its documentation before using it in those scenarios.
- Use a Library: For complex CSV files, consider using libraries like CsvHelper.
Conclusion
Converting a list to a CSV file in C# involves three steps:
- Creating a data model and populating the list.
-
Formatting the data as CSV using
StringBuilderor CsvHelper, with proper escaping. - Writing the CSV content to a file with a suitable encoding.
Whether you're creating reports, exporting data, or sharing structured
information, this is a valuable skill. This article covered two approaches: a
manual method using StringBuilder with proper
escaping, and the CsvHelper library for more complex needs.
Pick the approach that suits your project, and always check how special characters, encoding and culture are handled.
