From 83656d1c9f7b8c3a6feacfb45e7ebafd96e729da Mon Sep 17 00:00:00 2001 From: izanhzh Date: Sat, 22 Nov 2025 11:09:30 +0800 Subject: [PATCH 1/4] Release the internally created IAsyncEnumerator --- src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs | 215 +++++++++--------- .../AsyncEnumerableWriteAdapter.cs | 21 +- .../MiniExcelWriteAdapterFactory.cs | 8 +- 3 files changed, 137 insertions(+), 107 deletions(-) diff --git a/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs b/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs index f5b562e3..a487a870 100644 --- a/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs @@ -1,23 +1,23 @@ -using System.ComponentModel; -using System.Xml.Linq; using MiniExcelLib.Core.OpenXml.Constants; using MiniExcelLib.Core.OpenXml.Models; using MiniExcelLib.Core.OpenXml.Styles.Builder; using MiniExcelLib.Core.OpenXml.Zip; using MiniExcelLib.Core.WriteAdapters; +using System.ComponentModel; +using System.Xml.Linq; namespace MiniExcelLib.Core.OpenXml; internal partial class OpenXmlWriter : IMiniExcelWriter { private static readonly UTF8Encoding Utf8WithBom = new(true); - + private readonly MiniExcelZipArchive _archive; private readonly OpenXmlConfiguration _configuration; private readonly Stream _stream; private readonly List _sheets = []; private readonly List _files = []; - + private readonly string? _defaultSheetName; private readonly bool _printHeader; private readonly object? _value; @@ -41,16 +41,16 @@ internal OpenXmlWriter(Stream stream, object? value, string? sheetName, IMiniExc _printHeader = printHeader; _defaultSheetName = sheetName; } - + [CreateSyncVersion] - internal static Task CreateAsync(Stream stream, object? value, string? sheetName, bool printHeader, IMiniExcelConfiguration? configuration, CancellationToken cancellationToken = default) + internal static Task CreateAsync(Stream stream, object? value, string? sheetName, bool printHeader, IMiniExcelConfiguration? configuration, CancellationToken cancellationToken = default) { ThrowHelper.ThrowIfInvalidSheetName(sheetName); - + var writer = new OpenXmlWriter(stream, value, sheetName, configuration, printHeader); return Task.FromResult(writer); } - + [CreateSyncVersion] public async Task SaveAsAsync(IProgress? progress = null, CancellationToken cancellationToken = default) { @@ -190,7 +190,7 @@ private async Task CreateSheetXmlAsync(object? values, string sheetPath, IP using var zipStream = entry.Open(); #endif using var writer = new SafeStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize); - + if (values is null) { await WriteEmptySheetAsync(writer).ConfigureAwait(false); @@ -241,125 +241,136 @@ private async Task WriteValuesAsync(SafeStreamWriter writer, object values, { writeAdapter = MiniExcelWriteAdapterFactory.GetWriteAdapter(values, _configuration); } + try + { + var count = 0; + var isKnownCount = writeAdapter is not null && writeAdapter.TryGetKnownCount(out count); - var count = 0; - var isKnownCount = writeAdapter is not null && writeAdapter.TryGetKnownCount(out count); - #if SYNC_ONLY var props = writeAdapter?.GetColumns(); #else - var props = writeAdapter is not null - ? writeAdapter.GetColumns() - : await (asyncWriteAdapter?.GetColumnsAsync() ?? Task.FromResult?>(null)).ConfigureAwait(false); + var props = writeAdapter is not null + ? writeAdapter.GetColumns() + : await (asyncWriteAdapter?.GetColumnsAsync() ?? Task.FromResult?>(null)).ConfigureAwait(false); #endif - - if (props is null) - { - await WriteEmptySheetAsync(writer).ConfigureAwait(false); - return 0; - } - - int maxRowIndex; - var maxColumnIndex = props.Count(x => x is { ExcelIgnore: false }); - await writer.WriteAsync(WorksheetXml.StartWorksheetWithRelationship, cancellationToken).ConfigureAwait(false); + if (props is null) + { + await WriteEmptySheetAsync(writer).ConfigureAwait(false); + return 0; + } - long dimensionPlaceholderPostition = 0; + int maxRowIndex; + var maxColumnIndex = props.Count(x => x is { ExcelIgnore: false }); - // We can write the dimensions directly if the row count is known - if (isKnownCount) - { - maxRowIndex = _printHeader ? count + 1 : count; - await writer.WriteAsync(WorksheetXml.Dimension(GetDimensionRef(maxRowIndex, props.Count)), cancellationToken).ConfigureAwait(false); - } - else if (_configuration.FastMode) - { - dimensionPlaceholderPostition = await WriteDimensionPlaceholderAsync(writer).ConfigureAwait(false); - } + await writer.WriteAsync(WorksheetXml.StartWorksheetWithRelationship, cancellationToken).ConfigureAwait(false); - //sheet view - await writer.WriteAsync(GetSheetViews(), cancellationToken).ConfigureAwait(false); + long dimensionPlaceholderPostition = 0; - //cols:width - ExcelWidthCollection? widths = null; - long columnWidthsPlaceholderPosition = 0; - if (_configuration.EnableAutoWidth) - { - columnWidthsPlaceholderPosition = await WriteColumnWidthPlaceholdersAsync(writer, maxColumnIndex, cancellationToken).ConfigureAwait(false); - widths = new ExcelWidthCollection(_configuration.MinWidth, _configuration.MaxWidth, props); - } - else - { - await WriteColumnsWidthsAsync(writer, ExcelColumnWidth.FromProps(props), cancellationToken).ConfigureAwait(false); - } + // We can write the dimensions directly if the row count is known + if (isKnownCount) + { + maxRowIndex = _printHeader ? count + 1 : count; + await writer.WriteAsync(WorksheetXml.Dimension(GetDimensionRef(maxRowIndex, props.Count)), cancellationToken).ConfigureAwait(false); + } + else if (_configuration.FastMode) + { + dimensionPlaceholderPostition = await WriteDimensionPlaceholderAsync(writer).ConfigureAwait(false); + } - //header - await writer.WriteAsync(WorksheetXml.StartSheetData, cancellationToken).ConfigureAwait(false); - var currentRowIndex = 0; - if (_printHeader) - { - await PrintHeaderAsync(writer, props!, cancellationToken).ConfigureAwait(false); - currentRowIndex++; - } + //sheet view + await writer.WriteAsync(GetSheetViews(), cancellationToken).ConfigureAwait(false); - if (writeAdapter is not null) - { - foreach (var row in writeAdapter.GetRows(props, cancellationToken)) + //cols:width + ExcelWidthCollection? widths = null; + long columnWidthsPlaceholderPosition = 0; + if (_configuration.EnableAutoWidth) { - cancellationToken.ThrowIfCancellationRequested(); + columnWidthsPlaceholderPosition = await WriteColumnWidthPlaceholdersAsync(writer, maxColumnIndex, cancellationToken).ConfigureAwait(false); + widths = new ExcelWidthCollection(_configuration.MinWidth, _configuration.MaxWidth, props); + } + else + { + await WriteColumnsWidthsAsync(writer, ExcelColumnWidth.FromProps(props), cancellationToken).ConfigureAwait(false); + } - await writer.WriteAsync(WorksheetXml.StartRow(++currentRowIndex), cancellationToken).ConfigureAwait(false); - foreach (var cellValue in row) + //header + await writer.WriteAsync(WorksheetXml.StartSheetData, cancellationToken).ConfigureAwait(false); + var currentRowIndex = 0; + if (_printHeader) + { + await PrintHeaderAsync(writer, props!, cancellationToken).ConfigureAwait(false); + currentRowIndex++; + } + + if (writeAdapter is not null) + { + foreach (var row in writeAdapter.GetRows(props, cancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); - await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Prop, widths).ConfigureAwait(false); - progress?.Report(1); + + await writer.WriteAsync(WorksheetXml.StartRow(++currentRowIndex), cancellationToken).ConfigureAwait(false); + foreach (var cellValue in row) + { + cancellationToken.ThrowIfCancellationRequested(); + await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Prop, widths).ConfigureAwait(false); + progress?.Report(1); + } + await writer.WriteAsync(WorksheetXml.EndRow, cancellationToken).ConfigureAwait(false); } - await writer.WriteAsync(WorksheetXml.EndRow, cancellationToken).ConfigureAwait(false); } - } - else - { -#if !SYNC_ONLY - await foreach (var row in asyncWriteAdapter.GetRowsAsync(props, cancellationToken).ConfigureAwait(false)) + else { - cancellationToken.ThrowIfCancellationRequested(); - await writer.WriteAsync(WorksheetXml.StartRow(++currentRowIndex), cancellationToken).ConfigureAwait(false); - - await foreach (var cellValue in row.ConfigureAwait(false).WithCancellation(cancellationToken)) +#if !SYNC_ONLY + await foreach (var row in asyncWriteAdapter!.GetRowsAsync(props, cancellationToken).ConfigureAwait(false)) { - await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Prop, widths).ConfigureAwait(false); - progress?.Report(1); + cancellationToken.ThrowIfCancellationRequested(); + await writer.WriteAsync(WorksheetXml.StartRow(++currentRowIndex), cancellationToken).ConfigureAwait(false); + + await foreach (var cellValue in row.ConfigureAwait(false).WithCancellation(cancellationToken)) + { + await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Prop, widths).ConfigureAwait(false); + progress?.Report(1); + } + await writer.WriteAsync(WorksheetXml.EndRow, cancellationToken).ConfigureAwait(false); } - await writer.WriteAsync(WorksheetXml.EndRow, cancellationToken).ConfigureAwait(false); - } #endif - } - maxRowIndex = currentRowIndex; + } + maxRowIndex = currentRowIndex; - await writer.WriteAsync(WorksheetXml.EndSheetData, cancellationToken).ConfigureAwait(false); + await writer.WriteAsync(WorksheetXml.EndSheetData, cancellationToken).ConfigureAwait(false); - if (_configuration.AutoFilter) - { - await writer.WriteAsync(WorksheetXml.Autofilter(GetDimensionRef(maxRowIndex, maxColumnIndex)), cancellationToken).ConfigureAwait(false); - } + if (_configuration.AutoFilter) + { + await writer.WriteAsync(WorksheetXml.Autofilter(GetDimensionRef(maxRowIndex, maxColumnIndex)), cancellationToken).ConfigureAwait(false); + } - await writer.WriteAsync(WorksheetXml.Drawing(_currentSheetIndex), cancellationToken).ConfigureAwait(false); - await writer.WriteAsync(WorksheetXml.EndWorksheet, cancellationToken).ConfigureAwait(false); + await writer.WriteAsync(WorksheetXml.Drawing(_currentSheetIndex), cancellationToken).ConfigureAwait(false); + await writer.WriteAsync(WorksheetXml.EndWorksheet, cancellationToken).ConfigureAwait(false); - if (_configuration.FastMode && dimensionPlaceholderPostition != 0) - { - await WriteDimensionAsync(writer, maxRowIndex, maxColumnIndex, dimensionPlaceholderPostition).ConfigureAwait(false); + if (_configuration.FastMode && dimensionPlaceholderPostition != 0) + { + await WriteDimensionAsync(writer, maxRowIndex, maxColumnIndex, dimensionPlaceholderPostition).ConfigureAwait(false); + } + if (_configuration.EnableAutoWidth) + { + await OverwriteColumnWidthPlaceholdersAsync(writer, columnWidthsPlaceholderPosition, widths?.Columns, cancellationToken).ConfigureAwait(false); + } + + if (_printHeader) + maxRowIndex--; + + return maxRowIndex; } - if (_configuration.EnableAutoWidth) + finally { - await OverwriteColumnWidthPlaceholdersAsync(writer, columnWidthsPlaceholderPosition, widths?.Columns, cancellationToken).ConfigureAwait(false); +#if !SYNC_ONLY + if (asyncWriteAdapter is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync().ConfigureAwait(false); + } +#endif } - - if (_printHeader) - maxRowIndex--; - - return maxRowIndex; } [CreateSyncVersion] @@ -390,7 +401,7 @@ private static async Task OverwriteColumnWidthPlaceholdersAsync(SafeStreamWriter private static async Task WriteColumnsWidthsAsync(SafeStreamWriter writer, IEnumerable? columnWidths, CancellationToken cancellationToken = default) { var hasWrittenStart = false; - + columnWidths ??= []; foreach (var column in columnWidths) { @@ -610,9 +621,9 @@ private async Task InsertContentTypesXmlAsync(CancellationToken cancellationToke #if NET5_0_OR_GREATER #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task #if NET10_0_OR_GREATER - await using var stream = await contentTypesZipEntry.OpenAsync(cancellationToken).ConfigureAwait(false); + await using var stream = await contentTypesZipEntry.OpenAsync(cancellationToken).ConfigureAwait(false); #else - await using var stream = contentTypesZipEntry.Open(); + await using var stream = contentTypesZipEntry.Open(); #endif #pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task #else diff --git a/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs b/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs index a48b25ac..0f16f9dc 100644 --- a/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs +++ b/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs @@ -1,7 +1,8 @@ namespace MiniExcelLib.Core.WriteAdapters; -internal class AsyncEnumerableWriteAdapter(IAsyncEnumerable values, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync +internal class AsyncEnumerableWriteAdapter(IAsyncEnumerable values, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync, IAsyncDisposable { + private bool _disposed = false; private readonly IAsyncEnumerable _values = values; private readonly MiniExcelBaseConfiguration _configuration = configuration; private IAsyncEnumerator? _enumerator; @@ -70,4 +71,22 @@ public static async IAsyncEnumerable GetRowValuesAsync(T currentV column++; } } + + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore().ConfigureAwait(false); + } + + protected virtual async ValueTask DisposeAsyncCore() + { + if (!_disposed) + { + if (_enumerator != null) + { + await _enumerator.DisposeAsync().ConfigureAwait(false); + } + _disposed = true; + } + } + } \ No newline at end of file diff --git a/src/MiniExcel.Core/WriteAdapters/MiniExcelWriteAdapterFactory.cs b/src/MiniExcel.Core/WriteAdapters/MiniExcelWriteAdapterFactory.cs index 09185b8d..21e8cf5f 100644 --- a/src/MiniExcel.Core/WriteAdapters/MiniExcelWriteAdapterFactory.cs +++ b/src/MiniExcel.Core/WriteAdapters/MiniExcelWriteAdapterFactory.cs @@ -7,13 +7,13 @@ public static class MiniExcelWriteAdapterFactory public static bool TryGetAsyncWriteAdapter(object values, MiniExcelBaseConfiguration configuration, out IMiniExcelWriteAdapterAsync? writeAdapter) { writeAdapter = null; - if (values.GetType().IsAsyncEnumerable(out var genericArgument)) + if (values.GetType().IsAsyncEnumerable(out var genericArgument) && genericArgument != null) { - var writeAdapterType = typeof(AsyncEnumerableWriteAdapter<>).MakeGenericType(genericArgument); - writeAdapter = (IMiniExcelWriteAdapterAsync)Activator.CreateInstance(writeAdapterType, values, configuration); + var writeAdapterType = typeof(AsyncEnumerableWriteAdapter<>).MakeGenericType(genericArgument!); + writeAdapter = Activator.CreateInstance(writeAdapterType, values, configuration) as IMiniExcelWriteAdapterAsync; return true; } - + if (values is IMiniExcelDataReader miniExcelDataReader) { writeAdapter = new MiniExcelDataReaderWriteAdapter(miniExcelDataReader, configuration); From 5576335bc132ac9b5e01d2910025c6bc6bc28672 Mon Sep 17 00:00:00 2001 From: izanhzh Date: Sat, 22 Nov 2025 11:17:29 +0800 Subject: [PATCH 2/4] Minor adjustments --- .../WriteAdapters/MiniExcelWriteAdapterFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MiniExcel.Core/WriteAdapters/MiniExcelWriteAdapterFactory.cs b/src/MiniExcel.Core/WriteAdapters/MiniExcelWriteAdapterFactory.cs index 21e8cf5f..9f6fa87c 100644 --- a/src/MiniExcel.Core/WriteAdapters/MiniExcelWriteAdapterFactory.cs +++ b/src/MiniExcel.Core/WriteAdapters/MiniExcelWriteAdapterFactory.cs @@ -7,7 +7,7 @@ public static class MiniExcelWriteAdapterFactory public static bool TryGetAsyncWriteAdapter(object values, MiniExcelBaseConfiguration configuration, out IMiniExcelWriteAdapterAsync? writeAdapter) { writeAdapter = null; - if (values.GetType().IsAsyncEnumerable(out var genericArgument) && genericArgument != null) + if (values.GetType().IsAsyncEnumerable(out var genericArgument)) { var writeAdapterType = typeof(AsyncEnumerableWriteAdapter<>).MakeGenericType(genericArgument!); writeAdapter = Activator.CreateInstance(writeAdapterType, values, configuration) as IMiniExcelWriteAdapterAsync; From 331d39d718d4aeec0167b50b29cab198ab30db16 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 22 Nov 2025 15:37:30 +0100 Subject: [PATCH 3/4] Added disposing logic to csvwriter and some other minor changes --- .../AsyncEnumerableWriteAdapter.cs | 32 ++-- src/MiniExcel.Csv/CsvWriter.cs | 154 ++++++++++-------- 2 files changed, 96 insertions(+), 90 deletions(-) diff --git a/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs b/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs index 0f16f9dc..3cb76af6 100644 --- a/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs +++ b/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs @@ -1,13 +1,15 @@ namespace MiniExcelLib.Core.WriteAdapters; -internal class AsyncEnumerableWriteAdapter(IAsyncEnumerable values, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync, IAsyncDisposable +internal sealed class AsyncEnumerableWriteAdapter(IAsyncEnumerable values, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync, IAsyncDisposable { - private bool _disposed = false; private readonly IAsyncEnumerable _values = values; private readonly MiniExcelBaseConfiguration _configuration = configuration; + private IAsyncEnumerator? _enumerator; private bool _empty; + private bool _disposed = false; + public async Task?> GetColumnsAsync() { if (CustomPropertyHelper.TryGetTypeColumnInfo(typeof(T), _configuration, out var props)) @@ -21,6 +23,7 @@ internal class AsyncEnumerableWriteAdapter(IAsyncEnumerable values, MiniEx _empty = true; return null; } + return CustomPropertyHelper.GetColumnInfoFromValue(_enumerator.Current, _configuration); } @@ -44,22 +47,21 @@ public async IAsyncEnumerable> GetRowsAsync(List { cancellationToken.ThrowIfCancellationRequested(); yield return GetRowValuesAsync(_enumerator.Current, props); - - } while (await _enumerator.MoveNextAsync().ConfigureAwait(false)); + } + while (await _enumerator.MoveNextAsync().ConfigureAwait(false)); } #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - public static async IAsyncEnumerable GetRowValuesAsync(T currentValue, List props) -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + private static async IAsyncEnumerable GetRowValuesAsync(T currentValue, List props) +#pragma warning restore CS1998 { - var column = 1; + var column = 0; foreach (var prop in props) { + column++; + if (prop is null) - { - column++; continue; - } yield return currentValue switch { @@ -67,26 +69,18 @@ public static async IAsyncEnumerable GetRowValuesAsync(T currentV IDictionary dictionary => new CellWriteInfo(dictionary[prop.Key], column, prop), _ => new CellWriteInfo(prop.Property.GetValue(currentValue), column, prop) }; - - column++; } } public async ValueTask DisposeAsync() - { - await DisposeAsyncCore().ConfigureAwait(false); - } - - protected virtual async ValueTask DisposeAsyncCore() { if (!_disposed) { - if (_enumerator != null) + if (_enumerator is not null) { await _enumerator.DisposeAsync().ConfigureAwait(false); } _disposed = true; } } - } \ No newline at end of file diff --git a/src/MiniExcel.Csv/CsvWriter.cs b/src/MiniExcel.Csv/CsvWriter.cs index 49d5abe6..5ad24a6e 100644 --- a/src/MiniExcel.Csv/CsvWriter.cs +++ b/src/MiniExcel.Csv/CsvWriter.cs @@ -47,61 +47,35 @@ private async Task WriteValuesAsync(StreamWriter writer, object values, str { writeAdapter = MiniExcelWriteAdapterFactory.GetWriteAdapter(values, _configuration); } - + + try + { #if SYNC_ONLY - var props = writeAdapter?.GetColumns(); + var props = writeAdapter?.GetColumns(); #else - var props = writeAdapter is not null - ? writeAdapter.GetColumns() - : await asyncWriteAdapter!.GetColumnsAsync().ConfigureAwait(false); + var props = writeAdapter is not null + ? writeAdapter.GetColumns() + : await asyncWriteAdapter!.GetColumnsAsync().ConfigureAwait(false); #endif - - if (props is null) - { - await _writer.WriteAsync(_configuration.NewLine + + if (props is null) + { + await _writer.WriteAsync(_configuration.NewLine #if NET5_0_OR_GREATER - .AsMemory(), cancellationToken + .AsMemory(), cancellationToken #endif - ).ConfigureAwait(false); - await _writer.FlushAsync( + ).ConfigureAwait(false); + await _writer.FlushAsync( #if NET8_0_OR_GREATER - cancellationToken + cancellationToken #endif - ).ConfigureAwait(false); - return 0; - } - - if (_printHeader) - { - await _writer.WriteAsync(GetHeader(props) -#if NET5_0_OR_GREATER - .AsMemory(), cancellationToken -#endif - ).ConfigureAwait(false); - await _writer.WriteAsync(newLine -#if NET5_0_OR_GREATER - .AsMemory(), cancellationToken -#endif - ).ConfigureAwait(false); - } - - var rowBuilder = new StringBuilder(); - var rowsWritten = 0; - - if (writeAdapter is not null) - { - foreach (var row in writeAdapter.GetRows(props, cancellationToken)) + ).ConfigureAwait(false); + return 0; + } + + if (_printHeader) { - rowBuilder.Clear(); - foreach (var column in row) - { - cancellationToken.ThrowIfCancellationRequested(); - AppendColumn(rowBuilder, column); - progress?.Report(1); - } - - RemoveTrailingSeparator(rowBuilder); - await _writer.WriteAsync(rowBuilder.ToString() + await _writer.WriteAsync(GetHeader(props) #if NET5_0_OR_GREATER .AsMemory(), cancellationToken #endif @@ -111,41 +85,79 @@ await _writer.WriteAsync(newLine .AsMemory(), cancellationToken #endif ).ConfigureAwait(false); - - rowsWritten++; } - } - else - { -#if !SYNC_ONLY - await foreach (var row in asyncWriteAdapter!.GetRowsAsync(props, cancellationToken).ConfigureAwait(false)) + + var rowBuilder = new StringBuilder(); + var rowsWritten = 0; + + if (writeAdapter is not null) { - cancellationToken.ThrowIfCancellationRequested(); - rowBuilder.Clear(); - - await foreach (var column in row.WithCancellation(cancellationToken).ConfigureAwait(false)) + foreach (var row in writeAdapter.GetRows(props, cancellationToken)) { - AppendColumn(rowBuilder, column); - progress?.Report(1); + rowBuilder.Clear(); + foreach (var column in row) + { + cancellationToken.ThrowIfCancellationRequested(); + AppendColumn(rowBuilder, column); + progress?.Report(1); + } + + RemoveTrailingSeparator(rowBuilder); + await _writer.WriteAsync(rowBuilder.ToString() +#if NET5_0_OR_GREATER + .AsMemory(), cancellationToken +#endif + ).ConfigureAwait(false); + await _writer.WriteAsync(newLine +#if NET5_0_OR_GREATER + .AsMemory(), cancellationToken +#endif + ).ConfigureAwait(false); + + rowsWritten++; } - - RemoveTrailingSeparator(rowBuilder); - await _writer.WriteAsync(rowBuilder.ToString() + } + else + { +#if !SYNC_ONLY + await foreach (var row in asyncWriteAdapter!.GetRowsAsync(props, cancellationToken).ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); + rowBuilder.Clear(); + + await foreach (var column in row.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + AppendColumn(rowBuilder, column); + progress?.Report(1); + } + + RemoveTrailingSeparator(rowBuilder); + await _writer.WriteAsync(rowBuilder.ToString() #if NET5_0_OR_GREATER - .AsMemory(), cancellationToken + .AsMemory(), cancellationToken #endif - ).ConfigureAwait(false); - await _writer.WriteAsync(newLine + ).ConfigureAwait(false); + await _writer.WriteAsync(newLine #if NET5_0_OR_GREATER - .AsMemory(), cancellationToken + .AsMemory(), cancellationToken #endif - ).ConfigureAwait(false); - - rowsWritten++; + ).ConfigureAwait(false); + + rowsWritten++; + } +#endif + } + return rowsWritten; + } + finally + { +#if !SYNC_ONLY + if (asyncWriteAdapter is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync().ConfigureAwait(false); } #endif } - return rowsWritten; } [CreateSyncVersion] From 7fbe7356726b3700a35687f94d24d8dc99f5f06d Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 22 Nov 2025 15:37:43 +0100 Subject: [PATCH 4/4] Updated gitignore --- .gitignore | 3 ++- tests/data/xlsx/Test_EnableWriteFilePath.xlsx | Bin 4835 -> 0 bytes 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 tests/data/xlsx/Test_EnableWriteFilePath.xlsx diff --git a/.gitignore b/.gitignore index c30278fc..f064017d 100644 --- a/.gitignore +++ b/.gitignore @@ -401,4 +401,5 @@ FodyWeavers.xsd /BenchmarkDotNet.Artifacts /TestTemplate /tests/MiniExcel.Tests/TemplateOptimization -/tests/data/xlsx/Test_EnableWriteFilePath.xlsx \ No newline at end of file +/tests/data +samples/xlsx/Test_EnableWriteFilePath.xlsx diff --git a/tests/data/xlsx/Test_EnableWriteFilePath.xlsx b/tests/data/xlsx/Test_EnableWriteFilePath.xlsx deleted file mode 100644 index 6038d0927cdeceffb063f5196f69c9333792c46b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4835 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