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
5 changes: 4 additions & 1 deletion V2-Upgrade-Notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@
- `IConfiguration` is now `IMiniExcelConfiguration`, but most methods now require the proper implementation (`OpenXmlConfiguration` or `CsvConfiguration`) to be provided rather than the interface
- MiniExcel now fully supports asynchronous streaming the queries,
so the return type for `OpenXmlImporter.QueryAsync` is `IAsyncEnumerable<T>` instead of `Task<IEnumerable<T>>`
- When applying a template, unlike version 1.x, the flag for overwriting an already existing file must be provided explicitly.
- When applying a template, unlike version 1.x, the flag for overwriting an already existing file must be provided explicitly.
- `leaveOpen` parameter has been added to most methods that take a stream as input in both `OpenXmlImporter` and `CsvImporter` to configure whether the stream must be disposed after the operation performed is completed.
- `useHeaderRow` parameter in multiple `OpenXmlImporter` methods has been renamed to `hasHeaderRow` for making its usage clearer.
- `CsvExporter.Export` API methods, not being required to return the same type of `OpenXmlExporter.Export`, now return `int` instead of `int[]`.
3 changes: 2 additions & 1 deletion benchmarks/MiniExcel.Benchmarks/MiniExcel.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
<PackageReference Include="DocumentFormat.OpenXml" Version="3.5.1" />
<PackageReference Include="EPPlus" Version="7.7.3" />
<PackageReference Include="ExcelDataReader" Version="3.8.0" />
<PackageReference Include="NPOI" Version="2.7.5" />
<PackageReference Include="NPOI" Version="2.8.0" />
<PackageReference Include="System.Security.Cryptography.Xml" Version="8.0.3" />
</ItemGroup>

<ItemGroup>
Expand Down
6 changes: 3 additions & 3 deletions src/MiniExcel.Core/Abstractions/IMiniExcelReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
public partial interface IMiniExcelReader : IDisposable
{
[CreateSyncVersion]
IAsyncEnumerable<IDictionary<string, object?>> QueryAsync(bool useHeaderRow, string? sheetName, string startCell, CancellationToken cancellationToken = default);
IAsyncEnumerable<IDictionary<string, object?>> QueryAsync(bool hasHeaderRow, string? sheetName, string startCell, CancellationToken cancellationToken = default);

[CreateSyncVersion]
IAsyncEnumerable<T> QueryAsync<T>(string? sheetName, string startCell, bool mapHeaderAsData, CancellationToken cancellationToken = default) where T : class, new();

[CreateSyncVersion]
IAsyncEnumerable<IDictionary<string, object?>> QueryRangeAsync(bool useHeaderRow, string? sheetName, string startCell, string endCell, CancellationToken cancellationToken = default);
IAsyncEnumerable<IDictionary<string, object?>> QueryRangeAsync(bool hasHeaderRow, string? sheetName, string startCell, string endCell, CancellationToken cancellationToken = default);

[CreateSyncVersion]
IAsyncEnumerable<T> QueryRangeAsync<T>(string? sheetName, string startCell, string endCell, bool treatHeaderAsData, CancellationToken cancellationToken = default) where T : class, new();

[CreateSyncVersion]
IAsyncEnumerable<IDictionary<string, object?>> QueryRangeAsync(bool useHeaderRow, string? sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, CancellationToken cancellationToken = default);
IAsyncEnumerable<IDictionary<string, object?>> QueryRangeAsync(bool hasHeaderRow, string? sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, CancellationToken cancellationToken = default);

[CreateSyncVersion]
IAsyncEnumerable<T> QueryRangeAsync<T>(string? sheetName, int startRowIndex, int startColumnIndex, int? endRowIndex, int? endColumnIndex, bool treatHeaderAsData, CancellationToken cancellationToken = default) where T : class, new();
Expand Down
44 changes: 31 additions & 13 deletions src/MiniExcel.Core/MiniExcelDataReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ public sealed class MiniExcelDataReader : IMiniExcelDataReader
{
private readonly IEnumerator<IDictionary<string, object?>>? _source;
private readonly IAsyncEnumerator<IDictionary<string, object?>>? _asyncSource;
private readonly Dictionary<string, int> _ordinals = [];
private readonly Stream _stream;
private readonly bool _leaveOpen;

private bool _isEmpty;
private List<string> _columns = [];
Expand All @@ -25,15 +27,16 @@ public object this[string name]
public int RecordsAffected => 0;


private MiniExcelDataReader(Stream? stream, IEnumerable<IDictionary<string, object?>>? values)
private MiniExcelDataReader(Stream? stream, IEnumerable<IDictionary<string, object?>>? values, bool leaveOpen)
{
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
_source = values?.GetEnumerator() ?? throw new ArgumentNullException(nameof(values));
_leaveOpen = leaveOpen;
}

public static MiniExcelDataReader Create(Stream? stream, IEnumerable<IDictionary<string, object?>> values)
public static MiniExcelDataReader Create(Stream? stream, IEnumerable<IDictionary<string, object?>> values, bool leaveOpen = false)
{
var reader = new MiniExcelDataReader(stream, values);
var reader = new MiniExcelDataReader(stream, values, leaveOpen);
if (reader._source!.MoveNext())
{
reader._columns = reader._source.Current?.Keys.ToList() ?? [];
Expand All @@ -47,19 +50,20 @@ public static MiniExcelDataReader Create(Stream? stream, IEnumerable<IDictionary
return reader;
}

private MiniExcelDataReader(Stream? stream, IAsyncEnumerable<IDictionary<string, object?>>? values)
private MiniExcelDataReader(Stream? stream, IAsyncEnumerable<IDictionary<string, object?>>? values, bool leaveOpen = false)
{
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
_asyncSource = values?.GetAsyncEnumerator() ?? throw new ArgumentNullException(nameof(values));
_leaveOpen = leaveOpen;
_isAsyncSource = true;
}

public static async Task<MiniExcelDataReader> CreateAsync(Stream? stream, IAsyncEnumerable<IDictionary<string, object?>> values)
public static async Task<MiniExcelDataReader> CreateAsync(Stream? stream, IAsyncEnumerable<IDictionary<string, object?>> values, bool leaveOpen = false)
{
var reader = new MiniExcelDataReader(stream, values);
var reader = new MiniExcelDataReader(stream, values, leaveOpen);
if (await reader._asyncSource!.MoveNextAsync().ConfigureAwait(false))
{
reader._columns = reader._asyncSource.Current.Keys.ToList();
reader._columns = reader._asyncSource.Current?.Keys.ToList() ?? [];
reader.FieldCount = reader._columns.Count;
}
else
Expand Down Expand Up @@ -219,8 +223,8 @@ public byte GetByte(int i) => GetValue(i) is { } value
public object GetValue(int i)
{
var currentRow = _isAsyncSource
? _asyncSource!.Current
: _source!.Current!;
? _asyncSource?.Current
: _source?.Current;

return currentRow is not null
? currentRow[_columns[i]] ?? DBNull.Value
Expand All @@ -244,8 +248,19 @@ public string GetName(int i)
=> _columns[i];

public int GetOrdinal(string name)
=> _columns.IndexOf(name);

{
if (name is null)
throw new ArgumentNullException(nameof(name));

if (_ordinals.TryGetValue(name, out var ordinal))
return ordinal;

var ord = _columns.IndexOf(name);
_ordinals[name] = ord;

return ord;
}
Comment thread
michelebastione marked this conversation as resolved.

public DataTable GetSchemaTable()
{
if (_schema is null)
Expand Down Expand Up @@ -285,7 +300,9 @@ public void Close()
_source!.Dispose();
}

_stream.Dispose();
if (!_leaveOpen)
_stream.Dispose();

IsClosed = true;
}

Expand All @@ -298,7 +315,8 @@ public async Task CloseAsync()
await _asyncSource!.DisposeAsync().ConfigureAwait(false);

_source?.Dispose();
await _stream!.DisposeAsync().ConfigureAwait(false);
if (!_leaveOpen)
await _stream.DisposeAsync().ConfigureAwait(false);

IsClosed = true;
}
Expand Down
60 changes: 48 additions & 12 deletions src/MiniExcel.Csv/Api/CsvExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@ namespace MiniExcelLib.Csv;
public partial class CsvExporter
{
internal CsvExporter() { }


#region Append / Export


/// <summary>
/// Appends data rows to an existing CSV file without overwriting existing content.
/// </summary>
/// <param name="path">The path to the CSV file to append to.</param>
/// <param name="value">The data to append. Can be an object, <see cref="IEnumerable"/>, <see cref="DataTable"/>, or <see cref="IDataReader"/>.</param>
/// <param name="printHeader">If true, when the file does not exist already the header row is added to the output. Default is true</param>
/// <param name="configuration">Optional configuration settings (delimiters, encoding, etc.).</param>
/// <param name="progress">An optional <see cref="IProgress{T}"/> to report progress as values are written.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>The number of rows appended.</returns>
[CreateSyncVersion]
public async Task<int> AppendAsync(string path, object value, bool printHeader = true,
CsvConfiguration? configuration = null, IProgress<int>? progress = null, CancellationToken cancellationToken = default)
{
if (!File.Exists(path))
{
var rowsWritten = await ExportAsync(path, value, printHeader, false, configuration, progress, cancellationToken).ConfigureAwait(false);
return rowsWritten.FirstOrDefault();
return await ExportAsync(path, value, printHeader, false, configuration, progress, cancellationToken).ConfigureAwait(false);
}

var stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read, 4096, FileOptions.SequentialScan);
Expand All @@ -24,6 +30,15 @@ public async Task<int> AppendAsync(string path, object value, bool printHeader =
return await AppendAsync(stream, value, configuration, progress, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Appends data rows to an existing CSV stream without overwriting existing content.
/// </summary>
/// <param name="stream">The stream containing the CSV file to append to. The stream will be positioned at the end before appending.</param>
/// <param name="value">The data to append. Can be an object, <see cref="IEnumerable"/>, <see cref="DataTable"/>, or <see cref="IDataReader"/>.</param>
/// <param name="configuration">Optional configuration settings (delimiters, encoding, etc.).</param>
/// <param name="progress">An optional <see cref="IProgress{T}"/> to report progress as values are written.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>The number of rows appended.</returns>
[CreateSyncVersion]
public async Task<int> AppendAsync(Stream stream, object value, CsvConfiguration? configuration = null, IProgress<int>? progress = null, CancellationToken cancellationToken = default)
{
Expand All @@ -35,8 +50,19 @@ public async Task<int> AppendAsync(Stream stream, object value, CsvConfiguration
return await writer.InsertAsync(false, progress, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Exports data to a CSV file.
/// </summary>
/// <param name="path">The path to write CSV data to.</param>
/// <param name="value">The data to export. Can be an object, <see cref="IEnumerable"/>, <see cref="DataTable"/>, or <see cref="IDataReader"/>.</param>
/// <param name="printHeader">If true, the first row will contain column headers derived from property names or DataTable column names. Default is true.</param>
/// <param name="overwriteFile">If true, when a file at the specified path already exists it will be overwritten, otherwise an <see cref="IOException" /> will be thrown. Default is false.</param>
/// <param name="configuration">Optional configuration settings (delimiters, encoding, etc.).</param>
/// <param name="progress">An optional <see cref="IProgress{T}"/> to report progress as values are written.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>The number of rows written.</returns>
[CreateSyncVersion]
public async Task<int[]> ExportAsync(string path, object value, bool printHeader = true, bool overwriteFile = false,
public async Task<int> ExportAsync(string path, object value, bool printHeader = true, bool overwriteFile = false,
CsvConfiguration? configuration = null, IProgress<int>? progress = null, CancellationToken cancellationToken = default)
{
var stream = overwriteFile ? File.Create(path) : new FileStream(path, FileMode.CreateNew);
Expand All @@ -45,13 +71,23 @@ public async Task<int[]> ExportAsync(string path, object value, bool printHeader
return await ExportAsync(stream, value, printHeader, configuration, progress, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Exports data to a CSV stream.
/// </summary>
/// <param name="stream">The stream to write CSV data to. Existing content will be overwritten.</param>
/// <param name="value">The data to export. Can be an object, <see cref="IEnumerable"/>, <see cref="DataTable"/>, or <see cref="IDataReader"/>.</param>
/// <param name="printHeader">If true, the first row will contain column headers derived from property names or DataTable column names. Default is true.</param>
/// <param name="configuration">Optional configuration settings (delimiters, encoding, etc.).</param>
/// <param name="progress">An optional <see cref="IProgress{T}"/> to report progress as values are written.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>The number of rows written.</returns>
[CreateSyncVersion]
public async Task<int[]> ExportAsync(Stream stream, object value, bool printHeader = true,
public async Task<int> ExportAsync(Stream stream, object value, bool printHeader = true,
CsvConfiguration? configuration = null, IProgress<int>? progress = null, CancellationToken cancellationToken = default)
{
using var writer = new CsvWriter(stream, value, printHeader, configuration);
return await writer.SaveAsAsync(progress, cancellationToken).ConfigureAwait(false);
}
var result = await writer.SaveAsAsync(progress, cancellationToken).ConfigureAwait(false);

#endregion
}
return result.FirstOrDefault();
}
}
Loading
Loading