CsvToolkit.Core is a high-performance CSV library targeting netstandard2.1, focused on streaming and low allocations with Span<T>, Memory<T>, and ArrayPool<T>.
Sep is still the fastest option in the latest full benchmark run, but CsvToolkit.Core now beats CsvHelper in 10/10 common typed scenarios and wins allocations in 3/10 of those scenarios, all on typed write paths. In the current 100k-row full run, CsvToolkit.Core writes typed POCOs in 17.1 ms vs 22.8 ms for CsvHelper, and writes typed records with converter options in 14.9 ms vs 19.3 ms, while allocating far less in both cases. If your only KPI is absolute CSV throughput, Sep remains the speed ceiling; CsvToolkit.Core is now much closer while keeping a higher-level typed API and broader feature coverage.
Package name on NuGet.org: CsvToolkit.Core
dotnet add package CsvToolkit.CoreThis project is licensed under the MIT License. See LICENSE.
var options = new CsvOptions
{
DelimiterString = ",", // supports multi-character delimiters too
DetectDelimiter = false,
DelimiterCandidates = new[] { ",", ";", "\t", "|" },
HasHeader = true,
Quote = '"',
Escape = '"',
TrimOptions = CsvTrimOptions.Trim,
DetectColumnCount = true,
ReadMode = CsvReadMode.Strict,
CultureInfo = CultureInfo.InvariantCulture,
PrepareHeaderForMatch = static (header, _) => header.Trim().ToLowerInvariant(),
SanitizeForInjection = true
};
using var reader = new CsvReader(streamOrTextReader, options);
using var writer = new CsvWriter(streamOrTextWriter, options);
// Row/field iteration
while (reader.TryReadRow(out _))
{
int id = reader.GetField<int>("id");
ReadOnlySpan<char> name = reader.GetFieldSpan(1);
string materialized = reader.GetField("email");
}
// Dictionary / dynamic
if (reader.TryReadDictionary(out var dict)) { /* header -> value */ }
if (reader.TryReadDynamic(out dynamic dyn)) { /* ExpandoObject */ }
// Strongly typed POCO
while (reader.TryReadRecord<MyRow>(out var record)) { }
writer.WriteHeader<MyRow>();
writer.WriteRecord(new MyRow());
var records = new List<MyRow> { new MyRow() };
writer.WriteRecords(records, writeHeader: false);
// Async
while (await reader.ReadAsync()) { var current = reader.GetRecord<MyRow>(); }
await reader.ReadRecordAsync<MyRow>();
await foreach (var row in reader.GetRecordsAsync<MyRow>()) { }
await writer.WriteRecordAsync(new MyRow());
await writer.WriteRecordsAsync(records, writeHeader: true);
// ADO.NET adapter
using var dataReader = reader.AsDataReader();- Read from
TextReaderand UTF-8Stream - Write to
TextWriterand UTF-8Stream - Streaming row-by-row parser (no full-file load)
- Quoted fields, escaped quotes, delimiters inside quotes, CRLF/LF handling
- Header support, delimiter/quote/escape/newline configuration
- Multi-character delimiters via
DelimiterString - Optional delimiter auto-detection via
DetectDelimiter+DelimiterCandidates - Trim options and strict/lenient error handling
- Error and validation callbacks:
BadDataFound,MissingFieldFound,HeaderValidated,ReadingExceptionOccurred - Header normalization callback:
PrepareHeaderForMatch - Field access as
ReadOnlySpan<char>/ReadOnlyMemory<char> - Typed field access helpers:
GetField<T>(index)/GetField<T>(name, nameIndex) - Reader APIs:
TryReadRow,ReadAsync,TryReadDictionary,ReadDictionaryAsync,TryReadDynamic - Record APIs:
TryReadRecord<T>,ReadRecordAsync<T>,GetRecords<T>,GetRecordsAsync<T> CsvDataReaderadapter viaAsDataReader()- POCO mapping with:
- Attributes:
[CsvColumn],[CsvIndex],[CsvIgnore],[CsvNameIndex],[CsvOptional],[CsvDefault],[CsvConstant],[CsvValidate] - Converter option attributes:
[CsvNullValues],[CsvTrueValues],[CsvFalseValues],[CsvFormats],[CsvNumberStyles],[CsvDateTimeStyles],[CsvCulture] - Fluent mapping:
CsvMapRegistry.Register<T>(...)withOptional,Default,Constant,Validate,NameIndex, member converter options - Constructor-based record materialization (immutable / constructor-only models)
- Attributes:
- Type conversion:
- primitives, enums, nullable,
DateTime,DateOnly,TimeOnly,Guid - culture-aware parsing/formatting
- custom converters (
ICsvTypeConverter<T>) - global converter options per type via
CsvOptions.ConverterOptions
- primitives, enums, nullable,
- Writer bulk APIs:
WriteRecords(...),WriteRecordsAsync(...) - CSV injection sanitization for spreadsheet-safe output (
SanitizeForInjection)
using var reader = new CsvReader(new StreamReader("people.csv"));
while (reader.TryReadRecord<Person>(out var person))
{
Console.WriteLine(person.Name);
}using var writer = new CsvWriter(File.Create("people.csv"), new CsvOptions { NewLine = "\n" });
writer.WriteHeader<Person>();
foreach (var person in people)
{
writer.WriteRecord(person);
}while (reader.TryReadRow(out var row))
{
ReadOnlySpan<char> id = row.GetFieldSpan(0);
// parse directly from span
}var maps = new CsvMapRegistry();
maps.Register<Person>(map =>
{
map.Map(x => x.Id).Name("person_id");
map.Map(x => x.Name).Name("full_name");
});public sealed class PersonRow
{
[CsvColumn("name"), CsvNameIndex(0)]
public string FirstName { get; set; } = string.Empty;
[CsvColumn("name"), CsvNameIndex(1)]
public string LastName { get; set; } = string.Empty;
[CsvOptional, CsvDefault(18)]
public int Age { get; set; }
[CsvConstant("US")]
public string Country { get; set; } = string.Empty;
[CsvValidate(nameof(IsValidAge), Message = "Age must be >= 0.")]
public int CheckedAge { get; set; }
[CsvTrueValues("Y"), CsvFalseValues("N")]
public bool Active { get; set; }
[CsvFormats("dd-MM-yyyy"), CsvCulture("en-GB")]
public DateTime CreatedAt { get; set; }
[CsvNullValues("NULL")]
public decimal? Score { get; set; }
private static bool IsValidAge(int value) => value >= 0;
}For read/write conversion options, resolution order is:
- Member-level fluent options (
map.Map(...).TrueValues(...).Formats(...)) - Member-level attribute options (
[CsvTrueValues],[CsvFormats], etc.) - Global type options (
options.ConverterOptions.Configure<T>(...)) - Built-in defaults
Notes:
- Fluent member options override attribute options for the same member.
- Attribute/member options are isolated to that mapped member and do not affect other properties of the same type.
ConverterOptionsprecedence applies to bothCsvReaderandCsvWriter.
Benchmarks compare CsvToolkit.Core with CsvHelper and Sep for:
- Typed read/write (default mapping)
- Typed read/write with converter options
- Typed read with duplicate headers (
NameIndex) - Dictionary/dynamic read
- Semicolon + high quoting parse
- Dataset sizes:
10kand100krows
If you want deeper implementation details and rationale, start here:
- Technical Architecture: components, data flow, and technologies used.
- Performance Design Decisions: why hot paths are fast and the tradeoffs behind each optimization.
Run all benchmarks non-interactively:
dotnet run -c Release --project benchmarks/CsvToolkit.Benchmarks -- --filter "*CsvReadWriteBenchmarks*"Run one benchmark (faster while iterating):
dotnet run -c Release --project benchmarks/CsvToolkit.Benchmarks -- --filter "*CsvReadWriteBenchmarks.CsvToolkitCore_ReadTyped_Stream*"Run from IDE:
- Project:
benchmarks/CsvToolkit.Benchmarks - Configuration:
Release - Program arguments:
--filter "*CsvReadWriteBenchmarks*"
If you run without --filter, BenchmarkDotNet enters interactive selection mode and waits for input.
Benchmark dataset generation is deterministic (Random seed-based) inside benchmark setup.
Run date: 2026-03-07
Machine: Apple M3 Pro (11 logical / 11 physical cores)
OS: macOS Tahoe 26.3 (25D125) [Darwin 25.3.0]
Runtime: .NET 10.0.0
Command: dotnet run -c Release --project benchmarks/CsvToolkit.Benchmarks -- --filter "*CsvReadWriteBenchmarks*"
Common scenarios benchmarked across all three libraries:
| Scenario | RowCount | CsvToolkit.Core (Mean / Alloc) | CsvHelper (Mean / Alloc) | Sep (Mean / Alloc) | Time Winner | Allocation Winner |
|---|---|---|---|---|---|---|
| ReadTyped | 10,000 | 3.907 ms / 0.99 MB | 4.879 ms / 3.76 MB | 2.097 ms / 0.92 MB | Sep (1.86x vs CsvToolkit) |
Sep (1.08x lower vs CsvToolkit) |
| WriteTyped | 10,000 | 2.038 ms / 1.05 MB | 2.936 ms / 3.45 MB | 1.145 ms / 2.01 MB | Sep (1.78x) |
CsvToolkit (1.92x lower vs Sep) |
| ReadTyped_SemicolonHighQuote | 10,000 | 4.661 ms / 0.99 MB | 5.292 ms / 3.76 MB | 2.310 ms / 0.92 MB | Sep (2.02x) |
Sep (1.08x lower) |
| ReadTyped_WithConverterOptions | 10,000 | 2.606 ms / 1.33 MB | 3.193 ms / 2.67 MB | 1.088 ms / 0.39 MB | Sep (2.40x) |
Sep (3.43x lower) |
| WriteTyped_WithConverterOptions | 10,000 | 2.197 ms / 0.54 MB | 2.434 ms / 2.46 MB | 0.746 ms / 0.70 MB | Sep (2.94x) |
CsvToolkit (1.29x lower vs Sep) |
| ReadTyped | 100,000 | 30.120 ms / 9.30 MB | 46.449 ms / 36.72 MB | 21.639 ms / 9.23 MB | Sep (1.39x) |
Sep (1.01x lower) |
| WriteTyped | 100,000 | 17.117 ms / 16.05 MB | 22.847 ms / 39.40 MB | 12.028 ms / 16.01 MB | Sep (1.42x) |
Sep (1.00x lower) |
| ReadTyped_SemicolonHighQuote | 100,000 | 32.651 ms / 9.30 MB | 49.259 ms / 36.72 MB | 23.793 ms / 9.23 MB | Sep (1.37x) |
Sep (1.01x lower) |
| ReadTyped_WithConverterOptions | 100,000 | 23.674 ms / 12.80 MB | 27.904 ms / 25.81 MB | 10.971 ms / 3.82 MB | Sep (2.16x) |
Sep (3.35x lower) |
| WriteTyped_WithConverterOptions | 100,000 | 14.878 ms / 8.04 MB | 19.270 ms / 26.60 MB | 7.409 ms / 9.93 MB | Sep (2.01x) |
CsvToolkit (1.24x lower vs Sep) |
Additional scenarios:
| Scenario | RowCount | CsvToolkit.Core (Mean / Alloc) | CsvHelper (Mean / Alloc) | Time Winner | Allocation Winner |
|---|---|---|---|---|---|
| ReadTyped_DuplicateHeader_NameIndex | 10,000 | 0.927 ms / 1.17 MB | 1.605 ms / 1.78 MB | CsvToolkit (1.73x) |
CsvToolkit (1.51x lower) |
| ReadTyped_DuplicateHeader_NameIndex | 100,000 | 8.585 ms / 12.23 MB | 14.277 ms / 17.64 MB | CsvToolkit (1.66x) |
CsvToolkit (1.44x lower) |
| ReadDictionary vs ReadDynamic | 10,000 | 1.762 ms / 5.26 MB | 4.312 ms / 8.00 MB | CsvToolkit (2.45x) |
CsvToolkit (1.52x lower) |
| ReadDictionary vs ReadDynamic | 100,000 | 18.355 ms / 52.64 MB | 43.219 ms / 79.41 MB | CsvToolkit (2.35x) |
CsvToolkit (1.51x lower) |
Observed trend from this run:
- Across the
10common scenarios benchmarked for all three libraries,Sepis the fastest option in10/10. - On allocations,
Sepwins7/10of those common scenarios andCsvToolkit.Corewins3/10, all on typed write scenarios. CsvToolkit.CorebeatsCsvHelperin10/10of those common scenarios.- In the extra
DuplicateHeader_NameIndexscenario,CsvToolkit.CorebeatsCsvHelperat both sizes. - In the extra
ReadDictionary vs ReadDynamicscenario,CsvToolkit.CorebeatsCsvHelperat both sizes. - Important caveat:
Sepis benchmarked here through explicit/manual column mapping and writing, whileCsvToolkit.CoreandCsvHelperuse higher-level typed POCO APIs. TreatSepas a low-level throughput ceiling, not a direct ergonomic equivalent.
Benchmark run time:
- Benchmark execution:
00:14:31(871.21 sec) - Global total:
00:14:37(877.11 sec)
Benchmark artifacts:
- Generated local output:
BenchmarkDotNet.Artifacts/results/