From 536c19679cfcca037714862202b72dfd8c7a13a3 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sun, 26 Apr 2026 00:08:54 +0200 Subject: [PATCH 1/5] Renaming MappingTemplater's ApplyTemplateAsync method overloads to FillTemplateAsync for keeping consistency with the OpenXmlTemplater methods' names Previous method signatures were marked with ObsoleteAttribute to prevent current preview version builds to break, and will be subsequently removed. --- .../TemplateExcelBenchmark.cs | 2 +- src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs | 45 ++++++++++++++ .../FluentMapping/Api/MappingExporter.cs | 10 +++- .../FluentMapping/Api/MappingImporter.cs | 14 ++++- .../FluentMapping/Api/MappingTemplater.cs | 60 ++++++++++++++++--- .../MiniExcelMappingTemplateTests.cs | 14 ++--- 6 files changed, 123 insertions(+), 22 deletions(-) diff --git a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/TemplateExcelBenchmark.cs b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/TemplateExcelBenchmark.cs index 373db436..b6aab13c 100644 --- a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/TemplateExcelBenchmark.cs +++ b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/TemplateExcelBenchmark.cs @@ -96,6 +96,6 @@ public void MiniExcel_Mapping_Template_Generate_Test() Department = "HR" }); - _mappingTemplater.ApplyTemplate(outputPath.FilePath, templatePath.FilePath, employees); + _mappingTemplater.FillTemplate(outputPath.FilePath, templatePath.FilePath, employees); } } \ No newline at end of file diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs b/src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs index e6fcc435..3dabd0e0 100644 --- a/src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs +++ b/src/MiniExcel.OpenXml/Api/OpenXmlTemplater.cs @@ -132,4 +132,49 @@ private static OpenXmlTemplate GetOpenXmlTemplate(Stream stream, OpenXmlConfigur } #endregion + + #region Obsolete + + [CreateSyncVersion, Obsolete("Please use FillTemplate or FillTemplateAsync instead")] + public Task ApplyTemplateAsync(string path, string templatePath, object value, bool overwriteFile = false, + OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) + { + return FillTemplateAsync(path, templatePath, value, overwriteFile, configuration, cancellationToken); + } + + [CreateSyncVersion, Obsolete("Please use FillTemplate or FillTemplateAsync instead")] + public Task ApplyTemplateAsync(string path, Stream templateStream, object value, bool overwriteFile = false, + OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) + { + return FillTemplateAsync(path, templateStream, value, overwriteFile, configuration, cancellationToken); + } + + [CreateSyncVersion, Obsolete("Please use FillTemplate or FillTemplateAsync instead")] + public Task ApplyTemplateAsync(Stream stream, string templatePath, object value, + OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) + { + return FillTemplateAsync(stream, templatePath, value, configuration, cancellationToken); + } + + [CreateSyncVersion, Obsolete("Please use FillTemplate or FillTemplateAsync instead")] + public Task ApplyTemplateAsync(Stream stream, Stream templateStream, object value, + OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) + { + return FillTemplateAsync(stream, templateStream, value, configuration, cancellationToken); + } + + [CreateSyncVersion, Obsolete("Please use FillTemplate or FillTemplateAsync instead")] + public Task ApplyTemplateAsync(string path, byte[] templateBytes, object value, bool overwriteFile = false, + OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) + { + return FillTemplateAsync(path, templateBytes, value, overwriteFile, configuration, cancellationToken); + } + + [CreateSyncVersion, Obsolete("Please use FillTemplate or FillTemplateAsync instead")] + public Task ApplyTemplateAsync(Stream stream, byte[] templateBytes, object value, + OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) + { + return FillTemplateAsync(stream, templateBytes, value, configuration, cancellationToken); + } + #endregion } \ No newline at end of file diff --git a/src/MiniExcel.OpenXml/FluentMapping/Api/MappingExporter.cs b/src/MiniExcel.OpenXml/FluentMapping/Api/MappingExporter.cs index d4dc10ea..5cfed4c6 100644 --- a/src/MiniExcel.OpenXml/FluentMapping/Api/MappingExporter.cs +++ b/src/MiniExcel.OpenXml/FluentMapping/Api/MappingExporter.cs @@ -1,5 +1,3 @@ -using Zomp.SyncMethodGenerator; - namespace MiniExcelLib.OpenXml.FluentMapping.Api; public sealed partial class MappingExporter @@ -20,8 +18,14 @@ public MappingExporter(MappingRegistry registry) public async Task ExportAsync(string path, IEnumerable? values, bool overwriteFile = false, CancellationToken cancellationToken = default) where T : class { var filePath = path.EndsWith(".xlsx", StringComparison.InvariantCultureIgnoreCase) ? path : $"{path}.xlsx" ; - + +#if NET8_0_OR_GREATER + var stream = overwriteFile ? File.Create(filePath) : new FileStream(filePath, FileMode.CreateNew); + await using var disposableStream = stream.ConfigureAwait(false); +#else using var stream = overwriteFile ? File.Create(filePath) : new FileStream(filePath, FileMode.CreateNew); +#endif + await ExportAsync(stream, values, cancellationToken).ConfigureAwait(false); } diff --git a/src/MiniExcel.OpenXml/FluentMapping/Api/MappingImporter.cs b/src/MiniExcel.OpenXml/FluentMapping/Api/MappingImporter.cs index f0d37781..d6427048 100644 --- a/src/MiniExcel.OpenXml/FluentMapping/Api/MappingImporter.cs +++ b/src/MiniExcel.OpenXml/FluentMapping/Api/MappingImporter.cs @@ -1,6 +1,3 @@ -using System.Runtime.CompilerServices; -using Zomp.SyncMethodGenerator; - namespace MiniExcelLib.OpenXml.FluentMapping.Api; public sealed partial class MappingImporter() @@ -15,7 +12,13 @@ public MappingImporter(MappingRegistry registry) : this() [CreateSyncVersion] public async IAsyncEnumerable QueryAsync(string path, [EnumeratorCancellation] CancellationToken cancellationToken = default) where T : class, new() { + #if NET8_0_OR_GREATER + var stream = File.OpenRead(path); + await using var disposableStream = stream.ConfigureAwait(false); + #else using var stream = File.OpenRead(path); + #endif + await foreach (var item in QueryAsync(stream, cancellationToken).ConfigureAwait(false)) yield return item; } @@ -36,7 +39,12 @@ public MappingImporter(MappingRegistry registry) : this() [CreateSyncVersion] public async Task QuerySingleAsync(string path, CancellationToken cancellationToken = default) where T : class, new() { +#if NET8_0_OR_GREATER + var stream = File.OpenRead(path); + await using var disposableStream = stream.ConfigureAwait(false); +#else using var stream = File.OpenRead(path); +#endif return await QuerySingleAsync(stream, cancellationToken).ConfigureAwait(false); } diff --git a/src/MiniExcel.OpenXml/FluentMapping/Api/MappingTemplater.cs b/src/MiniExcel.OpenXml/FluentMapping/Api/MappingTemplater.cs index f185b088..248f9702 100644 --- a/src/MiniExcel.OpenXml/FluentMapping/Api/MappingTemplater.cs +++ b/src/MiniExcel.OpenXml/FluentMapping/Api/MappingTemplater.cs @@ -1,5 +1,3 @@ -using Zomp.SyncMethodGenerator; - namespace MiniExcelLib.OpenXml.FluentMapping.Api; public sealed partial class MappingTemplater() @@ -12,7 +10,7 @@ public MappingTemplater(MappingRegistry registry) : this() } [CreateSyncVersion] - public async Task ApplyTemplateAsync( + public async Task FillTemplateAsync( string? outputPath, string? templatePath, IEnumerable? values, @@ -25,13 +23,21 @@ public async Task ApplyTemplateAsync( if (values is null) throw new ArgumentNullException(nameof(values)); +#if NET8_0_OR_GREATER + var outputStream = File.Create(outputPath); + await using var disposableOutputStream = outputStream.ConfigureAwait(false); + + var templateStream = File.OpenRead(templatePath); + await using var disposableTemplateStream = templateStream.ConfigureAwait(false); +#else using var outputStream = File.Create(outputPath); using var templateStream = File.OpenRead(templatePath); - await ApplyTemplateAsync(outputStream, templateStream, values, cancellationToken).ConfigureAwait(false); +#endif + await FillTemplateAsync(outputStream, templateStream, values, cancellationToken).ConfigureAwait(false); } [CreateSyncVersion] - public async Task ApplyTemplateAsync( + public async Task FillTemplateAsync( Stream? outputStream, Stream? templateStream, IEnumerable? values, @@ -54,7 +60,7 @@ await MappingTemplateApplicator.ApplyTemplateAsync( } [CreateSyncVersion] - public async Task ApplyTemplateAsync( + public async Task FillTemplateAsync( Stream? outputStream, byte[]? templateBytes, IEnumerable? values, @@ -67,7 +73,45 @@ public async Task ApplyTemplateAsync( if (values is null) throw new ArgumentNullException(nameof(values)); +#if NET8_0_OR_GREATER + var templateStream = new MemoryStream(templateBytes); + await using var disposableTemplateStream = templateStream.ConfigureAwait(false); +#else using var templateStream = new MemoryStream(templateBytes); - await ApplyTemplateAsync(outputStream, templateStream, values, cancellationToken).ConfigureAwait(false); +#endif + await FillTemplateAsync(outputStream, templateStream, values, cancellationToken).ConfigureAwait(false); } -} \ No newline at end of file + +#region Obsolete +[CreateSyncVersion, Obsolete("Please use FillTemplate or FillTemplateAsync instead.")] +public Task ApplyTemplateAsync( + string? outputPath, + string? templatePath, + IEnumerable? values, + CancellationToken cancellationToken = default) where T : class +{ + return FillTemplateAsync(outputPath, templatePath, values, cancellationToken); +} + +[CreateSyncVersion, Obsolete("Please use FillTemplate or FillTemplateAsync instead.")] +public Task ApplyTemplateAsync( + Stream? outputStream, + Stream? templateStream, + IEnumerable? values, + CancellationToken cancellationToken = default) where T : class +{ + return FillTemplateAsync(outputStream, templateStream, values, cancellationToken); +} + + +[CreateSyncVersion, Obsolete("Please use FillTemplate or FillTemplateAsync instead.")] +public Task ApplyTemplateAsync( + Stream? outputStream, + byte[]? templateBytes, + IEnumerable? values, + CancellationToken cancellationToken = default) where T : class +{ + return FillTemplateAsync(outputStream, templateBytes, values, cancellationToken); +} +#endregion +} diff --git a/tests/MiniExcel.OpenXml.Tests/FluentMapping/MiniExcelMappingTemplateTests.cs b/tests/MiniExcel.OpenXml.Tests/FluentMapping/MiniExcelMappingTemplateTests.cs index d0084c2d..9d778d5c 100644 --- a/tests/MiniExcel.OpenXml.Tests/FluentMapping/MiniExcelMappingTemplateTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/FluentMapping/MiniExcelMappingTemplateTests.cs @@ -71,7 +71,7 @@ public async Task BasicTemplateTest() using var outputPath = AutoDeletingPath.Create(); var templater = MiniExcel.Templaters.GetMappingTemplater(registry); - await templater.ApplyTemplateAsync(outputPath.ToString(), templatePath.ToString(), [data]); + await templater.FillTemplateAsync(outputPath.ToString(), templatePath.ToString(), [data]); var rows = _importer.Query(outputPath.ToString(), useHeaderRow: false).ToList(); @@ -130,7 +130,7 @@ public async Task StreamOverloadTest() using (var templateStream = File.OpenRead(templatePath.ToString())) { var templater = MiniExcel.Templaters.GetMappingTemplater(registry); - await templater.ApplyTemplateAsync(outputStream, templateStream, [data]); + await templater.FillTemplateAsync(outputStream, templateStream, [data]); } var rows = _importer.Query(outputPath.ToString(), useHeaderRow: false).ToList(); @@ -171,7 +171,7 @@ public async Task ByteArrayOverloadTest() using (var outputStream = File.Create(outputPath.ToString())) { var templater = MiniExcel.Templaters.GetMappingTemplater(registry); - await templater.ApplyTemplateAsync(outputStream, templateBytes, [data]); + await templater.FillTemplateAsync(outputStream, templateBytes, [data]); } var rows = _importer.Query(outputPath.ToString(), useHeaderRow: false).ToList(); @@ -236,7 +236,7 @@ public async Task CollectionTemplateTest() using var outputPath = AutoDeletingPath.Create(); var templater = MiniExcel.Templaters.GetMappingTemplater(registry); - await templater.ApplyTemplateAsync(outputPath.ToString(), templatePath.ToString(), [dept]); + await templater.FillTemplateAsync(outputPath.ToString(), templatePath.ToString(), [dept]); var rows = _importer.Query(outputPath.ToString(), useHeaderRow: false).ToList(); @@ -284,7 +284,7 @@ public async Task EmptyDataTest() using var outputPath = AutoDeletingPath.Create(); var templater = MiniExcel.Templaters.GetMappingTemplater(registry); - await templater.ApplyTemplateAsync(outputPath.ToString(), templatePath.ToString(), Array.Empty()); + await templater.FillTemplateAsync(outputPath.ToString(), templatePath.ToString(), Array.Empty()); var rows = _importer.Query(outputPath.ToString(), useHeaderRow: false).ToList(); Assert.Equal(3, rows.Count); // Column headers + our headers + empty data row @@ -328,7 +328,7 @@ public async Task NullValuesTest() // Apply template using var outputPath = AutoDeletingPath.Create(); var templater = MiniExcel.Templaters.GetMappingTemplater(registry); - await templater.ApplyTemplateAsync(outputPath.ToString(), templatePath.ToString(), [data]); + await templater.FillTemplateAsync(outputPath.ToString(), templatePath.ToString(), [data]); // Verify null handling // Verify - use useHeaderRow=false since we want to see all rows @@ -373,7 +373,7 @@ public async Task MultipleItemsTest() // Apply template using var outputPath = AutoDeletingPath.Create(); var templater = MiniExcel.Templaters.GetMappingTemplater(registry); - await templater.ApplyTemplateAsync(outputPath.ToString(), templatePath.ToString(), data); + await templater.FillTemplateAsync(outputPath.ToString(), templatePath.ToString(), data); // Verify - should only update first item since mapping is for specific cells // Verify - use useHeaderRow=false since we want to see all rows From f62df114ac8a54a620ba58143b744de66acb5ce9 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sun, 26 Apr 2026 00:16:05 +0200 Subject: [PATCH 2/5] Updating packages and removing Newtonsoft.Json from the MiniExcel.OpenXml.Tests test project, also deleting pointless json serialization from some tests --- .../MiniExcel.Benchmarks.csproj | 8 +- src/Directory.Packages.props | 6 +- .../MiniExcel.Csv.Tests.csproj | 2 +- .../MiniExcel.OpenXml.Tests.csproj | 10 +- .../MiniExcelIssueAsyncTests.cs | 171 ++++++++------- .../MiniExcelIssueTests.cs | 196 +++++++++--------- .../Utils/EpplusLicense.cs | 6 +- .../MiniExcel.Tests.Common.csproj | 2 +- tests/data/xlsx/TestIssue157.xlsx | Bin 2465 -> 5997 bytes 9 files changed, 197 insertions(+), 204 deletions(-) diff --git a/benchmarks/MiniExcel.Benchmarks/MiniExcel.Benchmarks.csproj b/benchmarks/MiniExcel.Benchmarks/MiniExcel.Benchmarks.csproj index fb04cf56..c5d151fb 100644 --- a/benchmarks/MiniExcel.Benchmarks/MiniExcel.Benchmarks.csproj +++ b/benchmarks/MiniExcel.Benchmarks/MiniExcel.Benchmarks.csproj @@ -11,12 +11,12 @@ - + - - - + + + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index b152565c..d3a0242d 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -2,10 +2,12 @@ + - - + + + diff --git a/tests/MiniExcel.Csv.Tests/MiniExcel.Csv.Tests.csproj b/tests/MiniExcel.Csv.Tests/MiniExcel.Csv.Tests.csproj index 7e818ea0..e3901b2c 100644 --- a/tests/MiniExcel.Csv.Tests/MiniExcel.Csv.Tests.csproj +++ b/tests/MiniExcel.Csv.Tests/MiniExcel.Csv.Tests.csproj @@ -14,7 +14,7 @@ - + diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj b/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj index ffe2eebc..a31d2f89 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcel.OpenXml.Tests.csproj @@ -15,11 +15,10 @@ - - - - - + + + + @@ -53,7 +52,6 @@ - diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs index 3c11664d..ec9f7891 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs @@ -181,14 +181,16 @@ public async Task Issue132() { using var path = AutoDeletingPath.Create(); - var value = JsonConvert.DeserializeObject( - JsonConvert.SerializeObject(new[] - { - new { Name ="Jack", Age=25,InDate=new DateTime(2021,01,03)}, - new { Name ="Henry", Age=36,InDate=new DateTime(2020,05,03)}, - }) - ); - var rowsWritten = await _excelExporter.ExportAsync(path.ToString(), value); + + var dt = new DataTable(); + dt.Columns.Add("Name"); + dt.Columns.Add("Age"); + dt.Columns.Add("Date"); + + dt.Rows.Add("Jack", 25, new DateTime(2021, 01, 03)); + dt.Rows.Add("Henry", 36, new DateTime(2021, 01, 03)); + + var rowsWritten = await _excelExporter.ExportAsync(path.ToString(), dt); Assert.Single(rowsWritten); Assert.Equal(2, rowsWritten[0]); @@ -203,15 +205,22 @@ public async Task Issue235() { using var file = AutoDeletingPath.Create(); var path = file.ToString(); - var sheets = new DataSet(); - var users = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(new[] { new { Name = "Jack", Age = 25 }, new { Name = "Mike", Age = 44 } })); - users.TableName = "users"; + var users = new DataTable { TableName = "users" }; + users.Columns.Add("Name", typeof(string)); + users.Columns.Add("Age", typeof(int)); + users.Rows.Add("Jack", 25); + users.Rows.Add("Mike", 44); + + var departments = new DataTable { TableName = "departments" }; + departments.Columns.Add("ID"); + departments.Columns.Add("Name"); + departments.Rows.Add("01", "HR"); + departments.Rows.Add("02", "IT"); + + DataSet sheets = new(); sheets.Tables.Add(users); - - var department = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(new[] { new { ID = "01", Name = "HR" }, new { ID = "02", Name = "IT" } })); - department.TableName = "department"; - sheets.Tables.Add(department); + sheets.Tables.Add(departments); var rowsWritten = await _excelExporter.ExportAsync(path, sheets); Assert.Equal(2, rowsWritten.Length); @@ -219,24 +228,19 @@ public async Task Issue235() var sheetNames = await _excelImporter.GetSheetNamesAsync(path); Assert.Equal("users", sheetNames[0]); - Assert.Equal("department", sheetNames[1]); - - { - var q = _excelImporter.QueryAsync(path, true, sheetName: "users").ToBlockingEnumerable(); - var rows = q.ToList(); - Assert.Equal("Jack", rows[0].Name); - Assert.Equal(25, rows[0].Age); - Assert.Equal("Mike", rows[1].Name); - Assert.Equal(44, rows[1].Age); - } - { - var q = _excelImporter.QueryAsync(path, true, sheetName: "department").ToBlockingEnumerable(); - var rows = q.ToList(); - Assert.Equal("01", rows[0].ID); - Assert.Equal("HR", rows[0].Name); - Assert.Equal("02", rows[1].ID); - Assert.Equal("IT", rows[1].Name); - } + Assert.Equal("departments", sheetNames[1]); + + var rows1 = await _excelImporter.QueryAsync(path, true, sheetName: "users").ToListAsync(); + Assert.Equal("Jack", rows1[0].Name); + Assert.Equal(25, rows1[0].Age); + Assert.Equal("Mike", rows1[1].Name); + Assert.Equal(44, rows1[1].Age); + + var rows2 = await _excelImporter.QueryAsync(path, true, sheetName: "departments").ToListAsync(); + Assert.Equal("01", rows2[0].ID); + Assert.Equal("HR", rows2[0].Name); + Assert.Equal("02", rows2[1].ID); + Assert.Equal("IT", rows2[1].Name); } /// @@ -246,11 +250,8 @@ public async Task Issue235() public async Task Issue233() { var path = PathHelper.GetFile("xlsx/TestIssue233.xlsx"); - var dt = await _excelImporter.QueryAsDataTableAsync(path); - - var rows = dt.Rows; Assert.Equal(0.55, rows[0]["Size"]); @@ -1131,59 +1132,49 @@ public async Task Issue157() { using var file = AutoDeletingPath.Create(); var path = file.ToString(); - - _output.WriteLine("==== SaveAs by strongly type ===="); - var input = JsonConvert.DeserializeObject>( - """ - [ - { - "ID":"78de23d2-dcb6-bd3d-ec67-c112bbc322a2", - "Name":"Wade", - "BoD":"2020-09-27T00:00:00", - "Age":5019, - "VIP":false, - "Points":5019.12, - "IgnoredProperty":null - }, - { - "ID":"20d3bfce-27c3-ad3e-4f70-35c81c7e8e45", - "Name":"Felix", - "BoD":"2020-10-25T00:00:00", - "Age":7028, - "VIP":true, - "Points":7028.46, - "IgnoredProperty":null - }, - { - "ID":"52013bf0-9aeb-48e6-e5f5-e9500afb034f", - "Name":"Phelan", - "BoD":"2021-10-04T00:00:00", - "Age":3836, - "VIP":true, - "Points":3835.7, - "IgnoredProperty":null - }, - { - "ID":"3b97b87c-7afe-664f-1af5-6914d313ae25", - "Name":"Samuel", - "BoD":"2020-06-21T00:00:00", - "Age":9352, - "VIP":false, - "Points":9351.71, - "IgnoredProperty":null - }, - { - "ID":"9a989c43-d55f-5306-0d2f-0fbafae135bb", - "Name":"Raymond", - "BoD":"2021-07-12T00:00:00", - "Age":8210, - "VIP":true, - "Points":8209.76, - "IgnoredProperty":null - } - ] - """); - var rowsWritten = await _excelExporter.ExportAsync(path, input); + + List data = + [ + new() + { + ID = new Guid("78de23d2-dcb6-bd3d-ec67-c112bbc322a2"), + Name = "Wade", + BoD = new DateTime(2020, 9, 27), + Points = 5019.12m + }, + new() + { + ID = new Guid("20d3bfce-27c3-ad3e-4f70-35c81c7e8e45"), + Name = "Felix", + BoD = new DateTime(2020, 10, 25), + Points = 7028.46m + }, + new() + { + ID = new Guid("52013bf0-9aeb-48e6-e5f5-e9500afb034f"), + Name = "Phelan", + BoD = new DateTime(2020, 10, 25), + Points = 3835.7m, + VIP = true + }, + new() + { + ID = new Guid("3b97b87c-7afe-664f-1af5-6914d313ae25"), + Name = "Samuel", + BoD = new DateTime(2020, 6, 21), + Points = 9351.71m + }, + new() + { + ID = new Guid("9a989c43-d55f-5306-0d2f-0fbafae135bb"), + Name = "Raymond", + BoD = new DateTime(2021, 7, 12), + Points = 8209.76m, + VIP = true + } + ]; + + var rowsWritten = await _excelExporter.ExportAsync(path, data); Assert.Single(rowsWritten); Assert.Equal(5, rowsWritten[0]); @@ -1221,7 +1212,7 @@ public async Task Issue157() Assert.Equal("Wade", rows[0].Name); Assert.Equal(DateTime.ParseExact("27/09/2020", "dd/MM/yyyy", CultureInfo.InvariantCulture), rows[0].BoD); Assert.False(rows[0].VIP); - Assert.Equal(5019m, rows[0].Points); + Assert.Equal(5019.12m, rows[0].Points); Assert.Equal(1, rows[0].IgnoredProperty); } } diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs index 15cbfb65..f79fb95c 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs @@ -17,6 +17,10 @@ public class MiniExcelIssueTests(ITestOutputHelper output) private readonly OpenXmlExporter _excelExporter = MiniExcel.Exporters.GetOpenXmlExporter(); private readonly OpenXmlTemplater _excelTemplater = MiniExcel.Templaters.GetOpenXmlTemplater(); + static MiniExcelIssueTests() + { + EpplusLicence.SetContext(); + } /// /// https://github.com/mini-software/MiniExcel/issues/549 @@ -207,34 +211,32 @@ public void TestIssue370() { DynamicColumns = [ - new DynamicExcelColumn("id") { Ignore=true }, - new DynamicExcelColumn("name") { Index=1,Width=10 }, - new DynamicExcelColumn("createdate") { Index=0, Format="yyyy-MM-dd", Width=15 }, - new DynamicExcelColumn("point") { Index=2, Name="Account Point" } + new DynamicExcelColumn("Id") { Ignore = true }, + new DynamicExcelColumn("Name") { Index = 1,Width = 10 }, + new DynamicExcelColumn("Date") { Index = 0, Format="yyyy-MM-dd", Width = 15 }, + new DynamicExcelColumn("Point") { Index = 2, Name = "Account Point" } ] }; using var path = AutoDeletingPath.Create(); - var json = JsonConvert.SerializeObject(new[] - { - new + List> value = + [ + new() { - id = 1, - name = "Jack", - createdate = new DateTime(2022, 04, 12), - point = 123.456 + ["Id"] = 1, + ["Name"] = "Jack", + ["Date"] = new DateTime(2022, 04, 12), + ["Point"] = 123.456 } - }, Formatting.Indented); - - var value = JsonConvert.DeserializeObject>>(json); + ]; _excelExporter.Export(path.ToString(), value, configuration: config); var rows = _excelImporter.Query(path.ToString()).ToList(); - Assert.Equal("createdate", rows[0].A); + Assert.Equal("Date", rows[0].A); Assert.Equal(new DateTime(2022, 04, 12), rows[1].A); - Assert.Equal("name", rows[0].B); + Assert.Equal("Name", rows[0].B); Assert.Equal("Jack", rows[1].B); Assert.Equal("Account Point", rows[0].C); - Assert.Equal(123.456, rows[1].C); + Assert.Equal(123.456, rows[1].C); } [Fact] @@ -1526,14 +1528,16 @@ public void Issue132() { using var path = AutoDeletingPath.Create(); - var value = JsonConvert.DeserializeObject( - JsonConvert.SerializeObject(new[] - { - new { name = "Jack", Age = 25, InDate = new DateTime(2021,01,03)}, - new { name = "Henry", Age = 36, InDate = new DateTime(2020,05,03)}, - }) - ); - _excelExporter.Export(path.ToString(), value); + + var dt = new DataTable(); + dt.Columns.Add("Name"); + dt.Columns.Add("Age"); + dt.Columns.Add("Date"); + + dt.Rows.Add("Jack", 25, new DateTime(2021, 01, 03)); + dt.Rows.Add("Henry", 36, new DateTime(2021, 01, 03)); + + _excelExporter.Export(path.ToString(), dt); } } @@ -1544,24 +1548,22 @@ public void Issue132() public void Issue235() { using var path = AutoDeletingPath.Create(); + + var users = new DataTable { TableName = "users" }; + users.Columns.Add("Name", typeof(string)); + users.Columns.Add("Age", typeof(int)); + users.Rows.Add("Jack", 25); + users.Rows.Add("Mike", 44); + + var departments = new DataTable { TableName = "departments" }; + departments.Columns.Add("ID"); + departments.Columns.Add("Name"); + departments.Rows.Add("01", "HR"); + departments.Rows.Add("02", "IT"); DataSet dataSet = new(); - var users = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(new[] - { - new { Name = "Jack", Age = 25 }, - new { Name = "Mike", Age = 44 } - })); - users!.TableName = "users"; - - var department = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(new[] - { - new { ID = "01", Name = "HR" }, - new { ID = "02", Name = "IT" } - })); - department!.TableName = "department"; - dataSet.Tables.Add(users); - dataSet.Tables.Add(department); + dataSet.Tables.Add(departments); var rowsWritten = _excelExporter.Export(path.ToString(), dataSet); Assert.Equal(2, rowsWritten.Length); @@ -1569,22 +1571,19 @@ public void Issue235() var sheetNames = _excelImporter.GetSheetNames(path.ToString()); Assert.Equal("users", sheetNames[0]); - Assert.Equal("department", sheetNames[1]); + Assert.Equal("departments", sheetNames[1]); - { - var rows = _excelImporter.Query(path.ToString(), true, sheetName: "users").ToList(); - Assert.Equal("Jack", rows[0].Name); - Assert.Equal(25, rows[0].Age); - Assert.Equal("Mike", rows[1].Name); - Assert.Equal(44, rows[1].Age); - } - { - var rows = _excelImporter.Query(path.ToString(), true, sheetName: "department").ToList(); - Assert.Equal("01", rows[0].ID); - Assert.Equal("HR", rows[0].Name); - Assert.Equal("02", rows[1].ID); - Assert.Equal("IT", rows[1].Name); - } + var rows1 = _excelImporter.Query(path.ToString(), true, sheetName: "users").ToList(); + Assert.Equal("Jack", rows1[0].Name); + Assert.Equal(25, rows1[0].Age); + Assert.Equal("Mike", rows1[1].Name); + Assert.Equal(44, rows1[1].Age); + + var rows2 = _excelImporter.Query(path.ToString(), true, sheetName: "departments").ToList(); + Assert.Equal("01", rows2[0].ID); + Assert.Equal("HR", rows2[0].Name); + Assert.Equal("02", rows2[1].ID); + Assert.Equal("IT", rows2[1].Name); } /// @@ -2355,44 +2354,49 @@ public void Issue157() { using var file = AutoDeletingPath.Create(); var path = file.ToString(); - _output.WriteLine("==== SaveAs by strongly type ===="); - - var input = JsonConvert.DeserializeObject>( - """ - [ - { - "ID":"78de23d2-dcb6-bd3d-ec67-c112bbc322a2", - "Name":"Wade","BoD":"2020-09-27T00:00:00", - "Age":5019,"VIP":false,"Points":5019.12, - "IgnoredProperty":null - }, - { - "ID":"20d3bfce-27c3-ad3e-4f70-35c81c7e8e45", - "Name":"Felix","BoD":"2020-10-25T00:00:00", - "Age":7028,"VIP":true,"Points":7028.46, - "IgnoredProperty":null - }, - { - "ID":"52013bf0-9aeb-48e6-e5f5-e9500afb034f", - "Name":"Phelan","BoD":"2021-10-04T00:00:00", - "Age":3836,"VIP":true,"Points":3835.7, - "IgnoredProperty":null - }, - { - "ID":"3b97b87c-7afe-664f-1af5-6914d313ae25", - "Name":"Samuel","BoD":"2020-06-21T00:00:00", - "Age":9352,"VIP":false,"Points":9351.71, - "IgnoredProperty":null - }, - { - "ID":"9a989c43-d55f-5306-0d2f-0fbafae135bb", - "Name":"Raymond","BoD":"2021-07-12T00:00:00", - "Age":8210,"VIP":true,"Points":8209.76, - "IgnoredProperty":null - } - ] - """); - _excelExporter.Export(path, input); + + List data = + [ + new() + { + ID = new Guid("78de23d2-dcb6-bd3d-ec67-c112bbc322a2"), + Name = "Wade", + BoD = new DateTime(2020, 9, 27), + Points = 5019.12m + }, + new() + { + ID = new Guid("20d3bfce-27c3-ad3e-4f70-35c81c7e8e45"), + Name = "Felix", + BoD = new DateTime(2020, 10, 25), + Points = 7028.46m + }, + new() + { + ID = new Guid("52013bf0-9aeb-48e6-e5f5-e9500afb034f"), + Name = "Phelan", + BoD = new DateTime(2020, 10, 25), + Points = 3835.7m, + VIP = true + }, + new() + { + ID = new Guid("3b97b87c-7afe-664f-1af5-6914d313ae25"), + Name = "Samuel", + BoD = new DateTime(2020, 6, 21), + Points = 9351.71m + }, + new() + { + ID = new Guid("9a989c43-d55f-5306-0d2f-0fbafae135bb"), + Name = "Raymond", + BoD = new DateTime(2021, 7, 12), + Points = 8209.76m, + VIP = true + } + ]; + + _excelExporter.Export(path, data); var rows = _excelImporter.Query(path, sheetName: "Sheet1").ToList(); Assert.Equal(6, rows.Count); @@ -2422,11 +2426,11 @@ public void Issue157() var rows = _excelImporter.Query(path, sheetName: "Sheet1").ToList(); Assert.Equal(5, rows.Count); - Assert.Equal(Guid.Parse("78DE23D2-DCB6-BD3D-EC67-C112BBC322A2"), rows[0].ID); + Assert.Equal(new Guid("78DE23D2-DCB6-BD3D-EC67-C112BBC322A2"), rows[0].ID); Assert.Equal("Wade", rows[0].Name); - Assert.Equal(DateTime.ParseExact("27/09/2020", "dd/MM/yyyy", CultureInfo.InvariantCulture), rows[0].BoD); + Assert.Equal(new DateTime(2020,9,27), rows[0].BoD); Assert.False(rows[0].VIP); - Assert.Equal(5019m, rows[0].Points); + Assert.Equal(5019.12m, rows[0].Points); Assert.Equal(1, rows[0].IgnoredProperty); } } diff --git a/tests/MiniExcel.OpenXml.Tests/Utils/EpplusLicense.cs b/tests/MiniExcel.OpenXml.Tests/Utils/EpplusLicense.cs index 37b8dcc8..e61a8f0d 100644 --- a/tests/MiniExcel.OpenXml.Tests/Utils/EpplusLicense.cs +++ b/tests/MiniExcel.OpenXml.Tests/Utils/EpplusLicense.cs @@ -2,8 +2,6 @@ namespace MiniExcelLib.OpenXml.Tests.Utils; internal static class EpplusLicence { - static EpplusLicence() - { - ExcelPackage.LicenseContext = LicenseContext.NonCommercial; - } + internal static void SetContext() + => ExcelPackage.LicenseContext = LicenseContext.NonCommercial; } \ No newline at end of file diff --git a/tests/MiniExcel.Tests.Common/MiniExcel.Tests.Common.csproj b/tests/MiniExcel.Tests.Common/MiniExcel.Tests.Common.csproj index cafefd80..67a7fb1a 100644 --- a/tests/MiniExcel.Tests.Common/MiniExcel.Tests.Common.csproj +++ b/tests/MiniExcel.Tests.Common/MiniExcel.Tests.Common.csproj @@ -9,7 +9,7 @@ - + diff --git a/tests/data/xlsx/TestIssue157.xlsx b/tests/data/xlsx/TestIssue157.xlsx index 11a782b18e919a724774d061b527d7e70a2be139..07953f0c777a02b4191a65241da63c7dcd1806f1 100644 GIT binary patch literal 5997 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 literal 2465 zcmZ{m2{e@JAIB%zCl^_ANw;vbO=gs`r4efE%b>Dn8B2CEL{VgGm@qVT6Y?kP6ir4A zp$UzQEuk@48i`1Z$(qD{-Ol;zs{6j@ywCfd=lTB5`F-Bs^Zh;6X4|>NK_Gs9kb43a zncFe%Oap;H2e*MhU|@6$Lj|CrY8y{nmwH7bYzHq(7OQJCTxQ12FA|qHcK<3kfkn0- z&@$x`?xa!i6fXDp5=~aeXSUVKACJGDLr?047K_M42+~dnT*(>n997{d@j{^c?vfiE-%9cL@vszm?2=I;?4?5`FX81PT_WPH74 z6HFxbyu$Fa+T5JE!lVL<%b7F8hFJeQS*6%OrW(16MwRCXp26hfrOr^sFqiKfn>_Xw zUOOF?*RPtWrK6e;(SkjsPj)YIS+*WM+8BYp%$WAgRAx83&*R7BWq5Wde^G2M<;_Mx zaujbX(h?ntos;&|+aZ`@r*bM`g9rE8I;8R3nug}BHHOo0DS5d%9bP@q$Y}J46kh>% z0MyXR1e8~DC!(2cCwXhfrxW0Qg8hJR1w%qJTz*PWYOTu4{3iC@tJybCy2}^b>)#RV zdLLcuP9~m8PQ4k>0A&9&A2GSGsYzgl0q_y}&IgT&3P7PZxJ2Xi(i&kRi5KtI$B$hK z)h9}%@MDZMH2fZgXR4Fl!cG3qUF6hv=;I7xdh5e3!=oxnPO-)r>igK>H?)Z`n!(VOarbkwQu7H5*$BwD zJf076n(kDx#~kFVcea@!N%2{7OyXhnLAoyX!8*U$p?E0af|0eUsJ8dZ%6q{fZQ`{l z0yoBCw(-cZ2P^53`4{mS&?}m&vf3)eJunpyo={`W2G``G*J=z~VZkQw2HFkU{C6Nf;RWTsub@~FLR3P(J_^edxnXhQ}Pz%PD;Xolcg=04Ry~V z4&~lU{%F8iHo=G=6-7$;`=V!5m2G*2 z**<`rTH)@&vTFAw`(%`*ms*??tzrimRl0y~>^6a1m@ay{y1evsT+TQBa5U~cS95sN z@T}1BW;$oIUg;*du)W+aqT%J-&V)KAI$wwSVEU6UJimf*bWQHF34ke%4+N6hgn{-& zp)hFZKZE*4t>F6Ff^O@J^RiBp`i0wN9|RywWX*=`Y*eeKYI3mmrLV>+%v6PUBR;B% zf7;(TDz7Tz+1{1j#V5bEwpK;aw)@HilZPtGZVH(B9gKf-_|j>(TGoeS8FDoa2Y#Dl z6n|p|Zd+l77z}j?w|NaLSgw{MKCS*QB-bq(_86It@feig&TiXUAl#dxwtxw zT8OHDL7QHl9QHgP`YE*x1iq9Ke_qIul7<%UsF=}(T=)a=^ztQY*ll4uZ$!KR&mHU8 ziPgGUYT}1xT_Sd&Q7lA9F-<+_zF3Y-n#c1Ka9j&1>FiR3uInk}LAv!l+mXn^miUy1 zI}>%De_EG1fmb6UX_YcqEdJ`r(O$63Zjn6Qib7|GHM;|4ud)!XpnW{EJ0QjOh>{Ib`hk&TzCdX{|Me847)0V= zzHw-GljLIYNrKm11$3NBYgM<=K86gR=4wh$HwLNJq62DIEJ6qpmI+O9<8= zK?txFJZ*{(B5hZR)^9N#_2k30QIOl_rB9W1wtY76WACihFku>Y`e^o3Y5PO<8hoZq z4ftRC8t*nE-4cJSm+DD(Cc3o;y2OZhKNLn9kqw2b_rGNBi?ydG@VT069A4Y?PSm3| zIZdw@?Oi%(gv^ob8_=8XYgtHLn!2L9X1r+Osqxx1SZ9y2l2JmoV{`NC>k*Ra<5Gln zxJ2z()dZE|xy%M@*~H?cX5bt?d~b;P(<`MTh8=K#;CHs$xWqyKeHZ`$|8V_qG_W@N zw{UA)-BhuG*Z*#^TjOkPKbvuc0nY*AY&N5<(ycDNDU}Cm1(5#e*;~b1U2Rhg Date: Sun, 26 Apr 2026 00:30:17 +0200 Subject: [PATCH 3/5] Refactoring benchmarks to be based off a 100,000 rows Excel file instead of a 1 million rows one This reduces the benchmarking duration from 6 hours to 25 minutes. --- .../MiniExcel.Benchmarks/BenchmarkBase.cs | 6 ++-- .../BenchmarkSections/QueryExcelBenchmark.cs | 30 ++++++++++++------- .../TemplateExcelBenchmark.cs | 6 ++-- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/benchmarks/MiniExcel.Benchmarks/BenchmarkBase.cs b/benchmarks/MiniExcel.Benchmarks/BenchmarkBase.cs index bbd60a07..943ca502 100644 --- a/benchmarks/MiniExcel.Benchmarks/BenchmarkBase.cs +++ b/benchmarks/MiniExcel.Benchmarks/BenchmarkBase.cs @@ -2,10 +2,10 @@ public abstract class BenchmarkBase { - public const string FilePath = "Test1,000,000x10.xlsx"; - public const int RowCount = 1_000_000; + protected const string FilePath = "Test100,000x10.xlsx"; + protected const int RowCount = 100_000; - public IEnumerable GetValue() => Enumerable.Range(1, RowCount).Select(s => new DemoDto()); + protected IEnumerable GetValue() => Enumerable.Range(1, RowCount).Select(_ => new DemoDto()); public class DemoDto { diff --git a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/QueryExcelBenchmark.cs b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/QueryExcelBenchmark.cs index 531d6a64..83218068 100644 --- a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/QueryExcelBenchmark.cs +++ b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/QueryExcelBenchmark.cs @@ -53,7 +53,10 @@ public void MiniExcel_QueryFirst_Test() [Benchmark(Description = "MiniExcel Query")] public void MiniExcel_Query() { - foreach (var _ in _importer.Query(FilePath)) { } + foreach (var row in _importer.Query(FilePath)) + { + var value = row; + } } [Benchmark(Description = "MiniExcel QueryFirst with Mapping")] @@ -65,7 +68,10 @@ public void MiniExcel_QueryFirst_Mapping_Test() [Benchmark(Description = "MiniExcel Query with Mapping")] public void MiniExcel_Query_Mapping() { - foreach (var _ in _mappingImporter.Query(FilePath)) { } + foreach (var row in _mappingImporter.Query(FilePath)) + { + var value = row; + } } [Benchmark(Description = "ExcelDataReader QueryFirst")] @@ -74,11 +80,11 @@ public void ExcelDataReader_QueryFirst_Test() using var stream = File.Open(FilePath, FileMode.Open, FileAccess.Read); using var reader = ExcelReaderFactory.CreateReader(stream); - List d = []; reader.Read(); - for (var i = 0; i < reader.FieldCount; i++) - d.Add(reader.GetValue(i)); + { + var value = reader.GetValue(i); + } } [Benchmark(Description = "ExcelDataReader Query")] @@ -89,9 +95,10 @@ public void ExcelDataReader_Query_Test() while (reader.Read()) { - List d = []; for (var i = 0; i < reader.FieldCount; i++) - d.Add(reader.GetValue(i)); + { + var value = reader.GetValue(i); + } } } @@ -105,8 +112,6 @@ public void Epplus_QueryFirst_Test() [Benchmark(Description = "Epplus Query")] public void Epplus_Query_Test() { - // [How do I iterate through rows in an excel table using epplus? - Stack Overflow] (https://stackoverflow.com/questions/21742038/how-do-i-iterate-through-rows-in-an-excel-table-using-epplus) - using var p = new ExcelPackage(new FileInfo(FilePath)); var workSheet = p.Workbook.Worksheets[0]; @@ -135,12 +140,14 @@ public void ClosedXml_Query_Test() using var workbook = new XLWorkbook(FilePath); workbook.Worksheet(1).Rows(); } + [Benchmark(Description = "NPOI QueryFirst")] public void NPOI_QueryFirst_Test() { using var wb = new XSSFWorkbook(FilePath,true); wb.GetSheetAt(0).GetRow(1); } + [Benchmark(Description = "NPOI Query")] public void NPOI_Query_Test() { @@ -181,6 +188,9 @@ public void OpenXmlSDK_Query_Test() var worksheetPart = workbookPart!.WorksheetParts.First(); var sheetData = worksheetPart.Worksheet.Elements().First(); - var firstRow = sheetData.Elements().ToList(); + foreach(var row in sheetData.Elements()) + { + var cellValue = row; + } } } \ No newline at end of file diff --git a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/TemplateExcelBenchmark.cs b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/TemplateExcelBenchmark.cs index b6aab13c..20cc7d92 100644 --- a/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/TemplateExcelBenchmark.cs +++ b/benchmarks/MiniExcel.Benchmarks/BenchmarkSections/TemplateExcelBenchmark.cs @@ -35,7 +35,7 @@ public void Setup() _mappingTemplater = MiniExcel.Templaters.GetMappingTemplater(registry); } - [Benchmark(Description = "MiniExcel Template Generate")] + [Benchmark(Description = "MiniExcel Fill Template")] public void MiniExcel_Template_Generate_Test() { const string templatePath = "TestTemplateBasicIEmumerableFill.xlsx"; @@ -54,7 +54,7 @@ public void MiniExcel_Template_Generate_Test() _templater.FillTemplate(path.FilePath, templatePath, value); } - [Benchmark(Description = "ClosedXml.Report Template Generate")] + [Benchmark(Description = "ClosedXml.Report Generate Template")] public void ClosedXml_Report_Template_Generate_Test() { const string templatePath = "TestTemplateBasicIEmumerableFill_ClosedXML_Report.xlsx"; @@ -77,7 +77,7 @@ public void ClosedXml_Report_Template_Generate_Test() template.SaveAs(path.FilePath); } - [Benchmark(Description = "MiniExcel Mapping Template Generate")] + [Benchmark(Description = "MiniExcel Mapping Fill Template")] public void MiniExcel_Mapping_Template_Generate_Test() { using var templatePath = AutoDeletingPath.Create(); From b4f9eeca3766e7e2bf2741fa0bcea6349fc6128c Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sun, 26 Apr 2026 00:51:29 +0200 Subject: [PATCH 4/5] Fixing benchmark.yml workflow file and bumping project version --- .github/workflows/benchmark.yml | 2 +- src/Directory.Build.props | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 1ae0d79f..02f35fe4 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -120,4 +120,4 @@ jobs: git config user.email github-actions@github.com git add ./*.md git commit -am "Automated benchmark report - ${{ github.ref_name }}" - git push origin master --force-with-lease + git push origin HEAD:master --force-with-lease diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 8247ea17..a2cb8fdf 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ netstandard2.0;net8.0;net9.0;net10.0 - 2.0.0-preview.2 + 2.0.0-preview.3 enable enable 14 @@ -12,11 +12,7 @@ MiniExcel Mini-Software excel;xlsx;csv;micro-helper;mini;openxml;helper; - Fast, Low-Memory, Easy Excel .NET processing tool for importing, exporting and templating spreadsheets - Github : https://github.com/mini-software/MiniExcel - Gitee : https://gitee.com/dotnetchina/MiniExcel - Issues : https://github.com/mini-software/MiniExcel/issues - Todo : https://github.com/mini-software/MiniExcel/projects/1?fullscreen=true + Lightweight, fast and simple .NET processing tool for importing, exporting and templating spreadsheets. Wei Lin, Michele Bastione, PING-HSIU SHIH, Amos(izanhzh), eynarhaji, Mini-Software team Mini-Software, 2021 onwards en @@ -25,7 +21,7 @@ https://github.com/mini-software/MiniExcel Github icon.png - Please Check [Release Notes](https://github.com/mini-software/MiniExcel/tree/master/docs) + https://github.com/mini-software/MiniExcel/tree/master/docs true true snupkg From 10401132694e9c5ae184a81f7f0dcfd9cc4e11c9 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sun, 26 Apr 2026 18:00:06 +0200 Subject: [PATCH 5/5] Updating readme files Removed the suprefluous README-NuGet.md file from the solution, revised the actual README.md file included in the nuget package and updated minor details in the README-V2.md and V2-Upgrade-Notes.md files. --- MiniExcel.slnx | 1 - README-NuGet.md | 101 -------------------------- README-V2.md | 8 +- V2-Upgrade-Notes.md | 2 +- src/README.md | 173 ++++++++++++++++++++++++++++---------------- 5 files changed, 115 insertions(+), 170 deletions(-) delete mode 100644 README-NuGet.md diff --git a/MiniExcel.slnx b/MiniExcel.slnx index 73698bc3..290ecf86 100644 --- a/MiniExcel.slnx +++ b/MiniExcel.slnx @@ -9,7 +9,6 @@ - diff --git a/README-NuGet.md b/README-NuGet.md deleted file mode 100644 index 6d9fb9f5..00000000 --- a/README-NuGet.md +++ /dev/null @@ -1,101 +0,0 @@ - -This project is part of the [.NET Foundation](https://dotnetfoundation.org/projects/project-detail/miniexcel) and operates under their code of conduct. - ---- - -### Introduction - -MiniExcel is simple and efficient to avoid OOM's .NET processing Excel tool. - -At present, most popular frameworks need to load all the data into the memory to facilitate operation, but it will cause memory consumption problems. MiniExcel tries to use algorithm from a stream to reduce the original 1000 MB occupation to a few MB to avoid OOM(out of memory). - -![image](https://user-images.githubusercontent.com/12729184/113086657-ab8bd000-9214-11eb-9563-c970ac1ee35e.png) - - -### Features - -- Low memory consumption, avoid OOM (out of memory) and full GC -- Supports real time operation of each row of data -- Supports LINQ deferred execution, it can do low-consumption, fast paging and other complex queries -- Lightweight, without Microsoft Office installed, no COM+, DLL size is less than 400KB -- Easy API style to read/write/fill excel - -### Get Started - -- [Import/Query Excel](#getstart1) - -- [Export/Create Excel](#getstart2) - -- [Excel Template](#getstart3) - -- [Excel Column Name/Index/Ignore Attribute](#getstart4) - -- [Examples](#getstart5) - - - -### Installation - -You can install the package [from NuGet](https://www.nuget.org/packages/MiniExcel) - -### Release Notes - -Please Check [Release Notes](docs) - -### TODO - -Please Check [TODO](https://github.com/mini-software/MiniExcel/projects/1?fullscreen=true) - -### Performance - -The code for the benchmarks can be found in [MiniExcel.Benchmarks](https://github.com/mini-software/MiniExcel/tree/master/benchmarks/MiniExcel.Benchmarks). -To run all the benchmarks use: - -```bash -dotnet run -project .\benchmarks\MiniExcel.Benchmarks -c Release -f net9.0 -filter * --join -``` - -Hardware and settings used are the following: -``` -BenchmarkDotNet v0.15.0, Linux Ubuntu 24.04.2 LTS (Noble Numbat) -AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores -.NET SDK 9.0.300 - [Host] : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2 - ShortRun : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2 -``` - -#### Import/Query Excel - -The file used to test performance is [**Test1,000,000x10.xlsx**](https://github.com/mini-software/MiniExcel/tree/master/benchmarks/MiniExcel.Benchmarks/Test1%2C000%2C000x10.xlsx), a 32MB document containing 1,000,000 rows * 10 columns whose cells are filled with the string "HelloWorld". - -| Method | Mean | StdDev | Error | Gen0 | Gen1 | Gen2 | Allocated | -|--------------------------------------|-----------------:|---------------:|-----------------:|------------:|------------:|----------:|--------------:| -| 'MiniExcel QueryFirst' | 63.70 μs | 0.337 μs | 6.144 μs | 2.9297 | 2.7669 | - | 49.67 KB | -| 'ExcelDataReader QueryFirst' | 5,010,679.51 μs | 53,245.186 μs | 971,390.400 μs | 105000.0000 | 333.3333 | - | 1717272.56 KB | -| 'MiniExcel Query' | 9,172,286.91 μs | 12,805.326 μs | 233,616.824 μs | 448500.0000 | 4666.6667 | - | 7327883.36 KB | -| 'ExcelDataReader Query' | 10,609,617.09 μs | 29,055.953 μs | 530,088.745 μs | 275666.6667 | 68666.6667 | - | 4504691.87 KB | -| 'Epplus QueryFirst' | 13,770,656.24 μs | 45,909.809 μs | 837,565.827 μs | 174333.3333 | 88833.3333 | 4333.3333 | 3700587.76 KB | -| 'Epplus Query' | 19,257,306.83 μs | 63,117.956 μs | 1,151,506.486 μs | 452333.3333 | 90500.0000 | 5333.3333 | 8223933.16 KB | -| 'ClosedXml Query' | 31,070,263.83 μs | 342,973.671 μs | 6,257,116.502 μs | 401666.6667 | 104166.6667 | 3333.3333 | 6822559.68 KB | -| 'ClosedXml QueryFirst' | 31,141,877.48 μs | 21,006.538 μs | 383,237.459 μs | 402166.6667 | 104833.3333 | 3833.3333 | 6738357.8 KB | -| 'OpenXmlSDK QueryFirst' | 31,750,686.63 μs | 263,328.569 μs | 4,804,093.357 μs | 374666.6667 | 374500.0000 | 3166.6667 | 6069266.96 KB | -| 'OpenXmlSDK Query' | 32,919,119.46 μs | 411,395.682 μs | 7,505,388.691 μs | 374666.6667 | 374500.0000 | 3166.6667 | 6078467.83 KB | - - -#### Export/Create Excel - -Logic: create a total of 10,000,000 "HelloWorld" cells Excel document - -| Method | Mean | StdDev | Error | Gen0 | Gen1 | Gen2 | Allocated | -|----------------------------------------------|---------:|---------:|---------:|------------:|------------:|----------:|----------:| -| 'MiniExcel Create Xlsx' | 4.427 s | 0.0056 s | 0.1023 s | 251666.6667 | 1833.3333 | 1666.6667 | 3.92 GB | -| 'OpenXmlSdk Create Xlsx by DOM mode' | 22.729 s | 0.1226 s | 2.2374 s | 307000.0000 | 306833.3333 | 3833.3333 | 6.22 GB | -| 'ClosedXml Create Xlsx' | 22.851 s | 0.0190 s | 0.3473 s | 195500.0000 | 54500.0000 | 4166.6667 | 4.48 GB | -| 'Epplus Create Xlsx' | 23.027 s | 0.0088 s | 0.1596 s | 89000.0000 | 17500.0000 | 6000.0000 | 2.51 GB | - -Warning: these results may be outdated. You can find the benchmarks for the latest release [here](https://github.com/mini-software/MiniExcel/tree/master/benchmarks/results). - - -### Documents - -https://github.com/mini-software/MiniExcel diff --git a/README-V2.md b/README-V2.md index 3f77c5af..ccec8fcc 100644 --- a/README-V2.md +++ b/README-V2.md @@ -107,12 +107,12 @@ You can download the full package from [NuGet](https://www.nuget.org/packages/Mi dotnet add package MiniExcel ``` -This package will contain the assemblies with both Excel and Csv functionalities, along with the original `v1.x` methods' signatures. -~~If you don't care for those you can also install the Excel and Csv packages separately:~~ -We're still pondering whether this is the best way to move forward with the library, and if we do this is how you'll be able to add the separate packages: +This package will contain the assemblies with both Excel and Csv functionalities, +along with the `MiniExcelConverter` utility class and the original `v1.x` methods' signatures. +If you don't care for those you can also install the OpenXml and Csv packages separately: ```bash -dotnet add package MiniExcel.Core +dotnet add package MiniExcel.OpenXml ``` ```bash diff --git a/V2-Upgrade-Notes.md b/V2-Upgrade-Notes.md index 7135c8a1..3056388c 100644 --- a/V2-Upgrade-Notes.md +++ b/V2-Upgrade-Notes.md @@ -6,7 +6,7 @@ `MiniExcel.Importers`, `MiniExcel.Exporters` and `MiniExcel.Templaters` will give you access to, respectively, the `MiniExcelImporterProvider`, `MiniExcelExporterProvider` and `MiniExcelTemplaterProvider`. - This way Excel and Csv query methods are split between the `OpenXmlImporter` and the `CsvImporter`, accessible from the `MiniExcelImporterProvider`. - The same structure was adopted for export methods through `OpenXmlExporter` and `CsvExporter`, while template methods are instead currently only found in `OpenXmlTemplater`. -- Csv methods are only available if the MiniExcel.Csv package is installed, which is pulled down automatically when the full MiniExcel package is downloaded. +- OpenXml and Csv methods are only available if the respective `MiniExcel.OpenXml` and `MiniExcel.Csv` packages are downloaded, or if the complete `MiniExcel` package is installed. - You can only access the conversion methods `ConvertCsvToXlsx` and `ConvertXlsxToCsv` from the `MiniExcelConverter` utility class, which is part of the full MiniExcel package. - If the full MiniExcel package is downloaded, the previous namespace will coexist along the new one, containing the original static methods' signatures, which have become a facade for the aferomentioned providers. - `IConfiguration` is now `IMiniExcelConfiguration`, but most methods now require the proper implementation (`OpenXmlConfiguration` or `CsvConfiguration`) to be provided rather than the interface diff --git a/src/README.md b/src/README.md index 6d9fb9f5..d9cb2979 100644 --- a/src/README.md +++ b/src/README.md @@ -1,101 +1,148 @@ - -This project is part of the [.NET Foundation](https://dotnetfoundation.org/projects/project-detail/miniexcel) and operates under their code of conduct. +## MiniExcel + + --- -### Introduction +MiniExcel is a simple and efficient Excel processing tool for .NET, specifically designed to minimize memory usage. -MiniExcel is simple and efficient to avoid OOM's .NET processing Excel tool. +At present, most popular frameworks need to load all the data from an Excel document into memory to facilitate operations, but this may cause memory consumption problems. MiniExcel's approach is different: the data is processed row by row in a streaming manner, reducing the original consumption from potentially hundreds of megabytes to just a few megabytes, effectively preventing out-of-memory(OOM) issues. -At present, most popular frameworks need to load all the data into the memory to facilitate operation, but it will cause memory consumption problems. MiniExcel tries to use algorithm from a stream to reduce the original 1000 MB occupation to a few MB to avoid OOM(out of memory). +```mermaid +flowchart LR + A1(["Excel analysis
process"]) --> A2{{"Unzipping
XLSX file"}} --> A3{{"Parsing
OpenXML"}} --> A4{{"Model
conversion"}} --> A5(["Output"]) -![image](https://user-images.githubusercontent.com/12729184/113086657-ab8bd000-9214-11eb-9563-c970ac1ee35e.png) + B1(["Other Excel
Frameworks"]) --> B2{{"Memory"}} --> B3{{"Memory"}} --> B4{{"Workbooks &
Worksheets"}} --> B5(["All rows at
the same time"]) + C1(["MiniExcel"]) --> C2{{"Stream"}} --> C3{{"Stream"}} --> C4{{"POCO or dynamic"}} --> C5(["Deferred execution
row by row"]) -### Features + classDef analysis fill:#D0E8FF,stroke:#1E88E5,color:#0D47A1,font-weight:bold; + classDef others fill:#FCE4EC,stroke:#EC407A,color:#880E4F,font-weight:bold; + classDef miniexcel fill:#E8F5E9,stroke:#388E3C,color:#1B5E20,font-weight:bold; -- Low memory consumption, avoid OOM (out of memory) and full GC -- Supports real time operation of each row of data -- Supports LINQ deferred execution, it can do low-consumption, fast paging and other complex queries -- Lightweight, without Microsoft Office installed, no COM+, DLL size is less than 400KB -- Easy API style to read/write/fill excel + class A1,A2,A3,A4,A5 analysis; + class B1,B2,B3,B4,B5 others; + class C1,C2,C3,C4,C5 miniexcel; +``` -### Get Started +### Features -- [Import/Query Excel](#getstart1) +- Minimizes memory consumption, preventing out-of-memory (OOM) errors and avoiding full garbage collections +- Enables real-time, row-level data operations for better performance on large datasets +- Supports LINQ with deferred execution, allowing for fast, memory-efficient paging and complex queries +- Lightweight, without the need for Microsoft Office or COM+ components, and a size under 800KB +- Simple and intuitive API style to import, export, and template Excel worksheets -- [Export/Create Excel](#getstart2) +### Quickstart -- [Excel Template](#getstart3) +#### Importing -- [Excel Column Name/Index/Ignore Attribute](#getstart4) +You can query worksheets and map the data either to strongly typed classes or dynamic objects: -- [Examples](#getstart5) +```csharp +public class UserAccount +{ + public Guid ID { get; set; } + public string Name { get; set; } + public DateTime DateOfBirth { get; set; } + public int Age { get; set; } + public bool Vip { get; set; } + public decimal Points { get; set; } +} +var userRows = MiniExcel.Query(path); +// or simply -### Installation +var dynamicRows = MiniExcel.Query(path); +``` -You can install the package [from NuGet](https://www.nuget.org/packages/MiniExcel) +#### Exporting -### Release Notes +There are multiple ways to exprt data to an Excel document: -Please Check [Release Notes](docs) +```csharp +// From strongly typed objects -### TODO +var values = new[] +{ + new { Name = "MiniExcel", Value = 1 }, + new { Name = "Github", Value = 2 } +}; +MiniExcel.SaveAs(yourPath, values); -Please Check [TODO](https://github.com/mini-software/MiniExcel/projects/1?fullscreen=true) -### Performance +// From anonymous objects -The code for the benchmarks can be found in [MiniExcel.Benchmarks](https://github.com/mini-software/MiniExcel/tree/master/benchmarks/MiniExcel.Benchmarks). -To run all the benchmarks use: +public class TestType +{ + public string Name { get; set; } + public int Value { get; set; } +} -```bash -dotnet run -project .\benchmarks\MiniExcel.Benchmarks -c Release -f net9.0 -filter * --join -``` +TestType[] values = +[ + new TestType { Name = "MiniExcel", Value = 1 }, + new TestType { Name = "Github", Value = 2 } +]; +MiniExcel.SaveAs(yourPath, values); -Hardware and settings used are the following: -``` -BenchmarkDotNet v0.15.0, Linux Ubuntu 24.04.2 LTS (Noble Numbat) -AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores -.NET SDK 9.0.300 - [Host] : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2 - ShortRun : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2 -``` -#### Import/Query Excel +//From a IEnumerable> -The file used to test performance is [**Test1,000,000x10.xlsx**](https://github.com/mini-software/MiniExcel/tree/master/benchmarks/MiniExcel.Benchmarks/Test1%2C000%2C000x10.xlsx), a 32MB document containing 1,000,000 rows * 10 columns whose cells are filled with the string "HelloWorld". +new List>() dicts = +[ + new Dictionary { { "Name", "MiniExcel" }, { "Value", 1 } }, + new Dictionary { { "Name", "Github" }, { "Value", 2 } } +]; +MiniExcel.SaveAs(yourPath, dicts); -| Method | Mean | StdDev | Error | Gen0 | Gen1 | Gen2 | Allocated | -|--------------------------------------|-----------------:|---------------:|-----------------:|------------:|------------:|----------:|--------------:| -| 'MiniExcel QueryFirst' | 63.70 μs | 0.337 μs | 6.144 μs | 2.9297 | 2.7669 | - | 49.67 KB | -| 'ExcelDataReader QueryFirst' | 5,010,679.51 μs | 53,245.186 μs | 971,390.400 μs | 105000.0000 | 333.3333 | - | 1717272.56 KB | -| 'MiniExcel Query' | 9,172,286.91 μs | 12,805.326 μs | 233,616.824 μs | 448500.0000 | 4666.6667 | - | 7327883.36 KB | -| 'ExcelDataReader Query' | 10,609,617.09 μs | 29,055.953 μs | 530,088.745 μs | 275666.6667 | 68666.6667 | - | 4504691.87 KB | -| 'Epplus QueryFirst' | 13,770,656.24 μs | 45,909.809 μs | 837,565.827 μs | 174333.3333 | 88833.3333 | 4333.3333 | 3700587.76 KB | -| 'Epplus Query' | 19,257,306.83 μs | 63,117.956 μs | 1,151,506.486 μs | 452333.3333 | 90500.0000 | 5333.3333 | 8223933.16 KB | -| 'ClosedXml Query' | 31,070,263.83 μs | 342,973.671 μs | 6,257,116.502 μs | 401666.6667 | 104166.6667 | 3333.3333 | 6822559.68 KB | -| 'ClosedXml QueryFirst' | 31,141,877.48 μs | 21,006.538 μs | 383,237.459 μs | 402166.6667 | 104833.3333 | 3833.3333 | 6738357.8 KB | -| 'OpenXmlSDK QueryFirst' | 31,750,686.63 μs | 263,328.569 μs | 4,804,093.357 μs | 374666.6667 | 374500.0000 | 3166.6667 | 6069266.96 KB | -| 'OpenXmlSDK Query' | 32,919,119.46 μs | 411,395.682 μs | 7,505,388.691 μs | 374666.6667 | 374500.0000 | 3166.6667 | 6078467.83 KB | +// Directly from a IDataReader -#### Export/Create Excel +using var connection = yourConnectionProvider.GetConnection(); +connection.Open(); -Logic: create a total of 10,000,000 "HelloWorld" cells Excel document +using var cmd = connection.CreateCommand(); +cmd.CommandText = """ + SELECT 'MiniExcel' AS "Name", 1 AS "Value" + UNION ALL + SELECT 'Github', 2 + """; -| Method | Mean | StdDev | Error | Gen0 | Gen1 | Gen2 | Allocated | -|----------------------------------------------|---------:|---------:|---------:|------------:|------------:|----------:|----------:| -| 'MiniExcel Create Xlsx' | 4.427 s | 0.0056 s | 0.1023 s | 251666.6667 | 1833.3333 | 1666.6667 | 3.92 GB | -| 'OpenXmlSdk Create Xlsx by DOM mode' | 22.729 s | 0.1226 s | 2.2374 s | 307000.0000 | 306833.3333 | 3833.3333 | 6.22 GB | -| 'ClosedXml Create Xlsx' | 22.851 s | 0.0190 s | 0.3473 s | 195500.0000 | 54500.0000 | 4166.6667 | 4.48 GB | -| 'Epplus Create Xlsx' | 23.027 s | 0.0088 s | 0.1596 s | 89000.0000 | 17500.0000 | 6000.0000 | 2.51 GB | +using var reader = cmd.ExecuteReader(); +MiniExcel.SaveAs(yourPath, reader); -Warning: these results may be outdated. You can find the benchmarks for the latest release [here](https://github.com/mini-software/MiniExcel/tree/master/benchmarks/results). +// From a DataTable -### Documents +var table = new DataTable(); +table.Columns.Add("Name", typeof(string)); +table.Columns.Add("Value", typeof(int)); +table.Rows.Add("MiniExcel", 1); +table.Rows.Add("Github", 2); -https://github.com/mini-software/MiniExcel +MiniExcel.SaveAs(path, table); +```