From 8b708a28cc51d0db342e3375bfbfdc84411a9afd Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Fri, 15 May 2026 23:03:34 +0200 Subject: [PATCH 01/10] Fixes issue 627 Corrected oversight that prevented numerical string to be read as text by Excel when exported even if exported with the format `"@"` --- .../OpenXmlWriter.DefaultOpenXml.cs | 5 +++- .../MiniExcelIssueAsyncTests.cs | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs index 9e44f8af..80802961 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs @@ -163,7 +163,10 @@ private string GetPanes() return (RegularCellStyleIndex, "str", string.Empty); if (value is string str) - return (RegularCellStyleIndex, "str", XmlHelper.EncodeXml(str)); + { + var styleIndex = columnInfo?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : RegularCellStyleIndex; + return (styleIndex, "str", XmlHelper.EncodeXml(str)); + } var type = GetValueType(value, columnInfo); diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs index e3aede7f..5370e2a9 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs @@ -1534,4 +1534,28 @@ class Issue951 public object this[string test] => new(); } + + [Fact] + public async Task TestIssue627() + { + var data = new[] { new { LongNumber = "1550432695793487872" } }; + + var config = new OpenXmlConfiguration + { + DynamicColumns = + [ + new DynamicExcelColumn("LongNumber") { Format = "@" } + ] + }; + + await using var ms = new MemoryStream(); + await _excelExporter.ExportAsync(ms, data, configuration: config); + ms.Seek(0, SeekOrigin.Begin); + + using var package = new ExcelPackage(ms); + var cell = package.Workbook.Worksheets[0].Cells["A2"]; + + Assert.Equal("1550432695793487872", cell.GetValue()); + Assert.Equal("@", cell.Style.Numberformat.Format); + } } \ No newline at end of file From 91e0a0b471da627404b451d13837aa5922fd7c80 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Fri, 15 May 2026 23:19:09 +0200 Subject: [PATCH 02/10] Minor code cleanup --- src/MiniExcel.OpenXml/Constants/ExcelXml.cs | 7 +- .../OpenXmlWriter.DefaultOpenXml.cs | 44 ++++--- src/MiniExcel.OpenXml/OpenXmlWriter.cs | 122 +++++++++--------- .../Builder/DefaultSheetStyleBuilder.cs | 30 ++--- .../Builder/MinimalSheetStyleBuilder.cs | 7 +- 5 files changed, 104 insertions(+), 106 deletions(-) diff --git a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs index 868d512b..5efcf968 100644 --- a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs +++ b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs @@ -11,6 +11,10 @@ static ExcelXml() DefaultDrawing = XmlHelper.MinifyXml(DefaultDrawing); } + internal const string StringDataType = "str"; + internal const string NumericDataType = "n"; + internal const string BooleanDataType = "b"; + internal const string EmptySheetXml = """"""; internal static readonly string DefaultRels = @@ -122,5 +126,4 @@ internal static string DrawingXml(FileDto file, int fileIndex) internal static string Sheet(SheetDto sheetDto, int sheetId) => $""""""; - -} \ No newline at end of file +} diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs index 80802961..10e8b6a7 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs @@ -9,6 +9,10 @@ internal partial class OpenXmlWriter private const string DefaultCellStyleIndex = "0"; private const string HeaderCellStyleIndex = "1"; private const string RegularCellStyleIndex = "2"; + private const string DateCellStyleIndex = "3"; + private const string FillCellStyleIndex = "4"; + private const string TimeCellStyleIndex = "5"; + private static readonly DateTime ExcelZeroDate = new(1899, 12, 31); private readonly Dictionary _zipDictionary = []; @@ -157,40 +161,40 @@ private string GetPanes() return sb.ToString(); } - private (string StyleIndex, string? DataType, string? CellValue) GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnMapping? columnInfo, bool valueIsNull) + private (string StyleIndex, string? DataType, string? CellValue) GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnMapping? columnMapping, bool valueIsNull) { if (valueIsNull) return (RegularCellStyleIndex, "str", string.Empty); if (value is string str) { - var styleIndex = columnInfo?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : RegularCellStyleIndex; + var styleIndex = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : RegularCellStyleIndex; return (styleIndex, "str", XmlHelper.EncodeXml(str)); } - var type = GetValueType(value, columnInfo); + var type = GetValueType(value, columnMapping); - if (columnInfo is { ExcelFormat: not null, ExcelFormatId: -1 } && value is IFormattable formattableValue) + if (columnMapping is { ExcelFormat: not null, ExcelFormatId: -1 } && value is IFormattable formattableValue) { - var formattedStr = formattableValue.ToString(columnInfo.ExcelFormat, _configuration.Culture); + var formattedStr = formattableValue.ToString(columnMapping.ExcelFormat, _configuration.Culture); return (RegularCellStyleIndex, "str", XmlHelper.EncodeXml(formattedStr)); } if (type == typeof(DateTime)) - return GetDateTimeValue((DateTime)value, columnInfo); + return GetDateTimeValue((DateTime)value, columnMapping); if (type == typeof(DateTimeOffset)) - return GetDateTimeValue(((DateTimeOffset)value).DateTime, columnInfo); + return GetDateTimeValue(((DateTimeOffset)value).DateTime, columnMapping); if (type == typeof(TimeSpan)) - return GetTimeSpanValue((TimeSpan)value, columnInfo); + return GetTimeSpanValue((TimeSpan)value, columnMapping); #if NET8_0_OR_GREATER if (type == typeof(DateOnly)) - return GetDateTimeValue(((DateOnly)value).ToDateTime(default), columnInfo); + return GetDateTimeValue(((DateOnly)value).ToDateTime(default), columnMapping); if (type == typeof(TimeOnly)) - return GetTimeSpanValue(((TimeOnly)value).ToTimeSpan(), columnInfo); + return GetTimeSpanValue(((TimeOnly)value).ToTimeSpan(), columnMapping); #endif if (type.IsEnum) @@ -211,25 +215,25 @@ private string GetPanes() if (TypeHelper.IsNumericType(type)) { var cellValue = GetNumericValue(value, type); - if (columnInfo?.ExcelFormat is null) + if (columnMapping?.ExcelFormat is null) { var dataType = ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture) ? "n" : "str"; return (RegularCellStyleIndex, dataType, cellValue); } - return (columnInfo.ExcelFormatId.ToString(), null, cellValue); + return (columnMapping.ExcelFormatId.ToString(), null, cellValue); } if (type == typeof(bool)) - return (RegularCellStyleIndex, "b", (bool)value ? "1" : "0"); + return (RegularCellStyleIndex, ExcelXml.BooleanDataType, (bool)value ? "1" : "0"); if (type == typeof(byte[]) && _configuration.EnableConvertByteArray) { - if (!_configuration.EnableWriteFilePath) - return ("4", "str", ""); + if (!_configuration.EnableWriteFilePath) + return (FillCellStyleIndex, "str", ""); var base64 = GetFileValue(rowIndex, cellIndex, value); - return ("4", "str", XmlHelper.EncodeXml(base64)); + return (FillCellStyleIndex, "str", XmlHelper.EncodeXml(base64)); } return (RegularCellStyleIndex, "str", XmlHelper.EncodeXml(value.ToString())); @@ -319,12 +323,12 @@ private string GetFileValue(int rowIndex, int cellIndex, object value) if (!ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture)) { cellValue = value.ToString(_configuration.Culture); - return (RegularCellStyleIndex, (string?)"str", cellValue); + return (RegularCellStyleIndex, ExcelXml.StringDataType, cellValue); } var oaDate = CorrectDateTimeValue(value); cellValue = oaDate.ToString(CultureInfo.InvariantCulture); - var format = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : "3"; + var format = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : DateCellStyleIndex; return (format, null, cellValue); } @@ -350,8 +354,8 @@ private static double CorrectDateTimeValue(DateTime value) if (value.TotalDays >= 1) return GetDateTimeValue(ExcelZeroDate + value, columnMapping); - var cellValue = (value.TotalSeconds / 86400).ToString(CultureInfo.InvariantCulture); - var format = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : "5"; + var cellValue = value.TotalDays.ToString(CultureInfo.InvariantCulture); + var format = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : TimeCellStyleIndex; return (format, null, cellValue); } diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.cs index 02219182..3b80384c 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.cs @@ -67,7 +67,7 @@ public async Task SaveAsAsync(IProgress? progress = null, Cancellati await CreateZipEntryAsync(ExcelFileNames.SharedStrings, ExcelContentTypes.SharedStrings, ExcelXml.DefaultSharedString, cancellationToken).ConfigureAwait(false); await using var sbc = _sheetStyleBuildContext.ConfigureAwait(false); - var styleBuilder = await GetSheetStyleBuilderAsync(_sheetStyleBuildContext, cancellationToken).ConfigureAwait(false); + var styleBuilder = await GetSheetStyleBuilderAsync(cancellationToken).ConfigureAwait(false); var sheets = GetSheets(); var rowsWritten = new List(); @@ -93,81 +93,75 @@ public async Task InsertAsync(bool overwriteSheet = false, IProgress? if (!_configuration.FastMode) throw new InvalidOperationException("Insert requires fast mode to be enabled"); - try - { #if NET10_0_OR_GREATER - await using var disposableArchive = _archive.ConfigureAwait(false); + await using var disposableArchive = _archive.ConfigureAwait(false); #else - using var disposableArchive = _archive; + using var disposableArchive = _archive; #endif + await using var sbc = _sheetStyleBuildContext.ConfigureAwait(false); - using var reader = await OpenXmlReader.CreateAsync(_stream, _configuration, cancellationToken: cancellationToken).ConfigureAwait(false); - var rels = await reader.GetWorkbookRelsAsync(_archive.Entries, cancellationToken).ConfigureAwait(false) ?? []; + using var reader = await OpenXmlReader.CreateAsync(_stream, _configuration, cancellationToken: cancellationToken).ConfigureAwait(false); + var rels = await reader.GetWorkbookRelsAsync(_archive.Entries, cancellationToken).ConfigureAwait(false) ?? []; - _sheets.AddRange(rels - .OrderBy(sheet => sheet.Id) - .Select(sheet => new SheetDto - { - Name = sheet.Name, - SheetIdx = (int)sheet.Id, - State = sheet.State - }) - ); + _sheets.AddRange(rels + .OrderBy(sheet => sheet.Id) + .Select(sheet => new SheetDto + { + Name = sheet.Name, + SheetIdx = (int)sheet.Id, + State = sheet.State + }) + ); - var existSheetDto = _sheets.SingleOrDefault(s => s.Name == _defaultSheetName); - if (existSheetDto is not null && !overwriteSheet) - throw new Exception($"Sheet \"{_defaultSheetName}\" already exist"); + var existSheetDto = _sheets.SingleOrDefault(s => s.Name == _defaultSheetName); + if (existSheetDto is not null && !overwriteSheet) + throw new Exception($"Sheet \"{_defaultSheetName}\" already exist"); - // GenerateStylesXml must be invoked after validating the overwritesheet parameter to avoid unnecessary style changes. - var styleBuilder = await GetSheetStyleBuilderAsync(_sheetStyleBuildContext, cancellationToken).ConfigureAwait(false); + // GenerateStylesXml must be invoked after validating the overwritesheet parameter to avoid unnecessary style changes. + var styleBuilder = await GetSheetStyleBuilderAsync(cancellationToken).ConfigureAwait(false); - int rowsWritten; - if (existSheetDto is null) - { - _currentSheetIndex = (int)rels.Max(m => m.Id) + 1; - var insertSheetInfo = GetSheetInfos(_defaultSheetName); - var insertSheetDto = insertSheetInfo.ToDto(_currentSheetIndex); - _sheets.Add(insertSheetDto); - rowsWritten = await CreateSheetXmlAsync(_value, insertSheetDto.Path, progress, cancellationToken).ConfigureAwait(false); - } - else - { - _currentSheetIndex = existSheetDto.SheetIdx; - _archive.Entries.Single(s => s.FullName == existSheetDto.Path).Delete(); - rowsWritten = await CreateSheetXmlAsync(_value, existSheetDto.Path, progress, cancellationToken).ConfigureAwait(false); - } + int rowsWritten; + if (existSheetDto is null) + { + _currentSheetIndex = (int)rels.Max(m => m.Id) + 1; + var insertSheetInfo = GetSheetInfos(_defaultSheetName); + var insertSheetDto = insertSheetInfo.ToDto(_currentSheetIndex); + _sheets.Add(insertSheetDto); + rowsWritten = await CreateSheetXmlAsync(_value, insertSheetDto.Path, progress, cancellationToken).ConfigureAwait(false); + } + else + { + _currentSheetIndex = existSheetDto.SheetIdx; + _archive.Entries.Single(s => s.FullName == existSheetDto.Path).Delete(); + rowsWritten = await CreateSheetXmlAsync(_value, existSheetDto.Path, progress, cancellationToken).ConfigureAwait(false); + } - await styleBuilder.BuildAsync(cancellationToken).ConfigureAwait(false); - await AddFilesToZipAsync(cancellationToken).ConfigureAwait(false); + await styleBuilder.BuildAsync(cancellationToken).ConfigureAwait(false); + await AddFilesToZipAsync(cancellationToken).ConfigureAwait(false); - _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.DrawingRels(_currentSheetIndex - 1))?.Delete(); - await GenerateDrawinRelXmlAsync(_currentSheetIndex - 1, cancellationToken).ConfigureAwait(false); + _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.DrawingRels(_currentSheetIndex - 1))?.Delete(); + await GenerateDrawinRelXmlAsync(_currentSheetIndex - 1, cancellationToken).ConfigureAwait(false); - _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Drawing(_currentSheetIndex - 1))?.Delete(); - await GenerateDrawingXmlAsync(_currentSheetIndex - 1, cancellationToken).ConfigureAwait(false); + _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Drawing(_currentSheetIndex - 1))?.Delete(); + await GenerateDrawingXmlAsync(_currentSheetIndex - 1, cancellationToken).ConfigureAwait(false); - GenerateWorkBookXmls(out var workbookXml, out var workbookRelsXml, out var sheetsRelsXml); - foreach (var (key, value) in sheetsRelsXml) - { - var sheetRelsXmlPath = ExcelFileNames.SheetRels(key); - _archive.Entries.SingleOrDefault(s => s.FullName == sheetRelsXmlPath)?.Delete(); - await CreateZipEntryAsync(sheetRelsXmlPath, null, ExcelXml.DefaultSheetRelXml.Replace("{{format}}", value), cancellationToken).ConfigureAwait(false); - } + GenerateWorkBookXmls(out var workbookXml, out var workbookRelsXml, out var sheetsRelsXml); + foreach (var (key, value) in sheetsRelsXml) + { + var sheetRelsXmlPath = ExcelFileNames.SheetRels(key); + _archive.Entries.SingleOrDefault(s => s.FullName == sheetRelsXmlPath)?.Delete(); + await CreateZipEntryAsync(sheetRelsXmlPath, null, ExcelXml.DefaultSheetRelXml.Replace("{{format}}", value), cancellationToken).ConfigureAwait(false); + } - _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Workbook)?.Delete(); - await CreateZipEntryAsync(ExcelFileNames.Workbook, ExcelContentTypes.Workbook, ExcelXml.DefaultWorkbookXml.Replace("{{sheets}}", workbookXml.ToString()), cancellationToken).ConfigureAwait(false); + _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Workbook)?.Delete(); + await CreateZipEntryAsync(ExcelFileNames.Workbook, ExcelContentTypes.Workbook, ExcelXml.DefaultWorkbookXml.Replace("{{sheets}}", workbookXml.ToString()), cancellationToken).ConfigureAwait(false); - _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.WorkbookRels)?.Delete(); - await CreateZipEntryAsync(ExcelFileNames.WorkbookRels, null, ExcelXml.DefaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml.ToString()), cancellationToken).ConfigureAwait(false); + _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.WorkbookRels)?.Delete(); + await CreateZipEntryAsync(ExcelFileNames.WorkbookRels, null, ExcelXml.DefaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml.ToString()), cancellationToken).ConfigureAwait(false); - await InsertContentTypesXmlAsync(cancellationToken).ConfigureAwait(false); + await InsertContentTypesXmlAsync(cancellationToken).ConfigureAwait(false); - return rowsWritten; - } - finally - { - await _sheetStyleBuildContext.DisposeAsync().ConfigureAwait(false); - } + return rowsWritten; } [CreateSyncVersion] @@ -497,17 +491,17 @@ private async Task AddFilesToZipAsync(CancellationToken cancellationToken) } [CreateSyncVersion] - private async Task GetSheetStyleBuilderAsync(SheetStyleBuildContext context, CancellationToken cancellationToken = default) + private async Task GetSheetStyleBuilderAsync(CancellationToken cancellationToken = default) { SheetStyleBuilderBase builder = _configuration.TableStyles switch { - TableStyles.None => new MinimalSheetStyleBuilder(context), - TableStyles.Default => new DefaultSheetStyleBuilder(context, _configuration.StyleOptions), + TableStyles.None => new MinimalSheetStyleBuilder(_sheetStyleBuildContext), + TableStyles.Default => new DefaultSheetStyleBuilder(_sheetStyleBuildContext, _configuration.StyleOptions), _ => throw new InvalidEnumArgumentException(nameof(_configuration.TableStyles), (int)_configuration.TableStyles, typeof(TableStyles)) }; var newInfos = builder.GetGeneratedElementInfos(); - await context.CreateAsync(newInfos, cancellationToken).ConfigureAwait(false); + await _sheetStyleBuildContext.CreateAsync(newInfos, cancellationToken).ConfigureAwait(false); return builder; } diff --git a/src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs b/src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs index 45fbeab5..54b1f2ad 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs @@ -275,9 +275,9 @@ protected override async Task GenerateCellStyleXfAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, "0").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 0}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyFill", null, "1").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyBorder", null, "0").ConfigureAwait(false); @@ -317,8 +317,8 @@ protected override async Task GenerateCellStyleXfAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, "0").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyFill", null, "1").ConfigureAwait(false); @@ -414,8 +414,8 @@ protected override async Task GenerateCellXfAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, "0").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); @@ -451,8 +451,8 @@ protected override async Task GenerateCellXfAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, "14").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); @@ -484,8 +484,8 @@ protected override async Task GenerateCellXfAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, "0").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyBorder", null, "1").ConfigureAwait(false); @@ -503,8 +503,8 @@ protected override async Task GenerateCellXfAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, "21").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); @@ -540,8 +540,8 @@ protected override async Task GenerateCellXfAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, (numFmtIndex + i + _context.OldElementInfos.NumFmtCount).ToString()).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); diff --git a/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs b/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs index 7228be1d..4f657422 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs @@ -110,16 +110,13 @@ protected override async Task GenerateCellXfAsync() await NewWriter.WriteEndElementAsync().ConfigureAwait(false); const int numFmtIndex = 166; - var index = 0; - for (var i = 0; i < _context.CustomFormatCount; i++) + for (var i = 1; i <= _context.CustomFormatCount; i++) { - index++; - /* * Date: Sat, 16 May 2026 00:25:53 +0200 Subject: [PATCH 03/10] Corrected implementation of string content serialization Modifies the cell witing process of string content replacing the tag with tags and changes the cell data type from "str" to "inlineStr" in order to adhere to the OpenXml specification for inlined strings --- src/MiniExcel.OpenXml/Constants/ExcelXml.cs | 1 + .../Constants/WorksheetXml.cs | 6 +++++- .../OpenXmlWriter.DefaultOpenXml.cs | 19 +++++++++++-------- src/MiniExcel.OpenXml/OpenXmlWriter.cs | 2 +- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs index 5efcf968..8efef7f7 100644 --- a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs +++ b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs @@ -11,6 +11,7 @@ static ExcelXml() DefaultDrawing = XmlHelper.MinifyXml(DefaultDrawing); } + internal const string InlineStringDataType = "inlineStr"; internal const string StringDataType = "str"; internal const string NumericDataType = "n"; internal const string BooleanDataType = "b"; diff --git a/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs b/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs index dd69f623..a28d5fce 100644 --- a/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs +++ b/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs @@ -55,7 +55,11 @@ internal static string Column(int colIndex, double columnWidth, bool hidden = fa //t check avoid format error ![image](https://user-images.githubusercontent.com/12729184/118770190-9eee3480-b8b3-11eb-9f5a-87a439f5e320.png) internal static string Cell(string cellReference, string? cellType, string styleIndex, string? cellValue, bool preserveSpace = false, ColumnType columnType = ColumnType.Value) - => $"{cellValue}"; + { + return cellType == ExcelXml.InlineStringDataType + ? $"""{cellValue}""" + : $"""{cellValue}"""; + } internal static string Autofilter(string dimensionRef) => $""; internal static string Drawing(int sheetIndex) => $""; diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs index 10e8b6a7..dd71d89c 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs @@ -1,5 +1,6 @@ using MiniExcelLib.OpenXml.Constants; using System.ComponentModel; +using MiniExcelLib.Core.Attributes; using static MiniExcelLib.Core.Helpers.ImageHelper; namespace MiniExcelLib.OpenXml; @@ -164,12 +165,12 @@ private string GetPanes() private (string StyleIndex, string? DataType, string? CellValue) GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnMapping? columnMapping, bool valueIsNull) { if (valueIsNull) - return (RegularCellStyleIndex, "str", string.Empty); + return (RegularCellStyleIndex, GetStringType(), string.Empty); if (value is string str) { var styleIndex = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : RegularCellStyleIndex; - return (styleIndex, "str", XmlHelper.EncodeXml(str)); + return (styleIndex, GetStringType(), XmlHelper.EncodeXml(str)); } var type = GetValueType(value, columnMapping); @@ -177,7 +178,7 @@ private string GetPanes() if (columnMapping is { ExcelFormat: not null, ExcelFormatId: -1 } && value is IFormattable formattableValue) { var formattedStr = formattableValue.ToString(columnMapping.ExcelFormat, _configuration.Culture); - return (RegularCellStyleIndex, "str", XmlHelper.EncodeXml(formattedStr)); + return (RegularCellStyleIndex, GetStringType(), XmlHelper.EncodeXml(formattedStr)); } if (type == typeof(DateTime)) @@ -209,7 +210,7 @@ private string GetPanes() } description ??= value.ToString(); - return (RegularCellStyleIndex, "str", description); + return (RegularCellStyleIndex, GetStringType(), description); } if (TypeHelper.IsNumericType(type)) @@ -217,7 +218,7 @@ private string GetPanes() var cellValue = GetNumericValue(value, type); if (columnMapping?.ExcelFormat is null) { - var dataType = ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture) ? "n" : "str"; + var dataType = ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture) ? ExcelXml.NumericDataType : GetStringType(); return (RegularCellStyleIndex, dataType, cellValue); } @@ -230,13 +231,15 @@ private string GetPanes() if (type == typeof(byte[]) && _configuration.EnableConvertByteArray) { if (!_configuration.EnableWriteFilePath) - return (FillCellStyleIndex, "str", ""); + return (FillCellStyleIndex, GetStringType(), ""); var base64 = GetFileValue(rowIndex, cellIndex, value); - return (FillCellStyleIndex, "str", XmlHelper.EncodeXml(base64)); + return (FillCellStyleIndex, GetStringType(), XmlHelper.EncodeXml(base64)); } - return (RegularCellStyleIndex, "str", XmlHelper.EncodeXml(value.ToString())); + return (RegularCellStyleIndex, GetStringType(), XmlHelper.EncodeXml(value.ToString())); + + string GetStringType() => columnMapping?.ExcelColumnType == ColumnType.Value ? ExcelXml.InlineStringDataType : ExcelXml.StringDataType; } private static Type GetValueType(object value, MiniExcelColumnMapping? columnInfo) diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.cs index 3b80384c..88a7f185 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.cs @@ -428,7 +428,7 @@ private static async Task PrintHeaderAsync(MiniExcelStreamWriter writer, List Date: Sat, 16 May 2026 00:55:28 +0200 Subject: [PATCH 04/10] Removed superfluous utility class `ZipPackageInfo` --- src/MiniExcel.OpenXml/GlobalUsings.cs | 1 - .../OpenXmlWriter.DefaultOpenXml.cs | 6 +++--- src/MiniExcel.OpenXml/OpenXmlWriter.cs | 14 +++++++------- .../Styles/Builder/SheetStyleBuildContext.cs | 8 ++++---- src/MiniExcel.OpenXml/{Zip => Utils}/OpenXmlZip.cs | 2 +- src/MiniExcel.OpenXml/Zip/ZipPackageInfo.cs | 7 ------- 6 files changed, 15 insertions(+), 23 deletions(-) rename src/MiniExcel.OpenXml/{Zip => Utils}/OpenXmlZip.cs (95%) delete mode 100644 src/MiniExcel.OpenXml/Zip/ZipPackageInfo.cs diff --git a/src/MiniExcel.OpenXml/GlobalUsings.cs b/src/MiniExcel.OpenXml/GlobalUsings.cs index 05f2a69b..c8bc554b 100644 --- a/src/MiniExcel.OpenXml/GlobalUsings.cs +++ b/src/MiniExcel.OpenXml/GlobalUsings.cs @@ -15,5 +15,4 @@ global using MiniExcelLib.OpenXml.Helpers; global using MiniExcelLib.OpenXml.Models; global using MiniExcelLib.OpenXml.Utils; -global using MiniExcelLib.OpenXml.Zip; global using Zomp.SyncMethodGenerator; diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs index dd71d89c..e7641fba 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs @@ -16,7 +16,7 @@ internal partial class OpenXmlWriter private static readonly DateTime ExcelZeroDate = new(1899, 12, 31); - private readonly Dictionary _zipDictionary = []; + private readonly Dictionary _zipContentsMap = []; private IEnumerable<(SheetDto Sheet, object? Data)> GetSheets() { @@ -426,9 +426,9 @@ private void GenerateWorkBookXmls( private string GetContentTypesXml() { var sb = new StringBuilder(ExcelXml.StartTypes); - foreach (var p in _zipDictionary) + foreach (var p in _zipContentsMap) { - sb.Append(ExcelXml.ContentType(p.Value.ContentType, p.Key)); + sb.Append(ExcelXml.ContentType(p.Value, p.Key)); } sb.Append(ExcelXml.EndTypes); diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.cs index 88a7f185..9a814ad1 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.cs @@ -35,7 +35,7 @@ private OpenXmlWriter(Stream stream, ZipArchive archive, object? value, string? _printHeader = printHeader; _defaultSheetName = sheetName; - _sheetStyleBuildContext = new SheetStyleBuildContext(_zipDictionary, _archive, Utf8WithBom); + _sheetStyleBuildContext = new SheetStyleBuildContext(_zipContentsMap, _archive, Utf8WithBom); } [CreateSyncVersion] @@ -189,7 +189,7 @@ private async Task CreateSheetXmlAsync(object? values, string sheetPath, IP rowsWritten = await WriteValuesAsync(writer, values, cancellationToken, progress).ConfigureAwait(false); } - _zipDictionary.Add(sheetPath, new ZipPackageInfo(entry, ExcelContentTypes.Worksheet)); + _zipContentsMap.Add(sheetPath, ExcelContentTypes.Worksheet); return rowsWritten; } @@ -616,14 +616,14 @@ private async Task InsertContentTypesXmlAsync(CancellationToken cancellationToke partNames.Add(partName); } - foreach (var p in _zipDictionary) + foreach (var (entry, contentType) in _zipContentsMap) { cancellationToken.ThrowIfCancellationRequested(); - var partName = $"/{p.Key}"; - if (!partNames.Contains(partName)) + var entryPath = $"/{entry}"; + if (!partNames.Contains(entryPath)) { - var newElement = new XElement(ns + "Override", new XAttribute("ContentType", p.Value.ContentType), new XAttribute("PartName", partName)); + var newElement = new XElement(ns + "Override", new XAttribute("ContentType", contentType), new XAttribute("PartName", entryPath)); typesElement.Add(newElement); } } @@ -656,7 +656,7 @@ private async Task CreateZipEntryAsync(string path, string? contentType, string await writer.WriteAsync(content, cancellationToken).ConfigureAwait(false); if (contentType is not (null or "")) - _zipDictionary.Add(path, new ZipPackageInfo(entry, contentType)); + _zipContentsMap.Add(path, contentType); } [CreateSyncVersion] diff --git a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildContext.cs b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildContext.cs index 4f0b5e7a..dea1c482 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildContext.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildContext.cs @@ -2,7 +2,7 @@ namespace MiniExcelLib.OpenXml.Styles.Builder; -internal sealed partial class SheetStyleBuildContext(Dictionary zipDictionary, ZipArchive archive, Encoding encoding) : IDisposable, IAsyncDisposable +internal sealed partial class SheetStyleBuildContext(Dictionary contentTypes, ZipArchive archive, Encoding encoding) : IDisposable, IAsyncDisposable { private const string EmptyStylesXml = """ @@ -10,7 +10,7 @@ internal sealed partial class SheetStyleBuildContext(Dictionary """; - private readonly Dictionary _zipDictionary = zipDictionary; + private readonly Dictionary _contentTypes = contentTypes; private readonly ZipArchive _archive = archive; private readonly Encoding _encoding = encoding; @@ -176,7 +176,7 @@ public async Task FinalizeAndUpdateZipDictionaryAsync(CancellationToken cancella if (_oldStyleXmlZipEntry is null) { - _zipDictionary.Add(ExcelFileNames.Styles, new ZipPackageInfo(_newStyleXmlZipEntry!, ExcelContentTypes.Styles)); + _contentTypes.Add(ExcelFileNames.Styles, ExcelContentTypes.Styles); } else { @@ -197,7 +197,7 @@ public async Task FinalizeAndUpdateZipDictionaryAsync(CancellationToken cancella await tempStream.CopyToAsync(newStream, 4096, cancellationToken).ConfigureAwait(false); } - _zipDictionary[ExcelFileNames.Styles] = new ZipPackageInfo(finalStyleXmlZipEntry, ExcelContentTypes.Styles); + _contentTypes[ExcelFileNames.Styles] = ExcelContentTypes.Styles; _newStyleXmlZipEntry?.Delete(); _newStyleXmlZipEntry = null; } diff --git a/src/MiniExcel.OpenXml/Zip/OpenXmlZip.cs b/src/MiniExcel.OpenXml/Utils/OpenXmlZip.cs similarity index 95% rename from src/MiniExcel.OpenXml/Zip/OpenXmlZip.cs rename to src/MiniExcel.OpenXml/Utils/OpenXmlZip.cs index 1c13805a..28387d13 100644 --- a/src/MiniExcel.OpenXml/Zip/OpenXmlZip.cs +++ b/src/MiniExcel.OpenXml/Utils/OpenXmlZip.cs @@ -1,6 +1,6 @@ using System.Collections.ObjectModel; -namespace MiniExcelLib.OpenXml.Zip; +namespace MiniExcelLib.OpenXml.Utils; /// Copied & modified from ExcelDataReader ZipWorker @MIT License internal sealed partial class OpenXmlZip : IDisposable, IAsyncDisposable diff --git a/src/MiniExcel.OpenXml/Zip/ZipPackageInfo.cs b/src/MiniExcel.OpenXml/Zip/ZipPackageInfo.cs deleted file mode 100644 index f8ca652a..00000000 --- a/src/MiniExcel.OpenXml/Zip/ZipPackageInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MiniExcelLib.OpenXml.Zip; - -internal class ZipPackageInfo(ZipArchiveEntry zipArchiveEntry, string contentType) -{ - public ZipArchiveEntry ZipArchiveEntry { get; set; } = zipArchiveEntry; - public string ContentType { get; set; } = contentType; -} \ No newline at end of file From b12aac0887c11a81cea1292fe9acaa91ee4cf636 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 16 May 2026 13:16:06 +0200 Subject: [PATCH 05/10] Added invalid sheet name characters check in methods for exporting, inserting and altering sheets --- src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs | 8 +++---- src/MiniExcel.OpenXml/Helpers/ThrowHelper.cs | 6 +++++- .../OpenXmlWriter.DefaultOpenXml.cs | 4 ++-- src/MiniExcel.OpenXml/OpenXmlWriter.cs | 21 +++++++++---------- .../MiniExcelOpenXmlAsyncTests.cs | 18 ++++++++++++++++ .../MiniExcelOpenXmlTests.cs | 18 ++++++++++++++++ 6 files changed, 57 insertions(+), 18 deletions(-) diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs b/src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs index 8f8378b1..dcd4911f 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs @@ -5,7 +5,7 @@ public sealed partial class OpenXmlExporter internal OpenXmlExporter() { } [CreateSyncVersion] - public async Task InsertSheetAsync(string path, object value, string? sheetName = "Sheet1", + public async Task InsertSheetAsync(string path, object value, string sheetName = "Sheet1", bool printHeader = true, bool overwriteSheet = false, OpenXmlConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) { @@ -28,7 +28,7 @@ public async Task InsertSheetAsync(string path, object value, string? sheet } [CreateSyncVersion] - public async Task InsertSheetAsync(Stream stream, object value, string? sheetName = "Sheet1", + public async Task InsertSheetAsync(Stream stream, object value, string sheetName = "Sheet1", bool printHeader = true, bool overwriteSheet = false, OpenXmlConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) { @@ -44,7 +44,7 @@ public async Task InsertSheetAsync(Stream stream, object value, string? she [CreateSyncVersion] public async Task ExportAsync(string path, object value, bool printHeader = true, - string? sheetName = "Sheet1", bool overwriteFile = false, OpenXmlConfiguration? configuration = null, + string sheetName = "Sheet1", bool overwriteFile = false, OpenXmlConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) { if (Path.GetExtension(path).Equals(".xlsm", StringComparison.InvariantCultureIgnoreCase)) @@ -60,7 +60,7 @@ public async Task ExportAsync(string path, object value, bool printHeader } [CreateSyncVersion] - public async Task ExportAsync(Stream stream, object value, bool printHeader = true, string? sheetName = "Sheet1", + public async Task ExportAsync(Stream stream, object value, bool printHeader = true, string sheetName = "Sheet1", OpenXmlConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) { var writer = await OpenXmlWriter diff --git a/src/MiniExcel.OpenXml/Helpers/ThrowHelper.cs b/src/MiniExcel.OpenXml/Helpers/ThrowHelper.cs index 656e1c70..fb5e5570 100644 --- a/src/MiniExcel.OpenXml/Helpers/ThrowHelper.cs +++ b/src/MiniExcel.OpenXml/Helpers/ThrowHelper.cs @@ -3,6 +3,7 @@ internal static class ThrowHelper { private static readonly byte[] ZipArchiveHeader = [0x50, 0x4B]; + private static readonly char[] InvalidSheetNameCharacters = ['\\', '/', '?', '*', '[', ']']; private const int ExcelMaxSheetNameLength = 31; internal static void ThrowIfInvalidOpenXml(Stream stream) @@ -23,9 +24,12 @@ internal static void ThrowIfInvalidOpenXml(Stream stream) internal static void ThrowIfInvalidSheetName(string? sheetName) { if (string.IsNullOrEmpty(sheetName)) - throw new ArgumentException("Sheet names cannot be empty or null"); + throw new ArgumentException("Sheet names cannot be empty"); if (sheetName.Length > ExcelMaxSheetNameLength) throw new ArgumentException("Sheet names must be less than 31 characters"); + + if (sheetName.Intersect(InvalidSheetNameCharacters).Any()) + throw new ArgumentException($"Sheet names cannot contain any of the following charachters: {string.Join(", ", InvalidSheetNameCharacters)}"); } } diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs index e7641fba..46d2a185 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs @@ -48,8 +48,8 @@ internal partial class OpenXmlWriter } sheetId++; - var defaultSheetInfo = GetSheetInfos(_defaultSheetName); - yield return (defaultSheetInfo.ToDto(sheetId), _value); + var sheetInfo = GetSheetInfos(_sheetName); + yield return (sheetInfo.ToDto(sheetId), _value); } private ExcelSheetInfo GetSheetInfos(string sheetName) diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.cs index 9a814ad1..cb91c0ba 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.cs @@ -18,13 +18,14 @@ internal partial class OpenXmlWriter : IMiniExcelWriter private readonly List _files = []; private readonly SheetStyleBuildContext _sheetStyleBuildContext; - private readonly string? _defaultSheetName; + private readonly string _sheetName; private readonly bool _printHeader; + private readonly bool _useSharedStrings; private readonly object? _value; private int _currentSheetIndex = 0; - private OpenXmlWriter(Stream stream, ZipArchive archive, object? value, string? sheetName, OpenXmlConfiguration configuration, bool printHeader) + private OpenXmlWriter(Stream stream, ZipArchive archive, object? value, string sheetName, OpenXmlConfiguration configuration, bool printHeader) { _stream = stream; @@ -33,13 +34,13 @@ private OpenXmlWriter(Stream stream, ZipArchive archive, object? value, string? _value = value; _printHeader = printHeader; - _defaultSheetName = sheetName; + _sheetName = sheetName; _sheetStyleBuildContext = new SheetStyleBuildContext(_zipContentsMap, _archive, Utf8WithBom); } [CreateSyncVersion] - internal static async ValueTask CreateAsync(Stream stream, object? value, string? sheetName, bool printHeader, IMiniExcelConfiguration? configuration, CancellationToken cancellationToken = default) + internal static async ValueTask CreateAsync(Stream stream, object? value, string sheetName, bool printHeader, IMiniExcelConfiguration? configuration, CancellationToken cancellationToken = default) { ThrowHelper.ThrowIfInvalidSheetName(sheetName); @@ -113,9 +114,9 @@ public async Task InsertAsync(bool overwriteSheet = false, IProgress? }) ); - var existSheetDto = _sheets.SingleOrDefault(s => s.Name == _defaultSheetName); + var existSheetDto = _sheets.SingleOrDefault(s => s.Name == _sheetName); if (existSheetDto is not null && !overwriteSheet) - throw new Exception($"Sheet \"{_defaultSheetName}\" already exist"); + throw new Exception($"Sheet \"{_sheetName}\" already exist"); // GenerateStylesXml must be invoked after validating the overwritesheet parameter to avoid unnecessary style changes. var styleBuilder = await GetSheetStyleBuilderAsync(cancellationToken).ConfigureAwait(false); @@ -124,7 +125,7 @@ public async Task InsertAsync(bool overwriteSheet = false, IProgress? if (existSheetDto is null) { _currentSheetIndex = (int)rels.Max(m => m.Id) + 1; - var insertSheetInfo = GetSheetInfos(_defaultSheetName); + var insertSheetInfo = GetSheetInfos(_sheetName); var insertSheetDto = insertSheetInfo.ToDto(_currentSheetIndex); _sheets.Add(insertSheetDto); rowsWritten = await CreateSheetXmlAsync(_value, insertSheetDto.Path, progress, cancellationToken).ConfigureAwait(false); @@ -735,11 +736,9 @@ async Task LoadWorkbook() 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 is not null) { - if (newSheetName.Length > 31) - throw new ArgumentException($"The name \"{newSheetName}\" is too long, the maximum allowed length is 31 characters."); - + ThrowHelper.ThrowIfInvalidSheetName(newSheetName); sheet.SetAttributeValue("name", newSheetName); } diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs index 9e6da8a0..cdf8b212 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs @@ -1930,4 +1930,22 @@ private class DateTimeFormattingTestDto( [MiniExcelFormat("mmmm yyyy")] public DateTime MonthYear { get; set; } = monthYear; } + + [Fact] + public async Task InvalidSheetNameCharactersShouldThrow() + { + await using var ms1 = new MemoryStream(); + await Assert.ThrowsAsync(() => _excelExporter.ExportAsync(ms1, Array.Empty(), sheetName: "Sheet?")); + + await using var ms2 = new MemoryStream(); + await Assert.ThrowsAsync(() => _excelExporter.InsertSheetAsync(ms2, Array.Empty(), sheetName: "Sheet[]")); + + await using var ms3 = new MemoryStream(); + using var package = new ExcelPackage(ms3); + package.Workbook.Worksheets.Add("Sheet1"); + await package.SaveAsync(); + + ms1.Seek(0, SeekOrigin.Begin); + await Assert.ThrowsAsync(() => _excelExporter.AlterSheetAsync(ms3, "Sheet1", "Sheet*")); + } } diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs index 44bbcccc..fedb55ef 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs @@ -1713,4 +1713,22 @@ public void ExportAndQueryFieldsWithoutAttributeTest() Assert.Contains("Mapped", rows[0].Keys); Assert.DoesNotContain("NotMappedField", rows[0].Keys); } + + [Fact] + public async Task InvalidSheetNameCharactersShouldThrow() + { + await using var ms1 = new MemoryStream(); + Assert.Throws(() => _excelExporter.Export(ms1, Array.Empty(), sheetName: "Sheet?")); + + await using var ms2 = new MemoryStream(); + Assert.Throws(() => _excelExporter.InsertSheet(ms2, Array.Empty(), sheetName: "Sheet[]")); + + await using var ms3 = new MemoryStream(); + using var package = new ExcelPackage(ms3); + package.Workbook.Worksheets.Add("Sheet1"); + package.Save(); + + ms1.Seek(0, SeekOrigin.Begin); + Assert.Throws(() => _excelExporter.AlterSheet(ms3, "Sheet1", "Sheet*")); + } } From ed61604f365f59d013ceb8f8edca90e6363e2088 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 16 May 2026 13:45:18 +0200 Subject: [PATCH 06/10] Removed conditional compilation for async disposal of `MiniExcelStreamWriter` --- .../Helpers/MiniExcelStreamWriter.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/MiniExcel.Core/Helpers/MiniExcelStreamWriter.cs b/src/MiniExcel.Core/Helpers/MiniExcelStreamWriter.cs index 1eb002a6..03ce8479 100644 --- a/src/MiniExcel.Core/Helpers/MiniExcelStreamWriter.cs +++ b/src/MiniExcel.Core/Helpers/MiniExcelStreamWriter.cs @@ -1,9 +1,6 @@ namespace MiniExcelLib.Core.Helpers; -public sealed partial class MiniExcelStreamWriter(Stream stream, Encoding encoding, int bufferSize) : IDisposable -#if NET8_0_OR_GREATER - , IAsyncDisposable -#endif +public sealed partial class MiniExcelStreamWriter(Stream stream, Encoding encoding, int bufferSize) : IDisposable, IAsyncDisposable { // if leaveOpen is set to false, the StreamWriter closes the underlying stream synchronously in a finally block. // Since we want to avoid all synchronous operations when dealing with streams we leave it open here, as it will disposed from the caller anyways @@ -57,14 +54,20 @@ public void Dispose() } } -#if NET8_0_OR_GREATER public async ValueTask DisposeAsync() { if (!_disposed) { - await _streamWriter.DisposeAsync().ConfigureAwait(false); + await CastAndDispose(_streamWriter).ConfigureAwait(false); _disposed = true; } + + static async ValueTask CastAndDispose(IDisposable? resource) + { + if (resource is IAsyncDisposable asyncDisposable) + await asyncDisposable.DisposeAsync().ConfigureAwait(false); + else + resource?.Dispose(); + } } -#endif } From 43ceb999cbd3deca6209c0290f7a8abee3d35715 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 16 May 2026 15:49:20 +0200 Subject: [PATCH 07/10] Minor OpenXmlReader cleanup --- .../Constants/ExcelFileNames.cs | 4 ++- src/MiniExcel.OpenXml/OpenXmlReader.cs | 27 +++++++------------ 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/MiniExcel.OpenXml/Constants/ExcelFileNames.cs b/src/MiniExcel.OpenXml/Constants/ExcelFileNames.cs index 1ea810f6..4de4779f 100644 --- a/src/MiniExcel.OpenXml/Constants/ExcelFileNames.cs +++ b/src/MiniExcel.OpenXml/Constants/ExcelFileNames.cs @@ -6,11 +6,13 @@ internal static class ExcelFileNames internal const string SharedStrings = "xl/sharedStrings.xml"; internal const string ContentTypes = "[Content_Types].xml"; + internal const string Person = "xl/persons/person.xml"; internal const string Styles = "xl/styles.xml"; internal const string Workbook = "xl/workbook.xml"; internal const string WorkbookRels = "xl/_rels/workbook.xml.rels"; + internal const string Worksheet = "xl/worksheets/sheet"; internal static string SheetRels(int sheetId) => $"xl/worksheets/_rels/sheet{sheetId}.xml.rels"; internal static string Drawing(int sheetIndex) => $"xl/drawings/drawing{sheetIndex + 1}.xml"; internal static string DrawingRels(int sheetIndex) => $"xl/drawings/_rels/drawing{sheetIndex + 1}.xml.rels"; -} \ No newline at end of file +} diff --git a/src/MiniExcel.OpenXml/OpenXmlReader.cs b/src/MiniExcel.OpenXml/OpenXmlReader.cs index c623398d..683d251d 100644 --- a/src/MiniExcel.OpenXml/OpenXmlReader.cs +++ b/src/MiniExcel.OpenXml/OpenXmlReader.cs @@ -344,8 +344,7 @@ private ZipArchiveEntry GetSheetEntry(string? sheetName) { // if sheets count > 1 need to read xl/_rels/workbook.xml.rels var sheets = Archive.EntryCollection - .Where(w => w.FullName.StartsWith("xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase) || - w.FullName.StartsWith("/xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase)) + .Where(w => w.FullName.TrimStart('/').StartsWith(ExcelFileNames.Worksheet, StringComparison.OrdinalIgnoreCase)) .ToArray(); ZipArchiveEntry sheetEntry; @@ -358,24 +357,18 @@ private ZipArchiveEntry GetSheetEntry(string? sheetName) if (_config.DynamicSheets is null) throw new InvalidOperationException("Please check that parameters sheetName/Index are correct"); - var sheetConfig = _config.DynamicSheets.FirstOrDefault(ds => ds.Key == sheetName); - if (sheetConfig is not null) + if (_config.DynamicSheets.FirstOrDefault(ds => ds.Key == sheetName) is { } sheetConfig) { sheetRecord = _sheetRecords.SingleOrDefault(s => s.Name == sheetConfig.Name); } } - sheetEntry = sheets.Single(w => w.FullName == $"xl/{sheetRecord.Path}" || - w.FullName == $"/xl/{sheetRecord.Path}" || - w.FullName == sheetRecord.Path || - $"/{w.FullName}" == sheetRecord.Path); + sheetEntry = sheets.Single(w => w.FullName.TrimStart('/') == $"xl/{sheetRecord?.Path}" || w.FullName == sheetRecord?.Path?.TrimStart('/')); } else if (sheets.Length > 1) { SetWorkbookRels(Archive.EntryCollection); var s = _sheetRecords[0]; - sheetEntry = sheets.Single(w => w.FullName == $"xl/{s.Path}" || - w.FullName == $"/xl/{s.Path}" || - w.FullName.TrimStart('/') == s.Path?.TrimStart('/')); + sheetEntry = sheets.Single(w => w.FullName.TrimStart('/') == $"xl/{s.Path}" || w.FullName.TrimStart('/') == s.Path?.TrimStart('/')); } else { @@ -421,8 +414,7 @@ private async Task SetSharedStringsAsync(CancellationToken cancellationToken = d if (SharedStrings is { Count: > 0 }) return; - var sharedStringsEntry = Archive.GetEntry("xl/sharedStrings.xml"); - if (sharedStringsEntry is null) + if (Archive.GetEntry(ExcelFileNames.SharedStrings) is not { } sharedStringsEntry) return; var idx = 0; @@ -466,7 +458,7 @@ private static async IAsyncEnumerable ReadWorkbookAsync(ReadOnlyCol #endif ); - var entry = entries.Single(w => w.FullName == "xl/workbook.xml"); + var entry = entries.Single(w => w.FullName == ExcelFileNames.Workbook); #if NET8_0_OR_GREATER var stream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableStream = stream.ConfigureAwait(false); @@ -562,7 +554,7 @@ await reader.SkipAsync() .CreateListAsync(cancellationToken) .ConfigureAwait(false); - var entry = entries.Single(w => w.FullName == "xl/_rels/workbook.xml.rels"); + var entry = entries.Single(w => w.FullName == ExcelFileNames.WorkbookRels); #if NET8_0_OR_GREATER var stream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); @@ -755,8 +747,7 @@ internal async Task> GetDimensionsAsync(CancellationToken canc var ranges = new List(); var sheets = Archive.EntryCollection.Where(e => - e.FullName.StartsWith("xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase) || - e.FullName.StartsWith("/xl/worksheets/sheet", StringComparison.OrdinalIgnoreCase)); + e.FullName.TrimStart('/').StartsWith(ExcelFileNames.Worksheet, StringComparison.OrdinalIgnoreCase)); foreach (var sheet in sheets) { @@ -1131,7 +1122,7 @@ internal async Task ReadCommentsAsync(string? sheetName, Cance throw new InvalidDataException($"There is no sheet named {sheetName}"); List people = []; - if (Archive.GetEntry("xl/persons/person.xml") is { } persons) + if (Archive.GetEntry(ExcelFileNames.Person) is { } persons) { #if NET8_0_OR_GREATER var personStream = await persons.OpenAsync(cancellationToken).ConfigureAwait(false); From 6a53a3f754b7093e90e631b8a322dd8fcf26c221 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 16 May 2026 18:48:42 +0200 Subject: [PATCH 08/10] Added documentation to the OpenXmlExporter and adjusted namespaces in solution --- .../BenchmarkSections/CreateExcelBenchmark.cs | 2 +- .../BenchmarkSections/QueryExcelBenchmark.cs | 2 +- .../TemplateExcelBenchmark.cs | 2 +- .../BenchmarkSections/XlsxAsyncBenchmark.cs | 2 +- src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs | 62 ++++++++++++++++- src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs | 5 +- src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs | 3 +- .../Api/ProviderExtensions.cs | 3 +- src/MiniExcel/MiniExcel.cs | 69 +++++++++---------- src/MiniExcel/MiniExcelConverter.cs | 2 +- tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs | 4 +- tests/MiniExcel.Csv.Tests/IssueTests.cs | 2 +- .../MiniExcel.OpenXml.Tests.csproj | 1 - 13 files changed, 105 insertions(+), 54 deletions(-) diff --git a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/CreateExcelBenchmark.cs b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/CreateExcelBenchmark.cs index 733eddb0..e80b77a7 100644 --- a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/CreateExcelBenchmark.cs +++ b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/CreateExcelBenchmark.cs @@ -6,7 +6,7 @@ using DocumentFormat.OpenXml.Spreadsheet; using MiniExcelLib.Benchmarks.Utils; using MiniExcelLib.Core; -using MiniExcelLib.OpenXml.Api; +using MiniExcelLib.OpenXml; using MiniExcelLib.OpenXml.FluentMapping; using MiniExcelLib.OpenXml.FluentMapping.Api; using NPOI.XSSF.UserModel; diff --git a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/QueryExcelBenchmark.cs b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/QueryExcelBenchmark.cs index 83218068..4d06bd45 100644 --- a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/QueryExcelBenchmark.cs +++ b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/QueryExcelBenchmark.cs @@ -5,7 +5,7 @@ using DocumentFormat.OpenXml.Spreadsheet; using ExcelDataReader; using MiniExcelLib.Core; -using MiniExcelLib.OpenXml.Api; +using MiniExcelLib.OpenXml; using MiniExcelLib.OpenXml.FluentMapping; using MiniExcelLib.OpenXml.FluentMapping.Api; using NPOI.XSSF.UserModel; diff --git a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/TemplateExcelBenchmark.cs b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/TemplateExcelBenchmark.cs index 20cc7d92..8a37a075 100644 --- a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/TemplateExcelBenchmark.cs +++ b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/TemplateExcelBenchmark.cs @@ -2,7 +2,7 @@ using ClosedXML.Report; using MiniExcelLib.Benchmarks.Utils; using MiniExcelLib.Core; -using MiniExcelLib.OpenXml.Api; +using MiniExcelLib.OpenXml; using MiniExcelLib.OpenXml.FluentMapping; using MiniExcelLib.OpenXml.FluentMapping.Api; diff --git a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/XlsxAsyncBenchmark.cs b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/XlsxAsyncBenchmark.cs index 1852a375..ea90613b 100644 --- a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/XlsxAsyncBenchmark.cs +++ b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/XlsxAsyncBenchmark.cs @@ -1,7 +1,7 @@ using BenchmarkDotNet.Attributes; using MiniExcelLib.Benchmarks.Utils; using MiniExcelLib.Core; -using MiniExcelLib.OpenXml.Api; +using MiniExcelLib.OpenXml; namespace MiniExcelLib.Benchmarks.BenchmarkSections; diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs b/src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs index dcd4911f..5042d6d1 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlExporter.cs @@ -1,9 +1,25 @@ -namespace MiniExcelLib.OpenXml.Api; +// ReSharper disable once CheckNamespace +namespace MiniExcelLib.OpenXml; public sealed partial class OpenXmlExporter { internal OpenXmlExporter() { } + /// + /// Inserts a new worksheet into an existing OpenXml document. + /// + /// The path to the OpenXml document to modify. + /// The data object to insert into the new sheet. This can be an enumerable collection of a reference type, a or a . + /// The name to assign to the new worksheet. + /// If true, includes the header row in the new sheet; otherwise, only data rows are written. + /// If true, overwrites any existing sheet with the same name; otherwise, an exception will be raised if the sheet already exists. + /// Optional configuration settings for the insert operation. + /// Optional progress reporter to track insertion progress. The report value represents the number of cells written. + /// A cancellation token to monitor for cancellation requests. + /// The number of rows written to the new sheet. + /// + /// FastMode is automatically enabled for this process and disabling it will result in . + /// [CreateSyncVersion] public async Task InsertSheetAsync(string path, object value, string sheetName = "Sheet1", bool printHeader = true, bool overwriteSheet = false, OpenXmlConfiguration? configuration = null, @@ -27,6 +43,22 @@ public async Task InsertSheetAsync(string path, object value, string sheetN return await InsertSheetAsync(stream, value, sheetName, printHeader, overwriteSheet, configuration, progress, cancellationToken).ConfigureAwait(false); } + /// + /// Inserts a new worksheet into an existing OpenXml document. + /// + /// The stream containing the OpenXml document to modify. + /// The data object to insert into the new sheet. This can be an enumerable collection of a reference type, a IEnumeable<IDictionary<string, object>>, a or a . + /// The name to assign to the new worksheet. + /// If true, includes the header row in the new sheet; otherwise, only data rows are written. + /// If true, overwrites any existing sheet with the same name; otherwise, an exception will be raised if the sheet already exists. + /// Optional configuration settings for the insert operation. + /// Optional progress reporter to track insertion progress. The report value represents the number of cells written. + /// A cancellation token to monitor for cancellation requests. + /// The number of rows written to the new sheet. + /// + /// The stream position is reset to the end before writing. + /// FastMode is automatically enabled for this process and disabling it will result in . + /// [CreateSyncVersion] public async Task InsertSheetAsync(Stream stream, object value, string sheetName = "Sheet1", bool printHeader = true, bool overwriteSheet = false, OpenXmlConfiguration? configuration = null, @@ -39,9 +71,21 @@ public async Task InsertSheetAsync(Stream stream, object value, string shee .CreateAsync(stream, value, sheetName, printHeader, configuration, cancellationToken) .ConfigureAwait(false); - return await writer.InsertAsync(overwriteSheet, cancellationToken: cancellationToken).ConfigureAwait(false); + return await writer.InsertAsync(overwriteSheet, progress, cancellationToken).ConfigureAwait(false); } + /// + /// Exports data to a file as an OpenXml document. + /// + /// The path to write the OpenXml document to. + /// The data object to export. This can be an enumerable collection of a reference type, a IEnumeable<IDictionary<string, object>>, a or a . + /// If true, includes the header row in the output; otherwise, only data rows are written. + /// The name to assign to the worksheet. + /// If true, overwrites the file at the specified path, otherwise a will be raised if the file already exists. + /// Optional configuration settings for the export operation. + /// Optional progress reporter to track export progress. The report value represents the number of cells written. + /// A cancellation token to monitor for cancellation requests. + /// An array of integers representing the number of rows written for each exported sheet. [CreateSyncVersion] public async Task ExportAsync(string path, object value, bool printHeader = true, string sheetName = "Sheet1", bool overwriteFile = false, OpenXmlConfiguration? configuration = null, @@ -59,6 +103,20 @@ public async Task ExportAsync(string path, object value, bool printHeader return await ExportAsync(stream, value, printHeader, sheetName, configuration, progress, cancellationToken).ConfigureAwait(false); } + /// + /// Exports data to a stream as an OpenXml document. + /// + /// The stream to write the OpenXml document. + /// The data object to export. This can be an enumerable collection of a reference type, a IEnumeable<IDictionary<string, object>>, a or a . + /// If true, includes the header row in the output; otherwise, only data rows are written. + /// The name to assign to the worksheet. + /// Optional configuration settings for the export operation. + /// Optional progress reporter to track export progress. The report value represents the number of cells written. + /// A cancellation token to monitor for cancellation requests. + /// An array of integers representing the number of rows written for each exported sheet. + /// + /// The stream position is not reset before writing. + /// [CreateSyncVersion] public async Task ExportAsync(Stream stream, object value, bool printHeader = true, string sheetName = "Sheet1", OpenXmlConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs index 24e78a8b..738db2f0 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs @@ -1,8 +1,7 @@ -using MiniExcelLib.OpenXml; -using OpenXmlReader = MiniExcelLib.OpenXml.OpenXmlReader; +using MiniExcelLib.Core; // ReSharper disable once CheckNamespace -namespace MiniExcelLib.Core; +namespace MiniExcelLib.OpenXml; public sealed partial class OpenXmlImporter { diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs b/src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs index 462e0e2b..dd6d486f 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs @@ -1,10 +1,9 @@ -using MiniExcelLib.OpenXml; using MiniExcelLib.OpenXml.Picture; using MiniExcelLib.OpenXml.Templates; using OpenXmlTemplate = MiniExcelLib.OpenXml.Templates.OpenXmlTemplate; // ReSharper disable once CheckNamespace -namespace MiniExcelLib.Core; +namespace MiniExcelLib.OpenXml; public sealed partial class OpenXmlTemplater { diff --git a/src/MiniExcel.OpenXml/Api/ProviderExtensions.cs b/src/MiniExcel.OpenXml/Api/ProviderExtensions.cs index b5717dc6..782f71ba 100644 --- a/src/MiniExcel.OpenXml/Api/ProviderExtensions.cs +++ b/src/MiniExcel.OpenXml/Api/ProviderExtensions.cs @@ -1,6 +1,7 @@ using MiniExcelLib.Core; -namespace MiniExcelLib.OpenXml.Api; +// ReSharper disable once CheckNamespace +namespace MiniExcelLib.OpenXml; public static class ProviderExtensions { diff --git a/src/MiniExcel/MiniExcel.cs b/src/MiniExcel/MiniExcel.cs index af744c9f..51c016bd 100644 --- a/src/MiniExcel/MiniExcel.cs +++ b/src/MiniExcel/MiniExcel.cs @@ -2,16 +2,13 @@ using MiniExcelLib; using MiniExcelLib.Core; using MiniExcelLib.Csv; -using MiniExcelLib.OpenXml.Api; +using MiniExcelLib.OpenXml; using MiniExcelLib.OpenXml.Models; using MiniExcelLib.OpenXml.Picture; -using MiniExcelLibs.OpenXml; using Zomp.SyncMethodGenerator; using NewMiniExcel = MiniExcelLib.Core.MiniExcel; -using OpenXmlExporter = MiniExcelLib.OpenXml.Api.OpenXmlExporter; -using OpenXmlImporter = MiniExcelLib.Core.OpenXmlImporter; -using OpenXmlTemplater = MiniExcelLib.Core.OpenXmlTemplater; +using NewOpenXmlConfiguration = MiniExcelLib.OpenXml.OpenXmlConfiguration; // ReSharper disable once CheckNamespace namespace MiniExcelLibs; @@ -39,7 +36,7 @@ public static MiniExcelDataReader GetReader(string path, bool useHeaderRow = fal var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.GetDataReader(path, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration), + ExcelType.XLSX => ExcelImporter.GetDataReader(path, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration), ExcelType.CSV => CsvImporter.GetDataReader(path, useHeaderRow), _ => throw new NotSupportedException($"Type {type} is not a valid Excel type") }; @@ -50,7 +47,7 @@ public static MiniExcelDataReader GetReader(this Stream stream, bool useHeaderRo var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.GetDataReader(stream, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration), + ExcelType.XLSX => ExcelImporter.GetDataReader(stream, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration), ExcelType.CSV => CsvImporter.GetDataReader(stream, useHeaderRow), _ => throw new NotSupportedException($"Type {type} is not a valid Excel type") }; @@ -64,7 +61,7 @@ public static async Task InsertAsync(string path, object value, string shee var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelExporter.InsertSheetAsync(path, value, sheetName, printHeader, overwriteSheet, configuration as OpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelExporter.InsertSheetAsync(path, value, sheetName, printHeader, overwriteSheet, configuration as NewOpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvExporter.AppendAsync(path, value, printHeader, configuration as Csv.CsvConfiguration, progress, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -78,7 +75,7 @@ public static async Task InsertAsync(this Stream stream, object value, stri var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelExporter.InsertSheetAsync(stream, value, sheetName, printHeader, overwriteSheet, configuration as OpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelExporter.InsertSheetAsync(stream, value, sheetName, printHeader, overwriteSheet, configuration as NewOpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvExporter.AppendAsync(stream, value, configuration as Csv.CsvConfiguration, progress, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -92,7 +89,7 @@ public static async Task SaveAsAsync(string path, object value, bool prin var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelExporter.ExportAsync(path, value, printHeader, sheetName, overwriteFile, configuration as OpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelExporter.ExportAsync(path, value, printHeader, sheetName, overwriteFile, configuration as NewOpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvExporter.ExportAsync(path, value, printHeader, overwriteFile, configuration as Csv.CsvConfiguration, progress, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -106,7 +103,7 @@ public static async Task SaveAsAsync(this Stream stream, object value, bo var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelExporter.ExportAsync(stream, value, printHeader, sheetName, configuration as OpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelExporter.ExportAsync(stream, value, printHeader, sheetName, configuration as NewOpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvExporter.ExportAsync(stream, value, printHeader, configuration as Csv.CsvConfiguration, progress, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -118,7 +115,7 @@ public static async Task SaveAsAsync(this Stream stream, object value, bo var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryAsync(path, sheetName, startCell, hasHeader, configuration as OpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryAsync(path, sheetName, startCell, hasHeader, configuration as NewOpenXmlConfiguration, cancellationToken), ExcelType.CSV => CsvImporter.QueryAsync(path, hasHeader, configuration as Csv.CsvConfiguration, cancellationToken), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -130,7 +127,7 @@ public static async Task SaveAsAsync(this Stream stream, object value, bo var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryAsync(stream, sheetName, startCell, hasHeader, configuration as OpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryAsync(stream, sheetName, startCell, hasHeader, configuration as NewOpenXmlConfiguration, cancellationToken), ExcelType.CSV => CsvImporter.QueryAsync(stream, hasHeader, configuration as Csv.CsvConfiguration, cancellationToken), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -142,7 +139,7 @@ public static IAsyncEnumerable QueryAsync(string path, bool useHeaderRo var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryAsync(path, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryAsync(path, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, cancellationToken), ExcelType.CSV => CsvImporter.QueryAsync(path, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -154,7 +151,7 @@ public static IAsyncEnumerable QueryAsync(this Stream stream, bool useH var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, cancellationToken), ExcelType.CSV => CsvImporter.QueryAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -175,7 +172,7 @@ public static IAsyncEnumerable QueryRangeAsync(string path, bool useHea var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryRangeAsync(path, useHeaderRow, sheetName, startCell, endCell, configuration as OpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryRangeAsync(path, useHeaderRow, sheetName, startCell, endCell, configuration as NewOpenXmlConfiguration, cancellationToken), ExcelType.CSV => throw new NotSupportedException("QueryRange is not supported for csv"), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -187,7 +184,7 @@ public static IAsyncEnumerable QueryRangeAsync(this Stream stream, bool var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryRangeAsync(stream, useHeaderRow, sheetName, startCell, endCell, configuration as OpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryRangeAsync(stream, useHeaderRow, sheetName, startCell, endCell, configuration as NewOpenXmlConfiguration, cancellationToken), ExcelType.CSV => CsvImporter.QueryAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -199,7 +196,7 @@ public static IAsyncEnumerable QueryRangeAsync(string path, bool useHea var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryRangeAsync(path, useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration as OpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryRangeAsync(path, useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration as NewOpenXmlConfiguration, cancellationToken), ExcelType.CSV => throw new NotSupportedException("QueryRange is not supported for csv"), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -211,7 +208,7 @@ public static IAsyncEnumerable QueryRangeAsync(this Stream stream, bool var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => ExcelImporter.QueryRangeAsync(stream, useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration as OpenXmlConfiguration, cancellationToken), + ExcelType.XLSX => ExcelImporter.QueryRangeAsync(stream, useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration as NewOpenXmlConfiguration, cancellationToken), ExcelType.CSV => throw new NotSupportedException("QueryRange is not supported for csv"), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -221,27 +218,27 @@ public static IAsyncEnumerable QueryRangeAsync(this Stream stream, bool [CreateSyncVersion] public static async Task SaveAsByTemplateAsync(string path, string templatePath, object value, IConfiguration? configuration = null, CancellationToken cancellationToken = default) - => await ExcelTemplater.FillTemplateAsync(path, templatePath, value, true, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false); + => await ExcelTemplater.FillTemplateAsync(path, templatePath, value, true, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] public static async Task SaveAsByTemplateAsync(string path, byte[] templateBytes, object value, IConfiguration? configuration = null) - => await ExcelTemplater.FillTemplateAsync(path, templateBytes, value, true, configuration as OpenXmlConfiguration).ConfigureAwait(false); + => await ExcelTemplater.FillTemplateAsync(path, templateBytes, value, true, configuration as NewOpenXmlConfiguration).ConfigureAwait(false); [CreateSyncVersion] public static async Task SaveAsByTemplateAsync(this Stream stream, string templatePath, object value, IConfiguration? configuration = null) - => await ExcelTemplater.FillTemplateAsync(stream, templatePath, value, configuration as OpenXmlConfiguration).ConfigureAwait(false); + => await ExcelTemplater.FillTemplateAsync(stream, templatePath, value, configuration as NewOpenXmlConfiguration).ConfigureAwait(false); [CreateSyncVersion] public static async Task SaveAsByTemplateAsync(this Stream stream, byte[] templateBytes, object value, IConfiguration? configuration = null) - => await ExcelTemplater.FillTemplateAsync(stream, templateBytes, value, configuration as OpenXmlConfiguration).ConfigureAwait(false); + => await ExcelTemplater.FillTemplateAsync(stream, templateBytes, value, configuration as NewOpenXmlConfiguration).ConfigureAwait(false); [CreateSyncVersion] public static async Task SaveAsByTemplateAsync(string path, Stream templateStream, object value, IConfiguration? configuration = null) - => await ExcelTemplater.FillTemplateAsync(path, templateStream, value, true, configuration as OpenXmlConfiguration).ConfigureAwait(false); + => await ExcelTemplater.FillTemplateAsync(path, templateStream, value, true, configuration as NewOpenXmlConfiguration).ConfigureAwait(false); [CreateSyncVersion] public static async Task SaveAsByTemplateAsync(this Stream stream, Stream templateStream, object value, IConfiguration? configuration = null) - => await ExcelTemplater.FillTemplateAsync(stream, templateStream, value, configuration as OpenXmlConfiguration).ConfigureAwait(false); + => await ExcelTemplater.FillTemplateAsync(stream, templateStream, value, configuration as NewOpenXmlConfiguration).ConfigureAwait(false); #region MergeCells @@ -251,7 +248,7 @@ public static async Task MergeSameCellsAsync(string mergedFilePath, string path, if (excelType != ExcelType.XLSX) throw new NotSupportedException("MergeSameCells is only supported for Xlsx files"); - await ExcelTemplater.MergeSameCellsAsync(mergedFilePath, path, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false); + await ExcelTemplater.MergeSameCellsAsync(mergedFilePath, path, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false); } [CreateSyncVersion] @@ -260,7 +257,7 @@ public static async Task MergeSameCellsAsync(this Stream stream, string path, Ex if (excelType != ExcelType.XLSX) throw new NotSupportedException("MergeSameCells is only supported for Xlsx files"); - await ExcelTemplater.MergeSameCellsAsync(stream, path, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false); + await ExcelTemplater.MergeSameCellsAsync(stream, path, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false); } [CreateSyncVersion] @@ -269,7 +266,7 @@ public static async Task MergeSameCellsAsync(this Stream stream, byte[] filePath if (excelType != ExcelType.XLSX) throw new NotSupportedException("MergeSameCells is only supported for Xlsx files"); - await ExcelTemplater.MergeSameCellsAsync(stream, filePath, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false); + await ExcelTemplater.MergeSameCellsAsync(stream, filePath, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false); } #endregion @@ -281,7 +278,7 @@ public static async Task QueryAsDataTableAsync(string path, bool useH var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelImporter.QueryAsDataTableAsync(path, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelImporter.QueryAsDataTableAsync(path, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvImporter.QueryAsDataTableAsync(path, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; @@ -294,26 +291,26 @@ public static async Task QueryAsDataTableAsync(this Stream stream, bo var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelImporter.QueryAsDataTableAsync(stream, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelImporter.QueryAsDataTableAsync(stream, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvImporter.QueryAsDataTableAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") }; } [CreateSyncVersion] - public static async Task> GetSheetNamesAsync(string path, OpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) + public static async Task> GetSheetNamesAsync(string path, NewOpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) => await ExcelImporter.GetSheetNamesAsync(path, config, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] - public static async Task> GetSheetNamesAsync(this Stream stream, OpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) + public static async Task> GetSheetNamesAsync(this Stream stream, NewOpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) => await ExcelImporter.GetSheetNamesAsync(stream, config, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] - public static async Task> GetSheetInformationsAsync(string path, OpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) + public static async Task> GetSheetInformationsAsync(string path, NewOpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) => await ExcelImporter.GetSheetInformationsAsync(path, config, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] - public static async Task> GetSheetInformationsAsync(this Stream stream, OpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) + public static async Task> GetSheetInformationsAsync(this Stream stream, NewOpenXmlConfiguration? config = null, CancellationToken cancellationToken = default) => await ExcelImporter.GetSheetInformationsAsync(stream, config, cancellationToken).ConfigureAwait(false); [CreateSyncVersion] @@ -322,7 +319,7 @@ public static async Task> GetColumnsAsync(string path, bool var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelImporter.GetColumnNamesAsync(path, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelImporter.GetColumnNamesAsync(path, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvImporter.GetColumnNamesAsync(path, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") }; @@ -334,7 +331,7 @@ public static async Task> GetColumnsAsync(this Stream stream var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelImporter.GetColumnNamesAsync(stream, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelImporter.GetColumnNamesAsync(stream, useHeaderRow, sheetName, startCell, configuration as NewOpenXmlConfiguration, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvImporter.GetColumnNamesAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") }; diff --git a/src/MiniExcel/MiniExcelConverter.cs b/src/MiniExcel/MiniExcelConverter.cs index 564e6e96..ebec5f89 100644 --- a/src/MiniExcel/MiniExcelConverter.cs +++ b/src/MiniExcel/MiniExcelConverter.cs @@ -1,7 +1,7 @@ using MiniExcelLib.Core; using MiniExcelLib.Core.Helpers; using MiniExcelLib.Csv; -using MiniExcelLib.OpenXml.Api; +using MiniExcelLib.OpenXml; using Zomp.SyncMethodGenerator; namespace MiniExcelLib; diff --git a/tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs b/tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs index a8176b55..5487c9e5 100644 --- a/tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs +++ b/tests/MiniExcel.Csv.Tests/AsyncIssueTests.cs @@ -1,6 +1,4 @@ -using MiniExcelLib.OpenXml.Api; - -namespace MiniExcelLib.Csv.Tests; +namespace MiniExcelLib.Csv.Tests; public class AsyncIssueTests { diff --git a/tests/MiniExcel.Csv.Tests/IssueTests.cs b/tests/MiniExcel.Csv.Tests/IssueTests.cs index 6eb398b6..47f8adcc 100644 --- a/tests/MiniExcel.Csv.Tests/IssueTests.cs +++ b/tests/MiniExcel.Csv.Tests/IssueTests.cs @@ -1,4 +1,4 @@ -using MiniExcelLib.OpenXml.Api; +using MiniExcelLib.OpenXml; namespace MiniExcelLib.Csv.Tests; diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj b/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj index a31d2f89..ec76bc97 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj @@ -50,7 +50,6 @@ - From 8996c34d621a3d1f6f8f7058defcd249f4d8035d Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 16 May 2026 20:59:38 +0200 Subject: [PATCH 09/10] Added support for shared strings storage mode in the OpenXmlExporter Implemented shared strings handling across the OpenXmlWriter and wired it to the new OpenXmlConfiguration option StringStorageMode. --- .../Abstractions/IMiniExcelWriteAdapter.cs | 4 +- src/MiniExcel.OpenXml/Constants/ExcelXml.cs | 23 +++- .../Constants/WorksheetXml.cs | 18 ++- .../Models/ExcelColumnWidth.cs | 2 +- src/MiniExcel.OpenXml/OpenXmlConfiguration.cs | 16 +++ .../OpenXmlWriter.DefaultOpenXml.cs | 29 +++-- src/MiniExcel.OpenXml/OpenXmlWriter.cs | 107 +++++++++++------- .../MiniExcelIssueTests.cs | 2 +- 8 files changed, 142 insertions(+), 59 deletions(-) diff --git a/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapter.cs b/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapter.cs index 85408fc1..cebcd1e4 100644 --- a/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapter.cs +++ b/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapter.cs @@ -7,9 +7,9 @@ public interface IMiniExcelWriteAdapter IEnumerable GetRows(List mappings, CancellationToken cancellationToken = default); } -public readonly struct CellWriteInfo(object? value, int cellIndex, MiniExcelColumnMapping? mapping) +public readonly struct CellWriteInfo(object? value, int index, MiniExcelColumnMapping? mapping) { public object? Value { get; } = value; - public int CellIndex { get; } = cellIndex; + public int Index { get; } = index; public MiniExcelColumnMapping? Mapping { get; } = mapping; } diff --git a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs index 8efef7f7..b91e9f0e 100644 --- a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs +++ b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs @@ -12,7 +12,8 @@ static ExcelXml() } internal const string InlineStringDataType = "inlineStr"; - internal const string StringDataType = "str"; + internal const string CalculatedStringDataType = "str"; + internal const string SharedStringDataType = "s"; internal const string NumericDataType = "n"; internal const string BooleanDataType = "b"; @@ -73,7 +74,25 @@ static ExcelXml() """; - internal const string DefaultSharedString = ""; + internal static string SharedStrings(Dictionary sharedStrings) + { + var sb = new StringBuilder(); + sb.Append(""""); + + foreach(var (text, _) in sharedStrings.OrderBy(x => x.Value)) + { + sb.Append("{text}"); + } + + sb.Append(""); + return sb.ToString(); + } internal const string StartTypes = """"""; internal static string ContentType(string contentType, string partName) => $""; diff --git a/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs b/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs index a28d5fce..143a0347 100644 --- a/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs +++ b/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs @@ -52,13 +52,21 @@ internal static string Column(int colIndex, double columnWidth, bool hidden = fa internal const string EndCols = ""; internal static string EmptyCell(string cellReference, string styleIndex) => $""; - - //t check avoid format error ![image](https://user-images.githubusercontent.com/12729184/118770190-9eee3480-b8b3-11eb-9f5a-87a439f5e320.png) internal static string Cell(string cellReference, string? cellType, string styleIndex, string? cellValue, bool preserveSpace = false, ColumnType columnType = ColumnType.Value) { - return cellType == ExcelXml.InlineStringDataType - ? $"""{cellValue}""" - : $"""{cellValue}"""; + return cellType switch + { + _ when columnType == ColumnType.Formula + => $"""{cellValue}""", + + ExcelXml.InlineStringDataType + => $"""{cellValue}""", + + ExcelXml.SharedStringDataType + => $"""{cellValue}""", + + _ => $"""{cellValue}""" + }; } internal static string Autofilter(string dimensionRef) => $""; diff --git a/src/MiniExcel.OpenXml/Models/ExcelColumnWidth.cs b/src/MiniExcel.OpenXml/Models/ExcelColumnWidth.cs index d6b652c0..cbb1f838 100644 --- a/src/MiniExcel.OpenXml/Models/ExcelColumnWidth.cs +++ b/src/MiniExcel.OpenXml/Models/ExcelColumnWidth.cs @@ -53,7 +53,7 @@ internal static ExcelColumnWidthCollection GetFromMappings(ICollection + /// Prioritizes memory usage over file size during export by writing text directly to cells. + /// Ideal when exporting big datasets. + /// + Inline, + + /// + /// Prioritizes file size over memory usage during export by storing unique strings in the sharedStrings table. + /// Ideal for standard files with repetitive text. + /// + Shared +} diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs index 46d2a185..0cc07cea 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs @@ -17,6 +17,7 @@ internal partial class OpenXmlWriter private static readonly DateTime ExcelZeroDate = new(1899, 12, 31); private readonly Dictionary _zipContentsMap = []; + private readonly Dictionary _sharedStrings = []; private IEnumerable<(SheetDto Sheet, object? Data)> GetSheets() { @@ -162,7 +163,7 @@ private string GetPanes() return sb.ToString(); } - private (string StyleIndex, string? DataType, string? CellValue) GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnMapping? columnMapping, bool valueIsNull) + private (string StyleIndex, string DataType, string? CellValue) GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnMapping? columnMapping, bool valueIsNull) { if (valueIsNull) return (RegularCellStyleIndex, GetStringType(), string.Empty); @@ -222,7 +223,7 @@ private string GetPanes() return (RegularCellStyleIndex, dataType, cellValue); } - return (columnMapping.ExcelFormatId.ToString(), null, cellValue); + return (columnMapping.ExcelFormatId.ToString(), ExcelXml.NumericDataType, cellValue); } if (type == typeof(bool)) @@ -231,15 +232,23 @@ private string GetPanes() if (type == typeof(byte[]) && _configuration.EnableConvertByteArray) { if (!_configuration.EnableWriteFilePath) - return (FillCellStyleIndex, GetStringType(), ""); + return (FillCellStyleIndex, ExcelXml.CalculatedStringDataType, ""); var base64 = GetFileValue(rowIndex, cellIndex, value); - return (FillCellStyleIndex, GetStringType(), XmlHelper.EncodeXml(base64)); + return (FillCellStyleIndex, ExcelXml.InlineStringDataType, XmlHelper.EncodeXml(base64)); } return (RegularCellStyleIndex, GetStringType(), XmlHelper.EncodeXml(value.ToString())); - string GetStringType() => columnMapping?.ExcelColumnType == ColumnType.Value ? ExcelXml.InlineStringDataType : ExcelXml.StringDataType; + string GetStringType() + { + if (columnMapping?.ExcelColumnType == ColumnType.Formula) + return ExcelXml.CalculatedStringDataType; + + return _configuration.StringStorageMode == StringStorageMode.Shared + ? ExcelXml.SharedStringDataType + : ExcelXml.InlineStringDataType; + } } private static Type GetValueType(object value, MiniExcelColumnMapping? columnInfo) @@ -320,20 +329,20 @@ private string GetFileValue(int rowIndex, int cellIndex, object value) } //todo:reconsider cultureinfo - private (string, string?, string) GetDateTimeValue(DateTime value, MiniExcelColumnMapping? columnMapping) + private (string, string, string) GetDateTimeValue(DateTime value, MiniExcelColumnMapping? columnMapping) { string? cellValue; if (!ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture)) { cellValue = value.ToString(_configuration.Culture); - return (RegularCellStyleIndex, ExcelXml.StringDataType, cellValue); + return (RegularCellStyleIndex, ExcelXml.CalculatedStringDataType, cellValue); } var oaDate = CorrectDateTimeValue(value); cellValue = oaDate.ToString(CultureInfo.InvariantCulture); var format = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : DateCellStyleIndex; - return (format, null, cellValue); + return (format, ExcelXml.NumericDataType, cellValue); } private static double CorrectDateTimeValue(DateTime value) @@ -352,7 +361,7 @@ private static double CorrectDateTimeValue(DateTime value) return oaDate; } - private (string, string?, string) GetTimeSpanValue(TimeSpan value, MiniExcelColumnMapping? columnMapping) + private (string, string, string) GetTimeSpanValue(TimeSpan value, MiniExcelColumnMapping? columnMapping) { if (value.TotalDays >= 1) return GetDateTimeValue(ExcelZeroDate + value, columnMapping); @@ -360,7 +369,7 @@ private static double CorrectDateTimeValue(DateTime value) var cellValue = value.TotalDays.ToString(CultureInfo.InvariantCulture); var format = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : TimeCellStyleIndex; - return (format, null, cellValue); + return (format, ExcelXml.NumericDataType, cellValue); } private static string GetDimensionRef(int maxRowIndex, int maxColumnIndex) diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.cs index cb91c0ba..7e3d78c9 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.cs @@ -20,7 +20,6 @@ internal partial class OpenXmlWriter : IMiniExcelWriter private readonly string _sheetName; private readonly bool _printHeader; - private readonly bool _useSharedStrings; private readonly object? _value; private int _currentSheetIndex = 0; @@ -65,7 +64,6 @@ public async Task SaveAsAsync(IProgress? progress = null, Cancellati using var disposableArchive = _archive; #endif await CreateZipEntryAsync(ExcelFileNames.Rels, ExcelContentTypes.Relationships, ExcelXml.DefaultRels, cancellationToken).ConfigureAwait(false); - await CreateZipEntryAsync(ExcelFileNames.SharedStrings, ExcelContentTypes.SharedStrings, ExcelXml.DefaultSharedString, cancellationToken).ConfigureAwait(false); await using var sbc = _sheetStyleBuildContext.ConfigureAwait(false); var styleBuilder = await GetSheetStyleBuilderAsync(cancellationToken).ConfigureAwait(false); @@ -83,7 +81,13 @@ public async Task SaveAsAsync(IProgress? progress = null, Cancellati } await styleBuilder.BuildAsync(cancellationToken).ConfigureAwait(false); - await GenerateEndXmlAsync(cancellationToken).ConfigureAwait(false); + + await AddFilesToZipAsync(cancellationToken).ConfigureAwait(false); + await GenerateSharedStringsAsync(cancellationToken).ConfigureAwait(false); + await GenerateDrawinRelXmlAsync(cancellationToken).ConfigureAwait(false); + await GenerateDrawingXmlAsync(cancellationToken).ConfigureAwait(false); + await GenerateWorkbookXmlAsync(cancellationToken).ConfigureAwait(false); + await GenerateContentTypesXmlAsync(cancellationToken).ConfigureAwait(false); return rowsWritten.ToArray(); } @@ -114,15 +118,32 @@ public async Task InsertAsync(bool overwriteSheet = false, IProgress? }) ); - var existSheetDto = _sheets.SingleOrDefault(s => s.Name == _sheetName); - if (existSheetDto is not null && !overwriteSheet) + var existingSheetDto = _sheets.SingleOrDefault(s => s.Name == _sheetName); + if (existingSheetDto is not null && !overwriteSheet) throw new Exception($"Sheet \"{_sheetName}\" already exist"); // GenerateStylesXml must be invoked after validating the overwritesheet parameter to avoid unnecessary style changes. var styleBuilder = await GetSheetStyleBuilderAsync(cancellationToken).ConfigureAwait(false); + var sharedStringsEntry = _archive.GetEntry(ExcelFileNames.SharedStrings); + if (sharedStringsEntry is not null) + { +#if NET8_0_OR_GREATER + var sharedStringsStream = await sharedStringsEntry.OpenAsync(cancellationToken).ConfigureAwait(false); + await using var disposableStream = sharedStringsStream.ConfigureAwait(false); +#else + using var sharedStringsStream = await sharedStringsEntry.OpenAsync(cancellationToken).ConfigureAwait(false); +#endif + + var index = 0; + await foreach (var sharedString in XmlReaderHelper.GetSharedStringsAsync(sharedStringsStream, cancellationToken).ConfigureAwait(false)) + { + _sharedStrings.Add(sharedString, index++); + } + } + int rowsWritten; - if (existSheetDto is null) + if (existingSheetDto is null) { _currentSheetIndex = (int)rels.Max(m => m.Id) + 1; var insertSheetInfo = GetSheetInfos(_sheetName); @@ -132,14 +153,17 @@ public async Task InsertAsync(bool overwriteSheet = false, IProgress? } else { - _currentSheetIndex = existSheetDto.SheetIdx; - _archive.Entries.Single(s => s.FullName == existSheetDto.Path).Delete(); - rowsWritten = await CreateSheetXmlAsync(_value, existSheetDto.Path, progress, cancellationToken).ConfigureAwait(false); + _currentSheetIndex = existingSheetDto.SheetIdx; + _archive.Entries.Single(s => s.FullName == existingSheetDto.Path).Delete(); + rowsWritten = await CreateSheetXmlAsync(_value, existingSheetDto.Path, progress, cancellationToken).ConfigureAwait(false); } await styleBuilder.BuildAsync(cancellationToken).ConfigureAwait(false); await AddFilesToZipAsync(cancellationToken).ConfigureAwait(false); + sharedStringsEntry?.Delete(); + await GenerateSharedStringsAsync(cancellationToken).ConfigureAwait(false); + _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.DrawingRels(_currentSheetIndex - 1))?.Delete(); await GenerateDrawinRelXmlAsync(_currentSheetIndex - 1, cancellationToken).ConfigureAwait(false); @@ -174,13 +198,13 @@ private async Task CreateSheetXmlAsync(object? values, string sheetPath, IP #if NET8_0_OR_GREATER var zipStream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); await using var disposableZipStream = zipStream.ConfigureAwait(false); - - var writer = new MiniExcelStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize); - await using var disposableWriter = writer.ConfigureAwait(false); #else using var zipStream = entry.Open(); - using var writer = new MiniExcelStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize); #endif + + var writer = new MiniExcelStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize); + await using var disposableWriter = writer.ConfigureAwait(false); + if (values is null) { await WriteEmptySheetAsync(writer).ConfigureAwait(false); @@ -301,10 +325,10 @@ private async Task WriteValuesAsync(MiniExcelStreamWriter writer, object va cancellationToken.ThrowIfCancellationRequested(); await writer.WriteAsync(WorksheetXml.StartRow(++currentRowIndex), cancellationToken).ConfigureAwait(false); - foreach (var cellValue in row) + foreach (var cell in row) { cancellationToken.ThrowIfCancellationRequested(); - await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Mapping, widths, cancellationToken).ConfigureAwait(false); + await WriteCellAsync(writer, currentRowIndex, cell.Index, cell.Value, cell.Mapping, widths, cancellationToken).ConfigureAwait(false); progress?.Report(1); } await writer.WriteAsync(WorksheetXml.EndRow, cancellationToken).ConfigureAwait(false); @@ -318,9 +342,9 @@ private async Task WriteValuesAsync(MiniExcelStreamWriter writer, object va cancellationToken.ThrowIfCancellationRequested(); await writer.WriteAsync(WorksheetXml.StartRow(++currentRowIndex), cancellationToken).ConfigureAwait(false); - foreach (var cellValue in row) + foreach (var cell in row) { - await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Mapping, widths, cancellationToken).ConfigureAwait(false); + await WriteCellAsync(writer, currentRowIndex, cell.Index, cell.Value, cell.Mapping, widths, cancellationToken).ConfigureAwait(false); progress?.Report(1); } await writer.WriteAsync(WorksheetXml.EndRow, cancellationToken).ConfigureAwait(false); @@ -438,9 +462,9 @@ private static async Task PrintHeaderAsync(MiniExcelStreamWriter writer, List sheetsRelsXml); + GenerateWorkBookXmls(out var workbookXml, out var workbookRelsXml, out var sheetsRelsXml); foreach (var (key, value) in sheetsRelsXml) { @@ -581,6 +606,12 @@ await CreateZipEntryAsync( cancellationToken).ConfigureAwait(false); } + [CreateSyncVersion] + private async Task GenerateSharedStringsAsync(CancellationToken cancellationToken) + { + await CreateZipEntryAsync(ExcelFileNames.SharedStrings, ExcelContentTypes.SharedStrings, ExcelXml.SharedStrings(_sharedStrings), cancellationToken).ConfigureAwait(false); + } + [CreateSyncVersion] private async Task GenerateContentTypesXmlAsync(CancellationToken cancellationToken) { diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs index 739722fb..59ffd65a 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs @@ -203,7 +203,7 @@ public void TestIssue405() _excelExporter.Export(path.ToString(), value); var xml = SheetHelper.GetZipFileContent(path.ToString(), "xl/sharedStrings.xml"); - Assert.StartsWith(" Date: Sun, 17 May 2026 01:55:45 +0200 Subject: [PATCH 10/10] Centralized xml encoding to WorksheetXml.Cell method --- src/MiniExcel.OpenXml/Constants/ExcelXml.cs | 4 ++-- src/MiniExcel.OpenXml/Constants/WorksheetXml.cs | 6 +++--- src/MiniExcel.OpenXml/Helpers/ThrowHelper.cs | 2 +- .../OpenXmlWriter.DefaultOpenXml.cs | 8 ++++---- src/MiniExcel.OpenXml/OpenXmlWriter.cs | 13 ++++++------- 5 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs index b91e9f0e..13ba39dd 100644 --- a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs +++ b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs @@ -77,7 +77,7 @@ static ExcelXml() internal static string SharedStrings(Dictionary sharedStrings) { var sb = new StringBuilder(); - sb.Append(""""); foreach(var (text, _) in sharedStrings.OrderBy(x => x.Value)) @@ -87,7 +87,7 @@ internal static string SharedStrings(Dictionary sharedStrings) if (text.StartsWith(" ") || text.EndsWith(" ")) sb.Append(" xml:space=\"preserve\""); - sb.Append($">{text}"); + sb.Append($">{XmlHelper.EncodeXml(text)}"); } sb.Append(""); diff --git a/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs b/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs index 143a0347..fdfbee8a 100644 --- a/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs +++ b/src/MiniExcel.OpenXml/Constants/WorksheetXml.cs @@ -57,15 +57,15 @@ internal static string Cell(string cellReference, string? cellType, string style return cellType switch { _ when columnType == ColumnType.Formula - => $"""{cellValue}""", + => $"""{XmlHelper.EncodeXml(cellValue)}""", ExcelXml.InlineStringDataType - => $"""{cellValue}""", + => $"""{XmlHelper.EncodeXml(cellValue)}""", ExcelXml.SharedStringDataType => $"""{cellValue}""", - _ => $"""{cellValue}""" + _ => $"""{cellValue}""" }; } diff --git a/src/MiniExcel.OpenXml/Helpers/ThrowHelper.cs b/src/MiniExcel.OpenXml/Helpers/ThrowHelper.cs index fb5e5570..42634a7a 100644 --- a/src/MiniExcel.OpenXml/Helpers/ThrowHelper.cs +++ b/src/MiniExcel.OpenXml/Helpers/ThrowHelper.cs @@ -30,6 +30,6 @@ internal static void ThrowIfInvalidSheetName(string? sheetName) throw new ArgumentException("Sheet names must be less than 31 characters"); if (sheetName.Intersect(InvalidSheetNameCharacters).Any()) - throw new ArgumentException($"Sheet names cannot contain any of the following charachters: {string.Join(", ", InvalidSheetNameCharacters)}"); + throw new ArgumentException($"Sheet names cannot contain any of the following characters: {string.Join(", ", InvalidSheetNameCharacters)}"); } } diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs index 0cc07cea..bdb882b9 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs @@ -171,7 +171,7 @@ private string GetPanes() if (value is string str) { var styleIndex = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : RegularCellStyleIndex; - return (styleIndex, GetStringType(), XmlHelper.EncodeXml(str)); + return (styleIndex, GetStringType(), str); } var type = GetValueType(value, columnMapping); @@ -179,7 +179,7 @@ private string GetPanes() if (columnMapping is { ExcelFormat: not null, ExcelFormatId: -1 } && value is IFormattable formattableValue) { var formattedStr = formattableValue.ToString(columnMapping.ExcelFormat, _configuration.Culture); - return (RegularCellStyleIndex, GetStringType(), XmlHelper.EncodeXml(formattedStr)); + return (RegularCellStyleIndex, GetStringType(), formattedStr); } if (type == typeof(DateTime)) @@ -235,10 +235,10 @@ private string GetPanes() return (FillCellStyleIndex, ExcelXml.CalculatedStringDataType, ""); var base64 = GetFileValue(rowIndex, cellIndex, value); - return (FillCellStyleIndex, ExcelXml.InlineStringDataType, XmlHelper.EncodeXml(base64)); + return (FillCellStyleIndex, ExcelXml.InlineStringDataType, base64); } - return (RegularCellStyleIndex, GetStringType(), XmlHelper.EncodeXml(value.ToString())); + return (RegularCellStyleIndex, GetStringType(), value.ToString()); string GetStringType() { diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.cs index 7e3d78c9..898d0eef 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.cs @@ -120,7 +120,7 @@ public async Task InsertAsync(bool overwriteSheet = false, IProgress? var existingSheetDto = _sheets.SingleOrDefault(s => s.Name == _sheetName); if (existingSheetDto is not null && !overwriteSheet) - throw new Exception($"Sheet \"{_sheetName}\" already exist"); + throw new Exception($"Sheet \"{_sheetName}\" already exists"); // GenerateStylesXml must be invoked after validating the overwritesheet parameter to avoid unnecessary style changes. var styleBuilder = await GetSheetStyleBuilderAsync(cancellationToken).ConfigureAwait(false); @@ -453,7 +453,7 @@ private static async Task PrintHeaderAsync(MiniExcelStreamWriter writer, List