From cdc944514dc8ce779187d2d21c93d04ca831e8a5 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Wed, 27 May 2026 23:03:29 +0200 Subject: [PATCH 01/14] Integrated missing documentation in the OpenXmlTemplater --- src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs | 105 +++++++++++++++++- 1 file changed, 100 insertions(+), 5 deletions(-) diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs b/src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs index 61932cdc..bb855a59 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs @@ -9,6 +9,13 @@ public sealed partial class OpenXmlTemplater { internal OpenXmlTemplater() { } + /// + /// Adds pictures to an existing OpenXml document. + /// + /// The path to the OpenXml document to modify. The stream must be readable and writable. + /// A cancellation token to monitor for cancellation requests. + /// A parameter array of objects representing the pictures to add to the document. + /// A task representing the asynchronous operation. [CreateSyncVersion] public async Task AddPictureAsync(string path, CancellationToken cancellationToken = default, params MiniExcelPicture[] images) { @@ -18,12 +25,29 @@ public async Task AddPictureAsync(string path, CancellationToken cancellationTok await MiniExcelPictureImplement.AddPictureAsync(stream, cancellationToken, images).ConfigureAwait(false); } + /// + /// Adds pictures to an existing OpenXml document. + /// + /// The stream containing the OpenXml document to modify. The stream must be readable and writable. + /// A cancellation token to monitor for cancellation requests. + /// A parameter array of objects representing the pictures to add to the document. + /// A task representing the asynchronous operation. [CreateSyncVersion] - public async Task AddPictureAsync(Stream excelStream, CancellationToken cancellationToken = default, params MiniExcelPicture[] images) + public async Task AddPictureAsync(Stream stream, CancellationToken cancellationToken = default, params MiniExcelPicture[] images) { - await MiniExcelPictureImplement.AddPictureAsync(excelStream, cancellationToken, images).ConfigureAwait(false); + await MiniExcelPictureImplement.AddPictureAsync(stream, cancellationToken, images).ConfigureAwait(false); } + /// + /// Fills a template with the provided data and saves the result to a file. + /// + /// The destination file to write the filled document to. + /// The path to the OpenXml template document. + /// The data object to use for populating the template. This can be an enumerable collection, DataTable, or other supported data source. + /// If true, overwrites the file at the specified path, otherwise a will be raised if the file already exists. + /// Optional configuration settings for the template fill operation. + /// A cancellation token to monitor for cancellation requests. + /// A task representing the asynchronous operation. [CreateSyncVersion] public async Task FillTemplateAsync(string path, string templatePath, object value, bool overwriteFile = false, OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) @@ -34,6 +58,16 @@ public async Task FillTemplateAsync(string path, string templatePath, object val await FillTemplateAsync(stream, templatePath, value, configuration, cancellationToken).ConfigureAwait(false); } + /// + /// Fills a template with the provided data and saves the result to a file. + /// + /// The destination file to write the filled document to. + /// The stream containing the OpenXml template document. + /// The data object to use for populating the template. This can be an enumerable collection, DataTable, or other supported data source. + /// If true, overwrites the file at the specified path, otherwise a will be raised if the file already exists. + /// Optional configuration settings for the template fill operation. + /// A cancellation token to monitor for cancellation requests. + /// A task representing the asynchronous operation. [CreateSyncVersion] public async Task FillTemplateAsync(string path, Stream templateStream, object value, bool overwriteFile = false, OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) @@ -45,6 +79,15 @@ public async Task FillTemplateAsync(string path, Stream templateStream, object v await template.SaveAsByTemplateAsync(templateStream, value, cancellationToken).ConfigureAwait(false); } + /// + /// Fills a template with the provided data and saves the result to a destination stream. + /// + /// The destination stream to write the filled document to. + /// The path to the OpenXml template document. + /// The data object to use for populating the template. This can be an enumerable collection, DataTable, or other supported data source. + /// Optional configuration settings for the template fill operation. + /// A cancellation token to monitor for cancellation requests. + /// A task representing the asynchronous operation. [CreateSyncVersion] public async Task FillTemplateAsync(Stream stream, string templatePath, object value, OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) @@ -53,6 +96,15 @@ public async Task FillTemplateAsync(Stream stream, string templatePath, object v await template.SaveAsByTemplateAsync(templatePath, value, cancellationToken).ConfigureAwait(false); } + /// + /// Fills a template with the provided data and saves the result to a destination stream. + /// + /// The destination stream to write the filled document to. + /// The stream containing the OpenXml template document. + /// The data object to use for populating the template. This can be an enumerable collection, DataTable, or other supported data source. + /// Optional configuration settings for the template fill operation. + /// A cancellation token to monitor for cancellation requests. + /// A task representing the asynchronous operation. [CreateSyncVersion] public async Task FillTemplateAsync(Stream stream, Stream templateStream, object value, OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) @@ -60,7 +112,17 @@ public async Task FillTemplateAsync(Stream stream, Stream templateStream, object var template = GetOpenXmlTemplate(stream, configuration); await template.SaveAsByTemplateAsync(templateStream, value, cancellationToken).ConfigureAwait(false); } - + + /// + /// Fills a template with the provided data and saves the result to a file. + /// + /// The destination stream to write the filled document to. + /// A byte array containing the OpenXml template document. + /// The data object to use for populating the template. This can be an enumerable collection, DataTable, or other supported data source. + /// If true, overwrites the file at the specified path, otherwise a will be raised if the file already exists. + /// Optional configuration settings for the template fill operation. + /// A cancellation token to monitor for cancellation requests. + /// A task representing the asynchronous operation. [CreateSyncVersion] public async Task FillTemplateAsync(string path, byte[] templateBytes, object value, bool overwriteFile = false, OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) @@ -71,6 +133,15 @@ public async Task FillTemplateAsync(string path, byte[] templateBytes, object va await FillTemplateAsync(stream, templateBytes, value, configuration, cancellationToken).ConfigureAwait(false); } + /// + /// Fills a template with the provided data and saves the result to a destination stream. + /// + /// The destination stream to write the filled document to. + /// A byte array containing the OpenXml template document. + /// The data object to use for populating the template. This can be an enumerable collection, DataTable, or other supported data source. + /// Optional configuration settings for the template fill operation. + /// A cancellation token to monitor for cancellation requests. + /// A task representing the asynchronous operation. [CreateSyncVersion] public async Task FillTemplateAsync(Stream stream, byte[] templateBytes, object value, OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) @@ -81,6 +152,14 @@ public async Task FillTemplateAsync(Stream stream, byte[] templateBytes, object #region Merge Cells + /// + /// Merges cells with identical values in a specified OpenXml document. + /// + /// The destination file to write the merged document to. + /// The file path to the original OpenXml document to process for cell merging. + /// Optional configuration settings for the merge operation. + /// A cancellation token to monitor for cancellation requests. + /// A task representing the asynchronous operation. [CreateSyncVersion] public async Task MergeSameCellsAsync(string mergedFilePath, string path, OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) @@ -91,6 +170,14 @@ public async Task MergeSameCellsAsync(string mergedFilePath, string path, await MergeSameCellsAsync(stream, path, configuration, cancellationToken).ConfigureAwait(false); } + /// + /// Merges cells with identical values in a specified OpenXml document. + /// + /// The destination stream to write the merged document to. + /// The file path to the original OpenXml document to process for cell merging. + /// Optional configuration settings for the merge operation. + /// A cancellation token to monitor for cancellation requests. + /// A task representing the asynchronous operation. [CreateSyncVersion] public async Task MergeSameCellsAsync(Stream stream, string path, OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) @@ -99,12 +186,20 @@ public async Task MergeSameCellsAsync(Stream stream, string path, await template.MergeSameCellsAsync(path, cancellationToken).ConfigureAwait(false); } + /// + /// Merges cells with identical values in a specified OpenXml document. + /// + /// The destination stream to write the merged document to. + /// A byte array containing the original OpenXml document to process for cell merging. + /// Optional configuration settings for the merge operation. + /// A cancellation token to monitor for cancellation requests. + /// A task representing the asynchronous operation. [CreateSyncVersion] - public async Task MergeSameCellsAsync(Stream stream, byte[] file, + public async Task MergeSameCellsAsync(Stream stream, byte[] documentData, OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) { var template = GetOpenXmlTemplate(stream, configuration); - await template.MergeSameCellsAsync(file, cancellationToken).ConfigureAwait(false); + await template.MergeSameCellsAsync(documentData, cancellationToken).ConfigureAwait(false); } From f881e799ce050f30dad932cd5c73aa5cc5385a94 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Thu, 28 May 2026 00:03:59 +0200 Subject: [PATCH 02/14] Minor optimizations --- src/MiniExcel.Core/MiniExcelDataReader.cs | 13 +++++++++-- src/MiniExcel.Csv/CsvReader.cs | 9 ++++---- .../Utils/SharedStringsDiskCache.cs | 23 ++++++++++++++++--- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/MiniExcel.Core/MiniExcelDataReader.cs b/src/MiniExcel.Core/MiniExcelDataReader.cs index f7f2d77d..f90d6b40 100644 --- a/src/MiniExcel.Core/MiniExcelDataReader.cs +++ b/src/MiniExcel.Core/MiniExcelDataReader.cs @@ -5,6 +5,7 @@ public sealed class MiniExcelDataReader : IMiniExcelDataReader private readonly IEnumerator>? _source; private readonly IAsyncEnumerator>? _asyncSource; private readonly Stream _stream; + private readonly Dictionary _ordinals = []; private bool _isEmpty; private List _columns = []; @@ -244,8 +245,16 @@ public string GetName(int i) => _columns[i]; public int GetOrdinal(string name) - => _columns.IndexOf(name); - + { + if (_ordinals.TryGetValue(name, out var ordinal)) + return ordinal; + + var ord = _columns.IndexOf(name); + _ordinals[name] = ord; + + return ord; + } + public DataTable GetSchemaTable() { if (_schema is null) diff --git a/src/MiniExcel.Csv/CsvReader.cs b/src/MiniExcel.Csv/CsvReader.cs index f28e2f1c..2a11dd58 100644 --- a/src/MiniExcel.Csv/CsvReader.cs +++ b/src/MiniExcel.Csv/CsvReader.cs @@ -163,10 +163,6 @@ private string[] Split(string row) .ToArray(); } - public void Dispose() - { - _stream?.Dispose(); - } #if NET [GeneratedRegex("^\"|\"$")] private static partial Regex DoubleQuotesRegex(); @@ -174,4 +170,9 @@ public void Dispose() #else private static readonly Regex DoubleQuotesRegexImpl = new Regex("^\"|\"$", RegexOptions.Compiled); #endif + + public void Dispose() + { + _stream?.Dispose(); + } } diff --git a/src/MiniExcel.OpenXml/Utils/SharedStringsDiskCache.cs b/src/MiniExcel.OpenXml/Utils/SharedStringsDiskCache.cs index 0d857461..d6922979 100644 --- a/src/MiniExcel.OpenXml/Utils/SharedStringsDiskCache.cs +++ b/src/MiniExcel.OpenXml/Utils/SharedStringsDiskCache.cs @@ -53,21 +53,38 @@ private string GetValue(int index) if (index > _maxIndex) throw new KeyNotFoundException(); - _positionFs.Position = index * 4; + _positionFs.Seek(index * 4, SeekOrigin.Begin); +#if NET + Span bytes = stackalloc byte[4]; + _ = _positionFs.Read(bytes); + var position = BitConverter.ToInt32(bytes); + + bytes.Clear(); + _lengthFs.Seek(index * 4, SeekOrigin.Begin); + _ = _lengthFs.Read(bytes); + var length = BitConverter.ToInt32(bytes); + + bytes = stackalloc byte[length]; + _valueFs.Seek(position, SeekOrigin.Begin); + _ = _valueFs.Read(bytes); + + return Encoding.GetString(bytes[..length]); +#else var bytes = new byte[4]; _ = _positionFs.Read(bytes, 0, 4); var position = BitConverter.ToInt32(bytes, 0); - + bytes = new byte[4]; _lengthFs.Position = index * 4; _ = _lengthFs.Read(bytes, 0, 4); var length = BitConverter.ToInt32(bytes, 0); - + bytes = new byte[length]; _valueFs.Position = position; _ = _valueFs.Read(bytes, 0, length); return Encoding.GetString(bytes); +#endif } public ICollection Keys => throw new NotSupportedException(); From 1a7f10ad19f0002786c745f7bf827977b455668c Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 30 May 2026 14:33:19 +0200 Subject: [PATCH 03/14] Update packages suffering from security vulnerabilities in test and benchmark projects --- benchmarks/MiniExcel.Benchmarks/MiniExcel.Benchmarks.csproj | 3 ++- tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/benchmarks/MiniExcel.Benchmarks/MiniExcel.Benchmarks.csproj b/benchmarks/MiniExcel.Benchmarks/MiniExcel.Benchmarks.csproj index c5d151fb..1e68a8bd 100644 --- a/benchmarks/MiniExcel.Benchmarks/MiniExcel.Benchmarks.csproj +++ b/benchmarks/MiniExcel.Benchmarks/MiniExcel.Benchmarks.csproj @@ -17,7 +17,8 @@ - + + diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj b/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj index ec76bc97..8bdd0da0 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj @@ -21,6 +21,8 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive From d8b7e424c7464ca413683af6a4823acce995c26e Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 30 May 2026 14:42:06 +0200 Subject: [PATCH 04/14] Added leaveOpen parameter to GetDataReader API method overloads to configure if the underlying stream is closed when the reader is disposed --- src/MiniExcel.Core/MiniExcelDataReader.cs | 30 ++++++++++++-------- src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs | 5 ++++ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/MiniExcel.Core/MiniExcelDataReader.cs b/src/MiniExcel.Core/MiniExcelDataReader.cs index f90d6b40..42b65078 100644 --- a/src/MiniExcel.Core/MiniExcelDataReader.cs +++ b/src/MiniExcel.Core/MiniExcelDataReader.cs @@ -4,8 +4,9 @@ public sealed class MiniExcelDataReader : IMiniExcelDataReader { private readonly IEnumerator>? _source; private readonly IAsyncEnumerator>? _asyncSource; - private readonly Stream _stream; private readonly Dictionary _ordinals = []; + private readonly Stream _stream; + private readonly bool _leaveOpen; private bool _isEmpty; private List _columns = []; @@ -26,18 +27,19 @@ public object this[string name] public int RecordsAffected => 0; - private MiniExcelDataReader(Stream? stream, IEnumerable>? values) + private MiniExcelDataReader(Stream? stream, IEnumerable>? values, bool leaveOpen) { _stream = stream ?? throw new ArgumentNullException(nameof(stream)); _source = values?.GetEnumerator() ?? throw new ArgumentNullException(nameof(values)); + _leaveOpen = leaveOpen; } - public static MiniExcelDataReader Create(Stream? stream, IEnumerable> values) + public static MiniExcelDataReader Create(Stream? stream, IEnumerable> values, bool leaveOpen = false) { - var reader = new MiniExcelDataReader(stream, values); + var reader = new MiniExcelDataReader(stream, values, leaveOpen); if (reader._source!.MoveNext()) { - reader._columns = reader._source.Current?.Keys.ToList() ?? []; + reader._columns = reader._source.Current!.Keys.ToList(); reader.FieldCount = reader._columns.Count; } else @@ -48,16 +50,17 @@ public static MiniExcelDataReader Create(Stream? stream, IEnumerable>? values) + private MiniExcelDataReader(Stream? stream, IAsyncEnumerable>? values, bool leaveOpen = false) { _stream = stream ?? throw new ArgumentNullException(nameof(stream)); _asyncSource = values?.GetAsyncEnumerator() ?? throw new ArgumentNullException(nameof(values)); + _leaveOpen = leaveOpen; _isAsyncSource = true; } - public static async Task CreateAsync(Stream? stream, IAsyncEnumerable> values) + public static async Task CreateAsync(Stream? stream, IAsyncEnumerable> values, bool leaveOpen = false) { - var reader = new MiniExcelDataReader(stream, values); + var reader = new MiniExcelDataReader(stream, values, leaveOpen); if (await reader._asyncSource!.MoveNextAsync().ConfigureAwait(false)) { reader._columns = reader._asyncSource.Current.Keys.ToList(); @@ -220,8 +223,8 @@ public byte GetByte(int i) => GetValue(i) is { } value public object GetValue(int i) { var currentRow = _isAsyncSource - ? _asyncSource!.Current - : _source!.Current!; + ? _asyncSource?.Current + : _source?.Current; return currentRow is not null ? currentRow[_columns[i]] ?? DBNull.Value @@ -294,7 +297,9 @@ public void Close() _source!.Dispose(); } - _stream.Dispose(); + if (!_leaveOpen) + _stream.Dispose(); + IsClosed = true; } @@ -307,7 +312,8 @@ public async Task CloseAsync() await _asyncSource!.DisposeAsync().ConfigureAwait(false); _source?.Dispose(); - await _stream!.DisposeAsync().ConfigureAwait(false); + if (!_leaveOpen) + await _stream.DisposeAsync().ConfigureAwait(false); IsClosed = true; } diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs index aaf53100..e353c2f5 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs @@ -307,6 +307,7 @@ public MiniExcelDataReader GetDataReader(string path, bool useHeaderRow = false, var values = Query(stream, useHeaderRow, sheetName, startCell, configuration).Cast>(); return MiniExcelDataReader.Create(stream, values); + return MiniExcelDataReader.Create(stream, values, leaveOpen: false); } /// @@ -317,9 +318,11 @@ public MiniExcelDataReader GetDataReader(string path, bool useHeaderRow = false, /// public MiniExcelDataReader GetDataReader(Stream stream, bool useHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null) + string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, bool leaveOpen = false) { var values = Query(stream, useHeaderRow, sheetName, startCell, configuration).Cast>(); return MiniExcelDataReader.Create(stream, values); + return MiniExcelDataReader.Create(stream, values, leaveOpen); } /// @@ -334,6 +337,7 @@ public async Task GetAsyncDataReader(string path, bool useH var values = QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken); return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken)).ConfigureAwait(false); + return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken), leaveOpen: false).ConfigureAwait(false); } /// @@ -346,6 +350,7 @@ public async Task GetAsyncDataReader(Stream stream, bool us { var values = QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken); return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken)).ConfigureAwait(false); + return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken), leaveOpen).ConfigureAwait(false); } #endregion From 4ffc7a0f0227deb020d3c6dcb979c392f29250e5 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 30 May 2026 15:09:59 +0200 Subject: [PATCH 05/14] Removed unused configuration parameter from API methods GetSheetNames, GetSheetInformations and GetColumnNames --- src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs | 26 +++++++++++--------- src/MiniExcel/MiniExcel.cs | 12 ++++----- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs index e353c2f5..b1d0158e 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs @@ -139,7 +139,7 @@ public async Task QueryAsDataTableAsync(Stream stream, bool useHeader string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) { - sheetName ??= (await GetSheetNamesAsync(stream, configuration, cancellationToken).ConfigureAwait(false)).First(); + sheetName ??= (await GetSheetNamesAsync(stream, cancellationToken).ConfigureAwait(false)).First(); var dt = new DataTable(sheetName); var first = true; @@ -190,44 +190,46 @@ public async Task QueryAsDataTableAsync(Stream stream, bool useHeader #region Sheet Info [CreateSyncVersion] - public async Task> GetSheetNamesAsync(string path, OpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) + public async Task> GetSheetNamesAsync(string path, CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - return await GetSheetNamesAsync(stream, config, cancellationToken).ConfigureAwait(false); + return await GetSheetNamesAsync(stream, cancellationToken).ConfigureAwait(false); } [CreateSyncVersion] - public async Task> GetSheetNamesAsync(Stream stream, OpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) + public async Task> GetSheetNamesAsync(Stream stream, CancellationToken cancellationToken = default) { - config ??= OpenXmlConfiguration.Default; - var archive = await OpenXmlZip.CreateAsync(stream, leaveOpen: true, cancellationToken: cancellationToken).ConfigureAwait(false); await using var disposableArchive = archive.ConfigureAwait(false); - using var reader = await OpenXmlReader.CreateAsync(stream, config, cancellationToken: cancellationToken).ConfigureAwait(false); + using var reader = await OpenXmlReader.CreateAsync(stream, null, cancellationToken).ConfigureAwait(false); var rels = await reader.GetWorkbookRelsAsync(archive.EntryCollection, cancellationToken).ConfigureAwait(false); return rels?.Select(s => s.Name).ToList() ?? []; } + /// Retrieves detailed information about all sheets in an Excel workbook. + /// [CreateSyncVersion] - public async Task> GetSheetInformationsAsync(string path, OpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) + public async Task> GetSheetInformationsAsync(string path, CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - return await GetSheetInformationsAsync(stream, config, cancellationToken).ConfigureAwait(false); + return await GetSheetInformationsAsync(stream, cancellationToken).ConfigureAwait(false); } + /// The stream containing the Excel file data. The stream position is not reset after reading. + /// A token to cancel the asynchronous operation. [CreateSyncVersion] - public async Task> GetSheetInformationsAsync(Stream stream, OpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) + public async Task> GetSheetInformationsAsync(Stream stream, CancellationToken cancellationToken = default) { config ??= OpenXmlConfiguration.Default; var archive = await OpenXmlZip.CreateAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false); await using var disposableArchve = archive.ConfigureAwait(false); - using var reader = await OpenXmlReader.CreateAsync(stream, config, cancellationToken: cancellationToken).ConfigureAwait(false); + using var reader = await OpenXmlReader.CreateAsync(stream, null, cancellationToken: cancellationToken).ConfigureAwait(false); var rels = await reader.GetWorkbookRelsAsync(archive.EntryCollection, cancellationToken).ConfigureAwait(false); return rels?.Select((s, i) => s.ToSheetInfo((uint)i)).ToList() ?? []; @@ -260,6 +262,8 @@ public async Task> GetColumnNamesAsync(string path, bool use return await GetColumnNamesAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken).ConfigureAwait(false); } + /// If true, the first row values are used as column names. If false, column letters (A, B, C, etc.) are used. Default is false. + /// The name of the worksheet to query. If not provided, the first sheet is used. [CreateSyncVersion] public async Task> GetColumnNamesAsync(Stream stream, bool useHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, diff --git a/src/MiniExcel/MiniExcel.cs b/src/MiniExcel/MiniExcel.cs index 51c016bd..a9074a53 100644 --- a/src/MiniExcel/MiniExcel.cs +++ b/src/MiniExcel/MiniExcel.cs @@ -299,19 +299,19 @@ public static async Task QueryAsDataTableAsync(this Stream stream, bo [CreateSyncVersion] public static async Task> GetSheetNamesAsync(string path, NewOpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) - => await ExcelImporter.GetSheetNamesAsync(path, config, cancellationToken).ConfigureAwait(false); + => await ExcelImporter.GetSheetNamesAsync(path, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] public static async Task> GetSheetNamesAsync(this Stream stream, NewOpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) - => await ExcelImporter.GetSheetNamesAsync(stream, config, cancellationToken).ConfigureAwait(false); + => await ExcelImporter.GetSheetNamesAsync(stream, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] public static async Task> GetSheetInformationsAsync(string path, NewOpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) - => await ExcelImporter.GetSheetInformationsAsync(path, config, cancellationToken).ConfigureAwait(false); + => await ExcelImporter.GetSheetInformationsAsync(path, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] public static async Task> GetSheetInformationsAsync(this Stream stream, NewOpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) - => await ExcelImporter.GetSheetInformationsAsync(stream, config, cancellationToken).ConfigureAwait(false); + => await ExcelImporter.GetSheetInformationsAsync(stream, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] public static async Task> GetColumnsAsync(string path, bool useHeaderRow = false, string? sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration? configuration = null, CancellationToken cancellationToken = default) @@ -319,7 +319,7 @@ public static async Task> GetColumnsAsync(string path, bool var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelImporter.GetColumnNamesAsync(path, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelImporter.GetColumnNamesAsync(path, useHeaderRow, sheetName, startCell, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvImporter.GetColumnNamesAsync(path, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") }; @@ -331,7 +331,7 @@ public static async Task> GetColumnsAsync(this Stream stream var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelImporter.GetColumnNamesAsync(stream, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelImporter.GetColumnNamesAsync(stream, useHeaderRow, sheetName, startCell, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvImporter.GetColumnNamesAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") }; From 20658a5462a7e71f0cd1a6dfed5a6ecc2b9efbf8 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 30 May 2026 15:14:53 +0200 Subject: [PATCH 06/14] Renamed parameter useHeaderRow to hasHeaderRow in multiple OpenXmlImporter API methods --- src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs | 72 ++++++++++--------- src/MiniExcel.OpenXml/OpenXmlReader.cs | 2 +- src/MiniExcel/MiniExcelConverter.cs | 2 +- .../MiniExcelMappingTemplateTests.cs | 18 ++--- .../MiniExcelIssueAsyncTests.cs | 12 ++-- .../MiniExcelIssueTests.cs | 22 +++--- .../MiniExcelOpenXmlAsyncTests.cs | 60 ++++++++-------- .../MiniExcelOpenXmlMultipleSheetTests.cs | 12 ++-- .../MiniExcelOpenXmlTests.cs | 58 +++++++-------- 9 files changed, 130 insertions(+), 128 deletions(-) diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs index b1d0158e..bc1ca851 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs @@ -34,24 +34,24 @@ public async IAsyncEnumerable QueryAsync(Stream stream, string? sheetName } [CreateSyncVersion] - public async IAsyncEnumerable QueryAsync(string path, bool useHeaderRow = false, + public async IAsyncEnumerable QueryAsync(string path, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - await foreach (var item in QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken).ConfigureAwait(false)) + await foreach (var item in QueryAsync(stream, hasHeaderRow, sheetName, startCell, configuration, cancellationToken).ConfigureAwait(false)) yield return item; } [CreateSyncVersion] - public async IAsyncEnumerable QueryAsync(Stream stream, bool useHeaderRow = false, + public async IAsyncEnumerable QueryAsync(Stream stream, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { using var excelReader = await OpenXmlReader.CreateAsync(stream, configuration, cancellationToken).ConfigureAwait(false); - await foreach (var item in excelReader.QueryAsync(useHeaderRow, sheetName, startCell, cancellationToken).ConfigureAwait(false)) + await foreach (var item in excelReader.QueryAsync(hasHeaderRow, sheetName, startCell, cancellationToken).ConfigureAwait(false)) yield return item; } @@ -68,29 +68,29 @@ public async IAsyncEnumerable QueryAsync(Stream stream, bool useHeaderR /// /// [CreateSyncVersion] - public async IAsyncEnumerable QueryRangeAsync(string path, bool useHeaderRow = false, - string? sheetName = null, string startCell = "A1", string endCell = "", OpenXmlConfiguration? configuration = null, + public async IAsyncEnumerable QueryRangeAsync(string path, bool hasHeaderRow = false, + string? sheetName = null, string startCell = "A1", string? endCell = null, OpenXmlConfiguration? configuration = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - await foreach (var item in QueryRangeAsync(stream, useHeaderRow, sheetName, startCell, endCell, configuration, cancellationToken).ConfigureAwait(false)) + await foreach (var item in QueryRangeAsync(stream, hasHeaderRow, sheetName, startCell, endCell, configuration, cancellationToken).ConfigureAwait(false)) yield return item; } [CreateSyncVersion] - public async IAsyncEnumerable QueryRangeAsync(Stream stream, bool useHeaderRow = false, - string? sheetName = null, string startCell = "A1", string endCell = "", OpenXmlConfiguration? configuration = null, + public async IAsyncEnumerable QueryRangeAsync(Stream stream, bool hasHeaderRow = false, + string? sheetName = null, string startCell = "A1", string? endCell = null, OpenXmlConfiguration? configuration = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { using var excelReader = await OpenXmlReader.CreateAsync(stream, configuration, cancellationToken).ConfigureAwait(false); - await foreach (var item in excelReader.QueryRangeAsync(useHeaderRow, sheetName, startCell, endCell, cancellationToken).ConfigureAwait(false)) + await foreach (var item in excelReader.QueryRangeAsync(hasHeaderRow, sheetName, startCell, endCell, cancellationToken).ConfigureAwait(false)) yield return item; } [CreateSyncVersion] - public async IAsyncEnumerable QueryRangeAsync(string path, bool useHeaderRow = false, + public async IAsyncEnumerable QueryRangeAsync(string path, bool hasHeaderRow = false, string? sheetName = null, int startRowIndex = 1, int startColumnIndex = 1, int? endRowIndex = null, int? endColumnIndex = null, OpenXmlConfiguration? configuration = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) @@ -98,18 +98,18 @@ public async IAsyncEnumerable QueryRangeAsync(string path, bool useHead var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - await foreach (var item in QueryRangeAsync(stream, useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration, cancellationToken).ConfigureAwait(false)) + await foreach (var item in QueryRangeAsync(stream, hasHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration, cancellationToken).ConfigureAwait(false)) yield return item; } [CreateSyncVersion] - public async IAsyncEnumerable QueryRangeAsync(Stream stream, bool useHeaderRow = false, + public async IAsyncEnumerable QueryRangeAsync(Stream stream, bool hasHeaderRow = false, string? sheetName = null, int startRowIndex = 1, int startColumnIndex = 1, int? endRowIndex = null, int? endColumnIndex = null, OpenXmlConfiguration? configuration = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { using var excelReader = await OpenXmlReader.CreateAsync(stream, configuration, cancellationToken).ConfigureAwait(false); - await foreach (var item in excelReader.QueryRangeAsync(useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, cancellationToken).ConfigureAwait(false)) + await foreach (var item in excelReader.QueryRangeAsync(hasHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, cancellationToken).ConfigureAwait(false)) yield return item; } @@ -121,21 +121,21 @@ public async IAsyncEnumerable QueryRangeAsync(Stream stream, bool useHe /// QueryAsDataTable is not recommended, because it'll load all data into memory. /// [CreateSyncVersion] - public async Task QueryAsDataTableAsync(string path, bool useHeaderRow = true, + public async Task QueryAsDataTableAsync(string path, bool hasHeaderRow = true, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - return await QueryAsDataTableAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken).ConfigureAwait(false); + return await QueryAsDataTableAsync(stream, hasHeaderRow, sheetName, startCell, configuration, cancellationToken).ConfigureAwait(false); } /// /// QueryAsDataTable is not recommended, because it'll load all data into memory. /// [CreateSyncVersion] - public async Task QueryAsDataTableAsync(Stream stream, bool useHeaderRow = true, + public async Task QueryAsDataTableAsync(Stream stream, bool hasHeaderRow = true, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) { @@ -155,7 +155,7 @@ public async Task QueryAsDataTableAsync(Stream stream, bool useHeader { cancellationToken.ThrowIfCancellationRequested(); - var columnName = useHeaderRow ? entry.Value?.ToString() : entry.Key; + var columnName = hasHeaderRow ? entry.Value?.ToString() : entry.Key; if (!string.IsNullOrWhiteSpace(columnName)) // avoid #298 : Column '' does not belong to table { var column = new DataColumn(columnName, typeof(object)) { Caption = columnName }; @@ -166,7 +166,7 @@ public async Task QueryAsDataTableAsync(Stream stream, bool useHeader dt.BeginLoadData(); first = false; - if (useHeaderRow) + if (hasHeaderRow) { continue; } @@ -225,9 +225,8 @@ public async Task> GetSheetInformationsAsync(string path, Cancel [CreateSyncVersion] public async Task> GetSheetInformationsAsync(Stream stream, CancellationToken cancellationToken = default) { - config ??= OpenXmlConfiguration.Default; - var archive = await OpenXmlZip.CreateAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false); + await using var disposableArchve = archive.ConfigureAwait(false); using var reader = await OpenXmlReader.CreateAsync(stream, null, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -252,24 +251,22 @@ public async Task> GetSheetDimensionsAsync(Stream stream, Canc } [CreateSyncVersion] - public async Task> GetColumnNamesAsync(string path, bool useHeaderRow = false, - string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, - CancellationToken cancellationToken = default) + public async Task> GetColumnNamesAsync(string path, bool hasHeaderRow = false, + string? sheetName = null, string startCell = "A1", CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - return await GetColumnNamesAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken).ConfigureAwait(false); + return await GetColumnNamesAsync(stream, hasHeaderRow, sheetName, startCell, cancellationToken).ConfigureAwait(false); } /// If true, the first row values are used as column names. If false, column letters (A, B, C, etc.) are used. Default is false. /// The name of the worksheet to query. If not provided, the first sheet is used. [CreateSyncVersion] - public async Task> GetColumnNamesAsync(Stream stream, bool useHeaderRow = false, - string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, - CancellationToken cancellationToken = default) + public async Task> GetColumnNamesAsync(Stream stream, bool hasHeaderRow = false, + string? sheetName = null, string startCell = "A1", CancellationToken cancellationToken = default) { - var enumerator = QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken).GetAsyncEnumerator(cancellationToken); + var enumerator = QueryAsync(stream, hasHeaderRow, sheetName, startCell, null, cancellationToken).GetAsyncEnumerator(cancellationToken); await using var disposableEnumerator = enumerator.ConfigureAwait(false); if (await enumerator.MoveNextAsync().ConfigureAwait(false)) @@ -305,12 +302,12 @@ public async Task RetrieveCommentsAsync(Stream stream, string? /// Asynchronous reads are not allowed when creating the data reader from this overload and will result in an exception. /// public MiniExcelDataReader GetDataReader(string path, bool useHeaderRow = false, + public MiniExcelDataReader GetDataReader(string path, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null) { var stream = FileHelper.OpenSharedRead(path); - var values = Query(stream, useHeaderRow, sheetName, startCell, configuration).Cast>(); + var values = Query(stream, hasHeaderRow, sheetName, startCell, configuration).Cast>(); - return MiniExcelDataReader.Create(stream, values); return MiniExcelDataReader.Create(stream, values, leaveOpen: false); } @@ -322,13 +319,13 @@ public MiniExcelDataReader GetDataReader(string path, bool useHeaderRow = false, /// public MiniExcelDataReader GetDataReader(Stream stream, bool useHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null) + public MiniExcelDataReader GetDataReader(Stream stream, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, bool leaveOpen = false) { - var values = Query(stream, useHeaderRow, sheetName, startCell, configuration).Cast>(); - return MiniExcelDataReader.Create(stream, values); + var values = Query(stream, hasHeaderRow, sheetName, startCell, configuration).Cast>(); return MiniExcelDataReader.Create(stream, values, leaveOpen); } - + /// /// Gets an for the Excel document at the specific path. /// When created from this overload, the resulting data reader is supposed to be advanced asynchronously. @@ -336,11 +333,13 @@ public MiniExcelDataReader GetDataReader(Stream stream, bool useHeaderRow = fals public async Task GetAsyncDataReader(string path, bool useHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) + public async Task GetAsyncDataReader(string path, bool hasHeaderRow = false, + string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); var values = QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken); + var values = QueryAsync(stream, hasHeaderRow, sheetName, startCell, configuration, cancellationToken); - return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken)).ConfigureAwait(false); return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken), leaveOpen: false).ConfigureAwait(false); } @@ -350,10 +349,13 @@ public async Task GetAsyncDataReader(string path, bool useH /// public async Task GetAsyncDataReader(Stream stream, bool useHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, + public async Task GetAsyncDataReader(Stream stream, bool hasHeaderRow = false, + string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, bool leaveOpen = false, CancellationToken cancellationToken = default) { var values = QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken); return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken)).ConfigureAwait(false); + var values = QueryAsync(stream, hasHeaderRow, sheetName, startCell, configuration, cancellationToken); return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken), leaveOpen).ConfigureAwait(false); } diff --git a/src/MiniExcel.OpenXml/OpenXmlReader.cs b/src/MiniExcel.OpenXml/OpenXmlReader.cs index ab514bef..4c7e18f7 100644 --- a/src/MiniExcel.OpenXml/OpenXmlReader.cs +++ b/src/MiniExcel.OpenXml/OpenXmlReader.cs @@ -58,7 +58,7 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC } [CreateSyncVersion] - public IAsyncEnumerable> QueryRangeAsync(bool useHeaderRow, string? sheetName, string startCell, string endCell, CancellationToken cancellationToken = default) + public IAsyncEnumerable> QueryRangeAsync(bool useHeaderRow, string? sheetName, string? startCell, string? endCell, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/MiniExcel/MiniExcelConverter.cs b/src/MiniExcel/MiniExcelConverter.cs index ebec5f89..76613ae5 100644 --- a/src/MiniExcel/MiniExcelConverter.cs +++ b/src/MiniExcel/MiniExcelConverter.cs @@ -44,7 +44,7 @@ public static async Task ConvertXlsxToCsvAsync(Stream xlsx, Stream csv, bool xls { var value = MiniExcel.Importers .GetOpenXmlImporter() - .QueryAsync(xlsx, useHeaderRow: xlsxHasHeader, cancellationToken: cancellationToken) + .QueryAsync(xlsx, hasHeaderRow: xlsxHasHeader, cancellationToken: cancellationToken) .ConfigureAwait(false); await MiniExcel.Exporters diff --git a/tests/MiniExcel.OpenXml.Tests/FluentMapping/MiniExcelMappingTemplateTests.cs b/tests/MiniExcel.OpenXml.Tests/FluentMapping/MiniExcelMappingTemplateTests.cs index 9d778d5c..10c334a6 100644 --- a/tests/MiniExcel.OpenXml.Tests/FluentMapping/MiniExcelMappingTemplateTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/FluentMapping/MiniExcelMappingTemplateTests.cs @@ -73,7 +73,7 @@ public async Task BasicTemplateTest() var templater = MiniExcel.Templaters.GetMappingTemplater(registry); await templater.FillTemplateAsync(outputPath.ToString(), templatePath.ToString(), [data]); - var rows = _importer.Query(outputPath.ToString(), useHeaderRow: false).ToList(); + var rows = _importer.Query(outputPath.ToString(), hasHeaderRow: false).ToList(); Assert.Equal(3, rows.Count); @@ -133,7 +133,7 @@ public async Task StreamOverloadTest() await templater.FillTemplateAsync(outputStream, templateStream, [data]); } - var rows = _importer.Query(outputPath.ToString(), useHeaderRow: false).ToList(); + var rows = _importer.Query(outputPath.ToString(), hasHeaderRow: false).ToList(); Assert.Equal("Jack", rows[2].A); } @@ -174,7 +174,7 @@ public async Task ByteArrayOverloadTest() await templater.FillTemplateAsync(outputStream, templateBytes, [data]); } - var rows = _importer.Query(outputPath.ToString(), useHeaderRow: false).ToList(); + var rows = _importer.Query(outputPath.ToString(), hasHeaderRow: false).ToList(); Assert.Equal("Jack", rows[2].A); } @@ -238,7 +238,7 @@ public async Task CollectionTemplateTest() var templater = MiniExcel.Templaters.GetMappingTemplater(registry); await templater.FillTemplateAsync(outputPath.ToString(), templatePath.ToString(), [dept]); - var rows = _importer.Query(outputPath.ToString(), useHeaderRow: false).ToList(); + var rows = _importer.Query(outputPath.ToString(), hasHeaderRow: false).ToList(); Assert.Equal(11, rows.Count); // We expect 11 rows total @@ -286,7 +286,7 @@ public async Task EmptyDataTest() var templater = MiniExcel.Templaters.GetMappingTemplater(registry); await templater.FillTemplateAsync(outputPath.ToString(), templatePath.ToString(), Array.Empty()); - var rows = _importer.Query(outputPath.ToString(), useHeaderRow: false).ToList(); + var rows = _importer.Query(outputPath.ToString(), hasHeaderRow: false).ToList(); Assert.Equal(3, rows.Count); // Column headers + our headers + empty data row Assert.Equal("Name", rows[1].A); Assert.Equal("Date", rows[1].B); @@ -331,8 +331,8 @@ public async Task NullValuesTest() await templater.FillTemplateAsync(outputPath.ToString(), templatePath.ToString(), [data]); // Verify null handling - // Verify - use useHeaderRow=false since we want to see all rows - var rows = _importer.Query(outputPath.ToString(), useHeaderRow: false).ToList(); + // Verify - use hasHeaderRow=false since we want to see all rows + var rows = _importer.Query(outputPath.ToString(), hasHeaderRow: false).ToList(); Assert.True(string.IsNullOrEmpty(rows[2].A?.ToString())); // Null replaced the default Assert.Equal(new DateTime(2021, 01, 01), ParseDateValue(rows[2].B)); Assert.Equal(false, rows[2].C); @@ -376,8 +376,8 @@ public async Task MultipleItemsTest() await templater.FillTemplateAsync(outputPath.ToString(), templatePath.ToString(), data); // Verify - should only update first item since mapping is for specific cells - // Verify - use useHeaderRow=false since we want to see all rows - var rows = _importer.Query(outputPath.ToString(), useHeaderRow: false).ToList(); + // Verify - use hasHeaderRow=false since we want to see all rows + var rows = _importer.Query(outputPath.ToString(), hasHeaderRow: false).ToList(); Assert.Equal("Jack", rows[2].A); Assert.Equal(123, rows[2].D); diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs index 329d4f80..981feab2 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs @@ -405,7 +405,7 @@ public Task Issue122() }; var path1 = PathHelper.GetFile("xlsx/TestIssue122.xlsx"); - var rows1 = _excelImporter.QueryAsync(path1, useHeaderRow: true, configuration: config).ToBlockingEnumerable().ToList(); + var rows1 = _excelImporter.QueryAsync(path1, hasHeaderRow: true, configuration: config).ToBlockingEnumerable().ToList(); Assert.Equal("HR", rows1[0].Department); Assert.Equal("HR", rows1[1].Department); @@ -415,7 +415,7 @@ public Task Issue122() Assert.Equal("IT", rows1[5].Department); var path2 = PathHelper.GetFile("xlsx/TestIssue122_2.xlsx"); - var rows2 = _excelImporter.QueryAsync(path2, useHeaderRow: true, configuration: config).ToBlockingEnumerable().ToList(); + var rows2 = _excelImporter.QueryAsync(path2, hasHeaderRow: true, configuration: config).ToBlockingEnumerable().ToList(); Assert.Equal("V1", rows2[2].Test1); Assert.Equal("V2", rows2[5].Test2); @@ -537,7 +537,7 @@ public async Task Issue147() { { var path = PathHelper.GetFile("xlsx/TestIssue147.xlsx"); - var q = _excelImporter.QueryAsync(path, useHeaderRow: false, startCell: "C3", sheetName: "Sheet1").ToBlockingEnumerable(); + var q = _excelImporter.QueryAsync(path, hasHeaderRow: false, startCell: "C3", sheetName: "Sheet1").ToBlockingEnumerable(); var rows = q.ToList(); Assert.Equal(["C", "D", "E"], (rows[0] as IDictionary)?.Keys); @@ -559,7 +559,7 @@ public async Task Issue147() { var path = PathHelper.GetFile("xlsx/TestIssue147.xlsx"); - var q = _excelImporter.QueryAsync(path, useHeaderRow: true, startCell: "C3", sheetName: "Sheet1").ToBlockingEnumerable(); + var q = _excelImporter.QueryAsync(path, hasHeaderRow: true, startCell: "C3", sheetName: "Sheet1").ToBlockingEnumerable(); var rows = q.ToList(); Assert.Equal(["Column1", "Column2", "Column3"], (rows[0] as IDictionary)?.Keys); @@ -574,7 +574,7 @@ public async Task Issue147() } Assert.Equal(10, rows.Count); - var columns = await _excelImporter.GetColumnNamesAsync(path, useHeaderRow: true, startCell: "C3"); + var columns = await _excelImporter.GetColumnNamesAsync(path, hasHeaderRow: true, startCell: "C3"); Assert.Equal(["Column1", "Column2", "Column3"], columns); } } @@ -699,7 +699,7 @@ public async Task IssueI3OSKV() public async Task Issue220() { var path = PathHelper.GetFile("xlsx/TestIssue220.xlsx"); - var rows = _excelImporter.QueryAsync(path, useHeaderRow: true).ToBlockingEnumerable(); + var rows = _excelImporter.QueryAsync(path, hasHeaderRow: true).ToBlockingEnumerable(); var result = rows .GroupBy(s => s.PRT_ID) .Select(g => new diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs index 59ffd65a..ecb99e1f 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs @@ -137,7 +137,7 @@ public void TestIssue430() ]; _excelExporter.Export(path.ToString(), value); - var testValue = _excelImporter.Query(path.ToString(), useHeaderRow: true).First(); + var testValue = _excelImporter.Query(path.ToString(), hasHeaderRow: true).First(); Assert.Equal("2021-01-31 10:03:00", testValue.Date.ToString("yyyy-MM-dd HH:mm:ss")); } @@ -301,7 +301,7 @@ public void TestIssue360() var sheets = _excelImporter.GetSheetNames(path); foreach (var sheetName in sheets) { - var dt = _excelImporter.QueryAsDataTable(path, useHeaderRow: true, sheetName: sheetName, configuration: config); + var dt = _excelImporter.QueryAsDataTable(path, hasHeaderRow: true, sheetName: sheetName, configuration: config); } } @@ -1767,7 +1767,7 @@ public void Issue122() { var path = PathHelper.GetFile("xlsx/TestIssue122.xlsx"); { - var rows = _excelImporter.Query(path, useHeaderRow: true, configuration: config).ToList(); + var rows = _excelImporter.Query(path, hasHeaderRow: true, configuration: config).ToList(); Assert.Equal("HR", rows[0].Department); Assert.Equal("HR", rows[1].Department); Assert.Equal("HR", rows[2].Department); @@ -1780,7 +1780,7 @@ public void Issue122() { var path = PathHelper.GetFile("xlsx/TestIssue122_2.xlsx"); { - var rows = _excelImporter.Query(path, useHeaderRow: true, configuration: config).ToList(); + var rows = _excelImporter.Query(path, hasHeaderRow: true, configuration: config).ToList(); Assert.Equal("V1", rows[2].Test1); Assert.Equal("V2", rows[5].Test2); Assert.Equal("V3", rows[1].Test3); @@ -1897,7 +1897,7 @@ public void Issue147() { { var path = PathHelper.GetFile("xlsx/TestIssue147.xlsx"); - var rows = _excelImporter.Query(path, useHeaderRow: false, startCell: "C3", sheetName: "Sheet1").ToList(); + var rows = _excelImporter.Query(path, hasHeaderRow: false, startCell: "C3", sheetName: "Sheet1").ToList(); Assert.Equal(["C", "D", "E"], (rows[0] as IDictionary)?.Keys); Assert.Equal(["Column1", "Column2", "Column3"], new[] { rows[0].C as string, rows[0].D as string, rows[0].E as string }); @@ -1917,7 +1917,7 @@ public void Issue147() { var path = PathHelper.GetFile("xlsx/TestIssue147.xlsx"); - var rows = _excelImporter.Query(path, useHeaderRow: true, startCell: "C3", sheetName: "Sheet1").ToList(); + var rows = _excelImporter.Query(path, hasHeaderRow: true, startCell: "C3", sheetName: "Sheet1").ToList(); Assert.Equal(["Column1", "Column2", "Column3"], (rows[0] as IDictionary)?.Keys); Assert.Equal(["C4", "D4", "E4"], new[] { rows[0].Column1 as string, rows[0].Column2 as string, rows[0].Column3 as string }); @@ -1929,7 +1929,7 @@ public void Issue147() Assert.Equal(10, rows.Count); - var columns = _excelImporter.GetColumnNames(path, useHeaderRow: true, startCell: "C3"); + var columns = _excelImporter.GetColumnNames(path, hasHeaderRow: true, startCell: "C3"); Assert.Equal(["Column1", "Column2", "Column3"], columns); } } @@ -2029,7 +2029,7 @@ public void IssueI3OSKV() public void Issue220() { var path = PathHelper.GetFile("xlsx/TestIssue220.xlsx"); - var rows = _excelImporter.Query(path, useHeaderRow: true); + var rows = _excelImporter.Query(path, hasHeaderRow: true); var result = rows .GroupBy(s => s.PRT_ID) .Select(g => new @@ -3190,10 +3190,10 @@ public void Issue_686() { var path = PathHelper.GetFile("xlsx/TestIssue686.xlsx"); Assert.Throws(() => - _excelImporter.QueryRange(path, useHeaderRow: false, startCell: "ZZFF10", endCell: "ZZFF11").First()); + _excelImporter.QueryRange(path, hasHeaderRow: false, startCell: "ZZFF10", endCell: "ZZFF11").First()); Assert.Throws(() => - _excelImporter.QueryRange(path, useHeaderRow: false, startCell: "ZZFF@@10", endCell: "ZZFF@@11").First()); + _excelImporter.QueryRange(path, hasHeaderRow: false, startCell: "ZZFF@@10", endCell: "ZZFF@@11").First()); } [Fact] @@ -3236,7 +3236,7 @@ public void Issue_710() }); memoryStream.Position = 0; - using var dataReader = _excelImporter.GetDataReader(memoryStream, useHeaderRow: false); + using var dataReader = _excelImporter.GetDataReader(memoryStream, hasHeaderRow: false); dataReader.Read(); for (int i = 0; i < dataReader.FieldCount; i++) diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs index b00f523c..7e96ff28 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs @@ -225,7 +225,7 @@ public async Task CenterEmptyRowsQueryTest() await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true).Cast>().ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true).Cast>().ToListAsync(); Assert.Equal(1d, rows[0]["a"]); Assert.Null(rows[0]["b"]); Assert.Equal(3d, rows[0]["c"]); @@ -272,7 +272,7 @@ public async Task TestDynamicQueryBasic_useHeaderRow() var path = PathHelper.GetFile("xlsx/TestDynamicQueryBasic.xlsx"); await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true).Cast>().ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true).Cast>().ToListAsync(); Assert.Equal("MiniExcel", rows[0]["Column1"]); Assert.Equal(1d, rows[0]["Column2"]); Assert.Equal("Github", rows[1]["Column1"]); @@ -280,7 +280,7 @@ public async Task TestDynamicQueryBasic_useHeaderRow() } { - var rows = await _excelImporter.QueryAsync(path, useHeaderRow: true).ToListAsync(); + var rows = await _excelImporter.QueryAsync(path, hasHeaderRow: true).ToListAsync(); Assert.Equal("MiniExcel", rows[0].Column1); Assert.Equal(1d, rows[0].Column2); Assert.Equal("Github", rows[1].Column1); @@ -323,7 +323,7 @@ public async Task QueryStrongTypeMapping_Test() } { - var rows = _excelImporter.Query(path, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(path, hasHeaderRow: true).ToList(); Assert.Equal(100, rows.Count); Assert.Equal("78DE23D2-DCB6-BD3D-EC67-C112BBC322A2", rows[0].ID); @@ -469,7 +469,7 @@ public async Task SaveAsFileWithDimensionByICollection() await _excelExporter.ExportAsync(path, values); await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: false).Cast>().ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: false).Cast>().ToListAsync(); Assert.Equal(3, rows.Count); Assert.Equal("A", rows[0]["A"]); Assert.Equal("A", rows[1]["A"]); @@ -478,7 +478,7 @@ public async Task SaveAsFileWithDimensionByICollection() await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true).Cast>().ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true).Cast>().ToListAsync(); Assert.Equal(2, rows.Count); Assert.Equal("A", rows[0]["A"]); Assert.Equal("A", rows[1]["A"]); @@ -501,7 +501,7 @@ public async Task SaveAsFileWithDimensionByICollection() await _excelExporter.ExportAsync(path, values, false); await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: false).ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: false).ToListAsync(); Assert.Empty(rows); } @@ -514,7 +514,7 @@ public async Task SaveAsFileWithDimensionByICollection() await _excelExporter.ExportAsync(path, values); { await using var stream = File.OpenRead(path); - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: false).ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: false).ToListAsync(); Assert.Single(rows); } Assert.Equal("A1:B1", SheetHelper.GetFirstSheetDimensionRefValue(path)); @@ -536,7 +536,7 @@ public async Task SaveAsFileWithDimensionByICollection() await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: false).Cast>().ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: false).Cast>().ToListAsync(); Assert.Equal(3, rows.Count); Assert.Equal("A", rows[0]["A"]); Assert.Equal("A", rows[1]["A"]); @@ -545,7 +545,7 @@ public async Task SaveAsFileWithDimensionByICollection() await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true).Cast>().ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true).Cast>().ToListAsync(); Assert.Equal(2, rows.Count); Assert.Equal("A", rows[0]["A"]); Assert.Equal("A", rows[1]["A"]); @@ -603,7 +603,7 @@ public async Task SaveAsFileWithDimension() await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true) + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true) .Cast>() .ToListAsync(); @@ -712,7 +712,7 @@ public async Task EmptyTest() await using (var stream = File.OpenRead(path.ToString())) { - var row = await _excelImporter.QueryAsync(stream, useHeaderRow: true).ToListAsync(); + var row = await _excelImporter.QueryAsync(stream, hasHeaderRow: true).ToListAsync(); Assert.Empty(row); } } @@ -732,7 +732,7 @@ public async Task SaveAsByIEnumerableIDictionary() await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: false).Cast>().ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: false).Cast>().ToListAsync(); Assert.Equal("Column1", rows[0]["A"]); Assert.Equal("Column2", rows[0]["B"]); Assert.Equal("MiniExcel", rows[1]["A"]); @@ -743,7 +743,7 @@ public async Task SaveAsByIEnumerableIDictionary() await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true).Cast>().ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true).Cast>().ToListAsync(); Assert.Equal(2, rows.Count); Assert.Equal("MiniExcel", rows[0]["Column1"]); Assert.Equal(1d, rows[0]["Column2"]); @@ -767,7 +767,7 @@ public async Task SaveAsByIEnumerableIDictionary() await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: false).ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: false).ToListAsync(); Assert.Equal(3, rows.Count); } Assert.Equal("A1:B3", SheetHelper.GetFirstSheetDimensionRefValue(path)); @@ -798,7 +798,7 @@ public async Task SaveAsByIEnumerableIDictionaryWithDynamicConfiguration() await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true).Cast>().ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true).Cast>().ToListAsync(); Assert.Equal(2, rows.Count); Assert.Equal("Name Column", rows[0].Keys.ElementAt(0)); Assert.Equal("Value Column", rows[0].Keys.ElementAt(1)); @@ -836,7 +836,7 @@ await _excelExporter.ExportAsync( await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true).ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true).ToListAsync(); Assert.Equal("MiniExcel", rows[0].Column1); Assert.Equal(1, rows[0].Column2); @@ -881,7 +881,7 @@ public async Task SaveAsByDapperRows() await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true).Cast>().ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true).Cast>().ToListAsync(); Assert.Equal("MiniExcel", rows[0]["Column1"]); Assert.Equal(1d, rows[0]["Column2"]); Assert.Equal("Github", rows[1]["Column1"]); @@ -897,13 +897,13 @@ public async Task SaveAsByDapperRows() await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: false).ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: false).ToListAsync(); Assert.Empty(rows); } await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true).ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true).ToListAsync(); Assert.Empty(rows); } Assert.Equal("A1", SheetHelper.GetFirstSheetDimensionRefValue(path)); @@ -918,7 +918,7 @@ public async Task SaveAsByDapperRows() await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: false).Cast>().ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: false).Cast>().ToListAsync(); Assert.Equal("Column1", rows[0]["A"]); Assert.Equal("Column2", rows[0]["B"]); Assert.Equal("MiniExcel", rows[1]["A"]); @@ -929,7 +929,7 @@ public async Task SaveAsByDapperRows() await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true).Cast>().ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true).Cast>().ToListAsync(); Assert.Equal("MiniExcel", rows[0]["Column1"]); Assert.Equal(1d, rows[0]["Column2"]); Assert.Equal("Github", rows[1]["Column1"]); @@ -956,7 +956,7 @@ public async Task QueryByStrongTypeParameterTest() await _excelExporter.ExportAsync(path, values); await using var stream = File.OpenRead(path); - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true).Cast>().ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true).Cast>().ToListAsync(); Assert.Equal("MiniExcel", rows[0]["Column1"]); Assert.Equal(1d, rows[0]["Column2"]); @@ -977,7 +977,7 @@ public async Task QueryByDictionaryStringAndObjectParameterTest() await _excelExporter.ExportAsync(path, values); await using var stream = File.OpenRead(path); - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true).Cast>().ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true).Cast>().ToListAsync(); Assert.Equal("MiniExcel", rows[0]["Column1"]); Assert.Equal(1d, rows[0]["Column2"]); @@ -1037,7 +1037,7 @@ await _excelExporter.ExportAsync(path, new[] await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true).Cast>().ToListAsync(); + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true).Cast>().ToListAsync(); Assert.Equal("MiniExcel", rows[0]["Column1"]); Assert.Equal(1d, rows[0]["Column2"]); Assert.Equal("Github", rows[1]["Column1"]); @@ -1066,7 +1066,7 @@ public async Task SaveAsBasicStreamTest() await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true) + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true) .Cast>() .ToListAsync(); @@ -1095,7 +1095,7 @@ public async Task SaveAsBasicStreamTest() await using (var stream = File.OpenRead(path)) { - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true) + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true) .Cast>() .ToListAsync(); @@ -1275,7 +1275,7 @@ public async Task DynamicColumnsConfigurationIsUsedWhenCreatingExcelUsingIDataRe await _excelExporter.ExportAsync(path.ToString(), reader, configuration: configuration); await using var stream = File.OpenRead(path.ToString()); - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true) + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true) .Cast>() .ToListAsync(); @@ -1341,7 +1341,7 @@ public async Task DynamicColumnsConfigurationIsUsedWhenCreatingExcelUsingDataTab await _excelExporter.ExportAsync(path.ToString(), table, configuration: configuration); await using var stream = File.OpenRead(path.ToString()); - var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true) + var rows = await _excelImporter.QueryAsync(stream, hasHeaderRow: true) .Cast>() .ToListAsync(); @@ -1525,7 +1525,7 @@ public async Task SaveAsByAsyncEnumerable() using var path = AutoDeletingPath.Create(); await _excelExporter.ExportAsync(path.ToString(), GetValues()); - var results = await _excelImporter.QueryAsync(path.ToString(), useHeaderRow: true).ToListAsync(); + var results = await _excelImporter.QueryAsync(path.ToString(), hasHeaderRow: true).ToListAsync(); Assert.Equal(2, results.Count); Assert.Equal("MiniExcel", results[0].Column1); diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlMultipleSheetTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlMultipleSheetTests.cs index c9e79a2e..d7de0b0f 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlMultipleSheetTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlMultipleSheetTests.cs @@ -154,34 +154,34 @@ public void DynamicSheetConfigurationIsUsedWhenReadExcel() using (var stream = File.OpenRead(path)) { // take first sheet as default - var users = _excelImporter.Query(stream, configuration: configuration, useHeaderRow: true).ToList(); + var users = _excelImporter.Query(stream, configuration: configuration, hasHeaderRow: true).ToList(); Assert.Equal(2, users.Count); Assert.Equal("Jack", users[0].Name); // take second sheet by sheet name - var departments = _excelImporter.Query(stream, sheetName: "Departments", configuration: configuration, useHeaderRow: true).ToList(); + var departments = _excelImporter.Query(stream, sheetName: "Departments", configuration: configuration, hasHeaderRow: true).ToList(); Assert.Equal(2, departments.Count); Assert.Equal("HR", departments[0].Name); // take second sheet by sheet key - departments = _excelImporter.Query(stream, sheetName: "departmentSheet", configuration: configuration, useHeaderRow: true).ToList(); + departments = _excelImporter.Query(stream, sheetName: "departmentSheet", configuration: configuration, hasHeaderRow: true).ToList(); Assert.Equal(2, departments.Count); Assert.Equal("HR", departments[0].Name); } { // take first sheet as default - var users = _excelImporter.Query(path, configuration: configuration, useHeaderRow: true).ToList(); + var users = _excelImporter.Query(path, configuration: configuration, hasHeaderRow: true).ToList(); Assert.Equal(2, users.Count); Assert.Equal("Jack", users[0].Name); // take second sheet by sheet name - var departments = _excelImporter.Query(path, sheetName: "Departments", configuration: configuration, useHeaderRow: true).ToList(); + var departments = _excelImporter.Query(path, sheetName: "Departments", configuration: configuration, hasHeaderRow: true).ToList(); Assert.Equal(2, departments.Count); Assert.Equal("HR", departments[0].Name); // take second sheet by sheet key - departments = _excelImporter.Query(path, sheetName: "departmentSheet", configuration: configuration, useHeaderRow: true).ToList(); + departments = _excelImporter.Query(path, sheetName: "departmentSheet", configuration: configuration, hasHeaderRow: true).ToList(); Assert.Equal(2, departments.Count); Assert.Equal("HR", departments[0].Name); } diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs index db513c08..d619d02d 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs @@ -28,7 +28,7 @@ public void GetColumnsTest() var tePath = PathHelper.GetFile("xlsx/TestEmpty.xlsx"); { - var columns = _excelImporter.GetColumnNames (tmPath); + var columns = _excelImporter.GetColumnNames(tmPath); Assert.Equal(["A", "B", "C", "D", "E", "F", "G", "H"], columns); } @@ -232,7 +232,7 @@ public void CenterEmptyRowsQueryTest() using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Equal(1, rows[0].a); Assert.Null(rows[0].b); @@ -294,12 +294,12 @@ public void TestDynamicQueryBasic_WithoutHead() } [Fact] - public void TestDynamicQueryBasic_useHeaderRow() + public void TestDynamicQueryBasic_hasHeaderRow() { var path = PathHelper.GetFile("xlsx/TestDynamicQueryBasic.xlsx"); using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Equal("MiniExcel", rows[0].Column1); Assert.Equal(1, rows[0].Column2); @@ -308,7 +308,7 @@ public void TestDynamicQueryBasic_useHeaderRow() } { - var rows = _excelImporter.Query(path, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(path, hasHeaderRow: true).ToList(); Assert.Equal("MiniExcel", rows[0].Column1); Assert.Equal(1, rows[0].Column2); @@ -543,7 +543,7 @@ public void SaveAsFileWithDimensionByICollection() using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.Query(stream, useHeaderRow: false).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: false).ToList(); Assert.Equal(3, rows.Count); Assert.Equal("A", rows[0].A); Assert.Equal("A", rows[1].A); @@ -551,7 +551,7 @@ public void SaveAsFileWithDimensionByICollection() } using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Equal(2, rows.Count); Assert.Equal("A", rows[0].A); Assert.Equal("A", rows[1].A); @@ -572,7 +572,7 @@ public void SaveAsFileWithDimensionByICollection() { using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.Query(stream, useHeaderRow: false).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: false).ToList(); Assert.Empty(rows); } Assert.Equal("A1:B1", SheetHelper.GetFirstSheetDimensionRefValue(path)); @@ -581,7 +581,7 @@ public void SaveAsFileWithDimensionByICollection() _excelExporter.Export(path, values, overwriteFile: true); { using var stream = File.OpenRead(path); - var rows = _excelImporter.Query(stream, useHeaderRow: false).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: false).ToList(); Assert.Single(rows); } Assert.Equal("A1:B1", SheetHelper.GetFirstSheetDimensionRefValue(path)); @@ -600,7 +600,7 @@ public void SaveAsFileWithDimensionByICollection() { using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.Query(stream, useHeaderRow: false).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: false).ToList(); Assert.Equal(3, rows.Count); Assert.Equal("A", rows[0].A); Assert.Equal("A", rows[1].A); @@ -608,7 +608,7 @@ public void SaveAsFileWithDimensionByICollection() } using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Equal(2, rows.Count); Assert.Equal("A", rows[0].A); Assert.Equal("A", rows[1].A); @@ -661,7 +661,7 @@ public void SaveAsFileWithDimension() _excelExporter.Export(path, table); Assert.Equal("A1:D3", SheetHelper.GetFirstSheetDimensionRefValue(path)); - var rowsWithHeader = _excelImporter.Query(path, useHeaderRow: true).ToList(); + var rowsWithHeader = _excelImporter.Query(path, hasHeaderRow: true).ToList(); Assert.Equal(2, rowsWithHeader.Count); Assert.Equal(@"""<>+-*//}{\\n", rowsWithHeader[0].a); Assert.Equal(1234567890, rowsWithHeader[0].b); @@ -767,7 +767,7 @@ public void EmptyTest() } using (var stream = File.OpenRead(path.ToString())) { - var rows = _excelImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Empty(rows); } } @@ -793,7 +793,7 @@ public void SaveAsByIEnumerableIDictionary() using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.Query(stream, useHeaderRow: false).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: false).ToList(); Assert.Equal("Column1", rows[0].A); Assert.Equal("Column2", rows[0].B); Assert.Equal("MiniExcel", rows[1].A); @@ -806,7 +806,7 @@ public void SaveAsByIEnumerableIDictionary() using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Equal(2, rows.Count); Assert.Equal("MiniExcel", rows[0].Column1); @@ -830,7 +830,7 @@ public void SaveAsByIEnumerableIDictionary() using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.Query(stream, useHeaderRow: false).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: false).ToList(); Assert.Equal(3, rows.Count); } @@ -861,7 +861,7 @@ public void SaveAsFrozenRowsAndColumnsTest() using (var stream = File.OpenRead(path.ToString())) { - var rows = _excelImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Equal("MiniExcel", rows[0].Column1); Assert.Equal(1, rows[0].Column2); @@ -909,7 +909,7 @@ public void SaveAsByDapperRows() using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Equal("MiniExcel", rows[0].Column1); Assert.Equal(1, rows[0].Column2); @@ -926,13 +926,13 @@ public void SaveAsByDapperRows() using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.Query(stream, useHeaderRow: false).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: false).ToList(); Assert.Empty(rows); } using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Empty(rows); } @@ -949,7 +949,7 @@ public void SaveAsByDapperRows() using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.Query(stream, useHeaderRow: false).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: false).ToList(); Assert.Equal("Column1", rows[0].A); Assert.Equal("Column2", rows[0].B); @@ -961,7 +961,7 @@ public void SaveAsByDapperRows() using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Equal("MiniExcel", rows[0].Column1); Assert.Equal(1, rows[0].Column2); @@ -987,7 +987,7 @@ public void QueryByStrongTypeParameterTest() _excelExporter.Export(path.ToString(), values); using var stream = File.OpenRead(path.ToString()); - var rows = _excelImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Equal("MiniExcel", rows[0].Column1); Assert.Equal(1, rows[0].Column2); @@ -1007,7 +1007,7 @@ public void QueryByDictionaryStringAndObjectParameterTest() _excelExporter.Export(path.ToString(), values); using var stream = File.OpenRead(path.ToString()); - var rows = _excelImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Equal("MiniExcel", rows[0].Column1); Assert.Equal(1, rows[0].Column2); @@ -1067,7 +1067,7 @@ public void SaveAsBasicCreateTest() using (var stream = File.OpenRead(path.ToString())) { - var rows = _excelImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Equal("MiniExcel", rows[0].Column1); Assert.Equal(1, rows[0].Column2); @@ -1097,7 +1097,7 @@ public void SaveAsBasicStreamTest() using (var stream = File.OpenRead(path.ToString())) { - var rows = _excelImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Equal("MiniExcel", rows[0].Column1); Assert.Equal(1, rows[0].Column2); @@ -1124,7 +1124,7 @@ public void SaveAsBasicStreamTest() using (var stream = File.OpenRead(path.ToString())) { - var rows = _excelImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _excelImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Equal("MiniExcel", rows[0].Column1); Assert.Equal(1, rows[0].Column2); @@ -1319,7 +1319,7 @@ public void DynamicColumnsConfigurationIsUsedWhenCreatingExcelUsingIDataReader() _excelExporter.Export(path.ToString(), reader, configuration: configuration); using var stream = File.OpenRead(path.ToString()); - var rows = _excelImporter.Query(stream, useHeaderRow: true) + var rows = _excelImporter.Query(stream, hasHeaderRow: true) .Select(x => (IDictionary)x) .ToList(); @@ -1385,7 +1385,7 @@ public void DynamicColumnsConfigurationIsUsedWhenCreatingExcelUsingDataTable() _excelExporter.Export(path.ToString(), table, configuration: configuration); using var stream = File.OpenRead(path.ToString()); - var rows = _excelImporter.Query(stream, useHeaderRow: true) + var rows = _excelImporter.Query(stream, hasHeaderRow: true) .Select(x => (IDictionary)x) .ToList(); From 65fd24af5967ec99eb36425d0c885322ce9ca9f5 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 30 May 2026 15:22:25 +0200 Subject: [PATCH 07/14] Added documentation to all OpenXmlImporter methods --- V2-Upgrade-Notes.md | 4 +- src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs | 290 ++++++++++++++++--- 2 files changed, 259 insertions(+), 35 deletions(-) diff --git a/V2-Upgrade-Notes.md b/V2-Upgrade-Notes.md index 3056388c..a54923f8 100644 --- a/V2-Upgrade-Notes.md +++ b/V2-Upgrade-Notes.md @@ -12,4 +12,6 @@ - `IConfiguration` is now `IMiniExcelConfiguration`, but most methods now require the proper implementation (`OpenXmlConfiguration` or `CsvConfiguration`) to be provided rather than the interface - MiniExcel now fully supports asynchronous streaming the queries, so the return type for `OpenXmlImporter.QueryAsync` is `IAsyncEnumerable` instead of `Task>` -- When applying a template, unlike version 1.x, the flag for overwriting an already existing file must be provided explicitly. \ No newline at end of file +- When applying a template, unlike version 1.x, the flag for overwriting an already existing file must be provided explicitly. +- `leaveOpen` parameter has been added to `GetDataReader` and `GetDataReaderAsync` to configure whether the underlying stream must be disposed alongside the data reader. +- `useHeaderRow` parameter in multiple `OpenXmlImporter` methods has been renamed to `hasHeaderRow` for making its usage clearer. \ No newline at end of file diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs index bc1ca851..45c8d821 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs @@ -1,14 +1,22 @@ - - // ReSharper disable once CheckNamespace namespace MiniExcelLib.OpenXml; public sealed partial class OpenXmlImporter { internal OpenXmlImporter() { } - + #region Query + /// + /// Queries an Excel document using a strongly-typed class model. + /// + /// The class type to map each row to. Must have a parameterless constructor. + /// The path to the Excel document. + /// The name of the worsksheet to query. If not specified, the first sheet is used. + /// The starting cell reference (e.g., "C2"). Default is "A1". + /// If true, the first row is treated as data. If false (default), the first row is used as headers. + /// Optional configuration settings. + /// A token to cancel the asynchronous operation. [CreateSyncVersion] public async IAsyncEnumerable QueryAsync(string path, string? sheetName = null, string startCell = "A1", bool treatHeaderAsData = false, OpenXmlConfiguration? configuration = null, @@ -23,6 +31,16 @@ public async IAsyncEnumerable QueryAsync(string path, string? sheetName = yield return item; } + /// + /// Queries an Excel document using a strongly-typed class model. + /// + /// The class type to map each row to. Must have a parameterless constructor. + /// The stream containing the Excel file data. The stream position is not reset after reading. + /// The name of the worsksheet to query. If not specified, the first sheet is used. + /// The starting cell reference (e.g., "C2"). Default is "A1". + /// If true, the first row is treated as data. If false (default), the first row is used as headers. + /// Optional configuration settings. + /// A token to cancel the asynchronous operation. [CreateSyncVersion] public async IAsyncEnumerable QueryAsync(Stream stream, string? sheetName = null, string startCell = "A1", bool treatHeaderAsData = false, OpenXmlConfiguration? configuration = null, @@ -33,6 +51,18 @@ public async IAsyncEnumerable QueryAsync(Stream stream, string? sheetName yield return item; } + /// + /// Queries an Excel document and returns dynamic objects representing each row. + /// + /// The path to the OpenXml document. + /// If true, the first row is used as column headers for the dynamic object properties. Default is false. + /// The name of the sheet to query. If null, the first sheet is used. + /// The starting cell reference (e.g., "C2"). Default is "A1". + /// Optional configuration settings. + /// A token to cancel the asynchronous operation. + /// + /// When is true, column names from the first row become dynamic property names, otherwise they will be assigned alphabetically (A, B, C, etc.). + /// [CreateSyncVersion] public async IAsyncEnumerable QueryAsync(string path, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, @@ -45,6 +75,18 @@ public async IAsyncEnumerable QueryAsync(string path, bool hasHeaderRow yield return item; } + /// + /// Queries an Excel document and returns dynamic objects representing each row. + /// + /// The stream containing the Excel file data. The stream position is not reset after reading. + /// If true, the first row is used as column headers for the dynamic object properties. Default is false. + /// The name of the sheet to query. If null, the first sheet is used. + /// The starting cell reference (e.g., "C2"). Default is "A1". + /// Optional configuration settings. + /// A token to cancel the asynchronous operation. + /// + /// When is true, column names from the first row become dynamic property names, otherwise they will be assigned alphabetically (A, B, C, etc.). + /// [CreateSyncVersion] public async IAsyncEnumerable QueryAsync(Stream stream, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, @@ -60,13 +102,15 @@ public async IAsyncEnumerable QueryAsync(Stream stream, bool hasHeaderR #region Query Range /// - /// Extract the given range。 Only uppercase letters are effective。 - /// e.g. - /// MiniExcel.QueryRange(path, startCell: "A2", endCell: "C3") - /// A2 represents the second row of column A, C3 represents the third row of column C - /// If you don't want to restrict rows, just don't include numbers + /// Queries a specific rectangular region within an worksheet using index-based coordinates. /// - /// + /// The path to the Excel document. + /// If true, the first row within the range is used as column headers for dynamic object properties. Default is false. + /// The name of the sheet to query. If null, the first sheet is used. + /// The starting cell reference. Default is "A1". + /// The ending cell reference. If left empty, the last cell containing data will be used. + /// Optional configuration settings. + /// A token to cancel the asynchronous operation. [CreateSyncVersion] public async IAsyncEnumerable QueryRangeAsync(string path, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", string? endCell = null, OpenXmlConfiguration? configuration = null, @@ -79,6 +123,16 @@ public async IAsyncEnumerable QueryRangeAsync(string path, bool hasHead yield return item; } + /// + /// Queries a specific rectangular region within an worksheet using index-based coordinates. + /// + /// The stream containing the Excel file data. The stream position is not reset after reading. + /// If true, the first row within the range is used as column headers for dynamic object properties. Default is false. + /// The name of the sheet to query. If null, the first sheet is used. + /// The starting cell reference. Default is "A1". + /// The ending cell reference. If left empty, the last cell containing data will be used. + /// Optional configuration settings. + /// A token to cancel the asynchronous operation. [CreateSyncVersion] public async IAsyncEnumerable QueryRangeAsync(Stream stream, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", string? endCell = null, OpenXmlConfiguration? configuration = null, @@ -89,6 +143,18 @@ public async IAsyncEnumerable QueryRangeAsync(Stream stream, bool hasHe yield return item; } + /// + /// Queries a specific rectangular region within an worksheet using index-based coordinates. + /// + /// The path to Excel document. + /// If true, the first row within the range is used as column headers for dynamic object properties. Default is false. + /// The name of the sheet to query. If null, the first sheet is used. + /// The 1-based index of the starting row. + /// The 1-based index of the starting column. + /// The 1-based index of the ending row (inclusive). If null, reads to the last row containing data. + /// The 1-based index of the ending column (inclusive). If null, reads to the last column containing data. + /// Optional configuration settings. + /// A token to cancel the asynchronous operation. [CreateSyncVersion] public async IAsyncEnumerable QueryRangeAsync(string path, bool hasHeaderRow = false, string? sheetName = null, int startRowIndex = 1, int startColumnIndex = 1, int? endRowIndex = null, @@ -102,6 +168,18 @@ public async IAsyncEnumerable QueryRangeAsync(string path, bool hasHead yield return item; } + /// + /// Queries a specific rectangular region within an worksheet using index-based coordinates. + /// + /// The stream containing the Excel file data. The stream position is not reset after reading. + /// If true, the first row within the range is used as column headers for dynamic object properties. Default is false. + /// The name of the sheet to query. If null, the first sheet is used. + /// The 1-based index of the starting row. + /// The 1-based index of the starting column. + /// The 1-based index of the ending row (inclusive). If null, reads to the last row containing data. + /// The 1-based index of the ending column (inclusive). If null, reads to the last column containing data. + /// Optional configuration settings. + /// A token to cancel the asynchronous operation. [CreateSyncVersion] public async IAsyncEnumerable QueryRangeAsync(Stream stream, bool hasHeaderRow = false, string? sheetName = null, int startRowIndex = 1, int startColumnIndex = 1, int? endRowIndex = null, @@ -118,8 +196,18 @@ public async IAsyncEnumerable QueryRangeAsync(Stream stream, bool hasHe #region Query As DataTable /// - /// QueryAsDataTable is not recommended, because it'll load all data into memory. + /// Queries an Excel sheet and returns the results as a . /// + /// The path to the Excel file data. + /// If true, the first row is used as column headers. + /// The name of the sheet to query. If not specified, the first sheet is used. + /// The starting cell reference (e.g., "C2"). Default is "A1". + /// Optional configuration settings. + /// A token to cancel the asynchronous operation. + /// + /// Empty column names are skipped. + /// This method loads the entire worksheet into memory, so its usage is recommended only for datasets of moderate size. + /// [CreateSyncVersion] public async Task QueryAsDataTableAsync(string path, bool hasHeaderRow = true, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, @@ -132,8 +220,18 @@ public async Task QueryAsDataTableAsync(string path, bool hasHeaderRo } /// - /// QueryAsDataTable is not recommended, because it'll load all data into memory. + /// Queries an Excel sheet and returns the results as a . /// + /// The stream containing the Excel file data. The stream position is not reset after reading. + /// If true, the first row is used as column headers. + /// The name of the sheet to query. If not specified, the first sheet is used. + /// The starting cell reference (e.g., "C2"). Default is "A1". + /// Optional configuration settings. + /// A token to cancel the asynchronous operation. + /// + /// Empty column names are skipped. + /// This method loads the entire worksheet into memory, so its usage is recommended only for datasets of moderate size. + /// [CreateSyncVersion] public async Task QueryAsDataTableAsync(Stream stream, bool hasHeaderRow = true, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, @@ -189,6 +287,15 @@ public async Task QueryAsDataTableAsync(Stream stream, bool hasHeader #region Sheet Info + /// + /// Retrieves the names of all sheets in an Excel workbook. + /// + /// The path to the Excel file. + /// A token to cancel the asynchronous operation. + /// A list of sheet names in the workbook, or an empty list if no sheets are found. + /// + /// Sheet names are returned in the order they appear in the workbook. + /// [CreateSyncVersion] public async Task> GetSheetNamesAsync(string path, CancellationToken cancellationToken = default) { @@ -198,6 +305,15 @@ public async Task> GetSheetNamesAsync(string path, CancellationToke return await GetSheetNamesAsync(stream, cancellationToken).ConfigureAwait(false); } + /// + /// Retrieves the names of all sheets in an Excel workbook. + /// + /// The stream containing the Excel file data. The stream position is not reset after reading. + /// A token to cancel the asynchronous operation. + /// A list of sheet names in the workbook, or an empty list if no sheets are found. + /// + /// Sheet names are returned in the order they appear in the workbook. + /// [CreateSyncVersion] public async Task> GetSheetNamesAsync(Stream stream, CancellationToken cancellationToken = default) { @@ -209,8 +325,15 @@ public async Task> GetSheetNamesAsync(Stream stream, CancellationTo return rels?.Select(s => s.Name).ToList() ?? []; } + /// /// Retrieves detailed information about all sheets in an Excel workbook. /// + /// The path to the Excel file. + /// A token to cancel the asynchronous operation. + /// A list of objects containing metadata for each sheet, including name, dimensions, and sheet index. + /// + /// Sheet information is returned in the order sheets appear in the workbook. + /// [CreateSyncVersion] public async Task> GetSheetInformationsAsync(string path, CancellationToken cancellationToken = default) { @@ -220,8 +343,15 @@ public async Task> GetSheetInformationsAsync(string path, Cancel return await GetSheetInformationsAsync(stream, cancellationToken).ConfigureAwait(false); } + /// + /// Retrieves detailed information about all sheets in an Excel workbook. + /// /// The stream containing the Excel file data. The stream position is not reset after reading. /// A token to cancel the asynchronous operation. + /// A list of objects containing metadata for each sheet, including name, dimensions, and sheet index. + /// + /// Sheet information is returned in the order sheets appear in the workbook. + /// [CreateSyncVersion] public async Task> GetSheetInformationsAsync(Stream stream, CancellationToken cancellationToken = default) { @@ -234,6 +364,18 @@ public async Task> GetSheetInformationsAsync(Stream stream, Canc return rels?.Select((s, i) => s.ToSheetInfo((uint)i)).ToList() ?? []; } + /// + /// Retrieves the dimensions (used cell range) for all sheets in an Excel workbook. + /// + /// The path to the Excel file. + /// A token to cancel the asynchronous operation. + /// A list of objects representing the used dimensions for each sheet in the workbook. + /// + /// The dimension of a sheet represents the rectangular range of cells that contain data. + /// Each in the returned list corresponds to a sheet, in the order sheets appear in the workbook. + /// Empty sheets will have dimensions that reflect no used cells. + /// A synchronous version of this method is automatically generated via the [CreateSyncVersion] attribute. + /// [CreateSyncVersion] public async Task> GetSheetDimensionsAsync(string path, CancellationToken cancellationToken = default) { @@ -243,6 +385,18 @@ public async Task> GetSheetDimensionsAsync(string path, Cancel return await GetSheetDimensionsAsync(stream, cancellationToken).ConfigureAwait(false); } + /// + /// Retrieves the dimensions (used cell range) for all sheets in an Excel workbook. + /// + /// The stream containing the Excel file data. The stream position is not reset after reading. + /// A token to cancel the asynchronous operation. + /// A list of objects representing the used dimensions for each sheet in the workbook. + /// + /// The dimension of a sheet represents the rectangular range of cells that contain data. + /// Each in the returned list corresponds to a sheet, in the order sheets appear in the workbook. + /// Empty sheets will have dimensions that reflect no used cells. + /// A synchronous version of this method is automatically generated via the [CreateSyncVersion] attribute. + /// [CreateSyncVersion] public async Task> GetSheetDimensionsAsync(Stream stream, CancellationToken cancellationToken = default) { @@ -250,6 +404,18 @@ public async Task> GetSheetDimensionsAsync(Stream stream, Canc return await reader.GetDimensionsAsync(cancellationToken).ConfigureAwait(false); } + /// + /// Retrieves the column names from the first row (header row) of an Excel sheet. + /// + /// The path to the Excel document. + /// If true, the first row values are used as column names. If false, column letters (A, B, C, etc.) are used. Default is false. + /// The name of the worksheet to query. If not provided, the first sheet is used. + /// The starting cell reference (e.g., "C2"). Default is "A1". + /// A token to cancel the asynchronous operation. + /// A collection of column names from the specified location, or an empty collection if the sheet is empty. + /// + /// Returns an empty collection if the sheet has no rows starting from . + /// [CreateSyncVersion] public async Task> GetColumnNamesAsync(string path, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", CancellationToken cancellationToken = default) @@ -260,8 +426,19 @@ public async Task> GetColumnNamesAsync(string path, bool has return await GetColumnNamesAsync(stream, hasHeaderRow, sheetName, startCell, cancellationToken).ConfigureAwait(false); } + + /// + /// Retrieves the column names from the first row (header row) of an Excel sheet. + /// + /// The stream containing the Excel file data. /// If true, the first row values are used as column names. If false, column letters (A, B, C, etc.) are used. Default is false. /// The name of the worksheet to query. If not provided, the first sheet is used. + /// The starting cell reference (e.g., "C2"). Default is "A1". + /// A token to cancel the asynchronous operation. + /// A collection of column names from the specified location, or an empty collection if the sheet is empty. + /// + /// Returns an empty collection if the sheet has no rows starting from . + /// [CreateSyncVersion] public async Task> GetColumnNamesAsync(Stream stream, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", CancellationToken cancellationToken = default) @@ -275,6 +452,16 @@ public async Task> GetColumnNamesAsync(Stream stream, bool h return []; } + /// + /// Retrieves all threaded comments and notes from a specific sheet in an Excel workbook. + /// + /// The path to the Excel document. + /// The name of the worksheet from which to retrieve comments. If not provided, comments from the first sheet are returned. + /// A token to cancel the asynchronous operation. + /// + /// Comments are cell-level annotations in Excel files that are stored separately from the cell data. + /// The returned provides access to both threaded comments and legacy note comments, along with the associated metadata. + /// [CreateSyncVersion] public async Task RetrieveCommentsAsync(string path, string? sheetName, CancellationToken cancellationToken = default) { @@ -284,6 +471,16 @@ public async Task RetrieveCommentsAsync(string path, string? s return await RetrieveCommentsAsync(stream, sheetName, cancellationToken).ConfigureAwait(false); } + /// + /// Retrieves all threaded comments and notes from a specific sheet in an Excel workbook. + /// + /// The stream containing the Excel file data. The stream position is not reset after reading. + /// The name of the worksheet from which to retrieve comments. If not provided, comments from the first sheet are retrieved. + /// A token to cancel the asynchronous operation. + /// + /// Comments are cell-level annotations in Excel files that are stored separately from the cell data. + /// The returned provides access to both threaded comments and legacy note comments, along with the associated metadata. + /// [CreateSyncVersion] public async Task RetrieveCommentsAsync(Stream stream, string? sheetName, CancellationToken cancellationToken = default) { @@ -296,12 +493,18 @@ public async Task RetrieveCommentsAsync(Stream stream, string? #region DataReader /// - /// Gets an for the Excel document at the specified path. + /// Gets an for the Excel document provided for synchronous reading. /// - /// - /// Asynchronous reads are not allowed when creating the data reader from this overload and will result in an exception. - /// - public MiniExcelDataReader GetDataReader(string path, bool useHeaderRow = false, + /// The path to the Excel document. + /// If true, the first row is used as column headers. Default is false. + /// The name of the worksheet to read. If not provided, the first sheet is used. + /// The starting cell reference (e.g."C2"). Default is "A1". + /// Optional configuration settings. + /// + /// The returned implements and supports its standard reading patterns. + /// The data reader returned by this method is designed to perform synchronous, blocking reads, and will throw if an asynchronous operation is called from it. + /// For asynchronous reading scenarios, use instead. + /// public MiniExcelDataReader GetDataReader(string path, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null) { @@ -312,13 +515,19 @@ public MiniExcelDataReader GetDataReader(string path, bool hasHeaderRow = false, } /// - /// Gets an for the Excel document from an underlying stream. + /// Gets an for the Excel document from an underlying stream for synchronous reading. /// - /// - /// Asynchronous reads are not allowed when creating the data reader from this overload and will result in an exception. - /// - public MiniExcelDataReader GetDataReader(Stream stream, bool useHeaderRow = false, - string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null) + /// The stream containing the Excel file data. + /// If true, the first row is used as column headers. Default is false. + /// The name of the worksheet to read. If not provided, the first sheet is used. + /// The starting cell reference (e.g."C2"). Default is "A1". + /// Optional configuration settings. + /// True to leave the stream open after the data reader is disposed, otherwise false. + /// + /// The returned implements and supports its standard reading patterns. + /// The data reader returned by this method is designed to perform synchronous, blocking reads, and will throw if an asynchronous operation is called from it. + /// For asynchronous reading scenarios, use instead. + /// public MiniExcelDataReader GetDataReader(Stream stream, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, bool leaveOpen = false) { @@ -327,34 +536,47 @@ public MiniExcelDataReader GetDataReader(Stream stream, bool hasHeaderRow = fals } /// - /// Gets an for the Excel document at the specific path. - /// When created from this overload, the resulting data reader is supposed to be advanced asynchronously. + /// Gets an for the Excel document from an underlying stream for asynchronous reading. /// - public async Task GetAsyncDataReader(string path, bool useHeaderRow = false, - string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, - CancellationToken cancellationToken = default) + /// The path to the Excel document. + /// If true, the first row is used as column headers. Default is false. + /// The name of the worksheet to read. If null, the first sheet is used. + /// The starting cell reference (e.g."C2"). Default is "A1". + /// Optional configuration settings. + /// A token to cancel the asynchronous operation. + /// + /// The returned implements and supports its standard reading patterns. + /// The data reader returned by this method is designed to supports asynchronous reads, but will not throw an exception if a synchronous operation is performed. + /// Still, it's advised to use for synchronous reads instead. + /// public async Task GetAsyncDataReader(string path, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); - var values = QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken); var values = QueryAsync(stream, hasHeaderRow, sheetName, startCell, configuration, cancellationToken); return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken), leaveOpen: false).ConfigureAwait(false); } /// - /// Gets an for the Excel document from an underlying stream. - /// When created from this overload, the resulting data reader is supposed to be advanced asynchronously. + /// Gets an for the Excel document from an underlying stream for asynchronous reading. /// - public async Task GetAsyncDataReader(Stream stream, bool useHeaderRow = false, - string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, + /// The stream containing the Excel file data. + /// If true, the first row is used as column headers. Default is false. + /// The name of the worksheet to read. If null, the first sheet is used. + /// The starting cell reference (e.g."C2"). Default is "A1". + /// Optional configuration settings. + /// True to leave the stream open after the data reader is disposed, otherwise false. + /// A token to cancel the asynchronous operation. + /// + /// The returned implements and supports its standard reading patterns. + /// The data reader returned by this method is designed to supports asynchronous reads, but will not throw an exception if a synchronous operation is performed. + /// Still, it's advised to use for synchronous reads instead. + /// public async Task GetAsyncDataReader(Stream stream, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, bool leaveOpen = false, CancellationToken cancellationToken = default) { - var values = QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken); - return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken)).ConfigureAwait(false); var values = QueryAsync(stream, hasHeaderRow, sheetName, startCell, configuration, cancellationToken); return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken), leaveOpen).ConfigureAwait(false); } From 0c912b6835c5c17bae0b5510aca223be4bae789d Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 30 May 2026 15:33:04 +0200 Subject: [PATCH 08/14] Simplified namespaces in the fluent mapping api --- src/MiniExcel.OpenXml/FluentMapping/Api/MappingExporter.cs | 5 +++-- src/MiniExcel.OpenXml/FluentMapping/Api/MappingImporter.cs | 3 ++- src/MiniExcel.OpenXml/FluentMapping/Api/MappingTemplater.cs | 3 ++- .../FluentMapping/Api/ProviderExtensions.cs | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/MiniExcel.OpenXml/FluentMapping/Api/MappingExporter.cs b/src/MiniExcel.OpenXml/FluentMapping/Api/MappingExporter.cs index 855dc066..8b1c1d2d 100644 --- a/src/MiniExcel.OpenXml/FluentMapping/Api/MappingExporter.cs +++ b/src/MiniExcel.OpenXml/FluentMapping/Api/MappingExporter.cs @@ -1,4 +1,5 @@ -namespace MiniExcelLib.OpenXml.FluentMapping.Api; +// ReSharper disable once CheckNamespace +namespace MiniExcelLib.OpenXml.FluentMapping; public sealed partial class MappingExporter { @@ -40,4 +41,4 @@ public async Task ExportAsync(Stream? stream, IEnumerable? values, Cancell await MappingWriter.SaveAsAsync(stream, values, mapping, cancellationToken).ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/src/MiniExcel.OpenXml/FluentMapping/Api/MappingImporter.cs b/src/MiniExcel.OpenXml/FluentMapping/Api/MappingImporter.cs index affafd18..504afbcc 100644 --- a/src/MiniExcel.OpenXml/FluentMapping/Api/MappingImporter.cs +++ b/src/MiniExcel.OpenXml/FluentMapping/Api/MappingImporter.cs @@ -1,4 +1,5 @@ -namespace MiniExcelLib.OpenXml.FluentMapping.Api; +// ReSharper disable once CheckNamespace +namespace MiniExcelLib.OpenXml.FluentMapping; public sealed partial class MappingImporter() { diff --git a/src/MiniExcel.OpenXml/FluentMapping/Api/MappingTemplater.cs b/src/MiniExcel.OpenXml/FluentMapping/Api/MappingTemplater.cs index cb32fa2b..417e78bf 100644 --- a/src/MiniExcel.OpenXml/FluentMapping/Api/MappingTemplater.cs +++ b/src/MiniExcel.OpenXml/FluentMapping/Api/MappingTemplater.cs @@ -1,4 +1,5 @@ -namespace MiniExcelLib.OpenXml.FluentMapping.Api; +// ReSharper disable once CheckNamespace +namespace MiniExcelLib.OpenXml.FluentMapping; public sealed partial class MappingTemplater() { diff --git a/src/MiniExcel.OpenXml/FluentMapping/Api/ProviderExtensions.cs b/src/MiniExcel.OpenXml/FluentMapping/Api/ProviderExtensions.cs index 19391f5c..a93ad015 100644 --- a/src/MiniExcel.OpenXml/FluentMapping/Api/ProviderExtensions.cs +++ b/src/MiniExcel.OpenXml/FluentMapping/Api/ProviderExtensions.cs @@ -10,4 +10,4 @@ public static class ProviderExtensions public static MappingTemplater GetMappingTemplater(this MiniExcelTemplaterProvider templaterProvider) => new(); public static MappingTemplater GetMappingTemplater(this MiniExcelTemplaterProvider templaterProvider, MappingRegistry registry) => new(registry); -} \ No newline at end of file +} From 863e5cb3ed94c27268c40cd851eb898fdafa21df Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 30 May 2026 15:43:45 +0200 Subject: [PATCH 09/14] Sealed CsvReader and CsvWriter classes and simplified Dispose pattern --- src/MiniExcel.Csv/CsvReader.cs | 2 +- src/MiniExcel.Csv/CsvWriter.cs | 23 +++-------------------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/MiniExcel.Csv/CsvReader.cs b/src/MiniExcel.Csv/CsvReader.cs index 2a11dd58..d0c5ee8b 100644 --- a/src/MiniExcel.Csv/CsvReader.cs +++ b/src/MiniExcel.Csv/CsvReader.cs @@ -5,7 +5,7 @@ namespace MiniExcelLib.Csv; -internal partial class CsvReader : IMiniExcelReader +internal sealed partial class CsvReader : IMiniExcelReader { private readonly Stream _stream; private readonly CsvConfiguration _config; diff --git a/src/MiniExcel.Csv/CsvWriter.cs b/src/MiniExcel.Csv/CsvWriter.cs index 506f9801..d22df409 100644 --- a/src/MiniExcel.Csv/CsvWriter.cs +++ b/src/MiniExcel.Csv/CsvWriter.cs @@ -4,7 +4,7 @@ namespace MiniExcelLib.Csv; -internal partial class CsvWriter : IMiniExcelWriter, IDisposable +internal sealed partial class CsvWriter : IMiniExcelWriter, IDisposable { private readonly StreamWriter _writer; private readonly CsvConfiguration _configuration; @@ -207,29 +207,12 @@ private string GetHeader(List mappings) => string.Join( _configuration.Seperator.ToString(), mappings.Select(s => CsvSanitizer.SanitizeCsvField(s?.ExcelColumnName, _configuration))); - private void Dispose(bool disposing) + public void Dispose() { if (!_disposed) { - if (disposing) - { - _writer.Dispose(); - } - + _writer.Dispose(); _disposed = true; } } - - ~CsvWriter() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: false); - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } } From f107c7b151ffea36ff46b99444bbf1496e10010a Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 30 May 2026 16:31:27 +0200 Subject: [PATCH 10/14] Changed return type of API methods CsvExporter.Export from int[] to int --- V2-Upgrade-Notes.md | 3 ++- src/MiniExcel.Csv/Api/CsvExporter.cs | 15 +++++++-------- src/MiniExcel/MiniExcel.cs | 4 ++-- tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs | 9 +++------ tests/MiniExcel.Csv.Tests/IssueTests.cs | 3 +-- .../MiniExcel.Csv.Tests/MiniExcelCsvAsyncTests.cs | 13 ++++++------- tests/MiniExcel.Csv.Tests/MiniExcelCsvTests.cs | 12 ++++++------ 7 files changed, 27 insertions(+), 32 deletions(-) diff --git a/V2-Upgrade-Notes.md b/V2-Upgrade-Notes.md index a54923f8..34712fe1 100644 --- a/V2-Upgrade-Notes.md +++ b/V2-Upgrade-Notes.md @@ -14,4 +14,5 @@ so the return type for `OpenXmlImporter.QueryAsync` is `IAsyncEnumerable` instead of `Task>` - When applying a template, unlike version 1.x, the flag for overwriting an already existing file must be provided explicitly. - `leaveOpen` parameter has been added to `GetDataReader` and `GetDataReaderAsync` to configure whether the underlying stream must be disposed alongside the data reader. -- `useHeaderRow` parameter in multiple `OpenXmlImporter` methods has been renamed to `hasHeaderRow` for making its usage clearer. \ No newline at end of file +- `useHeaderRow` parameter in multiple `OpenXmlImporter` methods has been renamed to `hasHeaderRow` for making its usage clearer. +- `CsvExporter.Export` API methods, not being required to return the same type of `OpenXmlExporter.Export`, now return `int` instead of `int[]`. \ No newline at end of file diff --git a/src/MiniExcel.Csv/Api/CsvExporter.cs b/src/MiniExcel.Csv/Api/CsvExporter.cs index 3da4a5ee..c4f76f49 100644 --- a/src/MiniExcel.Csv/Api/CsvExporter.cs +++ b/src/MiniExcel.Csv/Api/CsvExporter.cs @@ -14,8 +14,7 @@ public async Task AppendAsync(string path, object value, bool printHeader = { if (!File.Exists(path)) { - var rowsWritten = await ExportAsync(path, value, printHeader, false, configuration, progress, cancellationToken).ConfigureAwait(false); - return rowsWritten.FirstOrDefault(); + return await ExportAsync(path, value, printHeader, false, configuration, progress, cancellationToken).ConfigureAwait(false); } var stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read, 4096, FileOptions.SequentialScan); @@ -36,7 +35,7 @@ public async Task AppendAsync(Stream stream, object value, CsvConfiguration } [CreateSyncVersion] - public async Task ExportAsync(string path, object value, bool printHeader = true, bool overwriteFile = false, + public async Task ExportAsync(string path, object value, bool printHeader = true, bool overwriteFile = false, CsvConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) { var stream = overwriteFile ? File.Create(path) : new FileStream(path, FileMode.CreateNew); @@ -46,12 +45,12 @@ public async Task ExportAsync(string path, object value, bool printHeader } [CreateSyncVersion] - public async Task ExportAsync(Stream stream, object value, bool printHeader = true, + public async Task ExportAsync(Stream stream, object value, bool printHeader = true, CsvConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) { using var writer = new CsvWriter(stream, value, printHeader, configuration); - return await writer.SaveAsAsync(progress, cancellationToken).ConfigureAwait(false); - } + var result = await writer.SaveAsAsync(progress, cancellationToken).ConfigureAwait(false); - #endregion -} \ No newline at end of file + return result.FirstOrDefault(); + } +} diff --git a/src/MiniExcel/MiniExcel.cs b/src/MiniExcel/MiniExcel.cs index a9074a53..c47eee7e 100644 --- a/src/MiniExcel/MiniExcel.cs +++ b/src/MiniExcel/MiniExcel.cs @@ -90,7 +90,7 @@ public static async Task SaveAsAsync(string path, object value, bool prin return type switch { ExcelType.XLSX => await ExcelExporter.ExportAsync(path, value, printHeader, sheetName, overwriteFile, configuration as NewOpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), - ExcelType.CSV => await CsvExporter.ExportAsync(path, value, printHeader, overwriteFile, configuration as Csv.CsvConfiguration, progress, cancellationToken).ConfigureAwait(false), + ExcelType.CSV => [await CsvExporter.ExportAsync(path, value, printHeader, overwriteFile, configuration as Csv.CsvConfiguration, progress, cancellationToken).ConfigureAwait(false)], _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } @@ -104,7 +104,7 @@ public static async Task SaveAsAsync(this Stream stream, object value, bo return type switch { ExcelType.XLSX => await ExcelExporter.ExportAsync(stream, value, printHeader, sheetName, configuration as NewOpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), - ExcelType.CSV => await CsvExporter.ExportAsync(stream, value, printHeader, configuration as Csv.CsvConfiguration, progress, cancellationToken).ConfigureAwait(false), + ExcelType.CSV => [await CsvExporter.ExportAsync(stream, value, printHeader, configuration as Csv.CsvConfiguration, progress, cancellationToken).ConfigureAwait(false)], _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } diff --git a/tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs b/tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs index 6ef7284a..72252161 100644 --- a/tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs +++ b/tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs @@ -76,8 +76,7 @@ public async Task Issue251() using var path = AutoDeletingPath.Create(ExcelType.Csv); var rowsWritten = await _csvExporter.ExportAsync(path.ToString(), reader); - Assert.Single(rowsWritten); - Assert.Equal(2, rowsWritten[0]); + Assert.Equal(2, rowsWritten); const string expected = """" @@ -241,8 +240,7 @@ public async Task Issue241() var path = file.ToString(); var rowsWritten = await _csvExporter.ExportAsync(path, value); - Assert.Single(rowsWritten); - Assert.Equal(2, rowsWritten[0]); + Assert.Equal(2, rowsWritten); var rows1 = await _csvImporter.QueryAsync(path, true).ToListAsync(); Assert.Equal(rows1[0].InDate, "01 04, 2021"); @@ -268,8 +266,7 @@ public async Task Issue243() }; var rowsWritten = await _csvExporter.ExportAsync(path.ToString(), value); - Assert.Single(rowsWritten); - Assert.Equal(2, rowsWritten[0]); + Assert.Equal(2, rowsWritten); var rows = await _csvImporter.QueryAsync(path.ToString()).ToListAsync(); diff --git a/tests/MiniExcel.Csv.Tests/IssueTests.cs b/tests/MiniExcel.Csv.Tests/IssueTests.cs index a8a8fe17..1b770905 100644 --- a/tests/MiniExcel.Csv.Tests/IssueTests.cs +++ b/tests/MiniExcel.Csv.Tests/IssueTests.cs @@ -724,8 +724,7 @@ public void Issue142() } ]; var rowsWritten = _csvExporter.Export(path, values); - Assert.Single(rowsWritten); - Assert.Equal(1, rowsWritten[0]); + Assert.Equal(1, rowsWritten); const string expected = """ diff --git a/tests/MiniExcel.Csv.Tests/MiniExcelCsvAsyncTests.cs b/tests/MiniExcel.Csv.Tests/MiniExcelCsvAsyncTests.cs index 9978c2c0..723bcc3c 100644 --- a/tests/MiniExcel.Csv.Tests/MiniExcelCsvAsyncTests.cs +++ b/tests/MiniExcel.Csv.Tests/MiniExcelCsvAsyncTests.cs @@ -44,7 +44,7 @@ public async Task SeperatorTest() ]; var rowsWritten = await _csvExporter.ExportAsync(path, values, configuration: new CsvConfiguration { Seperator = ';' }); - Assert.Equal(2, rowsWritten[0]); + Assert.Equal(2, rowsWritten); const string expected = """" @@ -99,7 +99,7 @@ public async Task SaveAsByDictionary() } ]; var rowsWritten = await _csvExporter.ExportAsync(path, values); - Assert.Equal(2, rowsWritten[0]); + Assert.Equal(2, rowsWritten); using var reader = new StreamReader(path); using var csv = new global::CsvHelper.CsvReader(reader, CultureInfo.InvariantCulture); @@ -140,7 +140,7 @@ public async Task SaveAsByDictionary() ]; var rowsWritten = await _csvExporter.ExportAsync(path, values); - Assert.Equal(2, rowsWritten[0]); + Assert.Equal(2, rowsWritten); using var reader = new StreamReader(path); using var csv = new global::CsvHelper.CsvReader(reader, CultureInfo.InvariantCulture); @@ -185,7 +185,7 @@ public async Task SaveAsByDataTableTest() table.Rows.Add("Hello World", -1234567890, false, new DateTime(2021, 1, 2)); var rowsWritten = await _csvExporter.ExportAsync(path2, table); - Assert.Equal(2, rowsWritten[0]); + Assert.Equal(2, rowsWritten); using var reader = new StreamReader(path2); using var csv = new global::CsvHelper.CsvReader(reader, CultureInfo.InvariantCulture); @@ -361,7 +361,7 @@ static async IAsyncEnumerable GetValues() #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously var rowsWritten = await _csvExporter.ExportAsync(path, GetValues()); - Assert.Equal(2, rowsWritten[0]); + Assert.Equal(2, rowsWritten); var results = _csvImporter.Query(path).ToList(); Assert.Equal(2, results.Count); @@ -388,8 +388,7 @@ public async Task ExportDataTableWithProgressTest() var progress = new SimpleProgress(); var rowCounts = await _csvExporter.ExportAsync(tempFilePath, dataTable, progress: progress); - Assert.Single(rowCounts); - Assert.Equal(3, rowCounts.First()); + Assert.Equal(3, rowCounts); //Confirm the progress report is correct var cellCount = dataTable.Columns.Count * dataTable.Rows.Count; diff --git a/tests/MiniExcel.Csv.Tests/MiniExcelCsvTests.cs b/tests/MiniExcel.Csv.Tests/MiniExcelCsvTests.cs index 9b152d83..a36af7ae 100644 --- a/tests/MiniExcel.Csv.Tests/MiniExcelCsvTests.cs +++ b/tests/MiniExcel.Csv.Tests/MiniExcelCsvTests.cs @@ -43,7 +43,7 @@ public void SeperatorTest() } ]; var rowsWritten = _csvExporter.Export(path, values, configuration: new CsvConfiguration { Seperator = ';' }); - Assert.Equal(2, rowsWritten[0]); + Assert.Equal(2, rowsWritten); const string expected = """" @@ -80,7 +80,7 @@ public void DontQuoteWhitespacesTest() } ]; var rowsWritten = _csvExporter.Export(path, values, configuration: new CsvConfiguration { QuoteWhitespaces = false }); - Assert.Equal(2, rowsWritten[0]); + Assert.Equal(2, rowsWritten); const string expected = """" @@ -145,7 +145,7 @@ public void QuoteSpecialCharacters() ]; var rowsWritten = _csvExporter.Export(path, values, configuration: new CsvConfiguration()); - Assert.Equal(1, rowsWritten[0]); + Assert.Equal(1, rowsWritten); const string expected = "a,b,c,d\r\n\"potato,banana\",\"text\ntest\",\"text\rpotato\",\"2021-01-01 00:00:00\"\r\n"; Assert.Equal(expected, File.ReadAllText(path)); @@ -190,7 +190,7 @@ public void SaveAsByDictionary() ]; var rowsWritten = _csvExporter.Export(path.ToString(), values); - Assert.Equal(2, rowsWritten[0]); + Assert.Equal(2, rowsWritten); using var reader = new StreamReader(path.ToString()); using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); @@ -277,7 +277,7 @@ public void SaveAsByDataTableTest() } var rowsWritten = _csvExporter.Export(path.ToString(), table); - Assert.Equal(2, rowsWritten[0]); + Assert.Equal(2, rowsWritten); using (var reader = new StreamReader(path.ToString())) using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) @@ -495,7 +495,7 @@ private static string MiniExcelGenerateCsv(string value) { IEnumerable records = [new { v1 = value, v2 = value }]; var rowsWritten = MiniExcel.Exporters.GetCsvExporter().Export(stream, records); - Assert.Equal(1, rowsWritten[0]); + Assert.Equal(1, rowsWritten); } var content = File.ReadAllText(path); From 92117c5d318737b022e4aff1267c99e57d0769cc Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 30 May 2026 16:40:35 +0200 Subject: [PATCH 11/14] Added documentation for CsvExporter --- src/MiniExcel.Csv/Api/CsvExporter.cs | 45 +++++++++++++++++-- .../Api/ProviderExtensions.cs | 2 - 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/MiniExcel.Csv/Api/CsvExporter.cs b/src/MiniExcel.Csv/Api/CsvExporter.cs index c4f76f49..fae61041 100644 --- a/src/MiniExcel.Csv/Api/CsvExporter.cs +++ b/src/MiniExcel.Csv/Api/CsvExporter.cs @@ -4,10 +4,17 @@ namespace MiniExcelLib.Csv; public partial class CsvExporter { internal CsvExporter() { } - - - #region Append / Export - + + /// + /// Appends data rows to an existing CSV file without overwriting existing content. + /// + /// The path to the CSV file to append to. + /// The data to append. Can be an object, , , or . + /// If true, when the file does not exist already the header row is added to the output. Default is true + /// Optional configuration settings (delimiters, encoding, etc.). + /// An optional to report progress as values are written. + /// A token to cancel the asynchronous operation. + /// The number of rows appended. [CreateSyncVersion] public async Task AppendAsync(string path, object value, bool printHeader = true, CsvConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) @@ -23,6 +30,15 @@ public async Task AppendAsync(string path, object value, bool printHeader = return await AppendAsync(stream, value, configuration, progress, cancellationToken).ConfigureAwait(false); } + /// + /// Appends data rows to an existing CSV stream without overwriting existing content. + /// + /// The stream containing the CSV file to append to. The stream will be positioned at the end before appending. + /// The data to append. Can be an object, , , or . + /// Optional configuration settings (delimiters, encoding, etc.). + /// An optional to report progress as values are written. + /// A token to cancel the asynchronous operation. + /// The number of rows appended. [CreateSyncVersion] public async Task AppendAsync(Stream stream, object value, CsvConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) { @@ -34,6 +50,17 @@ public async Task AppendAsync(Stream stream, object value, CsvConfiguration return await writer.InsertAsync(false, progress, cancellationToken).ConfigureAwait(false); } + /// + /// Exports data to a CSV file. + /// + /// The path to write CSV data to. + /// The data to export. Can be an object, , , or . + /// If true, the first row will contain column headers derived from property names or DataTable column names. Default is true. + /// If true, when a file at the specified path already exists it will be overwritten, otherwise an will be thrown. Default is false. + /// Optional configuration settings (delimiters, encoding, etc.). + /// An optional to report progress as values are written. + /// A token to cancel the asynchronous operation. + /// The number of rows written. [CreateSyncVersion] public async Task ExportAsync(string path, object value, bool printHeader = true, bool overwriteFile = false, CsvConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) @@ -44,6 +71,16 @@ public async Task ExportAsync(string path, object value, bool printHeader = return await ExportAsync(stream, value, printHeader, configuration, progress, cancellationToken).ConfigureAwait(false); } + /// + /// Exports data to a CSV stream. + /// + /// The stream to write CSV data to. Existing content will be overwritten. + /// The data to export. Can be an object, , , or . + /// If true, the first row will contain column headers derived from property names or DataTable column names. Default is true. + /// Optional configuration settings (delimiters, encoding, etc.). + /// An optional to report progress as values are written. + /// A token to cancel the asynchronous operation. + /// The number of rows written. [CreateSyncVersion] public async Task ExportAsync(Stream stream, object value, bool printHeader = true, CsvConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) diff --git a/src/MiniExcel.OpenXml/Api/ProviderExtensions.cs b/src/MiniExcel.OpenXml/Api/ProviderExtensions.cs index da617808..7e62c6b7 100644 --- a/src/MiniExcel.OpenXml/Api/ProviderExtensions.cs +++ b/src/MiniExcel.OpenXml/Api/ProviderExtensions.cs @@ -1,5 +1,3 @@ - - // ReSharper disable once CheckNamespace namespace MiniExcelLib.OpenXml; From de82065d98608cee05427d70734bde5b8c072da6 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 30 May 2026 17:55:11 +0200 Subject: [PATCH 12/14] Changes to CsvImporter API methods Added `leaveOpen` parameter to `GetDataReader` API methods and renamed parameter `useHeaderRow` to `hasHeaderRow` for clarifying its usage. --- V2-Upgrade-Notes.md | 2 +- src/MiniExcel.Csv/Api/CsvImporter.cs | 51 ++++++++++--------- src/MiniExcel/MiniExcelConverter.cs | 2 +- tests/MiniExcel.Csv.Tests/IssueTests.cs | 4 +- .../MiniExcelCsvAsyncTests.cs | 4 +- .../MiniExcel.Csv.Tests/MiniExcelCsvTests.cs | 4 +- 6 files changed, 34 insertions(+), 33 deletions(-) diff --git a/V2-Upgrade-Notes.md b/V2-Upgrade-Notes.md index 34712fe1..dfea4e93 100644 --- a/V2-Upgrade-Notes.md +++ b/V2-Upgrade-Notes.md @@ -13,6 +13,6 @@ - MiniExcel now fully supports asynchronous streaming the queries, so the return type for `OpenXmlImporter.QueryAsync` is `IAsyncEnumerable` instead of `Task>` - When applying a template, unlike version 1.x, the flag for overwriting an already existing file must be provided explicitly. -- `leaveOpen` parameter has been added to `GetDataReader` and `GetDataReaderAsync` to configure whether the underlying stream must be disposed alongside the data reader. +- `leaveOpen` parameter has been added to `GetDataReader` and `GetDataReaderAsync` in both `OpenXmlImporter` and `CsvImporter` to configure whether the underlying stream must be disposed alongside the data reader. - `useHeaderRow` parameter in multiple `OpenXmlImporter` methods has been renamed to `hasHeaderRow` for making its usage clearer. - `CsvExporter.Export` API methods, not being required to return the same type of `OpenXmlExporter.Export`, now return `int` instead of `int[]`. \ No newline at end of file diff --git a/src/MiniExcel.Csv/Api/CsvImporter.cs b/src/MiniExcel.Csv/Api/CsvImporter.cs index 8d10290c..cd5a8245 100644 --- a/src/MiniExcel.Csv/Api/CsvImporter.cs +++ b/src/MiniExcel.Csv/Api/CsvImporter.cs @@ -36,22 +36,22 @@ public async IAsyncEnumerable QueryAsync(Stream stream, bool treatHeaderAs } [CreateSyncVersion] - public async IAsyncEnumerable QueryAsync(string path, bool useHeaderRow = false, + public async IAsyncEnumerable QueryAsync(string path, bool hasHeaderRow = false, CsvConfiguration? configuration = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - await foreach (var item in QueryAsync(stream, useHeaderRow, configuration, cancellationToken).ConfigureAwait(false)) + await foreach (var item in QueryAsync(stream, hasHeaderRow, configuration, cancellationToken).ConfigureAwait(false)) yield return item; } [CreateSyncVersion] - public async IAsyncEnumerable QueryAsync(Stream stream, bool useHeaderRow = false, + public async IAsyncEnumerable QueryAsync(Stream stream, bool hasHeaderRow = false, CsvConfiguration? configuration = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { using var excelReader = new CsvReader(stream, configuration); - await foreach (var item in excelReader.QueryAsync(useHeaderRow, null, "A1", cancellationToken).ConfigureAwait(false)) + await foreach (var item in excelReader.QueryAsync(hasHeaderRow, null, "A1", cancellationToken).ConfigureAwait(false)) yield return item; //yield return item.ToDynamicObject(); } @@ -64,20 +64,20 @@ public async IAsyncEnumerable QueryAsync(Stream stream, bool useHeaderR /// QueryAsDataTable is not recommended, because it'll load all data into memory. /// [CreateSyncVersion] - public async Task QueryAsDataTableAsync(string path, bool useHeaderRow = true, + public async Task QueryAsDataTableAsync(string path, bool hasHeaderRow = true, CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - return await QueryAsDataTableAsync(stream, useHeaderRow, configuration, cancellationToken).ConfigureAwait(false); + return await QueryAsDataTableAsync(stream, hasHeaderRow, configuration, cancellationToken).ConfigureAwait(false); } /// /// QueryAsDataTable is not recommended, because it'll load all data into memory. /// [CreateSyncVersion] - public async Task QueryAsDataTableAsync(Stream stream, bool useHeaderRow = true, + public async Task QueryAsDataTableAsync(Stream stream, bool hasHeaderRow = true, CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) { var dt = new DataTable(); @@ -94,7 +94,7 @@ public async Task QueryAsDataTableAsync(Stream stream, bool useHeader { cancellationToken.ThrowIfCancellationRequested(); - var columnName = useHeaderRow ? entry.Value?.ToString() : entry.Key; + var columnName = hasHeaderRow ? entry.Value?.ToString() : entry.Key; if (!string.IsNullOrWhiteSpace(columnName)) // avoid #298 : Column '' does not belong to table { var column = new DataColumn(columnName, typeof(object)) { Caption = columnName }; @@ -105,7 +105,7 @@ public async Task QueryAsDataTableAsync(Stream stream, bool useHeader dt.BeginLoadData(); first = false; - if (useHeaderRow) + if (hasHeaderRow) { continue; } @@ -129,19 +129,19 @@ public async Task QueryAsDataTableAsync(Stream stream, bool useHeader #region Info [CreateSyncVersion] - public async Task> GetColumnNamesAsync(string path, bool useHeaderRow = false, + public async Task> GetColumnNamesAsync(string path, bool hasHeaderRow = false, CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - return await GetColumnNamesAsync(stream, useHeaderRow, configuration, cancellationToken).ConfigureAwait(false); + return await GetColumnNamesAsync(stream, hasHeaderRow, configuration, cancellationToken).ConfigureAwait(false); } [CreateSyncVersion] - public async Task> GetColumnNamesAsync(Stream stream, bool useHeaderRow = false, + public async Task> GetColumnNamesAsync(Stream stream, bool hasHeaderRow = false, CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) { - var enumerator = QueryAsync(stream, useHeaderRow, configuration, cancellationToken).GetAsyncEnumerator(cancellationToken); + var enumerator = QueryAsync(stream, hasHeaderRow, configuration, cancellationToken).GetAsyncEnumerator(cancellationToken); await using var disposableEnumerator = enumerator.ConfigureAwait(false); if (await enumerator.MoveNextAsync().ConfigureAwait(false)) @@ -161,11 +161,12 @@ public async Task> GetColumnNamesAsync(Stream stream, bool u /// Asynchronous reads are not allowed when creating the data reader from this overload and will result in an exception. /// public MiniExcelDataReader GetDataReader(string path, bool useHeaderRow = false, CsvConfiguration? configuration = null) + public MiniExcelDataReader GetDataReader(string path, bool hasHeaderRow = false, CsvConfiguration? configuration = null) { var stream = FileHelper.OpenSharedRead(path); - var values = Query(stream, useHeaderRow, configuration).Cast>(); + var values = Query(stream, hasHeaderRow, configuration).Cast>(); - return MiniExcelDataReader.Create(stream, values); + return MiniExcelDataReader.Create(stream, values, leaveOpen: false); } /// @@ -174,34 +175,34 @@ public MiniExcelDataReader GetDataReader(string path, bool useHeaderRow = false, /// /// Asynchronous reads are not allowed when creating the data reader from this overload and will result in an exception. /// - public MiniExcelDataReader GetDataReader(Stream stream, bool useHeaderRow = false, CsvConfiguration ? configuration = null) + public MiniExcelDataReader GetDataReader(Stream stream, bool hasHeaderRow = false, CsvConfiguration ? configuration = null, bool leaveOpen = false) { - var values = Query(stream, useHeaderRow, configuration).Cast>(); - return MiniExcelDataReader.Create(stream, values); + var values = Query(stream, hasHeaderRow, configuration).Cast>(); + return MiniExcelDataReader.Create(stream, values, leaveOpen); } /// /// Gets an for the Csv document at the specific path. /// When created from this overload, the resulting data reader is supposed to be advanced asynchronously. /// - public async Task GetAsyncDataReader(string path, bool useHeaderRow = false, + public async Task GetAsyncDataReader(string path, bool hasHeaderRow = false, CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); - var values = QueryAsync(stream, useHeaderRow, configuration, cancellationToken); + var values = QueryAsync(stream, hasHeaderRow, configuration, cancellationToken); - return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken)).ConfigureAwait(false); + return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken), leaveOpen: false).ConfigureAwait(false); } /// /// Gets an for the Csv document at the specific path. /// When created from this overload, the resulting data reader is supposed to be advanced asynchronously. /// - public async Task GetAsyncDataReader(Stream stream, bool useHeaderRow = false, - CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) + public async Task GetAsyncDataReader(Stream stream, bool hasHeaderRow = false, + CsvConfiguration? configuration = null, bool leaveOpen = false, CancellationToken cancellationToken = default) { - var values = QueryAsync(stream, useHeaderRow, configuration, cancellationToken); - return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken)).ConfigureAwait(false); + var values = QueryAsync(stream, hasHeaderRow, configuration, cancellationToken); + return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken), leaveOpen).ConfigureAwait(false); } #endregion diff --git a/src/MiniExcel/MiniExcelConverter.cs b/src/MiniExcel/MiniExcelConverter.cs index 76613ae5..8e860a17 100644 --- a/src/MiniExcel/MiniExcelConverter.cs +++ b/src/MiniExcel/MiniExcelConverter.cs @@ -13,7 +13,7 @@ public static async Task ConvertCsvToXlsxAsync(Stream csv, Stream xlsx, bool csv { var value = MiniExcel.Importers .GetCsvImporter() - .QueryAsync(csv, useHeaderRow: csvHasHeader, cancellationToken: cancellationToken); + .QueryAsync(csv, hasHeaderRow: csvHasHeader, cancellationToken: cancellationToken); await MiniExcel.Exporters .GetOpenXmlExporter() diff --git a/tests/MiniExcel.Csv.Tests/IssueTests.cs b/tests/MiniExcel.Csv.Tests/IssueTests.cs index 1b770905..1a43a316 100644 --- a/tests/MiniExcel.Csv.Tests/IssueTests.cs +++ b/tests/MiniExcel.Csv.Tests/IssueTests.cs @@ -464,7 +464,7 @@ public void TestIssue293() var path = PathHelper.GetFile("/csv/Test5x2.csv"); using var tempPath = AutoDeletingPath.Create(); using var csv = File.OpenRead(path); - var value = _csvImporter.Query(csv, useHeaderRow: false); + var value = _csvImporter.Query(csv, hasHeaderRow: false); _openXmlExporter.Export(tempPath.ToString(), value, printHeader: false); } @@ -616,7 +616,7 @@ public void Issue89() writer.Write(text); writer.Flush(); stream.Position = 0; - var rows = _csvImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _csvImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Equal(nameof(Issue89Model.WorkState.OnDuty), rows[0].State); Assert.Equal(nameof(Issue89Model.WorkState.Fired), rows[1].State); diff --git a/tests/MiniExcel.Csv.Tests/MiniExcelCsvAsyncTests.cs b/tests/MiniExcel.Csv.Tests/MiniExcelCsvAsyncTests.cs index 723bcc3c..0c5b7b57 100644 --- a/tests/MiniExcel.Csv.Tests/MiniExcelCsvAsyncTests.cs +++ b/tests/MiniExcel.Csv.Tests/MiniExcelCsvAsyncTests.cs @@ -252,7 +252,7 @@ await _csvExporter.ExportAsync(path, new[] await using (var stream = File.OpenRead(path)) { - var rows = _csvImporter.QueryAsync(stream, useHeaderRow: true).ToBlockingEnumerable().ToList(); + var rows = _csvImporter.QueryAsync(stream, hasHeaderRow: true).ToBlockingEnumerable().ToList(); Assert.Equal("A1", rows[0].C1); Assert.Equal("B1", rows[0].C2); Assert.Equal("A2", rows[1].C1); @@ -260,7 +260,7 @@ await _csvExporter.ExportAsync(path, new[] } { - var rows = _csvImporter.QueryAsync(path, useHeaderRow: true).ToBlockingEnumerable().ToList(); + var rows = _csvImporter.QueryAsync(path, hasHeaderRow: true).ToBlockingEnumerable().ToList(); Assert.Equal("A1", rows[0].C1); Assert.Equal("B1", rows[0].C2); Assert.Equal("A2", rows[1].C1); diff --git a/tests/MiniExcel.Csv.Tests/MiniExcelCsvTests.cs b/tests/MiniExcel.Csv.Tests/MiniExcelCsvTests.cs index a36af7ae..c9467d58 100644 --- a/tests/MiniExcel.Csv.Tests/MiniExcelCsvTests.cs +++ b/tests/MiniExcel.Csv.Tests/MiniExcelCsvTests.cs @@ -352,7 +352,7 @@ public void Create2x2_Test() using (var stream = File.OpenRead(path)) { - var rows = _csvImporter.Query(stream, useHeaderRow: true).ToList(); + var rows = _csvImporter.Query(stream, hasHeaderRow: true).ToList(); Assert.Equal("A1", rows[0].C1); Assert.Equal("B1", rows[0].C2); Assert.Equal("A2", rows[1].C1); @@ -360,7 +360,7 @@ public void Create2x2_Test() } { - var rows = _csvImporter.Query(path, useHeaderRow: true).ToList(); + var rows = _csvImporter.Query(path, hasHeaderRow: true).ToList(); Assert.Equal("A1", rows[0].C1); Assert.Equal("B1", rows[0].C2); Assert.Equal("A2", rows[1].C1); From d8e67dd13fac6f9e5080c5f4fec3dca802e49bcb Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 30 May 2026 18:02:00 +0200 Subject: [PATCH 13/14] Added documentation to CsvImporter class --- src/MiniExcel.Csv/Api/CsvImporter.cs | 124 +++++++++++++++++++++++---- 1 file changed, 108 insertions(+), 16 deletions(-) diff --git a/src/MiniExcel.Csv/Api/CsvImporter.cs b/src/MiniExcel.Csv/Api/CsvImporter.cs index cd5a8245..8606be32 100644 --- a/src/MiniExcel.Csv/Api/CsvImporter.cs +++ b/src/MiniExcel.Csv/Api/CsvImporter.cs @@ -10,6 +10,14 @@ internal CsvImporter() { } #region Query + /// + /// Queries a CSV document using a strongly-typed class model. + /// + /// The class type to map each row to. Must have a parameterless constructor. + /// The path to the CSV document. + /// If true, the first row is treated as data. If false (default), the first row is used as headers. + /// Optional configuration settings (delimiters, encoding, etc.). + /// A token to cancel the asynchronous operation. [CreateSyncVersion] public async IAsyncEnumerable QueryAsync(string path, bool treatHeaderAsData = false, CsvConfiguration? configuration = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) @@ -25,6 +33,14 @@ public async IAsyncEnumerable QueryAsync(string path, bool treatHeaderAsDa yield return item; } + /// + /// Queries a CSV document using a strongly-typed class model. + /// + /// The class type to map each row to. Must have a parameterless constructor. + /// The stream containing the CSV data. + /// If true, the first row is treated as data. If false (default), the first row is used as headers. + /// Optional configuration settings (delimiters, encoding, etc.). + /// A token to cancel the asynchronous operation. [CreateSyncVersion] public async IAsyncEnumerable QueryAsync(Stream stream, bool treatHeaderAsData = false, CsvConfiguration? configuration = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) @@ -35,6 +51,16 @@ public async IAsyncEnumerable QueryAsync(Stream stream, bool treatHeaderAs yield return item; } + /// + /// Queries a CSV document and returns dynamic objects representing each row. + /// + /// The path to the CSV document. + /// If true, the first row is used as column headers for the dynamic object properties. Default is false. + /// Optional configuration settings (delimiters, encoding, etc.). + /// A token to cancel the asynchronous operation. + /// + /// When is true, column names from the first row become dynamic property names, otherwise they will be assigned alphabetically (A, B, C, etc.). + /// [CreateSyncVersion] public async IAsyncEnumerable QueryAsync(string path, bool hasHeaderRow = false, CsvConfiguration? configuration = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) @@ -46,6 +72,16 @@ public async IAsyncEnumerable QueryAsync(string path, bool hasHeaderRow yield return item; } + /// + /// Queries a CSV document and returns dynamic objects representing each row. + /// + /// The stream containing the CSV data. + /// If true, the first row is used as column headers for the dynamic object properties. Default is false. + /// Optional configuration settings (delimiters, encoding, etc.). + /// A token to cancel the asynchronous operation. + /// + /// When is true, column names from the first row become dynamic property names, otherwise they will be assigned alphabetically (A, B, C, etc.). + /// [CreateSyncVersion] public async IAsyncEnumerable QueryAsync(Stream stream, bool hasHeaderRow = false, CsvConfiguration? configuration = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) @@ -53,7 +89,6 @@ public async IAsyncEnumerable QueryAsync(Stream stream, bool hasHeaderR using var excelReader = new CsvReader(stream, configuration); await foreach (var item in excelReader.QueryAsync(hasHeaderRow, null, "A1", cancellationToken).ConfigureAwait(false)) yield return item; - //yield return item.ToDynamicObject(); } #endregion @@ -61,8 +96,16 @@ public async IAsyncEnumerable QueryAsync(Stream stream, bool hasHeaderR #region Query As DataTable /// - /// QueryAsDataTable is not recommended, because it'll load all data into memory. + /// Queries a CSV file and returns the results as a . /// + /// The path to the CSV document. + /// If true, the first row is used as column headers. + /// Optional configuration settings. + /// A token to cancel the asynchronous operation. + /// + /// Empty column names are skipped. + /// This method loads the entire file into memory, so its usage is recommended only for datasets of moderate size. + /// [CreateSyncVersion] public async Task QueryAsDataTableAsync(string path, bool hasHeaderRow = true, CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) @@ -74,8 +117,16 @@ public async Task QueryAsDataTableAsync(string path, bool hasHeaderRo } /// - /// QueryAsDataTable is not recommended, because it'll load all data into memory. + /// Queries a CSV stream and returns the results as a . /// + /// The stream containing the CSV data. + /// If true, the first row is used as column headers. + /// Optional configuration settings. + /// A token to cancel the asynchronous operation. + /// + /// Empty column names are skipped. + /// This method loads the entire file into memory, so its usage is recommended only for datasets of moderate size. + /// [CreateSyncVersion] public async Task QueryAsDataTableAsync(Stream stream, bool hasHeaderRow = true, CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) @@ -128,6 +179,14 @@ public async Task QueryAsDataTableAsync(Stream stream, bool hasHeader #region Info + /// + /// Retrieves the column names from the first row (header row) of a CSV document. + /// + /// The path to the CSV document. + /// If true, the first row values are used as column names. If false, column letters (A, B, C, etc.) are used. Default is false. + /// Optional configuration settings (delimiters, encoding, etc.). + /// A token to cancel the asynchronous operation. + /// A collection of column names from the specified location, or an empty collection if the sheet is empty. [CreateSyncVersion] public async Task> GetColumnNamesAsync(string path, bool hasHeaderRow = false, CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) @@ -137,6 +196,14 @@ public async Task> GetColumnNamesAsync(string path, bool has return await GetColumnNamesAsync(stream, hasHeaderRow, configuration, cancellationToken).ConfigureAwait(false); } + /// + /// Retrieves the column names from the first row (header row) of a CSV document. + /// + /// The stream containing the CSV data. + /// If true, the first row values are used as column names. If false, column letters (A, B, C, etc.) are used. Default is false. + /// Optional configuration settings (delimiters, encoding, etc.). + /// A token to cancel the asynchronous operation. + /// A collection of column names from the specified location, or an empty collection if the sheet is empty. [CreateSyncVersion] public async Task> GetColumnNamesAsync(Stream stream, bool hasHeaderRow = false, CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) @@ -155,12 +222,15 @@ public async Task> GetColumnNamesAsync(Stream stream, bool h #region DataReader /// - /// Gets an for the Csv document at the specified path. + /// Gets an for the CSV document provided for synchronous reading. /// - /// - /// Asynchronous reads are not allowed when creating the data reader from this overload and will result in an exception. - /// - public MiniExcelDataReader GetDataReader(string path, bool useHeaderRow = false, CsvConfiguration? configuration = null) + /// The path to the CSV document. + /// If true, the first row is used as column headers. Default is false. + /// Optional configuration settings (delimiters, encoding, etc.). /// + /// The returned implements and supports its standard reading patterns. + /// The data reader returned by this method is designed to perform synchronous, blocking reads, and will throw if an asynchronous operation is called from it. + /// For asynchronous reading scenarios, use instead. + /// public MiniExcelDataReader GetDataReader(string path, bool hasHeaderRow = false, CsvConfiguration? configuration = null) { var stream = FileHelper.OpenSharedRead(path); @@ -170,11 +240,17 @@ public MiniExcelDataReader GetDataReader(string path, bool hasHeaderRow = false, } /// - /// Gets an for the Csv document from an underlying stream. + /// Gets an for the CSV document provided for synchronous reading. /// - /// - /// Asynchronous reads are not allowed when creating the data reader from this overload and will result in an exception. - /// + /// The stream containing the CSV data. + /// If true, the first row is used as column headers. Default is false. + /// Optional configuration settings (delimiters, encoding, etc.). + /// True to leave the stream open after the data reader is disposed, otherwise false. + /// + /// The returned implements and supports its standard reading patterns. + /// The data reader returned by this method is designed to perform synchronous, blocking reads, and will throw if an asynchronous operation is called from it. + /// For asynchronous reading scenarios, use instead. + /// public MiniExcelDataReader GetDataReader(Stream stream, bool hasHeaderRow = false, CsvConfiguration ? configuration = null, bool leaveOpen = false) { var values = Query(stream, hasHeaderRow, configuration).Cast>(); @@ -182,9 +258,16 @@ public MiniExcelDataReader GetDataReader(Stream stream, bool hasHeaderRow = fals } /// - /// Gets an for the Csv document at the specific path. - /// When created from this overload, the resulting data reader is supposed to be advanced asynchronously. + /// Gets an for the CSV document provided for synchronous reading. /// + /// The path to the CSV document. + /// If true, the first row is used as column headers. Default is false. + /// Optional configuration settings (delimiters, encoding, etc.). /// A token to cancel the asynchronous operation. + /// + /// The returned implements and supports its standard reading patterns. + /// The data reader returned by this method is designed to supports asynchronous reads, but will not throw an exception if a synchronous operation is performed. + /// Still, it's advised to use for synchronous reads instead. + /// public async Task GetAsyncDataReader(string path, bool hasHeaderRow = false, CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) { @@ -195,9 +278,18 @@ public async Task GetAsyncDataReader(string path, bool hasH } /// - /// Gets an for the Csv document at the specific path. - /// When created from this overload, the resulting data reader is supposed to be advanced asynchronously. + /// Gets an for the CSV document provided for synchronous reading. /// + /// The stream containing the CSV data. + /// If true, the first row is used as column headers. Default is false. + /// Optional configuration settings (delimiters, encoding, etc.). + /// True to leave the stream open after the data reader is disposed, otherwise false. + /// A token to cancel the asynchronous operation. + /// + /// The returned implements and supports its standard reading patterns. + /// The data reader returned by this method is designed to supports asynchronous reads, but will not throw an exception if a synchronous operation is performed. + /// Still, it's advised to use for synchronous reads instead. + /// public async Task GetAsyncDataReader(Stream stream, bool hasHeaderRow = false, CsvConfiguration? configuration = null, bool leaveOpen = false, CancellationToken cancellationToken = default) { From 343ca405a3bcb0dd789b8cb47f851bec06282fc8 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 30 May 2026 20:31:09 +0200 Subject: [PATCH 14/14] Fixed new leaveOpen parameter oversight by adding it to all methods that take a stream as an input --- V2-Upgrade-Notes.md | 2 +- .../Abstractions/IMiniExcelReader.cs | 6 +- src/MiniExcel.Core/MiniExcelDataReader.cs | 7 +- src/MiniExcel.Csv/Api/CsvImporter.cs | 37 ++++---- src/MiniExcel.Csv/CsvReader.cs | 15 ++-- src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs | 89 ++++++++++--------- .../FluentMapping/Api/MappingImporter.cs | 12 +-- .../FluentMapping/MappingReader.cs | 11 +-- src/MiniExcel.OpenXml/OpenXmlReader.cs | 26 +++--- .../Picture/OpenXmlPictureImplement.cs | 2 +- .../Utils/SharedStringsDiskCache.cs | 69 +++++++++----- src/MiniExcel/MiniExcel.cs | 26 +++--- 12 files changed, 172 insertions(+), 130 deletions(-) diff --git a/V2-Upgrade-Notes.md b/V2-Upgrade-Notes.md index dfea4e93..53bdff76 100644 --- a/V2-Upgrade-Notes.md +++ b/V2-Upgrade-Notes.md @@ -13,6 +13,6 @@ - MiniExcel now fully supports asynchronous streaming the queries, so the return type for `OpenXmlImporter.QueryAsync` is `IAsyncEnumerable` instead of `Task>` - When applying a template, unlike version 1.x, the flag for overwriting an already existing file must be provided explicitly. -- `leaveOpen` parameter has been added to `GetDataReader` and `GetDataReaderAsync` in both `OpenXmlImporter` and `CsvImporter` to configure whether the underlying stream must be disposed alongside the data reader. +- `leaveOpen` parameter has been added to most methods that take a stream as input in both `OpenXmlImporter` and `CsvImporter` to configure whether the stream must be disposed after the operation performed is completed. - `useHeaderRow` parameter in multiple `OpenXmlImporter` methods has been renamed to `hasHeaderRow` for making its usage clearer. - `CsvExporter.Export` API methods, not being required to return the same type of `OpenXmlExporter.Export`, now return `int` instead of `int[]`. \ No newline at end of file diff --git a/src/MiniExcel.Core/Abstractions/IMiniExcelReader.cs b/src/MiniExcel.Core/Abstractions/IMiniExcelReader.cs index 3760c139..f2a58c86 100644 --- a/src/MiniExcel.Core/Abstractions/IMiniExcelReader.cs +++ b/src/MiniExcel.Core/Abstractions/IMiniExcelReader.cs @@ -3,19 +3,19 @@ public partial interface IMiniExcelReader : IDisposable { [CreateSyncVersion] - IAsyncEnumerable> QueryAsync(bool useHeaderRow, string? sheetName, string startCell, CancellationToken cancellationToken = default); + IAsyncEnumerable> QueryAsync(bool hasHeaderRow, string? sheetName, string startCell, CancellationToken cancellationToken = default); [CreateSyncVersion] IAsyncEnumerable QueryAsync(string? sheetName, string startCell, bool mapHeaderAsData, CancellationToken cancellationToken = default) where T : class, new(); [CreateSyncVersion] - IAsyncEnumerable> QueryRangeAsync(bool useHeaderRow, string? sheetName, string startCell, string endCell, CancellationToken cancellationToken = default); + IAsyncEnumerable> QueryRangeAsync(bool hasHeaderRow, string? sheetName, string startCell, string endCell, CancellationToken cancellationToken = default); [CreateSyncVersion] IAsyncEnumerable QueryRangeAsync(string? sheetName, string startCell, string endCell, bool treatHeaderAsData, CancellationToken cancellationToken = default) where T : class, new(); [CreateSyncVersion] - IAsyncEnumerable> QueryRangeAsync(bool useHeaderRow, string? sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, CancellationToken cancellationToken = default); + IAsyncEnumerable> QueryRangeAsync(bool hasHeaderRow, string? sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, CancellationToken cancellationToken = default); [CreateSyncVersion] IAsyncEnumerable QueryRangeAsync(string? sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, bool treatHeaderAsData, CancellationToken cancellationToken = default) where T : class, new(); diff --git a/src/MiniExcel.Core/MiniExcelDataReader.cs b/src/MiniExcel.Core/MiniExcelDataReader.cs index 42b65078..1128a815 100644 --- a/src/MiniExcel.Core/MiniExcelDataReader.cs +++ b/src/MiniExcel.Core/MiniExcelDataReader.cs @@ -39,7 +39,7 @@ public static MiniExcelDataReader Create(Stream? stream, IEnumerable CreateAsync(Stream? stream, IAsync var reader = new MiniExcelDataReader(stream, values, leaveOpen); if (await reader._asyncSource!.MoveNextAsync().ConfigureAwait(false)) { - reader._columns = reader._asyncSource.Current.Keys.ToList(); + reader._columns = reader._asyncSource.Current?.Keys.ToList() ?? []; reader.FieldCount = reader._columns.Count; } else @@ -249,6 +249,9 @@ public string GetName(int i) public int GetOrdinal(string name) { + if (name is null) + throw new ArgumentNullException(nameof(name)); + if (_ordinals.TryGetValue(name, out var ordinal)) return ordinal; diff --git a/src/MiniExcel.Csv/Api/CsvImporter.cs b/src/MiniExcel.Csv/Api/CsvImporter.cs index 8606be32..e83603d6 100644 --- a/src/MiniExcel.Csv/Api/CsvImporter.cs +++ b/src/MiniExcel.Csv/Api/CsvImporter.cs @@ -26,9 +26,7 @@ public async IAsyncEnumerable QueryAsync(string path, bool treatHeaderAsDa var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - var query = QueryAsync(stream, treatHeaderAsData, configuration, cancellationToken); - - //Foreach yield return twice reason : https://stackoverflow.com/questions/66791982/ienumerable-extract-code-lazy-loading-show-stream-was-not-readable + var query = QueryAsync(stream, treatHeaderAsData, configuration, leaveOpen: false, cancellationToken); await foreach (var item in query.ConfigureAwait(false)) yield return item; } @@ -40,13 +38,14 @@ public async IAsyncEnumerable QueryAsync(string path, bool treatHeaderAsDa /// The stream containing the CSV data. /// If true, the first row is treated as data. If false (default), the first row is used as headers. /// Optional configuration settings (delimiters, encoding, etc.). + /// True to leave the stream open after the query is completed, otherwise false. /// A token to cancel the asynchronous operation. [CreateSyncVersion] public async IAsyncEnumerable QueryAsync(Stream stream, bool treatHeaderAsData = false, - CsvConfiguration? configuration = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + CsvConfiguration? configuration = null, bool leaveOpen = false, [EnumeratorCancellation] CancellationToken cancellationToken = default) where T : class, new() { - using var csv = new CsvReader(stream, configuration); + using var csv = new CsvReader(stream, configuration, leaveOpen); await foreach (var item in csv.QueryAsync(null, "A1", treatHeaderAsData, cancellationToken).ConfigureAwait(false)) yield return item; } @@ -68,7 +67,7 @@ public async IAsyncEnumerable QueryAsync(string path, bool hasHeaderRow var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - await foreach (var item in QueryAsync(stream, hasHeaderRow, configuration, cancellationToken).ConfigureAwait(false)) + await foreach (var item in QueryAsync(stream, hasHeaderRow, configuration, leaveOpen: false, cancellationToken).ConfigureAwait(false)) yield return item; } @@ -78,15 +77,16 @@ public async IAsyncEnumerable QueryAsync(string path, bool hasHeaderRow /// The stream containing the CSV data. /// If true, the first row is used as column headers for the dynamic object properties. Default is false. /// Optional configuration settings (delimiters, encoding, etc.). + /// True to leave the stream open after the query is completed, otherwise false. /// A token to cancel the asynchronous operation. /// /// When is true, column names from the first row become dynamic property names, otherwise they will be assigned alphabetically (A, B, C, etc.). /// [CreateSyncVersion] public async IAsyncEnumerable QueryAsync(Stream stream, bool hasHeaderRow = false, - CsvConfiguration? configuration = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + CsvConfiguration? configuration = null, bool leaveOpen = false, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - using var excelReader = new CsvReader(stream, configuration); + using var excelReader = new CsvReader(stream, configuration, leaveOpen); await foreach (var item in excelReader.QueryAsync(hasHeaderRow, null, "A1", cancellationToken).ConfigureAwait(false)) yield return item; } @@ -113,7 +113,7 @@ public async Task QueryAsDataTableAsync(string path, bool hasHeaderRo var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - return await QueryAsDataTableAsync(stream, hasHeaderRow, configuration, cancellationToken).ConfigureAwait(false); + return await QueryAsDataTableAsync(stream, hasHeaderRow, configuration, leaveOpen: false, cancellationToken).ConfigureAwait(false); } /// @@ -122,6 +122,7 @@ public async Task QueryAsDataTableAsync(string path, bool hasHeaderRo /// The stream containing the CSV data. /// If true, the first row is used as column headers. /// Optional configuration settings. + /// True to leave the stream open after the query is completed, otherwise false. /// A token to cancel the asynchronous operation. /// /// Empty column names are skipped. @@ -129,11 +130,11 @@ public async Task QueryAsDataTableAsync(string path, bool hasHeaderRo /// [CreateSyncVersion] public async Task QueryAsDataTableAsync(Stream stream, bool hasHeaderRow = true, - CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) + CsvConfiguration? configuration = null, bool leaveOpen = false, CancellationToken cancellationToken = default) { var dt = new DataTable(); var first = true; - using var reader = new CsvReader(stream, configuration); + using var reader = new CsvReader(stream, configuration, leaveOpen); var rows = reader.QueryAsync(false, null, "A1", cancellationToken); var columnDict = new Dictionary(); @@ -208,7 +209,7 @@ public async Task> GetColumnNamesAsync(string path, bool has public async Task> GetColumnNamesAsync(Stream stream, bool hasHeaderRow = false, CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) { - var enumerator = QueryAsync(stream, hasHeaderRow, configuration, cancellationToken).GetAsyncEnumerator(cancellationToken); + var enumerator = QueryAsync(stream, hasHeaderRow, configuration, leaveOpen: false, cancellationToken).GetAsyncEnumerator(cancellationToken); await using var disposableEnumerator = enumerator.ConfigureAwait(false); if (await enumerator.MoveNextAsync().ConfigureAwait(false)) @@ -234,9 +235,9 @@ public async Task> GetColumnNamesAsync(Stream stream, bool h public MiniExcelDataReader GetDataReader(string path, bool hasHeaderRow = false, CsvConfiguration? configuration = null) { var stream = FileHelper.OpenSharedRead(path); - var values = Query(stream, hasHeaderRow, configuration).Cast>(); + var values = Query(stream, hasHeaderRow, configuration, leaveOpen: false).Cast>(); - return MiniExcelDataReader.Create(stream, values, leaveOpen: false); + return MiniExcelDataReader.Create(stream, values); } /// @@ -253,7 +254,7 @@ public MiniExcelDataReader GetDataReader(string path, bool hasHeaderRow = false, /// public MiniExcelDataReader GetDataReader(Stream stream, bool hasHeaderRow = false, CsvConfiguration ? configuration = null, bool leaveOpen = false) { - var values = Query(stream, hasHeaderRow, configuration).Cast>(); + var values = Query(stream, hasHeaderRow, configuration, leaveOpen).Cast>(); return MiniExcelDataReader.Create(stream, values, leaveOpen); } @@ -272,9 +273,9 @@ public async Task GetAsyncDataReader(string path, bool hasH CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); - var values = QueryAsync(stream, hasHeaderRow, configuration, cancellationToken); + var values = QueryAsync(stream, hasHeaderRow, configuration, leaveOpen: false, cancellationToken); - return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken), leaveOpen: false).ConfigureAwait(false); + return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken)).ConfigureAwait(false); } /// @@ -293,7 +294,7 @@ public async Task GetAsyncDataReader(string path, bool hasH public async Task GetAsyncDataReader(Stream stream, bool hasHeaderRow = false, CsvConfiguration? configuration = null, bool leaveOpen = false, CancellationToken cancellationToken = default) { - var values = QueryAsync(stream, hasHeaderRow, configuration, cancellationToken); + var values = QueryAsync(stream, hasHeaderRow, configuration, leaveOpen, cancellationToken); return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken), leaveOpen).ConfigureAwait(false); } diff --git a/src/MiniExcel.Csv/CsvReader.cs b/src/MiniExcel.Csv/CsvReader.cs index d0c5ee8b..fdbc5275 100644 --- a/src/MiniExcel.Csv/CsvReader.cs +++ b/src/MiniExcel.Csv/CsvReader.cs @@ -8,16 +8,18 @@ namespace MiniExcelLib.Csv; internal sealed partial class CsvReader : IMiniExcelReader { private readonly Stream _stream; + private readonly bool _leaveOpen; private readonly CsvConfiguration _config; - internal CsvReader(Stream stream, IMiniExcelConfiguration? configuration) + internal CsvReader(Stream stream, IMiniExcelConfiguration? configuration, bool leaveOpen = false) { _stream = stream; + _leaveOpen = leaveOpen; _config = configuration as CsvConfiguration ?? CsvConfiguration.Default; } [CreateSyncVersion] - public async IAsyncEnumerable> QueryAsync(bool useHeaderRow, string? sheetName, string startCell, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public async IAsyncEnumerable> QueryAsync(bool hasHeaderRow, string? sheetName, string startCell, [EnumeratorCancellation] CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -75,7 +77,7 @@ internal CsvReader(Stream stream, IMiniExcelConfiguration? configuration) } //header - if (useHeaderRow) + if (hasHeaderRow) { if (firstRow) { @@ -127,7 +129,7 @@ internal CsvReader(Stream stream, IMiniExcelConfiguration? configuration) } [CreateSyncVersion] - public IAsyncEnumerable> QueryRangeAsync(bool useHeaderRow, string? sheetName, string startCell, string endCell, CancellationToken cancellationToken = default) + public IAsyncEnumerable> QueryRangeAsync(bool hasHeaderRow, string? sheetName, string startCell, string endCell, CancellationToken cancellationToken = default) { throw new NotSupportedException("Ranged queries are not supported for csv file"); } @@ -140,7 +142,7 @@ internal CsvReader(Stream stream, IMiniExcelConfiguration? configuration) } [CreateSyncVersion] - public IAsyncEnumerable> QueryRangeAsync(bool useHeaderRow, string? sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, CancellationToken cancellationToken = default) + public IAsyncEnumerable> QueryRangeAsync(bool hasHeaderRow, string? sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, CancellationToken cancellationToken = default) { throw new NotSupportedException("Ranged queries are not supported for csv file"); } @@ -173,6 +175,7 @@ private string[] Split(string row) public void Dispose() { - _stream?.Dispose(); + if (!_leaveOpen) + _stream.Dispose(); } } diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs index 45c8d821..113abdc8 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs @@ -25,8 +25,7 @@ public async IAsyncEnumerable QueryAsync(string path, string? sheetName = var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - var query = QueryAsync(stream, sheetName, startCell, treatHeaderAsData, configuration, cancellationToken); - + var query = QueryAsync(stream, sheetName, startCell, treatHeaderAsData, configuration, false, cancellationToken); await foreach (var item in query.ConfigureAwait(false)) yield return item; } @@ -40,13 +39,14 @@ public async IAsyncEnumerable QueryAsync(string path, string? sheetName = /// The starting cell reference (e.g., "C2"). Default is "A1". /// If true, the first row is treated as data. If false (default), the first row is used as headers. /// Optional configuration settings. + /// True to leave the stream open after the query is completed, otherwise false. /// A token to cancel the asynchronous operation. [CreateSyncVersion] public async IAsyncEnumerable QueryAsync(Stream stream, string? sheetName = null, string startCell = "A1", bool treatHeaderAsData = false, OpenXmlConfiguration? configuration = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) where T : class, new() + bool leaveOpen = false, [EnumeratorCancellation] CancellationToken cancellationToken = default) where T : class, new() { - using var reader = await OpenXmlReader.CreateAsync(stream, configuration, cancellationToken).ConfigureAwait(false); + using var reader = await OpenXmlReader.CreateAsync(stream, configuration, leaveOpen, cancellationToken).ConfigureAwait(false); await foreach (var item in reader.QueryAsync(sheetName, startCell, treatHeaderAsData, cancellationToken).ConfigureAwait(false)) yield return item; } @@ -71,7 +71,7 @@ public async IAsyncEnumerable QueryAsync(string path, bool hasHeaderRow var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - await foreach (var item in QueryAsync(stream, hasHeaderRow, sheetName, startCell, configuration, cancellationToken).ConfigureAwait(false)) + await foreach (var item in QueryAsync(stream, hasHeaderRow, sheetName, startCell, configuration, false, cancellationToken).ConfigureAwait(false)) yield return item; } @@ -83,6 +83,7 @@ public async IAsyncEnumerable QueryAsync(string path, bool hasHeaderRow /// The name of the sheet to query. If null, the first sheet is used. /// The starting cell reference (e.g., "C2"). Default is "A1". /// Optional configuration settings. + /// True to leave the stream open after the query is completed, otherwise false. /// A token to cancel the asynchronous operation. /// /// When is true, column names from the first row become dynamic property names, otherwise they will be assigned alphabetically (A, B, C, etc.). @@ -90,10 +91,10 @@ public async IAsyncEnumerable QueryAsync(string path, bool hasHeaderRow [CreateSyncVersion] public async IAsyncEnumerable QueryAsync(Stream stream, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + bool leaveOpen = false, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - using var excelReader = await OpenXmlReader.CreateAsync(stream, configuration, cancellationToken).ConfigureAwait(false); - await foreach (var item in excelReader.QueryAsync(hasHeaderRow, sheetName, startCell, cancellationToken).ConfigureAwait(false)) + using var reader = await OpenXmlReader.CreateAsync(stream, configuration, leaveOpen, cancellationToken).ConfigureAwait(false); + await foreach (var item in reader.QueryAsync(hasHeaderRow, sheetName, startCell, cancellationToken).ConfigureAwait(false)) yield return item; } @@ -119,7 +120,7 @@ public async IAsyncEnumerable QueryRangeAsync(string path, bool hasHead var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - await foreach (var item in QueryRangeAsync(stream, hasHeaderRow, sheetName, startCell, endCell, configuration, cancellationToken).ConfigureAwait(false)) + await foreach (var item in QueryRangeAsync(stream, hasHeaderRow, sheetName, startCell, endCell, configuration, false, cancellationToken).ConfigureAwait(false)) yield return item; } @@ -132,14 +133,15 @@ public async IAsyncEnumerable QueryRangeAsync(string path, bool hasHead /// The starting cell reference. Default is "A1". /// The ending cell reference. If left empty, the last cell containing data will be used. /// Optional configuration settings. + /// True to leave the stream open after the query is completed, otherwise false. /// A token to cancel the asynchronous operation. [CreateSyncVersion] public async IAsyncEnumerable QueryRangeAsync(Stream stream, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", string? endCell = null, OpenXmlConfiguration? configuration = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + bool leaveOpen = false, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - using var excelReader = await OpenXmlReader.CreateAsync(stream, configuration, cancellationToken).ConfigureAwait(false); - await foreach (var item in excelReader.QueryRangeAsync(hasHeaderRow, sheetName, startCell, endCell, cancellationToken).ConfigureAwait(false)) + using var reader = await OpenXmlReader.CreateAsync(stream, configuration, leaveOpen, cancellationToken).ConfigureAwait(false); + await foreach (var item in reader.QueryRangeAsync(hasHeaderRow, sheetName, startCell, endCell, cancellationToken).ConfigureAwait(false)) yield return item; } @@ -164,7 +166,7 @@ public async IAsyncEnumerable QueryRangeAsync(string path, bool hasHead var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - await foreach (var item in QueryRangeAsync(stream, hasHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration, cancellationToken).ConfigureAwait(false)) + await foreach (var item in QueryRangeAsync(stream, hasHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration, false, cancellationToken).ConfigureAwait(false)) yield return item; } @@ -179,15 +181,16 @@ public async IAsyncEnumerable QueryRangeAsync(string path, bool hasHead /// The 1-based index of the ending row (inclusive). If null, reads to the last row containing data. /// The 1-based index of the ending column (inclusive). If null, reads to the last column containing data. /// Optional configuration settings. + /// True to leave the stream open after the query is completed, otherwise false. /// A token to cancel the asynchronous operation. [CreateSyncVersion] public async IAsyncEnumerable QueryRangeAsync(Stream stream, bool hasHeaderRow = false, string? sheetName = null, int startRowIndex = 1, int startColumnIndex = 1, int? endRowIndex = null, - int? endColumnIndex = null, OpenXmlConfiguration? configuration = null, + int? endColumnIndex = null, OpenXmlConfiguration? configuration = null, bool leaveOpen = false, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - using var excelReader = await OpenXmlReader.CreateAsync(stream, configuration, cancellationToken).ConfigureAwait(false); - await foreach (var item in excelReader.QueryRangeAsync(hasHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, cancellationToken).ConfigureAwait(false)) + using var reader = await OpenXmlReader.CreateAsync(stream, configuration, leaveOpen, cancellationToken).ConfigureAwait(false); + await foreach (var item in reader.QueryRangeAsync(hasHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, cancellationToken).ConfigureAwait(false)) yield return item; } @@ -216,7 +219,7 @@ public async Task QueryAsDataTableAsync(string path, bool hasHeaderRo var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - return await QueryAsDataTableAsync(stream, hasHeaderRow, sheetName, startCell, configuration, cancellationToken).ConfigureAwait(false); + return await QueryAsDataTableAsync(stream, hasHeaderRow, sheetName, startCell, configuration, false, cancellationToken).ConfigureAwait(false); } /// @@ -227,6 +230,7 @@ public async Task QueryAsDataTableAsync(string path, bool hasHeaderRo /// The name of the sheet to query. If not specified, the first sheet is used. /// The starting cell reference (e.g., "C2"). Default is "A1". /// Optional configuration settings. + /// True to leave the stream open after the query is completed, otherwise false. /// A token to cancel the asynchronous operation. /// /// Empty column names are skipped. @@ -235,13 +239,13 @@ public async Task QueryAsDataTableAsync(string path, bool hasHeaderRo [CreateSyncVersion] public async Task QueryAsDataTableAsync(Stream stream, bool hasHeaderRow = true, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, - CancellationToken cancellationToken = default) + bool leaveOpen = false, CancellationToken cancellationToken = default) { - sheetName ??= (await GetSheetNamesAsync(stream, cancellationToken).ConfigureAwait(false)).First(); + sheetName ??= (await GetSheetNamesAsync(stream, false, cancellationToken).ConfigureAwait(false)).First(); var dt = new DataTable(sheetName); var first = true; - using var reader = await OpenXmlReader.CreateAsync(stream, configuration, cancellationToken).ConfigureAwait(false); + using var reader = await OpenXmlReader.CreateAsync(stream, configuration, leaveOpen, cancellationToken).ConfigureAwait(false); var rows = reader.QueryAsync(false, sheetName, startCell, cancellationToken); var columnDict = new Dictionary(); @@ -302,24 +306,25 @@ public async Task> GetSheetNamesAsync(string path, CancellationToke var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - return await GetSheetNamesAsync(stream, cancellationToken).ConfigureAwait(false); + return await GetSheetNamesAsync(stream, false, cancellationToken).ConfigureAwait(false); } /// /// Retrieves the names of all sheets in an Excel workbook. /// /// The stream containing the Excel file data. The stream position is not reset after reading. + /// True to leave the stream open after the operation is completed, otherwise false. /// A token to cancel the asynchronous operation. /// A list of sheet names in the workbook, or an empty list if no sheets are found. /// /// Sheet names are returned in the order they appear in the workbook. /// [CreateSyncVersion] - public async Task> GetSheetNamesAsync(Stream stream, CancellationToken cancellationToken = default) + public async Task> GetSheetNamesAsync(Stream stream, bool leaveOpen = false, CancellationToken cancellationToken = default) { var archive = await OpenXmlZip.CreateAsync(stream, leaveOpen: true, cancellationToken: cancellationToken).ConfigureAwait(false); await using var disposableArchive = archive.ConfigureAwait(false); - using var reader = await OpenXmlReader.CreateAsync(stream, null, cancellationToken).ConfigureAwait(false); + using var reader = await OpenXmlReader.CreateAsync(stream, null, leaveOpen, cancellationToken).ConfigureAwait(false); var rels = await reader.GetWorkbookRelsAsync(archive.EntryCollection, cancellationToken).ConfigureAwait(false); return rels?.Select(s => s.Name).ToList() ?? []; @@ -340,25 +345,26 @@ public async Task> GetSheetInformationsAsync(string path, Cancel var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - return await GetSheetInformationsAsync(stream, cancellationToken).ConfigureAwait(false); + return await GetSheetInformationsAsync(stream, false, cancellationToken).ConfigureAwait(false); } /// /// Retrieves detailed information about all sheets in an Excel workbook. /// /// The stream containing the Excel file data. The stream position is not reset after reading. + /// True to leave the stream open after the operation is completed, otherwise false. /// A token to cancel the asynchronous operation. /// A list of objects containing metadata for each sheet, including name, dimensions, and sheet index. /// /// Sheet information is returned in the order sheets appear in the workbook. /// [CreateSyncVersion] - public async Task> GetSheetInformationsAsync(Stream stream, CancellationToken cancellationToken = default) + public async Task> GetSheetInformationsAsync(Stream stream, bool leaveOpen = false, CancellationToken cancellationToken = default) { var archive = await OpenXmlZip.CreateAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false); await using var disposableArchve = archive.ConfigureAwait(false); - using var reader = await OpenXmlReader.CreateAsync(stream, null, cancellationToken: cancellationToken).ConfigureAwait(false); + using var reader = await OpenXmlReader.CreateAsync(stream, null, leaveOpen, cancellationToken).ConfigureAwait(false); var rels = await reader.GetWorkbookRelsAsync(archive.EntryCollection, cancellationToken).ConfigureAwait(false); return rels?.Select((s, i) => s.ToSheetInfo((uint)i)).ToList() ?? []; @@ -382,13 +388,14 @@ public async Task> GetSheetDimensionsAsync(string path, Cancel var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - return await GetSheetDimensionsAsync(stream, cancellationToken).ConfigureAwait(false); + return await GetSheetDimensionsAsync(stream, false, cancellationToken).ConfigureAwait(false); } /// /// Retrieves the dimensions (used cell range) for all sheets in an Excel workbook. /// /// The stream containing the Excel file data. The stream position is not reset after reading. + /// True to leave the stream open after the operation is completed, otherwise false. /// A token to cancel the asynchronous operation. /// A list of objects representing the used dimensions for each sheet in the workbook. /// @@ -398,9 +405,9 @@ public async Task> GetSheetDimensionsAsync(string path, Cancel /// A synchronous version of this method is automatically generated via the [CreateSyncVersion] attribute. /// [CreateSyncVersion] - public async Task> GetSheetDimensionsAsync(Stream stream, CancellationToken cancellationToken = default) + public async Task> GetSheetDimensionsAsync(Stream stream, bool leaveOpen = false, CancellationToken cancellationToken = default) { - using var reader = await OpenXmlReader.CreateAsync(stream, null, cancellationToken: cancellationToken).ConfigureAwait(false); + using var reader = await OpenXmlReader.CreateAsync(stream, null, leaveOpen, cancellationToken).ConfigureAwait(false); return await reader.GetDimensionsAsync(cancellationToken).ConfigureAwait(false); } @@ -423,7 +430,7 @@ public async Task> GetColumnNamesAsync(string path, bool has var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - return await GetColumnNamesAsync(stream, hasHeaderRow, sheetName, startCell, cancellationToken).ConfigureAwait(false); + return await GetColumnNamesAsync(stream, hasHeaderRow, sheetName, startCell, false, cancellationToken).ConfigureAwait(false); } @@ -434,16 +441,17 @@ public async Task> GetColumnNamesAsync(string path, bool has /// If true, the first row values are used as column names. If false, column letters (A, B, C, etc.) are used. Default is false. /// The name of the worksheet to query. If not provided, the first sheet is used. /// The starting cell reference (e.g., "C2"). Default is "A1". + /// True to leave the stream open after the query is completed, otherwise false. /// A token to cancel the asynchronous operation. /// A collection of column names from the specified location, or an empty collection if the sheet is empty. /// /// Returns an empty collection if the sheet has no rows starting from . /// [CreateSyncVersion] - public async Task> GetColumnNamesAsync(Stream stream, bool hasHeaderRow = false, - string? sheetName = null, string startCell = "A1", CancellationToken cancellationToken = default) + public async Task> GetColumnNamesAsync(Stream stream, bool hasHeaderRow = false, + string? sheetName = null, string startCell = "A1", bool leaveOpen = false, CancellationToken cancellationToken = default) { - var enumerator = QueryAsync(stream, hasHeaderRow, sheetName, startCell, null, cancellationToken).GetAsyncEnumerator(cancellationToken); + var enumerator = QueryAsync(stream, hasHeaderRow, sheetName, startCell, null, leaveOpen, cancellationToken).GetAsyncEnumerator(cancellationToken); await using var disposableEnumerator = enumerator.ConfigureAwait(false); if (await enumerator.MoveNextAsync().ConfigureAwait(false)) @@ -468,7 +476,7 @@ public async Task RetrieveCommentsAsync(string path, string? s var stream = FileHelper.OpenSharedRead(path); await using var disposableStream = stream.ConfigureAwait(false); - return await RetrieveCommentsAsync(stream, sheetName, cancellationToken).ConfigureAwait(false); + return await RetrieveCommentsAsync(stream, sheetName, false, cancellationToken).ConfigureAwait(false); } /// @@ -476,15 +484,16 @@ public async Task RetrieveCommentsAsync(string path, string? s /// /// The stream containing the Excel file data. The stream position is not reset after reading. /// The name of the worksheet from which to retrieve comments. If not provided, comments from the first sheet are retrieved. + /// True to leave the stream open after the operation is completed, otherwise false. /// A token to cancel the asynchronous operation. /// /// Comments are cell-level annotations in Excel files that are stored separately from the cell data. /// The returned provides access to both threaded comments and legacy note comments, along with the associated metadata. /// [CreateSyncVersion] - public async Task RetrieveCommentsAsync(Stream stream, string? sheetName, CancellationToken cancellationToken = default) + public async Task RetrieveCommentsAsync(Stream stream, string? sheetName, bool leaveOpen = false, CancellationToken cancellationToken = default) { - using var reader = await OpenXmlReader.CreateAsync(stream, null, cancellationToken).ConfigureAwait(false); + using var reader = await OpenXmlReader.CreateAsync(stream, null, leaveOpen, cancellationToken).ConfigureAwait(false); return await reader.ReadCommentsAsync(sheetName, cancellationToken).ConfigureAwait(false); } @@ -509,7 +518,7 @@ public MiniExcelDataReader GetDataReader(string path, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null) { var stream = FileHelper.OpenSharedRead(path); - var values = Query(stream, hasHeaderRow, sheetName, startCell, configuration).Cast>(); + var values = Query(stream, hasHeaderRow, sheetName, startCell, configuration, leaveOpen: false).Cast>(); return MiniExcelDataReader.Create(stream, values, leaveOpen: false); } @@ -531,7 +540,7 @@ public MiniExcelDataReader GetDataReader(string path, bool hasHeaderRow = false, public MiniExcelDataReader GetDataReader(Stream stream, bool hasHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, bool leaveOpen = false) { - var values = Query(stream, hasHeaderRow, sheetName, startCell, configuration).Cast>(); + var values = Query(stream, hasHeaderRow, sheetName, startCell, configuration, leaveOpen).Cast>(); return MiniExcelDataReader.Create(stream, values, leaveOpen); } @@ -553,7 +562,7 @@ public async Task GetAsyncDataReader(string path, bool hasH string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); - var values = QueryAsync(stream, hasHeaderRow, sheetName, startCell, configuration, cancellationToken); + var values = QueryAsync(stream, hasHeaderRow, sheetName, startCell, configuration, leaveOpen: false, cancellationToken); return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken), leaveOpen: false).ConfigureAwait(false); } @@ -577,7 +586,7 @@ public async Task GetAsyncDataReader(Stream stream, bool ha string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, bool leaveOpen = false, CancellationToken cancellationToken = default) { - var values = QueryAsync(stream, hasHeaderRow, sheetName, startCell, configuration, cancellationToken); + var values = QueryAsync(stream, hasHeaderRow, sheetName, startCell, configuration, leaveOpen, cancellationToken); return await MiniExcelDataReader.CreateAsync(stream, values.CastToDictionary(cancellationToken), leaveOpen).ConfigureAwait(false); } diff --git a/src/MiniExcel.OpenXml/FluentMapping/Api/MappingImporter.cs b/src/MiniExcel.OpenXml/FluentMapping/Api/MappingImporter.cs index 504afbcc..dc682cb1 100644 --- a/src/MiniExcel.OpenXml/FluentMapping/Api/MappingImporter.cs +++ b/src/MiniExcel.OpenXml/FluentMapping/Api/MappingImporter.cs @@ -16,12 +16,12 @@ public MappingImporter(MappingRegistry registry) : this() var stream = File.OpenRead(path); await using var disposableStream = stream.ConfigureAwait(false); - await foreach (var item in QueryAsync(stream, cancellationToken).ConfigureAwait(false)) + await foreach (var item in QueryAsync(stream, false, cancellationToken).ConfigureAwait(false)) yield return item; } [CreateSyncVersion] - public async IAsyncEnumerable QueryAsync(Stream? stream, [EnumeratorCancellation] CancellationToken cancellationToken = default) where T : class, new() + public async IAsyncEnumerable QueryAsync(Stream? stream, bool leaveOpen = false, [EnumeratorCancellation] CancellationToken cancellationToken = default) where T : class, new() { if (stream is null) throw new ArgumentNullException(nameof(stream)); @@ -29,7 +29,7 @@ public MappingImporter(MappingRegistry registry) : this() if (_registry.GetCompiledMapping() is not { } mapping) throw new InvalidOperationException($"No mapping configuration found for type {typeof(T).Name}. Configure the mapping using MappingRegistry.Configure<{typeof(T).Name}>()."); - await foreach (var item in MappingReader.QueryAsync(stream, mapping, cancellationToken).ConfigureAwait(false)) + await foreach (var item in MappingReader.QueryAsync(stream, mapping, leaveOpen, cancellationToken).ConfigureAwait(false)) yield return item; } @@ -39,11 +39,11 @@ public MappingImporter(MappingRegistry registry) : this() var stream = File.OpenRead(path); await using var disposableStream = stream.ConfigureAwait(false); - return await QuerySingleAsync(stream, cancellationToken).ConfigureAwait(false); + return await QuerySingleAsync(stream, false, cancellationToken).ConfigureAwait(false); } [CreateSyncVersion] - private async Task QuerySingleAsync(Stream? stream, CancellationToken cancellationToken = default) where T : class, new() + private async Task QuerySingleAsync(Stream? stream, bool leaveOpen = false, CancellationToken cancellationToken = default) where T : class, new() { if (stream is null) throw new ArgumentNullException(nameof(stream)); @@ -51,7 +51,7 @@ public MappingImporter(MappingRegistry registry) : this() if (_registry.GetCompiledMapping() is not { } mapping) throw new InvalidOperationException($"No mapping configuration found for type {typeof(T).Name}. Configure the mapping using MappingRegistry.Configure<{typeof(T).Name}>()."); - await foreach (var item in MappingReader.QueryAsync(stream, mapping, cancellationToken).ConfigureAwait(false)) + await foreach (var item in MappingReader.QueryAsync(stream, mapping, leaveOpen, cancellationToken).ConfigureAwait(false)) { return item; // Return the first item } diff --git a/src/MiniExcel.OpenXml/FluentMapping/MappingReader.cs b/src/MiniExcel.OpenXml/FluentMapping/MappingReader.cs index 611f9f1b..049fd3a5 100644 --- a/src/MiniExcel.OpenXml/FluentMapping/MappingReader.cs +++ b/src/MiniExcel.OpenXml/FluentMapping/MappingReader.cs @@ -1,24 +1,21 @@ -using System.Runtime.CompilerServices; -using Zomp.SyncMethodGenerator; - namespace MiniExcelLib.OpenXml.FluentMapping; internal static partial class MappingReader where T : class, new() { [CreateSyncVersion] - public static async IAsyncEnumerable QueryAsync(Stream stream, CompiledMapping mapping, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public static async IAsyncEnumerable QueryAsync(Stream stream, CompiledMapping mapping, bool leaveOpen = false, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (stream is null) throw new ArgumentNullException(nameof(stream)); if (mapping is null) throw new ArgumentNullException(nameof(mapping)); - await foreach (var item in QueryOptimizedAsync(stream, mapping, cancellationToken).ConfigureAwait(false)) + await foreach (var item in QueryOptimizedAsync(stream, mapping, leaveOpen, cancellationToken).ConfigureAwait(false)) yield return item; } [CreateSyncVersion] - private static async IAsyncEnumerable QueryOptimizedAsync(Stream stream, CompiledMapping mapping, [EnumeratorCancellation] CancellationToken cancellationToken = default) + private static async IAsyncEnumerable QueryOptimizedAsync(Stream stream, CompiledMapping mapping, bool leaveOpen = false, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (mapping.OptimizedCellGrid is null || mapping.OptimizedBoundaries is null) throw new InvalidOperationException("QueryOptimizedAsync requires an optimized mapping"); @@ -31,7 +28,7 @@ private static async IAsyncEnumerable QueryOptimizedAsync(Stream stream, Comp { FillMergedCells = false, FastMode = false - }, cancellationToken).ConfigureAwait(false); + }, leaveOpen, cancellationToken).ConfigureAwait(false); // If we have collections, we need to handle multiple items with collections if (mapping.Collections.Any()) diff --git a/src/MiniExcel.OpenXml/OpenXmlReader.cs b/src/MiniExcel.OpenXml/OpenXmlReader.cs index 4c7e18f7..be3b2236 100644 --- a/src/MiniExcel.OpenXml/OpenXmlReader.cs +++ b/src/MiniExcel.OpenXml/OpenXmlReader.cs @@ -25,11 +25,11 @@ private OpenXmlReader(OpenXmlZip archive, IMiniExcelConfiguration? configuration } [CreateSyncVersion] - internal static async Task CreateAsync(Stream stream, IMiniExcelConfiguration? configuration, CancellationToken cancellationToken = default) + internal static async Task CreateAsync(Stream stream, IMiniExcelConfiguration? configuration, bool leaveOpen = false, CancellationToken cancellationToken = default) { ThrowHelper.ThrowIfInvalidOpenXml(stream); - var archive = await OpenXmlZip.CreateAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false); + var archive = await OpenXmlZip.CreateAsync(stream, leaveOpen: leaveOpen, cancellationToken: cancellationToken).ConfigureAwait(false); var reader = new OpenXmlReader(archive, configuration); await reader.SetSharedStringsAsync(cancellationToken).ConfigureAwait(false); @@ -37,9 +37,9 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC } [CreateSyncVersion] - public IAsyncEnumerable> QueryAsync(bool useHeaderRow, string? sheetName, string startCell, CancellationToken cancellationToken = default) + public IAsyncEnumerable> QueryAsync(bool hasHeaderRow, string? sheetName, string startCell, CancellationToken cancellationToken = default) { - return QueryRangeAsync(useHeaderRow, sheetName, startCell, "", cancellationToken); + return QueryRangeAsync(hasHeaderRow, sheetName, startCell, "", cancellationToken); } [CreateSyncVersion] @@ -58,7 +58,7 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC } [CreateSyncVersion] - public IAsyncEnumerable> QueryRangeAsync(bool useHeaderRow, string? sheetName, string? startCell, string? endCell, CancellationToken cancellationToken = default) + public IAsyncEnumerable> QueryRangeAsync(bool hasHeaderRow, string? sheetName, string? startCell, string? endCell, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -82,7 +82,7 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC endRowIndex = rIndex - 1; } - return InternalQueryRangeAsync(useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, cancellationToken); + return InternalQueryRangeAsync(hasHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, cancellationToken); } [CreateSyncVersion] @@ -96,7 +96,7 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC } [CreateSyncVersion] - public IAsyncEnumerable> QueryRangeAsync(bool useHeaderRow, string? sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, CancellationToken cancellationToken = default) + public IAsyncEnumerable> QueryRangeAsync(bool hasHeaderRow, string? sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -130,7 +130,7 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC } } - return InternalQueryRangeAsync(useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, cancellationToken); + return InternalQueryRangeAsync(hasHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, cancellationToken); } [CreateSyncVersion] @@ -258,7 +258,7 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC int? endColumnIndex, int maxColumnIndex, bool withoutCr, - bool useHeaderRow, + bool hasHeaderRow, Dictionary headRows, MergeCells? mergeCells, [EnumeratorCancellation] CancellationToken cancellationToken = default) @@ -271,7 +271,7 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC { for (int i = expectedRowIndex; i < rowIndex; i++) { - yield return GetCell(useHeaderRow, maxColumnIndex, headRows, startColumnIndex); + yield return GetCell(hasHeaderRow, maxColumnIndex, headRows, startColumnIndex); } } } @@ -280,11 +280,11 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false) && !_config.IgnoreEmptyRows) { //Fill in case of self closed empty row tag eg. - yield return GetCell(useHeaderRow, maxColumnIndex, headRows, startColumnIndex); + yield return GetCell(hasHeaderRow, maxColumnIndex, headRows, startColumnIndex); yield break; } - var cell = GetCell(useHeaderRow, maxColumnIndex, headRows, startColumnIndex); + var cell = GetCell(hasHeaderRow, maxColumnIndex, headRows, startColumnIndex); var columnIndex = withoutCr ? -1 : 0; while (!reader.EOF) { @@ -324,7 +324,7 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC cellValue = _style.ConvertValueByStyleFormat(xfIndex, cellValue); } - SetCellsValueAndHeaders(cellValue, useHeaderRow, headRows, isFirstRow, cell, columnIndex); + SetCellsValueAndHeaders(cellValue, hasHeaderRow, headRows, isFirstRow, cell, columnIndex); } else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { diff --git a/src/MiniExcel.OpenXml/Picture/OpenXmlPictureImplement.cs b/src/MiniExcel.OpenXml/Picture/OpenXmlPictureImplement.cs index 67b15e5e..9e3113e8 100644 --- a/src/MiniExcel.OpenXml/Picture/OpenXmlPictureImplement.cs +++ b/src/MiniExcel.OpenXml/Picture/OpenXmlPictureImplement.cs @@ -19,7 +19,7 @@ public static async Task AddPictureAsync(Stream excelStream, CancellationToken c var excelArchive = await OpenXmlZip.CreateAsync(excelStream, cancellationToken: cancellationToken).ConfigureAwait(false); await using var disposableExcelArchive = excelArchive.ConfigureAwait(false); - using var reader = await OpenXmlReader.CreateAsync(excelStream, null, cancellationToken).ConfigureAwait(false); + using var reader = await OpenXmlReader.CreateAsync(excelStream, null, false, cancellationToken).ConfigureAwait(false); #if NET10_0_OR_GREATER var archive = await ZipArchive.CreateAsync(excelStream, ZipArchiveMode.Update, true, null, cancellationToken).ConfigureAwait(false); diff --git a/src/MiniExcel.OpenXml/Utils/SharedStringsDiskCache.cs b/src/MiniExcel.OpenXml/Utils/SharedStringsDiskCache.cs index d6922979..a3f9821e 100644 --- a/src/MiniExcel.OpenXml/Utils/SharedStringsDiskCache.cs +++ b/src/MiniExcel.OpenXml/Utils/SharedStringsDiskCache.cs @@ -1,4 +1,6 @@ -namespace MiniExcelLib.OpenXml.Utils; +using System.Buffers; + +namespace MiniExcelLib.OpenXml.Utils; internal sealed class SharedStringsDiskCache : IDictionary, IDisposable { @@ -64,26 +66,53 @@ private string GetValue(int index) _ = _lengthFs.Read(bytes); var length = BitConverter.ToInt32(bytes); - bytes = stackalloc byte[length]; - _valueFs.Seek(position, SeekOrigin.Begin); - _ = _valueFs.Read(bytes); - - return Encoding.GetString(bytes[..length]); + byte[] rented = []; + if (length <= 1024) + { + bytes = stackalloc byte[length]; + } + else + { + rented = ArrayPool.Shared.Rent(length); + bytes = rented; + } + + try + { + _valueFs.Seek(position, SeekOrigin.Begin); + _ = _valueFs.Read(bytes); + + return Encoding.GetString(bytes[..length]); + } + finally + { + ArrayPool.Shared.Return(rented); + } #else - var bytes = new byte[4]; - _ = _positionFs.Read(bytes, 0, 4); - var position = BitConverter.ToInt32(bytes, 0); - - bytes = new byte[4]; - _lengthFs.Position = index * 4; - _ = _lengthFs.Read(bytes, 0, 4); - var length = BitConverter.ToInt32(bytes, 0); - - bytes = new byte[length]; - _valueFs.Position = position; - _ = _valueFs.Read(bytes, 0, length); - - return Encoding.GetString(bytes); + var bytes1 = ArrayPool.Shared.Rent(4); + byte[] bytes2 = []; + + try + { + _ = _positionFs.Read(bytes1, 0, 4); + var position = BitConverter.ToInt32(bytes1, 0); + + Array.Clear(bytes1, 0, 4); + _lengthFs.Position = index * 4; + _ = _lengthFs.Read(bytes1, 0, 4); + var length = BitConverter.ToInt32(bytes1, 0); + + bytes2 = ArrayPool.Shared.Rent(length); + _valueFs.Position = position; + _ = _valueFs.Read(bytes2, 0, length); + + return Encoding.GetString(bytes2); + } + finally + { + ArrayPool.Shared.Return(bytes1); + ArrayPool.Shared.Return(bytes2); + } #endif } diff --git a/src/MiniExcel/MiniExcel.cs b/src/MiniExcel/MiniExcel.cs index c47eee7e..217a7555 100644 --- a/src/MiniExcel/MiniExcel.cs +++ b/src/MiniExcel/MiniExcel.cs @@ -127,8 +127,8 @@ public static async Task SaveAsAsync(this Stream stream, object value, bo var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryAsync(stream, sheetName, startCell, hasHeader, configuration as NewOpenXmlConfiguration, cancellationToken), - ExcelType.CSV => CsvImporter.QueryAsync(stream, hasHeader, configuration as Csv.CsvConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryAsync(stream, sheetName, startCell, hasHeader, configuration as NewOpenXmlConfiguration, false, cancellationToken), + ExcelType.CSV => CsvImporter.QueryAsync(stream, hasHeader, configuration as Csv.CsvConfiguration, false, cancellationToken), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } @@ -151,8 +151,8 @@ public static IAsyncEnumerable QueryAsync(this Stream stream, bool useH var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, cancellationToken), - ExcelType.CSV => CsvImporter.QueryAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, false, cancellationToken), + ExcelType.CSV => CsvImporter.QueryAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration,false, cancellationToken), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } @@ -184,8 +184,8 @@ public static IAsyncEnumerable QueryRangeAsync(this Stream stream, bool var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryRangeAsync(stream, useHeaderRow, sheetName, startCell, endCell, configuration as NewOpenXmlConfiguration, cancellationToken), - ExcelType.CSV => CsvImporter.QueryAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryRangeAsync(stream, useHeaderRow, sheetName, startCell, endCell, configuration as NewOpenXmlConfiguration, false, cancellationToken), + ExcelType.CSV => CsvImporter.QueryAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, false, cancellationToken), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } @@ -208,7 +208,7 @@ public static IAsyncEnumerable QueryRangeAsync(this Stream stream, bool var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryRangeAsync(stream, useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration as NewOpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryRangeAsync(stream, useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration as NewOpenXmlConfiguration, false, cancellationToken), ExcelType.CSV => throw new NotSupportedException("QueryRange is not supported for csv"), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -291,8 +291,8 @@ public static async Task QueryAsDataTableAsync(this Stream stream, bo var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelImporter.QueryAsDataTableAsync(stream, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false), - ExcelType.CSV => await CsvImporter.QueryAsDataTableAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelImporter.QueryAsDataTableAsync(stream, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, false, cancellationToken).ConfigureAwait(false), + ExcelType.CSV => await CsvImporter.QueryAsDataTableAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, false, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") }; } @@ -303,7 +303,7 @@ public static async Task> GetSheetNamesAsync(string path, NewOpenXm [CreateSyncVersion] public static async Task> GetSheetNamesAsync(this Stream stream, NewOpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) - => await ExcelImporter.GetSheetNamesAsync(stream, cancellationToken).ConfigureAwait(false); + => await ExcelImporter.GetSheetNamesAsync(stream, false, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] public static async Task> GetSheetInformationsAsync(string path, NewOpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) @@ -311,7 +311,7 @@ public static async Task> GetSheetInformationsAsync(string path, [CreateSyncVersion] public static async Task> GetSheetInformationsAsync(this Stream stream, NewOpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) - => await ExcelImporter.GetSheetInformationsAsync(stream, cancellationToken).ConfigureAwait(false); + => await ExcelImporter.GetSheetInformationsAsync(stream, false, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] public static async Task> GetColumnsAsync(string path, bool useHeaderRow = false, string? sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration? configuration = null, CancellationToken cancellationToken = default) @@ -331,7 +331,7 @@ public static async Task> GetColumnsAsync(this Stream stream var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelImporter.GetColumnNamesAsync(stream, useHeaderRow, sheetName, startCell, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelImporter.GetColumnNamesAsync(stream, useHeaderRow, sheetName, startCell, false, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvImporter.GetColumnNamesAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") }; @@ -343,7 +343,7 @@ public static async Task> GetSheetDimensionsAsync(string path, [CreateSyncVersion] public static async Task> GetSheetDimensionsAsync(this Stream stream, CancellationToken cancellationToken = default) - => await ExcelImporter.GetSheetDimensionsAsync(stream, cancellationToken).ConfigureAwait(false); + => await ExcelImporter.GetSheetDimensionsAsync(stream, false, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] public static async Task ConvertCsvToXlsxAsync(string csv, string xlsx, CancellationToken cancellationToken = default)