A lightweight C# utility to read CSV, Excel (XLSX), and JSON files into strongly-typed models using attribute-based mapping and optional streaming support — and to write those models back out as CSV under the same column names.
- ✅ Supports
.csv,.xlsx,.json - ✅ Attribute-based property mapping
- ✅
Read<T>()for small files (sync, memory-based) - ✅
ReadAsync<T>()for large files (async stream) - ✅
WriteCsv<T>()for export — same attribute, so import and export never drift apart - ✅ RFC 4180 CSV parsing: quoted separators, quoted line breaks,
""escapes - ✅ Culture-invariant parsing and formatting
- ✅ Built with clean architecture & extensibility
- ✨ Created with AI-assisted engineering (OpenAI ChatGPT)
- ⚙️ Powered by ExcelDataReader
dotnet add package LS.FileReaderusing LS.FileReader.Attributes;
public class PersonDto
{
[HeaderColumn("Name", "FullName")]
public string Name { get; set; }
[HeaderColumn("Age")]
public int Age { get; set; }
[HeaderColumn("DOB")]
public DateTime BirthDate { get; set; }
}var reader = new LsFileReader();
var result = reader.Read<PersonDto>(formFile); // Supports .csv, .xlsx, .json
foreach (var person in result.Data)
{
Console.WriteLine($"{person.Name}, {person.Age}");
}
foreach (var error in result.ErrorRows)
{
Console.WriteLine($"Row {error.RowIndex} failed: {error.Error}");
}await foreach (var row in reader.ReadAsync<PersonDto>(formFile))
{
if (row.IsSuccess)
{
var person = row.Data;
// Process person
// Do something at here, add to db / signal r notify
}
else
{
Console.WriteLine($"Row {row.RowIndex} failed: {row.Error}");
}
}var batch = new List<PersonDto>();
var batchSize = 100;
var lastNotifyTime = DateTime.UtcNow;
var estimatedTotalRows = 0;
await foreach (var row in reader.ReadAsync<PersonDto>(formFile))
{
if (!row.IsSuccess)
{
Console.WriteLine($"Row {row.RowIndex} failed: {row.Error}");
continue;
}
// Estimate total rows once (based on file size and column count)
if (estimatedTotalRows == 0)
{
estimatedTotalRows = FileEstimateHelper.EstimateTotalRows(formFile, row.ColumnCount);
}
batch.Add(row.Data);
if (batch.Count >= batchSize)
{
await db.SaveBatchAsync(batch); // Replace with your batch save logic
batch.Clear();
}
// Notify progress every 250ms
if ((DateTime.UtcNow - lastNotifyTime).TotalMilliseconds >= 250)
{
double percent = (double)row.RowIndex / estimatedTotalRows * 100;
await notifier.SendProgressAsync(connectionId, percent);
lastNotifyTime = DateTime.UtcNow;
}
}
// Flush remaining rows
if (batch.Count > 0)
{
await db.SaveBatchAsync(batch);
}var writer = new LsFileWriter();
byte[] csv = writer.WriteCsv(people);
return File(csv, "text/csv", "people.csv");The header row comes from the first alias of the same HeaderColumnAttribute the reader matches
against (a property with no attribute uses its own name). So the file you export is a file you can
edit and import straight back — a column cannot be renamed on one side only.
Values holding ,, ", CR or LF are quoted and escaped; null is written as an empty cell; the
output carries a UTF-8 BOM so Excel opens non-ASCII text correctly.
To write into a stream you already own (the stream is left open):
writer.WriteCsv(people, response.Body);| Format | Method | Notes |
|---|---|---|
.csv |
Read<T>(), ReadAsync<T>(), WriteCsv<T>() |
Header must match column/alias |
.xlsx |
Read<T>(), ReadAsync<T>() |
First sheet is used by default |
.json |
Read<T>() only |
Must be JSON array |
- All file reads are limited to 10MB to avoid memory spikes.
- Property names are matched case-insensitively to headers or aliases.
- Internally uses
System.Text.Jsonfor JSON andExcelDataReaderfor Excel files.
- A blank cell means "no value", not "bad value". For a
stringorNullable<T>property it maps tonulland the row succeeds. Only a non-nullable value type still fails the row — there is nowhere to put "nothing". - Numbers and dates are parsed and written with
InvariantCulture, so a file exported on one machine imports identically on another. - Booleans accept
true/false,yes/no,y/n,1/0(case-insensitive), and are written astrue/false. - Get-only computed properties are skipped rather than throwing.
- Parsing follows RFC 4180: a quoted field may contain commas, line breaks, and
""for a literal quote. A UTF-8 BOM is consumed if present. RowIndextherefore counts records, not physical lines — a quoted field spanning a line break is one row, matching what the user sees in the spreadsheet.- A blank line is skipped; a line of nothing but separators (
,,) is a row of empty values, not a blank line.
| Version | Change |
|---|---|
1.1.0 |
Added ILsFileWriter.WriteCsv<T>(). CSV parsing rewritten to RFC 4180 (previously line.Split(','), which silently shifted columns when a field held a comma). Blank cells on optional properties are now null instead of failing the whole row. Culture-invariant number/date handling; lenient booleans. |
1.0.2 |
Performance tuning. |
- Laoseng (developer)
- OpenAI ChatGPT (AI-assisted architecture & implementation)
MIT License
Includes Excel parsing powered by ExcelDataReader (MIT)