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:
- Annotate types with CsvIndexAttribute
- 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;
}
}