From 3751dd08511b9f145fc87ff6ccfbf9170f4240a3 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sat, 4 Jul 2026 23:18:48 +0200 Subject: [PATCH 1/3] Minor xml generation refactoring --- src/Directory.Packages.props | 2 +- src/MiniExcel.OpenXml/Constants/ExcelXml.cs | 63 ++++++++----------- .../Writer/OpenXmlWriter.CopyInsert.cs | 15 +++-- .../Writer/OpenXmlWriter.XmlGeneration.cs | 21 ++++--- src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs | 42 ++++++------- 5 files changed, 65 insertions(+), 78 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index d3a0242d..088e5b8a 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,6 +1,6 @@ - + diff --git a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs index b9e2e183..7d530b8d 100644 --- a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs +++ b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs @@ -2,69 +2,53 @@ internal static class ExcelXml { - static ExcelXml() - { - DefaultRels = XmlHelper.MinifyXml(DefaultRels); - DefaultWorkbookXml = XmlHelper.MinifyXml(DefaultWorkbookXml); - DefaultWorkbookXmlRels = XmlHelper.MinifyXml(DefaultWorkbookXmlRels); - DefaultSheetRelXml = XmlHelper.MinifyXml(DefaultSheetRelXml); - DefaultDrawing = XmlHelper.MinifyXml(DefaultDrawing); - } - internal const string EmptySheetXml = """"""; + internal const string DefaultRels = """"""; - internal static readonly string DefaultRels = - """ - - - - - """; - - internal static readonly string DefaultWorkbookXmlRels = - """ + internal static string DefaultWorkbookXmlRels(string sheets) => XmlHelper.MinifyXml( + $""" - {{sheets}} + {sheets} - """; + """); - internal static readonly string DefaultWorkbookXml = - """ + internal static string DefaultWorkbookXml(string content) => XmlHelper.MinifyXml( + $""" - {{sheets}} + {content} - """; + """); - internal static readonly string DefaultSheetRelXml = - """ + internal static string DefaultSheetRelXml(string content) => XmlHelper.MinifyXml( + $""" - {{format}} + {content} - """; + """); - internal static readonly string DefaultDrawing = - """ + internal static string DefaultDrawing(string content) => XmlHelper.MinifyXml( + $""" - {{format}} + {content} - """; + """); - internal const string DefaultDrawingXmlRels = - """ + internal static string DefaultDrawingXmlRels(string content) => + $""" - {{format}} + {content} """; @@ -98,8 +82,11 @@ internal static string WorksheetRelationship(SheetDto sheetDto) internal static string ImageRelationship(FileDto image) => $""""""; - internal static string DrawingRelationship(int sheetId) - => $""""""; + internal static string DrawingRelationship(int sheetIndex) + => $""""""; + + internal static string TableRelationship(int sheetIndex) + => $""""""; internal static string DrawingXml(FileDto file, int fileIndex) => $""" diff --git a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.CopyInsert.cs b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.CopyInsert.cs index 4092b7ba..635beb12 100644 --- a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.CopyInsert.cs +++ b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.CopyInsert.cs @@ -73,8 +73,7 @@ public async Task CopyAndInsertAsync(bool overwriteSheet = false, IProgress await _sheetStyleBuilderContext.DisposeAsync().ConfigureAwait(false); _sheetStyleBuilderContext = sheetStylesBuilderUtils.SheetStyleBuilderContext; - var sharedStringsEntry = _oldArchive.GetEntry(ExcelFileNames.SharedStrings); - if (sharedStringsEntry is not null) + if (_oldArchive.GetEntry(ExcelFileNames.SharedStrings) is not null) { foreach (var (key, value) in reader.SharedStrings) { @@ -126,26 +125,26 @@ public async Task CopyAndInsertAsync(bool overwriteSheet = false, IProgress await AddFilesToZipAsync(cancellationToken).ConfigureAwait(false); await GenerateSharedStringsAsync(cancellationToken).ConfigureAwait(false); - await GenerateDrawingRelXmlAsync(_currentSheetIndex, cancellationToken).ConfigureAwait(false); + await GenerateDrawingRelAsync(_currentSheetIndex, cancellationToken).ConfigureAwait(false); await GenerateDrawingXmlAsync(_currentSheetIndex, cancellationToken).ConfigureAwait(false); await CreateZipEntryAsync( ExcelFileNames.SheetRels(_currentSheetIndex), null, - ExcelXml.DefaultSheetRelXml.Replace("{{format}}", ExcelXml.DrawingRelationship(_currentSheetIndex)), + ExcelXml.DefaultSheetRelXml(ExcelXml.DrawingRelationship(_currentSheetIndex)), cancellationToken).ConfigureAwait(false); - var (workbookXml, workbookRelsXml, _) = GenerateWorkbookXmls(); + var (workbookXml, workbookRelsXml, _) = GenerateWorkbookItems(); await CreateZipEntryAsync( ExcelFileNames.Workbook, ExcelContentTypes.Workbook, - ExcelXml.DefaultWorkbookXml.Replace("{{sheets}}", workbookXml), + ExcelXml.DefaultWorkbookXml(workbookXml), cancellationToken).ConfigureAwait(false); await CreateZipEntryAsync( ExcelFileNames.WorkbookRels, null, - ExcelXml.DefaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml), + ExcelXml.DefaultWorkbookXmlRels(workbookRelsXml), cancellationToken).ConfigureAwait(false); await CopyAndUpdateContentTypesAsync(cancellationToken).ConfigureAwait(false); @@ -228,7 +227,7 @@ private async Task CopyAndUpdateContentTypesAsync(CancellationToken cancellation var contentTypesZipEntry = _oldArchive!.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.ContentTypes); if (contentTypesZipEntry is null) { - await GenerateContentTypesXmlAsync(cancellationToken).ConfigureAwait(false); + await GenerateContentTypesAsync(cancellationToken).ConfigureAwait(false); return; } diff --git a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs index 6bd6091d..579b2ade 100644 --- a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs +++ b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs @@ -404,26 +404,27 @@ private string GetDrawingXml(int sheetIndex) return drawing.ToString(); } - private (string WorkbookXml, string WorkbookRelsXml, Dictionary SheetRelsXml) GenerateWorkbookXmls() + private (string WorkbookContents, string WorkbookRelsContents, Dictionary SheetRelsContents) GenerateWorkbookItems() { - var workbookXml = new StringBuilder(); - var workbookRelsXml = new StringBuilder(); - var sheetsRelsXml = new Dictionary(); + var workbookContents = new StringBuilder(); + var workbookRelsContents = new StringBuilder(); + var sheetsRelsContents = new StringBuilder(); + var sheetsRelsDict = new Dictionary(); var sheetId = 0; foreach (var sheetDto in _sheets) { sheetId++; - workbookXml.AppendLine(ExcelXml.Sheet(sheetDto, sheetId)); - workbookRelsXml.AppendLine(ExcelXml.WorksheetRelationship(sheetDto)); - + workbookContents.AppendLine(ExcelXml.Sheet(sheetDto, sheetId)); + workbookRelsContents.AppendLine(ExcelXml.WorksheetRelationship(sheetDto)); + sheetsRelsContents.AppendLine(ExcelXml.DrawingRelationship(sheetId)); + //TODO: support multiple drawing - //TODO: ../drawings/drawing1.xml or /xl/drawings/drawing1.xml - sheetsRelsXml.Add(sheetDto.SheetIdx, ExcelXml.DrawingRelationship(sheetId)); + sheetsRelsDict.Add(sheetDto.SheetIdx, sheetsRelsContents.ToString()); } - return (workbookXml.ToString(), workbookRelsXml.ToString(), sheetsRelsXml); + return (workbookContents.ToString(), workbookRelsContents.ToString(), sheetsRelsDict); } private string GetContentTypesXml() diff --git a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs index 30c1f2bb..74505df2 100644 --- a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs @@ -84,10 +84,10 @@ public async Task SaveAsAsync(IProgress? progress = null, Cancellati await AddFilesToZipAsync(cancellationToken).ConfigureAwait(false); await GenerateSharedStringsAsync(cancellationToken).ConfigureAwait(false); - await GenerateDrawingRelXmlAsync(cancellationToken).ConfigureAwait(false); - await GenerateDrawingXmlAsync(cancellationToken).ConfigureAwait(false); - await GenerateWorkbookXmlAsync(false, cancellationToken).ConfigureAwait(false); - await GenerateContentTypesXmlAsync(cancellationToken).ConfigureAwait(false); + await GenerateDrawingRelsAsync(cancellationToken).ConfigureAwait(false); + await GenerateDrawingsAsync(cancellationToken).ConfigureAwait(false); + await GenerateWorkbookAsync(false, cancellationToken).ConfigureAwait(false); + await GenerateContentTypesAsync(cancellationToken).ConfigureAwait(false); return rowsWritten.ToArray(); } @@ -159,11 +159,11 @@ public async Task InsertAsync(bool overwriteSheet = false, IProgress? await GenerateSharedStringsAsync(cancellationToken).ConfigureAwait(false); _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.DrawingRels(_currentSheetIndex))?.Delete(); - await GenerateDrawingRelXmlAsync(_currentSheetIndex, cancellationToken).ConfigureAwait(false); + await GenerateDrawingRelAsync(_currentSheetIndex, cancellationToken).ConfigureAwait(false); _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Drawing(_currentSheetIndex))?.Delete(); await GenerateDrawingXmlAsync(_currentSheetIndex, cancellationToken).ConfigureAwait(false); - await GenerateWorkbookXmlAsync(true, cancellationToken).ConfigureAwait(false); + await GenerateWorkbookAsync(true, cancellationToken).ConfigureAwait(false); await InsertContentTypesXmlAsync(cancellationToken).ConfigureAwait(false); return rowsWritten; @@ -523,28 +523,28 @@ private async Task GetSheetStyleBuilderAsync(CancellationTok } [CreateSyncVersion] - private async Task GenerateDrawingRelXmlAsync(CancellationToken cancellationToken) + private async Task GenerateDrawingRelsAsync(CancellationToken cancellationToken) { for (int sheetIndex = 1; sheetIndex <= _sheets.Count; sheetIndex++) { cancellationToken.ThrowIfCancellationRequested(); - await GenerateDrawingRelXmlAsync(sheetIndex, cancellationToken).ConfigureAwait(false); + await GenerateDrawingRelAsync(sheetIndex, cancellationToken).ConfigureAwait(false); } } [CreateSyncVersion] - private async Task GenerateDrawingRelXmlAsync(int sheetIndex, CancellationToken cancellationToken) + private async Task GenerateDrawingRelAsync(int sheetIndex, CancellationToken cancellationToken) { var drawing = GetDrawingRelationshipXml(sheetIndex); await CreateZipEntryAsync( ExcelFileNames.DrawingRels(sheetIndex), string.Empty, - ExcelXml.DefaultDrawingXmlRels.Replace("{{format}}", drawing), + ExcelXml.DefaultDrawingXmlRels(drawing), cancellationToken).ConfigureAwait(false); } [CreateSyncVersion] - private async Task GenerateDrawingXmlAsync(CancellationToken cancellationToken) + private async Task GenerateDrawingsAsync(CancellationToken cancellationToken) { for (int sheetIndex = 1; sheetIndex <= _sheets.Count; sheetIndex++) { @@ -560,24 +560,24 @@ private async Task GenerateDrawingXmlAsync(int sheetIndex, CancellationToken can await CreateZipEntryAsync( ExcelFileNames.Drawing(sheetIndex), ExcelContentTypes.Drawing, - ExcelXml.DefaultDrawing.Replace("{{format}}", drawing), + ExcelXml.DefaultDrawing(drawing), cancellationToken).ConfigureAwait(false); } [CreateSyncVersion] - private async Task GenerateWorkbookXmlAsync(bool removeOriginalEntry = false, CancellationToken cancellationToken = default) + private async Task GenerateWorkbookAsync(bool removeOriginalEntry = false, CancellationToken cancellationToken = default) { - var (workbookXml, workbookRelsXml, sheetsRelsXml) = GenerateWorkbookXmls(); - foreach (var (key, value) in sheetsRelsXml) + var (workbookXml, workbookRelsXml, sheetsRelsXml) = GenerateWorkbookItems(); + foreach (var (index, contents) in sheetsRelsXml) { - var sheetRelsXmlPath = ExcelFileNames.SheetRels(key); + var sheetRelsXmlPath = ExcelFileNames.SheetRels(index); if (removeOriginalEntry) _archive.Entries.SingleOrDefault(s => s.FullName == sheetRelsXmlPath)?.Delete(); await CreateZipEntryAsync( sheetRelsXmlPath, null, - ExcelXml.DefaultSheetRelXml.Replace("{{format}}", value), + ExcelXml.DefaultSheetRelXml(contents), cancellationToken).ConfigureAwait(false); } @@ -587,7 +587,7 @@ await CreateZipEntryAsync( await CreateZipEntryAsync( ExcelFileNames.Workbook, ExcelContentTypes.Workbook, - ExcelXml.DefaultWorkbookXml.Replace("{{sheets}}", workbookXml), + ExcelXml.DefaultWorkbookXml(workbookXml), cancellationToken).ConfigureAwait(false); if(removeOriginalEntry) @@ -596,7 +596,7 @@ await CreateZipEntryAsync( await CreateZipEntryAsync( ExcelFileNames.WorkbookRels, null, - ExcelXml.DefaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml), + ExcelXml.DefaultWorkbookXmlRels(workbookRelsXml), cancellationToken).ConfigureAwait(false); } @@ -611,7 +611,7 @@ await CreateZipEntryAsync( } [CreateSyncVersion] - private async Task GenerateContentTypesXmlAsync(CancellationToken cancellationToken) + private async Task GenerateContentTypesAsync(CancellationToken cancellationToken) { var contentTypes = GetContentTypesXml(); await CreateZipEntryAsync(ExcelFileNames.ContentTypes, null, contentTypes, cancellationToken).ConfigureAwait(false); @@ -623,7 +623,7 @@ private async Task InsertContentTypesXmlAsync(CancellationToken cancellationToke var contentTypesZipEntry = _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.ContentTypes); if (contentTypesZipEntry is null) { - await GenerateContentTypesXmlAsync(cancellationToken).ConfigureAwait(false); + await GenerateContentTypesAsync(cancellationToken).ConfigureAwait(false); return; } From fcdc1511d7589fa865a070b9d96b6ff2aff5a0c1 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sun, 5 Jul 2026 21:26:41 +0200 Subject: [PATCH 2/3] Changed sheets and files' ids to be deterministic Generated Excel files are now always consistent when the data source is the same, save for the metadata of the zip entries. --- src/MiniExcel.Core/GlobalUsings.cs | 2 +- src/MiniExcel.OpenXml/Constants/ExcelXml.cs | 54 ++++++++++++++----- src/MiniExcel.OpenXml/Models/FileDto.cs | 11 ++-- src/MiniExcel.OpenXml/Models/SheetDto.cs | 5 +- .../Writer/OpenXmlWriter.XmlGeneration.cs | 18 ++----- src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs | 2 +- .../Issues/MiniExcelGithubIssuesAsyncTests.cs | 42 +++++++++++++++ .../Issues/MiniExcelGithubIssuesTests.cs | 39 +++++++++++++- 8 files changed, 133 insertions(+), 40 deletions(-) diff --git a/src/MiniExcel.Core/GlobalUsings.cs b/src/MiniExcel.Core/GlobalUsings.cs index 88106cf2..1e346d37 100644 --- a/src/MiniExcel.Core/GlobalUsings.cs +++ b/src/MiniExcel.Core/GlobalUsings.cs @@ -9,4 +9,4 @@ global using MiniExcelLib.Core.Abstractions; global using MiniExcelLib.Core.Helpers; global using MiniExcelLib.Core.Reflection; -global using Zomp.SyncMethodGenerator; \ No newline at end of file +global using Zomp.SyncMethodGenerator; diff --git a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs index 7d530b8d..603e6af3 100644 --- a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs +++ b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs @@ -2,16 +2,28 @@ internal static class ExcelXml { - internal const string EmptySheetXml = """"""; - internal const string DefaultRels = """"""; + internal static readonly string EmptySheetXml = XmlHelper.MinifyXml(""" + + + + + + """); + + internal static readonly string DefaultRels = XmlHelper.MinifyXml(""" + + + + + """); internal static string DefaultWorkbookXmlRels(string sheets) => XmlHelper.MinifyXml( $""" {sheets} - - + + """); @@ -72,21 +84,37 @@ internal static string SharedStrings(Dictionary sharedStrings) return sb.ToString(); } - internal const string StartTypes = """"""; - internal static string ContentType(string contentType, string partName) => $""; - internal const string EndTypes = ""; + internal static string ContentTypes(Dictionary contentTypes) + { + var sb = new StringBuilder(); + sb.AppendLine(""" + + + + + + + + """); + + foreach (var (name, type) in contentTypes) + sb.AppendLine($""""""); + + sb.Append(""); + return XmlHelper.MinifyXml(sb.ToString()); + } internal static string WorksheetRelationship(SheetDto sheetDto) - => $""""""; + => $""""""; internal static string ImageRelationship(FileDto image) - => $""""""; + => $""""""; internal static string DrawingRelationship(int sheetIndex) - => $""""""; + => $""""""; internal static string TableRelationship(int sheetIndex) - => $""""""; + => $""""""; internal static string DrawingXml(FileDto file, int fileIndex) => $""" @@ -106,7 +134,7 @@ internal static string DrawingXml(FileDto file, int fileIndex) - + @@ -126,5 +154,5 @@ internal static string DrawingXml(FileDto file, int fileIndex) """; internal static string Sheet(SheetDto sheetDto, int sheetId) - => $""""""; + => $""""""; } diff --git a/src/MiniExcel.OpenXml/Models/FileDto.cs b/src/MiniExcel.OpenXml/Models/FileDto.cs index 9f032071..087be2ee 100644 --- a/src/MiniExcel.OpenXml/Models/FileDto.cs +++ b/src/MiniExcel.OpenXml/Models/FileDto.cs @@ -2,13 +2,12 @@ internal class FileDto { - internal string ID { get; set; } = $"R{Guid.NewGuid():N}"; - internal string Extension { get; set; } - internal string Path => $"xl/media/{ID}.{Extension}"; - internal string Path2 => $"/xl/media/{ID}.{Extension}"; - internal byte[] Contents { get; set; } + internal int SheetIndex { get; set; } internal int RowIndex { get; set; } internal int CellIndex { get; set; } + internal string Id => $"rFileId_{SheetIndex}_{RowIndex + 1}_{CellIndex + 1}"; + internal string Path => $"xl/media/{Id}.{Extension}"; internal bool IsImage { get; set; } - internal int SheetId { get; set; } + internal string Extension { get; set; } + internal byte[] Contents { get; set; } } \ No newline at end of file diff --git a/src/MiniExcel.OpenXml/Models/SheetDto.cs b/src/MiniExcel.OpenXml/Models/SheetDto.cs index 99f14e21..967b4763 100644 --- a/src/MiniExcel.OpenXml/Models/SheetDto.cs +++ b/src/MiniExcel.OpenXml/Models/SheetDto.cs @@ -2,10 +2,9 @@ internal class SheetDto { - internal string ID { get; set; } = $"R{Guid.NewGuid():N}"; - internal string? Name { get; set; } internal int SheetIdx { get; set; } + internal string Id => $"rSheetId{SheetIdx}"; + internal string? Name { get; set; } internal string Path => ExcelFileNames.Worksheet(SheetIdx); - internal string State { get; set; } } diff --git a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs index 579b2ade..25604ad5 100644 --- a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs +++ b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs @@ -302,7 +302,7 @@ private string GetFileValue(int rowIndex, int cellIndex, object value) Contents = bytes, RowIndex = rowIndex, CellIndex = cellIndex, - SheetId = _currentSheetIndex + SheetIndex = _currentSheetIndex }; if (format != ImageFormat.Unknown) @@ -380,7 +380,7 @@ private static string GetDimensionRef(int maxRowIndex, int maxColumnIndex) private string GetDrawingRelationshipXml(int sheetIndex) { var drawing = new StringBuilder(); - foreach (var image in _files.Where(w => w.IsImage && w.SheetId == sheetIndex)) + foreach (var image in _files.Where(w => w.IsImage && w.SheetIndex == sheetIndex)) { drawing.AppendLine(ExcelXml.ImageRelationship(image)); } @@ -395,7 +395,7 @@ private string GetDrawingXml(int sheetIndex) for (int fileIndex = 0; fileIndex < _files.Count; fileIndex++) { var file = _files[fileIndex]; - if (file.IsImage && file.SheetId == sheetIndex) + if (file.IsImage && file.SheetIndex == sheetIndex) { drawing.Append(ExcelXml.DrawingXml(file, fileIndex)); } @@ -426,16 +426,4 @@ private string GetDrawingXml(int sheetIndex) return (workbookContents.ToString(), workbookRelsContents.ToString(), sheetsRelsDict); } - - private string GetContentTypesXml() - { - var sb = new StringBuilder(ExcelXml.StartTypes); - foreach (var p in _zipContentsMap) - { - sb.Append(ExcelXml.ContentType(p.Value, p.Key)); - } - - sb.Append(ExcelXml.EndTypes); - return sb.ToString(); - } } diff --git a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs index 74505df2..4f36f42d 100644 --- a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.cs @@ -613,7 +613,7 @@ await CreateZipEntryAsync( [CreateSyncVersion] private async Task GenerateContentTypesAsync(CancellationToken cancellationToken) { - var contentTypes = GetContentTypesXml(); + var contentTypes = ExcelXml.ContentTypes(_zipContentsMap); await CreateZipEntryAsync(ExcelFileNames.ContentTypes, null, contentTypes, cancellationToken).ConfigureAwait(false); } diff --git a/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesAsyncTests.cs b/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesAsyncTests.cs index 64210668..abf556f6 100644 --- a/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesAsyncTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesAsyncTests.cs @@ -1271,4 +1271,46 @@ public async Task TestIssue951() // must not throw because of indexer await _excelTemplater.FillTemplateAsync(path.ToString(), templatePath, value); } + + [Fact] + public async Task TestIssue980() // deterministic file generation + { + using var path1 = AutoDeletingPath.Create(); + using var path2 = AutoDeletingPath.Create(); + + var value = new[] + { + new { Name = "Jack", CreateDate = new DateTime(2021, 01, 01), VIP = true, Points = 123 }, + new { Name = "John", CreateDate = new DateTime(2022, 02, 02), VIP = false, Points = 321 } + }; + + await _excelExporter.ExportAsync(path1.FilePath, value); + await Task.Delay(TimeSpan.FromSeconds(1)); + await _excelExporter.ExportAsync(path2.FilePath, value); + + await using (var fs1 = File.OpenRead(path1.FilePath)) + { + await using var fs2 = File.Open(path2.FilePath, FileMode.Open, FileAccess.ReadWrite); + +#if NET10_0_OR_GREATER + await using var zip1 = new ZipArchive(fs1); + await using var zip2 = new ZipArchive(fs2, ZipArchiveMode.Update); +#else + using var zip1 = new ZipArchive(fs1); + using var zip2 = new ZipArchive(fs2, ZipArchiveMode.Update); +#endif + + foreach (var entry in zip1.Entries) + { + if (zip2.GetEntry(entry.FullName) is { } e) + { + e.LastWriteTime = entry.LastWriteTime; + } + } + } + + var bytes1 = await File.ReadAllBytesAsync(path1.FilePath); + var bytes2 = await File.ReadAllBytesAsync(path2.FilePath); + Assert.True(bytes1.SequenceEqual(bytes2)); + } } diff --git a/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesTests.cs b/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesTests.cs index 49490d71..b6cb098d 100644 --- a/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesTests.cs @@ -1573,7 +1573,7 @@ public void TestIssue325() _excelExporter.Export(path.ToString(), value); var xml = SheetHelper.GetZipFileContent(path.ToString(), "xl/worksheets/_rels/sheet2.xml.rels"); - var cnt = Regex.Matches(xml, "Id=\"drawing2\"").Count; + var cnt = Regex.Matches(xml, "Id=\"rDrawing2\"").Count; Assert.True(cnt == 1); } @@ -2992,4 +2992,41 @@ public void TestIssue951() // must not throw _excelTemplater.FillTemplate(path.ToString(), templatePath, value); } + + [Fact] + public void TestIssue980() // deterministic file generation + { + using var path1 = AutoDeletingPath.Create(); + using var path2 = AutoDeletingPath.Create(); + + var value = new[] + { + new { Name = "Jack", CreateDate = new DateTime(2021, 01, 01), VIP = true, Points = 123 }, + new { Name = "John", CreateDate = new DateTime(2022, 02, 02), VIP = false, Points = 321 } + }; + + _excelExporter.Export(path1.FilePath, value); + Thread.Sleep(TimeSpan.FromSeconds(1)); + _excelExporter.Export(path2.FilePath, value); + + using (var fs1 = File.OpenRead(path1.FilePath)) + { + using var fs2 = File.Open(path2.FilePath, FileMode.Open, FileAccess.ReadWrite); + + using var zip1 = new ZipArchive(fs1); + using var zip2 = new ZipArchive(fs2, ZipArchiveMode.Update); + + foreach (var entry in zip1.Entries) + { + if (zip2.GetEntry(entry.FullName) is { } e) + { + e.LastWriteTime = entry.LastWriteTime; + } + } + } + + var bytes1 = File.ReadAllBytes(path1.FilePath); + var bytes2 = File.ReadAllBytes(path2.FilePath); + Assert.True(bytes1.SequenceEqual(bytes2)); + } } From 0231b09b1337cc67b7080afe4d61786a30f7d02d Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sun, 5 Jul 2026 23:09:39 +0200 Subject: [PATCH 3/3] bugfixes --- src/MiniExcel.OpenXml/Constants/ExcelXml.cs | 4 ++-- src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs index 603e6af3..511ab860 100644 --- a/src/MiniExcel.OpenXml/Constants/ExcelXml.cs +++ b/src/MiniExcel.OpenXml/Constants/ExcelXml.cs @@ -23,7 +23,7 @@ internal static string DefaultWorkbookXmlRels(string sheets) => XmlHelper.Minify {sheets} - + """); @@ -97,7 +97,7 @@ internal static string ContentTypes(Dictionary contentTypes) """); - foreach (var (name, type) in contentTypes) + foreach (var (name, type) in contentTypes.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase)) sb.AppendLine($""""""); sb.Append(""); diff --git a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs index 25604ad5..29e3aa00 100644 --- a/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs +++ b/src/MiniExcel.OpenXml/Writer/OpenXmlWriter.XmlGeneration.cs @@ -408,7 +408,6 @@ private string GetDrawingXml(int sheetIndex) { var workbookContents = new StringBuilder(); var workbookRelsContents = new StringBuilder(); - var sheetsRelsContents = new StringBuilder(); var sheetsRelsDict = new Dictionary(); var sheetId = 0; @@ -418,10 +417,9 @@ private string GetDrawingXml(int sheetIndex) workbookContents.AppendLine(ExcelXml.Sheet(sheetDto, sheetId)); workbookRelsContents.AppendLine(ExcelXml.WorksheetRelationship(sheetDto)); - sheetsRelsContents.AppendLine(ExcelXml.DrawingRelationship(sheetId)); //TODO: support multiple drawing - sheetsRelsDict.Add(sheetDto.SheetIdx, sheetsRelsContents.ToString()); + sheetsRelsDict.Add(sheetDto.SheetIdx, ExcelXml.DrawingRelationship(sheetId)); } return (workbookContents.ToString(), workbookRelsContents.ToString(), sheetsRelsDict);