ModernLibs.FileHelpers by Piotr-Nawrot
NuGet / site data
Details
Info
Name: ModernLibs.FileHelpers
Modern, AOT-ready rewrite of FileHelpers 3.x for .NET 10. Reads and writes delimited (CSV, TSV, ...) and fixed-length files. Record descriptors are created at compile time by a bundled source generator, so there is no runtime reflection and the library is trimming- and Native AOT-compatible. Reading record structs causes no per-record allocations by the engine (string field values still allocate their text). FileHelpers 3.x-style API (record attributes, engines, error modes); the migration guide lists the differences.
Author: Piotr-Nawrot
NuGet: https://www.nuget.org/packages/ModernLibs.FileHelpers/
You can find more details at https://github.com/ModernLibs/FileHelpers/
Author
Piotr-Nawrot

Original Readme
ModernLibs.FileHelpers
Typed reading and writing of delimited (CSV, TSV, pipe, ...) and fixed-length text files for .NET 10, with a FileHelpers 3.x-style API.
You describe a record layout with attributes. A source generator, shipped inside the package, turns the layout into parsing and formatting code at compile time. There is no runtime reflection, so the library works with Native AOT and trimming.
ModernLibs.FileHelpers is an independent reimplementation modelled on the API of FileHelpers 3.x by Marcos Meli and contributors. FileHelpers 3.x is the reference for its behaviour. It is not the original project: please report problems with this library here, not upstream.
Install
dotnet add package ModernLibs.FileHelpers
Requirements: .NET 10 (net10.0). Requires C# 11 or later (MLFH026): the .NET 10 SDK defaults to
C# 14, so this only matters if your project pins an older <LangVersion>. The package has no runtime
dependencies. The source generator is included as an analyzer;
there is nothing else to install.
Quick start
######### Delimited records
using ModernLibs.FileHelpers;
[DelimitedRecord(",")]
[IgnoreFirst(1)]
public sealed class Customer
{
public int Id;
[FieldQuoted]
public string Name = "";
public decimal Balance;
[FieldConverter(ConverterKind.Date, "yyyy-MM-dd")]
public DateTime Joined;
}
var engine = new FileHelperEngine<Customer>();
Customer[] customers = engine.ReadFile("customers.csv"); // the header line is kept in engine.HeaderText
foreach (Customer c in customers)
Console.WriteLine($"{c.Id}: {c.Name} owes {c.Balance}");
engine.WriteFile("customers-copy.csv", customers); // HeaderText is written back first
Fields are read in declaration order. Use [FieldOrder(n)] when you want the order to be explicit.
######### Fixed-length records
[FixedLengthRecord]
public sealed class Employee
{
[FieldFixedLength(6)]
[FieldAlign(AlignMode.Right, '0')]
public int Id;
[FieldFixedLength(20)]
[FieldTrim(TrimMode.Right)]
public string Name = "";
[FieldFixedLength(10)]
[FieldAlign(AlignMode.Right, '0')]
public decimal Salary;
}
var engine = new FileHelperEngine<Employee>();
Employee[] staff = engine.ReadString(
"000042John Smith 0001234.50\r\n" +
"000043Mary Jones 0000980.00\r\n");
string text = engine.WriteString(staff); // writes the same layout back
[FixedLengthRecord(FixedMode.AllowLessChars)] and the other FixedMode values control how lines
that are shorter or longer than the layout are handled.
######### Record structs without per-record allocations
Records can be struct or record struct types. A struct whose fields are all value types (numbers,
dates, bool, Guid, enums, ...) can be read through a callback or a streaming method without any
heap allocation per record:
[DelimitedRecord(",")]
public record struct Trade
{
public long Id;
public decimal Price;
public int Quantity;
}
var engine = new FileHelperEngine<Trade>();
decimal notional = 0;
int count = engine.ReadFile("trades.csv", trade => notional += trade.Price * trade.Quantity);
The callback overloads of ReadFile, ReadString and ReadSpan, and the streaming ReadSync, do
not allocate a result array or a per-line string. A string field still allocates its string, and
the array-returning overloads still allocate the array. ReadString and ReadSpan also accept a
SpanRecordCallback<T> that receives each record by reference.
Features
- Delimited and fixed-length records described with FileHelpers-style attributes:
[DelimitedRecord],[FixedLengthRecord],[FieldOrder],[FieldFixedLength],[FieldQuoted],[FieldTrim],[FieldAlign],[FieldConverter],[FieldNullValue],[FieldOptional],[FieldInNewLine],[FieldHidden],[FieldNotEmpty],[IgnoreFirst],[IgnoreLast],[IgnoreEmptyLines],[IgnoreCommentedLines],[ConditionalRecord]and more. - Record shapes: classes, structs,
recordandrecord structtypes, including positional records, types without a parameterless constructor (values are bound by constructor parameter name) andrequiredmembers.public,internalandprotected internalfields and properties are mapped; use[FieldHidden]to leave one out.privateandprotectedmembers cannot be mapped, and the ones FileHelpers 3.x would read as columns are reported (MLFH023,MLFH028). - Field types:
string,char,bool, all integer types,decimal,double,float,DateTime,DateOnly,TimeOnly,DateTimeOffset,TimeSpan,Guid, enums and their nullable forms. Other types use a custom converter. - Converters: built-in converters with FileHelpers 3.x parsing rules and culture-independent
defaults, per-field culture (
[FieldConverter(..., CultureName = "de-DE")]), 3.x-styleConverterBasecustom converters, and span-basedISpanConverter<T>converters. - Engines:
FileHelperEngine<T>(read/write string, span, file,TextReader/TextWriter, append),FileHelperAsyncEngine<T>(record-by-recordBeginRead*/ReadNextandBeginWrite*/WriteNext),MultiRecordEngine(several record types in one file) andMasterDetailEngine<TMaster, TDetail>(grouped master/detail lines). - Streaming:
ReadSync(IEnumerable<T>),ReadAsync(IAsyncEnumerable<T>with cancellation), callback reads, and async variants of the multi-record and master/detail engines. - Parallel reads:
ParallelReadFile/ParallelReadStringextension methods split large inputs into chunks and keep sequential semantics (same records, errors and line numbers). - Events:
BeforeReadRecord,AfterReadRecord,BeforeWriteRecord,AfterWriteRecord,Progress, andINotifyRead<T>/INotifyWrite<T>on the record type. - Errors: FileHelpers 3.x exception types and
ErrorModehandling (see below). - Quoting as in FileHelpers 3.x: per-field quote character,
QuoteModeon read and write, and multiline quoted values allowed by default. - Compile-time diagnostics: layout mistakes are reported as build errors and warnings
(
MLFH001-MLFH029), see the diagnostics reference. Diagnostics come from an analyzer shipped in the generator assembly.MLFHwarnings can be suppressed as usual; errors that block the descriptor cannot (#pragma,.editorconfig,NoWarnare ignored). If they are hidden with[SuppressMessage]orRunAnalyzers=false, using the record throws aBadUsageExceptionthat names the errors (see suppressing diagnostics).
Error handling
By default (ErrorMode.ThrowException) the first bad record stops the read with an exception that
carries the line number:
| Exception | Raised when |
|---|---|
ConvertException | A field value cannot be converted (also [FieldNotEmpty] violations). Has FieldName, FieldType, FieldStringValue, LineNumber, ColumnNumber. |
NullValueNotFoundException | A non-nullable value-type field is empty and has no [FieldNullValue]. |
BadUsageException | The line does not match the layout (extra fields, wrong fixed width, unclosed quote, ...). |
FileHelpersException | Base class of all of the above; also raised for lines with too few fields. |
try
{
engine.ReadFile("customers.csv");
}
catch (ConvertException ex)
{
Console.WriteLine($"Line {ex.LineNumber}, field {ex.FieldName}: '{ex.FieldStringValue}' is not a {ex.FieldType.Name}");
}
With ErrorMode.SaveAndContinue bad records are skipped and collected; ErrorMode.IgnoreAndContinue
skips them silently:
var engine = new FileHelperEngine<Customer> \{ ErrorMode = ErrorMode.SaveAndContinue };
Customer[] good = engine.ReadFile("customers.csv");
foreach (ErrorInfo error in engine.ErrorManager.Errors)
Console.WriteLine($"Line {error.LineNumber}, field {error.FieldName}: {error.ExceptionInfo?.Message}");
Streaming large files
await foreach (Customer c in engine.ReadAsync("big.csv", cancellationToken))
{
// one record at a time
}
using var reader = new FileHelperAsyncEngine<Customer>();
reader.BeginReadFile("big.csv");
while (reader.ReadNext() is \{ \} customer)
{
// FileHelpers 3.x-style pull loop
}
See Opt-in features for callbacks, record recycling and parallel reads.
Native AOT and trimming
All parsing and formatting code is generated at compile time. The library uses no reflection,
Expression compilation or DynamicMethod, and is built with IsAotCompatible and IsTrimmable
enabled, so the trim and AOT analyzers check it on every build. Applications that use it can publish
with PublishAot=true.
Performance
The read path works on ReadOnlySpan<char>: lines and fields are sliced, not copied, and numeric,
date and other value-type fields are parsed straight from the span. Buffers are pooled. Struct records
with value-type fields are read without per-record allocations (see above), and this is checked by
the test suite.
The repository contains a BenchmarkDotNet suite that compares this library with FileHelpers 3.5.2, Sep, Sylvan.Data.Csv and CsvHelper. In the 4.8.0 results (Ryzen 9 5900X, WSL2, .NET 10, 100K rows):
- Reading a 25-column delimited file into objects: 182 ms, against 344 ms for FileHelpers 3.5.2 and
352 ms for CsvHelper. Sylvan (107 ms) and Sep (122 ms) are faster, and allocate about 2.5 times less
than
ReadFile(path), which loads the whole file into a string (the streamingReadFile(path, callback)overload allocates as little as Sep). - Reading a fixed-width file: 53 ms, against 84 ms for FileHelpers 3.5.2 (which allocates 16 % less).
- Writing the same records to a string: 70 ms, against 75 ms for Sep and 173 ms for CsvHelper.
Results depend on hardware and record shape; see BENCHMARKS.md to run the suite on your own hardware.
Migrating from FileHelpers 3.x
The API follows FileHelpers 3.x: the same attribute names, engine classes, events and exception types. Most code migrates by changing the namespace:
- using FileHelpers;
+ using ModernLibs.FileHelpers;
Some things are different. Runtime-reflection features (ClassBuilder, non-generic engines,
DelimitedFileEngine options) are not available, a few signatures changed, and some defaults differ
(UTF-8 encoding, \r\n line endings on every platform). The
migration guide lists every
difference we know of.
Versioning
4.x is a ground-up modern reimplementation that follows FileHelpers 3.x, so its major version continues after 3.x. Versions 4.0.0 to 4.7.0 were development milestones, published only as source releases on GitHub. 4.8.0 is the first release on NuGet. From 4.8.0 on, the package follows Semantic Versioning. See the changelog.
Documentation
- Migration guide - every difference from FileHelpers 3.x
- Diagnostics reference -
MLFH001-MLFH029 - Opt-in features - callbacks, streaming, recycling, parallel reads
- Gap analysis - FileHelpers 3.x features that are not included, and why
- Benchmarks
- Changelog
- Samples
Contributing
Issues and pull requests are welcome. See CONTRIBUTING.md for how to build, test and submit changes.
License and credits
MIT, see LICENSE.
The API and behaviour are modelled on FileHelpers by Marcos Meli and contributors (also MIT licensed). Thank you to everyone who built and maintained it.
About
Read/write records based on fixed-width or delimited formats
How to use
Example (source csproj, source files)
- CSharp Project
- Person.cs
- Program.cs
This is the CSharp Project that references ModernLibs.FileHelpers
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ModernLibs.FileHelpers" Version="4.8.0" />
</ItemGroup>
</Project>
This is the use of ModernLibs.FileHelpers in Person.cs
using ModernLibs.FileHelpers;
namespace SerializerDemo;
[DelimitedRecord(",")]
public partial class Person
{
[FieldConverter(ConverterKind.Date, "yyyy-MM-dd")]
public DateTime DOB \{ get; set; }
[FieldQuoted]
public string Name \{ get; set; \} = string.Empty;
}
This is the use of ModernLibs.FileHelpers in Program.cs
using ModernLibs.FileHelpers;
using SerializerDemo;
var engine = new FileHelperEngine<Person>();
Person p = new () \{ Name= "Andrei Ignat" , DOB=new DateTime(1970, 4, 16)};
var data= engine.WriteString([p]);
var p1 = engine.ReadString(data);
Console.WriteLine(p1[0].Name);
Console.WriteLine(p1[0].DOB);
Generated Files
Those are taken from $(BaseIntermediateOutputPath)\GX
- SerializerDemo.Person.1e57ba39.DelimitedDescriptor.g.cs
// <auto-generated/>
#nullable enable
#pragma warning disable CA2255 // ModuleInitializer in source-generated consumer code
#pragma warning disable CS0612, CS0618 // obsolete record members
namespace SerializerDemo;
file sealed class __PersonDescriptor : global::ModernLibs.FileHelpers.IRecyclableDescriptor<global::SerializerDemo.Person>
{
public static readonly __PersonDescriptor Instance = new();
private static class __Conv
{
internal static readonly global::ModernLibs.FileHelpers.Converters.DateTimeConverter __conv_field0 = new("yyyy-MM-dd", null);
}
private static int __Column(global::System.ReadOnlySpan<char> lines, global::System.ReadOnlySpan<char> value)
{
if (global::System.MemoryExtensions.Overlaps(lines, value, out int offset))
return offset + 1;
if (!value.IsEmpty)
return -1;
nint bytes = global::System.Runtime.CompilerServices.Unsafe.ByteOffset(
ref global::System.Runtime.InteropServices.MemoryMarshal.GetReference(lines),
ref global::System.Runtime.InteropServices.MemoryMarshal.GetReference(value));
return bytes >= 0 && bytes <= (nint)lines.Length * sizeof(char) ? (int)(bytes / sizeof(char)) + 1 : -1;
}
private static global::ModernLibs.FileHelpers.ConvertException __ConvertError(global::System.ReadOnlySpan<char> lines, global::System.ReadOnlySpan<char> value, global::System.Type type, string field, string? extra, global::System.Exception? inner) =>
new(new string(value), type, field, -1, __Column(lines, value), extra, inner);
private static global::ModernLibs.FileHelpers.NullValueNotFoundException __NullValueNotFound(global::System.ReadOnlySpan<char> lines, global::System.ReadOnlySpan<char> value, string field) =>
new(-1, __Column(lines, value), "No value found for the value type field: '" + field + "' Class: 'Person'. You must use the [FieldNullValue] attribute because this is a value type and can't be null or use a Nullable Type instead of the current type.");
private static void __WriteText(global::System.Buffers.IBufferWriter<char> writer, global::System.ReadOnlySpan<char> text)
{
if (text.IsEmpty) return;
text.CopyTo(writer.GetSpan(text.Length));
writer.Advance(text.Length);
}
private static void __WriteQuoted(global::System.Buffers.IBufferWriter<char> writer, global::System.ReadOnlySpan<char> text, char quote)
{
// Indexed loops: foreach over a span needs C# 13 against .NET 10, whose span enumerator implements interfaces.
int count = global::System.MemoryExtensions.Count(text, quote);
int total = text.Length + count + 2;
var dest = writer.GetSpan(total);
dest[0] = quote;
int pos = 1;
for (int i = 0; i < text.Length; i++)
{
char c = text[i];
dest[pos++] = c;
if (c == quote) dest[pos++] = quote;
}
dest[pos] = quote;
writer.Advance(total);
}
private static void __WriteQuotedField(global::System.Buffers.IBufferWriter<char> writer, global::System.ReadOnlySpan<char> text, char quote, bool always, bool multilineWrite, string separator, string field)
{
bool newLine = global::System.MemoryExtensions.IndexOfAny(text, '\r', '\n') >= 0;
if (newLine && !multilineWrite)
throw new global::ModernLibs.FileHelpers.BadUsageException("One value for the field " + field + " has a new line inside. To allow this value to be written you must add a FieldQuoted attribute with the multiline option set.");
if (always || newLine || global::System.MemoryExtensions.IndexOf(text, separator, global::System.StringComparison.Ordinal) >= 0)
__WriteQuoted(writer, text, quote);
else
__WriteText(writer, text);
}
private static void __WritePadded(global::System.Buffers.IBufferWriter<char> writer, global::System.ReadOnlySpan<char> text, int width, int align, char pad, bool overflowError, string field)
{
if (text.Length > width)
{
if (overflowError)
throw new global::System.InvalidOperationException($"Field '{field}' exceeds fixed length {width} (actual: {text.Length} characters).");
text = text.Slice(0, width);
}
var dest = writer.GetSpan(width);
int padding = width - text.Length;
int left = align == 2 ? padding : align == 1 ? padding / 2 : 0;
dest.Slice(0, left).Fill(pad);
text.CopyTo(dest.Slice(left));
dest.Slice(left + text.Length, padding - left).Fill(pad);
writer.Advance(width);
}
private static global::System.ReadOnlySpan<char> __Format<TConverter, TValue>(TConverter converter, TValue value, global::System.Span<char> buffer, ref char[]? rented)
where TConverter : global::ModernLibs.FileHelpers.Converters.ISpanConverter<TValue>
{
int written;
while (!converter.TryFormat(value, buffer, out written))
{
if (buffer.Length >= 0x100000)
throw new global::System.FormatException("The field value could not be formatted by " + converter.GetType().Name + ".");
int size = buffer.Length * 2;
if (rented is not null)
global::System.Buffers.ArrayPool<char>.Shared.Return(rented);
rented = global::System.Buffers.ArrayPool<char>.Shared.Rent(size);
buffer = rented;
}
return buffer.Slice(0, written);
}
public global::SerializerDemo.Person Parse(global::System.ReadOnlySpan<char> lines, int lineCount)
{
const int __expectedLines = 1; // 1 + 0 FieldInNewLine fields
if (lineCount != __expectedLines)
throw new global::ModernLibs.FileHelpers.BadUsageException($"Expected {__expectedLines} lines for the record but received {lineCount}. Check that all multi-line fields are present.");
var line = lines;
char[]? __qRent1 = null;
try
{
// Quote-aware delimited splitting (3.x DelimitedField rules; quotes only in [FieldQuoted] fields).
global::System.ReadOnlySpan<char> __f0;
global::System.ReadOnlySpan<char> __qrem1;
bool __qhas1;
int __qsSep0 = global::System.MemoryExtensions.IndexOf(line, ',');
__f0 = __qsSep0 < 0 ? line : line.Slice(0, __qsSep0);
__qhas1 = __qsSep0 >= 0;
__qrem1 = __qsSep0 < 0 ? global::System.ReadOnlySpan<char>.Empty : line.Slice(__qsSep0 + 1);
global::System.ReadOnlySpan<char> __f1;
bool __fq1 = false;
int __qsPos1 = 0;
int __qsLen1 = __qrem1.Length;
if (__qsPos1 < __qsLen1 && __qrem1[__qsPos1] == '"')
{
int __qsStart1 = __qsPos1;
__qsPos1++;
while (true)
{
int __qsQ1 = global::System.MemoryExtensions.IndexOf(__qrem1.Slice(__qsPos1), '"');
if (__qsQ1 < 0)
throw new global::ModernLibs.FileHelpers.BadUsageException(-1, __Column(lines, __qrem1), "The field 'Name' has an unclosed quoted string. Check that every opening \" has a closing \" (use \"\" for a quote inside the value) or allow multiline values in [FieldQuoted].");
__qsPos1 += __qsQ1;
if (__qsPos1 + 1 < __qsLen1 && __qrem1[__qsPos1 + 1] == '"') \{ __qsPos1 += 2; continue; }
__qsPos1++;
break;
}
__f1 = __qrem1.Slice(__qsStart1, __qsPos1 - __qsStart1);
__fq1 = true;
}
else
{
__f1 = __qrem1;
}
// [FieldQuoted] strip + unescape for field 'Name'.
if (__fq1)
{
var __qInner1 = __f1.Slice(1, __f1.Length - 2);
bool __hasDoubled1 = false;
for (int __si1 = 0; __si1 < __qInner1.Length - 1; __si1++)
{
if (__qInner1[__si1] == '"' && __qInner1[__si1 + 1] == '"')
{
__hasDoubled1 = true;
break;
}
}
if (!__hasDoubled1)
{
__f1 = __qInner1;
}
else
{
// Rented buffer, returned in the finally block after the value is materialised.
var __qBuf1 = global::System.Buffers.ArrayPool<char>.Shared.Rent(__qInner1.Length);
__qRent1 = __qBuf1;
int __qLen1 = 0;
for (int __qi1 = 0; __qi1 < __qInner1.Length; __qi1++)
{
char __qc1 = __qInner1[__qi1];
if (__qc1 == '"' && __qi1 + 1 < __qInner1.Length && __qInner1[__qi1 + 1] == '"')
{
__qBuf1[__qLen1++] = '"';
__qi1++;
}
else
{
__qBuf1[__qLen1++] = __qc1;
}
}
__f1 = new global::System.ReadOnlySpan<char>(__qBuf1, 0, __qLen1);
}
}
global::System.DateTime __v0;
if (global::System.MemoryExtensions.IsWhiteSpace(__f0))
throw __NullValueNotFound(lines, __f0, "DOB");
else if (__Conv.__conv_field0.TryParse(__f0, out global::System.DateTime __p0))
__v0 = __p0;
else
throw __ConvertError(lines, __f0, typeof(global::System.DateTime), "DOB", null, null);
string __v1 = new string(__f1);
return new global::SerializerDemo.Person
{
DOB = __v0,
Name = __v1
};
}
finally
{
if (__qRent1 is not null)
global::System.Buffers.ArrayPool<char>.Shared.Return(__qRent1);
}
}
public void ParseInto(global::System.ReadOnlySpan<char> lines, int lineCount, global::SerializerDemo.Person existing)
{
const int __expectedLines = 1; // 1 + 0 FieldInNewLine fields
if (lineCount != __expectedLines)
throw new global::ModernLibs.FileHelpers.BadUsageException($"Expected {__expectedLines} lines for the record but received {lineCount}. Check that all multi-line fields are present.");
var line = lines;
char[]? __qRent1 = null;
try
{
// Quote-aware delimited splitting (3.x DelimitedField rules; quotes only in [FieldQuoted] fields).
global::System.ReadOnlySpan<char> __f0;
global::System.ReadOnlySpan<char> __qrem1;
bool __qhas1;
int __qsSep0 = global::System.MemoryExtensions.IndexOf(line, ',');
__f0 = __qsSep0 < 0 ? line : line.Slice(0, __qsSep0);
__qhas1 = __qsSep0 >= 0;
__qrem1 = __qsSep0 < 0 ? global::System.ReadOnlySpan<char>.Empty : line.Slice(__qsSep0 + 1);
global::System.ReadOnlySpan<char> __f1;
bool __fq1 = false;
int __qsPos1 = 0;
int __qsLen1 = __qrem1.Length;
if (__qsPos1 < __qsLen1 && __qrem1[__qsPos1] == '"')
{
int __qsStart1 = __qsPos1;
__qsPos1++;
while (true)
{
int __qsQ1 = global::System.MemoryExtensions.IndexOf(__qrem1.Slice(__qsPos1), '"');
if (__qsQ1 < 0)
throw new global::ModernLibs.FileHelpers.BadUsageException(-1, __Column(lines, __qrem1), "The field 'Name' has an unclosed quoted string. Check that every opening \" has a closing \" (use \"\" for a quote inside the value) or allow multiline values in [FieldQuoted].");
__qsPos1 += __qsQ1;
if (__qsPos1 + 1 < __qsLen1 && __qrem1[__qsPos1 + 1] == '"') \{ __qsPos1 += 2; continue; }
__qsPos1++;
break;
}
__f1 = __qrem1.Slice(__qsStart1, __qsPos1 - __qsStart1);
__fq1 = true;
}
else
{
__f1 = __qrem1;
}
// [FieldQuoted] strip + unescape for field 'Name'.
if (__fq1)
{
var __qInner1 = __f1.Slice(1, __f1.Length - 2);
bool __hasDoubled1 = false;
for (int __si1 = 0; __si1 < __qInner1.Length - 1; __si1++)
{
if (__qInner1[__si1] == '"' && __qInner1[__si1 + 1] == '"')
{
__hasDoubled1 = true;
break;
}
}
if (!__hasDoubled1)
{
__f1 = __qInner1;
}
else
{
// Rented buffer, returned in the finally block after the value is materialised.
var __qBuf1 = global::System.Buffers.ArrayPool<char>.Shared.Rent(__qInner1.Length);
__qRent1 = __qBuf1;
int __qLen1 = 0;
for (int __qi1 = 0; __qi1 < __qInner1.Length; __qi1++)
{
char __qc1 = __qInner1[__qi1];
if (__qc1 == '"' && __qi1 + 1 < __qInner1.Length && __qInner1[__qi1 + 1] == '"')
{
__qBuf1[__qLen1++] = '"';
__qi1++;
}
else
{
__qBuf1[__qLen1++] = __qc1;
}
}
__f1 = new global::System.ReadOnlySpan<char>(__qBuf1, 0, __qLen1);
}
}
global::System.DateTime __v0;
if (global::System.MemoryExtensions.IsWhiteSpace(__f0))
throw __NullValueNotFound(lines, __f0, "DOB");
else if (__Conv.__conv_field0.TryParse(__f0, out global::System.DateTime __p0))
__v0 = __p0;
else
throw __ConvertError(lines, __f0, typeof(global::System.DateTime), "DOB", null, null);
string __v1 = new string(__f1);
existing.DOB = __v0;
existing.Name = __v1;
}
finally
{
if (__qRent1 is not null)
global::System.Buffers.ArrayPool<char>.Shared.Return(__qRent1);
}
}
public void Format(global::SerializerDemo.Person record, global::System.Buffers.IBufferWriter<char> writer)
{
{
global::System.Span<char> __b0 = stackalloc char[64];
char[]? __r0 = null;
scoped global::System.ReadOnlySpan<char> __t0;
__t0 = __Format(__Conv.__conv_field0, record.DOB, __b0, ref __r0);
__WriteText(writer, __t0);
if (__r0 is not null)
global::System.Buffers.ArrayPool<char>.Shared.Return(__r0);
}
\{ writer.GetSpan(1)[0] = ','; writer.Advance(1); }
{
var __t1 = global::System.MemoryExtensions.AsSpan(record.Name);
__WriteQuotedField(writer, __t1, '"', true, true, ",", "Name");
}
}
public global::System.ReadOnlySpan<global::ModernLibs.FileHelpers.FieldDescriptor> Fields =>
new global::System.ReadOnlySpan<global::ModernLibs.FileHelpers.FieldDescriptor>(_fields);
public global::ModernLibs.FileHelpers.RecordOptions Options =>
new()
{
IgnoreFirst = 0,
IgnoreLast = 0,
IgnoreEmptyLines = false,
CommentMarker = '\0',
CommentAnyPlace = true,
Delimiter = ",",
Condition = (global::ModernLibs.FileHelpers.RecordCondition)0,
ConditionPattern = "",
LineCountPerLogicalRecord = 1,
MultilineMode = (global::ModernLibs.FileHelpers.MultilineMode)0,
IgnoreEmptySpaces = false,
QuoteChar = '"',
};
public global::SerializerDemo.Person CreateDefaultInstance()
{
return new global::SerializerDemo.Person();
}
private static readonly global::ModernLibs.FileHelpers.FieldDescriptor[] _fields =
new global::ModernLibs.FileHelpers.FieldDescriptor[]
{
new(nameof(global::SerializerDemo.Person.DOB), typeof(global::System.DateTime), 0, -1, -1, null, false, null, null, false, (global::ModernLibs.FileHelpers.TrimMode)0, '\0', (global::ModernLibs.FileHelpers.MultilineMode)0, null),
new(nameof(global::SerializerDemo.Person.Name), typeof(string), 1, -1, -1, null, false, null, null, false, (global::ModernLibs.FileHelpers.TrimMode)0, '"', (global::ModernLibs.FileHelpers.MultilineMode)0, null),
};
}
file static class __PersonRegistration
{
[global::System.Runtime.CompilerServices.ModuleInitializer]
internal static void Register()
{
global::ModernLibs.FileHelpers.FileHelperRegistry<global::SerializerDemo.Person>
.SetDescriptor(__PersonDescriptor.Instance);
}
}
Useful
Download Example (.NET C#)
Share ModernLibs.FileHelpers
https://ignatandrei.github.io/RSCG_Examples/v2/docs/ModernLibs.FileHelpers
Category "Serializer" has the following generators:
1 Csvcsharp
2025-10-05
2 GenPack
2024-11-06
3 jsonConverterSourceGenerator
2023-10-30
4 JsonPolymorphicGenerator
2023-10-06
5 ModernLibs.FileHelpers
2026-10-01
6 Nino
2025-08-06
7 ProtobufSourceGenerator
2023-09-24
8 Schema
2025-08-13
9 serde
2026-08-20
10 StackXML
2025-08-01
11 System.Text.Json
2023-04-16
12 VYaml
2025-10-03