Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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
21 changes: 20 additions & 1 deletion src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
namespace MiniExcelLib.Core.WriteAdapters;

internal class AsyncEnumerableWriteAdapter<T>(IAsyncEnumerable<T> values, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync
internal class AsyncEnumerableWriteAdapter<T>(IAsyncEnumerable<T> values, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync, IAsyncDisposable
{
private bool _disposed = false;
private readonly IAsyncEnumerable<T> _values = values;
private readonly MiniExcelBaseConfiguration _configuration = configuration;
private IAsyncEnumerator<T>? _enumerator;
Expand All @@ -20,7 +21,7 @@
_empty = true;
return null;
}
return CustomPropertyHelper.GetColumnInfoFromValue(_enumerator.Current, _configuration);

Check warning on line 24 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 Down Expand Up @@ -64,10 +65,28 @@
{
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 68 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()
{
await DisposeAsyncCore().ConfigureAwait(false);
}

protected virtual async ValueTask DisposeAsyncCore()
{
if (!_disposed)
{
if (_enumerator != null)
{
await _enumerator.DisposeAsync().ConfigureAwait(false);
}
_disposed = true;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The check && genericArgument != null is redundant. The IsAsyncEnumerable extension method's implementation already guarantees that genericArgument will not be null if the method returns true. Removing this redundant check will make the code slightly cleaner.

        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);
Expand Down
Loading