Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -401,4 +401,5 @@ FodyWeavers.xsd
/BenchmarkDotNet.Artifacts
/TestTemplate
/tests/MiniExcel.Tests/TemplateOptimization
/tests/data/xlsx/Test_EnableWriteFilePath.xlsx
/tests/data
samples/xlsx/Test_EnableWriteFilePath.xlsx
215 changes: 113 additions & 102 deletions src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs
Original file line number Diff line number Diff line change
@@ -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<SheetDto> _sheets = [];
private readonly List<FileDto> _files = [];

private readonly string? _defaultSheetName;
private readonly bool _printHeader;
private readonly object? _value;
Expand All @@ -41,16 +41,16 @@ internal OpenXmlWriter(Stream stream, object? value, string? sheetName, IMiniExc
_printHeader = printHeader;
_defaultSheetName = sheetName;
}

[CreateSyncVersion]
internal static Task<OpenXmlWriter> CreateAsync(Stream stream, object? value, string? sheetName, bool printHeader, IMiniExcelConfiguration? configuration, CancellationToken cancellationToken = default)
internal static Task<OpenXmlWriter> 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<int[]> SaveAsAsync(IProgress<int>? progress = null, CancellationToken cancellationToken = default)
{
Expand Down Expand Up @@ -190,7 +190,7 @@ private async Task<int> 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);
Expand Down Expand Up @@ -241,125 +241,136 @@ private async Task<int> 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<List<MiniExcelColumnInfo>?>(null)).ConfigureAwait(false);
var props = writeAdapter is not null
? writeAdapter.GetColumns()
: await (asyncWriteAdapter?.GetColumnsAsync() ?? Task.FromResult<List<MiniExcelColumnInfo>?>(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]
Expand Down Expand Up @@ -390,7 +401,7 @@ private static async Task OverwriteColumnWidthPlaceholdersAsync(SafeStreamWriter
private static async Task WriteColumnsWidthsAsync(SafeStreamWriter writer, IEnumerable<ExcelColumnWidth>? columnWidths, CancellationToken cancellationToken = default)
{
var hasWrittenStart = false;

columnWidths ??= [];
foreach (var column in columnWidths)
{
Expand Down Expand Up @@ -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
Expand Down
33 changes: 23 additions & 10 deletions src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
namespace MiniExcelLib.Core.WriteAdapters;

internal class AsyncEnumerableWriteAdapter<T>(IAsyncEnumerable<T> values, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync
internal sealed class AsyncEnumerableWriteAdapter<T>(IAsyncEnumerable<T> values, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync, IAsyncDisposable
{
private readonly IAsyncEnumerable<T> _values = values;
private readonly MiniExcelBaseConfiguration _configuration = configuration;

private IAsyncEnumerator<T>? _enumerator;
private bool _empty;
private bool _disposed = false;


public async Task<List<MiniExcelColumnInfo>?> GetColumnsAsync()
{
if (CustomPropertyHelper.TryGetTypeColumnInfo(typeof(T), _configuration, out var props))
Expand All @@ -20,7 +23,8 @@
_empty = true;
return null;
}

return CustomPropertyHelper.GetColumnInfoFromValue(_enumerator.Current, _configuration);

Check warning on line 27 in src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'value' in 'List<MiniExcelColumnInfo> CustomPropertyHelper.GetColumnInfoFromValue(object value, MiniExcelBaseConfiguration configuration)'.
}

public async IAsyncEnumerable<IAsyncEnumerable<CellWriteInfo>> GetRowsAsync(List<MiniExcelColumnInfo> props, [EnumeratorCancellation] CancellationToken cancellationToken)
Expand All @@ -43,31 +47,40 @@
{
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<CellWriteInfo> GetRowValuesAsync(T currentValue, List<MiniExcelColumnInfo> props)
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
private static async IAsyncEnumerable<CellWriteInfo> GetRowValuesAsync(T currentValue, List<MiniExcelColumnInfo> 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
{
IDictionary<string, object> genericDictionary => new CellWriteInfo(genericDictionary[prop.Key.ToString()], column, prop),
IDictionary dictionary => new CellWriteInfo(dictionary[prop.Key], column, prop),
_ => new CellWriteInfo(prop.Property.GetValue(currentValue), column, prop)

Check warning on line 70 in src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'instance' in 'object? MiniExcelProperty.GetValue(object instance)'.
};
}
}

column++;
public async ValueTask DisposeAsync()
{
if (!_disposed)
{
if (_enumerator is not null)
{
await _enumerator.DisposeAsync().ConfigureAwait(false);
}
_disposed = true;
}
}
}
Loading
Loading