Remigiusz ZalewskiRemigiusz Zalewski

How to EASILY read and create CSV Files in .NET

read-write-csv-files-dotnet

Introduction

line.Split(',') looks like it solves CSV parsing right up until a field contains a comma inside quotes, or an embedded newline, or a value that needs escaping - at which point it's not really parsing CSV, it's parsing "text that happens to look like CSV most of the time." CsvHelper implements the actual format properly and turns reading or writing CSV into a couple of lines instead of a hand-rolled parser you'll be patching for months.

🎬 Watch the full video here:


Reading, with types instead of strings

using var reader = new StreamReader("products.csv");
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);

var products = csv.GetRecords<Product>().ToList();

GetRecords<T>() maps each row directly to Product by matching column headers to property names - no manual int.Parse or DateTime.Parse scattered through the reading code, and no manual index-based column access that breaks the moment someone reorders a column in the source file.

Class maps for anything that doesn't match 1:1

The moment a CSV's header names don't match your class's property names, or a column needs custom parsing logic, a ClassMap takes over explicitly rather than relying on convention:

public class ProductMap : ClassMap<Product>
{
    public ProductMap()
    {
        Map(p => p.Id).Name("product_id");
        Map(p => p.Price).Name("unit_price").TypeConverter<CurrencyConverter>();
    }
}

csv.Context.RegisterClassMap<ProductMap>();

A custom TypeConverter (implementing ITypeConverter) handles anything that isn't a straightforward primitive conversion - a currency string like "$19.99" that needs stripping the symbol before parsing as decimal, for instance.

Writing follows the same shape

using var writer = new StreamWriter("export.csv");
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);

csv.WriteRecords(products);

WriteRecords handles quoting fields that contain commas or newlines automatically - the exact edge case that breaks a hand-rolled string.Join(",", ...) approach.

Streaming large files instead of loading everything into memory

GetRecords<T>() is lazy by default - it's an IEnumerable<T> that yields one row at a time as you iterate, rather than materializing the whole file into a List<T> up front:

foreach (var product in csv.GetRecords<Product>())
{
    await ProcessAsync(product);
}

For a file with a few hundred thousand rows, this is the difference between constant memory usage and loading the entire file into RAM before processing a single row - calling .ToList() on it defeats the laziness and should only happen when you genuinely need every row in memory at once.

Common pitfalls

  • Hand-rolling a Split(',') parser that breaks on quoted fields, embedded commas, or embedded newlines.
  • Calling .ToList() on GetRecords<T>() for a huge file out of habit, defeating the built-in streaming behavior.
  • Ignoring culture when parsing numbers and dates. CultureInfo.InvariantCulture avoids surprises from a machine's regional settings affecting decimal separators.
  • Not handling malformed rows. csv.Context.RegisterClassMap combined with BadDataFound / MissingFieldFound callbacks handles real-world messy CSVs more gracefully than letting an exception abort the whole read.
  • Assuming every CSV uses commas. Configuration.Delimiter handles semicolon- or tab-delimited variants some locales and export tools produce.

Key Takeaways

  • CSV parsing is a real format with quoting and escaping rules - a naive Split(',') doesn't implement it correctly.
  • GetRecords<T>() maps rows directly to typed objects by header name, and is lazy by default for memory-efficient streaming.
  • ClassMap and custom ITypeConverter implementations handle mismatched headers and non-trivial value conversions.
  • WriteRecords handles quoting and escaping automatically on the way out, avoiding the same edge cases a hand-rolled writer would miss.

Get the Full Source Code

The complete runnable solution - the class map, the custom type converter, and both the read and write examples - is available to Patreon supporters. If you want the mapping ready to adapt to your own CSV shape instead of rebuilding it from the walkthrough above, you can find it on Patreon.

Resources