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/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..3cb76af6 100644 --- a/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs +++ b/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs @@ -1,12 +1,15 @@ namespace MiniExcelLib.Core.WriteAdapters; -internal class AsyncEnumerableWriteAdapter(IAsyncEnumerable values, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync +internal sealed class AsyncEnumerableWriteAdapter(IAsyncEnumerable values, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync, IAsyncDisposable { 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)) @@ -20,6 +23,7 @@ internal class AsyncEnumerableWriteAdapter(IAsyncEnumerable values, MiniEx _empty = true; return null; } + return CustomPropertyHelper.GetColumnInfoFromValue(_enumerator.Current, _configuration); } @@ -43,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 { @@ -66,8 +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() + { + if (!_disposed) + { + if (_enumerator is not 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..9f6fa87c 100644 --- a/src/MiniExcel.Core/WriteAdapters/MiniExcelWriteAdapterFactory.cs +++ b/src/MiniExcel.Core/WriteAdapters/MiniExcelWriteAdapterFactory.cs @@ -9,11 +9,11 @@ public static bool TryGetAsyncWriteAdapter(object values, MiniExcelBaseConfigura writeAdapter = null; if (values.GetType().IsAsyncEnumerable(out var genericArgument)) { - 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); 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] diff --git a/tests/data/xlsx/Test_EnableWriteFilePath.xlsx b/tests/data/xlsx/Test_EnableWriteFilePath.xlsx deleted file mode 100644 index 6038d092..00000000 Binary files a/tests/data/xlsx/Test_EnableWriteFilePath.xlsx and /dev/null differ