Target Used by Scanner
net10.0 .NET 10 and later SIMD: AVX-512, AVX2 or 128-bit vectors (SSE, NEON), chosen at startup
net8.0 .NET 8 and 9 as net10.0, without the AVX-512 VBMI2 compress of the index, whose intrinsics came with .NET 10
net47 .NET Framework 4.7 and later 64-bit integers: the masks of each 64-byte block from eight ulongs, then the same pipeline
netstandard2.1 .NET Core 3.0 to .NET 7, Mono, Unity 2021.2 and later same as net47
netstandard2.0 everything else: .NET Core 2.x, Unity before 2021.2 same as net47

net47 and netstandard2.0 depend on two Microsoft packages, System.Memory and Microsoft.Bcl.AsyncInterfaces; netstandard2.1, which has spans and async streams built in, only on System.Runtime.CompilerServices.Unsafe. Every build reads and writes exactly the same CSV: the tests run on .NET 10, on .NET 8 and on .NET Framework 4.8 (the net47 build, whose code the netstandard2.0 and netstandard2.1 builds share), and on .NET 10 the 64-bit scanner is also checked against the SIMD ones. The differences between the .NET builds (net10.0 and net8.0) and the others:

  • Parsing to a type. Parse<T>, TryParse<T>, ParseOrNull<T> and ParseExact<T> parse the built-in types (int, long, short, byte, sbyte, ushort, uint, ulong, double, float, decimal, bool, char, DateTime, DateTimeOffset, TimeSpan, Guid, BigInteger, IPAddress, string and enums) on every target. Any other ISpanParsable<T> type (DateOnly, Half, your own types) needs generic math, so only the .NET builds parse it; the other builds throw NotSupportedException. Enums are not ISpanParsable<T>: on the .NET builds their overloads are the extension methods of CsvEnumExtensions, so the calls are the same everywhere. On every target, Parse(index, parser), TryParse(index, parser, out value) and CsvField.ParseOrNull(parser) take a parser that gets the field as a string and the reader's format provider: reader.Parse(3, (text, _) => Version.Parse(text)). [CsvRecord] members follow the same rule.
  • Writing values. WriteField<T> takes IUtf8SpanFormattable values on .NET and IFormattable values elsewhere (numbers in the invariant culture are still formatted straight to UTF-8). The DateOnly, Half and Int128 overloads are .NET only. On the older targets WriteRecord and WriteHeader also take arrays, for compilers before C# 13.
  • Floating point. Doubles and floats are written in the shortest form that round-trips, as .NET Core 3.0 and later write them, not as .NET Framework's 15-digit ToString(). Parsing is exactly rounded, except for inputs the fast parser leaves to the BCL (thousands separators, other cultures, more than 19 significant digits right on a rounding boundary): .NET Framework's double.Parse is not always exactly rounded.
  • C# version. The options are records with init properties: setting them needs C# 9 or later in the calling project (<LangVersion>latest</LangVersion> works for .NET Framework projects too).
  • Speed. Without SIMD, and with the slower spans of the System.Memory package, expect about a quarter of the .NET 10 speed. On an Intel Core i7-12800H, reading 52 MB of the PackageAssets data as strings took 89 ms on .NET Framework 4.8 x64 (23 ms on .NET 10). Scanning alone ran at 1.5 GB/s (7.2 GB/s on .NET 10), ParallelEnumerate at 1.2 GB/s.

Trimming and native AOT

The net10.0 and net8.0 builds of CsvHelper and CsvHelper.Linq are marked IsAotCompatible and IsTrimmable, so the trimming and AOT analyzers check them on every build. Reading, writing and [CsvRecord] types work in trimmed and native AOT applications: the generated mappers are registered by a module initializer, not found with reflection. Two things need code generated at run time: