From 7894e28c4da463e4bd4e299f188a723c6a65b12d Mon Sep 17 00:00:00 2001 From: duszekmestre Date: Sun, 24 Mar 2024 14:27:47 +0100 Subject: [PATCH] Use true async processing for excel writer (#573) * Use true async processing for excel writer * Add async processing to other methods --------- Co-authored-by: Lukasz Arciszewski --- src/MiniExcel/MiniExcel.Async.cs | 6 +- src/MiniExcel/MiniExcel.cs | 2 + .../OpenXml/ExcelOpenXmlSheetWriter.Async.cs | 634 ++++++++++++++++++ .../OpenXml/ExcelOpenXmlSheetWriter.cs | 29 +- .../OpenXml/MiniExcelAsyncStreamWriter.cs | 71 ++ 5 files changed, 730 insertions(+), 12 deletions(-) create mode 100644 src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs create mode 100644 src/MiniExcel/OpenXml/MiniExcelAsyncStreamWriter.cs diff --git a/src/MiniExcel/MiniExcel.Async.cs b/src/MiniExcel/MiniExcel.Async.cs index 63edd532..0da5d6e4 100644 --- a/src/MiniExcel/MiniExcel.Async.cs +++ b/src/MiniExcel/MiniExcel.Async.cs @@ -12,7 +12,11 @@ public static partial class MiniExcel { public static async Task SaveAsAsync(string path, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration configuration = null, bool overwriteFile = false,CancellationToken cancellationToken = default(CancellationToken)) { - await Task.Run(() => SaveAs(path, value, printHeader, sheetName, excelType, configuration, overwriteFile),cancellationToken).ConfigureAwait(false); + if (Path.GetExtension(path).ToLowerInvariant() == ".xlsm") + throw new NotSupportedException("MiniExcel SaveAs not support xlsm"); + + using (var stream = overwriteFile ? File.Create(path) : new FileStream(path, FileMode.CreateNew)) + await SaveAsAsync(stream, value, printHeader, sheetName, ExcelTypeHelper.GetExcelType(path, excelType), configuration); } public static async Task SaveAsAsync(this Stream stream, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, IConfiguration configuration = null,CancellationToken cancellationToken = default(CancellationToken)) diff --git a/src/MiniExcel/MiniExcel.cs b/src/MiniExcel/MiniExcel.cs index abf56496..dea683e3 100644 --- a/src/MiniExcel/MiniExcel.cs +++ b/src/MiniExcel/MiniExcel.cs @@ -8,6 +8,8 @@ using System.Dynamic; using System.IO; using System.Linq; + using System.Threading; + using System.Threading.Tasks; using Utils; using Zip; diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs new file mode 100644 index 00000000..c6b87886 --- /dev/null +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs @@ -0,0 +1,634 @@ +using MiniExcelLibs.Utils; +using MiniExcelLibs.Zip; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MiniExcelLibs.OpenXml +{ + internal partial class ExcelOpenXmlSheetWriter : IExcelWriter + { + public async Task SaveAsAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + await GenerateDefaultOpenXmlAsync(cancellationToken); + + switch (_value) + { + case IDictionary sheets: + { + var sheetId = 0; + _sheets.RemoveAt(0);//TODO:remove + foreach (var sheet in sheets) + { + sheetId++; + var sheetInfos = GetSheetInfos(sheet.Key); + var sheetDto = sheetInfos.ToDto(sheetId); + _sheets.Add(sheetDto); //TODO:remove + + currentSheetIndex = sheetId; + + await CreateSheetXmlAsync(sheet.Value, sheetDto.Path, cancellationToken); + } + + break; + } + + case DataSet sheets: + { + var sheetId = 0; + _sheets.RemoveAt(0);//TODO:remove + foreach (DataTable dt in sheets.Tables) + { + sheetId++; + var sheetInfos = GetSheetInfos(dt.TableName); + var sheetDto = sheetInfos.ToDto(sheetId); + _sheets.Add(sheetDto); //TODO:remove + + currentSheetIndex = sheetId; + + await CreateSheetXmlAsync(dt, sheetDto.Path, cancellationToken); + } + + break; + } + + default: + //Single sheet export. + currentSheetIndex++; + + await CreateSheetXmlAsync(_value, _sheets[0].Path, cancellationToken); + break; + } + + await GenerateEndXmlAsync(cancellationToken); + _archive.Dispose(); + } + + internal async Task GenerateDefaultOpenXmlAsync(CancellationToken cancellationToken) + { + await CreateZipEntryAsync("_rels/.rels", "application/vnd.openxmlformats-package.relationships+xml", ExcelOpenXmlSheetWriter._defaultRels, cancellationToken); + await CreateZipEntryAsync("xl/sharedStrings.xml", "application/vnd.openxmlformats-package.relationships+xml", ExcelOpenXmlSheetWriter._defaultSharedString, cancellationToken); + } + + private async Task CreateZipEntryAsync(string path, string contentType, string content, CancellationToken cancellationToken) + { + ZipArchiveEntry entry = _archive.CreateEntry(path, CompressionLevel.Fastest); + using (var zipStream = entry.Open()) + using (MiniExcelAsyncStreamWriter writer = new MiniExcelAsyncStreamWriter(zipStream, _utf8WithBom, _configuration.BufferSize, cancellationToken)) + await writer.WriteAsync(content); + if (!string.IsNullOrEmpty(contentType)) + _zipDictionary.Add(path, new ZipPackageInfo(entry, contentType)); + } + + private async Task CreateZipEntryAsync(string path, byte[] content, CancellationToken cancellationToken) + { + ZipArchiveEntry entry = _archive.CreateEntry(path, CompressionLevel.Fastest); + using (var zipStream = entry.Open()) + await zipStream.WriteAsync(content, 0, content.Length, cancellationToken); + } + + private async Task CreateSheetXmlAsync(object value, string sheetPath, CancellationToken cancellationToken) + { + ZipArchiveEntry entry = _archive.CreateEntry(sheetPath, CompressionLevel.Fastest); + using (var zipStream = entry.Open()) + using (MiniExcelAsyncStreamWriter writer = new MiniExcelAsyncStreamWriter(zipStream, _utf8WithBom, _configuration.BufferSize, cancellationToken)) + { + if (value == null) + { + await WriteEmptySheetAsync(writer); + goto End; //for re-using code + } + + //DapperRow + + switch (value) + { + case IDataReader dataReader: + await GenerateSheetByIDataReaderAsync(writer, dataReader); + break; + case IEnumerable enumerable: + await GenerateSheetByEnumerableAsync(writer, enumerable); + break; + case DataTable dataTable: + await GenerateSheetByDataTableAsync(writer, dataTable); + break; + default: + throw new NotImplementedException($"Type {value.GetType().FullName} is not implemented. Please open an issue."); + } + } + End: //for re-using code + _zipDictionary.Add(sheetPath, new ZipPackageInfo(entry, "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml")); + } + + private async Task WriteEmptySheetAsync(MiniExcelAsyncStreamWriter writer) + { + await writer.WriteAsync($@""); + } + + private async Task GenerateSheetByIDataReaderAsync(MiniExcelAsyncStreamWriter writer, IDataReader reader) + { + long dimensionWritePosition = 0; + await writer.WriteAsync($@""); + var xIndex = 1; + var yIndex = 1; + var maxColumnIndex = 0; + var maxRowIndex = 0; + { + + if (_configuration.FastMode) + { + dimensionWritePosition = await writer.WriteAndFlushAsync($@""); // end of code will be replaced + } + + var props = new List(); + for (var i = 0; i < reader.FieldCount; i++) + { + var columnName = reader.GetName(i); + var prop = GetColumnInfosFromDynamicConfiguration(columnName); + props.Add(prop); + } + maxColumnIndex = props.Count; + + await WriteColumnsWidthsAsync(writer, props); + + await writer.WriteAsync(""); + int fieldCount = reader.FieldCount; + if (_printHeader) + { + await PrintHeaderAsync(writer, props); + yIndex++; + } + + while (reader.Read()) + { + await writer.WriteAsync($""); + xIndex = 1; + for (int i = 0; i < fieldCount; i++) + { + var cellValue = reader.GetValue(i); + await WriteCellAsync(writer, yIndex, xIndex, cellValue, null); + xIndex++; + } + await writer.WriteAsync($""); + yIndex++; + } + + // Subtract 1 because cell indexing starts with 1 + maxRowIndex = yIndex - 1; + } + + await writer.WriteAsync(""); + if (_configuration.AutoFilter) + await writer.WriteAsync($""); + await writer.WriteAndFlushAsync(""); + + if (_configuration.FastMode) + { + writer.SetPosition(dimensionWritePosition); + await writer.WriteAndFlushAsync($@"{GetDimensionRef(maxRowIndex, maxColumnIndex)}"""); + } + } + + private async Task GenerateSheetByEnumerableAsync(MiniExcelAsyncStreamWriter writer, IEnumerable values) + { + var maxColumnIndex = 0; + var maxRowIndex = 0; + List props = null; + string mode = null; + + int? rowCount = null; + var collection = values as ICollection; + if (collection != null) + { + rowCount = collection.Count; + } + else if (!_configuration.FastMode) + { + // The row count is only required up front when not in fastmode + collection = new List(values.Cast()); + rowCount = collection.Count; + } + + // Get the enumerator once to ensure deferred linq execution + var enumerator = (collection ?? values).GetEnumerator(); + + // Move to the first item in order to inspect the value type and determine whether it is empty + var empty = !enumerator.MoveNext(); + + if (empty) + { + // only when empty IEnumerable need to check this issue #133 https://github.com/shps951023/MiniExcel/issues/133 + var genericType = TypeHelper.GetGenericIEnumerables(values).FirstOrDefault(); + if (genericType == null || genericType == typeof(object) // sometime generic type will be object, e.g: https://user-images.githubusercontent.com/12729184/132812859-52984314-44d1-4ee8-9487-2d1da159f1f0.png + || typeof(IDictionary).IsAssignableFrom(genericType) + || typeof(IDictionary).IsAssignableFrom(genericType)) + { + await WriteEmptySheetAsync(writer); + return; + } + else + { + SetGenericTypePropertiesMode(genericType, ref mode, out maxColumnIndex, out props); + } + } + else + { + var firstItem = enumerator.Current; + if (firstItem is IDictionary genericDic) + { + mode = "IDictionary"; + props = GetDictionaryColumnInfo(genericDic, null); + maxColumnIndex = props.Count; + } + else if (firstItem is IDictionary dic) + { + mode = "IDictionary"; + props = GetDictionaryColumnInfo(null, dic); + //maxColumnIndex = dic.Keys.Count; + maxColumnIndex = props.Count; // why not using keys, because ignore attribute ![image](https://user-images.githubusercontent.com/12729184/163686902-286abb70-877b-4e84-bd3b-001ad339a84a.png) + } + else + { + SetGenericTypePropertiesMode(firstItem.GetType(), ref mode, out maxColumnIndex, out props); + } + } + + await writer.WriteAsync($@""); + + long dimensionWritePosition = 0; + + // We can write the dimensions directly if the row count is known + if (_configuration.FastMode && rowCount == null) + { + // Write a placeholder for the table dimensions and save thee position for later + dimensionWritePosition = await writer.WriteAndFlushAsync(""); + } + else + { + maxRowIndex = rowCount.Value + (_printHeader && rowCount > 0 ? 1 : 0); + await writer.WriteAsync($@""); + } + + //cols:width + await WriteColumnsWidthsAsync(writer, props); + + //header + await writer.WriteAsync($@""); + var yIndex = 1; + var xIndex = 1; + if (_printHeader) + { + await PrintHeaderAsync(writer, props); + yIndex++; + } + + if (!empty) + { + // body + switch (mode) //Dapper Row + { + case "IDictionary": + maxRowIndex = await GenerateSheetByColumnInfoAsync>(writer, enumerator, props, xIndex, yIndex); + break; + case "IDictionary": + maxRowIndex = await GenerateSheetByColumnInfoAsync(writer, enumerator, props, xIndex, yIndex); + break; + case "Properties": + maxRowIndex = await GenerateSheetByColumnInfoAsync(writer, enumerator, props, xIndex, yIndex); + break; + default: + throw new NotImplementedException($"Type {values.GetType().FullName} is not implemented. Please open an issue."); + } + } + + await writer.WriteAsync(""); + if (_configuration.AutoFilter) + await writer.WriteAsync($""); + + // The dimension has already been written if row count is defined + if (_configuration.FastMode && rowCount == null) + { + // Flush and save position so that we can get back again. + var pos = await writer.FlushAsync(); + + // Seek back and write the dimensions of the table + writer.SetPosition(dimensionWritePosition); + await writer.WriteAndFlushAsync($@"{GetDimensionRef(maxRowIndex, maxColumnIndex)}"""); + writer.SetPosition(pos); + } + + await writer.WriteAsync(""); + } + + private async Task GenerateSheetByDataTableAsync(MiniExcelAsyncStreamWriter writer, DataTable value) + { + var xy = ExcelOpenXmlUtils.ConvertCellToXY("A1"); + + //GOTO Top Write: + await writer.WriteAsync($@""); + { + var yIndex = xy.Item2; + + // dimension + var maxRowIndex = value.Rows.Count + (_printHeader && value.Rows.Count > 0 ? 1 : 0); + var maxColumnIndex = value.Columns.Count; + await writer.WriteAsync($@""); + + var props = new List(); + for (var i = 0; i < value.Columns.Count; i++) + { + var columnName = value.Columns[i].Caption ?? value.Columns[i].ColumnName; + var prop = GetColumnInfosFromDynamicConfiguration(columnName); + props.Add(prop); + } + + await WriteColumnsWidthsAsync(writer, props); + + await writer.WriteAsync(""); + if (_printHeader) + { + await writer.WriteAsync($""); + var xIndex = xy.Item1; + foreach (var p in props) + { + var r = ExcelOpenXmlUtils.ConvertXyToCell(xIndex, yIndex); + await WriteCAsync(writer, r, columnName: p.ExcelColumnName); + xIndex++; + } + + await writer.WriteAsync($""); + yIndex++; + } + + for (int i = 0; i < value.Rows.Count; i++) + { + await writer.WriteAsync($""); + var xIndex = xy.Item1; + + for (int j = 0; j < value.Columns.Count; j++) + { + var cellValue = value.Rows[i][j]; + await WriteCellAsync(writer, yIndex, xIndex, cellValue, null); + xIndex++; + } + await writer.WriteAsync($""); + yIndex++; + } + } + await writer.WriteAsync(""); + } + + private static async Task WriteColumnsWidthsAsync(MiniExcelAsyncStreamWriter writer, IEnumerable props) + { + var ecwProps = props.Where(x => x?.ExcelColumnWidth != null).ToList(); + if (ecwProps.Count <= 0) + return; + await writer.WriteAsync($@""); + foreach (var p in ecwProps) + { + await writer.WriteAsync( + $@""); + } + + await writer.WriteAsync($@""); + } + + private static async Task PrintHeaderAsync(MiniExcelAsyncStreamWriter writer, List props) + { + var xIndex = 1; + var yIndex = 1; + await writer.WriteAsync($""); + + foreach (var p in props) + { + if (p == null) + { + xIndex++; //reason : https://github.com/shps951023/MiniExcel/issues/142 + continue; + } + + var r = ExcelOpenXmlUtils.ConvertXyToCell(xIndex, yIndex); + WriteCAsync(writer, r, columnName: p.ExcelColumnName); + xIndex++; + } + + await writer.WriteAsync(""); + } + + private static async Task WriteCAsync(MiniExcelAsyncStreamWriter writer, string r, string columnName) + { + await writer.WriteAsync($""); + await writer.WriteAsync($"{ExcelOpenXmlUtils.EncodeXML(columnName)}"); //issue I45TF5 + await writer.WriteAsync($""); + await writer.WriteAsync($""); + } + + private async Task WriteCellAsync(MiniExcelAsyncStreamWriter writer, int rowIndex, int cellIndex, object value, ExcelColumnInfo p) + { + var columname = ExcelOpenXmlUtils.ConvertXyToCell(cellIndex, rowIndex); + var s = "2"; + var valueIsNull = value is null || value is DBNull; + + if (_configuration.EnableWriteNullValueCell && valueIsNull) + { + await writer.WriteAsync($""); + return; + } + + var tuple = GetCellValue(rowIndex, cellIndex, value, p, valueIsNull); + + s = tuple.Item1; + var t = tuple.Item2; + var v = tuple.Item3; + + if (v != null && (v.StartsWith(" ", StringComparison.Ordinal) || v.EndsWith(" ", StringComparison.Ordinal))) /*Prefix and suffix blank space will lost after SaveAs #294*/ + await writer.WriteAsync($"{v}"); + else + //t check avoid format error ![image](https://user-images.githubusercontent.com/12729184/118770190-9eee3480-b8b3-11eb-9f5a-87a439f5e320.png) + await writer.WriteAsync($"{v}"); + } + + private async Task GenerateSheetByColumnInfoAsync(MiniExcelAsyncStreamWriter writer, IEnumerator value, List props, int xIndex = 1, int yIndex = 1) + { + var isDic = typeof(T) == typeof(IDictionary); + var isDapperRow = typeof(T) == typeof(IDictionary); + do + { + // The enumerator has already moved to the first item + T v = (T)value.Current; + + await writer.WriteAsync($""); + var cellIndex = xIndex; + foreach (var p in props) + { + if (p == null) //reason:https://github.com/shps951023/MiniExcel/issues/142 + { + cellIndex++; + continue; + } + object cellValue = null; + if (isDic) + { + cellValue = ((IDictionary)v)[p.Key]; + //WriteCell(writer, yIndex, cellIndex, cellValue, null); // why null because dictionary that needs to check type every time + //TODO: user can specefic type to optimize efficiency + } + else if (isDapperRow) + { + cellValue = ((IDictionary)v)[p.Key.ToString()]; + } + else + { + cellValue = p.Property.GetValue(v); + } + await WriteCellAsync(writer, yIndex, cellIndex, cellValue, p); + + + cellIndex++; + } + await writer.WriteAsync($""); + yIndex++; + } while (value.MoveNext()); + + return yIndex - 1; + } + + private async Task GenerateEndXmlAsync(CancellationToken cancellationToken) + { + //Files + { + foreach (var item in _files) + { + await this.CreateZipEntryAsync(item.Path, item.Byte, cancellationToken); + } + } + + // styles.xml + { + var styleXml = string.Empty; + + if (_configuration.TableStyles == TableStyles.None) + { + styleXml = _noneStylesXml; + } + else if (_configuration.TableStyles == TableStyles.Default) + { + styleXml = _defaultStylesXml; + } + + await CreateZipEntryAsync(@"xl/styles.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", styleXml, cancellationToken); + } + + // drawing rel + { + for (int j = 0; j < _sheets.Count; j++) + { + var drawing = new StringBuilder(); + foreach (var i in _files.Where(w => w.IsImage && w.SheetId == j + 1)) + { + drawing.AppendLine($@""); + } + await CreateZipEntryAsync($"xl/drawings/_rels/drawing{j + 1}.xml.rels", "", + _defaultDrawingXmlRels.Replace("{{format}}", drawing.ToString()), cancellationToken); + } + + } + // drawing + { + for (int j = 0; j < _sheets.Count; j++) + { + var drawing = new StringBuilder(); + foreach (var i in _files.Where(w => w.IsImage && w.SheetId == j + 1)) + { + drawing.Append($@" + + {i.CellIndex - 1/* why -1 : https://user-images.githubusercontent.com/12729184/150460189-f08ed939-44d4-44e1-be6e-9c533ece6be8.png*/} + 0 + {i.RowIndex - 1} + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + "); + } + await CreateZipEntryAsync($"xl/drawings/drawing{j + 1}.xml", "application/vnd.openxmlformats-officedocument.drawing+xml", + _defaultDrawing.Replace("{{format}}", drawing.ToString()), cancellationToken); + } + } + + // workbook.xml 、 workbookRelsXml + { + var workbookXml = new StringBuilder(); + var workbookRelsXml = new StringBuilder(); + + var sheetId = 0; + foreach (var s in _sheets) + { + sheetId++; + if (string.IsNullOrEmpty(s.State)) + { + workbookXml.AppendLine($@""); + } + else + { + workbookXml.AppendLine($@""); + } + workbookRelsXml.AppendLine($@""); + + //TODO: support multiple drawing + //TODO: ../drawings/drawing1.xml or /xl/drawings/drawing1.xml + var sheetRelsXml = $@""; + await CreateZipEntryAsync($"xl/worksheets/_rels/sheet{s.SheetIdx}.xml.rels", "", + _defaultSheetRelXml.Replace("{{format}}", sheetRelsXml), cancellationToken); + } + await CreateZipEntryAsync(@"xl/workbook.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", + _defaultWorkbookXml.Replace("{{sheets}}", workbookXml.ToString()), cancellationToken); + await CreateZipEntryAsync(@"xl/_rels/workbook.xml.rels", "", + _defaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml.ToString()), cancellationToken); + } + + //[Content_Types].xml + { + var sb = new StringBuilder(@""); + foreach (var p in _zipDictionary) + sb.Append($""); + sb.Append(""); + ZipArchiveEntry entry = _archive.CreateEntry("[Content_Types].xml", CompressionLevel.Fastest); + using (var zipStream = entry.Open()) + using (MiniExcelStreamWriter writer = new MiniExcelStreamWriter(zipStream, _utf8WithBom, _configuration.BufferSize)) + writer.Write(sb.ToString()); + } + } + } +} diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs index 409f9446..1e0c932a 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs @@ -122,7 +122,6 @@ public void SaveAs() _archive.Dispose(); } - private void GenerateSheetByEnumerable(MiniExcelStreamWriter writer, IEnumerable values) { var maxColumnIndex = 0; @@ -371,6 +370,7 @@ private void WriteEmptySheet(MiniExcelStreamWriter writer) { writer.Write($@""); } + private int GenerateSheetByColumnInfo(MiniExcelStreamWriter writer, IEnumerator value, List props, int xIndex = 1, int yIndex = 1) { var isDic = typeof(T) == typeof(IDictionary); @@ -428,6 +428,22 @@ private void WriteCell(MiniExcelStreamWriter writer, int rowIndex, int cellIndex return; } + var tuple = GetCellValue(rowIndex, cellIndex, value, p, valueIsNull); + + s = tuple.Item1; + var t = tuple.Item2; + var v = tuple.Item3; + + if (v != null && (v.StartsWith(" ", StringComparison.Ordinal) || v.EndsWith(" ", StringComparison.Ordinal))) /*Prefix and suffix blank space will lost after SaveAs #294*/ + writer.Write($"{v}"); + else + //t check avoid format error ![image](https://user-images.githubusercontent.com/12729184/118770190-9eee3480-b8b3-11eb-9f5a-87a439f5e320.png) + writer.Write($"{v}"); + } + + private Tuple GetCellValue(int rowIndex, int cellIndex, object value, ExcelColumnInfo p, bool valueIsNull) + { + var s = "2"; var v = string.Empty; var t = "str"; @@ -564,11 +580,7 @@ private void WriteCell(MiniExcelStreamWriter writer, int rowIndex, int cellIndex } } - if (v != null && (v.StartsWith(" ", StringComparison.Ordinal) || v.EndsWith(" ", StringComparison.Ordinal))) /*Prefix and suffix blank space will lost after SaveAs #294*/ - writer.Write($"{v}"); - else - //t check avoid format error ![image](https://user-images.githubusercontent.com/12729184/118770190-9eee3480-b8b3-11eb-9f5a-87a439f5e320.png) - writer.Write($"{v}"); + return Tuple.Create(s, t, v); } private void GenerateSheetByDataTable(MiniExcelStreamWriter writer, DataTable value) @@ -917,11 +929,6 @@ private string GetDimensionRef(int maxRowIndex, int maxColumnIndex) return dimensionRef; } - public async Task SaveAsAsync(CancellationToken cancellationToken = default(CancellationToken)) - { - await Task.Run(() => SaveAs(), cancellationToken).ConfigureAwait(false); - } - public void Insert() { throw new NotImplementedException(); diff --git a/src/MiniExcel/OpenXml/MiniExcelAsyncStreamWriter.cs b/src/MiniExcel/OpenXml/MiniExcelAsyncStreamWriter.cs new file mode 100644 index 00000000..8b81fb70 --- /dev/null +++ b/src/MiniExcel/OpenXml/MiniExcelAsyncStreamWriter.cs @@ -0,0 +1,71 @@ +// +// Copyright (c) 2024 Rolls-Royce plc +// + +namespace MiniExcelLibs.OpenXml +{ + using System.IO; + using System.Text; + using System; + using System.Threading.Tasks; + using System.Threading; + + internal class MiniExcelAsyncStreamWriter : IDisposable + { + private readonly Stream _stream; + private readonly Encoding _encoding; + private readonly CancellationToken _cancellationToken; + private readonly StreamWriter _streamWriter; + private bool disposedValue; + public MiniExcelAsyncStreamWriter(Stream stream, Encoding encoding, int bufferSize, System.Threading.CancellationToken cancellationToken) + { + this._stream = stream; + this._encoding = encoding; + this._cancellationToken = cancellationToken; + this._streamWriter = new StreamWriter(stream, this._encoding, bufferSize); + } + public async Task WriteAsync(string content) + { + this._cancellationToken.ThrowIfCancellationRequested(); + + if (string.IsNullOrEmpty(content)) + return; + await this._streamWriter.WriteAsync(content); + } + + public async Task WriteAndFlushAsync(string content) + { + await this.WriteAsync(content); + return await this.FlushAsync(); + } + + public async Task FlushAsync() + { + this._cancellationToken.ThrowIfCancellationRequested(); + + await this._streamWriter.FlushAsync(); + return this._streamWriter.BaseStream.Position; + } + + public void SetPosition(long position) + { + this._streamWriter.BaseStream.Position = position; + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + this._streamWriter?.Dispose(); + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file