Guide
Messy files
What the reader detects and adapts to by default, and the options for malformed input.
Real files break the rules: stray quotes, quotes that never close, records with too many or too few fields, whitespace around values. By default the reader recovers from all of these and keeps reading, so that a file that opens in a spreadsheet also opens here. The options on this page make it stricter, or tell you what it found.
What the reader adapts to by default
| Situation | Behavior |
|---|---|
| Separator | Detected from the first lines (, ; tab \|), scored by how consistently each appears per line. Excel's sep=; first line is honored and skipped. |
| Encoding | UTF-8 (BOM skipped). UTF-16/UTF-32 byte order marks are detected and transcoded. Encoding option for other code pages. |
| Line endings | \n, \r\n and lone \r, mixed freely. |
| Quotes | RFC 4180: a quote at the start of a field opens a quoted field (separators/line breaks inside are text, "" is a quote). |
Stray quotes (5'10", ab"c) |
Kept as text; they do not open a quoted region. |
Text after a closing quote ("ab"c) |
Appended: abc. |
| Unterminated quote | The field runs to the end of the input. |
| Empty lines | Skipped (SkipEmptyLines). |
| Ragged records | Allowed; missing fields read as empty (MissingField). |
| Header names | Exact match first, then case-insensitive and trimmed. Duplicates allowed (first wins; GetFieldIndex(name, n) finds the others). |
| Numbers | Invariant culture, fast UTF-8 paths for integers and floats; FormatProvider for other cultures. Booleans accept true/false, yes/no, y/n, t/f, 1/0. |
For example, this file has a stray quote in its second record and text after a closing quote in its third:
Name,Height
Anna,5'10"
"Ben"x,6'
Cara,"5'4"
using var reader = CsvReader.FromFile("heights.csv");
while (reader.Read()) Console.WriteLine($"[{reader.GetString(0)}] [{reader.GetString(1)}]");
// [Anna] [5'10"] the quotes are text: they are not at the start of the field
// [Benx] [6'] text after the closing quote is appended
// [Cara] [5'4] a well-formed quoted field
Options for malformed files
| Option | Values | Use it when |
|---|---|---|
MalformedData |
Recover (default), Throw, Skip |
Decide what happens to records with bad quoting. Throw raises CsvFormatException at the bad record (earlier records are still readable). |
OnMalformedRecord |
callback | Log every malformed record (record number, reason, byte offset, raw text) whether it was recovered or skipped. |
AllowLineBreaksInQuotes |
true (default) / false |
Set false for files with unbalanced quotes: every line break then ends the record, so one bad quote cannot swallow the rest of the file. |
FieldCountMismatch |
Allow (default), Throw, Skip |
Enforce that records have the header's field count. |
MissingField |
ReturnEmpty (default), Throw |
Accessing a field past the end of a short record. |
TrimWhitespace |
null (default: trim in FixedWidth mode only), true, false |
Trim spaces/tabs around values; "quoted" still unquotes, and escaped whitespace is kept. TrimInsideQuotes trims inside the quotes too, and WhitespaceCharacters says what is trimmed. |
ShouldSkipRecord |
callback | Skip records such as totals or section titles in the middle of the data. |
OnRecordError |
callback | Skip records of a record type whose fields do not parse or fail their checks, and go on. |
MaxRecordSize |
null (default: no limit) |
Stop a record that keeps growing, such as one after an unclosed quote, before it fills memory. |
TrackLineNumbers |
false (default) / true |
Report the line of each record and of errors (LineNumber), counting the lines inside quoted fields. |
IncludeValuesInExceptions |
true (default) / false |
Keep field values, which may be personal data, out of exception messages and logs. |
Mode |
Rfc4180 (default), Escape, NoEscape, FixedWidth |
NoEscape treats quotes as ordinary text (TSV exports, logs); see Parsing modes. |
CommentPrefix |
null (default) |
Skip lines starting with e.g. #. |
Separator, Quote, HasHeader, Encoding, FormatProvider |
Override detection and defaults. | |
PoolStrings |
false (default) / true |
Set true when you keep many records in memory: repeated values in a column then share one string instance, which saves memory and GC work (columns of mostly unique values switch this off by themselves). For strings that are used and dropped, the default is faster. |
Reporting malformed records
OnMalformedRecord is called for every malformed record the reader finds, with a CsvMalformedRecord that says what
is wrong and where. The reader still recovers (or skips, or throws) as MalformedData says:
var options = new CsvReaderOptions
{
OnMalformedRecord = bad => Console.WriteLine(
$"Record {bad.RecordNumber}: {bad.Reason} at byte {bad.ByteOffset}, skipped: {bad.Skipped}: {bad.RawRecord}"),
};
using var reader = CsvReader.FromFile("heights.csv", options);
while (reader.Read()) { }
// Record 2: StrayQuote at byte 21, skipped: False: Anna,5'10"
// Record 3: TextAfterClosingQuote at byte 28, skipped: False: "Ben"x,6'
CsvMalformedRecord |
|
|---|---|
RecordNumber |
The 1-based number of the record, counting the header and the records returned before it (not skipped records, empty lines or comments). |
Reason |
What is wrong: StrayQuote, TextAfterClosingQuote, UnterminatedQuote, LineBreakInQuotes, FieldCountMismatch or TrailingEscape (Escape mode). A record with several problems is reported once, with the first. |
Skipped |
Whether the reader skips the record; otherwise it recovers and returns it. |
ByteOffset |
The offset, in bytes of UTF-8 input, of the problem (bad quoting) or of the start of the record (field count). |
RawRecord |
The record as it appears in the input, without its line break. |
LineNumber |
The 1-based line where the record starts, counting every line, with TrackLineNumbers = true; 0 otherwise. |
The callback runs on the thread that reads, one record at a time and in input order, also with
ParallelEnumerate. Setting it turns on malformed-quote detection, which
costs a little speed on heavily quoted data; without a callback and with MalformedData = Recover, the reader does
not look for these problems at all.
Throwing or skipping
MalformedData = Throw stops at the first record with bad quoting. The records before it are returned normally, so
you can import up to the problem and report it:
// Name,Height
// Anna,170
// "Ben"x,180
// Cara,160
var options = new CsvReaderOptions { MalformedData = CsvMalformedDataHandling.Throw };
using var reader = CsvReader.FromFile("heights.csv", options);
try
{
while (reader.Read()) Console.WriteLine(reader.GetString(0)); // Anna
}
catch (CsvFormatException e)
{
// Malformed CSV in record 3: text after the closing quote of a quoted field (byte offset 26).
Console.WriteLine($"{e.Message} (record {e.RecordNumber}, byte {e.ByteOffset})");
}
MalformedData = Skip passes over records with bad quoting, and reports them to OnMalformedRecord with
Skipped = true. The same file then reads as Anna and Cara.
Unbalanced quotes
A quoted field may contain line breaks, so a quote that is opened and never closed makes the rest of the file one field:
Name,Note
Anna,"forgot to close
Ben,ok
Cara,ok
using var reader = CsvReader.FromFile("notes.csv");
// One record: [Anna] [forgot to close\nBen,ok\nCara,ok\n]
If your files never have line breaks inside quotes, set AllowLineBreaksInQuotes = false: every line break then ends
the record, and the damage stays on one line. The record with the open quote is reported as LineBreakInQuotes:
var options = new CsvReaderOptions { AllowLineBreaksInQuotes = false };
using var reader = CsvReader.FromFile("notes.csv", options);
// [Anna] [forgot to close]
// [Ben] [ok]
// [Cara] [ok]
Field counts
Records may have any number of fields by default. FieldCountMismatch checks every record against the header (or,
without a header, the first record):
// A,B
// 1,2
// 3
// 4,5,6
// 7,8
var options = new CsvReaderOptions
{
FieldCountMismatch = CsvFieldCountHandling.Skip,
OnMalformedRecord = bad => Console.WriteLine($"Skipped: {bad.RawRecord}"),
};
using var reader = CsvReader.FromFile("counts.csv", options);
// Records 1,2 and 7,8; "Skipped: 3" and "Skipped: 4,5,6" are logged.
With CsvFieldCountHandling.Throw, the reader throws CsvFormatException (Record 3 has 1 fields but 2 were expected.) at the first record with a different count.
Missing fields
Without a field count check, a short record reads as if the missing fields were empty. MissingField = Throw makes
reading such a field an error instead, while still allowing short records that you don't read past:
var options = new CsvReaderOptions { MissingField = CsvMissingFieldHandling.Throw };
using var reader = CsvReader.FromText("A,B,C\n1,2\n", options);
reader.Read();
reader.GetString(1); // "2"
reader.GetString(2); // IndexOutOfRangeException: Record 2 has 2 fields; field 2 does not exist.
Whitespace, comments and empty lines
TrimWhitespace = trueremoves spaces and tabs around values and header names; a quote after leading whitespace still opens a quoted field ("a, b"reads asa, b).TrimInsideQuotes = truealso turns" a "intoa, andWhitespaceCharacterstrims other padding characters. See Whitespace.CommentPrefix = '#'skips lines that start with#, such as notes before the header. See Comments and empty lines.- Empty lines are skipped;
SkipEmptyLines = falsereturns each as a record with one empty field.