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/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/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 373db436..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(); @@ -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/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.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 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/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/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); +``` 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/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 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 11a782b1..07953f0c 100644 Binary files a/tests/data/xlsx/TestIssue157.xlsx and b/tests/data/xlsx/TestIssue157.xlsx differ