Table of Contents

Examples

TL;DR

Use CsvReader to read CSV records or .NET objects, and CsvWriter to write them.

Here's the example class used throughout this documentation:

class User
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsAdmin { get; set; }
}

Customizing the CSV dialect

Configure the CSV format using these CsvOptions<T> properties:

Example for reading semicolon-delimited CSV with linefeed separators and space/tab trimming:

CsvOptions<char> options = new()
{
    Delimiter = ';',
    Quote = '"',
    Newline = "\n",
    Whitespace = " \t",
};

For more details, see Configuration. The configuration is identical between char and byte; the options-instance internally converts the UTF16 values into UTF8.

Reading objects

.NET types

Use the static CsvReader class for reading CSV data. The Read and ReadAsync methods accept various data sources and return an enumerable that works with foreach or LINQ. When options is omitted (or null), CsvOptions<T>.Default is used.

The Read and ReadAsync methods return an enumerable-object that can be used with foreach or LINQ.

The library supports both char (UTF-16) and byte (UTF-8) data through C# generics. For bytes, the library expects UTF-8 encoded text (which includes ASCII).

The synchronous methods accept common .NET data types: string, ReadOnlyMemory<T>. These types are converted internally to a ReadOnlySequence<T>, which can also be used directly.

Records can be asynchronously read in a streaming manner from TextReader, Stream, and PipeReader.

The data is read in chunks, and records are yielded by the enumerator on line-by-line basis. Internally, the library uses SIMD operations to read up to N fields ahead in the data for significantly improved performance. More info: Configuration.

// sync
string csv = "id,name,isadmin\r\n1,Bob,true\r\n2,Alice,false\r\n";
List<User> users = CsvReader.Read<User>(csv).ToList();

// async
await foreach (var user in CsvReader.ReadAsync<User>(new TextReader(csv)))
{
    ProcessUser(user);
}
Warning

The enumerator objects must be disposed after use to properly clean up internal objects and pooled buffers. This can be done implicitly by using them in a foreach, using, or LINQ statement, or explicitly with Dispose() or DisposeAsync().

CSV records and fields

The CsvReader class contains the Enumerate and EnumerateAsync methods to read CsvValueRecord<T> that wraps the CSV data, and can be used to inspect details about the records, such has field counts, unescaped record or field, line numbers, and exact character/byte offsets in the file.

Fields can be accessed directly using the CsvFieldIdentifier-struct, which is implicitly convertible from string and int. In the example below, passing "id" and 0 to ParseField are functionally equivalent. The string-overload however includes and additional array/dictionary-lookup. The Header and FieldCount properties can be used to determine if a specific field can be accessed, along with Contains(CsvFieldIdentifier).

The CsvValueRecord<T> struct can also be used in a foreach-statement to iterate the escaped field values.

foreach (var rec in CsvReader.Enumerate(csv))
{
    Console.WriteLine("Fields: {0}", rec.FieldCount);
    Console.WriteLine("Line in file: {0}", rec.Line);
    Console.WriteLine("Start offset: {0}", rec.Position);

    // all three calls below are equivalent if using default binding options
    yield return new User
    {
        Id = rec.ParseField<int>("id"),
        Name = rec.ParseField<string?>("name"),
        IsAdmin = rec.ParseField<bool>("isadmin"),
    };

    yield return new User
    {
        Id = rec.ParseField<int>(0),
        Name = rec.ParseField<string?>(1),
        IsAdmin = rec.ParseField<bool>(2),
    };

    yield return rec.ParseRecord<User>();
}
Warning

A CsvValueRecord<T> instance is only valid until MoveNext() is called on the enumerator. The struct is a thin wrapper around the actual data, and may use invalid or pooled memory if used after its intended lifetime. A runtime exception will be thrown if it is accessed after the enumeration has continued or ended. See CsvRecord<T> for an alternative.

Reading raw CSV data

For advanced performance-critical scenarios, you can create a CsvParser<T> (the internal type that handles parsing the data into fields) manually by using the static CsvParser-class, and using it in a foreach or await foreach-loop. Enumerating advances the parser and disposes it at the end.

foreach (var @record in CsvParser.Create(CsvOptions<char>, textReader).ParseRecords())
{
    for (int i = 0; i < @record.FieldCount; i++)
    {
        ReadOnlySpan<char> @field = @record[i];
        ProcessField(i, @field);
    }
}

Writing

.NET types

The CsvWriter class provides methods to write .NET objects as CSV records. Supported outputs include:

You can write both IEnumerable<T> and IAsyncEnumerable<T> data.

User[] users =
[
    new User { Id = 1, Name = "Bob", IsAdmin = true },
    new User { Id = 2, Name = "Alice", IsAdmin = false },
];

CsvWriter.Write(TextWriter.Null, users);

CSV records and fields

CsvWriter includes a Create method that can be used to create an instance of CsvWriter<T> that allows you to write fields, records, or unescaped raw data directly into your output, while taking care of field quoting, delimiters, and escaping. The writer can be configured to flush automatically if the library detects that the internal buffers are getting saturated or flushed manually.

Note

As PipeWriter does not support synchronous flushing, the returned type CsvAsyncWriter<T> lacks synchronous methods that can cause a flush.

using (CsvWriter<char> writer = CsvWriter.Create(TextWriter.Null))
{
    // header can be writted manually or automatically
    writer.WriteField("id");
    writer.WriteField("name");
    writer.WriteField("isadmin");
    writer.NextRecord();

    // alternative
    writer.WriteHeader<User>();
    writer.NextRecord();

    // fields can be written one by one, or as whole records
    writer.WriteField<int>(1);
    writer.WriteField<string>("Bob");
    writer.WriteField<bool>(true);
    writer.NextRecord();

    writer.WriteRecord(new User { Id = 1, Name = "Bob", IsAdmin = true });
    writer.NextRecord();
}

After writing, Complete(Exception?) or CompleteAsync(Exception?, CancellationToken) should be called to properly dispose of resources used by the writer instance.

Note

The Exception parameter is used to suppress flushing any remaining data if the write operation errored. A using-statement or Dispose can be used to clean up the writer instance similarly, but you lose the aforementioned benefit by only disposing. You can safely wrap a manually completed writer in a using block, since multiple completions are harmless.

CSV without a header

To read or write headerless CSV:

  1. Annotate types with CsvIndexAttribute
  2. Set CsvOptions<T>.HasHeader to false
class User
{
    [CsvIndex(0)] public int Id { get; set; }
    [CsvIndex(1)] public string? Name { get; set; }
    [CsvIndex(2)] public bool IsAdmin { get; set; }
}

CsvOptions<char> options = new() { HasHeader = false };

foreach (var user in CsvReader.Read<User>(csv, options))
{
    ProcessUser(user);
}

See Attributes for more details on how to customize the binding rules.

Converters

The converters in FlameCsv follow the common .NET pattern TryParse/TryFormat.

When reading CSV, TryParse(ReadOnlySpan<T>, out TValue) is used to convert the CSV field into a .NET type instance. If parsing fails the converter returns false and the library throws an appropriate exception.

When writing, TryFormat(Span<T>, TValue, out int) should attempt to write the value to the destination buffer. If the value was successfully written, the method returns true and sets the amount of written characters (or bytes). If the destination buffer is too small, the method returns false. In this case, the value of charsWritten and any data possibly written to the buffer are ignored.

Custom converter

The following example implements a converter that writes and reads booleans as "yes" or "no" (case insensitive).

class YesNoConverter : CsvConverter<char, bool>
{
    public override bool TryParse(ReadOnlySpan<char> source, out bool value)
    {
        if (source.Equals("yes", StringComparison.OrdinalIgnoreCase))
        {
            value = true;
            return true;
        }
        else if (source.Equals("no", StringComparison.OrdinalIgnoreCase))
        {
            value = false;
            return true;
        }
        else
        {
            value = default;
            return false;
        }
    }

    public override bool TryFormat(Span<char> destination, bool value, out int charsWritten)
    {
        string toWrite = value ? "yes" : "no";

        if (destination.Length >= toWrite.Length)
        {
            toWrite.AsSpan().CopyTo(destination);
            charsWritten = toWrite.Length;
            return true;
        }
        else
        {
            charsWritten = 0;
            return false;
        }
    }
}

Converter factory

This example implements a factory that creates a generic IEnumerable<T> converter that reads the item values separated by ; (semicolon).

Thanks to the IBinaryInteger<TSelf>-constraint, we can create the separator for both token without having to resort to typeof-checks and/or casts/unsafe. Alternatively, the factory could pass the separator as a constructor parameter.

CsvOptions<char> options = new() { Converters = { new EnumerableConverterFactory() } };

// reads and writes values in the form of "1;2;3"
var converter = options.GetConverter<IEnumerable<int>>();

class EnumerableConverterFactory : CsvConverterFactory<char>
{
    public override bool CanConvert(Type type)
    {
        return IsIEnumerable(type) || type.GetInterfaces().Any(IsIEnumerable);
    }

    public override CsvConverter<char> Create(Type type, CsvOptions<char> options)
    {
        var elementType = type.GetInterfaces().First(IsIEnumerable).GetGenericArguments()[0];
        return (CsvConverter<char>)Activator.CreateInstance(
            typeof(EnumerableConverterFactory<,>).MakeGenericType(typeof(char), elementType),
            args: [options])!;
    }

    private static bool IsIEnumerable(Type type)
        => type.IsInterface &&
           type.IsGenericTypeDefinition &&
           type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
}
class EnumerableConverterFactory<T, TElement>(CsvOptions<T> options)
    : CsvConverter<T, IEnumerable<TElement>>
    where T : unmanaged, System.Numerics.IBinaryInteger<T>
{
    private readonly CsvConverter<T, TElement> _elementConverter = options.GetConverter<TElement>();

    public override bool TryParse(
        ReadOnlySpan<T> source,
        [MaybeNullWhen(false)] out IEnumerable<TElement> value)
    {
        List<TElement> result = [];

        foreach (Range range in source.Split(T.CreateChecked(';')))
        {
            if (!_elementConverter.TryParse(source[range], out var element))
            {
                value = null;
                return false;
            }

            result.Add(element);
        }

        value = result;
        return true;
    }

    public override bool TryFormat(
        Span<T> destination,
        IEnumerable<TElement> value,
        out int charsWritten)
    {
        charsWritten = 0;
        bool first = true;

        foreach (var element in value)
        {
            if (!_elementConverter.TryFormat(destination.Slice(charsWritten), element, out int written))
            {
                return false;
            }

            charsWritten += written;

            if (first)
            {
                first = false;
            }
            else
            {
                if (charsWritten >= destination.Length)
                {
                    return false;
                }

                destination[charsWritten++] = T.CreateChecked(';');
            }
        }

        return true;
    }
}