Skip to content

Commit d7c5b7f

Browse files
Fix #905: Release the internally created IAsyncEnumerator (#906)
* Release the internally created IAsyncEnumerator * Minor adjustments * Added disposing logic to csvwriter and some other minor changes * Updated gitignore --------- Co-authored-by: Michele Bastione <[email protected]>
1 parent fa0ec7d commit d7c5b7f

File tree

6 files changed

+224
-187
lines changed

6 files changed

+224
-187
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,4 +401,5 @@ FodyWeavers.xsd
401401
/BenchmarkDotNet.Artifacts
402402
/TestTemplate
403403
/tests/MiniExcel.Tests/TemplateOptimization
404-
/tests/data/xlsx/Test_EnableWriteFilePath.xlsx
404+
/tests/data
405+
samples/xlsx/Test_EnableWriteFilePath.xlsx

src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs

Lines changed: 113 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
1-
using System.ComponentModel;
2-
using System.Xml.Linq;
31
using MiniExcelLib.Core.OpenXml.Constants;
42
using MiniExcelLib.Core.OpenXml.Models;
53
using MiniExcelLib.Core.OpenXml.Styles.Builder;
64
using MiniExcelLib.Core.OpenXml.Zip;
75
using MiniExcelLib.Core.WriteAdapters;
6+
using System.ComponentModel;
7+
using System.Xml.Linq;
88

99
namespace MiniExcelLib.Core.OpenXml;
1010

1111
internal partial class OpenXmlWriter : IMiniExcelWriter
1212
{
1313
private static readonly UTF8Encoding Utf8WithBom = new(true);
14-
14+
1515
private readonly MiniExcelZipArchive _archive;
1616
private readonly OpenXmlConfiguration _configuration;
1717
private readonly Stream _stream;
1818
private readonly List<SheetDto> _sheets = [];
1919
private readonly List<FileDto> _files = [];
20-
20+
2121
private readonly string? _defaultSheetName;
2222
private readonly bool _printHeader;
2323
private readonly object? _value;
@@ -41,16 +41,16 @@ internal OpenXmlWriter(Stream stream, object? value, string? sheetName, IMiniExc
4141
_printHeader = printHeader;
4242
_defaultSheetName = sheetName;
4343
}
44-
44+
4545
[CreateSyncVersion]
46-
internal static Task<OpenXmlWriter> CreateAsync(Stream stream, object? value, string? sheetName, bool printHeader, IMiniExcelConfiguration? configuration, CancellationToken cancellationToken = default)
46+
internal static Task<OpenXmlWriter> CreateAsync(Stream stream, object? value, string? sheetName, bool printHeader, IMiniExcelConfiguration? configuration, CancellationToken cancellationToken = default)
4747
{
4848
ThrowHelper.ThrowIfInvalidSheetName(sheetName);
49-
49+
5050
var writer = new OpenXmlWriter(stream, value, sheetName, configuration, printHeader);
5151
return Task.FromResult(writer);
5252
}
53-
53+
5454
[CreateSyncVersion]
5555
public async Task<int[]> SaveAsAsync(IProgress<int>? progress = null, CancellationToken cancellationToken = default)
5656
{
@@ -190,7 +190,7 @@ private async Task<int> CreateSheetXmlAsync(object? values, string sheetPath, IP
190190
using var zipStream = entry.Open();
191191
#endif
192192
using var writer = new SafeStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize);
193-
193+
194194
if (values is null)
195195
{
196196
await WriteEmptySheetAsync(writer).ConfigureAwait(false);
@@ -241,125 +241,136 @@ private async Task<int> WriteValuesAsync(SafeStreamWriter writer, object values,
241241
{
242242
writeAdapter = MiniExcelWriteAdapterFactory.GetWriteAdapter(values, _configuration);
243243
}
244+
try
245+
{
246+
var count = 0;
247+
var isKnownCount = writeAdapter is not null && writeAdapter.TryGetKnownCount(out count);
244248

245-
var count = 0;
246-
var isKnownCount = writeAdapter is not null && writeAdapter.TryGetKnownCount(out count);
247-
248249
#if SYNC_ONLY
249250
var props = writeAdapter?.GetColumns();
250251
#else
251-
var props = writeAdapter is not null
252-
? writeAdapter.GetColumns()
253-
: await (asyncWriteAdapter?.GetColumnsAsync() ?? Task.FromResult<List<MiniExcelColumnInfo>?>(null)).ConfigureAwait(false);
252+
var props = writeAdapter is not null
253+
? writeAdapter.GetColumns()
254+
: await (asyncWriteAdapter?.GetColumnsAsync() ?? Task.FromResult<List<MiniExcelColumnInfo>?>(null)).ConfigureAwait(false);
254255
#endif
255-
256-
if (props is null)
257-
{
258-
await WriteEmptySheetAsync(writer).ConfigureAwait(false);
259-
return 0;
260-
}
261-
262-
int maxRowIndex;
263-
var maxColumnIndex = props.Count(x => x is { ExcelIgnore: false });
264256

265-
await writer.WriteAsync(WorksheetXml.StartWorksheetWithRelationship, cancellationToken).ConfigureAwait(false);
257+
if (props is null)
258+
{
259+
await WriteEmptySheetAsync(writer).ConfigureAwait(false);
260+
return 0;
261+
}
266262

267-
long dimensionPlaceholderPostition = 0;
263+
int maxRowIndex;
264+
var maxColumnIndex = props.Count(x => x is { ExcelIgnore: false });
268265

269-
// We can write the dimensions directly if the row count is known
270-
if (isKnownCount)
271-
{
272-
maxRowIndex = _printHeader ? count + 1 : count;
273-
await writer.WriteAsync(WorksheetXml.Dimension(GetDimensionRef(maxRowIndex, props.Count)), cancellationToken).ConfigureAwait(false);
274-
}
275-
else if (_configuration.FastMode)
276-
{
277-
dimensionPlaceholderPostition = await WriteDimensionPlaceholderAsync(writer).ConfigureAwait(false);
278-
}
266+
await writer.WriteAsync(WorksheetXml.StartWorksheetWithRelationship, cancellationToken).ConfigureAwait(false);
279267

280-
//sheet view
281-
await writer.WriteAsync(GetSheetViews(), cancellationToken).ConfigureAwait(false);
268+
long dimensionPlaceholderPostition = 0;
282269

283-
//cols:width
284-
ExcelWidthCollection? widths = null;
285-
long columnWidthsPlaceholderPosition = 0;
286-
if (_configuration.EnableAutoWidth)
287-
{
288-
columnWidthsPlaceholderPosition = await WriteColumnWidthPlaceholdersAsync(writer, maxColumnIndex, cancellationToken).ConfigureAwait(false);
289-
widths = new ExcelWidthCollection(_configuration.MinWidth, _configuration.MaxWidth, props);
290-
}
291-
else
292-
{
293-
await WriteColumnsWidthsAsync(writer, ExcelColumnWidth.FromProps(props), cancellationToken).ConfigureAwait(false);
294-
}
270+
// We can write the dimensions directly if the row count is known
271+
if (isKnownCount)
272+
{
273+
maxRowIndex = _printHeader ? count + 1 : count;
274+
await writer.WriteAsync(WorksheetXml.Dimension(GetDimensionRef(maxRowIndex, props.Count)), cancellationToken).ConfigureAwait(false);
275+
}
276+
else if (_configuration.FastMode)
277+
{
278+
dimensionPlaceholderPostition = await WriteDimensionPlaceholderAsync(writer).ConfigureAwait(false);
279+
}
295280

296-
//header
297-
await writer.WriteAsync(WorksheetXml.StartSheetData, cancellationToken).ConfigureAwait(false);
298-
var currentRowIndex = 0;
299-
if (_printHeader)
300-
{
301-
await PrintHeaderAsync(writer, props!, cancellationToken).ConfigureAwait(false);
302-
currentRowIndex++;
303-
}
281+
//sheet view
282+
await writer.WriteAsync(GetSheetViews(), cancellationToken).ConfigureAwait(false);
304283

305-
if (writeAdapter is not null)
306-
{
307-
foreach (var row in writeAdapter.GetRows(props, cancellationToken))
284+
//cols:width
285+
ExcelWidthCollection? widths = null;
286+
long columnWidthsPlaceholderPosition = 0;
287+
if (_configuration.EnableAutoWidth)
308288
{
309-
cancellationToken.ThrowIfCancellationRequested();
289+
columnWidthsPlaceholderPosition = await WriteColumnWidthPlaceholdersAsync(writer, maxColumnIndex, cancellationToken).ConfigureAwait(false);
290+
widths = new ExcelWidthCollection(_configuration.MinWidth, _configuration.MaxWidth, props);
291+
}
292+
else
293+
{
294+
await WriteColumnsWidthsAsync(writer, ExcelColumnWidth.FromProps(props), cancellationToken).ConfigureAwait(false);
295+
}
310296

311-
await writer.WriteAsync(WorksheetXml.StartRow(++currentRowIndex), cancellationToken).ConfigureAwait(false);
312-
foreach (var cellValue in row)
297+
//header
298+
await writer.WriteAsync(WorksheetXml.StartSheetData, cancellationToken).ConfigureAwait(false);
299+
var currentRowIndex = 0;
300+
if (_printHeader)
301+
{
302+
await PrintHeaderAsync(writer, props!, cancellationToken).ConfigureAwait(false);
303+
currentRowIndex++;
304+
}
305+
306+
if (writeAdapter is not null)
307+
{
308+
foreach (var row in writeAdapter.GetRows(props, cancellationToken))
313309
{
314310
cancellationToken.ThrowIfCancellationRequested();
315-
await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Prop, widths).ConfigureAwait(false);
316-
progress?.Report(1);
311+
312+
await writer.WriteAsync(WorksheetXml.StartRow(++currentRowIndex), cancellationToken).ConfigureAwait(false);
313+
foreach (var cellValue in row)
314+
{
315+
cancellationToken.ThrowIfCancellationRequested();
316+
await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Prop, widths).ConfigureAwait(false);
317+
progress?.Report(1);
318+
}
319+
await writer.WriteAsync(WorksheetXml.EndRow, cancellationToken).ConfigureAwait(false);
317320
}
318-
await writer.WriteAsync(WorksheetXml.EndRow, cancellationToken).ConfigureAwait(false);
319321
}
320-
}
321-
else
322-
{
323-
#if !SYNC_ONLY
324-
await foreach (var row in asyncWriteAdapter.GetRowsAsync(props, cancellationToken).ConfigureAwait(false))
322+
else
325323
{
326-
cancellationToken.ThrowIfCancellationRequested();
327-
await writer.WriteAsync(WorksheetXml.StartRow(++currentRowIndex), cancellationToken).ConfigureAwait(false);
328-
329-
await foreach (var cellValue in row.ConfigureAwait(false).WithCancellation(cancellationToken))
324+
#if !SYNC_ONLY
325+
await foreach (var row in asyncWriteAdapter!.GetRowsAsync(props, cancellationToken).ConfigureAwait(false))
330326
{
331-
await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Prop, widths).ConfigureAwait(false);
332-
progress?.Report(1);
327+
cancellationToken.ThrowIfCancellationRequested();
328+
await writer.WriteAsync(WorksheetXml.StartRow(++currentRowIndex), cancellationToken).ConfigureAwait(false);
329+
330+
await foreach (var cellValue in row.ConfigureAwait(false).WithCancellation(cancellationToken))
331+
{
332+
await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Prop, widths).ConfigureAwait(false);
333+
progress?.Report(1);
334+
}
335+
await writer.WriteAsync(WorksheetXml.EndRow, cancellationToken).ConfigureAwait(false);
333336
}
334-
await writer.WriteAsync(WorksheetXml.EndRow, cancellationToken).ConfigureAwait(false);
335-
}
336337
#endif
337-
}
338-
maxRowIndex = currentRowIndex;
338+
}
339+
maxRowIndex = currentRowIndex;
339340

340-
await writer.WriteAsync(WorksheetXml.EndSheetData, cancellationToken).ConfigureAwait(false);
341+
await writer.WriteAsync(WorksheetXml.EndSheetData, cancellationToken).ConfigureAwait(false);
341342

342-
if (_configuration.AutoFilter)
343-
{
344-
await writer.WriteAsync(WorksheetXml.Autofilter(GetDimensionRef(maxRowIndex, maxColumnIndex)), cancellationToken).ConfigureAwait(false);
345-
}
343+
if (_configuration.AutoFilter)
344+
{
345+
await writer.WriteAsync(WorksheetXml.Autofilter(GetDimensionRef(maxRowIndex, maxColumnIndex)), cancellationToken).ConfigureAwait(false);
346+
}
346347

347-
await writer.WriteAsync(WorksheetXml.Drawing(_currentSheetIndex), cancellationToken).ConfigureAwait(false);
348-
await writer.WriteAsync(WorksheetXml.EndWorksheet, cancellationToken).ConfigureAwait(false);
348+
await writer.WriteAsync(WorksheetXml.Drawing(_currentSheetIndex), cancellationToken).ConfigureAwait(false);
349+
await writer.WriteAsync(WorksheetXml.EndWorksheet, cancellationToken).ConfigureAwait(false);
349350

350-
if (_configuration.FastMode && dimensionPlaceholderPostition != 0)
351-
{
352-
await WriteDimensionAsync(writer, maxRowIndex, maxColumnIndex, dimensionPlaceholderPostition).ConfigureAwait(false);
351+
if (_configuration.FastMode && dimensionPlaceholderPostition != 0)
352+
{
353+
await WriteDimensionAsync(writer, maxRowIndex, maxColumnIndex, dimensionPlaceholderPostition).ConfigureAwait(false);
354+
}
355+
if (_configuration.EnableAutoWidth)
356+
{
357+
await OverwriteColumnWidthPlaceholdersAsync(writer, columnWidthsPlaceholderPosition, widths?.Columns, cancellationToken).ConfigureAwait(false);
358+
}
359+
360+
if (_printHeader)
361+
maxRowIndex--;
362+
363+
return maxRowIndex;
353364
}
354-
if (_configuration.EnableAutoWidth)
365+
finally
355366
{
356-
await OverwriteColumnWidthPlaceholdersAsync(writer, columnWidthsPlaceholderPosition, widths?.Columns, cancellationToken).ConfigureAwait(false);
367+
#if !SYNC_ONLY
368+
if (asyncWriteAdapter is IAsyncDisposable asyncDisposable)
369+
{
370+
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
371+
}
372+
#endif
357373
}
358-
359-
if (_printHeader)
360-
maxRowIndex--;
361-
362-
return maxRowIndex;
363374
}
364375

365376
[CreateSyncVersion]
@@ -390,7 +401,7 @@ private static async Task OverwriteColumnWidthPlaceholdersAsync(SafeStreamWriter
390401
private static async Task WriteColumnsWidthsAsync(SafeStreamWriter writer, IEnumerable<ExcelColumnWidth>? columnWidths, CancellationToken cancellationToken = default)
391402
{
392403
var hasWrittenStart = false;
393-
404+
394405
columnWidths ??= [];
395406
foreach (var column in columnWidths)
396407
{
@@ -610,9 +621,9 @@ private async Task InsertContentTypesXmlAsync(CancellationToken cancellationToke
610621
#if NET5_0_OR_GREATER
611622
#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task
612623
#if NET10_0_OR_GREATER
613-
await using var stream = await contentTypesZipEntry.OpenAsync(cancellationToken).ConfigureAwait(false);
624+
await using var stream = await contentTypesZipEntry.OpenAsync(cancellationToken).ConfigureAwait(false);
614625
#else
615-
await using var stream = contentTypesZipEntry.Open();
626+
await using var stream = contentTypesZipEntry.Open();
616627
#endif
617628
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
618629
#else

src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
namespace MiniExcelLib.Core.WriteAdapters;
22

3-
internal class AsyncEnumerableWriteAdapter<T>(IAsyncEnumerable<T> values, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync
3+
internal sealed class AsyncEnumerableWriteAdapter<T>(IAsyncEnumerable<T> values, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync, IAsyncDisposable
44
{
55
private readonly IAsyncEnumerable<T> _values = values;
66
private readonly MiniExcelBaseConfiguration _configuration = configuration;
7+
78
private IAsyncEnumerator<T>? _enumerator;
89
private bool _empty;
10+
private bool _disposed = false;
911

12+
1013
public async Task<List<MiniExcelColumnInfo>?> GetColumnsAsync()
1114
{
1215
if (CustomPropertyHelper.TryGetTypeColumnInfo(typeof(T), _configuration, out var props))
@@ -20,6 +23,7 @@ internal class AsyncEnumerableWriteAdapter<T>(IAsyncEnumerable<T> values, MiniEx
2023
_empty = true;
2124
return null;
2225
}
26+
2327
return CustomPropertyHelper.GetColumnInfoFromValue(_enumerator.Current, _configuration);
2428
}
2529

@@ -43,31 +47,40 @@ public async IAsyncEnumerable<IAsyncEnumerable<CellWriteInfo>> GetRowsAsync(List
4347
{
4448
cancellationToken.ThrowIfCancellationRequested();
4549
yield return GetRowValuesAsync(_enumerator.Current, props);
46-
47-
} while (await _enumerator.MoveNextAsync().ConfigureAwait(false));
50+
}
51+
while (await _enumerator.MoveNextAsync().ConfigureAwait(false));
4852
}
4953

5054
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
51-
public static async IAsyncEnumerable<CellWriteInfo> GetRowValuesAsync(T currentValue, List<MiniExcelColumnInfo> props)
52-
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
55+
private static async IAsyncEnumerable<CellWriteInfo> GetRowValuesAsync(T currentValue, List<MiniExcelColumnInfo> props)
56+
#pragma warning restore CS1998
5357
{
54-
var column = 1;
58+
var column = 0;
5559
foreach (var prop in props)
5660
{
61+
column++;
62+
5763
if (prop is null)
58-
{
59-
column++;
6064
continue;
61-
}
6265

6366
yield return currentValue switch
6467
{
6568
IDictionary<string, object> genericDictionary => new CellWriteInfo(genericDictionary[prop.Key.ToString()], column, prop),
6669
IDictionary dictionary => new CellWriteInfo(dictionary[prop.Key], column, prop),
6770
_ => new CellWriteInfo(prop.Property.GetValue(currentValue), column, prop)
6871
};
72+
}
73+
}
6974

70-
column++;
75+
public async ValueTask DisposeAsync()
76+
{
77+
if (!_disposed)
78+
{
79+
if (_enumerator is not null)
80+
{
81+
await _enumerator.DisposeAsync().ConfigureAwait(false);
82+
}
83+
_disposed = true;
7184
}
7285
}
7386
}

0 commit comments

Comments
 (0)