Skip to content

Refactor CSV and Excel enumerators for improved performance and clarity - #34

Merged
GabrielMarquezMatte merged 8 commits into
masterfrom
develop
Jul 21, 2026
Merged

Refactor CSV and Excel enumerators for improved performance and clarity#34
GabrielMarquezMatte merged 8 commits into
masterfrom
develop

Conversation

@GabrielMarquezMatte

Copy link
Copy Markdown
Owner

This pull request refactors and modernizes the Excel parsing core, with a focus on simplifying and generalizing the column parsing logic, improving date and time handling, and updating project configuration for consistency and maintainability. The changes consolidate multiple structurally similar parser methods into generic implementations, introduce a shared Excel epoch conversion utility, and streamline project build settings.

Parser refactoring and simplification

  • Refactored ColumnParserFactory to replace many type-specific parser methods with generic BuildValue and BuildNullableValue factories, reducing code duplication and centralizing logic for all value-type columns. This includes new generic readers for bool, DateTime, DateOnly, TimeOnly, and Guid, and uses delegates to select the correct parsing strategy based on configuration. [1] [2] [3] [4] [5]
  • Updated the logic for handling CSV text dates to select the appropriate parsing strategy via a delegate, instead of duplicating logic for each type. [1] [2]

Date and time handling improvements

  • Introduced the new ExcelEpoch utility class to centralize and standardize conversions between Excel serial dates and OADate, ensuring consistent handling of the 1900/1904 date systems and the Excel leap year bug.
  • Improved parsing of date and time types by using [SkipLocalsInit] for stack-allocated buffers, which can improve performance and reduce unnecessary zero-initialization. [1] [2]

API and type changes

  • Updated the parser API to return the unified generic type ExcelEnumerable<T, TReader, TEnumerator> for both synchronous and asynchronous XLS parsing, replacing the old XlsExcelEnumerable<T> type. [1] [2]

Project configuration updates

  • Moved common language and analysis settings (LangVersion, ImplicitUsings, Nullable, Deterministic) to Directory.Build.props for consistency across all projects, and removed them from ExcelReader.Core.csproj. Also enabled AllowUnsafeBlocks in the core project. [1] [2] [3]
  • Cleaned up release build settings by removing debug symbols and debug type from the release configuration in Directory.Build.props.

Minor improvements

  • Added comments to clarify special handling for Guid parsing across different .NET versions.
  • Added missing using System.Runtime.CompilerServices; for [SkipLocalsInit] attribute.

Gabriel Matte and others added 8 commits July 14, 2026 13:59
…chronous and asynchronous row enumeration

- Introduced `SyncRowEnumerator` and `AsyncRowEnumerator` base classes to encapsulate common enumeration logic for CSV and Excel formats.
- Updated `CsvEnumerable` and `ExcelEnumerable` to inherit from the new base classes, simplifying their implementations.
- Removed redundant code and improved performance by leveraging shared functionality.
- Added `ZipReaderOpen` utility class to streamline ZIP archive handling in `XlsxReader` and `XlsbReader`.
- Implemented `WriterStateGuard` for consistent state management across workbook writers.
- Enhanced error handling and resource management in ZIP-related operations.
- Deleted obsolete `XlsExcelEnumerable` class as its functionality is now covered by the refactored enumerators.
…rformance optimization across multiple files.
…rdWriter and BiffStringEncoder for cleaner code
- Introduced ProjectionRules class to encapsulate row classification logic and error handling for missing required values.
- Updated CsvEnumerable and RowProjector to utilize ProjectionRules for row processing.
- Enhanced ProjectionStep enum to include BuildMap step for better control flow.
- Refactored CellAccumulator to add methods for handling boolean and error cell types.
- Created PooledStreamRowEnumerator base class to reduce code duplication in CSV, XLSB, and XLSX enumerators.
- Improved error handling in Excel reader methods to ensure proper disposal of streams on failure.
- Streamlined cell writing logic in CellFormatter to reduce redundancy.
- Updated DateSerial to leverage ExcelEpoch for date serial conversions.
- Simplified RowWriter methods to directly write values instead of calling formatter methods.
- Added tests to cover edge cases in formatting and parsing.
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 84.25197% with 60 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.81%. Comparing base (bf6a94d) to head (2d7620f).

Files with missing lines Patch % Lines
...elReader.Core/Parser/Internal/RowEnumeratorBase.cs 66.66% 10 Missing and 3 partials ⚠️
...c/ExcelReader.Core/Reader/XlsxReader.Enumerator.cs 70.00% 5 Missing and 4 partials ⚠️
.../ExcelReader.Core/Writer/Internal/CellFormatter.cs 60.00% 0 Missing and 8 partials ⚠️
...Reader.Core/Parser/Internal/ColumnParserFactory.cs 83.72% 3 Missing and 4 partials ⚠️
src/ExcelReader.Core/ValueObjects/Cell.cs 30.00% 6 Missing and 1 partial ⚠️
src/ExcelReader.Core/Reader/XlsReader.cs 70.58% 0 Missing and 5 partials ⚠️
src/ExcelReader.Core/ExcelEpoch.cs 75.00% 2 Missing and 2 partials ⚠️
src/ExcelReader.Core/Reader/Excel.cs 91.66% 1 Missing and 1 partial ⚠️
...xcelReader.Core/Parser/Internal/ProjectionRules.cs 87.50% 0 Missing and 1 partial ⚠️
...rc/ExcelReader.Core/Reader/XlsReader.Enumerator.cs 95.83% 1 Missing ⚠️
... and 3 more
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #34      +/-   ##
==========================================
+ Coverage   87.66%   88.81%   +1.14%     
==========================================
  Files          78       83       +5     
  Lines        5701     5464     -237     
  Branches     1071     1038      -33     
==========================================
- Hits         4998     4853     -145     
+ Misses        459      385      -74     
+ Partials      244      226      -18     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

Copy link
Copy Markdown

Benchmark Results

Measured on ubuntu-latest (GitHub Actions). Runner noise may affect absolute numbers; use these for relative comparisons within a PR.

ExcelReader.Benchmarks.ColdStartBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.86GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
  [Host]     : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3
  Job-GOEFXF : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

InvocationCount=1  IterationCount=1  LaunchCount=16  
RunStrategy=ColdStart  UnrollFactor=1  WarmupCount=0  

Method Rows Mean Error StdDev Allocated
TypedParseFirstUse 200 45.56 ms 0.186 ms 0.183 ms 29.55 KB
RecordWriteFirstUse 200 24.96 ms 0.175 ms 0.171 ms 79.41 KB

ExcelReader.Benchmarks.CsvParseBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
  [Host]     : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio Gen0 Gen1 Allocated Alloc Ratio
ExcelParserSync 50000 8.241 ms 0.0816 ms 0.0212 ms 1.00 234.3750 - 3.86 MB 1.00
ExcelParserAsync 50000 8.253 ms 0.1419 ms 0.0369 ms 1.00 234.3750 - 3.86 MB 1.00
Sep 50000 14.502 ms 0.0451 ms 0.0117 ms 1.76 234.3750 - 3.87 MB 1.00
Sylvan 50000 19.475 ms 0.4770 ms 0.1239 ms 2.36 656.2500 62.5000 10.95 MB 2.84
CsvHelperLib 50000 36.060 ms 0.2885 ms 0.0749 ms 4.38 857.1429 71.4286 14.41 MB 3.73

ExcelReader.Benchmarks.CsvReadBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
  [Host]     : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio Gen0 Gen1 Allocated Alloc Ratio
ExcelReader 50000 5.934 ms 0.0741 ms 0.0115 ms 1.00 - - 232 B 1.00
ExcelReaderAsync 50000 5.557 ms 0.0492 ms 0.0128 ms 0.94 - - 352 B 1.52
Sep 50000 13.609 ms 0.0515 ms 0.0134 ms 2.29 - - 4024 B 17.34
Sylvan 50000 6.514 ms 0.0409 ms 0.0063 ms 1.10 93.7500 7.8125 1688701 B 7,278.88
CsvHelperLib 50000 37.038 ms 0.2155 ms 0.0333 ms 6.24 857.1429 71.4286 15073424 B 64,971.66

ExcelReader.Benchmarks.CsvWriteBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
  [Host]     : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio RatioSD Gen0 Gen1 Gen2 Allocated Alloc Ratio
ExcelReaderWriter 50000 9.311 ms 0.3096 ms 0.0479 ms 1.00 0.01 500.0000 500.0000 500.0000 4 MB 1.00
Sep 50000 9.930 ms 0.5860 ms 0.1522 ms 1.07 0.02 500.0000 500.0000 500.0000 4.01 MB 1.00
SylvanWriter 50000 10.065 ms 0.3427 ms 0.0890 ms 1.08 0.01 500.0000 500.0000 500.0000 4.04 MB 1.01
CsvHelperLib 50000 20.240 ms 1.3394 ms 0.2073 ms 2.17 0.02 1187.5000 656.2500 593.7500 13.79 MB 3.44

ExcelReader.Benchmarks.ParseBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
  [Host]     : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
ExcelParserSync 50000 22.449 ms 0.1455 ms 0.0378 ms 1.00 0.00 218.7500 3.88 MB 1.00
ExcelParserAsync 50000 22.085 ms 0.1775 ms 0.0461 ms 0.98 0.00 218.7500 3.88 MB 1.00
ExcelParserXlsbSync 50000 9.577 ms 0.1950 ms 0.0506 ms 0.43 0.00 234.3750 3.88 MB 1.00
ExcelParserXlsbAsync 50000 9.907 ms 0.1494 ms 0.0388 ms 0.44 0.00 234.3750 3.88 MB 1.00
MiniExcel 50000 233.339 ms 13.7454 ms 2.1271 ms 10.39 0.09 12000.0000 197.78 MB 51.04
Sylvan 50000 86.163 ms 4.8940 ms 0.7574 ms 3.84 0.03 - 10.48 MB 2.70
SylvanAsync 50000 93.930 ms 2.0376 ms 0.5292 ms 4.18 0.02 - 10.48 MB 2.70

ExcelReader.Benchmarks.ReadBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
  [Host]     : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
ExcelReader 50000 16.517 ms 0.0754 ms 0.0196 ms 1.00 0.00 - 12.59 KB 1.00
ExcelReaderAsync 50000 17.983 ms 0.0950 ms 0.0247 ms 1.09 0.00 - 14.72 KB 1.17
ExcelReaderXlsb 50000 6.170 ms 0.1320 ms 0.0343 ms 0.37 0.00 - 14.32 KB 1.14
ExcelReaderXlsbAsync 50000 6.443 ms 0.0277 ms 0.0072 ms 0.39 0.00 - 16.88 KB 1.34
MiniExcel 50000 218.699 ms 7.0971 ms 1.0983 ms 13.24 0.06 13000.0000 214012.34 KB 17,004.08
Sylvan 50000 56.168 ms 2.9897 ms 0.4627 ms 3.40 0.03 - 1939.18 KB 154.08

ExcelReader.Benchmarks.RealDataReadBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
  [Host]     : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Xlsx_ExcelReader 94.728 ms 4.4961 ms 1.1676 ms 1.00 0.02 - - 35008 B 1.000
Xlsx_Sylvan 313.981 ms 8.8869 ms 2.3079 ms 3.31 0.04 - - 659568 B 18.840
Xlsm_ExcelReader 95.558 ms 0.9208 ms 0.1425 ms 1.01 0.01 - - 35048 B 1.001
Xlsm_Sylvan 320.420 ms 43.8374 ms 6.7839 ms 3.38 0.07 - - 659648 B 18.843
Xlsb_ExcelReader 32.059 ms 0.0572 ms 0.0148 ms 0.34 0.00 - - 41640 B 1.189
Xlsb_Sylvan 43.517 ms 0.1789 ms 0.0465 ms 0.46 0.01 - - 346673 B 9.903
Xls_ExcelReader 19.649 ms 0.0891 ms 0.0138 ms 0.21 0.00 - - 10208 B 0.292
Xls_Sylvan 26.726 ms 0.0988 ms 0.0153 ms 0.28 0.00 - - 190366 B 5.438
Csv_ExcelReader 9.080 ms 0.0490 ms 0.0127 ms 0.10 0.00 - - 232 B 0.007
Csv_Sylvan 16.545 ms 0.4233 ms 0.1099 ms 0.17 0.00 2218.7500 125.0000 37484040 B 1,070.728

ExcelReader.Benchmarks.RecordWriteBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
  [Host]     : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio Gen0 Gen1 Gen2 Allocated Alloc Ratio
Xlsx 50000 20.642 ms 0.4338 ms 0.1126 ms 1.00 500.0000 500.0000 500.0000 4.02 MB 1.00
Xlsb 50000 10.317 ms 0.2749 ms 0.0425 ms 0.50 500.0000 500.0000 500.0000 4.02 MB 1.00
Xls 50000 7.799 ms 0.0666 ms 0.0173 ms 0.38 250.0000 250.0000 250.0000 4.03 MB 1.00
Csv 50000 10.188 ms 0.1291 ms 0.0335 ms 0.49 500.0000 500.0000 500.0000 4 MB 1.00

ExcelReader.Benchmarks.WriteBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
  [Host]     : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio RatioSD Gen0 Gen1 Gen2 Allocated Alloc Ratio
ExcelReaderWriter 50000 19.48 ms 0.124 ms 0.032 ms 1.00 0.00 500.0000 500.0000 500.0000 4.02 MB 1.00
ExcelReaderWriterSharedStrings 50000 19.63 ms 0.094 ms 0.024 ms 1.01 0.00 500.0000 500.0000 500.0000 4.06 MB 1.01
ExcelReaderXlsbWriter 50000 10.44 ms 0.527 ms 0.137 ms 0.54 0.01 500.0000 500.0000 500.0000 4.02 MB 1.00
ExcelReaderXlsbWriterSharedStrings 50000 10.28 ms 0.258 ms 0.067 ms 0.53 0.00 500.0000 500.0000 500.0000 4.06 MB 1.01
MiniExcel 50000 107.96 ms 64.447 ms 9.973 ms 5.54 0.45 5000.0000 1000.0000 1000.0000 84.89 MB 21.13
SpreadCheetah 50000 21.41 ms 0.395 ms 0.103 ms 1.10 0.01 1437.5000 718.7500 718.7500 15.84 MB 3.94

ExcelReader.Benchmarks.XlsReadBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
  [Host]     : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio Gen0 Gen1 Allocated Alloc Ratio
ExcelReader 50000 6.343 ms 0.1878 ms 0.0291 ms 1.00 - - 2.89 KB 1.00
ExcelReaderAsync 50000 6.455 ms 0.1459 ms 0.0379 ms 1.02 - - 2.96 KB 1.02
Sylvan 50000 8.727 ms 0.0050 ms 0.0008 ms 1.38 93.7500 15.6250 1717.73 KB 594.24

ExcelReader.Benchmarks.XlsWriteBenchmark


BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.302
  [Host]     : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3
  Job-MEHJPP : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

IterationCount=5  WarmupCount=1  

Method Rows Mean Error StdDev Ratio Gen0 Gen1 Gen2 Allocated Alloc Ratio
XlsWriter 50000 7.018 ms 0.1110 ms 0.0288 ms 1.00 492.1875 492.1875 492.1875 16.03 MB 1.00
XlsxWriter 50000 19.830 ms 0.3921 ms 0.0607 ms 2.83 500.0000 500.0000 500.0000 4.02 MB 0.25

@GabrielMarquezMatte
GabrielMarquezMatte merged commit e1d6e46 into master Jul 21, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants