diff --git a/src/MiniExcel.Core/Helpers/NetStandardHelper.cs b/src/MiniExcel.Core/Helpers/NetStandardHelper.cs deleted file mode 100644 index 16372635..00000000 --- a/src/MiniExcel.Core/Helpers/NetStandardHelper.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace MiniExcelLib.Core.Helpers; - -#if NETSTANDARD2_0 - -/// -/// Provides .NET Standard 2.0 polyfills for utility methods found in later framework versions. -/// This enables a unified API surface across the codebase without the need for conditional compilation directives. -/// -public static class NetStandardExtensions -{ - public static TValue? GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key, TValue? defaultValue = default) - { - return dictionary.TryGetValue(key, out var value) ? value : defaultValue; - } -} -#endif diff --git a/src/MiniExcel.Core/Helpers/Polyfills.cs b/src/MiniExcel.Core/Helpers/Polyfills.cs new file mode 100644 index 00000000..c84b1421 --- /dev/null +++ b/src/MiniExcel.Core/Helpers/Polyfills.cs @@ -0,0 +1,63 @@ +using System.ComponentModel; +using System.IO.Compression; + +namespace MiniExcelLib.Core.Helpers; + +/* todo: instead of using the EditorBrowsableAttribute consider making this class internal and link it for compilation + in the other projects that require it so as to prevent the consumers' IDEs to be polluted with these extension methods */ +[EditorBrowsable(EditorBrowsableState.Advanced)] +public static class Polyfills +{ +#if NETSTANDARD2_0 + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static TValue? GetValueOrDefault(this IDictionary dictionary, TKey key, TValue? defaultValue = default) + { + return dictionary.TryGetValue(key, out var value) ? value : defaultValue; + } + + extension(Math) + { + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static TNumber Clamp(TNumber value, TNumber min, TNumber max) where TNumber : unmanaged, IComparable + { + if (value.CompareTo(min) < 0) return min; + if (value.CompareTo(max) > 0) return max; + return value; + } + } +#endif + +#if !NET10_0_OR_GREATER + extension(ZipArchiveEntry entry) + { + [EditorBrowsable(EditorBrowsableState.Advanced)] + public ValueTask OpenAsync(CancellationToken cancellationToken = default) + { + var stream = entry.Open(); + return new ValueTask(stream); + } + } + + extension(ZipArchive) + { + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static ValueTask CreateAsync(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding? entryNameEncoding = null, CancellationToken cancellationToken = default) + { + ZipArchive? archive = null; + + try + { + archive = new ZipArchive(stream, mode, leaveOpen, entryNameEncoding); + var result = new ValueTask(archive); + + archive = null; + return result; + } + finally + { + archive?.Dispose(); + } + } + } +#endif +} diff --git a/src/MiniExcel.Core/Helpers/SynchronousHelper.cs b/src/MiniExcel.Core/Helpers/SynchronousHelper.cs index 9277af6c..5a1bac57 100644 --- a/src/MiniExcel.Core/Helpers/SynchronousHelper.cs +++ b/src/MiniExcel.Core/Helpers/SynchronousHelper.cs @@ -10,7 +10,7 @@ public static class SynchronousHelper { extension(ZipArchive) { - public static ZipArchive Create(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding? encoding = null) - => new(stream, mode, leaveOpen, encoding); + public static ZipArchive Create(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding? entryNameEncoding = null) + => new(stream, mode, leaveOpen, entryNameEncoding); } } diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs b/src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs index 7634227f..8f8378b1 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs @@ -69,4 +69,44 @@ public async Task ExportAsync(Stream stream, object value, bool printHead return await writer.SaveAsAsync(progress, cancellationToken).ConfigureAwait(false); } + + /// + /// Modify the properties of a worksheet in the specified document. + /// + /// The path to the OpenXml document. + /// The name of the worksheet to modify. + /// The new name to assign to the worksheet, or null to leave as is. + /// The position in the workbook to assign to the worksheet, or null to leave as is. + /// The visibility state to assign to the worksheet, or null to leave as is. + /// The token to monitor for cancellation requests + [CreateSyncVersion] + public async Task AlterSheetAsync(string path, string sheetName, string? newSheetName = null, int? newSheetIndex = null, SheetState? newSheetState = null, CancellationToken cancellationToken = default) + { +#if NET8_0_OR_GREATER + var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read); + await using var disposableStream = stream.ConfigureAwait(false); +#else + using var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read); +#endif + await AlterSheetAsync(stream, sheetName, newSheetName, newSheetIndex, newSheetState, cancellationToken).ConfigureAwait(false); + } + + /// + /// Modify the properties of a worksheet in the specified document. + /// + /// The stream to the OpenXml document. + /// The name of the worksheet to modify. + /// The new name to assign to the worksheet, or null to leave as is. + /// The position in the workbook to assign to the worksheet, or null to leave as is. + /// The visibility state to assign to the worksheet, or null to leave as is. + /// The token to monitor for cancellation requests + [CreateSyncVersion] + public async Task AlterSheetAsync(Stream stream, string sheetName, string? newSheetName = null, int? newSheetIndex = null, SheetState? newSheetState = null, CancellationToken cancellationToken = default) + { + var writer = await OpenXmlWriter + .CreateAsync(stream, null, sheetName, false, new OpenXmlConfiguration { FastMode = true }, cancellationToken) + .ConfigureAwait(false); + + await writer.AlterWorksheetAsync(sheetName, newSheetName, newSheetIndex, newSheetState, cancellationToken).ConfigureAwait(false); + } } diff --git a/src/MiniExcel.OpenXml/FluentMapping/MappingTemplateApplicator.cs b/src/MiniExcel.OpenXml/FluentMapping/MappingTemplateApplicator.cs index 969eb35e..1d9bcdd6 100644 --- a/src/MiniExcel.OpenXml/FluentMapping/MappingTemplateApplicator.cs +++ b/src/MiniExcel.OpenXml/FluentMapping/MappingTemplateApplicator.cs @@ -1,6 +1,3 @@ -using System.IO.Compression; -using Zomp.SyncMethodGenerator; - namespace MiniExcelLib.OpenXml.FluentMapping; internal static partial class MappingTemplateApplicator where T : class @@ -37,12 +34,20 @@ public static async Task ApplyTemplateAsync( } templateStream.Position = 0; - + +#if NET10_0_OR_GREATER // Open template archive for reading - using var templateArchive = new ZipArchive(templateStream, ZipArchiveMode.Read, leaveOpen: true); - + var templateArchive = await ZipArchive.CreateAsync(templateStream, ZipArchiveMode.Read, true, null, cancellationToken: cancellationToken).ConfigureAwait(false); + await using var disposableTemplateArchive = templateArchive.ConfigureAwait(false); + // Create output archive - using var outputArchive = new ZipArchive(outputStream, ZipArchiveMode.Create, leaveOpen: true); + var outputArchive = await ZipArchive.CreateAsync(outputStream, ZipArchiveMode.Create, true, null, cancellationToken).ConfigureAwait(false); + await using var disposableOutputArchive = outputArchive.ConfigureAwait(false); +#else + using var templateArchive = new ZipArchive(templateStream, ZipArchiveMode.Read, true); + using var outputArchive = new ZipArchive(outputStream, ZipArchiveMode.Create, true); +#endif + // Process each entry foreach (var entry in templateArchive.Entries) @@ -106,17 +111,18 @@ private static async Task CopyEntryAsync( targetEntry.LastWriteTime = sourceEntry.LastWriteTime; // Copy content -#if NET10_0_OR_GREATER - using var sourceStream = await sourceEntry.OpenAsync(cancellationToken).ConfigureAwait(false); - using var targetStream = await targetEntry.OpenAsync(cancellationToken).ConfigureAwait(false); +#if NET8_0_OR_GREATER + var sourceStream = await sourceEntry.OpenAsync(cancellationToken).ConfigureAwait(false); + var targetStream = await targetEntry.OpenAsync(cancellationToken).ConfigureAwait(false); + + await using var disposableSourceStream = sourceStream.ConfigureAwait(false); + await using var disposableTargetStream = targetStream.ConfigureAwait(false); + + await sourceStream.CopyToAsync(targetStream, cancellationToken).ConfigureAwait(false); #else using var sourceStream = sourceEntry.Open(); using var targetStream = targetEntry.Open(); -#endif - -#if NETCOREAPP2_1_OR_GREATER - await sourceStream.CopyToAsync(targetStream, cancellationToken).ConfigureAwait(false); -#else + await sourceStream.CopyToAsync(targetStream).ConfigureAwait(false); #endif } @@ -135,9 +141,12 @@ private static async Task ProcessWorksheetAsync( targetEntry.LastWriteTime = sourceEntry.LastWriteTime; // Open streams -#if NET10_0_OR_GREATER - using var sourceStream = await sourceEntry.OpenAsync(cancellationToken).ConfigureAwait(false); - using var targetStream = await targetEntry.OpenAsync(cancellationToken).ConfigureAwait(false); +#if NET8_0_OR_GREATER + var sourceStream = await sourceEntry.OpenAsync(cancellationToken).ConfigureAwait(false); + var targetStream = await targetEntry.OpenAsync(cancellationToken).ConfigureAwait(false); + + await using var disposableSourceStream = sourceStream.ConfigureAwait(false); + await using var disposableTargetStream = targetStream.ConfigureAwait(false); #else using var sourceStream = sourceEntry.Open(); using var targetStream = targetEntry.Open(); diff --git a/src/MiniExcel.OpenXml/Models/ExcelSheetInfo.cs b/src/MiniExcel.OpenXml/Models/ExcelSheetInfo.cs new file mode 100644 index 00000000..328683a7 --- /dev/null +++ b/src/MiniExcel.OpenXml/Models/ExcelSheetInfo.cs @@ -0,0 +1,15 @@ +namespace MiniExcelLib.OpenXml.Models; + +internal class ExcelSheetInfo +{ + public object Key { get; set; } + public string? ExcelSheetName { get; set; } + public SheetState ExcelSheetState { get; set; } + + public SheetDto ToDto(int sheetIndex) => new() + { + Name = ExcelSheetName, + SheetIdx = sheetIndex, + State = ExcelSheetState.ToString().ToLower() + }; +} \ No newline at end of file diff --git a/src/MiniExcel.OpenXml/Models/ExcellSheetInfo.cs b/src/MiniExcel.OpenXml/Models/ExcellSheetInfo.cs deleted file mode 100644 index 57cb3efd..00000000 --- a/src/MiniExcel.OpenXml/Models/ExcellSheetInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace MiniExcelLib.OpenXml.Models; - -internal class ExcellSheetInfo -{ - public object Key { get; set; } - public string? ExcelSheetName { get; set; } - public SheetState ExcelSheetState { get; set; } - - private string ExcelSheetStateAsString => ExcelSheetState.ToString().ToLower(); - - public SheetDto ToDto(int sheetIndex) - { - return new SheetDto { Name = ExcelSheetName, SheetIdx = sheetIndex, State = ExcelSheetStateAsString }; - } -} \ No newline at end of file diff --git a/src/MiniExcel.OpenXml/Models/SheetInfo.cs b/src/MiniExcel.OpenXml/Models/SheetInfo.cs index ac063d16..0e60e824 100644 --- a/src/MiniExcel.OpenXml/Models/SheetInfo.cs +++ b/src/MiniExcel.OpenXml/Models/SheetInfo.cs @@ -3,17 +3,17 @@ namespace MiniExcelLib.OpenXml.Models; public class SheetInfo(uint id, uint index, string name, SheetState sheetState, bool active) { /// - /// Internal sheet id - depends on the order in which the sheet is added + /// Internal sheet id - depends on the order in which the sheet is added. /// public uint Id { get; } = id; /// - /// Next sheet index - numbered from 0 + /// The 0-based index of the worksheet in the workbook /// public uint Index { get; } = index; /// - /// Sheet name + /// The name of the worksheet /// public string Name { get; } = name; @@ -23,7 +23,7 @@ public class SheetInfo(uint id, uint index, string name, SheetState sheetState, public SheetState State { get; } = sheetState; /// - /// Indicates whether the sheet is active + /// Indicates whether the worksheet was active the last time /// public bool Active { get; } = active; } diff --git a/src/MiniExcel.OpenXml/OpenXmlReader.cs b/src/MiniExcel.OpenXml/OpenXmlReader.cs index a6924561..c623398d 100644 --- a/src/MiniExcel.OpenXml/OpenXmlReader.cs +++ b/src/MiniExcel.OpenXml/OpenXmlReader.cs @@ -21,9 +21,9 @@ internal partial class OpenXmlReader : IMiniExcelReader internal readonly OpenXmlZip Archive; internal IDictionary SharedStrings = new Dictionary(); - private OpenXmlReader(OpenXmlZip openXmlZip, IMiniExcelConfiguration? configuration) + private OpenXmlReader(OpenXmlZip archive, IMiniExcelConfiguration? configuration) { - Archive = openXmlZip; + Archive = archive; _config = (OpenXmlConfiguration?)configuration ?? OpenXmlConfiguration.Default; } @@ -34,8 +34,8 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC var archive = await OpenXmlZip.CreateAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false); var reader = new OpenXmlReader(archive, configuration); - await reader.SetSharedStringsAsync(cancellationToken).ConfigureAwait(false); + return reader; } @@ -48,7 +48,7 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC [CreateSyncVersion] public IAsyncEnumerable QueryAsync(string? sheetName, string startCell, bool mapHeaderAsData, CancellationToken cancellationToken = default) where T : class, new() { - sheetName ??= MiniExcelPropertyHelper.GetExcellSheetInfo(typeof(T), _config)?.ExcelSheetName; + sheetName ??= MiniExcelPropertyHelper.GetExcelSheetInfo(typeof(T), _config)?.ExcelSheetName; var query = QueryAsync(false, sheetName, startCell, cancellationToken); if (!CellReferenceConverter.TryParseCellReference(startCell, out _, out var rowOffset)) { @@ -178,27 +178,24 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC maxColumnIndex = endColumnIndex.Value; } -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER var sheetStream = await sheetEntry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableSheetStream = sheetStream.ConfigureAwait(false); -#elif !NETSTANDARD2_0 - var sheetStream = sheetEntry.Open(); - await using var disposableSheetStream = sheetStream.ConfigureAwait(false); #else using var sheetStream = sheetEntry.Open(); #endif using var reader = XmlReader.Create(sheetStream, xmlSettings); - if (!XmlReaderHelper.IsStartElement(reader, "worksheet", Ns)) + if (!reader.IsStartElement("worksheet", Ns)) yield break; - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) yield break; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "sheetData", Ns)) + if (reader.IsStartElement("sheetData", Ns)) { - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) continue; int rowIndex = -1; @@ -206,7 +203,7 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC var headRows = new Dictionary(); while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "row", Ns)) + if (reader.IsStartElement("row", Ns)) { var nextRowIndex = rowIndex + 1; if (int.TryParse(reader.GetAttribute("r"), out int arValue)) @@ -216,9 +213,9 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC if (rowIndex < startRowIndex) { - if (await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) { - await XmlReaderHelper.SkipToNextSameLevelDomAsync(reader, cancellationToken).ConfigureAwait(false); + await reader.SkipToNextSiblingAsync(cancellationToken).ConfigureAwait(false); } continue; } @@ -243,13 +240,13 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC yield return row; } } - else if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } } } - else if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } @@ -286,7 +283,7 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC } // row -> c, must after `if (nextRowIndex < rowIndex)` condition code, eg. The first empty row has no xml element,and the second row xml element is - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false) && !_config.IgnoreEmptyRows) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false) && !_config.IgnoreEmptyRows) { //Fill in case of self closed empty row tag eg. yield return GetCell(useHeaderRow, maxColumnIndex, headRows, startColumnIndex); @@ -297,7 +294,7 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC var columnIndex = withoutCr ? -1 : 0; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "c", Ns)) + if (reader.IsStartElement("c", Ns)) { var aS = reader.GetAttribute("s"); var aR = reader.GetAttribute("r"); @@ -335,7 +332,7 @@ internal static async Task CreateAsync(Stream stream, IMiniExcelC SetCellsValueAndHeaders(cellValue, useHeaderRow, headRows, isFirstRow, cell, columnIndex); } - else if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } @@ -429,12 +426,9 @@ private async Task SetSharedStringsAsync(CancellationToken cancellationToken = d return; var idx = 0; -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER var stream = await sharedStringsEntry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableStream = stream.ConfigureAwait(false); -#elif !NETSTANDARD2_0 - var stream = sharedStringsEntry.Open(); - await using var disposableStream = stream.ConfigureAwait(false); #else using var stream = sharedStringsEntry.Open(); #endif @@ -473,33 +467,30 @@ private static async IAsyncEnumerable ReadWorkbookAsync(ReadOnlyCol ); var entry = entries.Single(w => w.FullName == "xl/workbook.xml"); -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER var stream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableStream = stream.ConfigureAwait(false); -#elif !NETSTANDARD2_0 - var stream = entry.Open(); - await using var disposableStream = stream.ConfigureAwait(false); #else using var stream = entry.Open(); #endif using var reader = XmlReader.Create(stream, xmlSettings); - if (!XmlReaderHelper.IsStartElement(reader, "workbook", Ns)) + if (!reader.IsStartElement("workbook", Ns)) yield break; - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) yield break; var activeSheetIndex = 0; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "bookViews", Ns)) + if (reader.IsStartElement("bookViews", Ns)) { - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) continue; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "workbookView", Ns)) + if (reader.IsStartElement("workbookView", Ns)) { var activeSheet = reader.GetAttribute("activeTab"); if (int.TryParse(activeSheet, out var index)) @@ -513,27 +504,27 @@ await reader.SkipAsync() #endif .ConfigureAwait(false); } - else if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } } } - else if (XmlReaderHelper.IsStartElement(reader, "sheets", Ns)) + else if (reader.IsStartElement("sheets", Ns)) { - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) continue; var sheetCount = 0; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "sheet", Ns)) + if (reader.IsStartElement("sheet", Ns)) { yield return new SheetRecord( reader.GetAttribute("name"), reader.GetAttribute("state"), uint.Parse(reader.GetAttribute("sheetId")), - XmlReaderHelper.GetAttribute(reader, "id", RelationshiopNs), + reader.GetAttribute("id", RelationshiopNs), sheetCount == activeSheetIndex ); sheetCount++; @@ -543,13 +534,13 @@ await reader.SkipAsync() #endif .ConfigureAwait(false); } - else if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } } } - else if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { yield break; } @@ -573,12 +564,9 @@ await reader.SkipAsync() var entry = entries.Single(w => w.FullName == "xl/_rels/workbook.xml.rels"); -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER var stream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableStream = stream.ConfigureAwait(false); -#elif !NETSTANDARD2_0 - var stream = entry.Open(); - await using var disposableStream = stream.ConfigureAwait(false); #else using var stream = entry.Open(); #endif @@ -586,7 +574,7 @@ await reader.SkipAsync() if (!XmlReaderHelper.IsStartElement(reader, "Relationships", Schemas.OpenXmlPackageRelationships)) return null; - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) return null; while (!reader.EOF) @@ -606,7 +594,7 @@ await reader.SkipAsync() #endif .ConfigureAwait(false); } - else if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } @@ -647,25 +635,25 @@ private async Task ReadCellAndSetColumnIndexAsync(XmlReader reade if (columnIndex < startColumnIndex) { - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) return new CellAndColumn(null, columnIndex); while (!reader.EOF) { - if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) break; } return new CellAndColumn(null, columnIndex); } - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) return new CellAndColumn(null, columnIndex); object? value = null; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "v", Ns)) + if (reader.IsStartElement("v", Ns)) { var rawValue = await reader.ReadElementContentAsStringAsync() #if NET6_0_OR_GREATER @@ -676,13 +664,13 @@ private async Task ReadCellAndSetColumnIndexAsync(XmlReader reade if (!string.IsNullOrEmpty(rawValue)) ConvertCellValue(rawValue, aT, xfIndex, out value); } - else if (XmlReaderHelper.IsStartElement(reader, "is", Ns)) + else if (reader.IsStartElement("is", Ns)) { - var rawValue = await XmlReaderHelper.ReadStringItemAsync(reader, cancellationToken).ConfigureAwait(false); + var rawValue = await reader.ReadStringItemAsync(cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(rawValue)) ConvertCellValue(rawValue, aT, xfIndex, out value); } - else if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } @@ -720,15 +708,13 @@ private void ConvertCellValue(string rawValue, string aT, int xfIndex, out objec if (v?.StartsWith("@@@fileid@@@,", StringComparison.Ordinal) ?? false) { var path = v[13..]; - var entry = Archive.GetEntry(path); - var bytes = new byte[entry.Length]; + var entry = Archive.GetEntry(path)!; - using (var stream = entry.Open()) - using (var ms = new MemoryStream(bytes)) - { - stream.CopyTo(ms); - } - value = bytes; + using var stream = entry.Open(); + using var ms = new MemoryStream((int)entry.Length); + + stream.CopyTo(ms); + value = ms.ToArray(); } } break; @@ -782,12 +768,9 @@ internal async Task> GetDimensionsAsync(CancellationToken canc var withoutCr = false; -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER var crSheetStream = await sheet.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableCrSheetStream = crSheetStream.ConfigureAwait(false); -#elif !NETSTANDARD2_0 - var crSheetStream = sheet.Open(); - await using var disposableCrSheetStream = crSheetStream.ConfigureAwait(false); #else using var crSheetStream = sheet.Open(); #endif @@ -795,7 +778,7 @@ internal async Task> GetDimensionsAsync(CancellationToken canc { while (await reader.ReadAsync().ConfigureAwait(false)) { - if (XmlReaderHelper.IsStartElement(reader, "c", Ns)) + if (reader.IsStartElement("c", Ns)) { var r = reader.GetAttribute("r"); if (r is not null) @@ -815,7 +798,7 @@ internal async Task> GetDimensionsAsync(CancellationToken canc } } - else if (XmlReaderHelper.IsStartElement(reader, "dimension", Ns)) + else if (reader.IsStartElement("dimension", Ns)) { var refAttr = reader.GetAttribute("ref"); if (string.IsNullOrEmpty(refAttr)) @@ -838,59 +821,56 @@ internal async Task> GetDimensionsAsync(CancellationToken canc if (withoutCr) { -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER var sheetStream = await sheet.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableSheetStream = sheetStream.ConfigureAwait(false); -#elif !NETSTANDARD2_0 - var sheetStream = sheet.Open(); - await using var disposableSheetStream = sheetStream.ConfigureAwait(false); #else using var sheetStream = sheet.Open(); #endif using var reader = XmlReader.Create(sheetStream, xmlSettings); - if (!XmlReaderHelper.IsStartElement(reader, "worksheet", Ns)) + if (!reader.IsStartElement("worksheet", Ns)) throw new InvalidDataException("No worksheet data found for the sheet"); - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) throw new InvalidOperationException("Excel sheet does not contain any data"); while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "sheetData", Ns)) + if (reader.IsStartElement("sheetData", Ns)) { - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) continue; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "row", Ns)) + if (reader.IsStartElement("row", Ns)) { maxRowIndex++; - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) continue; var cellIndex = -1; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "c", Ns)) + if (reader.IsStartElement("c", Ns)) { cellIndex++; maxColumnIndex = Math.Max(maxColumnIndex, cellIndex); } - if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) break; } } - else if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } } } - else if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } @@ -941,12 +921,9 @@ internal static async Task TryGetMaxRowColumnIndexAs int maxRowIndex = -1; int maxColumnIndex = -1; -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER var crSheetStream = await sheetEntry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableCrSheetStream = crSheetStream.ConfigureAwait(false); -#elif !NETSTANDARD2_0 - var crSheetStream = sheetEntry.Open(); - await using var disposableCrSheetStream = crSheetStream.ConfigureAwait(false); #else using var crSheetStream = sheetEntry.Open(); #endif @@ -958,7 +935,7 @@ internal static async Task TryGetMaxRowColumnIndexAs #endif .ConfigureAwait(false)) { - if (XmlReaderHelper.IsStartElement(reader, "c", Ns)) + if (reader.IsStartElement("c", Ns)) { var r = reader.GetAttribute("r"); if (r is not null) @@ -978,7 +955,7 @@ internal static async Task TryGetMaxRowColumnIndexAs } } //this method logic depends on dimension to get maxcolumnIndex, if without dimension then it need to foreach all rows first time to get maxColumn and maxRowColumn - else if (XmlReaderHelper.IsStartElement(reader, "dimension", Ns)) + else if (reader.IsStartElement("dimension", Ns)) { var refAttr = reader.GetAttribute("ref"); if (string.IsNullOrEmpty(refAttr)) @@ -999,60 +976,57 @@ internal static async Task TryGetMaxRowColumnIndexAs if (withoutCr) { -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER var sheetStream = await sheetEntry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableSheetStream = sheetStream.ConfigureAwait(false); -#elif !NETSTANDARD2_0 - var sheetStream = sheetEntry.Open(); - await using var disposableSheetStream = sheetStream.ConfigureAwait(false); #else using var sheetStream = sheetEntry.Open(); #endif using var reader = XmlReader.Create(sheetStream, xmlSettings); - if (!XmlReaderHelper.IsStartElement(reader, "worksheet", Ns)) + if (!reader.IsStartElement("worksheet", Ns)) return new GetMaxRowColumnIndexResult(false); - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) return new GetMaxRowColumnIndexResult(false); while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "sheetData", Ns)) + if (reader.IsStartElement("sheetData", Ns)) { - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) continue; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "row", Ns)) + if (reader.IsStartElement("row", Ns)) { maxRowIndex++; - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) continue; // Cells var cellIndex = -1; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "c", Ns)) + if (reader.IsStartElement("c", Ns)) { cellIndex++; maxColumnIndex = Math.Max(maxColumnIndex, cellIndex); } - if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) break; } } - else if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } } } - else if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } @@ -1082,31 +1056,28 @@ internal static async Task TryGetMergeCellsAsync(ZipArchiveEntry sheetEntr ); var mergeCells = new MergeCells(); -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER var sheetStream = await sheetEntry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableSheetStream = sheetStream.ConfigureAwait(false); -#elif !NETSTANDARD2_0 - var sheetStream = sheetEntry.Open(); - await using var disposableSheetStream = sheetStream.ConfigureAwait(false); #else using var sheetStream = sheetEntry.Open(); #endif using var reader = XmlReader.Create(sheetStream, xmlSettings); - if (!XmlReaderHelper.IsStartElement(reader, "worksheet", Ns)) + if (!reader.IsStartElement("worksheet", Ns)) return false; while (await reader.ReadAsync().ConfigureAwait(false)) { - if (!XmlReaderHelper.IsStartElement(reader, "mergeCells", Ns)) + if (!reader.IsStartElement("mergeCells", Ns)) continue; - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) return false; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "mergeCell", Ns)) + if (reader.IsStartElement("mergeCell", Ns)) { var refAttr = reader.GetAttribute("ref"); var refs = refAttr.Split(':'); @@ -1130,9 +1101,9 @@ internal static async Task TryGetMergeCellsAsync(ZipArchiveEntry sheetEntr } } - await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false); + await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false); } - else if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } @@ -1162,12 +1133,9 @@ internal async Task ReadCommentsAsync(string? sheetName, Cance List people = []; if (Archive.GetEntry("xl/persons/person.xml") is { } persons) { -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER var personStream = await persons.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposablePersonStream = personStream.ConfigureAwait(false); -#elif !NETSTANDARD2_0 - var personStream = persons.Open(); - await using var disposablePersonStream = personStream.ConfigureAwait(false); #else using var personStream = persons.Open(); #endif @@ -1191,12 +1159,9 @@ internal async Task ReadCommentsAsync(string? sheetName, Cance if (Archive.GetEntry($"xl/worksheets/_rels/{sheetFile}.rels") is not { } rel) return new CommentResultSet(sheetName, [], []); -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER var stream = await rel.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableStream = stream.ConfigureAwait(false); -#elif !NETSTANDARD2_0 - var stream = rel.Open(); - await using var disposableStream = stream.ConfigureAwait(false); #else using var stream = rel.Open(); #endif @@ -1222,12 +1187,9 @@ internal async Task ReadCommentsAsync(string? sheetName, Cance HashSet refCells = []; if (Archive.GetEntry($"xl/{threadedCommentsPath}") is { } threadEntry) { -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER var threadEntryStream = await threadEntry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableThreadEntryStream = threadEntryStream.ConfigureAwait(false); -#elif !NETSTANDARD2_0 - var threadEntryStream = threadEntry.Open(); - await using var disposableThreadEntryStream = threadEntryStream.ConfigureAwait(false); #else using var threadEntryStream = threadEntry.Open(); #endif @@ -1278,12 +1240,9 @@ internal async Task ReadCommentsAsync(string? sheetName, Cance if (Archive.GetEntry($"xl/{notesPath}") is { } noteEntry) { -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER var noteEntryStream = await noteEntry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableNoteEntryStream = noteEntryStream.ConfigureAwait(false); -#elif !NETSTANDARD2_0 - var noteEntryStream = noteEntry.Open(); - await using var disposableNoteEntryStream = noteEntryStream.ConfigureAwait(false); #else using var noteEntryStream = noteEntry.Open(); #endif @@ -1353,34 +1312,31 @@ internal async IAsyncEnumerable QueryMappedAsync( Async = true }; -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER var sheetStream = await sheetEntry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableSheetStream = sheetStream.ConfigureAwait(false); -#elif !NETSTANDARD2_0 - var sheetStream = sheetEntry.Open(); - await using var disposableSheetStream = sheetStream.ConfigureAwait(false); #else using var sheetStream = sheetEntry.Open(); #endif using var reader = XmlReader.Create(sheetStream, xmlSettings); - if (!XmlReaderHelper.IsStartElement(reader, "worksheet", Ns)) + if (!reader.IsStartElement("worksheet", Ns)) yield break; - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) yield break; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "sheetData", Ns)) + if (reader.IsStartElement("sheetData", Ns)) { - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) continue; int rowIndex = -1; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "row", Ns)) + if (reader.IsStartElement("row", Ns)) { if (int.TryParse(reader.GetAttribute("r"), out int arValue)) rowIndex = arValue - 1; // The row attribute is 1-based @@ -1393,13 +1349,13 @@ internal async IAsyncEnumerable QueryMappedAsync( yield return mappedRow; } } - else if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } } } - else if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } @@ -1414,7 +1370,7 @@ private async IAsyncEnumerable ReadMappedRowAsync( MergeCells? mergeCells, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - if (!await XmlReaderHelper.ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) { // Empty row yield return new MappedRow(rowIndex); @@ -1426,7 +1382,7 @@ private async IAsyncEnumerable ReadMappedRowAsync( while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "c", Ns)) + if (reader.IsStartElement("c", Ns)) { var aS = reader.GetAttribute("s"); var aR = reader.GetAttribute("r"); @@ -1459,7 +1415,7 @@ private async IAsyncEnumerable ReadMappedRowAsync( row.SetCell(columnIndex, cellValue); } - else if (!await XmlReaderHelper.SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs index feecc08a..bd7bc3ac 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs @@ -9,7 +9,7 @@ internal partial class OpenXmlWriter private readonly Dictionary _zipDictionary = []; private Dictionary _cellXfIdMap = []; - private IEnumerable> GetSheets() + private IEnumerable<(SheetDto, object?)> GetSheets() { var sheetId = 0; if (_value is IDictionary dictionary) @@ -20,7 +20,7 @@ internal partial class OpenXmlWriter sheetId++; var sheetInfos = GetSheetInfos(sheet.Key); - yield return Tuple.Create(sheetInfos.ToDto(sheetId), sheet.Value); + yield return (sheetInfos.ToDto(sheetId), sheet.Value); } yield break; @@ -32,7 +32,7 @@ internal partial class OpenXmlWriter { sheetId++; var sheetInfos = GetSheetInfos(dt.TableName); - yield return Tuple.Create(sheetInfos.ToDto(sheetId), (object?)dt); + yield return (sheetInfos.ToDto(sheetId), dt); } yield break; @@ -40,12 +40,12 @@ internal partial class OpenXmlWriter sheetId++; var defaultSheetInfo = GetSheetInfos(_defaultSheetName); - yield return Tuple.Create(defaultSheetInfo.ToDto(sheetId), _value); + yield return (defaultSheetInfo.ToDto(sheetId), _value); } - private ExcellSheetInfo GetSheetInfos(string sheetName) + private ExcelSheetInfo GetSheetInfos(string sheetName) { - var info = new ExcellSheetInfo + var info = new ExcelSheetInfo { ExcelSheetName = sheetName, Key = sheetName, @@ -153,20 +153,20 @@ private string GetPanes() return sb.ToString(); } - private Tuple GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnMapping? columnInfo, bool valueIsNull) + private (string, string?, string?) GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnMapping? columnInfo, bool valueIsNull) { if (valueIsNull) - return Tuple.Create("2", "str", string.Empty); + return ("2", "str", string.Empty); if (value is string str) - return Tuple.Create("2", "str", XmlHelper.EncodeXml(str)); + return ("2", "str", XmlHelper.EncodeXml(str)); var type = GetValueType(value, columnInfo); if (columnInfo is { ExcelFormat: not null, ExcelFormatId: -1 } && value is IFormattable formattableValue) { var formattedStr = formattableValue.ToString(columnInfo.ExcelFormat, _configuration.Culture); - return Tuple.Create("2", "str", XmlHelper.EncodeXml(formattedStr)); + return ("2", "str", XmlHelper.EncodeXml(formattedStr)); } if (type == typeof(DateTime)) @@ -188,7 +188,7 @@ private Tuple GetCellValue(int rowIndex, int cellIndex, } description ??= value.ToString(); - return Tuple.Create("2", "str", description); + return ("2", "str", description); } if (TypeHelper.IsNumericType(type)) @@ -197,26 +197,26 @@ private Tuple GetCellValue(int rowIndex, int cellIndex, if (columnInfo?.ExcelFormat is null) { - var dataType = _configuration.Culture == CultureInfo.InvariantCulture ? "n" : "str"; - return Tuple.Create("2", dataType, cellValue); + var dataType = ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture) ? "n" : "str"; + return ("2", dataType, cellValue); } - return Tuple.Create(columnInfo.ExcelFormatId.ToString(), (string?)null, cellValue); + return (columnInfo.ExcelFormatId.ToString(), null, cellValue); } if (type == typeof(bool)) - return Tuple.Create("2", "b", (bool)value ? "1" : "0"); + return ("2", "b", (bool)value ? "1" : "0"); if (type == typeof(byte[]) && _configuration.EnableConvertByteArray) { if (!_configuration.EnableWriteFilePath) - return Tuple.Create("4", "str", ""); + return ("4", "str", ""); var base64 = GetFileValue(rowIndex, cellIndex, value); - return Tuple.Create("4", "str", XmlHelper.EncodeXml(base64)); + return ("4", "str", XmlHelper.EncodeXml(base64)); } - return Tuple.Create("2", "str", XmlHelper.EncodeXml(value.ToString())); + return ("2", "str", XmlHelper.EncodeXml(value.ToString())); } private static Type? GetValueType(object value, MiniExcelColumnMapping? columnInfo) @@ -303,20 +303,20 @@ private string GetFileValue(int rowIndex, int cellIndex, object value) return base64; } - private Tuple GetDateTimeValue(DateTime value, MiniExcelColumnMapping columnInfo) + private (string, string?, string) GetDateTimeValue(DateTime value, MiniExcelColumnMapping columnInfo) { string? cellValue; if (!ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture)) { cellValue = value.ToString(_configuration.Culture); - return Tuple.Create("2", (string?)"str", cellValue); + return ("2", (string?)"str", cellValue); } var oaDate = CorrectDateTimeValue(value); cellValue = oaDate.ToString(CultureInfo.InvariantCulture); var format = columnInfo?.ExcelFormat is not null ? columnInfo.ExcelFormatId.ToString() : "3"; - return Tuple.Create(format, (string?)null, cellValue); + return (format, null, cellValue); } private static double CorrectDateTimeValue(DateTime value) @@ -409,6 +409,6 @@ private string GetContentTypesXml() private string GetCellXfId(string styleIndex) { - return _cellXfIdMap.TryGetValue(styleIndex, out var cellXfId) ? cellXfId : styleIndex; + return _cellXfIdMap.GetValueOrDefault(styleIndex, styleIndex); } } \ No newline at end of file diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.cs index 78a705e1..19ff839f 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.cs @@ -11,9 +11,9 @@ internal partial class OpenXmlWriter : IMiniExcelWriter { private static readonly UTF8Encoding Utf8WithBom = new(true); + private readonly Stream _stream; private readonly ZipArchive _archive; private readonly OpenXmlConfiguration _configuration; - private readonly Stream _stream; private readonly List _sheets = []; private readonly List _files = []; @@ -47,12 +47,8 @@ internal static async ValueTask CreateAsync(Stream stream, object // A. Why ZipArchiveMode.Update and not ZipArchiveMode.Create? // R. ZipArchiveEntry does not support seeking when Mode is Create. var archiveMode = conf.FastMode ? ZipArchiveMode.Update : ZipArchiveMode.Create; - -#if NET10_0_OR_GREATER var archive = await ZipArchive.CreateAsync(stream, archiveMode, true, Utf8WithBom, cancellationToken).ConfigureAwait(false); -#else - var archive = new ZipArchive(stream, archiveMode, true, Utf8WithBom); -#endif + return new OpenXmlWriter(stream, archive, value, sheetName, conf, printHeader); } @@ -61,7 +57,9 @@ public async Task SaveAsAsync(IProgress? progress = null, Cancellati { try { - await GenerateDefaultOpenXmlAsync(cancellationToken).ConfigureAwait(false); + await CreateZipEntryAsync(ExcelFileNames.Rels, ExcelContentTypes.Relationships, ExcelXml.DefaultRels, cancellationToken).ConfigureAwait(false); + await CreateZipEntryAsync(ExcelFileNames.SharedStrings, ExcelContentTypes.SharedStrings, ExcelXml.DefaultSharedString, cancellationToken).ConfigureAwait(false); + await GenerateStylesXmlAsync(cancellationToken).ConfigureAwait(false); var sheets = GetSheets(); var rowsWritten = new List(); @@ -169,14 +167,6 @@ public async Task InsertAsync(bool overwriteSheet = false, IProgress? } } - [CreateSyncVersion] - private async Task GenerateDefaultOpenXmlAsync(CancellationToken cancellationToken) - { - await CreateZipEntryAsync(ExcelFileNames.Rels, ExcelContentTypes.Relationships, ExcelXml.DefaultRels, cancellationToken).ConfigureAwait(false); - await CreateZipEntryAsync(ExcelFileNames.SharedStrings, ExcelContentTypes.SharedStrings, ExcelXml.DefaultSharedString, cancellationToken).ConfigureAwait(false); - await GenerateStylesXmlAsync(cancellationToken).ConfigureAwait(false); - } - [CreateSyncVersion] private async Task CreateSheetXmlAsync(object? values, string sheetPath, IProgress? progress, CancellationToken cancellationToken) { @@ -184,11 +174,7 @@ private async Task CreateSheetXmlAsync(object? values, string sheetPath, IP var rowsWritten = 0; #if NET8_0_OR_GREATER -#if NET10_0_OR_GREATER var zipStream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); -#else - var zipStream = entry.Open(); -#endif await using var disposableZipStream = zipStream.ConfigureAwait(false); var writer = new MiniExcelStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize); @@ -247,6 +233,7 @@ private async Task WriteValuesAsync(MiniExcelStreamWriter writer, object va { writeAdapter = MiniExcelWriteAdapterFactory.GetWriteAdapter(values, _configuration); } + try { var count = 0; @@ -482,11 +469,7 @@ private async Task WriteCellAsync(MiniExcelStreamWriter writer, int rowIndex, in return; } - var tuple = GetCellValue(rowIndex, cellIndex, value, columnInfo, valueIsNull); - - var styleIndex = tuple.Item1; - var dataType = tuple.Item2; - var cellValue = tuple.Item3; + var (styleIndex, dataType, cellValue) = GetCellValue(rowIndex, cellIndex, value, columnInfo, valueIsNull); var columnType = columnInfo.ExcelColumnType; /*Prefix and suffix blank space will lost after SaveAs #294*/ @@ -628,21 +611,14 @@ private async Task InsertContentTypesXmlAsync(CancellationToken cancellationToke } #if NET8_0_OR_GREATER -#if NET10_0_OR_GREATER var stream = await contentTypesZipEntry.OpenAsync(cancellationToken).ConfigureAwait(false); -#else - var stream = contentTypesZipEntry.Open(); -#endif await using var disposableStream = stream.ConfigureAwait(false); -#else - using var stream = contentTypesZipEntry.Open(); -#endif - -#if NETCOREAPP2_0_OR_GREATER var doc = await XDocument.LoadAsync(stream, LoadOptions.None, cancellationToken).ConfigureAwait(false); #else + using var stream = contentTypesZipEntry.Open(); var doc = XDocument.Load(stream); #endif + var ns = doc.Root?.GetDefaultNamespace(); var typesElement = doc.Descendants(ns + "Types").Single(); @@ -681,12 +657,9 @@ private async Task CreateZipEntryAsync(string path, string? contentType, string var entry = _archive.CreateEntry(path, CompressionLevel.Fastest); #if NET8_0_OR_GREATER -#if NET10_0_OR_GREATER var zipStream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); -#else - var zipStream = entry.Open(); -#endif await using var disposableZipStream = zipStream.ConfigureAwait(false); + var writer = new MiniExcelStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize); await using var disposableWriter = writer.ConfigureAwait(false); #else @@ -707,16 +680,104 @@ private async Task CreateZipEntryAsync(string path, byte[] content, Cancellation var entry = _archive.CreateEntry(path, CompressionLevel.Fastest); #if NET8_0_OR_GREATER -#if NET10_0_OR_GREATER var zipStream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); -#else - var zipStream = entry.Open(); -#endif - await using var disposableZipStream = zipStream.ConfigureAwait(false); + await using var disposableZipStream = zipStream.ConfigureAwait(false); await zipStream.WriteAsync(content, cancellationToken).ConfigureAwait(false); #else using var zipStream = entry.Open(); await zipStream.WriteAsync(content, 0, content.Length, cancellationToken).ConfigureAwait(false); #endif } + + [CreateSyncVersion] + /* Todo: this method is not very efficient, but workbook.xml is generally a very small file so at the moment it's not worth over-optimizing it. + Also, consider adding active sheet as one of the editable properties. */ + internal async Task AlterWorksheetAsync(string sheetName, string? newSheetName, int? newSheetIndex, SheetState? newSheetState, CancellationToken cancellationToken = default) + { + if (newSheetName is null && newSheetIndex is null && newSheetState is null) + return; + + var oldWorkbookEntry = _archive.GetEntry("xl/workbook.xml")!; + + try + { + var xmlDoc = await LoadWorkbook().ConfigureAwait(false); + + oldWorkbookEntry.Delete(); + var newWorkbookEntry = _archive.CreateEntry("xl/workbook.xml", CompressionLevel.Fastest); +#if NET8_0_OR_GREATER + var newZipStream = await newWorkbookEntry.OpenAsync(cancellationToken).ConfigureAwait(false); + await using var newDisposableZipStream = newZipStream.ConfigureAwait(false); + var writer = XmlWriter.Create(newZipStream, new XmlWriterSettings + { +#if !SYNC_ONLY + Async = true +#endif + }); + await using var disposableWriter = writer.ConfigureAwait(false); + await xmlDoc.WriteToAsync(writer, CancellationToken.None).ConfigureAwait(false); +#else + using var newZipStream = newWorkbookEntry.Open(); + using var writer = XmlWriter.Create(newZipStream, new XmlWriterSettings { Async = false }); + xmlDoc.WriteTo(writer); +#endif + } + finally + { +#if NET10_0_OR_GREATER + await _archive.DisposeAsync().ConfigureAwait(false); +#else + _archive.Dispose(); +#endif + } + return; + + async Task LoadWorkbook() + { +#if NET8_0_OR_GREATER + var zipStream = await oldWorkbookEntry.OpenAsync(cancellationToken).ConfigureAwait(false); + await using var disposableZipStream = zipStream.ConfigureAwait(false); + var workbookDoc = await XDocument.LoadAsync(zipStream, LoadOptions.None, cancellationToken).ConfigureAwait(false); +#else + using var zipStream = oldWorkbookEntry.Open(); + var workbookDoc = XDocument.Load(zipStream); +#endif + var sheetsContainer = workbookDoc.Root?.Element((XNamespace)Schemas.SpreadsheetmlXmlMain + "sheets")!; + var sheets = sheetsContainer.Elements().ToList(); + + if (sheets.Find(s => s.Attribute("name")?.Value.Equals(sheetName, StringComparison.OrdinalIgnoreCase) is true) is not { } sheet) + throw new InvalidDataException($"Sheet {sheetName} not found"); + + if (!string.IsNullOrEmpty(newSheetName)) + { + if (newSheetName.Length > 31) + throw new ArgumentException($"The name \"{newSheetName}\" is too long, the maximum allowed length is 31 characters."); + + sheet.SetAttributeValue("name", newSheetName); + } + + if (newSheetIndex is not null) + { + var newIndex = Math.Clamp(newSheetIndex.Value, 0, sheets.Count - 1); + sheets.Remove(sheet); + sheets.Insert(newIndex, sheet); + + sheetsContainer.RemoveAll(); + sheetsContainer.Add(sheets); + } + + if (newSheetState is not null) + { + sheet.SetAttributeValue("state", newSheetState switch + { + SheetState.Visible => "visible", + SheetState.Hidden => "hidden", + SheetState.VeryHidden => "veryHidden", + _ => "visible" + }); + } + + return workbookDoc; + } + } } diff --git a/src/MiniExcel.OpenXml/Picture/OpenXmlPictureImplement.cs b/src/MiniExcel.OpenXml/Picture/OpenXmlPictureImplement.cs index 920330cd..ab47642f 100644 --- a/src/MiniExcel.OpenXml/Picture/OpenXmlPictureImplement.cs +++ b/src/MiniExcel.OpenXml/Picture/OpenXmlPictureImplement.cs @@ -136,15 +136,12 @@ public static async Task AddPictureAsync(Stream excelStream, CancellationToken c var imagePath = $"xl/media/{imageName}"; var imageEntry = archive.CreateEntry(imagePath); -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER var entryStream = await imageEntry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableStream = entryStream.ConfigureAwait(false); -#else - using var entryStream = imageEntry.Open(); -#endif -#if NET5_0_OR_GREATER await entryStream.WriteAsync(imageBytes.AsMemory(), cancellationToken).ConfigureAwait(false); #else + using var entryStream = imageEntry.Open(); await entryStream.WriteAsync(imageBytes, 0, imageBytes.Length, cancellationToken).ConfigureAwait(false); #endif diff --git a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildContext.cs b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildContext.cs index aebade58..7e7d5d12 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildContext.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildContext.cs @@ -59,12 +59,9 @@ public async Task InitializeAsync(SheetStyleElementInfos generateElementInfos, C if (_oldStyleXmlZipEntry is not null) { -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER var oldStyleXmlStream = await _oldStyleXmlZipEntry.OpenAsync(cancellationToken).ConfigureAwait(false); await using (_ = oldStyleXmlStream.ConfigureAwait(false)) -#elif NET8_0_OR_GREATER - var oldStyleXmlStream = _oldStyleXmlZipEntry.Open(); - await using (_ = oldStyleXmlStream.ConfigureAwait(false)) #else using (var oldStyleXmlStream = _oldStyleXmlZipEntry.Open()) #endif @@ -73,7 +70,7 @@ public async Task InitializeAsync(SheetStyleElementInfos generateElementInfos, C OldElementInfos = await ReadSheetStyleElementInfosAsync(reader, cancellationToken).ConfigureAwait(false); } -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER _oldXmlReaderStream = await _oldStyleXmlZipEntry.OpenAsync(cancellationToken).ConfigureAwait(false); #else _oldXmlReaderStream = _oldStyleXmlZipEntry.Open(); @@ -90,7 +87,7 @@ public async Task InitializeAsync(SheetStyleElementInfos generateElementInfos, C _newStyleXmlZipEntry = _archive.CreateEntry(ExcelFileNames.Styles, CompressionLevel.Fastest); } -#if NET10_0_OR_GREATER +#if NET8_0_OR_GREATER _newXmlWriterStream = await _newStyleXmlZipEntry.OpenAsync(cancellationToken).ConfigureAwait(false); #else _newXmlWriterStream = _newStyleXmlZipEntry.Open(); @@ -160,15 +157,10 @@ public async Task FinalizeAndUpdateZipDictionaryAsync(CancellationToken cancella var finalStyleXmlZipEntry = _archive.CreateEntry(ExcelFileNames.Styles, CompressionLevel.Fastest); #if NET8_0_OR_GREATER -#if NET10_0_OR_GREATER var tempStream = await _newStyleXmlZipEntry!.OpenAsync(cancellationToken).ConfigureAwait(false); var newStream = await finalStyleXmlZipEntry.OpenAsync(cancellationToken).ConfigureAwait(false); -#else - var tempStream = _newStyleXmlZipEntry!.Open(); - var newStream = finalStyleXmlZipEntry.Open(); -#endif - await using (var disposableTempStream = tempStream.ConfigureAwait(false)) - await using (var disposableNewStream = newStream.ConfigureAwait(false)) + await using (_ = tempStream.ConfigureAwait(false)) + await using (_= newStream.ConfigureAwait(false)) #else using (var tempStream = _newStyleXmlZipEntry!.Open()) using (var newStream = finalStyleXmlZipEntry.Open()) @@ -178,7 +170,7 @@ public async Task FinalizeAndUpdateZipDictionaryAsync(CancellationToken cancella } _zipDictionary[ExcelFileNames.Styles] = new ZipPackageInfo(finalStyleXmlZipEntry, ExcelContentTypes.Styles); - _newStyleXmlZipEntry.Delete(); + _newStyleXmlZipEntry?.Delete(); _newStyleXmlZipEntry = null; } diff --git a/src/MiniExcel.OpenXml/Styles/OpenXmlStyles.cs b/src/MiniExcel.OpenXml/Styles/OpenXmlStyles.cs index dd55dc1b..20f51a19 100644 --- a/src/MiniExcel.OpenXml/Styles/OpenXmlStyles.cs +++ b/src/MiniExcel.OpenXml/Styles/OpenXmlStyles.cs @@ -17,14 +17,14 @@ public OpenXmlStyles(OpenXmlZip zip) if (reader is null) throw new InvalidDataException("The OpenXml styles could not be found, the file might be malformed."); - if (!XmlReaderHelper.IsStartElement(reader, "styleSheet", Ns)) + if (!reader.IsStartElement("styleSheet", Ns)) return; - if (!XmlReaderHelper.ReadFirstContent(reader)) + if (!reader.ReadFirstContent()) return; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "cellXfs", Ns)) + if (reader.IsStartElement("cellXfs", Ns)) { if (!XmlReaderHelper.ReadFirstContent(reader)) continue; @@ -32,7 +32,7 @@ public OpenXmlStyles(OpenXmlZip zip) var index = 0; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "xf", Ns)) + if (reader.IsStartElement("xf", Ns)) { int.TryParse(reader.GetAttribute("xfId"), out var xfId); int.TryParse(reader.GetAttribute("numFmtId"), out var numFmtId); @@ -40,19 +40,19 @@ public OpenXmlStyles(OpenXmlZip zip) reader.Skip(); index++; } - else if (!XmlReaderHelper.SkipContent(reader)) + else if (!reader.SkipContent()) break; } } - else if (XmlReaderHelper.IsStartElement(reader, "cellStyleXfs", Ns)) + else if (reader.IsStartElement("cellStyleXfs", Ns)) { - if (!XmlReaderHelper.ReadFirstContent(reader)) + if (!reader.ReadFirstContent()) continue; var index = 0; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "xf", Ns)) + if (reader.IsStartElement("xf", Ns)) { int.TryParse(reader.GetAttribute("xfId"), out var xfId); int.TryParse(reader.GetAttribute("numFmtId"), out var numFmtId); @@ -61,20 +61,20 @@ public OpenXmlStyles(OpenXmlZip zip) reader.Skip(); index++; } - else if (!XmlReaderHelper.SkipContent(reader)) + else if (!reader.SkipContent()) { break; } } } - else if (XmlReaderHelper.IsStartElement(reader, "numFmts", Ns)) + else if (reader.IsStartElement("numFmts", Ns)) { - if (!XmlReaderHelper.ReadFirstContent(reader)) + if (!reader.ReadFirstContent()) continue; while (!reader.EOF) { - if (XmlReaderHelper.IsStartElement(reader, "numFmt", Ns)) + if (reader.IsStartElement("numFmt", Ns)) { _ = int.TryParse(reader.GetAttribute("numFmtId"), out var numFmtId); var formatCode = reader.GetAttribute("formatCode"); @@ -94,13 +94,13 @@ public OpenXmlStyles(OpenXmlZip zip) #endif reader.Skip(); } - else if (!XmlReaderHelper.SkipContent(reader)) + else if (!reader.SkipContent()) { break; } } } - else if (!XmlReaderHelper.SkipContent(reader)) + else if (!reader.SkipContent()) { break; } diff --git a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs index 714c6394..859654d0 100644 --- a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs +++ b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs @@ -87,11 +87,7 @@ private async Task GenerateSheetByUpdateModeAsync(ZipArchiveEntry sheetZipEntry, private async Task GenerateSheetByCreateModeAsync(ZipArchiveEntry templateSheetZipEntry, Stream outputZipSheetEntryStream, IDictionary inputMaps, IDictionary sharedStrings, bool mergeCells = false, CancellationToken cancellationToken = default) { #if NET8_0_OR_GREATER -#if NET10_0_OR_GREATER var newTemplateStream = await templateSheetZipEntry.OpenAsync(cancellationToken).ConfigureAwait(false); -#else - var newTemplateStream = templateSheetZipEntry.Open(); -#endif await using var disposableNewTemplateStream = newTemplateStream.ConfigureAwait(false); var doc = await XDocument.LoadAsync(newTemplateStream, LoadOptions.None, cancellationToken).ConfigureAwait(false); #else diff --git a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.MergeCells.cs b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.MergeCells.cs index b5c7aa11..5d84f3b4 100644 --- a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.MergeCells.cs +++ b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.MergeCells.cs @@ -59,14 +59,10 @@ await stream.CopyToAsync(_outputFileStream var entry = archive.ZipFile.CreateEntry(sheet.FullName); #if NET8_0_OR_GREATER -#if NET10_0_OR_GREATER var sheetStream = await sheet.OpenAsync(cancellationToken).ConfigureAwait(false); - var zipStream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); -#else - var sheetStream = sheet.Open(); - var zipStream = entry.Open(); -#endif await using var disposableSheetStream = sheetStream.ConfigureAwait(false); + + var zipStream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableZipStream = zipStream.ConfigureAwait(false); #else using var sheetStream = sheet.Open(); diff --git a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.cs b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.cs index 56c33cf4..eb52eaa3 100644 --- a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.cs +++ b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.cs @@ -71,7 +71,7 @@ public async Task SaveAsByTemplateAsync(Stream templateStream, object value, Can templateStream.Position = 0; #if NET10_0_OR_GREATER var originalArchive = await ZipArchive.CreateAsync(templateStream, ZipArchiveMode.Read, false, null, cancellationToken).ConfigureAwait(false); - await using var disposableArchive = originalArchive.ConfigureAwait(false); + await using var disposableArchive = originalArchive.ConfigureAwait(false); #else using var originalArchive = new ZipArchive(templateStream, ZipArchiveMode.Read); #endif @@ -88,14 +88,10 @@ public async Task SaveAsByTemplateAsync(Stream templateStream, object value, Can // Copy the content of the original entry to the new entry #if NET8_0_OR_GREATER -#if NET10_0_OR_GREATER var originalEntryStream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); - var newEntryStream = await newEntry.OpenAsync(cancellationToken).ConfigureAwait(false); -#else - var originalEntryStream = entry.Open(); - var newEntryStream = newEntry.Open(); -#endif await using var disposableEntryStream = originalEntryStream.ConfigureAwait(false); + + var newEntryStream = await newEntry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableNewEntryStream = newEntryStream.ConfigureAwait(false); #else using var originalEntryStream = entry.Open(); @@ -133,11 +129,7 @@ await originalEntryStream.CopyToAsync(newEntryStream var outputZipEntry = outputFileArchive.ZipFile.CreateEntry(templateFullName); #if NET8_0_OR_GREATER -#if NET10_0_OR_GREATER var outputZipSheetEntryStream = await outputZipEntry.OpenAsync(cancellationToken).ConfigureAwait(false); -#else - var outputZipSheetEntryStream = outputZipEntry.Open(); -#endif await using var disposableSheetEntryStream = outputZipSheetEntryStream.ConfigureAwait(false); #else using var outputZipSheetEntryStream = outputZipEntry.Open(); @@ -160,11 +152,7 @@ await originalEntryStream.CopyToAsync(newEntryStream var calcChainEntry = outputFileArchive.ZipFile.CreateEntry(calcChainPathName); #if NET8_0_OR_GREATER -#if NET10_0_OR_GREATER var calcChainStream = await calcChainEntry.OpenAsync(cancellationToken).ConfigureAwait(false); -#else - var calcChainStream = calcChainEntry.Open(); -#endif await using var disposableChainEntryStream = calcChainStream.ConfigureAwait(false); #else using var calcChainStream = calcChainEntry.Open(); @@ -181,14 +169,10 @@ await originalEntryStream.CopyToAsync(newEntryStream // Copy the content of the original entry to the new entry #if NET8_0_OR_GREATER -#if NET10_0_OR_GREATER var originalEntryStream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); - var newEntryStream = await newEntry.OpenAsync(cancellationToken).ConfigureAwait(false); -#else - var originalEntryStream = entry.Open(); - var newEntryStream = newEntry.Open(); -#endif await using var disposableEntryStream = originalEntryStream.ConfigureAwait(false); + + var newEntryStream = await newEntry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableNewEntryStream = newEntryStream.ConfigureAwait(false); #else using var originalEntryStream = entry.Open(); diff --git a/src/MiniExcel.OpenXml/Utils/MiniExcelPropertyHelper.cs b/src/MiniExcel.OpenXml/Utils/MiniExcelPropertyHelper.cs index b9766a72..58f73568 100644 --- a/src/MiniExcel.OpenXml/Utils/MiniExcelPropertyHelper.cs +++ b/src/MiniExcel.OpenXml/Utils/MiniExcelPropertyHelper.cs @@ -4,10 +4,10 @@ namespace MiniExcelLib.OpenXml.Utils; internal static class MiniExcelPropertyHelper { - internal static ExcellSheetInfo GetExcellSheetInfo(Type type, MiniExcelBaseConfiguration configuration) + internal static ExcelSheetInfo GetExcelSheetInfo(Type type, MiniExcelBaseConfiguration configuration) { // default options - var sheetInfo = new ExcellSheetInfo + var sheetInfo = new ExcelSheetInfo { Key = type.Name, ExcelSheetName = null, // will be generated automatically as Sheet @@ -15,7 +15,7 @@ internal static ExcellSheetInfo GetExcellSheetInfo(Type type, MiniExcelBaseConfi }; // options from ExcelSheetAttribute - if (type.GetCustomAttribute(typeof(MiniExcelSheetAttribute)) is MiniExcelSheetAttribute excelSheetAttr) + if (type.GetCustomAttribute() is { } excelSheetAttr) { sheetInfo.ExcelSheetName = excelSheetAttr.Name ?? type.Name; sheetInfo.ExcelSheetState = excelSheetAttr.State; diff --git a/src/MiniExcel.OpenXml/Utils/XmlReaderHelper.cs b/src/MiniExcel.OpenXml/Utils/XmlReaderHelper.cs index 6b8f8c41..0373d302 100644 --- a/src/MiniExcel.OpenXml/Utils/XmlReaderHelper.cs +++ b/src/MiniExcel.OpenXml/Utils/XmlReaderHelper.cs @@ -5,41 +5,10 @@ namespace MiniExcelLib.OpenXml.Utils; internal static partial class XmlReaderHelper { private static readonly string[] Ns = [Schemas.SpreadsheetmlXmlMain, Schemas.SpreadsheetmlXmlStrictNs]; - - /// - /// Pass <?xml> and <worksheet> - /// - [CreateSyncVersion] - public static async Task PassXmlDeclarationAndWorksheetAsync(this XmlReader reader, CancellationToken cancellationToken = default) - { - await reader.MoveToContentAsync() -#if NET6_0_OR_GREATER - .WaitAsync(cancellationToken) -#endif - .ConfigureAwait(false); - await reader.ReadAsync() -#if NET6_0_OR_GREATER - .WaitAsync(cancellationToken) -#endif - .ConfigureAwait(false); - } - - /// - /// e.g skip row 1 to row 2 - /// - [CreateSyncVersion] - public static async Task SkipToNextSameLevelDomAsync(XmlReader reader, CancellationToken cancellationToken = default) - { - while (!reader.EOF) - { - if (!await SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) - break; - } - } - //Method from ExcelDataReader @MIT License + // Copied and modified from ExcelDataReader - @MIT License [CreateSyncVersion] - public static async Task ReadFirstContentAsync(XmlReader reader, CancellationToken cancellationToken = default) + public static async Task ReadFirstContentAsync(this XmlReader reader, CancellationToken cancellationToken = default) { if (reader.IsEmptyElement) { @@ -56,6 +25,7 @@ await reader.MoveToContentAsync() .WaitAsync(cancellationToken) #endif .ConfigureAwait(false); + await reader.ReadAsync() #if NET6_0_OR_GREATER .WaitAsync(cancellationToken) @@ -64,9 +34,9 @@ await reader.ReadAsync() return true; } - //Method from ExcelDataReader @MIT License + // Copied and modified from ExcelDataReader - @MIT License [CreateSyncVersion] - public static async Task SkipContentAsync(XmlReader reader, CancellationToken cancellationToken = default) + public static async Task SkipContentAsync(this XmlReader reader, CancellationToken cancellationToken = default) { if (reader.NodeType == XmlNodeType.EndElement) { @@ -86,77 +56,82 @@ await reader.SkipAsync() return true; } - public static bool IsStartElement(XmlReader reader, string name, params string[] nss) + /// + /// e.g skip row 1 to row 2 + /// + [CreateSyncVersion] + public static async Task SkipToNextSiblingAsync(this XmlReader reader, CancellationToken cancellationToken = default) + { + while (!reader.EOF) + { + if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) + break; + } + } + + public static bool IsStartElement(this XmlReader reader, string name, params string[] nss) { return nss.Any(s => reader.IsStartElement(name, s)); } - public static string? GetAttribute(XmlReader reader, string name, params string[] nss) + public static string? GetAttribute(this XmlReader reader, string name, params string[] nss) { return nss .Select(ns => reader.GetAttribute(name, ns)) .FirstOrDefault(at => at is not null); } + // Copied and modified from ExcelDataReader - @MIT License [CreateSyncVersion] - public static async IAsyncEnumerable GetSharedStringsAsync(Stream stream, [EnumeratorCancellation]CancellationToken cancellationToken = default, params string[] nss) + public static async Task ReadStringItemAsync(this XmlReader reader, CancellationToken cancellationToken = default) { - var xmlSettings = GetXmlReaderSettings( -#if SYNC_ONLY - false -#else - true -#endif - ); - - using var reader = XmlReader.Create(stream, xmlSettings); - if (!IsStartElement(reader, "sst", nss)) - yield break; - - if (!await ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) - yield break; + var result = new StringBuilder(); + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) + return string.Empty; while (!reader.EOF) { - if (IsStartElement(reader, "si", nss)) + if (reader.IsStartElement("t", Ns)) { - var value = await ReadStringItemAsync(reader, cancellationToken).ConfigureAwait(false); - yield return value; + // There are multiple in a . Concatenate within an . + result.Append(await reader.ReadElementContentAsStringAsync() +#if NET6_0_OR_GREATER + .WaitAsync(cancellationToken) +#endif + .ConfigureAwait(false)); } - else if (!await SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (reader.IsStartElement("r", Ns)) + { + result.Append(await reader.ReadRichTextRunAsync(cancellationToken).ConfigureAwait(false)); + } + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } } + + return result.ToString(); } - - /// - /// Copied and modified from ExcelDataReader - @MIT License - /// + // Copied and modified from ExcelDataReader - @MIT License [CreateSyncVersion] - public static async Task ReadStringItemAsync(XmlReader reader, CancellationToken cancellationToken = default) + private static async Task ReadRichTextRunAsync(this XmlReader reader, CancellationToken cancellationToken = default) { var result = new StringBuilder(); - if (!await ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) return string.Empty; while (!reader.EOF) { - if (IsStartElement(reader, "t", Ns)) + if (reader.IsStartElement("t", Ns)) { - // There are multiple in a . Concatenate within an . result.Append(await reader.ReadElementContentAsStringAsync() #if NET6_0_OR_GREATER - .WaitAsync(cancellationToken) + .WaitAsync(cancellationToken) #endif .ConfigureAwait(false)); } - else if (IsStartElement(reader, "r", Ns)) - { - result.Append(await ReadRichTextRunAsync(reader, cancellationToken).ConfigureAwait(false)); - } - else if (!await SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } @@ -165,40 +140,43 @@ public static async Task ReadStringItemAsync(XmlReader reader, Cancellat return result.ToString(); } - /// - /// Copied and modified from ExcelDataReader - @MIT License - /// [CreateSyncVersion] - private static async Task ReadRichTextRunAsync(XmlReader reader, CancellationToken cancellationToken = default) + public static async IAsyncEnumerable GetSharedStringsAsync(Stream stream, [EnumeratorCancellation]CancellationToken cancellationToken = default, params string[] nss) { - var result = new StringBuilder(); - if (!await ReadFirstContentAsync(reader, cancellationToken).ConfigureAwait(false)) - return string.Empty; + var xmlSettings = GetXmlReaderSettings( +#if SYNC_ONLY + false +#else + true +#endif + ); + + using var reader = XmlReader.Create(stream, xmlSettings); + if (!reader.IsStartElement("sst", nss)) + yield break; + + if (!await reader.ReadFirstContentAsync(cancellationToken).ConfigureAwait(false)) + yield break; while (!reader.EOF) { - if (IsStartElement(reader, "t", Ns)) + if (reader.IsStartElement("si", nss)) { - result.Append(await reader.ReadElementContentAsStringAsync() -#if NET6_0_OR_GREATER - .WaitAsync(cancellationToken) -#endif - .ConfigureAwait(false)); + var value = await reader.ReadStringItemAsync(cancellationToken).ConfigureAwait(false); + yield return value; } - else if (!await SkipContentAsync(reader, cancellationToken).ConfigureAwait(false)) + else if (!await reader.SkipContentAsync(cancellationToken).ConfigureAwait(false)) { break; } } - - return result.ToString(); } - + internal static XmlReaderSettings GetXmlReaderSettings(bool async) => new() { IgnoreComments = true, IgnoreWhitespace = true, XmlResolver = null, - Async = async, + Async = async }; } diff --git a/src/MiniExcel.OpenXml/Zip/OpenXmlZip.cs b/src/MiniExcel.OpenXml/Zip/OpenXmlZip.cs index 2e1a68c4..1c13805a 100644 --- a/src/MiniExcel.OpenXml/Zip/OpenXmlZip.cs +++ b/src/MiniExcel.OpenXml/Zip/OpenXmlZip.cs @@ -2,7 +2,7 @@ namespace MiniExcelLib.OpenXml.Zip; -/// Copy & modified by ExcelDataReader ZipWorker @MIT License +/// Copied & modified from ExcelDataReader ZipWorker @MIT License internal sealed partial class OpenXmlZip : IDisposable, IAsyncDisposable { private static readonly XmlReaderSettings XmlSettings = new() @@ -30,11 +30,7 @@ private OpenXmlZip(ZipArchive zipArchive, Dictionary e internal static async Task CreateAsync(Stream fileStream, ZipArchiveMode mode = ZipArchiveMode.Read, bool leaveOpen = false, Encoding? entryNameEncoding = null, bool isUpdateMode = true, CancellationToken cancellationToken = default) { entryNameEncoding ??= Encoding.UTF8; -#if NET10_0_OR_GREATER var zipFile = await ZipArchive.CreateAsync(fileStream, mode, leaveOpen, entryNameEncoding, cancellationToken).ConfigureAwait(false); -#else - var zipFile = new ZipArchive(fileStream, mode, leaveOpen, entryNameEncoding); -#endif if (!isUpdateMode) return new OpenXmlZip(zipFile, []); diff --git a/tests/MiniExcel.OpenXml.Tests/AlterSheets/MiniExcelAlterSheetsTests.cs b/tests/MiniExcel.OpenXml.Tests/AlterSheets/MiniExcelAlterSheetsTests.cs new file mode 100644 index 00000000..e76a37f9 --- /dev/null +++ b/tests/MiniExcel.OpenXml.Tests/AlterSheets/MiniExcelAlterSheetsTests.cs @@ -0,0 +1,120 @@ +using MiniExcelLib.OpenXml.Models; +using static MiniExcelLib.OpenXml.Tests.Utils.SheetHelper; + +namespace MiniExcelLib.OpenXml.Tests.AlterSheets; + +public class MiniExcelAlterSheetTests +{ + private readonly OpenXmlExporter _excelExporter = MiniExcel.Exporters.GetOpenXmlExporter(); + + [Fact] + public void AlterSheet_WhenNewNameProvided_RenamesWorksheet() + { + // Arrange + const string originalName = "Sheet1"; + const string newName = "RenamedSheet"; + using var stream = CreateTestWorkbookStream(); + + // Act + _excelExporter.AlterSheet(stream, originalName, newSheetName: newName); + + // Assert + stream.Position = 0; + using var package = new ExcelPackage(stream); + + Assert.Null(package.Workbook.Worksheets[originalName]); + Assert.NotNull(package.Workbook.Worksheets[newName]); + } + + [Fact] + public void AlterSheet_WhenNewIndexProvided_MovesWorksheet() + { + // Arrange + const string targetSheet = "Sheet1"; + const int newIndex = 2; + using var stream = CreateTestWorkbookStream(); + + // Act + _excelExporter.AlterSheet(stream, targetSheet, newSheetIndex: newIndex); + + // Assert + stream.Position = 0; + using var package = new ExcelPackage(stream); + + // Assert that the sheet at the new index is indeed our target sheet + Assert.Equal(targetSheet, package.Workbook.Worksheets[newIndex].Name); + } + + [Fact] + public void AlterSheet_WhenNewStateProvided_ChangesVisibility() + { + // Arrange + const string targetSheet = "Sheet2"; + using var stream = CreateTestWorkbookStream(); + + // Act + _excelExporter.AlterSheet(stream, targetSheet, newSheetState: SheetState.Hidden); + + // Assert + stream.Position = 0; + using var package = new ExcelPackage(stream); + + var sheet = package.Workbook.Worksheets[targetSheet]; + Assert.Equal(eWorkSheetHidden.Hidden, sheet.Hidden); + } + + [Fact] + public void AlterSheet_WhenAllPropertiesProvided_UpdatesAllSuccessfully() + { + // Arrange + const string originalName = "Sheet3"; + const string newName = "SecretData"; + const int newIndex = 0; + const SheetState newState = SheetState.VeryHidden; + using var stream = CreateTestWorkbookStream(); + + // Act + _excelExporter.AlterSheet( + stream, + originalName, + newSheetName: newName, + newSheetIndex: newIndex, + newSheetState: newState); + + // Assert + stream.Position = 0; + using var package = new ExcelPackage(stream); + + // 1. Check Name + Assert.Null(package.Workbook.Worksheets[originalName]); + var modifiedSheet = package.Workbook.Worksheets[newName]; + Assert.NotNull(modifiedSheet); + + // 2. Check Index (Should now be the first sheet) + Assert.Equal(newName, package.Workbook.Worksheets[newIndex].Name); + + // 3. Check State + Assert.Equal(eWorkSheetHidden.VeryHidden, modifiedSheet.Hidden); + } + + [Fact] + public void AlterSheet_WhenNoOptionalParametersProvided_LeavesSheetUnchanged() + { + // Arrange + const string targetSheet = "Sheet1"; + using var stream = CreateTestWorkbookStream(); + + // Act + _excelExporter.AlterSheet(stream, targetSheet); + + // Assert + stream.Position = 0; + using var package = new ExcelPackage(stream); + var sheet = package.Workbook.Worksheets[targetSheet]; + + // Ensure defaults remain intact + Assert.NotNull(sheet); + Assert.Equal("Sheet1", package.Workbook.Worksheets[0].Name); + Assert.Equal(eWorkSheetHidden.Visible, sheet.Hidden); + } +} \ No newline at end of file diff --git a/tests/MiniExcel.OpenXml.Tests/AlterSheets/MiniExcelAlterSheetsTestsAsync.cs b/tests/MiniExcel.OpenXml.Tests/AlterSheets/MiniExcelAlterSheetsTestsAsync.cs new file mode 100644 index 00000000..091b5199 --- /dev/null +++ b/tests/MiniExcel.OpenXml.Tests/AlterSheets/MiniExcelAlterSheetsTestsAsync.cs @@ -0,0 +1,120 @@ +using MiniExcelLib.OpenXml.Models; +using static MiniExcelLib.OpenXml.Tests.Utils.SheetHelper; + +namespace MiniExcelLib.OpenXml.Tests.AlterSheets; + +public class MiniExcelAlterSheetsTestAsync +{ + private readonly OpenXmlExporter _excelExporter = MiniExcel.Exporters.GetOpenXmlExporter(); + + [Fact] + public async Task AlterSheetAsync_WhenNewNameProvided_RenamesWorksheet() + { + // Arrange + const string originalName = "Sheet1"; + const string newName = "RenamedSheet"; + await using var stream = CreateTestWorkbookStream(); + + // Act + await _excelExporter.AlterSheetAsync(stream, originalName, newSheetName: newName); + + // Assert + stream.Position = 0; // Reset to read the saved results + using var package = new ExcelPackage(stream); + + Assert.Null(package.Workbook.Worksheets[originalName]); + Assert.NotNull(package.Workbook.Worksheets[newName]); + } + + [Fact] + public async Task AlterSheetAsync_WhenNewIndexProvided_MovesWorksheet() + { + // Arrange + const string targetSheet = "Sheet1"; + const int newIndex = 2; + await using var stream = CreateTestWorkbookStream(); + + // Act + await _excelExporter.AlterSheetAsync(stream, targetSheet, newSheetIndex: newIndex); + + // Assert + stream.Position = 0; + using var package = new ExcelPackage(stream); + + // Assert that the sheet at the new index is indeed our target sheet + Assert.Equal(targetSheet, package.Workbook.Worksheets[newIndex].Name); + } + + [Fact] + public async Task AlterSheetAsync_WhenNewStateProvided_ChangesVisibility() + { + // Arrange + const string targetSheet = "Sheet2"; + await using var stream = CreateTestWorkbookStream(); + + // Act + await _excelExporter.AlterSheetAsync(stream, targetSheet, newSheetState: SheetState.Hidden); + + // Assert + stream.Position = 0; + using var package = new ExcelPackage(stream); + + var sheet = package.Workbook.Worksheets[targetSheet]; + Assert.Equal(eWorkSheetHidden.Hidden, sheet.Hidden); + } + + [Fact] + public async Task AlterSheetAsync_WhenAllPropertiesProvided_UpdatesAllSuccessfully() + { + // Arrange + const string originalName = "Sheet3"; + const string newName = "SecretData"; + const int newIndex = 0; + const SheetState newState = SheetState.VeryHidden; + await using var stream = CreateTestWorkbookStream(); + + // Act + await _excelExporter.AlterSheetAsync( + stream, + originalName, + newSheetName: newName, + newSheetIndex: newIndex, + newSheetState: newState); + + // Assert + stream.Position = 0; + using var package = new ExcelPackage(stream); + + // 1. Check Name + Assert.Null(package.Workbook.Worksheets[originalName]); + var modifiedSheet = package.Workbook.Worksheets[newName]; + Assert.NotNull(modifiedSheet); + + // 2. Check Index (Should now be the first sheet) + Assert.Equal(newName, package.Workbook.Worksheets[newIndex].Name); + + // 3. Check State + Assert.Equal(eWorkSheetHidden.VeryHidden, modifiedSheet.Hidden); + } + + [Fact] + public async Task AlterSheetAsync_WhenNoOptionalParametersProvided_LeavesSheetUnchanged() + { + // Arrange + const string targetSheet = "Sheet1"; + await using var stream = CreateTestWorkbookStream(); + + // Act + await _excelExporter.AlterSheetAsync(stream, targetSheet); + + // Assert + stream.Position = 0; + using var package = new ExcelPackage(stream); + var sheet = package.Workbook.Worksheets[targetSheet]; + + // Ensure defaults remain intact + Assert.NotNull(sheet); + Assert.Equal("Sheet1", package.Workbook.Worksheets[0].Name); + Assert.Equal(eWorkSheetHidden.Visible, sheet.Hidden); + } +} \ No newline at end of file diff --git a/tests/MiniExcel.OpenXml.Tests/Utils/SheetHelper.cs b/tests/MiniExcel.OpenXml.Tests/Utils/SheetHelper.cs index eb89374b..4257f171 100644 --- a/tests/MiniExcel.OpenXml.Tests/Utils/SheetHelper.cs +++ b/tests/MiniExcel.OpenXml.Tests/Utils/SheetHelper.cs @@ -119,4 +119,18 @@ internal static Dictionary GetFirstSheetMergedCells(string path) return mergeCellsDict; } + + internal static MemoryStream CreateTestWorkbookStream() + { + var stream = new MemoryStream(); + + using var package = new ExcelPackage(stream); + package.Workbook.Worksheets.Add("Sheet1"); + package.Workbook.Worksheets.Add("Sheet2"); + package.Workbook.Worksheets.Add("Sheet3"); + package.Save(); + + stream.Position = 0; + return stream; + } } \ No newline at end of file