From 6c12dcdd5d81ddc8787627bd19f08e26ba0aae98 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Mon, 2 Mar 2026 00:44:24 +0100 Subject: [PATCH 1/5] Refactoring MiniExcelDataReader Integrating the implementation of MiniExcelDataReader to fully support nearly all IDataReader methods and turn it into a fully functional data reader. The new implementation also supports asynchronous reads so there's no need for a separate MiniExcelAsynDataReader, which has thus been removed. The MiniExcelDataReaderBase abstract class has also been removed, as it is no longer needed given its purpose was to hide all the non implemented IDataReader methods from the main class. The OpenXmlImporter and CsvImporter classes have been updated to reflect these changes. --- .../Abstractions/IMiniExcelDataReader.cs | 3 - .../DataReader/MiniExcelAsyncDataReader.cs | 108 ------ .../DataReader/MiniExcelDataReader.cs | 94 ----- .../DataReader/MiniExcelDataReaderBase.cs | 361 ------------------ src/MiniExcel.Core/MiniExcelDataReader.cs | 306 +++++++++++++++ src/MiniExcel.Csv/Api/CsvImporter.cs | 49 ++- src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs | 31 +- src/MiniExcel/MiniExcel.cs | 6 +- 8 files changed, 366 insertions(+), 592 deletions(-) delete mode 100644 src/MiniExcel.Core/DataReader/MiniExcelAsyncDataReader.cs delete mode 100644 src/MiniExcel.Core/DataReader/MiniExcelDataReader.cs delete mode 100644 src/MiniExcel.Core/DataReader/MiniExcelDataReaderBase.cs create mode 100644 src/MiniExcel.Core/MiniExcelDataReader.cs diff --git a/src/MiniExcel.Core/Abstractions/IMiniExcelDataReader.cs b/src/MiniExcel.Core/Abstractions/IMiniExcelDataReader.cs index a36aec05..c7fa756a 100644 --- a/src/MiniExcel.Core/Abstractions/IMiniExcelDataReader.cs +++ b/src/MiniExcel.Core/Abstractions/IMiniExcelDataReader.cs @@ -6,8 +6,5 @@ public interface IMiniExcelDataReader : IDataReader #endif { Task CloseAsync(); - Task GetNameAsync(int i, CancellationToken cancellationToken = default); - Task GetValueAsync(int i, CancellationToken cancellationToken = default); - Task NextResultAsync(CancellationToken cancellationToken = default); Task ReadAsync(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/MiniExcel.Core/DataReader/MiniExcelAsyncDataReader.cs b/src/MiniExcel.Core/DataReader/MiniExcelAsyncDataReader.cs deleted file mode 100644 index aafdca80..00000000 --- a/src/MiniExcel.Core/DataReader/MiniExcelAsyncDataReader.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace MiniExcelLib.Core.DataReader; - -// todo: this is way improvable, ideally the sync and async implementations into a single datareader -public class MiniExcelAsyncDataReader : MiniExcelDataReaderBase, IAsyncDisposable -{ - private readonly IAsyncEnumerator> _source; - - private readonly Stream _stream; - private List _keys; - private int _fieldCount; - - private bool _isFirst = true; - private bool _disposed = false; - - /// - /// Initializes a new instance of the class. - /// - - internal MiniExcelAsyncDataReader(Stream? stream, IAsyncEnumerable> values) - { - _stream = stream ?? throw new ArgumentNullException(nameof(stream)); - _source = values.GetAsyncEnumerator(); - } - - public static async Task CreateAsync(Stream? stream, IAsyncEnumerable> values) - { - var reader = new MiniExcelAsyncDataReader(stream, values); - if (await reader._source.MoveNextAsync().ConfigureAwait(false)) - { - reader._keys = reader._source.Current?.Keys.ToList() ?? []; - reader._fieldCount = reader._keys.Count; - } - return reader; - } - - - /// - public override object? GetValue(int i) - { - if (_source.Current is null) - throw new InvalidOperationException("No current row available."); - - return _source.Current[_keys[i]]; - } - - /// - public override int FieldCount => _fieldCount; - - /// - /// This method will throw a NotSupportedException. Please use ReadAsync or the synchronous MiniExcelDataReader implementation. - /// - public override bool Read() => throw new NotSupportedException("Use the ReadAsync method instead."); - - public override async Task ReadAsync(CancellationToken cancellationToken = default) - { - if (_isFirst) - { - _isFirst = false; - return await Task.FromResult(true).ConfigureAwait(false); - } - - return await _source.MoveNextAsync().ConfigureAwait(false); - } - - /// - public override string GetName(int i) - { - return _keys[i]; - } - - /// - public override int GetOrdinal(string name) - { - return _keys.IndexOf(name); - } - - /// - protected override void Dispose(bool disposing) - { - if (!_disposed) - { - if (disposing) - { - _stream?.Dispose(); - _source.DisposeAsync().GetAwaiter().GetResult(); - } - _disposed = true; - } - base.Dispose(disposing); - } - - /// - /// Disposes the object. - /// - public new void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public async ValueTask DisposeAsync() - { - _stream?.Dispose(); - await _source.DisposeAsync().ConfigureAwait(false); - - GC.SuppressFinalize(this); - } -} \ No newline at end of file diff --git a/src/MiniExcel.Core/DataReader/MiniExcelDataReader.cs b/src/MiniExcel.Core/DataReader/MiniExcelDataReader.cs deleted file mode 100644 index df1d14e0..00000000 --- a/src/MiniExcel.Core/DataReader/MiniExcelDataReader.cs +++ /dev/null @@ -1,94 +0,0 @@ -namespace MiniExcelLib.Core.DataReader; - -public class MiniExcelDataReader : MiniExcelDataReaderBase -{ - private readonly IEnumerator> _source; - - private readonly Stream _stream; - private readonly List _keys; - private readonly int _fieldCount; - - private bool _isFirst = true; - private bool _disposed = false; - - /// - /// Initializes a new instance of the class. - /// - internal MiniExcelDataReader(Stream? stream, IEnumerable> values) - { - _stream = stream ?? throw new ArgumentNullException(nameof(stream)); - _source = values.GetEnumerator(); - - if (_source.MoveNext()) - { - _keys = _source.Current?.Keys.ToList() ?? []; - _fieldCount = _keys.Count; - } - } - - public static MiniExcelDataReader Create(Stream? stream, IEnumerable> values) => new(stream, values); - - - /// - public override object? GetValue(int i) - { - if (_source.Current is null) - throw new InvalidOperationException("No current row available."); - - return _source.Current[_keys[i]]; - } - - /// - public override int FieldCount => _fieldCount; - - /// - /// - /// - /// - public override bool Read() - { - if (_isFirst) - { - _isFirst = false; - return true; - } - - return _source.MoveNext(); - } - - /// - public override string GetName(int i) - { - return _keys[i]; - } - - /// - public override int GetOrdinal(string name) - { - return _keys.IndexOf(name); - } - - /// - protected override void Dispose(bool disposing) - { - if (!_disposed) - { - if (disposing) - { - _stream?.Dispose(); - _source?.Dispose(); - } - _disposed = true; - } - base.Dispose(disposing); - } - - /// - /// Disposes the object. - /// - public new void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } -} \ No newline at end of file diff --git a/src/MiniExcel.Core/DataReader/MiniExcelDataReaderBase.cs b/src/MiniExcel.Core/DataReader/MiniExcelDataReaderBase.cs deleted file mode 100644 index 8bc3107d..00000000 --- a/src/MiniExcel.Core/DataReader/MiniExcelDataReaderBase.cs +++ /dev/null @@ -1,361 +0,0 @@ -namespace MiniExcelLib.Core.DataReader; - -/// -/// IMiniExcelDataReader Base Class -/// -public abstract class MiniExcelDataReaderBase : IMiniExcelDataReader -{ - /// - /// - /// - /// - /// - public virtual object? this[int i] => null; - - /// - /// - /// - /// - /// - public virtual object? this[string name] => null; - - /// - /// - /// - public virtual int Depth { get; } = 0; - - /// - /// - /// - public virtual bool IsClosed { get; } = false; - - /// - /// - /// - public virtual int RecordsAffected { get; } = 0; - - /// - /// - /// - public virtual int FieldCount { get; } = 0; - - /// - /// - /// - /// - /// - public virtual bool GetBoolean(int i) => false; - - /// - /// - /// - /// - /// - public virtual byte GetByte(int i) => byte.MinValue; - - /// - /// - /// - /// - /// - /// - /// - /// - /// - public virtual long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferOffset, int length) => 0; - - /// - /// - /// - /// - /// - public virtual char GetChar(int i) => char.MinValue; - - /// - /// - /// - /// - /// - /// - /// - /// - /// - public virtual long GetChars(int i, long fieldOffset, char[]? buffer, int bufferOffset, int length) => 0; - - /// - /// - /// - /// - /// - public virtual IDataReader? GetData(int i) => null; - - /// - /// - /// - /// - /// - public virtual string GetDataTypeName(int i) => string.Empty; - - /// - /// - /// - /// - /// - public virtual DateTime GetDateTime(int i) => DateTime.MinValue; - - /// - /// - /// - /// - /// - public virtual decimal GetDecimal(int i) => 0; - - /// - /// - /// - /// - /// - public virtual double GetDouble(int i) => 0; - - /// - /// - /// - /// - /// - public virtual Type GetFieldType(int i) => null!; - - /// - /// - /// - /// - /// - public virtual float GetFloat(int i) => 0f; - - /// - /// - /// - /// - /// - public virtual Guid GetGuid(int i) => Guid.Empty; - - /// - /// - /// - /// - /// - public virtual short GetInt16(int i) => 0; - - /// - /// - /// - /// - /// - public virtual int GetInt32(int i) => 0; - - /// - /// - /// - /// - /// - public virtual long GetInt64(int i) => 0; - - /// - /// - /// - /// - /// - public virtual int GetOrdinal(string name) => 0; - - /// - /// - /// - /// - public virtual DataTable? GetSchemaTable() => null; - - /// - /// - /// - /// - /// - public virtual string GetString(int i) => string.Empty; - - /// - /// - /// - /// - /// - public virtual int GetValues(object[] values) => 0; - - /// - /// - /// - /// - /// - public virtual bool IsDBNull(int i) => false; - - /// - /// - /// - /// - public virtual bool NextResult() => false; - - /// - /// - /// - /// - /// - public virtual Task NextResultAsync(CancellationToken cancellationToken = default) - { - if (cancellationToken.IsCancellationRequested) - return Task.FromCanceled(cancellationToken); - - try - { - return NextResult() ? Task.FromResult(true) : Task.FromResult(false); - } - catch (Exception e) - { - return Task.FromException(e); - } - } - - /// - /// - /// - /// - /// - public abstract string GetName(int i); - - /// - /// - /// - /// - /// - /// - public virtual Task GetNameAsync(int i, CancellationToken cancellationToken = default) - { - if (cancellationToken.IsCancellationRequested) - return Task.FromCanceled(cancellationToken); - - try - { - return Task.FromResult(GetName(i)); - } - catch (Exception e) - { - return Task.FromException(e); - } - } - - /// - /// - /// - /// - /// - public abstract object? GetValue(int i); - - /// - /// - /// - /// - /// - /// - public virtual Task GetValueAsync(int i, CancellationToken cancellationToken = default) - { - if (cancellationToken.IsCancellationRequested) - return Task.FromCanceled(cancellationToken); - - try - { - return Task.FromResult(GetValue(i)); - } - catch (Exception e) - { - return Task.FromException(e); - } - } - - /// - /// - /// - /// - public abstract bool Read(); - - /// - /// - /// - /// - /// - public virtual Task ReadAsync(CancellationToken cancellationToken = default) - { - if (cancellationToken.IsCancellationRequested) - return Task.FromCanceled(cancellationToken); - - try - { - return Read() ? Task.FromResult(true) : Task.FromResult(false); - } - catch (Exception e) - { - return Task.FromException(e); - } - } - - /// - /// - /// - public virtual void Close() - { - } - - /// - /// - /// - /// - public virtual Task CloseAsync() - { - try - { - Close(); - return Task.CompletedTask; - } - catch (Exception e) - { - return Task.FromException(e); - } - } - - /// - /// - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - -#if NET8_0_OR_GREATER - /// - /// - /// - /// - /// - public virtual ValueTask DisposeAsync() - { - Dispose(); - return default; - } -#endif - - /// - /// - /// - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - Close(); - } - } -} \ No newline at end of file diff --git a/src/MiniExcel.Core/MiniExcelDataReader.cs b/src/MiniExcel.Core/MiniExcelDataReader.cs new file mode 100644 index 00000000..52888a8a --- /dev/null +++ b/src/MiniExcel.Core/MiniExcelDataReader.cs @@ -0,0 +1,306 @@ +namespace MiniExcelLib.Core; + +public sealed class MiniExcelDataReader : IMiniExcelDataReader +{ + private readonly IEnumerator>? _source; + private readonly IAsyncEnumerator>? _asyncSource; + private readonly Stream _stream; + + private List _columns = []; + private DataTable? _schema; + + private readonly bool _isAsyncSource; + private bool _isFirst = true; + + public object this[int i] + => GetValue(i); + + public object this[string name] + => GetValue(GetOrdinal(name)); + + public int Depth { get; private set; } = -1; + public int FieldCount { get; private set; } + public bool IsClosed { get; private set; } + public int RecordsAffected => 0; + + + private MiniExcelDataReader(Stream? stream, IEnumerable>? values) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + _source = values?.GetEnumerator() ?? throw new ArgumentNullException(nameof(values)); + } + + public static MiniExcelDataReader Create(Stream? stream, IEnumerable> values) + { + var reader = new MiniExcelDataReader(stream, values); + if (reader._source!.MoveNext()) + { + reader._columns = reader._source.Current?.Keys.ToList() ?? []; + reader.FieldCount = reader._columns.Count; + reader.Depth++; + } + return reader; + } + + private MiniExcelDataReader(Stream? stream, IAsyncEnumerable>? values) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + _asyncSource = values?.GetAsyncEnumerator() ?? throw new ArgumentNullException(nameof(values)); + _isAsyncSource = true; + } + + public static async Task CreateAsync(Stream? stream, IAsyncEnumerable> values) + { + var reader = new MiniExcelDataReader(stream, values); + if (await reader._asyncSource!.MoveNextAsync().ConfigureAwait(false)) + { + reader._columns = reader._asyncSource.Current.Keys.ToList(); + reader.FieldCount = reader._columns.Count; + reader.Depth++; + } + return reader; + } + + public bool Read() + { + if (IsClosed) + throw new InvalidOperationException("The data reader has been closed"); + + if (_isAsyncSource) + throw new InvalidOperationException("The data reader was configured to execute asynchronously"); + + Depth++; + if (_isFirst) + { + _isFirst = false; + return true; + } + + return _source!.MoveNext(); + } + + public async Task ReadAsync(CancellationToken cancellationToken = default) + { + if (!_isAsyncSource) + return await Task.FromResult(Read()).ConfigureAwait(false); + + if (IsClosed) + throw new InvalidOperationException("The data reader has been closed"); + + Depth++; + if (_isFirst) + { + _isFirst = false; + return true; + } + + return await _asyncSource!.MoveNextAsync().ConfigureAwait(false); + } + + public IDataReader GetData(int i) + => throw new NotSupportedException(); + + public Type GetFieldType(int i) + => typeof(object); + + public string GetDataTypeName(int i) + => typeof(object).FullName!; + + /// + /// This method will alway throw a + /// + public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) + => throw new NotSupportedException("MiniExcelDataReader does not support this method"); + + public long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) + { + var s = GetString(i).Substring((int)fieldoffset, length); + + for (int j = bufferoffset; j < s.Length; j++) + buffer.AsSpan()[j] = s.AsSpan()[j]; + + return s.Length; + } + + public bool GetBoolean(int i) => GetValue(i) switch + { + bool b => b, + null => throw new InvalidOperationException("The value is null"), + var value => Convert.ToBoolean(value) + }; + + public byte GetByte(int i) => GetValue(i) is { } value + ? Convert.ToByte(value) + : throw new InvalidOperationException("The value is null"); + + public char GetChar(int i) => GetValue(i) switch + { + char c => c, + null => throw new InvalidOperationException("The value is null"), + var value => Convert.ToChar(value) + }; + + public DateTime GetDateTime(int i) => GetValue(i) switch + { + DateTime dt => dt, + double d => DateTime.FromOADate(d), + null => throw new InvalidOperationException("The value is null"), + var value => Convert.ToDateTime(value) + }; + + public decimal GetDecimal(int i) => GetValue(i) switch + { + decimal d => d, + null => throw new InvalidOperationException("The value is null"), + var value => Convert.ToDecimal(value) + }; + + public float GetFloat(int i) => GetValue(i) switch + { + float f => f, + null => throw new InvalidOperationException("The value is null"), + var value => Convert.ToSingle(value) + }; + + public double GetDouble(int i) => GetValue(i) switch + { + double d => d, + null => throw new InvalidOperationException("The value is null"), + var value => Convert.ToDouble(value) + }; + + public Guid GetGuid(int i) => GetValue(i) switch + { + Guid g => g, + string s => Guid.Parse(s), + byte[] b => new Guid(b), + null => throw new InvalidOperationException("The value is null"), + var value => throw new InvalidCastException($"The value {value} cannot be cast to Guid"), + }; + + public short GetInt16(int i) => GetValue(i) switch + { + short s => s, + null => throw new InvalidOperationException("The value is null"), + var value => Convert.ToInt16(value) + }; + + public int GetInt32(int i) => GetValue(i) switch + { + int s => s, + null => throw new InvalidOperationException("The value is null"), + var value => Convert.ToInt32(value) + }; + + public long GetInt64(int i) => GetValue(i) switch + { + long l => l, + null => throw new InvalidOperationException("The value is null"), + var value => Convert.ToInt64(value) + }; + + public string GetString(int i) => GetValue(i) switch + { + string s => s, + var value => Convert.ToString(value) ?? "" + }; + + public object GetValue(int i) + { + var currentRow = _isAsyncSource + ? _asyncSource!.Current + : _source!.Current!; + + return currentRow is not null + ? currentRow[_columns[i]] ?? DBNull.Value + : throw new InvalidOperationException("Current row is not available."); + } + + public int GetValues(object?[] values) + { + var count = Math.Min(values.Length, FieldCount); + + for (int i = 0; i < values.Length; i++) + values[i] = GetValue(i); + + return count; + } + + public bool IsDBNull(int i) + => GetValue(i) is null or DBNull; + + public string GetName(int i) + => _columns[i]; + + public int GetOrdinal(string name) + => _columns.IndexOf(name); + + public DataTable GetSchemaTable() + { + if (_schema is null) + { + _schema = new DataTable(); + _schema.Columns.Add("ColumnOrdinal"); + _schema.Columns.Add("ColumnName"); + + for (int i = 0; i < _columns.Count; i++) + { + _schema.Rows.Add(i, _columns[i]); + } + } + + return _schema; + } + + /// + /// This method will alway return false + /// + public bool NextResult() => false; + + public void Close() + { + if (IsClosed) + return; + + if (_isAsyncSource) + { + if (_asyncSource is IDisposable disposable) + disposable.Dispose(); + else + _asyncSource!.DisposeAsync(); // fire and forget if all other options are exhausted + } + else + { + _source!.Dispose(); + } + + _stream.Dispose(); + IsClosed = true; + } + + public async Task CloseAsync() + { + if (IsClosed) + return; + + if (_isAsyncSource) + { + await _asyncSource!.DisposeAsync().ConfigureAwait(false); + } + _source?.Dispose(); + +#if NETCOREAPP3_0_OR_GREATER + await _stream!.DisposeAsync().ConfigureAwait(false); +#else + _stream.Dispose(); +#endif + + IsClosed = true; + } + + public void Dispose() + => Close(); + + public async ValueTask DisposeAsync() + => await CloseAsync().ConfigureAwait(false); +} diff --git a/src/MiniExcel.Csv/Api/CsvImporter.cs b/src/MiniExcel.Csv/Api/CsvImporter.cs index a63048f2..efaa6f54 100644 --- a/src/MiniExcel.Csv/Api/CsvImporter.cs +++ b/src/MiniExcel.Csv/Api/CsvImporter.cs @@ -1,5 +1,4 @@ -using MiniExcelLib.Core.DataReader; -using MiniExcelLib.Core.Helpers; +using MiniExcelLib.Core; // ReSharper disable once CheckNamespace namespace MiniExcelLib.Csv; @@ -152,36 +151,52 @@ public async Task> GetColumnNamesAsync(Stream stream, bool u #region DataReader - public MiniExcelDataReader GetDataReader(string path, bool useHeaderRow = false, CsvConfiguration? configuration = null) + /// + /// Gets an for the Csv document at the specified path. + /// + /// + /// Asynchronous reads are not allowed when creating the data reader from this overload and will result in an exception. + /// + public MiniExcelDataReader GetDataReader(string path, bool useHeaderRow = false) { var stream = FileHelper.OpenSharedRead(path); - var values = Query(stream, useHeaderRow, configuration).Cast>(); + var values = Query(stream, useHeaderRow).Cast>(); return MiniExcelDataReader.Create(stream, values); } - public MiniExcelDataReader GetDataReader(Stream stream, bool useHeaderRow = false, CsvConfiguration? configuration = null) + /// + /// Gets an for the Csv document from an underlying stream. + /// + /// + /// Asynchronous reads are not allowed when creating the data reader from this overload and will result in an exception. + /// + public MiniExcelDataReader GetDataReader(Stream stream, bool useHeaderRow = false) { - var values = Query(stream, useHeaderRow, configuration).Cast>(); + var values = Query(stream, useHeaderRow).Cast>(); return MiniExcelDataReader.Create(stream, values); } - public async Task GetAsyncDataReader(string path, bool useHeaderRow = false, - string? sheetName = null, string startCell = "A1", CsvConfiguration? configuration = null, - CancellationToken cancellationToken = default) + /// + /// Gets an for the Csv document at the specific path. + /// When created from this overload, the resulting data reader is supposed to be advanced asynchronously. + /// + public async Task GetAsyncDataReader(string path, bool useHeaderRow = false, CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); - var values = QueryAsync(stream, useHeaderRow, configuration, cancellationToken); + var values = QueryAsync(stream, useHeaderRow, cancellationToken: cancellationToken); - return await MiniExcelAsyncDataReader.CreateAsync(stream, CastAsync(values, cancellationToken)).ConfigureAwait(false); + return await MiniExcelDataReader.CreateAsync(stream, CastAsync(values, cancellationToken)).ConfigureAwait(false); } - public async Task GetAsyncDataReader(Stream stream, bool useHeaderRow = false, - string? sheetName = null, string startCell = "A1", CsvConfiguration? configuration = null, - CancellationToken cancellationToken = default) + /// + /// Gets an for the Csv document at the specific path. + /// When created from this overload, the resulting data reader is supposed to be advanced asynchronously. + /// + public async Task GetAsyncDataReader(Stream stream, bool useHeaderRow = false, CancellationToken cancellationToken = default) { - var values = QueryAsync(stream, useHeaderRow, configuration, cancellationToken); - return await MiniExcelAsyncDataReader.CreateAsync(stream, CastAsync(values, cancellationToken)).ConfigureAwait(false); + var values = QueryAsync(stream, useHeaderRow, cancellationToken: cancellationToken); + return await MiniExcelDataReader.CreateAsync(stream, CastAsync(values, cancellationToken)).ConfigureAwait(false); } #endregion @@ -201,4 +216,4 @@ public async Task GetAsyncDataReader(Stream stream, bo yield return dict; } } -} \ No newline at end of file +} diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs index 65d500a7..15788e19 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs @@ -1,5 +1,4 @@ using System.Dynamic; -using MiniExcelLib.Core.DataReader; using MiniExcelLib.OpenXml; using OpenXmlReader = MiniExcelLib.OpenXml.OpenXmlReader; @@ -269,6 +268,12 @@ public async Task> GetColumnNamesAsync(Stream stream, bool u #region DataReader + /// + /// Gets an for the Excel document at the specified path. + /// + /// + /// Asynchronous reads are not allowed when creating the data reader from this overload and will result in an exception. + /// public MiniExcelDataReader GetDataReader(string path, bool useHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null) { @@ -278,6 +283,12 @@ public MiniExcelDataReader GetDataReader(string path, bool useHeaderRow = false, return MiniExcelDataReader.Create(stream, values); } + /// + /// Gets an for the Excel document from an underlying stream. + /// + /// + /// Asynchronous reads are not allowed when creating the data reader from this overload and will result in an exception. + /// public MiniExcelDataReader GetDataReader(Stream stream, bool useHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null) { @@ -285,22 +296,30 @@ public MiniExcelDataReader GetDataReader(Stream stream, bool useHeaderRow = fals return MiniExcelDataReader.Create(stream, values); } - public async Task GetAsyncDataReader(string path, bool useHeaderRow = false, + /// + /// Gets an for the Excel document at the specific path. + /// When created from this overload, the resulting data reader is supposed to be advanced asynchronously. + /// + public async Task GetAsyncDataReader(string path, bool useHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); var values = QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken); - return await MiniExcelAsyncDataReader.CreateAsync(stream, CastAsync(values, cancellationToken)).ConfigureAwait(false); + return await MiniExcelDataReader.CreateAsync(stream, CastAsync(values, cancellationToken)).ConfigureAwait(false); } - public async Task GetAsyncDataReader(Stream stream, bool useHeaderRow = false, + /// + /// Gets an for the Excel document from an underlying stream. + /// When created from this overload, the resulting data reader is supposed to be advanced asynchronously. + /// + public async Task GetAsyncDataReader(Stream stream, bool useHeaderRow = false, string? sheetName = null, string startCell = "A1", OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) { var values = QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration, cancellationToken); - return await MiniExcelAsyncDataReader.CreateAsync(stream, CastAsync(values, cancellationToken)).ConfigureAwait(false); + return await MiniExcelDataReader.CreateAsync(stream, CastAsync(values, cancellationToken)).ConfigureAwait(false); } #endregion @@ -320,4 +339,4 @@ public async Task GetAsyncDataReader(Stream stream, bo yield return dict; } } -} \ No newline at end of file +} diff --git a/src/MiniExcel/MiniExcel.cs b/src/MiniExcel/MiniExcel.cs index 498b6b91..a641ad1b 100644 --- a/src/MiniExcel/MiniExcel.cs +++ b/src/MiniExcel/MiniExcel.cs @@ -1,6 +1,6 @@ using System.Data; using MiniExcelLib; -using MiniExcelLib.Core.DataReader; +using MiniExcelLib.Core; using MiniExcelLib.Csv; using MiniExcelLib.OpenXml.Api; using MiniExcelLib.OpenXml.Models; @@ -40,7 +40,7 @@ public static MiniExcelDataReader GetReader(string path, bool useHeaderRow = fal return type switch { ExcelType.XLSX => ExcelImporter.GetDataReader(path, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration), - ExcelType.CSV => CsvImporter.GetDataReader(path, useHeaderRow, configuration as Csv.CsvConfiguration), + ExcelType.CSV => CsvImporter.GetDataReader(path, useHeaderRow), _ => throw new NotSupportedException($"Type {type} is not a valid Excel type") }; } @@ -51,7 +51,7 @@ public static MiniExcelDataReader GetReader(this Stream stream, bool useHeaderRo return type switch { ExcelType.XLSX => ExcelImporter.GetDataReader(stream, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration), - ExcelType.CSV => CsvImporter.GetDataReader(stream, useHeaderRow, configuration as Csv.CsvConfiguration), + ExcelType.CSV => CsvImporter.GetDataReader(stream, useHeaderRow), _ => throw new NotSupportedException($"Type {type} is not a valid Excel type") }; } From 8c35afeee587dc5f5ed5322041188c9517059aea Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Mon, 2 Mar 2026 00:52:04 +0100 Subject: [PATCH 2/5] Removing MiniExcelDataReaderWriteAdapter I'm pretty confident this write adapter has never been of much usefulness, but now that MiniExcelDataReader has become a fully implemented IDataReader I think there is no reason to keep it around. --- .../MiniExcelDataReaderWriteAdapter.cs | 55 ------------------- .../MiniExcelWriteAdapterFactory.cs | 7 --- 2 files changed, 62 deletions(-) delete mode 100644 src/MiniExcel.Core/WriteAdapters/MiniExcelDataReaderWriteAdapter.cs diff --git a/src/MiniExcel.Core/WriteAdapters/MiniExcelDataReaderWriteAdapter.cs b/src/MiniExcel.Core/WriteAdapters/MiniExcelDataReaderWriteAdapter.cs deleted file mode 100644 index 12e73a2e..00000000 --- a/src/MiniExcel.Core/WriteAdapters/MiniExcelDataReaderWriteAdapter.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace MiniExcelLib.Core.WriteAdapters; - -internal class MiniExcelDataReaderWriteAdapter(IMiniExcelDataReader reader, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync -{ - private readonly IMiniExcelDataReader _reader = reader; - private readonly MiniExcelBaseConfiguration _configuration = configuration; - - public async Task?> GetColumnsAsync() - { - List props = []; - for (var i = 0; i < _reader.FieldCount; i++) - { - var columnName = await _reader.GetNameAsync(i).ConfigureAwait(false); - - if (!_configuration.DynamicColumnFirst) - { - var prop = CustomPropertyHelper.GetColumnInfosFromDynamicConfiguration(columnName, _configuration); - props.Add(prop); - continue; - } - - if (_configuration.DynamicColumns?.Any(a => string.Equals(a.Key, columnName, StringComparison.OrdinalIgnoreCase)) ?? false) - { - var prop = CustomPropertyHelper.GetColumnInfosFromDynamicConfiguration(columnName, _configuration); - props.Add(prop); - } - } - return props; - } - - public async IAsyncEnumerable> GetRowsAsync(List props, [EnumeratorCancellation] CancellationToken cancellationToken) - { - while (await _reader.ReadAsync(cancellationToken).ConfigureAwait(false)) - { - cancellationToken.ThrowIfCancellationRequested(); - yield return GetRowValuesAsync(props); - } - } - - private async IAsyncEnumerable GetRowValuesAsync(List props) - { - for (int i = 0, column = 1; i < _reader.FieldCount; i++, column++) - { - if (_configuration.DynamicColumnFirst) - { - var columnIndex = _reader.GetOrdinal(props[i].Key.ToString()); - yield return new CellWriteInfo(await _reader.GetValueAsync(columnIndex).ConfigureAwait(false), column, props[i]); - } - else - { - yield return new CellWriteInfo(await _reader.GetValueAsync(i).ConfigureAwait(false), column, props[i]); - } - } - } -} \ No newline at end of file diff --git a/src/MiniExcel.Core/WriteAdapters/MiniExcelWriteAdapterFactory.cs b/src/MiniExcel.Core/WriteAdapters/MiniExcelWriteAdapterFactory.cs index 4c89aa73..8abc3734 100644 --- a/src/MiniExcel.Core/WriteAdapters/MiniExcelWriteAdapterFactory.cs +++ b/src/MiniExcel.Core/WriteAdapters/MiniExcelWriteAdapterFactory.cs @@ -11,13 +11,6 @@ public static bool TryGetAsyncWriteAdapter(object values, MiniExcelBaseConfigura writeAdapter = Activator.CreateInstance(writeAdapterType, values, configuration) as IMiniExcelWriteAdapterAsync; return true; } - - if (values is IMiniExcelDataReader miniExcelDataReader) - { - writeAdapter = new MiniExcelDataReaderWriteAdapter(miniExcelDataReader, configuration); - return true; - } - return false; } From 997e76fd06e736d716e3d46c1087fc5217862035 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Mon, 2 Mar 2026 00:53:16 +0100 Subject: [PATCH 3/5] Simplifying the logic for retrieving cell values to export from a data reader There is no usefulness in forcing each field to be retrieved asynchronously since each row is fully loaded in memory anyways. Removing the superfluous IAsyncEnumerables cuts down a lot of overhead. --- .../Abstractions/IMiniExcelWriteAdapterAsync.cs | 2 +- .../AsyncEnumerableWriteAdapter.cs | 17 +++++++++-------- src/MiniExcel.Csv/CsvWriter.cs | 2 +- src/MiniExcel.OpenXml/OpenXmlWriter.cs | 12 ++++++------ 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapterAsync.cs b/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapterAsync.cs index 14a9aeb9..41908a66 100644 --- a/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapterAsync.cs +++ b/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapterAsync.cs @@ -3,5 +3,5 @@ public interface IMiniExcelWriteAdapterAsync { Task?> GetColumnsAsync(); - IAsyncEnumerable> GetRowsAsync(List props, CancellationToken cancellationToken); + IAsyncEnumerable GetRowsAsync(List props, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs b/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs index 3cb76af6..671abd5f 100644 --- a/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs +++ b/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs @@ -27,12 +27,10 @@ internal sealed class AsyncEnumerableWriteAdapter(IAsyncEnumerable values, return CustomPropertyHelper.GetColumnInfoFromValue(_enumerator.Current, _configuration); } - public async IAsyncEnumerable> GetRowsAsync(List props, [EnumeratorCancellation] CancellationToken cancellationToken) + public async IAsyncEnumerable GetRowsAsync(List props, [EnumeratorCancellation] CancellationToken cancellationToken) { if (_empty) - { yield break; - } if (_enumerator is null) { @@ -46,16 +44,16 @@ public async IAsyncEnumerable> GetRowsAsync(List do { cancellationToken.ThrowIfCancellationRequested(); - yield return GetRowValuesAsync(_enumerator.Current, props); + yield return GetRowValues(_enumerator.Current, props); } while (await _enumerator.MoveNextAsync().ConfigureAwait(false)); } -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - private static async IAsyncEnumerable GetRowValuesAsync(T currentValue, List props) -#pragma warning restore CS1998 + private static CellWriteInfo[] GetRowValues(T currentValue, List props) { var column = 0; + var result = new List(); + foreach (var prop in props) { column++; @@ -63,13 +61,16 @@ private static async IAsyncEnumerable GetRowValuesAsync(T current if (prop is null) continue; - yield return currentValue switch + var info = currentValue switch { IDictionary 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) }; + result.Add(info); } + + return result.ToArray(); } public async ValueTask DisposeAsync() diff --git a/src/MiniExcel.Csv/CsvWriter.cs b/src/MiniExcel.Csv/CsvWriter.cs index 7b220366..2f615494 100644 --- a/src/MiniExcel.Csv/CsvWriter.cs +++ b/src/MiniExcel.Csv/CsvWriter.cs @@ -125,7 +125,7 @@ await _writer.WriteAsync(newLine cancellationToken.ThrowIfCancellationRequested(); rowBuilder.Clear(); - await foreach (var column in row.WithCancellation(cancellationToken).ConfigureAwait(false)) + foreach (var column in row) { AppendColumn(rowBuilder, column); progress?.Report(1); diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.cs index c15b896c..dc4875d3 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.cs @@ -312,7 +312,7 @@ private async Task WriteValuesAsync(EnhancedStreamWriter writer, object val foreach (var cellValue in row) { cancellationToken.ThrowIfCancellationRequested(); - await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Prop, widths).ConfigureAwait(false); + await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Prop, widths, cancellationToken).ConfigureAwait(false); progress?.Report(1); } await writer.WriteAsync(WorksheetXml.EndRow, cancellationToken).ConfigureAwait(false); @@ -326,9 +326,9 @@ private async Task WriteValuesAsync(EnhancedStreamWriter writer, object val cancellationToken.ThrowIfCancellationRequested(); await writer.WriteAsync(WorksheetXml.StartRow(++currentRowIndex), cancellationToken).ConfigureAwait(false); - await foreach (var cellValue in row.ConfigureAwait(false).WithCancellation(cancellationToken)) + foreach (var cellValue in row) { - await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Prop, widths).ConfigureAwait(false); + await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Prop, widths, cancellationToken).ConfigureAwait(false); progress?.Report(1); } await writer.WriteAsync(WorksheetXml.EndRow, cancellationToken).ConfigureAwait(false); @@ -451,7 +451,7 @@ private async Task WriteCellAsync(EnhancedStreamWriter writer, string cellRefere } [CreateSyncVersion] - private async Task WriteCellAsync(EnhancedStreamWriter writer, int rowIndex, int cellIndex, object? value, MiniExcelColumnInfo columnInfo, ExcelWidthCollection? widthCollection) + private async Task WriteCellAsync(EnhancedStreamWriter writer, int rowIndex, int cellIndex, object? value, MiniExcelColumnInfo columnInfo, ExcelWidthCollection? widthCollection, CancellationToken cancellationToken = default) { if (columnInfo?.CustomFormatter is not null) { @@ -470,7 +470,7 @@ private async Task WriteCellAsync(EnhancedStreamWriter writer, int rowIndex, int if (_configuration.EnableWriteNullValueCell && valueIsNull) { - await writer.WriteAsync(WorksheetXml.EmptyCell(columnReference, GetCellXfId("2"))).ConfigureAwait(false); + await writer.WriteAsync(WorksheetXml.EmptyCell(columnReference, GetCellXfId("2")), cancellationToken).ConfigureAwait(false); return; } @@ -484,7 +484,7 @@ private async Task WriteCellAsync(EnhancedStreamWriter writer, int rowIndex, int /*Prefix and suffix blank space will lost after SaveAs #294*/ var preserveSpace = cellValue is [' ', ..] or [.., ' ']; - await writer.WriteAsync(WorksheetXml.Cell(columnReference, dataType, GetCellXfId(styleIndex), cellValue, preserveSpace: preserveSpace, columnType: columnType)).ConfigureAwait(false); + await writer.WriteAsync(WorksheetXml.Cell(columnReference, dataType, GetCellXfId(styleIndex), cellValue, preserveSpace: preserveSpace, columnType: columnType), cancellationToken).ConfigureAwait(false); widthCollection?.AdjustWidth(cellIndex, cellValue); } From d3f33be9eb3a8bd6dd2910a9fd570a9906989f79 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Mon, 2 Mar 2026 01:13:22 +0100 Subject: [PATCH 4/5] Adding test for issue 408 --- .../MiniExcelIssueTests.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs index 5d051b32..1759ca72 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs @@ -2719,6 +2719,23 @@ public void IssueI50VD5() } } + [Fact] + public void TestIssue408() + { + using var reader = _excelImporter.GetDataReader(PathHelper.GetFile("xlsx/TestTypeMapping.xlsx"), true); + + var dt = new DataTable(); + dt.Load(reader); + + Assert.Equal(100, dt.Rows.Count); + Assert.Equal("78DE23D2-DCB6-BD3D-EC67-C112BBC322A2", dt.Rows[0]["ID"]); + Assert.Equal("Wade", dt.Rows[0]["Name"]); + Assert.Equal(new DateTime(2020, 9, 27), Convert.ToDateTime(dt.Rows[0]["BoD"])); + Assert.Equal(36d, dt.Rows[0]["Age"]); + Assert.False(Convert.ToBoolean(dt.Rows[0]["VIP"])); + Assert.Equal(5019.12, dt.Rows[0]["Points"]); + } + private class Issues409_881 { public string Units { get; set; } @@ -3807,4 +3824,4 @@ public void TestIssue915() Assert.Equal(7.4, result[1].Altitude); Assert.Equal(8.6, result[2].Altitude); } -} \ No newline at end of file +} From dcfbdcf0e8b45819fe76af693e978749fe08bd73 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Mon, 2 Mar 2026 23:15:19 +0100 Subject: [PATCH 5/5] Corrections - Reintating the CsvConfiguration parameter removed by mistake from the GeDataReader methods in the CsvImporter - Removing superfluous preprocessor instruction in IMiniExcelDataReader interface - Adding checks for empty data source and for correct buffering of char array in the MiniExcelDataReader class - Fixing test 408 --- .../Abstractions/IMiniExcelDataReader.cs | 5 +-- src/MiniExcel.Core/MiniExcelDataReader.cs | 37 ++++++++++++------- src/MiniExcel.Csv/Api/CsvImporter.cs | 18 +++++---- .../MiniExcelIssueTests.cs | 4 +- 4 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/MiniExcel.Core/Abstractions/IMiniExcelDataReader.cs b/src/MiniExcel.Core/Abstractions/IMiniExcelDataReader.cs index c7fa756a..95e51bfa 100644 --- a/src/MiniExcel.Core/Abstractions/IMiniExcelDataReader.cs +++ b/src/MiniExcel.Core/Abstractions/IMiniExcelDataReader.cs @@ -1,9 +1,6 @@ namespace MiniExcelLib.Core.Abstractions; -public interface IMiniExcelDataReader : IDataReader -#if NET8_0_OR_GREATER - ,IAsyncDisposable -#endif +public interface IMiniExcelDataReader : IDataReader, IAsyncDisposable { Task CloseAsync(); Task ReadAsync(CancellationToken cancellationToken = default); diff --git a/src/MiniExcel.Core/MiniExcelDataReader.cs b/src/MiniExcel.Core/MiniExcelDataReader.cs index 52888a8a..6f34a85b 100644 --- a/src/MiniExcel.Core/MiniExcelDataReader.cs +++ b/src/MiniExcel.Core/MiniExcelDataReader.cs @@ -6,6 +6,7 @@ public sealed class MiniExcelDataReader : IMiniExcelDataReader private readonly IAsyncEnumerator>? _asyncSource; private readonly Stream _stream; + private bool _isEmpty; private List _columns = []; private DataTable? _schema; @@ -37,8 +38,12 @@ public static MiniExcelDataReader Create(Stream? stream, IEnumerable CreateAsync(Stream? stream, IAsync { reader._columns = reader._asyncSource.Current.Keys.ToList(); reader.FieldCount = reader._columns.Count; - reader.Depth++; } + else + { + reader._isEmpty = true; + } + return reader; } @@ -73,7 +82,7 @@ public bool Read() if (_isFirst) { _isFirst = false; - return true; + return !_isEmpty; } return _source!.MoveNext(); @@ -81,17 +90,17 @@ public bool Read() public async Task ReadAsync(CancellationToken cancellationToken = default) { - if (!_isAsyncSource) - return await Task.FromResult(Read()).ConfigureAwait(false); - if (IsClosed) throw new InvalidOperationException("The data reader has been closed"); + if (!_isAsyncSource) + return await Task.FromResult(Read()).ConfigureAwait(false); + Depth++; if (_isFirst) { _isFirst = false; - return true; + return !_isEmpty; } return await _asyncSource!.MoveNextAsync().ConfigureAwait(false); @@ -114,12 +123,14 @@ public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, public long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) { - var s = GetString(i).Substring((int)fieldoffset, length); + var s = GetString(i); + var len = Math.Min(length, s.Length - (int)fieldoffset); + var subs = s.Substring((int)fieldoffset, len); - for (int j = bufferoffset; j < s.Length; j++) - buffer.AsSpan()[j] = s.AsSpan()[j]; - - return s.Length; + if (buffer is not null) + subs.AsSpan().CopyTo(buffer.AsSpan(bufferoffset)); + + return subs.Length; } public bool GetBoolean(int i) => GetValue(i) switch @@ -220,7 +231,7 @@ public int GetValues(object?[] values) { var count = Math.Min(values.Length, FieldCount); - for (int i = 0; i < values.Length; i++) + for (int i = 0; i < count; i++) values[i] = GetValue(i); return count; diff --git a/src/MiniExcel.Csv/Api/CsvImporter.cs b/src/MiniExcel.Csv/Api/CsvImporter.cs index efaa6f54..306c29ed 100644 --- a/src/MiniExcel.Csv/Api/CsvImporter.cs +++ b/src/MiniExcel.Csv/Api/CsvImporter.cs @@ -157,10 +157,10 @@ public async Task> GetColumnNamesAsync(Stream stream, bool u /// /// Asynchronous reads are not allowed when creating the data reader from this overload and will result in an exception. /// - public MiniExcelDataReader GetDataReader(string path, bool useHeaderRow = false) + public MiniExcelDataReader GetDataReader(string path, bool useHeaderRow = false, CsvConfiguration? configuration = null) { var stream = FileHelper.OpenSharedRead(path); - var values = Query(stream, useHeaderRow).Cast>(); + var values = Query(stream, useHeaderRow, configuration).Cast>(); return MiniExcelDataReader.Create(stream, values); } @@ -171,9 +171,9 @@ public MiniExcelDataReader GetDataReader(string path, bool useHeaderRow = false) /// /// Asynchronous reads are not allowed when creating the data reader from this overload and will result in an exception. /// - public MiniExcelDataReader GetDataReader(Stream stream, bool useHeaderRow = false) + public MiniExcelDataReader GetDataReader(Stream stream, bool useHeaderRow = false, CsvConfiguration ? configuration = null) { - var values = Query(stream, useHeaderRow).Cast>(); + var values = Query(stream, useHeaderRow, configuration).Cast>(); return MiniExcelDataReader.Create(stream, values); } @@ -181,10 +181,11 @@ public MiniExcelDataReader GetDataReader(Stream stream, bool useHeaderRow = fals /// Gets an for the Csv document at the specific path. /// When created from this overload, the resulting data reader is supposed to be advanced asynchronously. /// - public async Task GetAsyncDataReader(string path, bool useHeaderRow = false, CancellationToken cancellationToken = default) + public async Task GetAsyncDataReader(string path, bool useHeaderRow = false, + CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); - var values = QueryAsync(stream, useHeaderRow, cancellationToken: cancellationToken); + var values = QueryAsync(stream, useHeaderRow, configuration, cancellationToken); return await MiniExcelDataReader.CreateAsync(stream, CastAsync(values, cancellationToken)).ConfigureAwait(false); } @@ -193,9 +194,10 @@ public async Task GetAsyncDataReader(string path, bool useH /// Gets an for the Csv document at the specific path. /// When created from this overload, the resulting data reader is supposed to be advanced asynchronously. /// - public async Task GetAsyncDataReader(Stream stream, bool useHeaderRow = false, CancellationToken cancellationToken = default) + public async Task GetAsyncDataReader(Stream stream, bool useHeaderRow = false, + CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) { - var values = QueryAsync(stream, useHeaderRow, cancellationToken: cancellationToken); + var values = QueryAsync(stream, useHeaderRow, configuration, cancellationToken); return await MiniExcelDataReader.CreateAsync(stream, CastAsync(values, cancellationToken)).ConfigureAwait(false); } diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs index 1759ca72..32ed7d05 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs @@ -2022,7 +2022,7 @@ public void Issue220() public void Issue215() { using var stream = new MemoryStream(); - _excelExporter.Export(stream, new[] { new { V = "test1" }, new { V = "test2" } }); + _excelExporter.Export(stream, new[] { new { V = "test1" }, new { V = "test2" } }); var rows = _excelImporter.Query(stream, true).ToList(); Assert.Equal("test1", rows[0].V); @@ -2730,7 +2730,7 @@ public void TestIssue408() Assert.Equal(100, dt.Rows.Count); Assert.Equal("78DE23D2-DCB6-BD3D-EC67-C112BBC322A2", dt.Rows[0]["ID"]); Assert.Equal("Wade", dt.Rows[0]["Name"]); - Assert.Equal(new DateTime(2020, 9, 27), Convert.ToDateTime(dt.Rows[0]["BoD"])); + Assert.Equal("27/09/2020", dt.Rows[0]["BoD"]); Assert.Equal(36d, dt.Rows[0]["Age"]); Assert.False(Convert.ToBoolean(dt.Rows[0]["VIP"])); Assert.Equal(5019.12, dt.Rows[0]["Points"]);