diff --git a/src/MiniExcel.Core/Abstractions/IMiniExcelDataReader.cs b/src/MiniExcel.Core/Abstractions/IMiniExcelDataReader.cs index a36aec05..95e51bfa 100644 --- a/src/MiniExcel.Core/Abstractions/IMiniExcelDataReader.cs +++ b/src/MiniExcel.Core/Abstractions/IMiniExcelDataReader.cs @@ -1,13 +1,7 @@ namespace MiniExcelLib.Core.Abstractions; -public interface IMiniExcelDataReader : IDataReader -#if NET8_0_OR_GREATER - ,IAsyncDisposable -#endif +public interface IMiniExcelDataReader : IDataReader, IAsyncDisposable { 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/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/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..6f34a85b --- /dev/null +++ b/src/MiniExcel.Core/MiniExcelDataReader.cs @@ -0,0 +1,317 @@ +namespace MiniExcelLib.Core; + +public sealed class MiniExcelDataReader : IMiniExcelDataReader +{ + private readonly IEnumerator>? _source; + private readonly IAsyncEnumerator>? _asyncSource; + private readonly Stream _stream; + + private bool _isEmpty; + 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; + } + else + { + reader._isEmpty = true; + } + + 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; + } + else + { + reader._isEmpty = true; + } + + 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 !_isEmpty; + } + + return _source!.MoveNext(); + } + + public async Task ReadAsync(CancellationToken cancellationToken = default) + { + 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 !_isEmpty; + } + + 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); + var len = Math.Min(length, s.Length - (int)fieldoffset); + var subs = s.Substring((int)fieldoffset, len); + + if (buffer is not null) + subs.AsSpan().CopyTo(buffer.AsSpan(bufferoffset)); + + return subs.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 < count; 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.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.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; } diff --git a/src/MiniExcel.Csv/Api/CsvImporter.cs b/src/MiniExcel.Csv/Api/CsvImporter.cs index a63048f2..306c29ed 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,6 +151,12 @@ public async Task> GetColumnNamesAsync(Stream stream, bool u #region DataReader + /// + /// 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, CsvConfiguration? configuration = null) { var stream = FileHelper.OpenSharedRead(path); @@ -160,28 +165,40 @@ public MiniExcelDataReader GetDataReader(string path, bool useHeaderRow = false, 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, CsvConfiguration ? configuration = null) { var values = Query(stream, useHeaderRow, configuration).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, + CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) { var stream = FileHelper.OpenSharedRead(path); var values = QueryAsync(stream, useHeaderRow, 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, - 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, + CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) { var values = QueryAsync(stream, useHeaderRow, configuration, cancellationToken); - return await MiniExcelAsyncDataReader.CreateAsync(stream, CastAsync(values, cancellationToken)).ConfigureAwait(false); + return await MiniExcelDataReader.CreateAsync(stream, CastAsync(values, cancellationToken)).ConfigureAwait(false); } #endregion @@ -201,4 +218,4 @@ public async Task GetAsyncDataReader(Stream stream, bo yield return dict; } } -} \ No newline at end of file +} 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/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.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); } 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") }; } diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs index 5d051b32..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); @@ -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("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"]); + } + 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 +}