Migration guide
Migrating to v3.0.0
Moving to v3.0.0 from v2.16.3. Changelog.
CsvHelper 3 is a large rewrite: a faster parser, configuration flags that became callbacks you can replace, and the
state of a reader or writer moved into a context object. Most class names lose their Csv prefix (CsvConfiguration
is Configuration, CsvClassMap<T> is ClassMap<T>), and "property" became "member" because fields can be mapped
too. The change that every hand-written read loop over a file with a header meets: Read no longer reads the
header. CsvHelper 3 is built for .NET Framework 4.5 and .NET Standard 2.0 only; the .NET Framework 2.0, 3.5 and 4.0,
portable and .NET Standard 1.3 builds are gone.
At a glance
| CsvHelper 2 | CsvHelper 3 |
|---|---|
CsvConfiguration |
Configuration, in CsvHelper.Configuration |
Read() reads the header first |
Read(), then ReadHeader() |
FieldHeaders, CurrentRecord, Row |
Context.HeaderRecord, Context.Record, Context.Row |
CsvClassMap<T>, CsvPropertyMap |
ClassMap<T>, MemberMap<TClass, TMember> |
Map(...).TypeConverterOption("format") |
Map(...).TypeConverterOption.Format("format") |
ConvertFromString(options, text) |
ConvertFromString(text, row, memberMapData) |
Static TypeConverterFactory, TypeConverterOptionsFactory |
TypeConverterCache, TypeConverterOptionsCache on the configuration |
WriteHeader<T>() and WriteRecord(record) end the record |
Call NextRecord() after them |
ThrowOnBadData, WillThrowOnMissingField, IgnoreReadingExceptions |
BadDataFound, MissingFieldFound, HeaderValidated, ReadingExceptionOccurred |
CsvBadDataException, CsvMissingFieldException, ... |
BadDataException, MissingFieldException, ... |
ICsvReader, ICsvReaderRow, ICsvWriter, ICsvParser |
IReader, IReaderRow, IWriter, IParser |
Renamed types
The Csv prefix is gone from the class names, except for CsvReader, CsvParser, CsvWriter, CsvSerializer and
CsvHelperException, and the types named after properties are named after members, since a map can now point at a
field. The namespaces are the same, so most of the port is a rename.
| CsvHelper 2 | CsvHelper 3 |
|---|---|
CsvConfiguration |
Configuration |
CsvClassMap, CsvClassMap<T>, DefaultCsvClassMap<T> |
ClassMap, ClassMap<T>, DefaultClassMap<T> |
CsvClassMapCollection |
ClassMapCollection |
CsvPropertyMap |
MemberMap, and MemberMap<TClass, TMember> from Map |
CsvPropertyMapData, CsvPropertyMapCollection |
MemberMapData, MemberMapCollection |
CsvPropertyReferenceMap, CsvPropertyNameCollection |
MemberReferenceMap, MemberNameCollection |
TypeConverterFactory, TypeConverterOptionsFactory |
TypeConverterCache, TypeConverterOptionsCache, instances on the configuration |
CsvFactory, ICsvFactory |
Factory, IFactory |
ICsvParser, ICsvReader, ICsvReaderRow |
IParser, IReader, IReaderRow |
ICsvSerializer, ICsvWriter |
ISerializer, IWriter (with IWriterRow) |
The exceptions are renamed the same way: see Exceptions.
Configuration has the name of its namespace. If a file also has using System.Configuration;, whose namespace has a
Configuration class too, write CsvHelper.Configuration.Configuration.
Creating a reader or writer
The constructors take a TextReader or TextWriter and an optional configuration, as before. Only the configuration
class is renamed.
var config = new CsvConfiguration { Delimiter = ";" };
using var csv = new CsvReader(reader, config);
csv.Configuration.HasHeaderRecord = false;var config = new Configuration { Delimiter = ";" };
using var csv = new CsvReader(reader, config);
csv.Configuration.HasHeaderRecord = false;csv.Configurationis an interface now:IReaderConfigurationon a reader,IWriterConfigurationon a writer. Each shows only the settings its side uses, so a reader has noQuoteAllFields. Code that tookcsv.Configurationas aCsvConfigurationtakes the interface, or theConfigurationobject you created.- Leaving the text reader open: disposing a reader or writer still disposes the
TextReaderorTextWriter.new CsvReader(reader, leaveOpen: true),new CsvReader(reader, config, leaveOpen: true)andnew CsvWriter(writer, leaveOpen: true)leave it open. For a writer with a configuration, passnew CsvSerializer(writer, config, leaveOpen: true)tonew CsvWriter(serializer).
Configuration
Most settings keep their names. Three groups changed: the flags that chose between throwing and ignoring became callbacks (see Bad data and errors), the header matching flags became one function, and trimming moved into the parser.
var config = new CsvConfiguration
{
Delimiter = ";",
IsHeaderCaseSensitive = false,
TrimFields = true,
SkipEmptyRecords = true,
IgnorePrivateAccessor = true,
};
using var csv = new CsvReader(reader, config);var config = new Configuration
{
Delimiter = ";",
PrepareHeaderForMatch = header => header.ToLower(),
TrimOptions = TrimOptions.Trim | TrimOptions.InsideQuotes,
ShouldSkipRecord = record => record != null && record.All(string.IsNullOrEmpty),
IncludePrivateMembers = true,
};
using var csv = new CsvReader(reader, config);CsvConfiguration |
Configuration |
Notes |
|---|---|---|
IsHeaderCaseSensitive = false |
PrepareHeaderForMatch = header => header.ToLower() |
See Header matching. |
TrimHeaders = true |
PrepareHeaderForMatch = header => header.Trim() |
|
IgnoreHeaderWhiteSpace = true |
PrepareHeaderForMatch = header => Regex.Replace(header, @"\s", string.Empty) |
|
TrimFields = true |
TrimOptions |
TrimOptions.Trim trims the spaces around a field, outside its quotes; TrimOptions.InsideQuotes trims the spaces inside them. 2 trimmed the value, quoted or not, so set both. |
SkipEmptyRecords = true |
ShouldSkipRecord = record => record != null && record.All(string.IsNullOrEmpty) |
3.0.0 also calls it with null at the end of the file; 3.1.0 doesn't. |
WillThrowOnMissingField = false |
MissingFieldFound = null, HeaderValidated = null |
See Reading records. |
ThrowOnBadData, BadDataCallback |
BadDataFound |
Throws by default now. |
IgnoreReadingExceptions, ReadingExceptionCallback |
ReadingExceptionOccurred |
|
IgnorePrivateAccessor |
IncludePrivateMembers |
|
UseNewObjectForNullReferenceProperties |
UseNewObjectForNullReferenceMembers |
|
PropertyBindingFlags |
MemberTypes, IncludePrivateMembers |
2.16.3 never read PropertyBindingFlags. MemberTypes.Fields maps fields too. |
HasExcelSeparator, UseExcelLeadingZerosFormatForNumerics |
none | See Removed with no replacement. |
These keep their names: Delimiter, Quote, Comment, AllowComments, HasHeaderRecord, IgnoreBlankLines,
IgnoreQuotes, QuoteAllFields, QuoteNoFields, CultureInfo, Encoding, CountBytes, BufferSize,
DetectColumnCountChanges, IgnoreReferences, PrefixReferenceHeaders, ShouldSkipRecord and the class map
methods (RegisterClassMap, UnregisterClassMap, AutoMap, Maps).
A ShouldSkipRecord function brought over from 2 must accept null on 3.0.0: 2 never passed it the end of the file,
but 3.0.0 calls it with null there, so record.All(...) throws an ArgumentNullException. 3.1.0 no longer does.
Header matching
IsHeaderCaseSensitive, TrimHeaders and IgnoreHeaderWhiteSpace are replaced by PrepareHeaderForMatch, a function
that both the header name and the member name go through before they are compared. One function covers any
combination, and anything else you need, such as removing underscores.
var config = new CsvConfiguration
{
IsHeaderCaseSensitive = false,
IgnoreHeaderWhiteSpace = true,
};using System.Text.RegularExpressions;
var config = new Configuration
{
PrepareHeaderForMatch = header => Regex.Replace(header, @"\s", string.Empty).ToLower(),
};After the function, names are compared exactly, case included.
Reading by hand
Read no longer reads the header. Call Read and then ReadHeader, which takes the current record as the header, so
the header doesn't have to be the first row. The state of the current record moved to Context.
using var csv = new CsvReader(reader);
csv.ReadHeader();
var headers = csv.FieldHeaders;
while (csv.Read())
{
var id = csv.GetField<int>("Id");
var name = csv.GetField("Name");
Console.WriteLine($"Row {csv.Row}: {id} {name}");
}using var csv = new CsvReader(reader);
csv.Read();
csv.ReadHeader();
var headers = csv.Context.HeaderRecord;
while (csv.Read())
{
var id = csv.GetField<int>("Id");
var name = csv.GetField("Name");
Console.WriteLine($"Row {csv.Context.Row}: {id} {name}");
}- Without
ReadHeader, the header row comes back as the first record, and getting a field by name throws aMissingFieldException, since there are no names to find. ReadHeaderbefore the firstRead, which 2 allowed, throws aReaderException: there is no current record to take the header from.ReadAsync()is new.
| CsvHelper 2 | CsvHelper 3 |
|---|---|
FieldHeaders |
Context.HeaderRecord |
CurrentRecord |
Context.Record |
Row |
Context.Row |
Parser.RawRecord |
Context.RawRecord |
Parser.CharPosition, Parser.BytePosition |
Context.CharPosition, Context.BytePosition |
IsRecordEmpty() |
Context.Record.All(string.IsNullOrEmpty) |
GetField(index, converter) and the other obsolete overloads that returned object |
GetField(type, index, converter) or GetField<T>(index, converter) |
GetField<T>, TryGetField<T>, GetField(name, index), csv[name] |
Unchanged |
Reading records
GetRecords<T>() is called the same way and still reads the header itself. What's new is that it validates the
header before the first record: every mapped member must have a column, or HeaderValidated throws a
ValidationException. In 2 a missing column threw CsvMissingFieldException, unless WillThrowOnMissingField was
false; that switch is now two callbacks.
public class Order
{
public int Id { get; set; }
public string Note { get; set; }
}
var config = new CsvConfiguration { WillThrowOnMissingField = false };
using var csv = new CsvReader(reader, config);
var orders = csv.GetRecords<Order>().ToList();public class Order
{
public int Id { get; set; }
public string Note { get; set; }
}
var config = new Configuration
{
HeaderValidated = null,
MissingFieldFound = null,
};
using var csv = new CsvReader(reader, config);
var orders = csv.GetRecords<Order>().ToList();Setting only HeaderValidated = null isn't enough: the member's column is then looked up when the first record is
created, and MissingFieldFound throws a MissingFieldException. With both null, members without a column are left
unset, as in 2.
GetRecord<T>()in aRead()loop works as before: it reads the header when it hasn't been read yet.- More classes can be read. Auto mapping still maps public properties;
MemberTypesadds fields andIncludePrivateMembersadds private members. A class without a parameterless constructor is created through its constructor (the one with the most parameters, unless you setGetConstructor), whose parameters are matched to columns. Anonymous types work withGetRecords(new { Id = 0, Name = "" }), andEnumerateRecords(record)fills one object again and again. ClearRecordCache()iscsv.Context.ClearCache(Caches.ReadRecord), which clears the cache of every type.
Class maps
CsvClassMap<T> is ClassMap<T>, and Map returns a MemberMap<TClass, TMember> that knows the member's type, so
Default and Constant are type checked. The overloads of TypeConverterOption(...) became methods of a
TypeConverterOption property, named after what they set.
public class Customer
{
public string Name { get; set; }
}
public class Order
{
public int Id { get; set; }
public DateTime Ordered { get; set; }
public bool Shipped { get; set; }
public decimal Total { get; set; }
public Customer Customer { get; set; }
}
public sealed class CustomerMap : CsvClassMap<Customer>
{
public CustomerMap()
{
Map(m => m.Name).Name("Customer");
}
}
public sealed class OrderMap : CsvClassMap<Order>
{
public OrderMap()
{
Map(m => m.Id).Name("Order Id");
Map(m => m.Ordered).Name("Date").TypeConverterOption("yyyyMMdd");
Map(m => m.Shipped).TypeConverterOption(true, "Yes").TypeConverterOption(false, "No");
Map(m => m.Total).ConvertUsing(row => row.GetField<decimal>("Net") + row.GetField<decimal>("Tax"));
References<CustomerMap>(m => m.Customer);
}
}
using var csv = new CsvReader(reader);
csv.Configuration.RegisterClassMap<OrderMap>();
var orders = csv.GetRecords<Order>().ToList();public class Customer
{
public string Name { get; set; }
}
public class Order
{
public int Id { get; set; }
public DateTime Ordered { get; set; }
public bool Shipped { get; set; }
public decimal Total { get; set; }
public Customer Customer { get; set; }
}
public sealed class CustomerMap : ClassMap<Customer>
{
public CustomerMap()
{
Map(m => m.Name).Name("Customer");
}
}
public sealed class OrderMap : ClassMap<Order>
{
public OrderMap()
{
Map(m => m.Id).Name("Order Id");
Map(m => m.Ordered).Name("Date").TypeConverterOption.Format("yyyyMMdd");
Map(m => m.Shipped)
.TypeConverterOption.BooleanValues(true, true, "Yes")
.TypeConverterOption.BooleanValues(false, true, "No");
Map(m => m.Total).ConvertUsing(row => row.GetField<decimal>("Net") + row.GetField<decimal>("Tax"));
References<CustomerMap>(m => m.Customer);
}
}
using var csv = new CsvReader(reader);
csv.Configuration.RegisterClassMap<OrderMap>();
var orders = csv.GetRecords<Order>().ToList();- Call
TypeConverterOptionlast. Its methods return aMemberMap, not the typed map, soMap(m => m.Ordered).TypeConverterOption.Format("yyyyMMdd").Name("Date")doesn't compile: putName,Indexand the rest first. - Members mapped with
ConvertUsingneed a column in 3.0.0. The header check also looks for a column named after them,Totalhere, and throws aValidationExceptionwhen there is none. 3.1.0 skips members mapped withConvertUsingorConstant; on 3.0.0, setHeaderValidated = null.
CsvClassMap<T> |
ClassMap<T> |
Notes |
|---|---|---|
Map(m => m.X) |
Map(m => m.X) |
Returns MemberMap<TClass, TMember>. |
.Default(object) |
.Default(TMember), .Default(string) |
A string default for a member of another type is converted with the member's converter. |
.TypeConverterOption(format) |
.TypeConverterOption.Format(formats) |
Takes several formats: dates and times are read with any of them, values are written with the first. |
.TypeConverterOption(cultureInfo) |
.TypeConverterOption.CultureInfo(cultureInfo) |
|
.TypeConverterOption(dateTimeStyle) |
.TypeConverterOption.DateTimeStyles(dateTimeStyle) |
|
.TypeConverterOption(numberStyle) |
.TypeConverterOption.NumberStyles(numberStyle) |
|
.TypeConverterOption(isTrue, values) |
.TypeConverterOption.BooleanValues(isTrue, clearValues, values) |
clearValues must be passed when you pass values. |
.ConvertUsing(row => ...) |
.ConvertUsing(row => ...) |
The row is an IReaderRow. |
AutoMap(ignoreReferences, prefixReferenceHeaders) |
AutoMap(), AutoMap(configuration) |
The two options come from the configuration's IgnoreReferences and PrefixReferenceHeaders. |
PropertyMaps |
MemberMaps |
|
PropertyMap<T>(expression) |
MemberMaps.Find<T>(expression) |
|
CreateMap() |
none | It was obsolete in 2: map in the constructor. |
New in the maps: Map(m => m.Customer.Name) maps a member of a child object without a second map,
.ConvertUsing(order => ...) with the record returns the string to write, .Constant(value) uses one value for every
record, .Validate(field => ...) checks a field when reading, .TypeConverterOption.NullValues("NULL") lists strings
read as null, and Map() with no member writes a field that isn't on the class.
Type converters
ITypeConverter gets the reader or writer row and the member's map data instead of the options alone, so a
converter can look at other fields. CanConvertFrom and CanConvertTo are gone. The converter registry and the
per-type options are no longer static: each configuration has its own.
public class PercentConverter : DefaultTypeConverter
{
public override bool CanConvertFrom(Type type) => type == typeof(string);
public override object ConvertFromString(TypeConverterOptions options, string text) =>
decimal.Parse(text.TrimEnd('%'), options.CultureInfo) / 100;
public override string ConvertToString(TypeConverterOptions options, object value) =>
((decimal)value * 100).ToString(options.CultureInfo) + "%";
}
TypeConverterFactory.AddConverter<decimal>(new PercentConverter());
using var csv = new CsvReader(reader);public class PercentConverter : DefaultTypeConverter
{
public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData) =>
decimal.Parse(text.TrimEnd('%'), memberMapData.TypeConverterOptions.CultureInfo) / 100;
public override string ConvertToString(object value, IWriterRow row, MemberMapData memberMapData) =>
((decimal)value * 100).ToString(memberMapData.TypeConverterOptions.CultureInfo) + "%";
}
using var csv = new CsvReader(reader);
csv.Configuration.TypeConverterCache.AddConverter<decimal>(new PercentConverter());- A converter is used for reading whenever it's mapped. In 2 the reader skipped a member whose converter's
CanConvertFromreturnedfalse, whichDefaultTypeConverterdid by default. 3 callsConvertFromStringfor every mapped member, andDefaultTypeConverter's throws aTypeConverterException: a converter derived fromDefaultTypeConverterthat only overrodeConvertToStringneedsConvertFromStringtoo. - Return
string.Empty, notnull, fromConvertToStringfor an empty field.nullnow means the converter wrote the field itself, so nothing is written and the columns shift. - A converter registered once is no longer global.
TypeConverterFactorywas static, so oneAddConvertercall applied to every reader and writer. Add it to each configuration, or share oneConfigurationobject. TypeConverter(...),GetField<T, TConverter>,WriteField<T, TConverter>work as before, with converters written for the new signature.
Options for a type
TypeConverterOptionsFactory is the configuration's TypeConverterOptionsCache, and Format is Formats, an array.
TypeConverterOptionsFactory.AddOptions<DateTime>(new TypeConverterOptions { Format = "yyyyMMdd" });
using var csv = new CsvReader(reader);using var csv = new CsvReader(reader);
var options = new TypeConverterOptions { Formats = new[] { "yyyyMMdd" } };
csv.Configuration.TypeConverterOptionsCache.AddOptions<DateTime>(options);Dates and times are read with any of the formats, and values are written with the first. TypeConverterOptions also
has NullValues, the strings that a string or nullable member reads as null. There are none by default.
Writing
WriteHeader<T>() and WriteRecord(record) no longer end the record: call NextRecord() after them. That lets you
add fields after a record, or write several records on one line, and matches the reader, where GetRecord doesn't
call Read.
public class Order
{
public int Id { get; set; }
}
var orders = new List<Order> { new Order { Id = 1 } };
using var csv = new CsvWriter(writer);
csv.WriteHeader<Order>();
foreach (var order in orders)
{
csv.WriteRecord(order);
}public class Order
{
public int Id { get; set; }
}
var orders = new List<Order> { new Order { Id = 1 } };
using var csv = new CsvWriter(writer);
csv.WriteHeader<Order>();
csv.NextRecord();
foreach (var order in orders)
{
csv.WriteRecord(order);
csv.NextRecord();
}A literal port doesn't write the file you expect, and nothing fails. On 3.0.0 it writes nothing: the fields wait
for a NextRecord() that never comes. From 3.1.0, which writes the waiting fields when the writer is disposed, the
header and the records run together on one line. Add the NextRecord() calls, or write the list with
csv.WriteRecords(orders), which writes the header and ends each record itself, as before.
WriteRecordsis generic in 3.0.0 and 3.1.0: it takes anIEnumerable<T>, so a non-generic collection, such as anArrayList, doesn't compile; passlist.Cast<object>(), and each record is written by its own type. 3.1.1 takes anyIEnumerableagain.nullfields:WriteField<T>(null)writes an empty field. 2 threw aNullReferenceException.Flush()is new and isn't theTextWriter's: it writes the fields of the current record without a line ending.NextRecord()isFlush()and a line ending.- New:
WriteComment(text), writingExpandoObjectand other dynamic objects withWriteRecords,NextRecordAsync()andFlushAsync(). ClearRecordCache()iscsv.Context.ClearCache(Caches.WriteRecord).
Bad data and errors
2 had flags that chose whether to throw (ThrowOnBadData, WillThrowOnMissingField, IgnoreReadingExceptions) and
separate callbacks to be told. 3 has one callback for each case, set to a function that throws by default: replace it
to log, or set it to null to ignore.
var config = new CsvConfiguration
{
BadDataCallback = field => Console.WriteLine($"Bad data: {field}"),
IgnoreReadingExceptions = true,
ReadingExceptionCallback = (ex, row) => Console.WriteLine($"Skipped row {row.Row}: {ex.Message}"),
};var config = new Configuration
{
BadDataFound = context => Console.WriteLine($"Bad data: {context.Field}"),
ReadingExceptionOccurred = ex => Console.WriteLine($"Skipped row {ex.ReadingContext.Row}: {ex.Message}"),
};| Callback | Called when | By default |
|---|---|---|
BadDataFound |
A field contains a quote but isn't quoted. Gets the IReadingContext; Field is the field. |
Throws BadDataException. 2 ignored bad data unless ThrowOnBadData was set. |
MissingFieldFound |
A field is asked for by a name the header lacks, or by an index past the end of the record. | Throws MissingFieldException. |
HeaderValidated |
GetRecords, GetRecord and ValidateHeader<T>() check that the header has a column for each mapped member. |
Throws ValidationException. |
ReadingExceptionOccurred |
GetRecords fails to create a record, or DetectColumnCountChanges finds a record of another length. Gets a CsvHelperException. |
Rethrows. When it returns instead, GetRecords skips the record it couldn't create and goes on. |
Exceptions
The exceptions lose the Csv prefix. Instead of a text in Data["CsvHelper"], a CsvHelperException has the
ReadingContext or WritingContext it happened in. The reading context has the Row, Record, RawRecord,
HeaderRecord and CurrentIndex.
public class Order
{
public int Id { get; set; }
}
using var csv = new CsvReader(reader);
try
{
var orders = csv.GetRecords<Order>().ToList();
}
catch (CsvMissingFieldException ex)
{
Console.WriteLine(ex.Data["CsvHelper"]);
}public class Order
{
public int Id { get; set; }
}
using var csv = new CsvReader(reader);
try
{
var orders = csv.GetRecords<Order>().ToList();
}
catch (ValidationException ex)
{
Console.WriteLine($"{ex.Message} Header: {string.Join(",", ex.ReadingContext.HeaderRecord)}");
}A column missing from the header is now a ValidationException from the header check (see
Reading records), so a catch for the missing field exception no longer catches it.
| CsvHelper 2 | CsvHelper 3 | Notes |
|---|---|---|
CsvHelperException |
CsvHelperException |
ReadingContext, WritingContext |
CsvBadDataException |
BadDataException |
|
CsvMissingFieldException |
MissingFieldException |
System has a MissingFieldException too: write CsvHelper.MissingFieldException. |
CsvParserException |
ParserException |
|
CsvReaderException |
ReaderException |
|
CsvWriterException |
WriterException |
|
CsvTypeConverterException |
TypeConverterException |
In CsvHelper.TypeConversion. |
CsvConfigurationException |
ConfigurationException |
In CsvHelper.Configuration. |
| none | ValidationException |
Header validation and Validate in a map. |
Other exceptions are wrapped. In 2 an exception from a converter, such as a FormatException, came out of
GetRecords as it was. Now the record methods throw it as the InnerException of a ReaderException (a
WriterException when writing, a ParserException from the parser), so catch (FormatException) around
GetRecords no longer catches it.
Interfaces and the parser
The interfaces lose the Csv prefix and gain the context and async members. The record methods (GetRecords,
GetRecords(Type)) moved from ICsvReaderRow to IReader, since they read many rows; code that takes an
IReaderRow and calls them takes an IReader instead. The parser's position is in its Context.
using var parser = new CsvParser(reader);
string[] record;
while ((record = parser.Read()) != null)
{
Console.WriteLine($"Row {parser.Row}: {parser.RawRecord}");
}using var parser = new CsvParser(reader);
string[] record;
while ((record = parser.Read()) != null)
{
Console.WriteLine($"Row {parser.Context.Row}: {parser.Context.RawRecord}");
}| CsvHelper 2 | CsvHelper 3 |
|---|---|
ICsvReader |
IReader: Read, ReadHeader, ReadAsync, GetRecords, EnumerateRecords |
ICsvReaderRow |
IReaderRow: Context, Configuration, GetField, TryGetField, GetRecord |
ICsvWriter |
IWriter: NextRecord, Flush, WriteRecords, and the async methods |
| none | IWriterRow: Context, Configuration, WriteField, WriteHeader, WriteRecord, WriteComment |
parser.Row, RawRow, RawRecord, CharPosition, BytePosition |
parser.Context.Row, RawRow, RawRecord, CharPosition, BytePosition |
parser.Configuration, serializer.Configuration (a CsvConfiguration) |
An IParserConfiguration, an ISerializerConfiguration |
new CsvReader(parser), new CsvWriter(serializer) |
Unchanged |
Behavior changes
These compile once the code is ported, but behave differently:
| CsvHelper 2 | CsvHelper 3 | |
|---|---|---|
Read() with a header |
Reads the header, then the first record | Returns the header row as a record; call ReadHeader() |
ReadHeader() |
Reads the next row as the header | Takes the current record as the header |
| A quote in an unquoted field | Read as it is | BadDataException, unless BadDataFound = null |
| A mapped member without a column | CsvMissingFieldException when the first record is read |
ValidationException before the first record |
| Booleans | true, false, 1, 0, yes, y, no, n |
true, false, 1, 0; add others with BooleanValues |
Empty or blank DateTime and DateTimeOffset |
MinValue |
Throws. For empty fields, use a nullable member or Default |
WriteHeader<T>(), WriteRecord(record) |
End the record | Don't; call NextRecord() |
WriteField<T>(null) |
NullReferenceException |
An empty field |
ConvertToString returns null |
An empty field | No field |
Exceptions from GetRecords and GetRecord |
Thrown as they are, details in Data["CsvHelper"] |
Wrapped in a ReaderException unless they are a CsvHelperException, details in ReadingContext |
DetectColumnCountChanges |
The parser throws CsvBadDataException |
The reader calls ReadingExceptionOccurred, which throws BadDataException by default |
Removed with no replacement
- The Excel separator line (
HasExcelSeparator,WriteExcelSeparator()): the Excel-specific code left the library. To write the line, write it as the first record:csv.WriteField("sep=;", false); csv.NextRecord();. To read a file that starts with one, read that line from theTextReader(reader.ReadLine()) before you create theCsvReader. UseExcelLeadingZerosFormatForNumerics: write such a field yourself withcsv.WriteField("=\"" + value + "\"", false). When reading, the="0123"form is now an unquoted field with quotes in it: setBadDataFound = nulland trim the=and quotes in a converter orConvertUsing.CanConvertFromandCanConvertTo: delete the overrides. See Type converters for the converters that relied on them.- The members that were obsolete in 2:
GetField(index, converter)and the otherobjectoverloads without aType,WriteField(type, field),WriteRecord(type, record),CsvClassMap.CreateMap()andPropertyMap<T>(). Use the generic orTypeoverloads, and map in the class map's constructor. PropertyBindingFlags: it had no effect in 2.16.3.MemberTypesandIncludePrivateMemberschoose what auto mapping uses.- The .NET Framework 2.0, 3.5 and 4.0, portable and .NET Standard 1.3 builds: stay on CsvHelper 2 for those platforms, or move to .NET Framework 4.5 or a .NET Standard 2.0 platform.
All API changes
The API of v2.16.3 or v3.0.0 could not be read, so there is no list of every change. The v3.0.0 changelog has the commits.