diff --git a/src/MiniExcel/Csv/CsvWriter.cs b/src/MiniExcel/Csv/CsvWriter.cs index ee50da7d..bc6d1cb5 100644 --- a/src/MiniExcel/Csv/CsvWriter.cs +++ b/src/MiniExcel/Csv/CsvWriter.cs @@ -140,10 +140,8 @@ public void Insert() await Task.Run(() => SaveAs(), cancellationToken).ConfigureAwait(false); } - private void GenerateSheetByIDataReader(object value, string seperator, string newLine, StreamWriter writer) + private void GenerateSheetByIDataReader(IDataReader reader, string seperator, string newLine, StreamWriter writer) { - var reader = (IDataReader)value; - int fieldCount = reader.FieldCount; if (fieldCount == 0) throw new InvalidDataException("fieldCount is 0"); diff --git a/src/MiniExcel/IMiniExcelDataReader.cs b/src/MiniExcel/IMiniExcelDataReader.cs new file mode 100644 index 00000000..3f304451 --- /dev/null +++ b/src/MiniExcel/IMiniExcelDataReader.cs @@ -0,0 +1,24 @@ +using System; +using System.Data; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniExcelLibs +{ +#if !NET8_0_OR_GREATER + public interface IMiniExcelDataReader : IDataReader +#else + public interface IMiniExcelDataReader : IDataReader, IAsyncDisposable +#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); + } +} diff --git a/src/MiniExcel/MiniExcelDataReader.cs b/src/MiniExcel/MiniExcelDataReader.cs index a2241f62..f2ae9a74 100644 --- a/src/MiniExcel/MiniExcelDataReader.cs +++ b/src/MiniExcel/MiniExcelDataReader.cs @@ -65,6 +65,7 @@ public override string GetName(int i) { return _keys[i]; } + /// protected override void Dispose(bool disposing) { diff --git a/src/MiniExcel/MiniExcelDataReaderBase.cs b/src/MiniExcel/MiniExcelDataReaderBase.cs index 3832bf6d..402e96c0 100644 --- a/src/MiniExcel/MiniExcelDataReaderBase.cs +++ b/src/MiniExcel/MiniExcelDataReaderBase.cs @@ -2,11 +2,13 @@ { using System; using System.Data; + using System.Threading; + using System.Threading.Tasks; /// - /// IDataReader Base Class + /// IMiniExcelDataReader Base Class /// - public abstract class MiniExcelDataReaderBase : IDataReader + public abstract class MiniExcelDataReaderBase : IMiniExcelDataReader { /// /// @@ -202,6 +204,30 @@ public abstract class MiniExcelDataReaderBase : IDataReader /// public virtual bool NextResult() => false; + /// + /// + /// + /// + /// + public virtual Task NextResultAsync(CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return MiniExcelTask.FromCanceled(cancellationToken); + } + else + { + try + { + return NextResult() ? Task.FromResult(true) : Task.FromResult(false); + } + catch (Exception e) + { + return MiniExcelTask.FromException(e); + } + } + } + /// /// /// @@ -209,6 +235,31 @@ public abstract class MiniExcelDataReaderBase : IDataReader /// public abstract string GetName(int i); + /// + /// + /// + /// + /// + /// + public virtual Task GetNameAsync(int i, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return MiniExcelTask.FromCanceled(cancellationToken); + } + else + { + try + { + return Task.FromResult(GetName(i)); + } + catch (Exception e) + { + return MiniExcelTask.FromException(e); + } + } + } + /// /// /// @@ -216,12 +267,61 @@ public abstract class MiniExcelDataReaderBase : IDataReader /// public abstract object GetValue(int i); + /// + /// + /// + /// + /// + /// + public virtual Task GetValueAsync(int i, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return MiniExcelTask.FromCanceled(cancellationToken); + } + else + { + try + { + return Task.FromResult(GetValue(i)); + } + catch (Exception e) + { + return MiniExcelTask.FromException(e); + } + } + } + /// /// /// /// public abstract bool Read(); + /// + /// + /// + /// + /// + public virtual Task ReadAsync(CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return MiniExcelTask.FromCanceled(cancellationToken); + } + else + { + try + { + return Read() ? Task.FromResult(true) : Task.FromResult(false); + } + catch (Exception e) + { + return MiniExcelTask.FromException(e); + } + } + } + /// /// /// @@ -233,10 +333,18 @@ public virtual void Close() /// /// /// - /// - protected virtual void Dispose(bool disposing) + /// + public virtual Task CloseAsync() { - + try + { + Close(); + return MiniExcelTask.CompletedTask; + } + catch (Exception e) + { + return MiniExcelTask.FromException(e); + } } /// @@ -247,5 +355,30 @@ 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(); + } + } } } diff --git a/src/MiniExcel/MiniExcelLibs.csproj b/src/MiniExcel/MiniExcelLibs.csproj index 12c9acf3..6cb332ce 100644 --- a/src/MiniExcel/MiniExcelLibs.csproj +++ b/src/MiniExcel/MiniExcelLibs.csproj @@ -1,7 +1,7 @@  net45;netstandard2.0;net8.0; - 1.36.0 + 1.36.1 MiniExcel diff --git a/src/MiniExcel/MiniExcelTask.cs b/src/MiniExcel/MiniExcelTask.cs new file mode 100644 index 00000000..06f8d414 --- /dev/null +++ b/src/MiniExcel/MiniExcelTask.cs @@ -0,0 +1,59 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniExcelLibs +{ + internal class MiniExcelTask + { +#if NET45 + public static Task CompletedTask = Task.FromResult(0); +#else + public static Task CompletedTask = Task.CompletedTask; +#endif + + public static Task FromException(Exception exception) + { +#if NET45 + var tcs = new TaskCompletionSource(); + tcs.SetException(exception); + return tcs.Task; +#else + return Task.FromException(exception); +#endif + } + + public static Task FromException(Exception exception) + { +#if NET45 + var tcs = new TaskCompletionSource(); + tcs.SetException(exception); + return tcs.Task; +#else + return Task.FromException(exception); +#endif + } + + public static Task FromCanceled(CancellationToken cancellationToken) + { +#if NET45 + var tcs = new TaskCompletionSource(); + cancellationToken.Register(() => tcs.SetCanceled()); + return tcs.Task; +#else + return Task.FromCanceled(cancellationToken); +#endif + } + + public static Task FromCanceled(CancellationToken cancellationToken) + { +#if NET45 + var tcs = new TaskCompletionSource(); + cancellationToken.Register(() => tcs.SetCanceled()); + return tcs.Task; +#else + return Task.FromCanceled(cancellationToken); +#endif + } + } +} diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs index 07b3f73f..5c0b08ca 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs @@ -1,12 +1,10 @@ -using MiniExcelLibs.Attributes; -using MiniExcelLibs.OpenXml.Constants; +using MiniExcelLibs.OpenXml.Constants; using MiniExcelLibs.Utils; using MiniExcelLibs.Zip; using System; using System.Collections; using System.Collections.Generic; using System.Data; -using System.Globalization; using System.IO.Compression; using System.Linq; using System.Text; @@ -58,7 +56,7 @@ private async Task CreateSheetXmlAsync(object value, string sheetPath, Cancellat switch (value) { case IDataReader dataReader: - await GenerateSheetByIDataReaderAsync(writer, dataReader); + await GenerateSheetByIDataReaderAsync(writer, dataReader, cancellationToken); break; case IEnumerable enumerable: await GenerateSheetByEnumerableAsync(writer, enumerable); @@ -98,8 +96,14 @@ private async Task WriteDimensionAsync(MiniExcelAsyncStreamWriter writer, int ma writer.SetPosition(position); } - private async Task GenerateSheetByIDataReaderAsync(MiniExcelAsyncStreamWriter writer, IDataReader reader) + private async Task GenerateSheetByIDataReaderAsync(MiniExcelAsyncStreamWriter writer, IDataReader reader, CancellationToken cancellationToken) { + if (reader is IMiniExcelDataReader miniExcelDataReader) + { + await GenerateSheetByIMiniExcelDataReaderAsync(writer, miniExcelDataReader, cancellationToken); + return; + } + long dimensionPlaceholderPostition = 0; await writer.WriteAsync(WorksheetXml.StartWorksheet); var yIndex = 1; @@ -157,7 +161,10 @@ private async Task GenerateSheetByIDataReaderAsync(MiniExcelAsyncStreamWriter wr if (_printHeader) { await PrintHeaderAsync(writer, props); - yIndex++; + if (props.Count > 0) + { + yIndex++; + } } while (reader.Read()) @@ -208,6 +215,119 @@ private async Task GenerateSheetByIDataReaderAsync(MiniExcelAsyncStreamWriter wr } } + private async Task GenerateSheetByIMiniExcelDataReaderAsync(MiniExcelAsyncStreamWriter writer, IMiniExcelDataReader reader, CancellationToken cancellationToken) + { + long dimensionPlaceholderPostition = 0; + await writer.WriteAsync(WorksheetXml.StartWorksheet); + var yIndex = 1; + int maxColumnIndex; + int maxRowIndex; + ExcelWidthCollection widths = null; + long columnWidthsPlaceholderPosition = 0; + { + if (_configuration.FastMode) + { + dimensionPlaceholderPostition = await WriteDimensionPlaceholderAsync(writer); + } + + var props = new List(); + for (var i = 0; i < reader.FieldCount; i++) + { + var columnName = await reader.GetNameAsync(i, cancellationToken); + + if (!_configuration.DynamicColumnFirst) + { + var prop = GetColumnInfosFromDynamicConfiguration(columnName); + props.Add(prop); + continue; + } + + if (_configuration + .DynamicColumns + .Any(a => string.Equals( + a.Key, + columnName, + StringComparison.OrdinalIgnoreCase))) + + { + var prop = GetColumnInfosFromDynamicConfiguration(columnName); + props.Add(prop); + } + } + maxColumnIndex = props.Count; + + //sheet view + await writer.WriteAsync(GetSheetViews()); + + if (_configuration.EnableAutoWidth) + { + columnWidthsPlaceholderPosition = await WriteColumnWidthPlaceholdersAsync(writer, props); + widths = new ExcelWidthCollection(_configuration.MinWidth, _configuration.MaxWidth, props); + } + else + { + await WriteColumnsWidthsAsync(writer, ExcelColumnWidth.FromProps(props)); + } + + await writer.WriteAsync(WorksheetXml.StartSheetData); + int fieldCount = reader.FieldCount; + if (_printHeader) + { + await PrintHeaderAsync(writer, props); + if (props.Count > 0) + { + yIndex++; + } + } + + while (await reader.ReadAsync(cancellationToken)) + { + await writer.WriteAsync(WorksheetXml.StartRow(yIndex)); + var xIndex = 1; + for (int i = 0; i < fieldCount; i++) + { + object cellValue; + + if (_configuration.DynamicColumnFirst) + { + var columnIndex = reader.GetOrdinal(props[i].Key.ToString()); + cellValue = await reader.GetValueAsync(columnIndex, cancellationToken); + } + else + { + cellValue = await reader.GetValueAsync(i, cancellationToken); + } + + await WriteCellAsync(writer, yIndex, xIndex, cellValue, props[i], widths); + xIndex++; + } + await writer.WriteAsync(WorksheetXml.EndRow); + yIndex++; + } + + // Subtract 1 because cell indexing starts with 1 + maxRowIndex = yIndex - 1; + } + + await writer.WriteAsync(WorksheetXml.EndSheetData); + + if (_configuration.AutoFilter) + { + await writer.WriteAsync(WorksheetXml.Autofilter(GetDimensionRef(maxRowIndex, maxColumnIndex))); + } + + await writer.WriteAndFlushAsync(WorksheetXml.EndWorksheet); + + if (_configuration.FastMode) + { + await WriteDimensionAsync(writer, maxRowIndex, maxColumnIndex, dimensionPlaceholderPostition); + } + if (_configuration.EnableAutoWidth) + { + await OverWriteColumnWidthPlaceholdersAsync(writer, columnWidthsPlaceholderPosition, widths.Columns); + } + } + private async Task GenerateSheetByEnumerableAsync(MiniExcelAsyncStreamWriter writer, IEnumerable values) { var maxColumnIndex = 0; @@ -571,12 +691,12 @@ private async Task WriteCellAsync(MiniExcelAsyncStreamWriter writer, int rowInde { cellValue = p.CustomFormatter(cellValue); } - catch (Exception e) + catch { //ignored } } - + await writer.WriteAsync(WorksheetXml.Cell(columnReference, dataType, styleIndex, cellValue, preserveSpace: preserveSpace, columnType: columnType)); widthCollection?.AdjustWidth(cellIndex, cellValue); } diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs index 1dea68c0..f04606c5 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs @@ -387,7 +387,7 @@ private void GenerateSheetByDataTable(MiniExcelStreamWriter writer, DataTable va var prop = GetColumnInfosFromDynamicConfiguration(columnName); props.Add(prop); } - + //sheet view writer.Write(GetSheetViews()); @@ -570,19 +570,19 @@ private void WriteCell(MiniExcelStreamWriter writer, int rowIndex, int cellIndex var styleIndex = tuple.Item1; // https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.cell?view=openxml-3.0.1 var dataType = tuple.Item2; // https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.cellvalues?view=openxml-3.0.1 var cellValue = tuple.Item3; - + if (columnInfo?.CustomFormatter != null) { try { cellValue = columnInfo.CustomFormatter(cellValue); } - catch (Exception e) + catch { //ignored } } - + var columnType = columnInfo?.ExcelColumnType ?? ColumnType.Value; /*Prefix and suffix blank space will lost after SaveAs #294*/ diff --git a/tests/MiniExcelTests/MiniExcelOpenXmlAsyncTests.cs b/tests/MiniExcelTests/MiniExcelOpenXmlAsyncTests.cs index b49e73c9..07d68332 100644 --- a/tests/MiniExcelTests/MiniExcelOpenXmlAsyncTests.cs +++ b/tests/MiniExcelTests/MiniExcelOpenXmlAsyncTests.cs @@ -1489,5 +1489,32 @@ public async Task DynamicColumnsConfigurationIsUsedWhenCreatingExcelUsingDataTab Assert.Equal(onlyDate.ToDateTime(TimeOnly.MinValue), (DateTime)rows[1]["Column4"]); } } + + [Fact] + public async Task SaveAsByMiniExcelDataReader() + { + var path1 = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); + + var values = new List() + { + new Demo { Column1= "MiniExcel" ,Column2 = 1 }, + new Demo { Column1 = "Github", Column2 = 2 } + }; + await MiniExcel.SaveAsAsync(path1, values); + + var reader = (IMiniExcelDataReader)MiniExcel.GetReader(path1, true); + + var path2 = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); + + await MiniExcel.SaveAsAsync(path2, reader); + + var results = MiniExcel.Query(path2); + + Assert.True(results.Count() == 2); + Assert.True(results.First().Column1 == "MiniExcel"); + Assert.True(results.First().Column2 == 1); + Assert.True(results.Last().Column1 == "Github"); + Assert.True(results.Last().Column2 == 2); + } } } \ No newline at end of file