From 85323b61798cf6ff301412e96a7d719f8bb96cdf Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Tue, 12 Aug 2025 17:12:35 +0200 Subject: [PATCH 1/9] New readme first draft --- MiniExcel.slnx | 3 +- README.md | 1754 ++++++++++++++++-------------------------- README_OLD.md | 1992 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 2649 insertions(+), 1100 deletions(-) create mode 100644 README_OLD.md diff --git a/MiniExcel.slnx b/MiniExcel.slnx index 92c62e02..9549e71d 100644 --- a/MiniExcel.slnx +++ b/MiniExcel.slnx @@ -9,12 +9,13 @@ - + + diff --git a/README.md b/README.md index becd65a2..73286db3 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ --- -### Introduction +## Introduction MiniExcel is a simple and efficient Excel processing tool for .NET, specifically designed to minimize memory usage. @@ -54,61 +54,71 @@ flowchart LR class C1,C2,C3,C4,C5 miniexcel; ``` -### Features +## Features - 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 DLL size under 500KB -- Simple and intuitive API style to read/write/fill excel +- Simple and intuitive API to read, write, and fill Excel documents -### Get Started +### Release Notes -- [Import/Query Excel](#getstart1) +You can check the release notes [here](docs). -- [Export/Create Excel](#getstart2) +### TODO -- [Excel Template](#getstart3) +Check what we are planning for future versions [here](https://github.com/mini-software/MiniExcel/projects/1?fullscreen=true). -- [Excel Column Name/Index/Ignore Attribute](#getstart4) +### Performance -- [Examples](#getstart5) +The code for the benchmarks can be found in [MiniExcel.Benchmarks](benchmarks/MiniExcel.Benchmarks/Program.cs). +The file used to test performance is [**Test1,000,000x10.xlsx**](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". +To run all the benchmarks use: -### Installation +```bash +dotnet run -project .\benchmarks\MiniExcel.Benchmarks -c Release -f net9.0 -filter * --join +``` -You can install the package [from NuGet](https://www.nuget.org/packages/MiniExcel) +You can find the benchmarks' results for the latest release [here](benchmarks/results). -### Release Notes -Please Check [Release Notes](docs) +## Get Started -### TODO +- [Import/Query Excel](#getstarted1) +- [Export/Create Excel](#getstarted2) +- [Excel Template](#getstarted3) +- [Excel Column Name/Index/Ignore Attribute](#getstarted4) +- [Examples](#getstarted5) -Please Check [TODO](https://github.com/mini-software/MiniExcel/projects/1?fullscreen=true) -### Performance +### Installation -The code for the benchmarks can be found in [MiniExcel.Benchmarks](benchmarks/MiniExcel.Benchmarks/Program.cs). +You can download the full package from [NuGet](https://www.nuget.org/packages/MiniExcel): -The file used to test performance is [**Test1,000,000x10.xlsx**](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". +```bash +dotnet add package MiniExcel +``` + +This package will contain the assemblies containing both Excel and Csv functionalities, along with the deprecated `v1.x` methods' signatures. +If you don't care for those you can also install the Excel and Csv assemblies separately: -To run all the benchmarks use: ```bash -dotnet run -project .\benchmarks\MiniExcel.Benchmarks -c Release -f net9.0 -filter * --join +dotnet add package MiniExcel.Core ``` -You can find the benchmarks' results for the latest release [here](benchmarks/results). +```bash +dotnet add package MiniExcel.Csv +``` -### Excel Query/Import +### Excel Query/Import #### 1. Execute a query and map the results to a strongly typed IEnumerable [[Try it]](https://dotnetfiddle.net/w5WD1J) -Recommand to use Stream.Query because of better efficiency. - ```csharp public class UserAccount { @@ -120,235 +130,223 @@ public class UserAccount public decimal Points { get; set; } } -var rows = MiniExcel.Query(path); +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +var rows = excelImporter.Query(path); -// or +// or -using (var stream = File.OpenRead(path)) - var rows = stream.Query(); +using var stream = File.OpenRead(path); +var rows = excelImporter.Query(stream); ``` -![image](https://user-images.githubusercontent.com/12729184/111107423-c8c46b80-8591-11eb-982f-c97a2dafb379.png) - -#### 2. Execute a query and map it to a list of dynamic objects without using head [[Try it]](https://dotnetfiddle.net/w5WD1J) +#### 2. Execute a query and map it to a list of dynamic objects -* dynamic key is `A.B.C.D..` +* By default no header will be used and the dynamic keys will be `.A`, `.B`, `.C`, etc... [[Try it]](https://dotnetfiddle.net/w5WD1J) | MiniExcel | 1 | |-----------|---| | Github | 2 | ```csharp +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +var rows = excelImporter.MiniExcel.Query(path).ToList(); -var rows = MiniExcel.Query(path).ToList(); - -// or -using (var stream = File.OpenRead(path)) -{ - var rows = stream.Query().ToList(); - - Assert.Equal("MiniExcel", rows[0].A); - Assert.Equal(1, rows[0].B); - Assert.Equal("Github", rows[1].A); - Assert.Equal(2, rows[1].B); -} +// rows[0].A = "MiniExcel" +// rows[0].B = 1 +// rows[1].A = "Github" +// rows[1].B = 2 ``` -#### 3. Execute a query with first header row [[Try it]](https://dotnetfiddle.net/w5WD1J) - -note : same column name use last right one +* You can also specify that a header must be used, in which case the dynamic keys will be mapped to it. [[Try it]](https://dotnetfiddle.net/w5WD1J) -Input Excel : - -| Column1 | Column2 | -|-----------|---------| -| MiniExcel | 1 | -| Github | 2 | +| Name | Value | +|-----------|-------| +| MiniExcel | 1 | +| Github | 2 | ```csharp +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +var rows = excelImporter.MiniExcel.Query(path, useHeaderRow: true).ToList(); -var rows = MiniExcel.Query(useHeaderRow:true).ToList(); +// rows[0].Name = "MiniExcel" +// rows[0].Value = 1 +// rows[1].Name = "Github" +// rows[1].Value = 2 +``` -// or +#### 3. Query Support for LINQ extensions First/Take/Skip etc... -using (var stream = File.OpenRead(path)) -{ - var rows = stream.Query(useHeaderRow:true).ToList(); +e.g: Query the tenth row by skipping the first 9 and taking the first - Assert.Equal("MiniExcel", rows[0].Column1); - Assert.Equal(1, rows[0].Column2); - Assert.Equal("Github", rows[1].Column1); - Assert.Equal(2, rows[1].Column2); -} +```csharp +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +var tenthRow = excelImporter.Query(path).Skip(9).First(); ``` -#### 4. Query Support LINQ Extension First/Take/Skip ...etc +#### 4. Specify the Excel sheet to query from -Query First ```csharp -var row = MiniExcel.Query(path).First(); -Assert.Equal("HelloWorld", row.A); - -// or - -using (var stream = File.OpenRead(path)) -{ - var row = stream.Query().First(); - Assert.Equal("HelloWorld", row.A); -} +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +excelImporter.Query(path, sheetName: "SheetName"); ``` -Performance between MiniExcel/ExcelDataReader/ClosedXML/EPPlus -![queryfirst](https://user-images.githubusercontent.com/12729184/111072392-6037a900-8515-11eb-9693-5ce2dad1e460.gif) - -#### 5. Query by sheet name +#### 5. Get the sheets' names from an Excel workbook ```csharp -MiniExcel.Query(path, sheetName: "SheetName"); -//or -stream.Query(sheetName: "SheetName"); +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +var sheetNames = excelImporter.GetSheetNames(path); ``` -#### 6. Query all sheet name and rows +#### 6. Get the columns' names from an Excel sheet ```csharp -var sheetNames = MiniExcel.GetSheetNames(path); -foreach (var sheetName in sheetNames) -{ - var rows = MiniExcel.Query(path, sheetName: sheetName); -} +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +var columns = excelImporter.GetColumnNames(path); + +// columns = [ColumnName1, ColumnName2, ...] when there is a header row +// columns = ["A","B",...] otherwise ``` -#### 7. Get Columns +#### 7. Casting dynamic rows to IDictionary + +Under the hood the dynamic objects returned in a query are implemented using `ExpandoObject`, +making it possible to cast them to `IDictionary`: ```csharp -var columns = MiniExcel.GetColumns(path); // e.g result : ["A","B"...] +var excelimporter = MiniExcel.Importers.GetOpenXmlImporter(); -var cnt = columns.Count; // get column count -``` +var rows = excelImporter.Query(path).Cast>(); -#### 8. Dynamic Query cast row to `IDictionary` +// or -```csharp -foreach(IDictionary row in MiniExcel.Query(path)) +foreach(IDictionary row in excelImporter.Query(path)) { - //.. + // your logic here } - -// or -var rows = MiniExcel.Query(path).Cast>(); -// or Query specified ranges (capitalized) -// A2 represents the second row of column A, C3 represents the third row of column C -// If you don't want to restrict rows, just don't include numbers -var rows = MiniExcel.QueryRange(path, startCell: "A2", endCell: "C3").Cast>(); ``` +#### 8. Query Excel sheet as a DataTable +This is not recommended, as `DataTable` will forcibly load all data into memory, effectively losing the advantages MiniExcel offers. -#### 9. Query Excel return DataTable - -Not recommended, because DataTable will load all data into memory and lose MiniExcel's low memory consumption feature. - -```C# -var table = MiniExcel.QueryAsDataTable(path, useHeaderRow: true); +```csharp +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +var table = excelImporter.QueryAsDataTable(path); ``` -![image](https://user-images.githubusercontent.com/12729184/116673475-07917200-a9d6-11eb-947e-a6f68cce58df.png) - - - -#### 10. Specify the cell to start reading data +#### 9. Specify what cell to start reading data from ```csharp -MiniExcel.Query(path,useHeaderRow:true,startCell:"B3") +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +excelImporter.Query(path, startCell: "B3") ``` - ![image](https://user-images.githubusercontent.com/12729184/117260316-8593c400-ae81-11eb-9877-c087b7ac2b01.png) +#### 10. Fill Merged Cells - -#### 11. Fill Merged Cells - -Note: The efficiency is slower compared to `not using merge fill` - -Reason: The OpenXml standard puts mergeCells at the bottom of the file, which leads to the need to foreach the sheetxml twice +If the Excel sheet being queried contains merged cells it is possble to enable the option to fill every row with the merged value. ```csharp - var config = new OpenXmlConfiguration() - { - FillMergedCells = true - }; - var rows = MiniExcel.Query(path, configuration: config); +var config = new OpenXmlConfiguration +{ + FillMergedCells = true +}; + +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +var rows = MiniExcel.Query(path, configuration: config); ``` ![image](https://user-images.githubusercontent.com/12729184/117973630-3527d500-b35f-11eb-95c3-bde255f8114e.png) -support variable length and width multi-row and column filling +Filling of cells with variable width and height is also supported ![image](https://user-images.githubusercontent.com/12729184/117973820-6d2f1800-b35f-11eb-88d8-555063938108.png) -#### 12. Reading big file by disk-base cache (Disk-Base Cache - SharedString) - -If the SharedStrings size exceeds 5 MB, MiniExcel default will use local disk cache, e.g, [10x100000.xlsx](https://github.com/MiniExcel/MiniExcel/files/8403819/NotDuplicateSharedStrings_10x100000.xlsx)(one million rows data), when disable disk cache the maximum memory usage is 195MB, but able disk cache only needs 65MB. Note, this optimization needs some efficiency cost, so this case will increase reading time from 7.4 seconds to 27.2 seconds, If you don't need it that you can disable disk cache with the following code: - -```csharp -var config = new OpenXmlConfiguration { EnableSharedStringCache = false }; -MiniExcel.Query(path,configuration: config) -``` - -You can use `SharedStringCacheSize ` to change the sharedString file size beyond the specified size for disk caching -```csharp -var config = new OpenXmlConfiguration { SharedStringCacheSize=500*1024*1024 }; -MiniExcel.Query(path, configuration: config); -``` - - -![image](https://user-images.githubusercontent.com/12729184/161411851-1c3f72a7-33b3-4944-84dc-ffc1d16747dd.png) - -![image](https://user-images.githubusercontent.com/12729184/161411825-17f53ec7-bef4-4b16-b234-e24799ea41b0.png) - - +>Note: The performance will take a hit when enabling the feature. +>This happens because in the OpenXml standard the `mergeCells` are indicated at the bottom of the file, which leads to the need of reading the whole sheet twice. +#### 11. Big files and disk-based cache +If the SharedStrings file size exceeds 5 MB, MiniExcel will default to use a local disk cache. +E.g: on the file [10x100000.xlsx](https://github.com/MiniExcel/MiniExcel/files/8403819/NotDuplicateSharedStrings_10x100000.xlsx) (one million rows of data), when disabling the disk cache the maximum memory usage is 195 MB, but with disk cache enabled only 65 MB of memory are used. +> Note: this optimization is not without cost. In the above example it increased reading times from 7 seconds to 27 seconds roughly. +If you prefer you can disable the disk cache with the following code: +```csharp +var config = new OpenXmlConfiguration +{ + EnableSharedStringCache = false +}; +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +importer.Query(path, configuration: config) +``` +You can use also change the disk caching triggering file size beyond the default 5 MB: -### Create/Export Excel +```csharp +var config = new OpenXmlConfiguration +{ + // the size has to be specified in bytes + SharedStringCacheSize = 10 * 1024 * 1024 +}; -1. Must be a non-abstract type with a public parameterless constructor . +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +importer.Query(path, configuration: config) +``` -2. MiniExcel support parameter IEnumerable Deferred Execution, If you want to use least memory, please do not call methods such as ToList -e.g : ToList or not memory usage -![image](https://user-images.githubusercontent.com/12729184/112587389-752b0b00-8e38-11eb-8a52-cfb76c57e5eb.png) +### Create/Export Excel +There are various ways to export data to an Excel document using MiniExcel. +#### 1. From anonymous or strongly typed collections [[Try it]](https://dotnetfiddle.net/w5WD1J) -#### 1. Anonymous or strongly type [[Try it]](https://dotnetfiddle.net/w5WD1J) +When using an anonymous type: ```csharp -var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); -MiniExcel.SaveAs(path, new[] { +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +var values = new[] +{ new { Column1 = "MiniExcel", Column2 = 1 }, new { Column1 = "Github", Column2 = 2} -}); +} +exporter.Export(path, values); ``` -#### 2. `IEnumerable>` +When using a strong type it must be non-abstract with a public parameterless constructor: ```csharp -var values = new List>() +class ExportTest { - new Dictionary{{ "Column1", "MiniExcel" }, { "Column2", 1 } }, - new Dictionary{{ "Column1", "Github" }, { "Column2", 2 } } -}; -MiniExcel.SaveAs(path, values); + public string Column1 { get; set; } + public int Column2 { get; set; } +} + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +ExportTest[] values = +[ + new() { Column1 = "MiniExcel", Column2 = 1 }, + new() { Column1 = "Github", Column2 = 2} +] +exporter.Export(path, values); +``` + +#### 2. From a IEnumerable> + +```csharp +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +List>() values = +[ + new() { { "Column1", "MiniExcel" }, { "Column2", 1 } }, + new() { { "Column1", "Github" }, { "Column2", 2 } } +]; +exporter.Export(path, values); ``` -Create File Result : +Result: | Column1 | Column2 | |-----------|---------| @@ -356,130 +354,112 @@ Create File Result : | Github | 2 | -#### 3. IDataReader -- `Recommended`, it can avoid to load all data into memory -```csharp -MiniExcel.SaveAs(path, reader); -``` +#### 3. IDataReader +MiniExcel supports exporting data directly from a `IDataReader` without the need to load the data into memory first. -![image](https://user-images.githubusercontent.com/12729184/121275378-149a5e80-c8bc-11eb-85fe-5453552134f0.png) - -DataReader export multiple sheets (recommand by Dapper ExecuteReader) +E.g. using the data reader returned by Dapper's `ExecuteReader` extension method: ```csharp -using (var cnn = Connection) -{ - cnn.Open(); - var sheets = new Dictionary(); - sheets.Add("sheet1", cnn.ExecuteReader("select 1 id")); - sheets.Add("sheet2", cnn.ExecuteReader("select 2 id")); - MiniExcel.SaveAs("Demo.xlsx", sheets); -} -``` - +using var connection = YourDbConnection(); +connection.Open(); +var reader = connection.ExecuteReader("SELECT 'MiniExcel' AS Column1, 1 as Column2 UNION ALL SELECT 'Github', 2"); +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export("Demo.xlsx", reader); +``` #### 4. Datatable -- `Not recommended`, it will load all data into memory +>**WARNING**: Not recommended, this will load all data into memory -- DataTable use Caption for column name first, then use columname +For `DataTable` use you have to add column names manually before adding the rows: ```csharp -var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); var table = new DataTable(); -{ - table.Columns.Add("Column1", typeof(string)); - table.Columns.Add("Column2", typeof(decimal)); - table.Rows.Add("MiniExcel", 1); - table.Rows.Add("Github", 2); -} -MiniExcel.SaveAs(path, table); +table.Columns.Add("Column1", typeof(string)); +table.Columns.Add("Column2", typeof(decimal)); + +table.Rows.Add("MiniExcel", 1); +table.Rows.Add("Github", 2); + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export("test.xlsx", table); ``` #### 5. Dapper Query -Thanks @shaofing #552 , please use `CommandDefinition + CommandFlags.NoCache` +Thanks to @shaofing (PR #552), by instatiating a `CommandDefinition` with the flag `CommandFlags.NoCache`, you can pass a Dapper query directly in the `Export` function instead of the corresponding `IDataReader`: ```csharp -using (var connection = GetConnection(connectionString)) -{ - var rows = connection.Query( - new CommandDefinition( - @"select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2" - , flags: CommandFlags.NoCache) - ); - // Note: QueryAsync will throw close connection exception - MiniExcel.SaveAs(path, rows); -} -``` +using var connection = YourDbConnection(); -Below code will load all data into memory +var cmd = new CommandDefinition( + "SELECT 'MiniExcel' AS Column1, 1 as Column2 UNION ALL SELECT 'Github', 2", + flags: CommandFlags.NoCache) +); -```csharp -using (var connection = GetConnection(connectionString)) -{ - var rows = connection.Query(@"select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2"); - MiniExcel.SaveAs(path, rows); -} -``` +// Note: QueryAsync will throw a closed connection exception +var rows = connection.Query(cmd); +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export("dapper_test.xslx", rows); +``` +> **WARNING**: If you simply use `var rows = connection.Query(sql)` all data will be loaded into memory instead! -#### 6. SaveAs to MemoryStream [[Try it]](https://dotnetfiddle.net/JOen0e) -```csharp -using (var stream = new MemoryStream()) //support FileStream,MemoryStream ect. -{ - stream.SaveAs(values); -} -``` +#### 6. Create Multiple Sheets -e.g : api of export excel +It is possible to create multiple sheets at the same time, using a `Dictionary` or `DataSet`: ```csharp -public IActionResult DownloadExcel() +// 1. Dictionary +var users = new[] { - var values = new[] { - new { Column1 = "MiniExcel", Column2 = 1 }, - new { Column1 = "Github", Column2 = 2} - }; - - var memoryStream = new MemoryStream(); - memoryStream.SaveAs(values); - memoryStream.Seek(0, SeekOrigin.Begin); - return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") - { - FileDownloadName = "demo.xlsx" - }; -} -``` - + new { Name = "Jack", Age = 25 }, + new { Name = "Mike", Age = 44 } +}; -#### 7. Create Multiple Sheets +var department = new[] +{ + new { ID = "01", Name = "HR" }, + new { ID = "02", Name = "IT" } +}; -```csharp -// 1. Dictionary -var users = new[] { new { Name = "Jack", Age = 25 }, new { Name = "Mike", Age = 44 } }; -var department = new[] { new { ID = "01", Name = "HR" }, new { ID = "02", Name = "IT" } }; var sheets = new Dictionary { ["users"] = users, ["department"] = department }; -MiniExcel.SaveAs(path, sheets); +var excelExporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export(path, sheets); +``` + +```csharp // 2. DataSet var sheets = new DataSet(); -sheets.Add(UsersDataTable); -sheets.Add(DepartmentDataTable); -//.. -MiniExcel.SaveAs(path, sheets); +sheets.Tables.Add(UsersDataTable); +sheets.Tables.Add(DepartmentDataTable); + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export(path, sheets); ``` ![image](https://user-images.githubusercontent.com/12729184/118130875-6e7c4580-b430-11eb-9b82-22f02716bd63.png) +#### 7. Save to Stream [[Try it]](https://dotnetfiddle.net/JOen0e) + +You can export data directly to a `MemoryStream`, `FileStream`, and generally any stream that supports seeking: + +```csharp +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); + +using var stream = new MemoryStream(); +exporter.Export(stream, values); +``` + #### 8. TableStyles Options Default style @@ -489,11 +469,14 @@ Default style Without style configuration ```csharp -var config = new OpenXmlConfiguration() +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); + +var config = new OpenXmlConfiguration { TableStyles = TableStyles.None }; -MiniExcel.SaveAs(path, value,configuration:config); + +exporter.Export(path, value, configuration: config); ``` ![image](https://user-images.githubusercontent.com/12729184/118784917-f3e57700-b8c2-11eb-8718-8d955b1bc197.png) @@ -501,166 +484,127 @@ MiniExcel.SaveAs(path, value,configuration:config); #### 9. AutoFilter -Since v0.19.0 `OpenXmlConfiguration.AutoFilter` can en/unable AutoFilter , default value is `true`, and setting AutoFilter way: +By default, autofilter is enabled on the headers of exported Excel documents. +You can disable this by setting the `AutoFilter` property of the configuration to `false`: ```csharp -MiniExcel.SaveAs(path, value, configuration: new OpenXmlConfiguration() { AutoFilter = false }); +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +var config = new OpenXmlConfiguration { AutoFilter = false }; +exporter.Export(path, value, configuration: config); ``` - - -#### 10. Create Image +#### 10. Creating images ```csharp -var value = new[] { - new { Name="github",Image=File.ReadAllBytes(PathHelper.GetFile("images/github_logo.png"))}, - new { Name="google",Image=File.ReadAllBytes(PathHelper.GetFile("images/google_logo.png"))}, - new { Name="microsoft",Image=File.ReadAllBytes(PathHelper.GetFile("images/microsoft_logo.png"))}, - new { Name="reddit",Image=File.ReadAllBytes(PathHelper.GetFile("images/reddit_logo.png"))}, - new { Name="statck_overflow",Image=File.ReadAllBytes(PathHelper.GetFile("images/statck_overflow_logo.png"))}, +var exporter = MiniExcel.Exporters.GetExcelExporter(); +var value = new[] +{ + new { Name = "github", Image = File.ReadAllBytes("images/github_logo.png") }, + new { Name = "google", Image = File.ReadAllBytes("images/google_logo.png") }, + new { Name = "microsoft", Image = File.ReadAllBytes("images/microsoft_logo.png") }, + new { Name = "reddit", Image = File.ReadAllBytes("images/reddit_logo.png") }, + new { Name = "statck_overflow", Image = File.ReadAllBytes("images/statck_overflow_logo.png") } }; -MiniExcel.SaveAs(path, value); +exporter.Export(path, value); ``` ![image](https://user-images.githubusercontent.com/12729184/150462383-ad9931b3-ed8d-4221-a1d6-66f799743433.png) - - -#### 11. Byte Array File Export - -Since 1.22.0, when value type is `byte[]` then system will save file path at cell by default, and when import system can be converted to `byte[]`. And if you don't want to use it, you can set `OpenXmlConfiguration.EnableConvertByteArray` to `false`, it can improve the system efficiency. - -![image](https://user-images.githubusercontent.com/12729184/153702334-c3b834f4-6ae4-4ddf-bd4e-e5005d5d8c6a.png) - -Since 1.22.0, when value type is `byte[]` then system will save file path at cell by default, and when import system can be converted to `byte[]`. And if you don't want to use it, you can set `OpenXmlConfiguration.EnableConvertByteArray` to `false`, it can improve the system efficiency. +Whenever you export a property of type `byte[]` it will be archived as an internal resource and the cell will contain a link to it. +When queried, the resource will be converted back to `byte[]`. If you don't need this functionality you can disable it by setting the configuration property `EnableConvertByteArray` to `false` and gain some performance. ![image](https://user-images.githubusercontent.com/12729184/153702334-c3b834f4-6ae4-4ddf-bd4e-e5005d5d8c6a.png) #### 12. Merge same cells vertically -This functionality is only supported in `xlsx` format and merges cells vertically between @merge and @endmerge tags. -You can use @mergelimit to limit boundaries of merging cells vertically. - -```csharp -var mergedFilePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid().ToString()}.xlsx"); - -var path = @"../../../../../samples/xlsx/TestMergeWithTag.xlsx"; - -MiniExcel.MergeSameCells(mergedFilePath, path); -``` +This functionality merges cells vertically between the tags `@merge` and `@endmerge`. +You can use `@mergelimit` to limit boundaries of merging cells vertically. ```csharp -var memoryStream = new MemoryStream(); - -var path = @"../../../../../samples/xlsx/TestMergeWithTag.xlsx"; - -memoryStream.MergeSameCells(path); +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); +templater.MergeSameCells(mergedFilePath, templatePath); ``` -File content before and after merge: - -Without merge limit: +File content before and after merge without merge limit: Screenshot 2023-08-07 at 11 59 24 Screenshot 2023-08-07 at 11 59 57 -With merge limit: +File content before and after merge with merge limit: Screenshot 2023-08-08 at 18 21 00 Screenshot 2023-08-08 at 18 21 40 -#### 13. Skip null values +#### 13. Null values handling -New explicit option to write empty cells for null values: +By default, null values will be treated as empty strings when exporting: ```csharp -DataTable dt = new DataTable(); - -/* ... */ +Dictionary[] value = +[ + new() + { + ["Name1"] = "Somebody once", + ["Name2"] = null, + ["Name3"] = "told me." + } +]; -DataRow dr = dt.NewRow(); +_exporter.Export("test.xlsx", value, configuration: config); +``` -dr["Name1"] = "Somebody once"; -dr["Name2"] = null; -dr["Name3"] = "told me."; +![image](https://user-images.githubusercontent.com/31481586/241419455-3c0aec8a-4e5f-4d83-b7ec-6572124c165d.png) -dt.Rows.Add(dr); +If you want you can change this behaviour in the configuration: -OpenXmlConfiguration configuration = new OpenXmlConfiguration() +```csharp +var config = new OpenXmlConfiguration { - EnableWriteNullValueCell = true // Default value. + EnableWriteNullValueCell = false // Default value is true }; -MiniExcel.SaveAs(@"C:\temp\Book1.xlsx", dt, configuration: configuration); +exporter.Export("test.xlsx", dt, configuration: config); ``` -![image](https://user-images.githubusercontent.com/31481586/241419455-3c0aec8a-4e5f-4d83-b7ec-6572124c165d.png) - -```xml - - - Somebody once - - - - told me. - - -``` +![image](https://user-images.githubusercontent.com/31481586/241419441-c4f27e8f-3f87-46db-a10f-08665864c874.png) -Previous behavior: +Similarly, there is an option to let empty strings be treated as null values: ```csharp -/* ... */ - -OpenXmlConfiguration configuration = new OpenXmlConfiguration() +var config = new OpenXmlConfiguration { - EnableWriteNullValueCell = false // Default value is true. + WriteEmptyStringAsNull = true // Default value is false }; -MiniExcel.SaveAs(@"C:\temp\Book1.xlsx", dt, configuration: configuration); +exporter.Export("test.xlsx", dt, configuration: config); ``` -![image](https://user-images.githubusercontent.com/31481586/241419441-c4f27e8f-3f87-46db-a10f-08665864c874.png) +Both properties work with `null` and `DBNull` values. -```xml - - - Somebody once - - - - - - told me. - - -``` +#### 14. Freeze Panes -Works for null and DBNull values. +MiniExcel allows you to freeze both rows and columns in place: -#### 14. Freeze Panes ```csharp -/* ... */ - -OpenXmlConfiguration configuration = new OpenXmlConfiguration() +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +var config = new OpenXmlConfiguration { FreezeRowCount = 1, // default is 1 FreezeColumnCount = 2 // default is 0 }; -MiniExcel.SaveAs(@"C:\temp\Book1.xlsx", dt, configuration: configuration); +exporter.Export("Book1.xlsx", dt, configuration: config); ``` ![image](docs/images/freeze-pane-1.png) -### Fill Data To Excel Template +### Fill Data To Excel Template -- The declaration is similar to Vue template `{{variable name}}`, or the collection rendering `{{collection name.field name}}` -- Collection rendering support IEnumerable/DataTable/DapperRow +- The declarations are similar to Vue templates `{{variable_name}}` and collection renderings `{{collection_name.field_name}}` +- Collection rendering supports `IEnumerable`, `DataTable` and `DapperRow` #### 1. Basic Fill @@ -695,117 +639,117 @@ MiniExcel.SaveAsByTemplate(path, templatePath, value); ``` - #### 2. IEnumerable Data Fill -> Note1: Use the first IEnumerable of the same column as the basis for filling list +> Note: Use the first IEnumerable of the same column as the basis for filling list Template: -![image](https://user-images.githubusercontent.com/12729184/114564652-14f2f080-9ca3-11eb-831f-09e3fedbc5fc.png) -Result: -![image](https://user-images.githubusercontent.com/12729184/114564204-b2015980-9ca2-11eb-900d-e21249f93f7c.png) +![image](https://user-images.githubusercontent.com/12729184/114564652-14f2f080-9ca3-11eb-831f-09e3fedbc5fc.png) Code: ```csharp //1. By POCO +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); var value = new { - employees = new[] { - new {name="Jack",department="HR"}, - new {name="Lisa",department="HR"}, - new {name="John",department="HR"}, - new {name="Mike",department="IT"}, - new {name="Neo",department="IT"}, - new {name="Loan",department="IT"} + employees = new[] + { + new { name = "Jack", department = "HR" }, + new { name = "Lisa", department = "HR" }, + new { name = "John", department = "HR" }, + new { name = "Mike", department = "IT" }, + new { name = "Neo", department = "IT" }, + new { name = "Loan", department = "IT" } } }; -MiniExcel.SaveAsByTemplate(path, templatePath, value); +templater.ApplyTemplate(path, templatePath, value); //2. By Dictionary +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); var value = new Dictionary() { - ["employees"] = new[] { - new {name="Jack",department="HR"}, - new {name="Lisa",department="HR"}, - new {name="John",department="HR"}, - new {name="Mike",department="IT"}, - new {name="Neo",department="IT"}, - new {name="Loan",department="IT"} + ["employees"] = new[] + { + new { name = "Jack", department = "HR" }, + new { name = "Lisa", department = "HR" }, + new { name = "John", department = "HR" }, + new { name = "Mike", department = "IT" }, + new { name = "Neo", department = "IT" }, + new { name = "Loan", department = "IT" } } }; -MiniExcel.SaveAsByTemplate(path, templatePath, value); +templater.ApplyTemplate(path, templatePath, value); ``` +Result: + +![image](https://user-images.githubusercontent.com/12729184/114564204-b2015980-9ca2-11eb-900d-e21249f93f7c.png) #### 3. Complex Data Fill -> Note: Support multi-sheets and using same varible - Template: ![image](https://user-images.githubusercontent.com/12729184/114565255-acf0da00-9ca3-11eb-8a7f-8131b2265ae8.png) -Result: -![image](https://user-images.githubusercontent.com/12729184/114565329-bf6b1380-9ca3-11eb-85e3-3969e8bf6378.png) +Code: ```csharp // 1. By POCO +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); var value = new { title = "FooCompany", - managers = new[] { - new {name="Jack",department="HR"}, - new {name="Loan",department="IT"} + managers = new[] + { + new { name = "Jack", department = "HR" }, + new { name = "Loan", department = "IT" } }, - employees = new[] { - new {name="Wade",department="HR"}, - new {name="Felix",department="HR"}, - new {name="Eric",department="IT"}, - new {name="Keaton",department="IT"} + employees = new[] + { + new { name = "Wade", department = "HR" }, + new { name = "Felix", department = "HR" }, + new { name = "Eric", department = "IT" }, + new { name = "Keaton", department = "IT" } } }; -MiniExcel.SaveAsByTemplate(path, templatePath, value); +templater.ApplyTemplate(path, templatePath, value); // 2. By Dictionary +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); var value = new Dictionary() { ["title"] = "FooCompany", - ["managers"] = new[] { - new {name="Jack",department="HR"}, - new {name="Loan",department="IT"} + ["managers"] = new[] + { + new { name = "Jack", department = "HR" }, + new { name = "Loan", department = "IT" } }, - ["employees"] = new[] { - new {name="Wade",department="HR"}, - new {name="Felix",department="HR"}, - new {name="Eric",department="IT"}, - new {name="Keaton",department="IT"} + ["employees"] = new[] + { + new { name = "Wade", department = "HR" }, + new { name = "Felix", department = "HR" }, + new { name = "Eric", department = "IT" }, + new { name = "Keaton", department = "IT" } } }; -MiniExcel.SaveAsByTemplate(path, templatePath, value); +templater.ApplyTemplate(path, templatePath, value); ``` -#### 4. Fill Big Data Performance - -> NOTE: Using IEnumerable deferred execution not ToList can save max memory usage in MiniExcel - -![image](https://user-images.githubusercontent.com/12729184/114577091-5046ec80-9cae-11eb-924b-087c7becf8da.png) +Result: +![image](https://user-images.githubusercontent.com/12729184/114565329-bf6b1380-9ca3-11eb-85e3-3969e8bf6378.png) -#### 5. Cell value auto mapping type +#### 4. Cell value auto mapping type -Template +Template: ![image](https://user-images.githubusercontent.com/12729184/114802504-64830a80-9dd0-11eb-8d56-8e8c401b3ace.png) -Result - -![image](https://user-images.githubusercontent.com/12729184/114802419-43221e80-9dd0-11eb-9ffe-a2ce34fe7076.png) - -Class +Model: ```csharp public class Poco @@ -820,25 +764,41 @@ public class Poco } ``` -Code +Code: ```csharp -var poco = new TestIEnumerableTypePoco { @string = "string", @int = 123, @decimal = decimal.Parse("123.45"), @double = (double)123.33, @datetime = new DateTime(2021, 4, 1), @bool = true, @Guid = Guid.NewGuid() }; +var poco = new Poco +{ + @string = "string", + @int = 123, + @decimal = 123.45M, + @double = 123.33D, + datetime = new DateTime(2021, 4, 1), + @bool = true, + Guid = Guid.NewGuid() +}; + var value = new { - Ts = new[] { + Ts = new[] + { poco, new TestIEnumerableTypePoco{}, null, poco } }; -MiniExcel.SaveAsByTemplate(path, templatePath, value); + +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); +templater.ApplyTemplate(path, templatePath, value); ``` +Result: + +![image](https://user-images.githubusercontent.com/12729184/114802419-43221e80-9dd0-11eb-9ffe-a2ce34fe7076.png) -#### 6. Example : List Github Projects +#### 5. Example : List Github Projects Template @@ -854,74 +814,80 @@ Code ```csharp var projects = new[] { - new {Name = "MiniExcel",Link="https://github.com/mini-software/MiniExcel",Star=146, CreateTime=new DateTime(2021,03,01)}, - new {Name = "HtmlTableHelper",Link="https://github.com/mini-software/HtmlTableHelper",Star=16, CreateTime=new DateTime(2020,02,01)}, - new {Name = "PocoClassGenerator",Link="https://github.com/mini-software/PocoClassGenerator",Star=16, CreateTime=new DateTime(2019,03,17)} + new { Name = "MiniExcel", Link = "https://github.com/mini-software/MiniExcel", Star=146, CreateTime = new DateTime(2021,03,01) }, + new { Name = "HtmlTableHelper", Link = "https://github.com/mini-software/HtmlTableHelper", Star=16, CreateTime = new DateTime(2020,02,01) }, + new { Name = "PocoClassGenerator", Link = "https://github.com/mini-software/PocoClassGenerator", Star=16, CreateTime = new DateTime(2019,03,17)} }; + var value = new { User = "ITWeiHan", Projects = projects, TotalStar = projects.Sum(s => s.Star) }; -MiniExcel.SaveAsByTemplate(path, templatePath, value); + +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); +templater.ApplyTemplate(path, templatePath, value); ``` -#### 7. Grouped Data Fill +#### 6. Grouped Data Fill ```csharp var value = new Dictionary() { - ["employees"] = new[] { - new {name="Jack",department="HR"}, - new {name="Jack",department="HR"}, - new {name="John",department="HR"}, - new {name="John",department="IT"}, - new {name="Neo",department="IT"}, - new {name="Loan",department="IT"} + ["employees"] = new[] + { + new { name = "Jack", department = "HR" }, + new { name = "Jack", department = "HR" }, + new { name = "John", department = "HR" }, + new { name = "John", department = "IT" }, + new { name = "Neo", department = "IT" }, + new { name = "Loan", department = "IT" } } }; -await MiniExcel.SaveAsByTemplateAsync(path, templatePath, value); + +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); +templater.ApplyTemplate(path, templatePath, value); ``` -##### 1. With `@group` tag and with `@header` tag +- With `@group` tag and with `@header` tag -Before +Before: ![before_with_header](https://user-images.githubusercontent.com/38832863/218646717-21b9d57a-2be2-4e9a-801b-ae212231d2b4.PNG) -After +After: ![after_with_header](https://user-images.githubusercontent.com/38832863/218646721-58a7a340-7004-4bc2-af24-cffcb2c20737.PNG) -##### 2. With @group tag and without @header tag +- With `@group` tag and without `@header` tag -Before +Before: ![before_without_header](https://user-images.githubusercontent.com/38832863/218646873-b12417fa-801b-4890-8e96-669ed3b43902.PNG) -After +After; ![after_without_header](https://user-images.githubusercontent.com/38832863/218646872-622461ba-342e-49ee-834f-b91ad9c2dac3.PNG) -##### 3. Without @group tag +- Without `@group` tag -Before +Before: ![without_group](https://user-images.githubusercontent.com/38832863/218646975-f52a68eb-e031-43b5-abaa-03b67c052d1a.PNG) -After +After: ![without_group_after](https://user-images.githubusercontent.com/38832863/218646974-4a3c0e07-7c66-4088-ad07-b4ad3695b7e1.PNG) -#### 8. If/ElseIf/Else Statements inside cell +#### 7. If/ElseIf/Else Statements inside cell Rules: -1. Supports DateTime, Double, Int with ==, !=, >, >=, <, <= operators. -2. Supports String with ==, != operators. -3. Each statement should be new line. -4. Single space should be added before and after operators. -5. There shouldn't be new line inside of statements. -6. Cell should be in exact format as below. +1. Supports `DateTime`, `double` and `int` with `==`, `!=`, `>`, `>=`,`<`, `<=` operators. +2. Supports `string` with `==`, `!=` operators. +3. Each statement should be on a new line. +4. A single space should be added before and after operators. +5. There shouldn't be any new lines inside of a statement. +6. Cells should be in the exact format as below. ```csharp @if(name == Jack) @@ -933,15 +899,15 @@ Test {{employees.name}} @endif ``` -Before +Before: ![if_before](https://user-images.githubusercontent.com/38832863/235360606-ca654769-ff55-4f5b-98d2-d2ec0edb8173.PNG) -After +After: ![if_after](https://user-images.githubusercontent.com/38832863/235360609-869bb960-d63d-45ae-8d64-9e8b0d0ab658.PNG) -#### 9. DataTable as parameter +#### 8. DataTable as parameter ```csharp var managers = new DataTable(); @@ -951,16 +917,17 @@ var managers = new DataTable(); managers.Rows.Add("Jack", "HR"); managers.Rows.Add("Loan", "IT"); } + var value = new Dictionary() { ["title"] = "FooCompany", ["managers"] = managers, }; -MiniExcel.SaveAsByTemplate(path, templatePath, value); -``` -#### 10. Formulas -##### 1. Example +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); +templater.ApplyTemplate(path, templatePath, value); +``` +#### 9. Formulas Prefix your formula with `$` and use `$enumrowstart` and `$enumrowend` to mark references to the enumerable start and end rows: ![image](docs/images/template-formulas-1.png) @@ -969,118 +936,110 @@ When the template is rendered, the `$` prefix will be removed and `$enumrowstart ![image](docs/images/template-formulas-2.png) -##### 2. Other Example Formulas: - -| | | -|--------------|-------------------------------------------------------------------------------------------| -| Sum | `$=SUM(C{{$enumrowstart}}:C{{$enumrowend}})` | -| Alt. Average | `$=SUM(C{{$enumrowstart}}:C{{$enumrowend}}) / COUNT(C{{$enumrowstart}}:C{{$enumrowend}})` | -| Range | `$=MAX(C{{$enumrowstart}}:C{{$enumrowend}}) - MIN(C{{$enumrowstart}}:C{{$enumrowend}})` | +_Other examples_: +| Formula | Example | +|---------|------------------------------------------------------------------------------------------------| +| Sum | `$=SUM(C{{$enumrowstart}}:C{{$enumrowend}})` | +| Count | `COUNT(C{{$enumrowstart}}:C{{$enumrowend}})` | +| Range | `$=MAX(C{{$enumrowstart}}:C{{$enumrowend}}) - MIN(C{{$enumrowstart}}:C{{$enumrowend}})` | -#### 11. Other -##### 1. Checking template parameter key +#### 10. Checking template parameter key -Since V1.24.0 , default ignore template missing parameter key and replace it with empty string, `IgnoreTemplateParameterMissing` can control throwing exception or not. +When a parameter key is missing it will be replaced with an empty string by default. +You can change this behaviour to throw an exception by setting the `IgnoreTemplateParameterMissing` configuration property: ```csharp -var config = new OpenXmlConfiguration() +var config = new OpenXmlConfiguration { IgnoreTemplateParameterMissing = false, }; -MiniExcel.SaveAsByTemplate(path, templatePath, value, config) + +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); +templater.ApplyTemplate(path, templatePath, value, config) ``` ![image](https://user-images.githubusercontent.com/12729184/157464332-e316f829-54aa-4c84-a5aa-9aef337b668d.png) -### Excel Column Name/Index/Ignore Attribute - - - -#### 1. Specify the column name, column index, column ignore +### MiniExcel Attributes -Excel Example +#### 1. Specify the column name, column index, or ignore the column entirely ![image](https://user-images.githubusercontent.com/12729184/114230869-3e163700-99ac-11eb-9a90-2039d4b4b313.png) -Code - ```csharp public class ExcelAttributeDemo { - [ExcelColumnName("Column1")] + [MiniExcelColumnName("Column1")] public string Test1 { get; set; } - [ExcelColumnName("Column2")] + + [MiniExcelColumnName("Column2")] public string Test2 { get; set; } - [ExcelIgnore] + + [MiniExcelIgnore] public string Test3 { get; set; } - [ExcelColumnIndex("I")] // system will convert "I" to 8 index + + [MiniExcelColumnIndex("I")] // "I" will be converted to index 8 public string Test4 { get; set; } - public string Test5 { get; } //wihout set will ignore - public string Test6 { get; private set; } //un-public set will ignore - [ExcelColumnIndex(3)] // start with 0 + + public string Test5 { get; } // properties wihout a setter will be ignored + public string Test6 { get; private set; } // properties with a non public setter will be ignored + + [MiniExcelColumnIndex(3)] // Indexes are 0-based public string Test7 { get; set; } } -var rows = MiniExcel.Query(path).ToList(); -Assert.Equal("Column1", rows[0].Test1); -Assert.Equal("Column2", rows[0].Test2); -Assert.Null(rows[0].Test3); -Assert.Equal("Test7", rows[0].Test4); -Assert.Null(rows[0].Test5); -Assert.Null(rows[0].Test6); -Assert.Equal("Test4", rows[0].Test7); -``` - +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var rows = importer.Query(path).ToList(); +// rows[0].Test1 = "Column1" +// rows[0].Test2 = "Column2" +// rows[0].Test3 = null +// rows[0].Test4 = "Test7" +// rows[0].Test5 = null +// rows[0].Test6 = null +// rows[0].Test7 = "Test4" +``` - -#### 2. Custom Format (ExcelFormatAttribute) - -Since V0.21.0 support class which contains `ToString(string content)` method format - -Class +#### 2. Custom Formatting ```csharp public class Dto { public string Name { get; set; } - [ExcelFormat("MMMM dd, yyyy")] + [MiniExcelFormat("MMMM dd, yyyy")] public DateTime InDate { get; set; } } -``` - -Code -```csharp -var value = new Dto[] { - new Issue241Dto{ Name="Jack",InDate=new DateTime(2021,01,04)}, - new Issue241Dto{ Name="Henry",InDate=new DateTime(2020,04,05)}, +var value = new Dto[] +{ + new Issue241Dto{ Name = "Jack", InDate = new DateTime(2021, 01, 04) }, + new Issue241Dto{ Name = "Henry", InDate = new DateTime(2020, 04, 05) } }; -MiniExcel.SaveAs(path, value); + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export(path, value); ``` -Result +Result: ![image](https://user-images.githubusercontent.com/12729184/118910788-ab2bcd80-b957-11eb-8d42-bfce36621b1b.png) -Query supports custom format conversion - -![image](https://user-images.githubusercontent.com/12729184/118911286-87b55280-b958-11eb-9a88-c8ff403d240a.png) -#### 3. Set Column Width(ExcelColumnWidthAttribute) +#### 3. Set Column Width ```csharp public class Dto { - [ExcelColumnWidth(20)] + [MiniExcelColumnWidth(20)] public int ID { get; set; } - [ExcelColumnWidth(15.50)] + + [MiniExcelColumnWidth(15.50)] public string Name { get; set; } } ``` @@ -1090,188 +1049,122 @@ public class Dto ```csharp public class Dto { - [ExcelColumnName(excelColumnName:"EmployeeNo",aliases:new[] { "EmpNo","No" })] - public string Empno { get; set; } public string Name { get; set; } + + [MiniExcelColumnName(columnName: "EmployeeNo", aliases: ["EmpNo", "No"])] + public string Empno { get; set; } } ``` +#### 5. System.ComponentModel.DisplayNameAttribute - -#### 5. System.ComponentModel.DisplayNameAttribute = ExcelColumnName.excelColumnNameAttribute - -Since 1.24.0, system supports System.ComponentModel.DisplayNameAttribute = ExcelColumnName.excelColumnNameAttribute +The `DisplayNameAttribute` has the same effect as the `MiniExcelColumnNameAttribute` ```C# -public class TestIssueI4TXGTDto +public class Dto { public int ID { get; set; } + public string Name { get; set; } + [DisplayName("Specification")] public string Spc { get; set; } + [DisplayName("Unit Price")] public decimal Up { get; set; } } ``` - - #### 6. ExcelColumnAttribute -Since V1.26.0, multiple attributes can be simplified like : -```csharp - public class TestIssueI4ZYUUDto - { - [ExcelColumn(Name = "ID",Index =0)] - public string MyProperty { get; set; } - [ExcelColumn(Name = "CreateDate", Index = 1,Format ="yyyy-MM",Width =100)] - public DateTime MyProperty2 { get; set; } - } -``` - - - -#### 7. DynamicColumnAttribute +Multiple attributes can be simplified using the `MiniExcelColumnAttribute`: -Since V1.26.0, we can set the attributes of Column dynamically ```csharp - var config = new OpenXmlConfiguration - { - DynamicColumns = new DynamicExcelColumn[] { - 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"}, - } - }; - var path = PathHelper.GetTempPath(); - var value = new[] { new { id = 1, name = "Jack", createdate = new DateTime(2022, 04, 12) ,point = 123.456} }; - MiniExcel.SaveAs(path, value, configuration: config); +public class Dto +{ + [MiniExcelColumn(Name = "ID",Index =0)] + public string MyProperty { get; set; } + + [MiniExcelColumn(Name = "CreateDate", Index = 1, Format = "yyyy-MM", Width = 100)] + public DateTime MyProperty2 { get; set; } +} ``` -![image](https://user-images.githubusercontent.com/12729184/164510353-5aecbc4e-c3ce-41e8-b6cf-afd55eb23b68.png) -#### 8. DynamicSheetAttribute +#### 7. DynamicColumnAttribute -Since V1.31.4 we can set the attributes of Sheet dynamically. We can set sheet name and state (visibility). +Attributes can also be set on columns dynamically: ```csharp - var configuration = new OpenXmlConfiguration - { - DynamicSheets = new DynamicExcelSheet[] { - new DynamicExcelSheet("usersSheet") { Name = "Users", State = SheetState.Visible }, - new DynamicExcelSheet("departmentSheet") { Name = "Departments", State = SheetState.Hidden } - } - }; - - var users = new[] { new { Name = "Jack", Age = 25 }, new { Name = "Mike", Age = 44 } }; - var department = new[] { new { ID = "01", Name = "HR" }, new { ID = "02", Name = "IT" } }; - var sheets = new Dictionary - { - ["usersSheet"] = users, - ["departmentSheet"] = department - }; - - var path = PathHelper.GetTempPath(); - MiniExcel.SaveAs(path, sheets, configuration: configuration); -``` +var config = new OpenXmlConfiguration +{ + 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"} + ] +}; -We can also use new attribute ExcelSheetAttribute: +var value = new[] { new { id = 1, name = "Jack", createdate = new DateTime(2022, 04, 12), point = 123.456 } }; -```C# - [ExcelSheet(Name = "Departments", State = SheetState.Hidden)] - private class DepartmentDto - { - [ExcelColumn(Name = "ID",Index = 0)] - public string ID { get; set; } - [ExcelColumn(Name = "Name",Index = 1)] - public string Name { get; set; } - } +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export(path, value, configuration: config); ``` -### Add, Delete, Update - -#### Add +#### 8. MiniExcelSheetAttribute -v1.28.0 support CSV insert N rows data after last row +It is possible to define the name and visibility of a sheet through the `MiniExcelSheetAttribute`: ```csharp -// Origin -{ - var value = new[] { - new { ID=1,Name ="Jack",InDate=new DateTime(2021,01,03)}, - new { ID=2,Name ="Henry",InDate=new DateTime(2020,05,03)}, - }; - MiniExcel.SaveAs(path, value); -} -// Insert 1 rows after last -{ - var value = new { ID=3,Name = "Mike", InDate = new DateTime(2021, 04, 23) }; - MiniExcel.Insert(path, value); -} -// Insert N rows after last +[MiniExcelSheet(Name = "Departments", State = SheetState.Hidden)] +private class DepartmentDto { - var value = new[] { - new { ID=4,Name ="Frank",InDate=new DateTime(2021,06,07)}, - new { ID=5,Name ="Gloria",InDate=new DateTime(2022,05,03)}, - }; - MiniExcel.Insert(path, value); + [MiniExcelColumn(Name = "ID",Index = 0)] + public string ID { get; set; } + + [MiniExcelColumn(Name = "Name",Index = 1)] + public string Name { get; set; } } ``` - -![image](https://user-images.githubusercontent.com/12729184/191023733-1e2fa732-db5c-4a3a-9722-b891fe5aa069.png) - -v1.37.0 support excel insert a new sheet into an existing workbook +It is also possible to do it dynamically through the `DynamicSheets` property of the `OpenXmlConfiguration`: ```csharp -// Origin excel -{ - var value = new[] { - new { ID=1,Name ="Jack",InDate=new DateTime(2021,01,03)}, - new { ID=2,Name ="Henry",InDate=new DateTime(2020,05,03)}, - }; - MiniExcel.SaveAs(path, value, sheetName: "Sheet1"); -} -// Insert a new sheet +var configuration = new OpenXmlConfiguration { - var value = new { ID=3,Name = "Mike", InDate = new DateTime(2021, 04, 23) }; - MiniExcel.Insert(path, table, sheetName: "Sheet2"); -} -``` - - - -#### Delete(waiting) - -#### Update(waiting) - - + DynamicSheets = + [ + new DynamicExcelSheet("usersSheet") { Name = "Users", State = SheetState.Visible }, + new DynamicExcelSheet("departmentSheet") { Name = "Departments", State = SheetState.Hidden } + ] +}; -### Excel Type Auto Check +var users = new[] +{ + new { Name = "Jack", Age = 25 }, + new { Name = "Mike", Age = 44 } +}; +var department = new[] +{ + new { ID = "01", Name = "HR" }, + new { ID = "02", Name = "IT" } +}; -- MiniExcel will check whether it is xlsx or csv based on the `file extension` by default, but there may be inaccuracy, please specify it manually. -- Stream cannot be know from which excel, please specify it manually. +var sheets = new Dictionary +{ + ["usersSheet"] = users, + ["departmentSheet"] = department +}; -```csharp -stream.SaveAs(excelType:ExcelType.CSV); -//or -stream.SaveAs(excelType:ExcelType.XLSX); -//or -stream.Query(excelType:ExcelType.CSV); -//or -stream.Query(excelType:ExcelType.XLSX); +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export(path, sheets, configuration: configuration); ``` - - - - -### CSV +## CSV #### Note - Default return `string` type, and value will not be converted to numbers or datetime, unless the type is defined by strong typing generic. - - #### Custom separator The default is `,` as the separator, you can modify the `Seperator` property for customization @@ -1295,8 +1188,6 @@ var config = new CsvConfiguration() var rows = MiniExcel.Query(path, configuration: config).ToList(); ``` - - #### Custom line break The default is `\r\n` as the newline character, you can modify the `NewLine` property for customization @@ -1309,12 +1200,10 @@ var config = new MiniExcelLibs.Csv.CsvConfiguration() MiniExcel.SaveAs(path, values,configuration: config); ``` - - -#### Custom coding +#### Custom encoding - The default encoding is "Detect Encoding From Byte Order Marks" (detectEncodingFromByteOrderMarks: true) -- f you have custom encoding requirements, please modify the StreamReaderFunc / StreamWriterFunc property +- If you have custom encoding requirements, please modify the StreamReaderFunc / StreamWriterFunc property ```csharp // Read @@ -1346,87 +1235,102 @@ var config = new MiniExcelLibs.Csv.CsvConfiguration() ### DataReader -#### 1. GetReader -Since 1.23.0, you can GetDataReader +There is support for reading one cell at a time using a custom `IDataReader`: ```csharp - using (var reader = MiniExcel.GetReader(path,true)) +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +using var reader = importer.GetDataReader(path, useHeaderRow: true); + +// or + +var importer = MiniExcel.Importers.GetCsvImporter(); +using var reader = MiniExcel.GetDataReader(path, useHeaderRow: true); + + +while (reader.Read()) +{ + for (int i = 0; i < reader.FieldCount; i++) { - while (reader.Read()) - { - for (int i = 0; i < reader.FieldCount; i++) - { - var value = reader.GetValue(i); - } - } + var value = reader.GetValue(i); } +} ``` +### Async -### Async -- v0.17.0 support Async (thanks isdaniel ( SHIH,BING-SIOU)](https://github.com/isdaniel)) +### Add, Delete, Update + +#### Add + +It is possible to append an arbitrary number of rows to a csv document: ```csharp -public static Task SaveAsAsync(string path, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration configuration = null) -public static Task SaveAsAsync(this Stream stream, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, IConfiguration configuration = null) -public static Task> QueryAsync(string path, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) -public static Task> QueryAsync(this Stream stream, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) where T : class, new() -public static Task> QueryAsync(string path, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) where T : class, new() -public static Task>> QueryAsync(this Stream stream, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) -public static Task SaveAsByTemplateAsync(this Stream stream, string templatePath, object value) -public static Task SaveAsByTemplateAsync(this Stream stream, byte[] templateBytes, object value) -public static Task SaveAsByTemplateAsync(string path, string templatePath, object value) -public static Task SaveAsByTemplateAsync(string path, byte[] templateBytes, object value) -public static Task QueryAsDataTableAsync(string path, bool useHeaderRow = true, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) +var exporter = MiniExcel.Exporters.GetCsvExporter(); + +// Insert 1 rows after last +var value = new { ID = 3, Name = "Mike", InDate = new DateTime(2021, 04, 23) }; +exporter.Append(path, value); + +// Insert N rows after last +var value = new[] +{ + new { ID = 4, Name = "Frank", InDate = new DateTime(2021, 06, 07)}, + new { ID = 5, Name = "Gloria", InDate = new DateTime(2022, 05, 03)}, +}; +exporter.AppendToCsv(path, value); ``` -- v1.25.0 support `cancellationToken`。 +There is also support to insert a new sheet into an existing Excel workbook: +```csharp +var exporter = MiniExcel.Exporters.GetOpenXmlExporter() + + var config = new OpenXmlConfiguration { FastMode = true }; +var value = new { ID = 3, Name = "Mike", InDate = new DateTime(2021, 04, 23) }; +exporter.InsertSheet(path, table, sheetName: "Sheet2", configuration: config); +``` +> **Note**: In order to insert worksheets FastMode must be enabled! +#### Delete (work in progress) -### Others +#### Update (work in progress) -#### 1. Enum +### Others -Be sure excel & property name same, system will auto mapping (case insensitive) +#### 1. Enums -![image](https://user-images.githubusercontent.com/12729184/116210595-9784b100-a775-11eb-936f-8e7a8b435961.png) +The serialization of enums is supported. Enum fields are mapped case insensitively. -Since V0.18.0 support Enum Description +The use of the `DescriptionAttribute` is also supported to map enum properties: ```csharp public class Dto { public string Name { get; set; } - public I49RYZUserType UserType { get; set; } + public UserTypeEnum UserType { get; set; } } -public enum Type +public enum UserTypeEnum { - [Description("General User")] - V1, - [Description("General Administrator")] - V2, - [Description("Super Administrator")] - V3 + [Description("General User")] V1, + [Description("General Administrator")] V2, + [Description("Super Administrator")] V3 } ``` ![image](https://user-images.githubusercontent.com/12729184/133116630-27cc7161-099a-48b8-9784-cd1e443af3d1.png) -Since 1.30.0 version support excel Description to Enum , thanks @KaneLeung -#### 2. Convert CSV to XLSX or Convert XLSX to CSV +#### 2. Convert Csv to Xlsx or vice-versa ```csharp -MiniExcel.ConvertXlsxToCsv(xlsxPath, csvPath); -MiniExcel.ConvertXlsxToCsv(xlsxStream, csvStream); -MiniExcel.ConvertCsvToXlsx(csvPath, xlsxPath); -MiniExcel.ConvertCsvToXlsx(csvStream, xlsxStream); -``` -```csharp +MiniExcel.Exporetr.GetCsvExporter().ConvertXlsxToCsv(xlsxPath, csvPath); +MiniExcel.Exporetr.GetCsvExporter().ConvertCsvToXlsx(csvPath, xlsxPath); + +// or + using (var excelStream = new FileStream(path: filePath, FileMode.Open, FileAccess.Read)) using (var csvStream = new MemoryStream()) { @@ -1436,40 +1340,34 @@ using (var csvStream = new MemoryStream()) #### 3. Custom CultureInfo -Since 1.22.0, you can custom CultureInfo like below, system default `CultureInfo.InvariantCulture`. +You can customise CultureInfo used by MiniExcel through the `Culture` configuration parameter. The default is `CultureInfo.InvariantCulture`: ```csharp -var config = new CsvConfiguration() +var config = new CsvConfiguration { Culture = new CultureInfo("fr-FR"), }; -MiniExcel.SaveAs(path, value, configuration: config); - -// or -MiniExcel.Query(path, configuration: config); ``` - #### 4. Custom Buffer Size + ```csharp - public abstract class Configuration : IConfiguration - { - public int BufferSize { get; set; } = 1024 * 512; - } +var conf = new OpenXmlConfiguration { BufferSize = 1024 * 10 }; ``` #### 5. FastMode -System will not control memory, but you can get faster save speed. +You can set the configuration property `FastMode` to achieve faster saving speeds, but this will make the memory consumption much higher: ```csharp -var config = new OpenXmlConfiguration() { FastMode = true }; -MiniExcel.SaveAs(path, reader,configuration:config); +var config = new OpenXmlConfiguration { FastMode = true }; +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export(path, reader, configuration: config); ``` -#### 6. Batch Add Image (MiniExcel.AddPicture) +#### 6. Adding images in batch -Please add pictures before batch generate rows data, or system will load large memory usage when calling AddPicture. +Please add pictures before batch generating the rows' data or a large amount of memory will be used when calling `AddPicture`: ```csharp var images = new[] @@ -1477,15 +1375,15 @@ var images = new[] new MiniExcelPicture { ImageBytes = File.ReadAllBytes(PathHelper.GetFile("images/github_logo.png")), - SheetName = null, // default null is first sheet + SheetName = null, // when null it will default to the first sheet CellAddress = "C3", // required }, new MiniExcelPicture { ImageBytes = File.ReadAllBytes(PathHelper.GetFile("images/google_logo.png")), - PictureType = "image/png", // default PictureType = image/png + PictureType = "image/png", // image/png is the default picture type SheetName = "Demo", - CellAddress = "C9", // required + CellAddress = "C9", WidthPx = 100, HeightPx = 100, }, @@ -1494,350 +1392,79 @@ MiniExcel.AddPicture(path, images); ``` ![Image](https://github.com/user-attachments/assets/19c4d241-9753-4ede-96c8-f810c1a22247) -#### 7. Get Sheets Dimension - -```csharp -var dim = MiniExcel.GetSheetDimensions(path); -``` - -### Examples: - -#### 1. SQLite & Dapper `Large Size File` SQL Insert Avoid OOM +#### 7. Get Sheets Dimensions -note : please don't call ToList/ToArray methods after Query, it'll load all data into memory +You can easily retrieve the dimensions of all worksheets of an Excel file: ```csharp -using (var connection = new SQLiteConnection(connectionString)) -{ - connection.Open(); - using (var transaction = connection.BeginTransaction()) - using (var stream = File.OpenRead(path)) - { - var rows = stream.Query(); - foreach (var row in rows) - connection.Execute("insert into T (A,B) values (@A,@B)", new { row.A, row.B }, transaction: transaction); - transaction.Commit(); - } -} +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var dim = importer.GetSheetDimensions(path); ``` -performance: -![image](https://user-images.githubusercontent.com/12729184/111072579-2dda7b80-8516-11eb-9843-c01a1edc88ec.png) - - - - - -#### 2. ASP.NET Core 3.1 or MVC 5 Download/Upload Excel Xlsx API Demo [Try it](tests/MiniExcel.Tests.AspNetCore) - -```csharp -public class ApiController : Controller -{ - public IActionResult Index() - { - return new ContentResult - { - ContentType = "text/html", - StatusCode = (int)HttpStatusCode.OK, - Content = @" -DownloadExcel
-DownloadExcelFromTemplatePath
-DownloadExcelFromTemplateBytes
-

Upload Excel

-
-
- -
- value = new Dictionary() - { - ["title"] = "FooCompany", - ["managers"] = new[] { - new {name="Jack",department="HR"}, - new {name="Loan",department="IT"} - }, - ["employees"] = new[] { - new {name="Wade",department="HR"}, - new {name="Felix",department="HR"}, - new {name="Eric",department="IT"}, - new {name="Keaton",department="IT"} - } - }; - - MemoryStream memoryStream = new MemoryStream(); - memoryStream.SaveAsByTemplate(templatePath, value); - memoryStream.Seek(0, SeekOrigin.Begin); - return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") - { - FileDownloadName = "demo.xlsx" - }; - } - - private static Dictionary TemplateBytesCache = new Dictionary(); - - static ApiController() - { - string templatePath = "TestTemplateComplex.xlsx"; - byte[] bytes = System.IO.File.ReadAllBytes(templatePath); - TemplateBytesCache.Add(templatePath, bytes); - } - - public IActionResult DownloadExcelFromTemplateBytes() - { - byte[] bytes = TemplateBytesCache["TestTemplateComplex.xlsx"]; - - Dictionary value = new Dictionary() - { - ["title"] = "FooCompany", - ["managers"] = new[] { - new {name="Jack",department="HR"}, - new {name="Loan",department="IT"} - }, - ["employees"] = new[] { - new {name="Wade",department="HR"}, - new {name="Felix",department="HR"}, - new {name="Eric",department="IT"}, - new {name="Keaton",department="IT"} - } - }; - - MemoryStream memoryStream = new MemoryStream(); - memoryStream.SaveAsByTemplate(bytes, value); - memoryStream.Seek(0, SeekOrigin.Begin); - return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") - { - FileDownloadName = "demo.xlsx" - }; - } - - public IActionResult UploadExcel(IFormFile excel) - { - var stream = new MemoryStream(); - excel.CopyTo(stream); - - foreach (var item in stream.Query(true)) - { - // do your logic etc. - } +### FAQ - return Ok("File uploaded successfully"); - } -} -``` +#### Q: Excel header title is not equal to my DTO class property name, how do I map it? -#### 3. Paging Query +A. You can use the `MiniExcelColumnName` attribute on the property you want to map: ```csharp -void Main() +class Dto { - var rows = MiniExcel.Query(path); - - Console.WriteLine("==== No.1 Page ===="); - Console.WriteLine(Page(rows,pageSize:3,page:1)); - Console.WriteLine("==== No.50 Page ===="); - Console.WriteLine(Page(rows,pageSize:3,page:50)); - Console.WriteLine("==== No.5000 Page ===="); - Console.WriteLine(Page(rows,pageSize:3,page:5000)); + [MiniExcelColumnName("ExcelPropertyName")] + public string MyPropertyName { get; set;} } - -public static IEnumerable Page(IEnumerable en, int pageSize, int page) -{ - return en.Skip(page * pageSize).Take(pageSize); -} -``` - -![20210419](https://user-images.githubusercontent.com/12729184/114679083-6ef4c400-9d3e-11eb-9f78-a86daa45fe46.gif) - - - -#### 4. WebForm export Excel by memorystream - -```csharp -var fileName = "Demo.xlsx"; -var sheetName = "Sheet1"; -HttpResponse response = HttpContext.Current.Response; -response.Clear(); -response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; -response.AddHeader("Content-Disposition", $"attachment;filename=\"{fileName}\""); -var values = new[] { - new { Column1 = "MiniExcel", Column2 = 1 }, - new { Column1 = "Github", Column2 = 2} -}; -var memoryStream = new MemoryStream(); -memoryStream.SaveAs(values, sheetName: sheetName); -memoryStream.Seek(0, SeekOrigin.Begin); -memoryStream.CopyTo(Response.OutputStream); -response.End(); ``` +#### Q. How do I query multiple sheets of an Excel file? - -#### 5. Dynamic i18n multi-language and role authority management - -Like the example, create a method to handle i18n and permission management, and use `yield return to return IEnumerable>` to achieve dynamic and low-memory processing effects +A. You can retrieve the sheet names with the `GetSheetNames` method and then Query them using the `sheetName` parameter: ```csharp -void Main() -{ - var value = new Order[] { - new Order(){OrderNo = "SO01",CustomerID="C001",ProductID="P001",Qty=100,Amt=500}, - new Order(){OrderNo = "SO02",CustomerID="C002",ProductID="P002",Qty=300,Amt=400}, - }; - - Console.WriteLine("en-Us and Sales role"); - { - var path = Path.GetTempPath() + Guid.NewGuid() + ".xlsx"; - var lang = "en-US"; - var role = "Sales"; - MiniExcel.SaveAs(path, GetOrders(lang, role, value)); - MiniExcel.Query(path, true).Dump(); - } - - Console.WriteLine("zh-CN and PMC role"); - { - var path = Path.GetTempPath() + Guid.NewGuid() + ".xlsx"; - var lang = "zh-CN"; - var role = "PMC"; - MiniExcel.SaveAs(path, GetOrders(lang, role, value)); - MiniExcel.Query(path, true).Dump(); - } -} - -private IEnumerable> GetOrders(string lang, string role, Order[] orders) -{ - foreach (var order in orders) - { - var newOrder = new Dictionary(); - - if (lang == "zh-CN") - { - newOrder.Add("客户编号", order.CustomerID); - newOrder.Add("订单编号", order.OrderNo); - newOrder.Add("产品编号", order.ProductID); - newOrder.Add("数量", order.Qty); - if (role == "Sales") - newOrder.Add("价格", order.Amt); - yield return newOrder; - } - else if (lang == "en-US") - { - newOrder.Add("Customer ID", order.CustomerID); - newOrder.Add("Order No", order.OrderNo); - newOrder.Add("Product ID", order.ProductID); - newOrder.Add("Quantity", order.Qty); - if (role == "Sales") - newOrder.Add("Amount", order.Amt); - yield return newOrder; - } - else - { - throw new InvalidDataException($"lang {lang} wrong"); - } - } -} +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +var sheetNames = excelImporter.GetSheetNames(path); -public class Order +var rows = new Dictionary>(); +foreach (var sheet in sheetNames) { - public string OrderNo { get; set; } - public string CustomerID { get; set; } - public decimal Qty { get; set; } - public string ProductID { get; set; } - public decimal Amt { get; set; } + rows[sheet] = excelImporter.Query(path, sheetName: sheet).ToList(); } ``` -![image](https://user-images.githubusercontent.com/12729184/118939964-d24bc480-b982-11eb-88dd-f06655f6121a.png) - - - -### FAQ - -#### Q: Excel header title not equal class property name, how to mapping? - -A. Please use ExcelColumnName attribute - -![image](https://user-images.githubusercontent.com/12729184/116020475-eac50980-a678-11eb-8804-129e87200e5e.png) - -#### Q. How to query or export multiple-sheets? - -A. `GetSheetNames` method with Query sheetName parameter. - +#### Q. Can I retrieve informations about what sheets are visible or active? +A. You can use the `GetSheetInformations` method: ```csharp -var sheets = MiniExcel.GetSheetNames(path); -foreach (var sheet in sheets) -{ - Console.WriteLine($"sheet name : {sheet} "); - var rows = MiniExcel.Query(path,useHeaderRow:true,sheetName:sheet); - Console.WriteLine(rows); -} -``` +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +var sheets = excelImporter.GetSheetInformations(path); -![image](https://user-images.githubusercontent.com/12729184/116199841-2a1f5300-a76a-11eb-90a3-6710561cf6db.png) - -#### Q. How to query or export information about sheet visibility? - -A. `GetSheetInformations` method. - - - -```csharp -var sheets = MiniExcel.GetSheetInformations(path); foreach (var sheetInfo in sheets) { Console.WriteLine($"sheet index : {sheetInfo.Index} "); // next sheet index - numbered from 0 Console.WriteLine($"sheet name : {sheetInfo.Name} "); // sheet name Console.WriteLine($"sheet state : {sheetInfo.State} "); // sheet visibility state - visible / hidden + Console.WriteLine($"sheet active : {sheetInfo.Active} "); // whether the sheet is currently marked as active } ``` +#### Q. Is there a way to count all rows from a sheet without having to query it first? -#### Q. Whether to use Count will load all data into the memory? +A. Yes, you can use the method `GetSheetDimensions`: -No, the image test has 1 million rows*10 columns of data, the maximum memory usage is <60MB, and it takes 13.65 seconds +```csharp +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +var dimensions = excelImporter.GetSheetDimensions(path); -![image](https://user-images.githubusercontent.com/12729184/117118518-70586000-adc3-11eb-9ce3-2ba76cf8b5e5.png) +Console.WriteLine($"Total rows: {dimensions[0].Rows.Count}"); +``` -#### Q. How does Query use integer indexs? +#### Q. Is it possible to use integer indexes for the columns? -The default index of Query is the string Key: A,B,C.... If you want to change to numeric index, please create the following method to convert +A. The default indexes of a MiniExcel Query are the strings "A", "B", "C"... +If you want to switch to a numeric index you can copy the following method for converting them: ```csharp -void Main() -{ - var path = @"D:\git\MiniExcel\samples\xlsx\TestTypeMapping.xlsx"; - var rows = MiniExcel.Query(path,true); - foreach (var r in ConvertToIntIndexRows(rows)) - { - Console.Write($"column 0 : {r[0]} ,column 1 : {r[1]}"); - Console.WriteLine(); - } -} - -private IEnumerable> ConvertToIntIndexRows(IEnumerable rows) +IEnumerable> ConvertToIntIndexRows(IEnumerable rows) { ICollection keys = null; var isFirst = true; @@ -1858,122 +1485,51 @@ private IEnumerable> ConvertToIntIndexRows(IEnumerable Strong type & DataTable will generate headers, but Dictionary are still empty Excel +#### Q. Why is no header generated when trying to export an empty enumerable? -#### Q. How to stop the foreach when blank row? +A. MiniExcel uses reflection to dynamically get the type from the values. If there's no data to begin with, the header is also skipped. You can check [issue 133](https://github.com/mini-software/MiniExcel/issues/133) for details. -MiniExcel can be used with `LINQ TakeWhile` to stop foreach iterator. +#### Q. How to stop iterating after a blank row is hit? -![Image](https://user-images.githubusercontent.com/12729184/130209137-162621c2-f337-4479-9996-beeac65bc4d4.png) +A. LINQ's `TakeWhile` extension method can be used for this purpose. -#### Q. How to remove empty rows? +#### Q. Some of the rows in my document are empty, can they be removed automatically? ![image](https://user-images.githubusercontent.com/12729184/137873865-7107d8f5-eb59-42db-903a-44e80589f1b2.png) - -IEnumerable : - -```csharp -public static IEnumerable QueryWithoutEmptyRow(Stream stream, bool useHeaderRow, string sheetName, ExcelType excelType, string startCell, IConfiguration configuration) -{ - var rows = stream.Query(useHeaderRow,sheetName,excelType,startCell,configuration); - foreach (IDictionary row in rows) - { - if(row.Keys.Any(key=>row[key]!=null)) - yield return row; - } -} -``` - - - -DataTable : - -```csharp -public static DataTable QueryAsDataTableWithoutEmptyRow(Stream stream, bool useHeaderRow, string sheetName, ExcelType excelType, string startCell, IConfiguration configuration) -{ - if (sheetName == null && excelType != ExcelType.CSV) /*Issue #279*/ - sheetName = stream.GetSheetNames().First(); - - var dt = new DataTable(sheetName); - var first = true; - var rows = stream.Query(useHeaderRow,sheetName,excelType,startCell,configuration); - foreach (IDictionary row in rows) - { - if (first) - { - - foreach (var key in row.Keys) - { - var column = new DataColumn(key, typeof(object)) { Caption = key }; - dt.Columns.Add(column); - } - - dt.BeginLoadData(); - first = false; - } - - var newRow = dt.NewRow(); - var isNull=true; - foreach (var key in row.Keys) - { - var _v = row[key]; - if(_v!=null) - isNull = false; - newRow[key] = _v; - } - - if(!isNull) - dt.Rows.Add(newRow); - } - - dt.EndLoadData(); - return dt; -} -``` - +A. Yes, simply set the `IgnoreEmptyRows` property of the `OpenXmlConfiguration`. #### Q. How SaveAs(path,value) to replace exists file and without throwing "The file ...xlsx already exists error" +A. You can use the `overwriteFile` parameter for overwriting an existing file: -Please use Stream class to custom file creating logic, e.g: - -```C# - using (var stream = File.Create("Demo.xlsx")) - MiniExcel.SaveAs(stream,value); +```csharp +var excelExporter = MiniExcel.Exporters.GetOpenXmlExporter(); +excelExporter.Export(path, value, overwriteFile: true); ``` - - -or, since V1.25.0, SaveAs support overwriteFile parameter for enable/unable overwriting exist file +You can also implement your own stream for finer grained control: ```csharp - MiniExcel.SaveAs(path, value, overwriteFile: true); -``` - +var excelExporter = MiniExcel.Exporters.GetOpenXmlExporter(); +using var stream = File.Create("Demo.xlsx"); +excelExporter.Export(stream,value); +``` ### Limitations and caveats -- Not support xls and encrypted file now -- xlsm only support Query - +- There is currently no support for the `.xls` legacy Excel format or for encrypted files +- There is only basic query support for the `.xlsm` Excel format -### Reference +### References [ExcelDataReader](https://github.com/ExcelDataReader/ExcelDataReader) / [ClosedXML](https://github.com/ClosedXML/ClosedXML) / [Dapper](https://github.com/DapperLib/Dapper) / [ExcelNumberFormat](https://github.com/andersnm/ExcelNumberFormat) - ### Thanks #### [Jetbrains](https://www.jetbrains.com/) @@ -1983,10 +1539,10 @@ or, since V1.25.0, SaveAs support overwriteFile parameter for enable/unable over Thanks for providing a free All product IDE for this project ([License](https://user-images.githubusercontent.com/12729184/123988233-6ab17c00-d9fa-11eb-8739-2a08c6a4a263.png)) +### Donations sharing +[Link](https://github.com/orgs/mini-software/discussions/754) -### Contribution sharing donate -Link https://github.com/orgs/mini-software/discussions/754 ### Contributors -![](https://contrib.rocks/image?repo=mini-software/MiniExcel) +![](https://contrib.rocks/image?repo=mini-software/MiniExcel) \ No newline at end of file diff --git a/README_OLD.md b/README_OLD.md new file mode 100644 index 00000000..becd65a2 --- /dev/null +++ b/README_OLD.md @@ -0,0 +1,1992 @@ +
+

NuGet +Build status +star GitHub stars +version +Ask DeepWiki +

+
+ +--- + +[](https://www.dotnetfoundation.org/) + +
+

This project is part of the .NET Foundation and operates under their code of conduct.

+
+ +--- + + + + +--- + +
+ Your Stars or Donations can make MiniExcel better +
+ +--- + +### Introduction + +MiniExcel is a simple and efficient Excel processing tool for .NET, specifically designed to minimize memory usage. + +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. + +```mermaid +flowchart LR + A1(["Excel analysis
process"]) --> A2{{"Unzipping
XLSX file"}} --> A3{{"Parsing
OpenXML"}} --> A4{{"Model
conversion"}} --> A5(["Output"]) + + 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"]) + + 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; + + class A1,A2,A3,A4,A5 analysis; + class B1,B2,B3,B4,B5 others; + class C1,C2,C3,C4,C5 miniexcel; +``` + +### Features + +- 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 DLL size under 500KB +- Simple and intuitive 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](benchmarks/MiniExcel.Benchmarks/Program.cs). + +The file used to test performance is [**Test1,000,000x10.xlsx**](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". + +To run all the benchmarks use: + +```bash +dotnet run -project .\benchmarks\MiniExcel.Benchmarks -c Release -f net9.0 -filter * --join +``` + +You can find the benchmarks' results for the latest release [here](benchmarks/results). + + +### Excel Query/Import + +#### 1. Execute a query and map the results to a strongly typed IEnumerable [[Try it]](https://dotnetfiddle.net/w5WD1J) + +Recommand to use Stream.Query because of better efficiency. + +```csharp +public class UserAccount +{ + public Guid ID { get; set; } + public string Name { get; set; } + public DateTime BoD { get; set; } + public int Age { get; set; } + public bool VIP { get; set; } + public decimal Points { get; set; } +} + +var rows = MiniExcel.Query(path); + +// or + +using (var stream = File.OpenRead(path)) + var rows = stream.Query(); +``` + +![image](https://user-images.githubusercontent.com/12729184/111107423-c8c46b80-8591-11eb-982f-c97a2dafb379.png) + +#### 2. Execute a query and map it to a list of dynamic objects without using head [[Try it]](https://dotnetfiddle.net/w5WD1J) + +* dynamic key is `A.B.C.D..` + +| MiniExcel | 1 | +|-----------|---| +| Github | 2 | + +```csharp + +var rows = MiniExcel.Query(path).ToList(); + +// or +using (var stream = File.OpenRead(path)) +{ + var rows = stream.Query().ToList(); + + Assert.Equal("MiniExcel", rows[0].A); + Assert.Equal(1, rows[0].B); + Assert.Equal("Github", rows[1].A); + Assert.Equal(2, rows[1].B); +} +``` + +#### 3. Execute a query with first header row [[Try it]](https://dotnetfiddle.net/w5WD1J) + +note : same column name use last right one + +Input Excel : + +| Column1 | Column2 | +|-----------|---------| +| MiniExcel | 1 | +| Github | 2 | + + +```csharp + +var rows = MiniExcel.Query(useHeaderRow:true).ToList(); + +// or + +using (var stream = File.OpenRead(path)) +{ + var rows = stream.Query(useHeaderRow:true).ToList(); + + Assert.Equal("MiniExcel", rows[0].Column1); + Assert.Equal(1, rows[0].Column2); + Assert.Equal("Github", rows[1].Column1); + Assert.Equal(2, rows[1].Column2); +} +``` + +#### 4. Query Support LINQ Extension First/Take/Skip ...etc + +Query First +```csharp +var row = MiniExcel.Query(path).First(); +Assert.Equal("HelloWorld", row.A); + +// or + +using (var stream = File.OpenRead(path)) +{ + var row = stream.Query().First(); + Assert.Equal("HelloWorld", row.A); +} +``` + +Performance between MiniExcel/ExcelDataReader/ClosedXML/EPPlus +![queryfirst](https://user-images.githubusercontent.com/12729184/111072392-6037a900-8515-11eb-9693-5ce2dad1e460.gif) + +#### 5. Query by sheet name + +```csharp +MiniExcel.Query(path, sheetName: "SheetName"); +//or +stream.Query(sheetName: "SheetName"); +``` + +#### 6. Query all sheet name and rows + +```csharp +var sheetNames = MiniExcel.GetSheetNames(path); +foreach (var sheetName in sheetNames) +{ + var rows = MiniExcel.Query(path, sheetName: sheetName); +} +``` + +#### 7. Get Columns + +```csharp +var columns = MiniExcel.GetColumns(path); // e.g result : ["A","B"...] + +var cnt = columns.Count; // get column count +``` + +#### 8. Dynamic Query cast row to `IDictionary` + +```csharp +foreach(IDictionary row in MiniExcel.Query(path)) +{ + //.. +} + +// or +var rows = MiniExcel.Query(path).Cast>(); +// or Query specified ranges (capitalized) +// A2 represents the second row of column A, C3 represents the third row of column C +// If you don't want to restrict rows, just don't include numbers +var rows = MiniExcel.QueryRange(path, startCell: "A2", endCell: "C3").Cast>(); +``` + + + +#### 9. Query Excel return DataTable + +Not recommended, because DataTable will load all data into memory and lose MiniExcel's low memory consumption feature. + +```C# +var table = MiniExcel.QueryAsDataTable(path, useHeaderRow: true); +``` + +![image](https://user-images.githubusercontent.com/12729184/116673475-07917200-a9d6-11eb-947e-a6f68cce58df.png) + + + +#### 10. Specify the cell to start reading data + +```csharp +MiniExcel.Query(path,useHeaderRow:true,startCell:"B3") +``` + +![image](https://user-images.githubusercontent.com/12729184/117260316-8593c400-ae81-11eb-9877-c087b7ac2b01.png) + + + +#### 11. Fill Merged Cells + +Note: The efficiency is slower compared to `not using merge fill` + +Reason: The OpenXml standard puts mergeCells at the bottom of the file, which leads to the need to foreach the sheetxml twice + +```csharp + var config = new OpenXmlConfiguration() + { + FillMergedCells = true + }; + var rows = MiniExcel.Query(path, configuration: config); +``` + +![image](https://user-images.githubusercontent.com/12729184/117973630-3527d500-b35f-11eb-95c3-bde255f8114e.png) + +support variable length and width multi-row and column filling + +![image](https://user-images.githubusercontent.com/12729184/117973820-6d2f1800-b35f-11eb-88d8-555063938108.png) + +#### 12. Reading big file by disk-base cache (Disk-Base Cache - SharedString) + +If the SharedStrings size exceeds 5 MB, MiniExcel default will use local disk cache, e.g, [10x100000.xlsx](https://github.com/MiniExcel/MiniExcel/files/8403819/NotDuplicateSharedStrings_10x100000.xlsx)(one million rows data), when disable disk cache the maximum memory usage is 195MB, but able disk cache only needs 65MB. Note, this optimization needs some efficiency cost, so this case will increase reading time from 7.4 seconds to 27.2 seconds, If you don't need it that you can disable disk cache with the following code: + +```csharp +var config = new OpenXmlConfiguration { EnableSharedStringCache = false }; +MiniExcel.Query(path,configuration: config) +``` + +You can use `SharedStringCacheSize ` to change the sharedString file size beyond the specified size for disk caching +```csharp +var config = new OpenXmlConfiguration { SharedStringCacheSize=500*1024*1024 }; +MiniExcel.Query(path, configuration: config); +``` + + +![image](https://user-images.githubusercontent.com/12729184/161411851-1c3f72a7-33b3-4944-84dc-ffc1d16747dd.png) + +![image](https://user-images.githubusercontent.com/12729184/161411825-17f53ec7-bef4-4b16-b234-e24799ea41b0.png) + + + + + + + + + +### Create/Export Excel + +1. Must be a non-abstract type with a public parameterless constructor . + +2. MiniExcel support parameter IEnumerable Deferred Execution, If you want to use least memory, please do not call methods such as ToList + +e.g : ToList or not memory usage +![image](https://user-images.githubusercontent.com/12729184/112587389-752b0b00-8e38-11eb-8a52-cfb76c57e5eb.png) + + + +#### 1. Anonymous or strongly type [[Try it]](https://dotnetfiddle.net/w5WD1J) + +```csharp +var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); +MiniExcel.SaveAs(path, new[] { + new { Column1 = "MiniExcel", Column2 = 1 }, + new { Column1 = "Github", Column2 = 2} +}); +``` + +#### 2. `IEnumerable>` + +```csharp +var values = new List>() +{ + new Dictionary{{ "Column1", "MiniExcel" }, { "Column2", 1 } }, + new Dictionary{{ "Column1", "Github" }, { "Column2", 2 } } +}; +MiniExcel.SaveAs(path, values); +``` + +Create File Result : + +| Column1 | Column2 | +|-----------|---------| +| MiniExcel | 1 | +| Github | 2 | + + +#### 3. IDataReader +- `Recommended`, it can avoid to load all data into memory +```csharp +MiniExcel.SaveAs(path, reader); +``` + +![image](https://user-images.githubusercontent.com/12729184/121275378-149a5e80-c8bc-11eb-85fe-5453552134f0.png) + +DataReader export multiple sheets (recommand by Dapper ExecuteReader) + +```csharp +using (var cnn = Connection) +{ + cnn.Open(); + var sheets = new Dictionary(); + sheets.Add("sheet1", cnn.ExecuteReader("select 1 id")); + sheets.Add("sheet2", cnn.ExecuteReader("select 2 id")); + MiniExcel.SaveAs("Demo.xlsx", sheets); +} +``` + + + +#### 4. Datatable + +- `Not recommended`, it will load all data into memory + +- DataTable use Caption for column name first, then use columname + +```csharp +var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); +var table = new DataTable(); +{ + table.Columns.Add("Column1", typeof(string)); + table.Columns.Add("Column2", typeof(decimal)); + table.Rows.Add("MiniExcel", 1); + table.Rows.Add("Github", 2); +} + +MiniExcel.SaveAs(path, table); +``` + +#### 5. Dapper Query + +Thanks @shaofing #552 , please use `CommandDefinition + CommandFlags.NoCache` + +```csharp +using (var connection = GetConnection(connectionString)) +{ + var rows = connection.Query( + new CommandDefinition( + @"select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2" + , flags: CommandFlags.NoCache) + ); + // Note: QueryAsync will throw close connection exception + MiniExcel.SaveAs(path, rows); +} +``` + +Below code will load all data into memory + +```csharp +using (var connection = GetConnection(connectionString)) +{ + var rows = connection.Query(@"select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2"); + MiniExcel.SaveAs(path, rows); +} +``` + + +#### 6. SaveAs to MemoryStream [[Try it]](https://dotnetfiddle.net/JOen0e) + +```csharp +using (var stream = new MemoryStream()) //support FileStream,MemoryStream ect. +{ + stream.SaveAs(values); +} +``` + +e.g : api of export excel + +```csharp +public IActionResult DownloadExcel() +{ + var values = new[] { + new { Column1 = "MiniExcel", Column2 = 1 }, + new { Column1 = "Github", Column2 = 2} + }; + + var memoryStream = new MemoryStream(); + memoryStream.SaveAs(values); + memoryStream.Seek(0, SeekOrigin.Begin); + return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + { + FileDownloadName = "demo.xlsx" + }; +} +``` + + +#### 7. Create Multiple Sheets + +```csharp +// 1. Dictionary +var users = new[] { new { Name = "Jack", Age = 25 }, new { Name = "Mike", Age = 44 } }; +var department = new[] { new { ID = "01", Name = "HR" }, new { ID = "02", Name = "IT" } }; +var sheets = new Dictionary +{ + ["users"] = users, + ["department"] = department +}; +MiniExcel.SaveAs(path, sheets); + +// 2. DataSet +var sheets = new DataSet(); +sheets.Add(UsersDataTable); +sheets.Add(DepartmentDataTable); +//.. +MiniExcel.SaveAs(path, sheets); +``` + +![image](https://user-images.githubusercontent.com/12729184/118130875-6e7c4580-b430-11eb-9b82-22f02716bd63.png) + + +#### 8. TableStyles Options + +Default style + +![image](https://user-images.githubusercontent.com/12729184/138234373-cfa97109-b71f-4711-b7f5-0eaaa4a0a3a6.png) + +Without style configuration + +```csharp +var config = new OpenXmlConfiguration() +{ + TableStyles = TableStyles.None +}; +MiniExcel.SaveAs(path, value,configuration:config); +``` + +![image](https://user-images.githubusercontent.com/12729184/118784917-f3e57700-b8c2-11eb-8718-8d955b1bc197.png) + + +#### 9. AutoFilter + +Since v0.19.0 `OpenXmlConfiguration.AutoFilter` can en/unable AutoFilter , default value is `true`, and setting AutoFilter way: + +```csharp +MiniExcel.SaveAs(path, value, configuration: new OpenXmlConfiguration() { AutoFilter = false }); +``` + + + +#### 10. Create Image + +```csharp +var value = new[] { + new { Name="github",Image=File.ReadAllBytes(PathHelper.GetFile("images/github_logo.png"))}, + new { Name="google",Image=File.ReadAllBytes(PathHelper.GetFile("images/google_logo.png"))}, + new { Name="microsoft",Image=File.ReadAllBytes(PathHelper.GetFile("images/microsoft_logo.png"))}, + new { Name="reddit",Image=File.ReadAllBytes(PathHelper.GetFile("images/reddit_logo.png"))}, + new { Name="statck_overflow",Image=File.ReadAllBytes(PathHelper.GetFile("images/statck_overflow_logo.png"))}, +}; +MiniExcel.SaveAs(path, value); +``` + +![image](https://user-images.githubusercontent.com/12729184/150462383-ad9931b3-ed8d-4221-a1d6-66f799743433.png) + + + +#### 11. Byte Array File Export + +Since 1.22.0, when value type is `byte[]` then system will save file path at cell by default, and when import system can be converted to `byte[]`. And if you don't want to use it, you can set `OpenXmlConfiguration.EnableConvertByteArray` to `false`, it can improve the system efficiency. + +![image](https://user-images.githubusercontent.com/12729184/153702334-c3b834f4-6ae4-4ddf-bd4e-e5005d5d8c6a.png) + +Since 1.22.0, when value type is `byte[]` then system will save file path at cell by default, and when import system can be converted to `byte[]`. And if you don't want to use it, you can set `OpenXmlConfiguration.EnableConvertByteArray` to `false`, it can improve the system efficiency. + +![image](https://user-images.githubusercontent.com/12729184/153702334-c3b834f4-6ae4-4ddf-bd4e-e5005d5d8c6a.png) + +#### 12. Merge same cells vertically + +This functionality is only supported in `xlsx` format and merges cells vertically between @merge and @endmerge tags. +You can use @mergelimit to limit boundaries of merging cells vertically. + +```csharp +var mergedFilePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid().ToString()}.xlsx"); + +var path = @"../../../../../samples/xlsx/TestMergeWithTag.xlsx"; + +MiniExcel.MergeSameCells(mergedFilePath, path); +``` + +```csharp +var memoryStream = new MemoryStream(); + +var path = @"../../../../../samples/xlsx/TestMergeWithTag.xlsx"; + +memoryStream.MergeSameCells(path); +``` + +File content before and after merge: + +Without merge limit: + +Screenshot 2023-08-07 at 11 59 24 + +Screenshot 2023-08-07 at 11 59 57 + +With merge limit: + +Screenshot 2023-08-08 at 18 21 00 + +Screenshot 2023-08-08 at 18 21 40 + +#### 13. Skip null values + +New explicit option to write empty cells for null values: + +```csharp +DataTable dt = new DataTable(); + +/* ... */ + +DataRow dr = dt.NewRow(); + +dr["Name1"] = "Somebody once"; +dr["Name2"] = null; +dr["Name3"] = "told me."; + +dt.Rows.Add(dr); + +OpenXmlConfiguration configuration = new OpenXmlConfiguration() +{ + EnableWriteNullValueCell = true // Default value. +}; + +MiniExcel.SaveAs(@"C:\temp\Book1.xlsx", dt, configuration: configuration); +``` + +![image](https://user-images.githubusercontent.com/31481586/241419455-3c0aec8a-4e5f-4d83-b7ec-6572124c165d.png) + +```xml + + + Somebody once + + + + told me. + + +``` + +Previous behavior: + +```csharp +/* ... */ + +OpenXmlConfiguration configuration = new OpenXmlConfiguration() +{ + EnableWriteNullValueCell = false // Default value is true. +}; + +MiniExcel.SaveAs(@"C:\temp\Book1.xlsx", dt, configuration: configuration); +``` + +![image](https://user-images.githubusercontent.com/31481586/241419441-c4f27e8f-3f87-46db-a10f-08665864c874.png) + +```xml + + + Somebody once + + + + + + told me. + + +``` + +Works for null and DBNull values. + +#### 14. Freeze Panes +```csharp +/* ... */ + +OpenXmlConfiguration configuration = new OpenXmlConfiguration() +{ + FreezeRowCount = 1, // default is 1 + FreezeColumnCount = 2 // default is 0 +}; + +MiniExcel.SaveAs(@"C:\temp\Book1.xlsx", dt, configuration: configuration); +``` + +![image](docs/images/freeze-pane-1.png) + + +### Fill Data To Excel Template + +- The declaration is similar to Vue template `{{variable name}}`, or the collection rendering `{{collection name.field name}}` +- Collection rendering support IEnumerable/DataTable/DapperRow + +#### 1. Basic Fill + +Template: +![image](https://user-images.githubusercontent.com/12729184/114537556-ed8d2b00-9c84-11eb-8303-a69f62c41e5b.png) + +Result: +![image](https://user-images.githubusercontent.com/12729184/114537490-d8180100-9c84-11eb-8c69-db58692f3a85.png) + +Code: +```csharp +// 1. By POCO +var value = new +{ + Name = "Jack", + CreateDate = new DateTime(2021, 01, 01), + VIP = true, + Points = 123 +}; +MiniExcel.SaveAsByTemplate(path, templatePath, value); + + +// 2. By Dictionary +var value = new Dictionary() +{ + ["Name"] = "Jack", + ["CreateDate"] = new DateTime(2021, 01, 01), + ["VIP"] = true, + ["Points"] = 123 +}; +MiniExcel.SaveAsByTemplate(path, templatePath, value); +``` + + + +#### 2. IEnumerable Data Fill + +> Note1: Use the first IEnumerable of the same column as the basis for filling list + +Template: +![image](https://user-images.githubusercontent.com/12729184/114564652-14f2f080-9ca3-11eb-831f-09e3fedbc5fc.png) + +Result: +![image](https://user-images.githubusercontent.com/12729184/114564204-b2015980-9ca2-11eb-900d-e21249f93f7c.png) + +Code: +```csharp +//1. By POCO +var value = new +{ + employees = new[] { + new {name="Jack",department="HR"}, + new {name="Lisa",department="HR"}, + new {name="John",department="HR"}, + new {name="Mike",department="IT"}, + new {name="Neo",department="IT"}, + new {name="Loan",department="IT"} + } +}; +MiniExcel.SaveAsByTemplate(path, templatePath, value); + +//2. By Dictionary +var value = new Dictionary() +{ + ["employees"] = new[] { + new {name="Jack",department="HR"}, + new {name="Lisa",department="HR"}, + new {name="John",department="HR"}, + new {name="Mike",department="IT"}, + new {name="Neo",department="IT"}, + new {name="Loan",department="IT"} + } +}; +MiniExcel.SaveAsByTemplate(path, templatePath, value); +``` + + + +#### 3. Complex Data Fill + +> Note: Support multi-sheets and using same varible + +Template: + +![image](https://user-images.githubusercontent.com/12729184/114565255-acf0da00-9ca3-11eb-8a7f-8131b2265ae8.png) + +Result: + +![image](https://user-images.githubusercontent.com/12729184/114565329-bf6b1380-9ca3-11eb-85e3-3969e8bf6378.png) + +```csharp +// 1. By POCO +var value = new +{ + title = "FooCompany", + managers = new[] { + new {name="Jack",department="HR"}, + new {name="Loan",department="IT"} + }, + employees = new[] { + new {name="Wade",department="HR"}, + new {name="Felix",department="HR"}, + new {name="Eric",department="IT"}, + new {name="Keaton",department="IT"} + } +}; +MiniExcel.SaveAsByTemplate(path, templatePath, value); + +// 2. By Dictionary +var value = new Dictionary() +{ + ["title"] = "FooCompany", + ["managers"] = new[] { + new {name="Jack",department="HR"}, + new {name="Loan",department="IT"} + }, + ["employees"] = new[] { + new {name="Wade",department="HR"}, + new {name="Felix",department="HR"}, + new {name="Eric",department="IT"}, + new {name="Keaton",department="IT"} + } +}; +MiniExcel.SaveAsByTemplate(path, templatePath, value); +``` + +#### 4. Fill Big Data Performance + +> NOTE: Using IEnumerable deferred execution not ToList can save max memory usage in MiniExcel + +![image](https://user-images.githubusercontent.com/12729184/114577091-5046ec80-9cae-11eb-924b-087c7becf8da.png) + + + +#### 5. Cell value auto mapping type + +Template + +![image](https://user-images.githubusercontent.com/12729184/114802504-64830a80-9dd0-11eb-8d56-8e8c401b3ace.png) + +Result + +![image](https://user-images.githubusercontent.com/12729184/114802419-43221e80-9dd0-11eb-9ffe-a2ce34fe7076.png) + +Class + +```csharp +public class Poco +{ + public string @string { get; set; } + public int? @int { get; set; } + public decimal? @decimal { get; set; } + public double? @double { get; set; } + public DateTime? datetime { get; set; } + public bool? @bool { get; set; } + public Guid? Guid { get; set; } +} +``` + +Code + +```csharp +var poco = new TestIEnumerableTypePoco { @string = "string", @int = 123, @decimal = decimal.Parse("123.45"), @double = (double)123.33, @datetime = new DateTime(2021, 4, 1), @bool = true, @Guid = Guid.NewGuid() }; +var value = new +{ + Ts = new[] { + poco, + new TestIEnumerableTypePoco{}, + null, + poco + } +}; +MiniExcel.SaveAsByTemplate(path, templatePath, value); +``` + + + +#### 6. Example : List Github Projects + +Template + +![image](https://user-images.githubusercontent.com/12729184/115068623-12073280-9f25-11eb-9124-f4b3efcdb2a7.png) + + +Result + +![image](https://user-images.githubusercontent.com/12729184/115068639-1a5f6d80-9f25-11eb-9f45-27c434d19a78.png) + +Code + +```csharp +var projects = new[] +{ + new {Name = "MiniExcel",Link="https://github.com/mini-software/MiniExcel",Star=146, CreateTime=new DateTime(2021,03,01)}, + new {Name = "HtmlTableHelper",Link="https://github.com/mini-software/HtmlTableHelper",Star=16, CreateTime=new DateTime(2020,02,01)}, + new {Name = "PocoClassGenerator",Link="https://github.com/mini-software/PocoClassGenerator",Star=16, CreateTime=new DateTime(2019,03,17)} +}; +var value = new +{ + User = "ITWeiHan", + Projects = projects, + TotalStar = projects.Sum(s => s.Star) +}; +MiniExcel.SaveAsByTemplate(path, templatePath, value); +``` + +#### 7. Grouped Data Fill + +```csharp +var value = new Dictionary() +{ + ["employees"] = new[] { + new {name="Jack",department="HR"}, + new {name="Jack",department="HR"}, + new {name="John",department="HR"}, + new {name="John",department="IT"}, + new {name="Neo",department="IT"}, + new {name="Loan",department="IT"} + } +}; +await MiniExcel.SaveAsByTemplateAsync(path, templatePath, value); +``` +##### 1. With `@group` tag and with `@header` tag + +Before + +![before_with_header](https://user-images.githubusercontent.com/38832863/218646717-21b9d57a-2be2-4e9a-801b-ae212231d2b4.PNG) + +After + +![after_with_header](https://user-images.githubusercontent.com/38832863/218646721-58a7a340-7004-4bc2-af24-cffcb2c20737.PNG) + +##### 2. With @group tag and without @header tag + +Before + +![before_without_header](https://user-images.githubusercontent.com/38832863/218646873-b12417fa-801b-4890-8e96-669ed3b43902.PNG) + +After + +![after_without_header](https://user-images.githubusercontent.com/38832863/218646872-622461ba-342e-49ee-834f-b91ad9c2dac3.PNG) + +##### 3. Without @group tag + +Before + +![without_group](https://user-images.githubusercontent.com/38832863/218646975-f52a68eb-e031-43b5-abaa-03b67c052d1a.PNG) + +After + +![without_group_after](https://user-images.githubusercontent.com/38832863/218646974-4a3c0e07-7c66-4088-ad07-b4ad3695b7e1.PNG) + +#### 8. If/ElseIf/Else Statements inside cell + +Rules: +1. Supports DateTime, Double, Int with ==, !=, >, >=, <, <= operators. +2. Supports String with ==, != operators. +3. Each statement should be new line. +4. Single space should be added before and after operators. +5. There shouldn't be new line inside of statements. +6. Cell should be in exact format as below. + +```csharp +@if(name == Jack) +{{employees.name}} +@elseif(name == Neo) +Test {{employees.name}} +@else +{{employees.department}} +@endif +``` + +Before + +![if_before](https://user-images.githubusercontent.com/38832863/235360606-ca654769-ff55-4f5b-98d2-d2ec0edb8173.PNG) + +After + +![if_after](https://user-images.githubusercontent.com/38832863/235360609-869bb960-d63d-45ae-8d64-9e8b0d0ab658.PNG) + +#### 9. DataTable as parameter + +```csharp +var managers = new DataTable(); +{ + managers.Columns.Add("name"); + managers.Columns.Add("department"); + managers.Rows.Add("Jack", "HR"); + managers.Rows.Add("Loan", "IT"); +} +var value = new Dictionary() +{ + ["title"] = "FooCompany", + ["managers"] = managers, +}; +MiniExcel.SaveAsByTemplate(path, templatePath, value); +``` +#### 10. Formulas + +##### 1. Example +Prefix your formula with `$` and use `$enumrowstart` and `$enumrowend` to mark references to the enumerable start and end rows: + +![image](docs/images/template-formulas-1.png) + +When the template is rendered, the `$` prefix will be removed and `$enumrowstart` and `$enumrowend` will be replaced with the start and end row numbers of the enumerable: + +![image](docs/images/template-formulas-2.png) + +##### 2. Other Example Formulas: + +| | | +|--------------|-------------------------------------------------------------------------------------------| +| Sum | `$=SUM(C{{$enumrowstart}}:C{{$enumrowend}})` | +| Alt. Average | `$=SUM(C{{$enumrowstart}}:C{{$enumrowend}}) / COUNT(C{{$enumrowstart}}:C{{$enumrowend}})` | +| Range | `$=MAX(C{{$enumrowstart}}:C{{$enumrowend}}) - MIN(C{{$enumrowstart}}:C{{$enumrowend}})` | + + +#### 11. Other + +##### 1. Checking template parameter key + +Since V1.24.0 , default ignore template missing parameter key and replace it with empty string, `IgnoreTemplateParameterMissing` can control throwing exception or not. + +```csharp +var config = new OpenXmlConfiguration() +{ + IgnoreTemplateParameterMissing = false, +}; +MiniExcel.SaveAsByTemplate(path, templatePath, value, config) +``` + +![image](https://user-images.githubusercontent.com/12729184/157464332-e316f829-54aa-4c84-a5aa-9aef337b668d.png) + + + +### Excel Column Name/Index/Ignore Attribute + + + +#### 1. Specify the column name, column index, column ignore + +Excel Example + +![image](https://user-images.githubusercontent.com/12729184/114230869-3e163700-99ac-11eb-9a90-2039d4b4b313.png) + +Code + +```csharp +public class ExcelAttributeDemo +{ + [ExcelColumnName("Column1")] + public string Test1 { get; set; } + [ExcelColumnName("Column2")] + public string Test2 { get; set; } + [ExcelIgnore] + public string Test3 { get; set; } + [ExcelColumnIndex("I")] // system will convert "I" to 8 index + public string Test4 { get; set; } + public string Test5 { get; } //wihout set will ignore + public string Test6 { get; private set; } //un-public set will ignore + [ExcelColumnIndex(3)] // start with 0 + public string Test7 { get; set; } +} + +var rows = MiniExcel.Query(path).ToList(); +Assert.Equal("Column1", rows[0].Test1); +Assert.Equal("Column2", rows[0].Test2); +Assert.Null(rows[0].Test3); +Assert.Equal("Test7", rows[0].Test4); +Assert.Null(rows[0].Test5); +Assert.Null(rows[0].Test6); +Assert.Equal("Test4", rows[0].Test7); +``` + + + + + +#### 2. Custom Format (ExcelFormatAttribute) + +Since V0.21.0 support class which contains `ToString(string content)` method format + +Class + +```csharp +public class Dto +{ + public string Name { get; set; } + + [ExcelFormat("MMMM dd, yyyy")] + public DateTime InDate { get; set; } +} +``` + +Code + +```csharp +var value = new Dto[] { + new Issue241Dto{ Name="Jack",InDate=new DateTime(2021,01,04)}, + new Issue241Dto{ Name="Henry",InDate=new DateTime(2020,04,05)}, +}; +MiniExcel.SaveAs(path, value); +``` + +Result + +![image](https://user-images.githubusercontent.com/12729184/118910788-ab2bcd80-b957-11eb-8d42-bfce36621b1b.png) + +Query supports custom format conversion + +![image](https://user-images.githubusercontent.com/12729184/118911286-87b55280-b958-11eb-9a88-c8ff403d240a.png) + +#### 3. Set Column Width(ExcelColumnWidthAttribute) + +```csharp +public class Dto +{ + [ExcelColumnWidth(20)] + public int ID { get; set; } + [ExcelColumnWidth(15.50)] + public string Name { get; set; } +} +``` + +#### 4. Multiple column names mapping to the same property. + +```csharp +public class Dto +{ + [ExcelColumnName(excelColumnName:"EmployeeNo",aliases:new[] { "EmpNo","No" })] + public string Empno { get; set; } + public string Name { get; set; } +} +``` + + + +#### 5. System.ComponentModel.DisplayNameAttribute = ExcelColumnName.excelColumnNameAttribute + +Since 1.24.0, system supports System.ComponentModel.DisplayNameAttribute = ExcelColumnName.excelColumnNameAttribute + +```C# +public class TestIssueI4TXGTDto +{ + public int ID { get; set; } + public string Name { get; set; } + [DisplayName("Specification")] + public string Spc { get; set; } + [DisplayName("Unit Price")] + public decimal Up { get; set; } +} +``` + + + +#### 6. ExcelColumnAttribute + +Since V1.26.0, multiple attributes can be simplified like : +```csharp + public class TestIssueI4ZYUUDto + { + [ExcelColumn(Name = "ID",Index =0)] + public string MyProperty { get; set; } + [ExcelColumn(Name = "CreateDate", Index = 1,Format ="yyyy-MM",Width =100)] + public DateTime MyProperty2 { get; set; } + } +``` + + + +#### 7. DynamicColumnAttribute + +Since V1.26.0, we can set the attributes of Column dynamically +```csharp + var config = new OpenXmlConfiguration + { + DynamicColumns = new DynamicExcelColumn[] { + 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"}, + } + }; + var path = PathHelper.GetTempPath(); + var value = new[] { new { id = 1, name = "Jack", createdate = new DateTime(2022, 04, 12) ,point = 123.456} }; + MiniExcel.SaveAs(path, value, configuration: config); +``` +![image](https://user-images.githubusercontent.com/12729184/164510353-5aecbc4e-c3ce-41e8-b6cf-afd55eb23b68.png) + +#### 8. DynamicSheetAttribute + +Since V1.31.4 we can set the attributes of Sheet dynamically. We can set sheet name and state (visibility). +```csharp + var configuration = new OpenXmlConfiguration + { + DynamicSheets = new DynamicExcelSheet[] { + new DynamicExcelSheet("usersSheet") { Name = "Users", State = SheetState.Visible }, + new DynamicExcelSheet("departmentSheet") { Name = "Departments", State = SheetState.Hidden } + } + }; + + var users = new[] { new { Name = "Jack", Age = 25 }, new { Name = "Mike", Age = 44 } }; + var department = new[] { new { ID = "01", Name = "HR" }, new { ID = "02", Name = "IT" } }; + var sheets = new Dictionary + { + ["usersSheet"] = users, + ["departmentSheet"] = department + }; + + var path = PathHelper.GetTempPath(); + MiniExcel.SaveAs(path, sheets, configuration: configuration); +``` + +We can also use new attribute ExcelSheetAttribute: + +```C# + [ExcelSheet(Name = "Departments", State = SheetState.Hidden)] + private class DepartmentDto + { + [ExcelColumn(Name = "ID",Index = 0)] + public string ID { get; set; } + [ExcelColumn(Name = "Name",Index = 1)] + public string Name { get; set; } + } +``` + +### Add, Delete, Update + +#### Add + +v1.28.0 support CSV insert N rows data after last row + +```csharp +// Origin +{ + var value = new[] { + new { ID=1,Name ="Jack",InDate=new DateTime(2021,01,03)}, + new { ID=2,Name ="Henry",InDate=new DateTime(2020,05,03)}, + }; + MiniExcel.SaveAs(path, value); +} +// Insert 1 rows after last +{ + var value = new { ID=3,Name = "Mike", InDate = new DateTime(2021, 04, 23) }; + MiniExcel.Insert(path, value); +} +// Insert N rows after last +{ + var value = new[] { + new { ID=4,Name ="Frank",InDate=new DateTime(2021,06,07)}, + new { ID=5,Name ="Gloria",InDate=new DateTime(2022,05,03)}, + }; + MiniExcel.Insert(path, value); +} +``` + +![image](https://user-images.githubusercontent.com/12729184/191023733-1e2fa732-db5c-4a3a-9722-b891fe5aa069.png) + +v1.37.0 support excel insert a new sheet into an existing workbook + +```csharp +// Origin excel +{ + var value = new[] { + new { ID=1,Name ="Jack",InDate=new DateTime(2021,01,03)}, + new { ID=2,Name ="Henry",InDate=new DateTime(2020,05,03)}, + }; + MiniExcel.SaveAs(path, value, sheetName: "Sheet1"); +} +// Insert a new sheet +{ + var value = new { ID=3,Name = "Mike", InDate = new DateTime(2021, 04, 23) }; + MiniExcel.Insert(path, table, sheetName: "Sheet2"); +} +``` + + + +#### Delete(waiting) + +#### Update(waiting) + + + +### Excel Type Auto Check + +- MiniExcel will check whether it is xlsx or csv based on the `file extension` by default, but there may be inaccuracy, please specify it manually. +- Stream cannot be know from which excel, please specify it manually. + +```csharp +stream.SaveAs(excelType:ExcelType.CSV); +//or +stream.SaveAs(excelType:ExcelType.XLSX); +//or +stream.Query(excelType:ExcelType.CSV); +//or +stream.Query(excelType:ExcelType.XLSX); +``` + + + + + +### CSV + +#### Note + +- Default return `string` type, and value will not be converted to numbers or datetime, unless the type is defined by strong typing generic. + + + +#### Custom separator + +The default is `,` as the separator, you can modify the `Seperator` property for customization + +```csharp +var config = new MiniExcelLibs.Csv.CsvConfiguration() +{ + Seperator=';' +}; +MiniExcel.SaveAs(path, values,configuration: config); +``` + +Since V1.30.1 support function to custom separator (thanks @hyzx86) + +```csharp +var config = new CsvConfiguration() +{ + SplitFn = (row) => Regex.Split(row, $"[\t,](?=(?:[^\"]|\"[^\"]*\")*$)") + .Select(s => Regex.Replace(s.Replace("\"\"", "\""), "^\"|\"$", "")).ToArray() +}; +var rows = MiniExcel.Query(path, configuration: config).ToList(); +``` + + + +#### Custom line break + +The default is `\r\n` as the newline character, you can modify the `NewLine` property for customization + +```csharp +var config = new MiniExcelLibs.Csv.CsvConfiguration() +{ + NewLine='\n' +}; +MiniExcel.SaveAs(path, values,configuration: config); +``` + + + +#### Custom coding + +- The default encoding is "Detect Encoding From Byte Order Marks" (detectEncodingFromByteOrderMarks: true) +- f you have custom encoding requirements, please modify the StreamReaderFunc / StreamWriterFunc property + +```csharp +// Read +var config = new MiniExcelLibs.Csv.CsvConfiguration() +{ + StreamReaderFunc = (stream) => new StreamReader(stream,Encoding.GetEncoding("gb2312")) +}; +var rows = MiniExcel.Query(path, true,excelType:ExcelType.CSV,configuration: config); + +// Write +var config = new MiniExcelLibs.Csv.CsvConfiguration() +{ + StreamWriterFunc = (stream) => new StreamWriter(stream, Encoding.GetEncoding("gb2312")) +}; +MiniExcel.SaveAs(path, value,excelType:ExcelType.CSV, configuration: config); +``` + +#### Read empty string as null + +By default, empty values are mapped to string.Empty. You can modify this behavior + +```csharp +var config = new MiniExcelLibs.Csv.CsvConfiguration() +{ + ReadEmptyStringAsNull = true +}; +``` + + +### DataReader + +#### 1. GetReader +Since 1.23.0, you can GetDataReader + +```csharp + using (var reader = MiniExcel.GetReader(path,true)) + { + while (reader.Read()) + { + for (int i = 0; i < reader.FieldCount; i++) + { + var value = reader.GetValue(i); + } + } + } +``` + + + +### Async + +- v0.17.0 support Async (thanks isdaniel ( SHIH,BING-SIOU)](https://github.com/isdaniel)) + +```csharp +public static Task SaveAsAsync(string path, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration configuration = null) +public static Task SaveAsAsync(this Stream stream, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, IConfiguration configuration = null) +public static Task> QueryAsync(string path, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) +public static Task> QueryAsync(this Stream stream, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) where T : class, new() +public static Task> QueryAsync(string path, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) where T : class, new() +public static Task>> QueryAsync(this Stream stream, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) +public static Task SaveAsByTemplateAsync(this Stream stream, string templatePath, object value) +public static Task SaveAsByTemplateAsync(this Stream stream, byte[] templateBytes, object value) +public static Task SaveAsByTemplateAsync(string path, string templatePath, object value) +public static Task SaveAsByTemplateAsync(string path, byte[] templateBytes, object value) +public static Task QueryAsDataTableAsync(string path, bool useHeaderRow = true, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) +``` + +- v1.25.0 support `cancellationToken`。 + + + +### Others + +#### 1. Enum + +Be sure excel & property name same, system will auto mapping (case insensitive) + +![image](https://user-images.githubusercontent.com/12729184/116210595-9784b100-a775-11eb-936f-8e7a8b435961.png) + +Since V0.18.0 support Enum Description + +```csharp +public class Dto +{ + public string Name { get; set; } + public I49RYZUserType UserType { get; set; } +} + +public enum Type +{ + [Description("General User")] + V1, + [Description("General Administrator")] + V2, + [Description("Super Administrator")] + V3 +} +``` + +![image](https://user-images.githubusercontent.com/12729184/133116630-27cc7161-099a-48b8-9784-cd1e443af3d1.png) + +Since 1.30.0 version support excel Description to Enum , thanks @KaneLeung + +#### 2. Convert CSV to XLSX or Convert XLSX to CSV + +```csharp +MiniExcel.ConvertXlsxToCsv(xlsxPath, csvPath); +MiniExcel.ConvertXlsxToCsv(xlsxStream, csvStream); +MiniExcel.ConvertCsvToXlsx(csvPath, xlsxPath); +MiniExcel.ConvertCsvToXlsx(csvStream, xlsxStream); +``` +```csharp +using (var excelStream = new FileStream(path: filePath, FileMode.Open, FileAccess.Read)) +using (var csvStream = new MemoryStream()) +{ + MiniExcel.ConvertXlsxToCsv(excelStream, csvStream); +} +``` + +#### 3. Custom CultureInfo + +Since 1.22.0, you can custom CultureInfo like below, system default `CultureInfo.InvariantCulture`. + +```csharp +var config = new CsvConfiguration() +{ + Culture = new CultureInfo("fr-FR"), +}; +MiniExcel.SaveAs(path, value, configuration: config); + +// or +MiniExcel.Query(path, configuration: config); +``` + + +#### 4. Custom Buffer Size +```csharp + public abstract class Configuration : IConfiguration + { + public int BufferSize { get; set; } = 1024 * 512; + } +``` + +#### 5. FastMode + +System will not control memory, but you can get faster save speed. + +```csharp +var config = new OpenXmlConfiguration() { FastMode = true }; +MiniExcel.SaveAs(path, reader,configuration:config); +``` + +#### 6. Batch Add Image (MiniExcel.AddPicture) + +Please add pictures before batch generate rows data, or system will load large memory usage when calling AddPicture. + +```csharp +var images = new[] +{ + new MiniExcelPicture + { + ImageBytes = File.ReadAllBytes(PathHelper.GetFile("images/github_logo.png")), + SheetName = null, // default null is first sheet + CellAddress = "C3", // required + }, + new MiniExcelPicture + { + ImageBytes = File.ReadAllBytes(PathHelper.GetFile("images/google_logo.png")), + PictureType = "image/png", // default PictureType = image/png + SheetName = "Demo", + CellAddress = "C9", // required + WidthPx = 100, + HeightPx = 100, + }, +}; +MiniExcel.AddPicture(path, images); +``` +![Image](https://github.com/user-attachments/assets/19c4d241-9753-4ede-96c8-f810c1a22247) + +#### 7. Get Sheets Dimension + +```csharp +var dim = MiniExcel.GetSheetDimensions(path); +``` + +### Examples: + +#### 1. SQLite & Dapper `Large Size File` SQL Insert Avoid OOM + +note : please don't call ToList/ToArray methods after Query, it'll load all data into memory + +```csharp +using (var connection = new SQLiteConnection(connectionString)) +{ + connection.Open(); + using (var transaction = connection.BeginTransaction()) + using (var stream = File.OpenRead(path)) + { + var rows = stream.Query(); + foreach (var row in rows) + connection.Execute("insert into T (A,B) values (@A,@B)", new { row.A, row.B }, transaction: transaction); + transaction.Commit(); + } +} +``` + +performance: +![image](https://user-images.githubusercontent.com/12729184/111072579-2dda7b80-8516-11eb-9843-c01a1edc88ec.png) + + + + + +#### 2. ASP.NET Core 3.1 or MVC 5 Download/Upload Excel Xlsx API Demo [Try it](tests/MiniExcel.Tests.AspNetCore) + +```csharp +public class ApiController : Controller +{ + public IActionResult Index() + { + return new ContentResult + { + ContentType = "text/html", + StatusCode = (int)HttpStatusCode.OK, + Content = @" +DownloadExcel
+DownloadExcelFromTemplatePath
+DownloadExcelFromTemplateBytes
+

Upload Excel

+
+
+ +
+ value = new Dictionary() + { + ["title"] = "FooCompany", + ["managers"] = new[] { + new {name="Jack",department="HR"}, + new {name="Loan",department="IT"} + }, + ["employees"] = new[] { + new {name="Wade",department="HR"}, + new {name="Felix",department="HR"}, + new {name="Eric",department="IT"}, + new {name="Keaton",department="IT"} + } + }; + + MemoryStream memoryStream = new MemoryStream(); + memoryStream.SaveAsByTemplate(templatePath, value); + memoryStream.Seek(0, SeekOrigin.Begin); + return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + { + FileDownloadName = "demo.xlsx" + }; + } + + private static Dictionary TemplateBytesCache = new Dictionary(); + + static ApiController() + { + string templatePath = "TestTemplateComplex.xlsx"; + byte[] bytes = System.IO.File.ReadAllBytes(templatePath); + TemplateBytesCache.Add(templatePath, bytes); + } + + public IActionResult DownloadExcelFromTemplateBytes() + { + byte[] bytes = TemplateBytesCache["TestTemplateComplex.xlsx"]; + + Dictionary value = new Dictionary() + { + ["title"] = "FooCompany", + ["managers"] = new[] { + new {name="Jack",department="HR"}, + new {name="Loan",department="IT"} + }, + ["employees"] = new[] { + new {name="Wade",department="HR"}, + new {name="Felix",department="HR"}, + new {name="Eric",department="IT"}, + new {name="Keaton",department="IT"} + } + }; + + MemoryStream memoryStream = new MemoryStream(); + memoryStream.SaveAsByTemplate(bytes, value); + memoryStream.Seek(0, SeekOrigin.Begin); + return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + { + FileDownloadName = "demo.xlsx" + }; + } + + public IActionResult UploadExcel(IFormFile excel) + { + var stream = new MemoryStream(); + excel.CopyTo(stream); + + foreach (var item in stream.Query(true)) + { + // do your logic etc. + } + + return Ok("File uploaded successfully"); + } +} +``` + +#### 3. Paging Query + +```csharp +void Main() +{ + var rows = MiniExcel.Query(path); + + Console.WriteLine("==== No.1 Page ===="); + Console.WriteLine(Page(rows,pageSize:3,page:1)); + Console.WriteLine("==== No.50 Page ===="); + Console.WriteLine(Page(rows,pageSize:3,page:50)); + Console.WriteLine("==== No.5000 Page ===="); + Console.WriteLine(Page(rows,pageSize:3,page:5000)); +} + +public static IEnumerable Page(IEnumerable en, int pageSize, int page) +{ + return en.Skip(page * pageSize).Take(pageSize); +} +``` + +![20210419](https://user-images.githubusercontent.com/12729184/114679083-6ef4c400-9d3e-11eb-9f78-a86daa45fe46.gif) + + + +#### 4. WebForm export Excel by memorystream + +```csharp +var fileName = "Demo.xlsx"; +var sheetName = "Sheet1"; +HttpResponse response = HttpContext.Current.Response; +response.Clear(); +response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; +response.AddHeader("Content-Disposition", $"attachment;filename=\"{fileName}\""); +var values = new[] { + new { Column1 = "MiniExcel", Column2 = 1 }, + new { Column1 = "Github", Column2 = 2} +}; +var memoryStream = new MemoryStream(); +memoryStream.SaveAs(values, sheetName: sheetName); +memoryStream.Seek(0, SeekOrigin.Begin); +memoryStream.CopyTo(Response.OutputStream); +response.End(); +``` + + + +#### 5. Dynamic i18n multi-language and role authority management + +Like the example, create a method to handle i18n and permission management, and use `yield return to return IEnumerable>` to achieve dynamic and low-memory processing effects + +```csharp +void Main() +{ + var value = new Order[] { + new Order(){OrderNo = "SO01",CustomerID="C001",ProductID="P001",Qty=100,Amt=500}, + new Order(){OrderNo = "SO02",CustomerID="C002",ProductID="P002",Qty=300,Amt=400}, + }; + + Console.WriteLine("en-Us and Sales role"); + { + var path = Path.GetTempPath() + Guid.NewGuid() + ".xlsx"; + var lang = "en-US"; + var role = "Sales"; + MiniExcel.SaveAs(path, GetOrders(lang, role, value)); + MiniExcel.Query(path, true).Dump(); + } + + Console.WriteLine("zh-CN and PMC role"); + { + var path = Path.GetTempPath() + Guid.NewGuid() + ".xlsx"; + var lang = "zh-CN"; + var role = "PMC"; + MiniExcel.SaveAs(path, GetOrders(lang, role, value)); + MiniExcel.Query(path, true).Dump(); + } +} + +private IEnumerable> GetOrders(string lang, string role, Order[] orders) +{ + foreach (var order in orders) + { + var newOrder = new Dictionary(); + + if (lang == "zh-CN") + { + newOrder.Add("客户编号", order.CustomerID); + newOrder.Add("订单编号", order.OrderNo); + newOrder.Add("产品编号", order.ProductID); + newOrder.Add("数量", order.Qty); + if (role == "Sales") + newOrder.Add("价格", order.Amt); + yield return newOrder; + } + else if (lang == "en-US") + { + newOrder.Add("Customer ID", order.CustomerID); + newOrder.Add("Order No", order.OrderNo); + newOrder.Add("Product ID", order.ProductID); + newOrder.Add("Quantity", order.Qty); + if (role == "Sales") + newOrder.Add("Amount", order.Amt); + yield return newOrder; + } + else + { + throw new InvalidDataException($"lang {lang} wrong"); + } + } +} + +public class Order +{ + public string OrderNo { get; set; } + public string CustomerID { get; set; } + public decimal Qty { get; set; } + public string ProductID { get; set; } + public decimal Amt { get; set; } +} +``` + +![image](https://user-images.githubusercontent.com/12729184/118939964-d24bc480-b982-11eb-88dd-f06655f6121a.png) + + + +### FAQ + +#### Q: Excel header title not equal class property name, how to mapping? + +A. Please use ExcelColumnName attribute + +![image](https://user-images.githubusercontent.com/12729184/116020475-eac50980-a678-11eb-8804-129e87200e5e.png) + +#### Q. How to query or export multiple-sheets? + +A. `GetSheetNames` method with Query sheetName parameter. + + + +```csharp +var sheets = MiniExcel.GetSheetNames(path); +foreach (var sheet in sheets) +{ + Console.WriteLine($"sheet name : {sheet} "); + var rows = MiniExcel.Query(path,useHeaderRow:true,sheetName:sheet); + Console.WriteLine(rows); +} +``` + +![image](https://user-images.githubusercontent.com/12729184/116199841-2a1f5300-a76a-11eb-90a3-6710561cf6db.png) + +#### Q. How to query or export information about sheet visibility? + +A. `GetSheetInformations` method. + + + +```csharp +var sheets = MiniExcel.GetSheetInformations(path); +foreach (var sheetInfo in sheets) +{ + Console.WriteLine($"sheet index : {sheetInfo.Index} "); // next sheet index - numbered from 0 + Console.WriteLine($"sheet name : {sheetInfo.Name} "); // sheet name + Console.WriteLine($"sheet state : {sheetInfo.State} "); // sheet visibility state - visible / hidden +} +``` + + +#### Q. Whether to use Count will load all data into the memory? + +No, the image test has 1 million rows*10 columns of data, the maximum memory usage is <60MB, and it takes 13.65 seconds + +![image](https://user-images.githubusercontent.com/12729184/117118518-70586000-adc3-11eb-9ce3-2ba76cf8b5e5.png) + +#### Q. How does Query use integer indexs? + +The default index of Query is the string Key: A,B,C.... If you want to change to numeric index, please create the following method to convert + +```csharp +void Main() +{ + var path = @"D:\git\MiniExcel\samples\xlsx\TestTypeMapping.xlsx"; + var rows = MiniExcel.Query(path,true); + foreach (var r in ConvertToIntIndexRows(rows)) + { + Console.Write($"column 0 : {r[0]} ,column 1 : {r[1]}"); + Console.WriteLine(); + } +} + +private IEnumerable> ConvertToIntIndexRows(IEnumerable rows) +{ + ICollection keys = null; + var isFirst = true; + foreach (IDictionary r in rows) + { + if(isFirst) + { + keys = r.Keys; + isFirst = false; + } + + var dic = new Dictionary(); + var index = 0; + foreach (var key in keys) + dic[index++] = r[key]; + yield return dic; + } +} +``` + +#### Q. No title empty excel is generated when the value is empty when exporting Excel + +Because MiniExcel uses a logic similar to JSON.NET to dynamically get type from values to simplify API operations, type cannot be knew without data. You can check [issue #133](https://github.com/mini-software/MiniExcel/issues/133) for understanding. + +![image](https://user-images.githubusercontent.com/12729184/122639771-546c0c00-d12e-11eb-800c-498db27889ca.png) + +> Strong type & DataTable will generate headers, but Dictionary are still empty Excel + +#### Q. How to stop the foreach when blank row? + +MiniExcel can be used with `LINQ TakeWhile` to stop foreach iterator. + +![Image](https://user-images.githubusercontent.com/12729184/130209137-162621c2-f337-4479-9996-beeac65bc4d4.png) + +#### Q. How to remove empty rows? + +![image](https://user-images.githubusercontent.com/12729184/137873865-7107d8f5-eb59-42db-903a-44e80589f1b2.png) + + +IEnumerable : + +```csharp +public static IEnumerable QueryWithoutEmptyRow(Stream stream, bool useHeaderRow, string sheetName, ExcelType excelType, string startCell, IConfiguration configuration) +{ + var rows = stream.Query(useHeaderRow,sheetName,excelType,startCell,configuration); + foreach (IDictionary row in rows) + { + if(row.Keys.Any(key=>row[key]!=null)) + yield return row; + } +} +``` + + + +DataTable : + +```csharp +public static DataTable QueryAsDataTableWithoutEmptyRow(Stream stream, bool useHeaderRow, string sheetName, ExcelType excelType, string startCell, IConfiguration configuration) +{ + if (sheetName == null && excelType != ExcelType.CSV) /*Issue #279*/ + sheetName = stream.GetSheetNames().First(); + + var dt = new DataTable(sheetName); + var first = true; + var rows = stream.Query(useHeaderRow,sheetName,excelType,startCell,configuration); + foreach (IDictionary row in rows) + { + if (first) + { + + foreach (var key in row.Keys) + { + var column = new DataColumn(key, typeof(object)) { Caption = key }; + dt.Columns.Add(column); + } + + dt.BeginLoadData(); + first = false; + } + + var newRow = dt.NewRow(); + var isNull=true; + foreach (var key in row.Keys) + { + var _v = row[key]; + if(_v!=null) + isNull = false; + newRow[key] = _v; + } + + if(!isNull) + dt.Rows.Add(newRow); + } + + dt.EndLoadData(); + return dt; +} +``` + + + +#### Q. How SaveAs(path,value) to replace exists file and without throwing "The file ...xlsx already exists error" + + +Please use Stream class to custom file creating logic, e.g: + +```C# + using (var stream = File.Create("Demo.xlsx")) + MiniExcel.SaveAs(stream,value); +``` + + + +or, since V1.25.0, SaveAs support overwriteFile parameter for enable/unable overwriting exist file + +```csharp + MiniExcel.SaveAs(path, value, overwriteFile: true); +``` + + + + +### Limitations and caveats + +- Not support xls and encrypted file now +- xlsm only support Query + + + +### Reference + +[ExcelDataReader](https://github.com/ExcelDataReader/ExcelDataReader) / [ClosedXML](https://github.com/ClosedXML/ClosedXML) / [Dapper](https://github.com/DapperLib/Dapper) / [ExcelNumberFormat](https://github.com/andersnm/ExcelNumberFormat) + + + +### Thanks + +#### [Jetbrains](https://www.jetbrains.com/) + +![jetbrains-variant-2](https://user-images.githubusercontent.com/12729184/123997015-8456c180-da02-11eb-829a-aec476fe8e94.png) + +Thanks for providing a free All product IDE for this project ([License](https://user-images.githubusercontent.com/12729184/123988233-6ab17c00-d9fa-11eb-8739-2a08c6a4a263.png)) + + + +### Contribution sharing donate +Link https://github.com/orgs/mini-software/discussions/754 + +### Contributors + +![](https://contrib.rocks/image?repo=mini-software/MiniExcel) From 85a25aebf77b5d1d0b3467af94c86c239ffd33b2 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Tue, 12 Aug 2025 17:12:47 +0200 Subject: [PATCH 2/9] Some side warning fixes and cleanup --- src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs | 14 ++++++-------- .../Reflection/MiniExcelColumnInfo.cs | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs b/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs index 80b5c268..579600df 100644 --- a/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs @@ -103,7 +103,7 @@ public async Task InsertAsync(bool overwriteSheet = false, CancellationToke cancellationToken.ThrowIfCancellationRequested(); using var reader = await OpenXmlReader.CreateAsync(_stream, _configuration, cancellationToken: cancellationToken).ConfigureAwait(false); - var sheetRecords = (await reader.GetWorkbookRelsAsync(_archive.Entries, cancellationToken).ConfigureAwait(false)).ToArray(); + var sheetRecords = (await reader.GetWorkbookRelsAsync(_archive.Entries, cancellationToken).ConfigureAwait(false))?.ToArray() ?? []; foreach (var sheetRecord in sheetRecords.OrderBy(o => o.Id)) { cancellationToken.ThrowIfCancellationRequested(); @@ -443,7 +443,7 @@ private async Task WriteCellAsync(SafeStreamWriter writer, string cellReference, } [CreateSyncVersion] - private async Task WriteCellAsync(SafeStreamWriter writer, int rowIndex, int cellIndex, object value, MiniExcelColumnInfo columnInfo, ExcelWidthCollection? widthCollection) + private async Task WriteCellAsync(SafeStreamWriter writer, int rowIndex, int cellIndex, object? value, MiniExcelColumnInfo columnInfo, ExcelWidthCollection? widthCollection) { if (columnInfo?.CustomFormatter is not null) { @@ -458,9 +458,7 @@ private async Task WriteCellAsync(SafeStreamWriter writer, int rowIndex, int cel } var columnReference = ReferenceHelper.ConvertCoordinatesToCell(cellIndex, rowIndex); - var valueIsNull = value is null || - value is DBNull || - (_configuration.WriteEmptyStringAsNull && value is string vs && vs == string.Empty); + var valueIsNull = value is null or DBNull || (_configuration.WriteEmptyStringAsNull && value is ""); if (_configuration.EnableWriteNullValueCell && valueIsNull) { @@ -476,8 +474,7 @@ value is DBNull || var columnType = columnInfo.ExcelColumnType; /*Prefix and suffix blank space will lost after SaveAs #294*/ - var preserveSpace = cellValue is not null && ( - cellValue.StartsWith(" ") || cellValue.EndsWith(" ")); + var preserveSpace = cellValue is " " or [' ', .., ' ']; await writer.WriteAsync(WorksheetXml.Cell(columnReference, dataType, GetCellXfId(styleIndex), cellValue, preserveSpace: preserveSpace, columnType: columnType)).ConfigureAwait(false); widthCollection?.AdjustWidth(cellIndex, cellValue); @@ -632,7 +629,8 @@ private async Task InsertContentTypesXmlAsync(CancellationToken cancellationToke var typesElement = doc.Descendants(ns + "Types").Single(); var partNames = new HashSet(StringComparer.InvariantCultureIgnoreCase); - foreach (var partName in typesElement.Elements(ns + "Override").Select(s => s.Attribute("PartName").Value)) + var attrNames = typesElement.Elements(ns + "Override").Select(s => s.Attribute("PartName")?.Value); + foreach (var partName in attrNames.OfType()) { partNames.Add(partName); } diff --git a/src/MiniExcel.Core/Reflection/MiniExcelColumnInfo.cs b/src/MiniExcel.Core/Reflection/MiniExcelColumnInfo.cs index 076332a7..6b4a2d93 100644 --- a/src/MiniExcel.Core/Reflection/MiniExcelColumnInfo.cs +++ b/src/MiniExcel.Core/Reflection/MiniExcelColumnInfo.cs @@ -17,5 +17,5 @@ public class MiniExcelColumnInfo public bool ExcelIgnore { get; internal set; } public int ExcelFormatId { get; internal set; } public ColumnType ExcelColumnType { get; internal set; } - public Func? CustomFormatter { get; set; } + public Func? CustomFormatter { get; set; } } \ No newline at end of file From dc22fda1ad69f14fc74a4441cc3caae688371004 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sun, 17 Aug 2025 21:49:00 +0200 Subject: [PATCH 3/9] Fixed issue 294 resurfacing --- src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs b/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs index 579600df..7763d857 100644 --- a/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs @@ -474,7 +474,7 @@ private async Task WriteCellAsync(SafeStreamWriter writer, int rowIndex, int cel var columnType = columnInfo.ExcelColumnType; /*Prefix and suffix blank space will lost after SaveAs #294*/ - var preserveSpace = cellValue is " " or [' ', .., ' ']; + var preserveSpace = cellValue is [' ', ..] or [.., ' ']; await writer.WriteAsync(WorksheetXml.Cell(columnReference, dataType, GetCellXfId(styleIndex), cellValue, preserveSpace: preserveSpace, columnType: columnType)).ConfigureAwait(false); widthCollection?.AdjustWidth(cellIndex, cellValue); From 94a0f18bbff014078e0c3fa391f6941738ce1f27 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sun, 17 Aug 2025 21:03:39 +0200 Subject: [PATCH 4/9] Improving new readme's content and structure --- README.md | 427 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 282 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index 73286db3..3b477aaf 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,26 @@ @@ -12,27 +29,45 @@ [](https://www.dotnetfoundation.org/)
-

This project is part of the .NET Foundation and operates under their code of conduct.

+

This project is part of the .NET Foundation and operates under their code of conduct.

--- ---
- Your Stars or Donations can make MiniExcel better + If MiniExcel was useful to you please star the project and consider donating. A small gesture can make a big difference in improving the library!
--- -## Introduction - MiniExcel is a simple and efficient Excel processing tool for .NET, specifically designed to minimize memory usage. 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. @@ -54,70 +89,153 @@ flowchart LR class C1,C2,C3,C4,C5 miniexcel; ``` -## Features +### Features - 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 DLL size under 500KB +- Lightweight, without the need for Microsoft Office or COM+ components, and a DLL size under 600KB - Simple and intuitive API to read, write, and fill Excel documents -### Release Notes -You can check the release notes [here](docs). +## Usage +### Installation -### TODO +You can download the full package from [NuGet](https://www.nuget.org/packages/MiniExcel): -Check what we are planning for future versions [here](https://github.com/mini-software/MiniExcel/projects/1?fullscreen=true). +```bash +dotnet add package MiniExcel +``` -### Performance +This package will contain the assemblies with both Excel and Csv functionalities, along with the deprecated `v1.x` methods' signatures. +If you don't care for those you can also install the Excel and Csv packages separately: -The code for the benchmarks can be found in [MiniExcel.Benchmarks](benchmarks/MiniExcel.Benchmarks/Program.cs). +```bash +dotnet add package MiniExcel.Core +``` -The file used to test performance is [**Test1,000,000x10.xlsx**](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". +```bash +dotnet add package MiniExcel.Csv +``` -To run all the benchmarks use: +### Quickstart -```bash -dotnet run -project .\benchmarks\MiniExcel.Benchmarks -c Release -f net9.0 -filter * --join +#### Importing + +Firstly, you have to get an importer. The available ones are the `OpenXmlImporter` and the `CsvImporter`: +```csharp +var importer = MiniExcel.Importers.GetOpenXmlImporter(); + +// or + +var importer = MiniExcel.Importers.GetCsvImporter(); ``` -You can find the benchmarks' results for the latest release [here](benchmarks/results). +You can then use it to query Excel or csv documents as dynamic objects, or map them directly to a suitable strong type: +```csharp +var query = importer.Query(excelPath); +// or -## Get Started +var query = importer.Query(csvPath); +``` -- [Import/Query Excel](#getstarted1) -- [Export/Create Excel](#getstarted2) -- [Excel Template](#getstarted3) -- [Excel Column Name/Index/Ignore Attribute](#getstarted4) -- [Examples](#getstarted5) +Finally, you can materialize the results or enumerate them and perform some custom logic: +```csharp +var rows = query.ToList(); +// or -### Installation +foreach (var row in query) +{ + // your logic here +} +``` -You can download the full package from [NuGet](https://www.nuget.org/packages/MiniExcel): +MiniExcel also fully supports `IAsyncEnumerable`, allowing you to perform all sorts of asynchronous operations: +```csharp +var query = importer.QueryAsync(inputPath); +await foreach (var row in query) +{ + // your asynchronous logic here +} +``` -```bash -dotnet add package MiniExcel +#### Exporting + +Similarly to what was described before, the first thing you need to do is getting an exporter: +```csharp +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); + +// or + +var exporter = MiniExcel.Exporters.GetCsvExporter(); ``` -This package will contain the assemblies containing both Excel and Csv functionalities, along with the deprecated `v1.x` methods' signatures. -If you don't care for those you can also install the Excel and Csv assemblies separately: +You can then use it to create an Excel or csv document from a `IEnumerable` whose generic type can be some strong type, anonymous type or even a `IDictionary`: +```csharp +var values = new[] // can also be a strong type +{ + new { Column1 = "MiniExcel", Column2 = 1 }, + new { Column1 = "Github", Column2 = 2 } +} +exporter.Export(outputPath, values); +// or -```bash -dotnet add package MiniExcel.Core +List>() values = +[ + new() { { "Column1", "MiniExcel" }, { "Column2", 1 } }, + new() { { "Column1", "Github" }, { "Column2", 2 } } +]; +exporter.Export(outputPath, values); +``` + +The exporters also fully support asynchronous operations: +```csharp +await exporter.ExportAsync(outputPath, values); ``` +### Release Notes + +If you're migrating from a `1.x` version, please check the [upgrade notes](V2-upgrade-notes.md). + +You can check the full release notes [here](docs). + +### TODO + +Check what we are planning for future versions [here](https://github.com/mini-software/MiniExcel/projects/1?fullscreen=true). + +### Performance + +The code for the benchmarks can be found in [MiniExcel.Benchmarks](benchmarks/MiniExcel.Benchmarks/Program.cs). + +The file used to test performance is [**Test1,000,000x10.xlsx**](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". + +To run all the benchmarks use: + ```bash -dotnet add package MiniExcel.Csv +dotnet run -project .\benchmarks\MiniExcel.Benchmarks -c Release -f net9.0 -filter * --join ``` +You can find the benchmarks' results for the latest release [here](benchmarks/results). + + +## Documentation + +- [Query/Import](#docs-import) +- [Create/Export](#docs-export) +- [Excel Template](#docs-template) +- [Attributes and configuration](#docs-attributes) +- [CSV specifics](#docs-csv) +- [Other functionalities](#docs-other) +- [FAQ](#docs-faq) +- [Limitations](#docs-limitations) + -### Excel Query/Import +### Query/Import -#### 1. Execute a query and map the results to a strongly typed IEnumerable [[Try it]](https://dotnetfiddle.net/w5WD1J) +#### 1. Execute a query and map the results to a strongly typed IEnumerable ```csharp public class UserAccount @@ -130,18 +248,18 @@ public class UserAccount public decimal Points { get; set; } } -var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); -var rows = excelImporter.Query(path); +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var rows = importer.Query(path); // or using var stream = File.OpenRead(path); -var rows = excelImporter.Query(stream); +var rows = importer.Query(stream); ``` #### 2. Execute a query and map it to a list of dynamic objects -* By default no header will be used and the dynamic keys will be `.A`, `.B`, `.C`, etc... [[Try it]](https://dotnetfiddle.net/w5WD1J) +By default no header will be used and the dynamic keys will be `.A`, `.B`, `.C`, etc..: | MiniExcel | 1 | |-----------|---| @@ -157,7 +275,7 @@ var rows = excelImporter.MiniExcel.Query(path).ToList(); // rows[1].B = 2 ``` -* You can also specify that a header must be used, in which case the dynamic keys will be mapped to it. [[Try it]](https://dotnetfiddle.net/w5WD1J) +You can also specify that a header must be used, in which case the dynamic keys will be mapped to it: | Name | Value | |-----------|-------| @@ -298,11 +416,11 @@ importer.Query(path, configuration: config) ``` -### Create/Export Excel +### Create/Export Excel There are various ways to export data to an Excel document using MiniExcel. -#### 1. From anonymous or strongly typed collections [[Try it]](https://dotnetfiddle.net/w5WD1J) +#### 1. From anonymous or strongly typed collections When using an anonymous type: @@ -448,8 +566,21 @@ exporter.Export(path, sheets); ![image](https://user-images.githubusercontent.com/12729184/118130875-6e7c4580-b430-11eb-9b82-22f02716bd63.png) +#### 7. Inserting sheets + +MiniExcel supports the functionality of inserting a new sheet into an existing Excel workbook: -#### 7. Save to Stream [[Try it]](https://dotnetfiddle.net/JOen0e) +```csharp +var config = new OpenXmlConfiguration { FastMode = true }; +var value = new { ID = 3, Name = "Mike", InDate = new DateTime(2021, 04, 23) }; + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter() +exporter.InsertSheet(path, table, sheetName: "Sheet2", configuration: config); +``` +> **Note**: In order to insert worksheets FastMode must be enabled! + + +#### 8. Save to Stream You can export data directly to a `MemoryStream`, `FileStream`, and generally any stream that supports seeking: @@ -460,7 +591,7 @@ using var stream = new MemoryStream(); exporter.Export(stream, values); ``` -#### 8. TableStyles Options +#### 9. TableStyles Options Default style @@ -482,7 +613,7 @@ exporter.Export(path, value, configuration: config); ![image](https://user-images.githubusercontent.com/12729184/118784917-f3e57700-b8c2-11eb-8718-8d955b1bc197.png) -#### 9. AutoFilter +#### 10. AutoFilter By default, autofilter is enabled on the headers of exported Excel documents. You can disable this by setting the `AutoFilter` property of the configuration to `false`: @@ -493,7 +624,7 @@ var config = new OpenXmlConfiguration { AutoFilter = false }; exporter.Export(path, value, configuration: config); ``` -#### 10. Creating images +#### 11. Creating images ```csharp var exporter = MiniExcel.Exporters.GetExcelExporter(); @@ -601,19 +732,17 @@ exporter.Export("Book1.xlsx", dt, configuration: config); ![image](docs/images/freeze-pane-1.png) -### Fill Data To Excel Template +### Fill Data To Excel Template + +The declarations are similar to Vue templates `{{variable_name}}` and collection renderings `{{collection_name.field_name}}`. -- The declarations are similar to Vue templates `{{variable_name}}` and collection renderings `{{collection_name.field_name}}` -- Collection rendering supports `IEnumerable`, `DataTable` and `DapperRow` +Collection renderings support `IEnumerable`, `DataTable` and `DapperRow`. #### 1. Basic Fill Template: ![image](https://user-images.githubusercontent.com/12729184/114537556-ed8d2b00-9c84-11eb-8303-a69f62c41e5b.png) -Result: -![image](https://user-images.githubusercontent.com/12729184/114537490-d8180100-9c84-11eb-8c69-db58692f3a85.png) - Code: ```csharp // 1. By POCO @@ -638,10 +767,13 @@ var value = new Dictionary() MiniExcel.SaveAsByTemplate(path, templatePath, value); ``` +Result: +![image](https://user-images.githubusercontent.com/12729184/114537490-d8180100-9c84-11eb-8c69-db58692f3a85.png) + #### 2. IEnumerable Data Fill -> Note: Use the first IEnumerable of the same column as the basis for filling list +> Note: The first IEnumerable of the same column is the basis for filling the template Template: @@ -798,17 +930,12 @@ Result: ![image](https://user-images.githubusercontent.com/12729184/114802419-43221e80-9dd0-11eb-9ffe-a2ce34fe7076.png) -#### 5. Example : List Github Projects +#### 5. Example: List Github Projects Template ![image](https://user-images.githubusercontent.com/12729184/115068623-12073280-9f25-11eb-9124-f4b3efcdb2a7.png) - -Result - -![image](https://user-images.githubusercontent.com/12729184/115068639-1a5f6d80-9f25-11eb-9f45-27c434d19a78.png) - Code ```csharp @@ -830,6 +957,11 @@ var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); templater.ApplyTemplate(path, templatePath, value); ``` +Result: + +![image](https://user-images.githubusercontent.com/12729184/115068639-1a5f6d80-9f25-11eb-9f45-27c434d19a78.png) + + #### 6. Grouped Data Fill ```csharp @@ -887,7 +1019,7 @@ Rules: 3. Each statement should be on a new line. 4. A single space should be added before and after operators. 5. There shouldn't be any new lines inside of a statement. -6. Cells should be in the exact format as below. +6. Cells should be in the exact format as below: ```csharp @if(name == Jack) @@ -964,7 +1096,7 @@ templater.ApplyTemplate(path, templatePath, value, config) -### MiniExcel Attributes +### Attributes and configuration #### 1. Specify the column name, column index, or ignore the column entirely @@ -1058,7 +1190,7 @@ public class Dto #### 5. System.ComponentModel.DisplayNameAttribute -The `DisplayNameAttribute` has the same effect as the `MiniExcelColumnNameAttribute` +The `DisplayNameAttribute` has the same effect as the `MiniExcelColumnNameAttribute`: ```C# public class Dto @@ -1075,7 +1207,7 @@ public class Dto } ``` -#### 6. ExcelColumnAttribute +#### 6. MiniExcelColumnAttribute Multiple attributes can be simplified using the `MiniExcelColumnAttribute`: @@ -1159,81 +1291,91 @@ var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); exporter.Export(path, sheets, configuration: configuration); ``` -## CSV - -#### Note +### CSV -- Default return `string` type, and value will not be converted to numbers or datetime, unless the type is defined by strong typing generic. +> Unlike Excel queries, csv always maps values to `string` by default, unless you are querying to a strongly defined type. #### Custom separator -The default is `,` as the separator, you can modify the `Seperator` property for customization +The default separator is the comma (`,`), but you can customize it using the `CsvConfiguration.Seperator` property: ```csharp -var config = new MiniExcelLibs.Csv.CsvConfiguration() +var config = new CsvConfiguration { Seperator=';' }; -MiniExcel.SaveAs(path, values,configuration: config); + +var exporter = MiniExcel.Exporters.GetCsvExporter(); +exporter.Export(path, values, configuration: config); ``` -Since V1.30.1 support function to custom separator (thanks @hyzx86) +You also have the option to define a more complex separator: ```csharp -var config = new CsvConfiguration() +var config = new CsvConfiguration { - SplitFn = (row) => Regex.Split(row, $"[\t,](?=(?:[^\"]|\"[^\"]*\")*$)") - .Select(s => Regex.Replace(s.Replace("\"\"", "\""), "^\"|\"$", "")).ToArray() + SplitFn = row => Regex + .Split(row, $"[\t,](?=(?:[^\"]|\"[^\"]*\")*$)") + .Select(str => Regex.Replace(str.Replace("\"\"", "\""), "^\"|\"$", "")) + .ToArray() }; -var rows = MiniExcel.Query(path, configuration: config).ToList(); + +var importer = MiniExcel.Exporters.GetCsvImporter(); +var rows = importer.Query(path, configuration: config).ToList(); ``` #### Custom line break -The default is `\r\n` as the newline character, you can modify the `NewLine` property for customization +The default line break is `\r\n`, but you can customize it using the `CsvConfiguration.NewLine`: ```csharp -var config = new MiniExcelLibs.Csv.CsvConfiguration() +var config = new CsvConfiguration { NewLine='\n' }; -MiniExcel.SaveAs(path, values,configuration: config); + +var exporter = MiniExcel.Exporters.GetCsvExporter(); +exporter.Export(path, values,configuration: config); ``` #### Custom encoding -- The default encoding is "Detect Encoding From Byte Order Marks" (detectEncodingFromByteOrderMarks: true) -- If you have custom encoding requirements, please modify the StreamReaderFunc / StreamWriterFunc property +The default encoding is UTF8 with BOM. If you have custom encoding requirements you can modify the `StreamReaderFunc` and `StreamWriterFunc` properties: ```csharp // Read -var config = new MiniExcelLibs.Csv.CsvConfiguration() +var config = new CsvConfiguration { - StreamReaderFunc = (stream) => new StreamReader(stream,Encoding.GetEncoding("gb2312")) + StreamReaderFunc = stream => new StreamReader(stream,Encoding.GetEncoding("gb2312")) }; -var rows = MiniExcel.Query(path, true,excelType:ExcelType.CSV,configuration: config); + +var importer = MiniExcel.Importers.GetCsvImporter(); +var rows = importer.Query(path, useHeaderRow: true, configuration: config); // Write -var config = new MiniExcelLibs.Csv.CsvConfiguration() +var config = new CsvConfiguration { - StreamWriterFunc = (stream) => new StreamWriter(stream, Encoding.GetEncoding("gb2312")) + StreamWriterFunc = stream => new StreamWriter(stream, Encoding.GetEncoding("gb2312")) }; -MiniExcel.SaveAs(path, value,excelType:ExcelType.CSV, configuration: config); + +var exporter = MiniExcel.Exporters.GetCsvExporter(); +exporter.Export(path, value, configuration: config); ``` #### Read empty string as null -By default, empty values are mapped to string.Empty. You can modify this behavior +By default, empty values are mapped to `string.Empty`. +You can modify this behavior and map them to `null` using the `CsvConfiguration.ReadEmptyStringAsNull` property: ```csharp -var config = new MiniExcelLibs.Csv.CsvConfiguration() +var config = new CsvConfiguration { ReadEmptyStringAsNull = true }; ``` -### DataReader +#### DataReader There is support for reading one cell at a time using a custom `IDataReader`: @@ -1244,7 +1386,7 @@ using var reader = importer.GetDataReader(path, useHeaderRow: true); // or var importer = MiniExcel.Importers.GetCsvImporter(); -using var reader = MiniExcel.GetDataReader(path, useHeaderRow: true); +using var reader = importer.GetDataReader(path, useHeaderRow: true); while (reader.Read()) @@ -1256,13 +1398,8 @@ while (reader.Read()) } ``` -### Async - - -### Add, Delete, Update - -#### Add +#### Add records It is possible to append an arbitrary number of rows to a csv document: @@ -1276,28 +1413,13 @@ exporter.Append(path, value); // Insert N rows after last var value = new[] { - new { ID = 4, Name = "Frank", InDate = new DateTime(2021, 06, 07)}, - new { ID = 5, Name = "Gloria", InDate = new DateTime(2022, 05, 03)}, + new { ID = 4, Name = "Frank", InDate = new DateTime(2021, 06, 07) }, + new { ID = 5, Name = "Gloria", InDate = new DateTime(2022, 05, 03) }, }; exporter.AppendToCsv(path, value); ``` -There is also support to insert a new sheet into an existing Excel workbook: - -```csharp -var exporter = MiniExcel.Exporters.GetOpenXmlExporter() - - var config = new OpenXmlConfiguration { FastMode = true }; -var value = new { ID = 3, Name = "Mike", InDate = new DateTime(2021, 04, 23) }; -exporter.InsertSheet(path, table, sheetName: "Sheet2", configuration: config); -``` -> **Note**: In order to insert worksheets FastMode must be enabled! - -#### Delete (work in progress) - -#### Update (work in progress) - -### Others +### Other functionalities #### 1. Enums @@ -1326,8 +1448,8 @@ public enum UserTypeEnum #### 2. Convert Csv to Xlsx or vice-versa ```csharp -MiniExcel.Exporetr.GetCsvExporter().ConvertXlsxToCsv(xlsxPath, csvPath); -MiniExcel.Exporetr.GetCsvExporter().ConvertCsvToXlsx(csvPath, xlsxPath); +MiniExcel.Exporters.GetCsvExporter().ConvertXlsxToCsv(xlsxPath, csvPath); +MiniExcel.Exporters.GetCsvExporter().ConvertCsvToXlsx(csvPath, xlsxPath); // or @@ -1351,16 +1473,18 @@ var config = new CsvConfiguration #### 4. Custom Buffer Size +The default buffer size is 5MB, but you can easily customize it: ```csharp -var conf = new OpenXmlConfiguration { BufferSize = 1024 * 10 }; +var conf = new OpenXmlConfiguration { BufferSize = 1024 * 1024 * 10 }; ``` #### 5. FastMode -You can set the configuration property `FastMode` to achieve faster saving speeds, but this will make the memory consumption much higher: +You can set the configuration property `FastMode` to achieve faster saving speeds, but this will make the memory consumption much higher, so it's not recommended: ```csharp var config = new OpenXmlConfiguration { FastMode = true }; + var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); exporter.Export(path, reader, configuration: config); ``` @@ -1370,15 +1494,15 @@ exporter.Export(path, reader, configuration: config); Please add pictures before batch generating the rows' data or a large amount of memory will be used when calling `AddPicture`: ```csharp -var images = new[] -{ - new MiniExcelPicture +MiniExcelPicture[] images = +[ + new() { ImageBytes = File.ReadAllBytes(PathHelper.GetFile("images/github_logo.png")), SheetName = null, // when null it will default to the first sheet CellAddress = "C3", // required }, - new MiniExcelPicture + new() { ImageBytes = File.ReadAllBytes(PathHelper.GetFile("images/google_logo.png")), PictureType = "image/png", // image/png is the default picture type @@ -1387,8 +1511,10 @@ var images = new[] WidthPx = 100, HeightPx = 100, }, -}; -MiniExcel.AddPicture(path, images); +]; + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.AddPicture(path, images); ``` ![Image](https://github.com/user-attachments/assets/19c4d241-9753-4ede-96c8-f810c1a22247) @@ -1401,7 +1527,7 @@ var importer = MiniExcel.Importers.GetOpenXmlImporter(); var dim = importer.GetSheetDimensions(path); ``` -### FAQ +### FAQ #### Q: Excel header title is not equal to my DTO class property name, how do I map it? @@ -1420,13 +1546,13 @@ class Dto A. You can retrieve the sheet names with the `GetSheetNames` method and then Query them using the `sheetName` parameter: ```csharp -var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); -var sheetNames = excelImporter.GetSheetNames(path); +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var sheetNames = importer.GetSheetNames(path); var rows = new Dictionary>(); foreach (var sheet in sheetNames) { - rows[sheet] = excelImporter.Query(path, sheetName: sheet).ToList(); + rows[sheet] = importer.Query(path, sheetName: sheet).ToList(); } ``` @@ -1435,8 +1561,8 @@ foreach (var sheet in sheetNames) A. You can use the `GetSheetInformations` method: ```csharp -var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); -var sheets = excelImporter.GetSheetInformations(path); +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var sheets = importer.GetSheetInformations(path); foreach (var sheetInfo in sheets) { @@ -1466,21 +1592,25 @@ If you want to switch to a numeric index you can copy the following method for c ```csharp IEnumerable> ConvertToIntIndexRows(IEnumerable rows) { - ICollection keys = null; var isFirst = true; - foreach (IDictionary r in rows) + ICollection keys = []; + foreach (IDictionary row in rows) { if(isFirst) { - keys = r.Keys; + keys = row.Keys; isFirst = false; } - var dic = new Dictionary(); + var dict = new Dictionary(); + var index = 0; foreach (var key in keys) - dic[index++] = r[key]; - yield return dic; + { + dict[index++] = row[key]; + } + + yield return dict; } } ``` @@ -1519,7 +1649,7 @@ excelExporter.Export(stream,value); ``` -### Limitations and caveats +### Limitations and caveats - There is currently no support for the `.xls` legacy Excel format or for encrypted files - There is only basic query support for the `.xlsm` Excel format @@ -1532,12 +1662,19 @@ excelExporter.Export(stream,value); ### Thanks -#### [Jetbrains](https://www.jetbrains.com/) +#### Jetbrains ![jetbrains-variant-2](https://user-images.githubusercontent.com/12729184/123997015-8456c180-da02-11eb-829a-aec476fe8e94.png) -Thanks for providing a free All product IDE for this project ([License](https://user-images.githubusercontent.com/12729184/123988233-6ab17c00-d9fa-11eb-8739-2a08c6a4a263.png)) +Thanks to [**Jetbrains**](https://www.jetbrains.com/) for providing a free All product IDE for this project ([License](https://user-images.githubusercontent.com/12729184/123988233-6ab17c00-d9fa-11eb-8739-2a08c6a4a263.png)) + +#### Zomp + +![](https://avatars.githubusercontent.com/u/63680941?s=200&v=4) +Thanks to [**Zomp**](https://github.com/zompinc) and [@virzak](https://github.com/virzak) for helping us implement a new asynchronous API +and for their [sync-method-generator](https://github.com/zompinc/sync-method-generator), a great source generator +for automating the creation of synchronous functions based on asynchronous ones. ### Donations sharing [Link](https://github.com/orgs/mini-software/discussions/754) From 0f24148bccbc91afa334d14e8a3316cabe3f445e Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sun, 17 Aug 2025 21:04:32 +0200 Subject: [PATCH 5/9] Fixing minor code style issues --- src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs | 1 - .../Picture/OpenXmlPictureImplement.cs | 2 +- .../SaveByTemplate/MiniExcelTemplateTests.cs | 56 ++++++++++--------- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs b/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs index 7763d857..c4c3b7e2 100644 --- a/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs @@ -5,7 +5,6 @@ using MiniExcelLib.Core.OpenXml.Styles.Builder; using MiniExcelLib.Core.OpenXml.Zip; using MiniExcelLib.Core.WriteAdapters; -using SafeStreamWriter = MiniExcelLib.Core.Helpers.SafeStreamWriter; namespace MiniExcelLib.Core.OpenXml; diff --git a/src/MiniExcel.Core/OpenXml/Picture/OpenXmlPictureImplement.cs b/src/MiniExcel.Core/OpenXml/Picture/OpenXmlPictureImplement.cs index 602658aa..8e9d9859 100644 --- a/src/MiniExcel.Core/OpenXml/Picture/OpenXmlPictureImplement.cs +++ b/src/MiniExcel.Core/OpenXml/Picture/OpenXmlPictureImplement.cs @@ -22,7 +22,7 @@ private static bool CheckRelationshipExists(XmlDocument doc, string id, string t var xpath = $"/x:Relationships/x:Relationship[@Id='{id}' and @Type='{type}' and @Target='{target}']"; var node = doc.SelectSingleNode(xpath, namespaceManager); - return node != null; + return node is not null; } //todo: why does the sync version break everything? diff --git a/tests/MiniExcel.Core.Tests/SaveByTemplate/MiniExcelTemplateTests.cs b/tests/MiniExcel.Core.Tests/SaveByTemplate/MiniExcelTemplateTests.cs index ecbd167a..6dcabfaa 100644 --- a/tests/MiniExcel.Core.Tests/SaveByTemplate/MiniExcelTemplateTests.cs +++ b/tests/MiniExcel.Core.Tests/SaveByTemplate/MiniExcelTemplateTests.cs @@ -65,31 +65,35 @@ public void TestImageType() Assert.Equal(pictures.Length, mediaEntries.Count); // Assert (use EPPlus to verify that images are inserted correctly) - using (var package = new ExcelPackage(new FileInfo(path.FilePath))) - { - var sheet = package.Workbook.Worksheets[0]; - var picB2 = sheet.Drawings.OfType() - .FirstOrDefault(p => p.EditAs == eEditAs.Absolute); - - Assert.NotNull(picB2); - Assert.Equal(1920 * 9525, picB2.Size.Width); - Assert.Equal(1032 * 9525, picB2.Size.Height); - //Console.WriteLine("✅ AbsoluteAnchor image exists and the size is as expected (1920x1032)"); - - //Console.WriteLine("✅ Image inserted successfully (B2 - AbsoluteAnchor)"); - - // Validate image at D4 (ImgType.TwoCellAnchor) - var picD4 = sheet.Drawings.OfType() - .FirstOrDefault(p => p.EditAs == eEditAs.TwoCell && p.From != null && p.From.Column == 3 && p.From.Row == 3); - Assert.NotNull(picD4); - //Console.WriteLine("✅ Image inserted successfully (D4 - TwoCellAnchor)"); - - // Validate image at F6 (ImgType.OneCellAnchor) - var picF6 = sheet.Drawings.OfType() - .FirstOrDefault(p => p.EditAs == eEditAs.OneCell && p.From != null && p.From.Column == 5 && p.From.Row == 5); - Assert.NotNull(picF6); - //Console.WriteLine("✅ Image inserted successfully (F6 - OneCellAnchor)"); - } + using var package = new ExcelPackage(new FileInfo(path.FilePath)); + + var sheet = package.Workbook.Worksheets[0]; + var picB2 = sheet.Drawings + .OfType() + .FirstOrDefault(p => p.EditAs == eEditAs.Absolute); + + Assert.NotNull(picB2); + Assert.Equal(1920 * 9525, picB2.Size.Width); + Assert.Equal(1032 * 9525, picB2.Size.Height); + //Console.WriteLine("✅ AbsoluteAnchor image exists and the size is as expected (1920x1032)"); + + //Console.WriteLine("✅ Image inserted successfully (B2 - AbsoluteAnchor)"); + + // Validate image at D4 (ImgType.TwoCellAnchor) + var picD4 = sheet.Drawings + .OfType() + .FirstOrDefault(p => p is { EditAs: eEditAs.TwoCell, From: { Column: 3, Row: 3 } }); + + Assert.NotNull(picD4); + //Console.WriteLine("✅ Image inserted successfully (D4 - TwoCellAnchor)"); + + // Validate image at F6 (ImgType.OneCellAnchor) + var picF6 = sheet.Drawings + .OfType() + .FirstOrDefault(p => p is { EditAs: eEditAs.OneCell, From: { Column: 5, Row: 5 } }); + + Assert.NotNull(picF6); + //Console.WriteLine("✅ Image inserted successfully (F6 - OneCellAnchor)"); } } @@ -108,7 +112,7 @@ public void DatatableTemptyRowTest() employees.Columns.Add("name"); employees.Columns.Add("department"); - var value = new Dictionary() + var value = new Dictionary { ["title"] = "FooCompany", ["managers"] = managers, From 993b38fda7b7d7ccccfbe06fc3ee745d68e5859a Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sun, 17 Aug 2025 21:23:42 +0200 Subject: [PATCH 6/9] Added draft for v2 migration guide --- MiniExcel.slnx | 1 + V2-upgrade-notes.md | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 V2-upgrade-notes.md diff --git a/MiniExcel.slnx b/MiniExcel.slnx index 9549e71d..01fbde58 100644 --- a/MiniExcel.slnx +++ b/MiniExcel.slnx @@ -19,6 +19,7 @@ + diff --git a/V2-upgrade-notes.md b/V2-upgrade-notes.md new file mode 100644 index 00000000..eb7d5764 --- /dev/null +++ b/V2-upgrade-notes.md @@ -0,0 +1,12 @@ +## MiniExcel 2.0 Migration Guide + +- The root namespace was changed from `MiniExcelLibs` to `MiniExcelLib`. If the full MiniExcel package is downloaded, the previous namespace will still exist and will contain the now old and deprecated methods' signatures +- Instead of having all methods being part of the `MiniExcel` static class, the functionalities are now split into 3 providers accessible from the same class: +`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 division was adopted for export methods with `OpenXmlExporter` and `CsvExporter` +- 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 +- `IConfiguration` is now `IMiniExcelConfiguration`, but most methods now require the proper implementation (`OpenXmlConfiguration` or `CsvConfiguration`) to be provided rather than the interface +- MiniExcel now fully supports asynchronous streaming the queries, +so the return type for `OpenXmlImporter.QueryAsync` is `IAsyncEnumerable` instead of `Task>` \ No newline at end of file From afd91433e724fd6ae9c2484081f9e62a5906472b Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sun, 17 Aug 2025 22:11:22 +0200 Subject: [PATCH 7/9] Fixing typos in the readme --- README.md | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 3b477aaf..370da122 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ exporter.Export(outputPath, values); // or -List>() values = +List> values = [ new() { { "Column1", "MiniExcel" }, { "Column2", 1 } }, new() { { "Column1", "Github" }, { "Column2", 2 } } @@ -266,8 +266,8 @@ By default no header will be used and the dynamic keys will be `.A`, `.B`, `.C`, | Github | 2 | ```csharp -var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); -var rows = excelImporter.MiniExcel.Query(path).ToList(); +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var rows = importer.Query(path).ToList(); // rows[0].A = "MiniExcel" // rows[0].B = 1 @@ -284,8 +284,8 @@ You can also specify that a header must be used, in which case the dynamic keys ```csharp -var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); -var rows = excelImporter.MiniExcel.Query(path, useHeaderRow: true).ToList(); +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var rows = importer.Query(path, useHeaderRow: true).ToList(); // rows[0].Name = "MiniExcel" // rows[0].Value = 1 @@ -298,8 +298,8 @@ var rows = excelImporter.MiniExcel.Query(path, useHeaderRow: true).ToList(); e.g: Query the tenth row by skipping the first 9 and taking the first ```csharp -var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); -var tenthRow = excelImporter.Query(path).Skip(9).First(); +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var tenthRow = importer.Query(path).Skip(9).First(); ``` #### 4. Specify the Excel sheet to query from @@ -371,8 +371,8 @@ var config = new OpenXmlConfiguration FillMergedCells = true }; -var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); -var rows = MiniExcel.Query(path, configuration: config); +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var rows = importer.Query(path, configuration: config); ``` ![image](https://user-images.githubusercontent.com/12729184/117973630-3527d500-b35f-11eb-95c3-bde255f8114e.png) @@ -521,7 +521,7 @@ var cmd = new CommandDefinition( var rows = connection.Query(cmd); var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -exporter.Export("dapper_test.xslx", rows); +exporter.Export("dapper_test.xlsx", rows); ``` > **WARNING**: If you simply use `var rows = connection.Query(sql)` all data will be loaded into memory instead! @@ -550,7 +550,7 @@ var sheets = new Dictionary ["department"] = department }; -var excelExporter = MiniExcel.Exporters.GetOpenXmlExporter(); +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); exporter.Export(path, sheets); ``` @@ -574,8 +574,8 @@ MiniExcel supports the functionality of inserting a new sheet into an existing E var config = new OpenXmlConfiguration { FastMode = true }; var value = new { ID = 3, Name = "Mike", InDate = new DateTime(2021, 04, 23) }; -var exporter = MiniExcel.Exporters.GetOpenXmlExporter() -exporter.InsertSheet(path, table, sheetName: "Sheet2", configuration: config); +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.InsertSheet(path, value, sheetName: "Sheet2", configuration: config); ``` > **Note**: In order to insert worksheets FastMode must be enabled! @@ -627,7 +627,7 @@ exporter.Export(path, value, configuration: config); #### 11. Creating images ```csharp -var exporter = MiniExcel.Exporters.GetExcelExporter(); +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); var value = new[] { new { Name = "github", Image = File.ReadAllBytes("images/github_logo.png") }, @@ -683,7 +683,8 @@ Dictionary[] value = } ]; -_exporter.Export("test.xlsx", value, configuration: config); +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export("test.xlsx", value, configuration: config); ``` ![image](https://user-images.githubusercontent.com/31481586/241419455-3c0aec8a-4e5f-4d83-b7ec-6572124c165d.png) @@ -696,7 +697,8 @@ var config = new OpenXmlConfiguration EnableWriteNullValueCell = false // Default value is true }; -exporter.Export("test.xlsx", dt, configuration: config); +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export("test.xlsx", value, configuration: config); ``` ![image](https://user-images.githubusercontent.com/31481586/241419441-c4f27e8f-3f87-46db-a10f-08665864c874.png) @@ -709,7 +711,8 @@ var config = new OpenXmlConfiguration WriteEmptyStringAsNull = true // Default value is false }; -exporter.Export("test.xlsx", dt, configuration: config); +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export("test.xlsx", value, configuration: config); ``` Both properties work with `null` and `DBNull` values. From 5bd19f5d896001c87e52cd3adbb73ee066abee53 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Fri, 22 Aug 2025 22:57:21 +0200 Subject: [PATCH 8/9] Exchanged the new readme with the old one, improved docs and upgrade notes --- MiniExcel.slnx | 6 +- README-V2.md | 1686 ++++++++++++++++ README.md | 2004 +++++++++++--------- README_OLD.md | 1992 ------------------- V2-upgrade-notes.md => V2-Upgrade-Notes.md | 19 +- 5 files changed, 2857 insertions(+), 2850 deletions(-) create mode 100644 README-V2.md delete mode 100644 README_OLD.md rename V2-upgrade-notes.md => V2-Upgrade-Notes.md (56%) diff --git a/MiniExcel.slnx b/MiniExcel.slnx index 01fbde58..f111dbe4 100644 --- a/MiniExcel.slnx +++ b/MiniExcel.slnx @@ -9,17 +9,17 @@ - + - + - + diff --git a/README-V2.md b/README-V2.md new file mode 100644 index 00000000..653a7152 --- /dev/null +++ b/README-V2.md @@ -0,0 +1,1686 @@ + + +--- + +[](https://www.dotnetfoundation.org/) + +
+

This project is part of the .NET Foundation and operates under their code of conduct.

+
+ +--- + + + + +--- + +
+ If MiniExcel was useful to you please star the project and consider donating. A small gesture can make a big difference in improving the library! +
+ +--- + +MiniExcel is a simple and efficient Excel processing tool for .NET, specifically designed to minimize memory usage. + +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. + +```mermaid +flowchart LR + A1(["Excel analysis
process"]) --> A2{{"Unzipping
XLSX file"}} --> A3{{"Parsing
OpenXML"}} --> A4{{"Model
conversion"}} --> A5(["Output"]) + + 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"]) + + 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; + + class A1,A2,A3,A4,A5 analysis; + class B1,B2,B3,B4,B5 others; + class C1,C2,C3,C4,C5 miniexcel; +``` + +### Features + +- 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 DLL size under 600KB +- Simple and intuitive API to read, write, and fill Excel documents + + +## Usage +### Installation + +You can download the full package from [NuGet](https://www.nuget.org/packages/MiniExcel): + +```bash +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: + +```bash +dotnet add package MiniExcel.Core +``` + +```bash +dotnet add package MiniExcel.Csv +``` + +### Quickstart + +#### Importing + +Firstly, you have to get an importer. The available ones are the `OpenXmlImporter` and the `CsvImporter`: +```csharp +var importer = MiniExcel.Importers.GetOpenXmlImporter(); + +// or + +var importer = MiniExcel.Importers.GetCsvImporter(); +``` + +You can then use it to query Excel or csv documents as dynamic objects, or map them directly to a suitable strong type: +```csharp +var query = importer.Query(excelPath); + +// or + +var query = importer.Query(csvPath); +``` + +Finally, you can materialize the results or enumerate them and perform some custom logic: +```csharp +var rows = query.ToList(); + +// or + +foreach (var row in query) +{ + // your logic here +} +``` + +MiniExcel also fully supports `IAsyncEnumerable`, allowing you to perform all sorts of asynchronous operations: +```csharp +var query = importer.QueryAsync(inputPath); +await foreach (var row in query) +{ + // your asynchronous logic here +} +``` + +#### Exporting + +Similarly to what was described before, the first thing you need to do is getting an exporter: +```csharp +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); + +// or + +var exporter = MiniExcel.Exporters.GetCsvExporter(); +``` + +You can then use it to create an Excel or csv document from a `IEnumerable` whose generic type can be some strong type, anonymous type or even a `IDictionary`: +```csharp +var values = new[] // can also be a strong type +{ + new { Column1 = "MiniExcel", Column2 = 1 }, + new { Column1 = "Github", Column2 = 2 } +} +exporter.Export(outputPath, values); + +// or + +List> values = +[ + new() { { "Column1", "MiniExcel" }, { "Column2", 1 } }, + new() { { "Column1", "Github" }, { "Column2", 2 } } +]; +exporter.Export(outputPath, values); +``` + +The exporters also fully support asynchronous operations: +```csharp +await exporter.ExportAsync(outputPath, values); +``` + +### Release Notes + +If you're migrating from a `1.x` version, please check the [upgrade notes](V2-upgrade-notes.md). + +You can check the full release notes [here](docs). + +### TODO + +Check what we are planning for future versions [here](https://github.com/mini-software/MiniExcel/projects/1?fullscreen=true). + +### Performance + +The code for the benchmarks can be found in [MiniExcel.Benchmarks](benchmarks/MiniExcel.Benchmarks/Program.cs). + +The file used to test performance is [**Test1,000,000x10.xlsx**](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". + +To run all the benchmarks use: + +```bash +dotnet run -project .\benchmarks\MiniExcel.Benchmarks -c Release -f net9.0 -filter * --join +``` + +You can find the benchmarks' results for the latest release [here](benchmarks/results). + + +## Documentation + +- [Query/Import](#docs-import) +- [Create/Export](#docs-export) +- [Excel Template](#docs-template) +- [Attributes and configuration](#docs-attributes) +- [CSV specifics](#docs-csv) +- [Other functionalities](#docs-other) +- [FAQ](#docs-faq) +- [Limitations](#docs-limitations) + + +### Query/Import + +#### 1. Execute a query and map the results to a strongly typed IEnumerable + +```csharp +public class UserAccount +{ + public Guid ID { get; set; } + public string Name { get; set; } + public DateTime BoD { get; set; } + public int Age { get; set; } + public bool VIP { get; set; } + public decimal Points { get; set; } +} + +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var rows = importer.Query(path); + +// or + +using var stream = File.OpenRead(path); +var rows = importer.Query(stream); +``` + +#### 2. Execute a query and map it to a list of dynamic objects + +By default no header will be used and the dynamic keys will be `.A`, `.B`, `.C`, etc..: + +| MiniExcel | 1 | +|-----------|---| +| Github | 2 | + +```csharp +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var rows = importer.Query(path).ToList(); + +// rows[0].A = "MiniExcel" +// rows[0].B = 1 +// rows[1].A = "Github" +// rows[1].B = 2 +``` + +You can also specify that a header must be used, in which case the dynamic keys will be mapped to it: + +| Name | Value | +|-----------|-------| +| MiniExcel | 1 | +| Github | 2 | + + +```csharp +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var rows = importer.Query(path, useHeaderRow: true).ToList(); + +// rows[0].Name = "MiniExcel" +// rows[0].Value = 1 +// rows[1].Name = "Github" +// rows[1].Value = 2 +``` + +#### 3. Query Support for LINQ extensions First/Take/Skip etc... + +e.g: Query the tenth row by skipping the first 9 and taking the first + +```csharp +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var tenthRow = importer.Query(path).Skip(9).First(); +``` + +#### 4. Specify the Excel sheet to query from + +```csharp +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +excelImporter.Query(path, sheetName: "SheetName"); +``` + +#### 5. Get the sheets' names from an Excel workbook + +```csharp +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +var sheetNames = excelImporter.GetSheetNames(path); +``` + +#### 6. Get the columns' names from an Excel sheet + +```csharp +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +var columns = excelImporter.GetColumnNames(path); + +// columns = [ColumnName1, ColumnName2, ...] when there is a header row +// columns = ["A","B",...] otherwise +``` + +#### 7. Casting dynamic rows to IDictionary + +Under the hood the dynamic objects returned in a query are implemented using `ExpandoObject`, +making it possible to cast them to `IDictionary`: + +```csharp +var excelimporter = MiniExcel.Importers.GetOpenXmlImporter(); + +var rows = excelImporter.Query(path).Cast>(); + +// or + +foreach(IDictionary row in excelImporter.Query(path)) +{ + // your logic here +} +``` + +#### 8. Query Excel sheet as a DataTable + +This is not recommended, as `DataTable` will forcibly load all data into memory, effectively losing the advantages MiniExcel offers. + +```csharp +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +var table = excelImporter.QueryAsDataTable(path); +``` + +#### 9. Specify what cell to start reading data from + +```csharp +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +excelImporter.Query(path, startCell: "B3") +``` +![image](https://user-images.githubusercontent.com/12729184/117260316-8593c400-ae81-11eb-9877-c087b7ac2b01.png) + +#### 10. Fill Merged Cells + +If the Excel sheet being queried contains merged cells it is possble to enable the option to fill every row with the merged value. + +```csharp +var config = new OpenXmlConfiguration +{ + FillMergedCells = true +}; + +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var rows = importer.Query(path, configuration: config); +``` + +![image](https://user-images.githubusercontent.com/12729184/117973630-3527d500-b35f-11eb-95c3-bde255f8114e.png) + +Filling of cells with variable width and height is also supported + +![image](https://user-images.githubusercontent.com/12729184/117973820-6d2f1800-b35f-11eb-88d8-555063938108.png) + +>Note: The performance will take a hit when enabling the feature. +>This happens because in the OpenXml standard the `mergeCells` are indicated at the bottom of the file, which leads to the need of reading the whole sheet twice. + +#### 11. Big files and disk-based cache + +If the SharedStrings file size exceeds 5 MB, MiniExcel will default to use a local disk cache. +E.g: on the file [10x100000.xlsx](https://github.com/MiniExcel/MiniExcel/files/8403819/NotDuplicateSharedStrings_10x100000.xlsx) (one million rows of data), when disabling the disk cache the maximum memory usage is 195 MB, but with disk cache enabled only 65 MB of memory are used. +> Note: this optimization is not without cost. In the above example it increased reading times from 7 seconds to 27 seconds roughly. + +If you prefer you can disable the disk cache with the following code: + +```csharp +var config = new OpenXmlConfiguration +{ + EnableSharedStringCache = false +}; + +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +importer.Query(path, configuration: config) +``` + +You can use also change the disk caching triggering file size beyond the default 5 MB: + +```csharp +var config = new OpenXmlConfiguration +{ + // the size has to be specified in bytes + SharedStringCacheSize = 10 * 1024 * 1024 +}; + +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +importer.Query(path, configuration: config) +``` + + +### Create/Export Excel + +There are various ways to export data to an Excel document using MiniExcel. + +#### 1. From anonymous or strongly typed collections + +When using an anonymous type: + +```csharp +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +var values = new[] +{ + new { Column1 = "MiniExcel", Column2 = 1 }, + new { Column1 = "Github", Column2 = 2} +} +exporter.Export(path, values); +``` + +When using a strong type it must be non-abstract with a public parameterless constructor: + +```csharp +class ExportTest +{ + public string Column1 { get; set; } + public int Column2 { get; set; } +} + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +ExportTest[] values = +[ + new() { Column1 = "MiniExcel", Column2 = 1 }, + new() { Column1 = "Github", Column2 = 2} +] +exporter.Export(path, values); +``` + +#### 2. From a IEnumerable> + +```csharp +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +List>() values = +[ + new() { { "Column1", "MiniExcel" }, { "Column2", 1 } }, + new() { { "Column1", "Github" }, { "Column2", 2 } } +]; +exporter.Export(path, values); +``` + +Result: + +| Column1 | Column2 | +|-----------|---------| +| MiniExcel | 1 | +| Github | 2 | + + +#### 3. IDataReader +MiniExcel supports exporting data directly from a `IDataReader` without the need to load the data into memory first. + +E.g. using the data reader returned by Dapper's `ExecuteReader` extension method: + +```csharp +using var connection = YourDbConnection(); +connection.Open(); +var reader = connection.ExecuteReader("SELECT 'MiniExcel' AS Column1, 1 as Column2 UNION ALL SELECT 'Github', 2"); + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export("Demo.xlsx", reader); +``` + +#### 4. Datatable + +>**WARNING**: Not recommended, this will load all data into memory + +For `DataTable` use you have to add column names manually before adding the rows: + +```csharp +var table = new DataTable(); + +table.Columns.Add("Column1", typeof(string)); +table.Columns.Add("Column2", typeof(decimal)); + +table.Rows.Add("MiniExcel", 1); +table.Rows.Add("Github", 2); + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export("test.xlsx", table); +``` + +#### 5. Dapper Query + +Thanks to @shaofing (PR #552), by instatiating a `CommandDefinition` with the flag `CommandFlags.NoCache`, you can pass a Dapper query directly in the `Export` function instead of the corresponding `IDataReader`: + +```csharp +using var connection = YourDbConnection(); + +var cmd = new CommandDefinition( + "SELECT 'MiniExcel' AS Column1, 1 as Column2 UNION ALL SELECT 'Github', 2", + flags: CommandFlags.NoCache) +); + +// Note: QueryAsync will throw a closed connection exception +var rows = connection.Query(cmd); + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export("dapper_test.xlsx", rows); +``` +> **WARNING**: If you simply use `var rows = connection.Query(sql)` all data will be loaded into memory instead! + + +#### 6. Create Multiple Sheets + +It is possible to create multiple sheets at the same time, using a `Dictionary` or `DataSet`: + +```csharp +// 1. Dictionary +var users = new[] +{ + new { Name = "Jack", Age = 25 }, + new { Name = "Mike", Age = 44 } +}; + +var department = new[] +{ + new { ID = "01", Name = "HR" }, + new { ID = "02", Name = "IT" } +}; + +var sheets = new Dictionary +{ + ["users"] = users, + ["department"] = department +}; + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export(path, sheets); +``` + +```csharp +// 2. DataSet +var sheets = new DataSet(); +sheets.Tables.Add(UsersDataTable); +sheets.Tables.Add(DepartmentDataTable); + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export(path, sheets); +``` + +![image](https://user-images.githubusercontent.com/12729184/118130875-6e7c4580-b430-11eb-9b82-22f02716bd63.png) + +#### 7. Inserting sheets + +MiniExcel supports the functionality of inserting a new sheet into an existing Excel workbook: + +```csharp +var config = new OpenXmlConfiguration { FastMode = true }; +var value = new { ID = 3, Name = "Mike", InDate = new DateTime(2021, 04, 23) }; + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.InsertSheet(path, value, sheetName: "Sheet2", configuration: config); +``` +> **Note**: In order to insert worksheets FastMode must be enabled! + + +#### 8. Save to Stream + +You can export data directly to a `MemoryStream`, `FileStream`, and generally any stream that supports seeking: + +```csharp +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); + +using var stream = new MemoryStream(); +exporter.Export(stream, values); +``` + +#### 9. TableStyles Options + +Default style + +![image](https://user-images.githubusercontent.com/12729184/138234373-cfa97109-b71f-4711-b7f5-0eaaa4a0a3a6.png) + +Without style configuration + +```csharp +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); + +var config = new OpenXmlConfiguration +{ + TableStyles = TableStyles.None +}; + +exporter.Export(path, value, configuration: config); +``` + +![image](https://user-images.githubusercontent.com/12729184/118784917-f3e57700-b8c2-11eb-8718-8d955b1bc197.png) + + +#### 10. AutoFilter + +By default, autofilter is enabled on the headers of exported Excel documents. +You can disable this by setting the `AutoFilter` property of the configuration to `false`: + +```csharp +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +var config = new OpenXmlConfiguration { AutoFilter = false }; +exporter.Export(path, value, configuration: config); +``` + +#### 11. Creating images + +```csharp +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +var value = new[] +{ + new { Name = "github", Image = File.ReadAllBytes("images/github_logo.png") }, + new { Name = "google", Image = File.ReadAllBytes("images/google_logo.png") }, + new { Name = "microsoft", Image = File.ReadAllBytes("images/microsoft_logo.png") }, + new { Name = "reddit", Image = File.ReadAllBytes("images/reddit_logo.png") }, + new { Name = "statck_overflow", Image = File.ReadAllBytes("images/statck_overflow_logo.png") } +}; +exporter.Export(path, value); +``` + +![image](https://user-images.githubusercontent.com/12729184/150462383-ad9931b3-ed8d-4221-a1d6-66f799743433.png) + +Whenever you export a property of type `byte[]` it will be archived as an internal resource and the cell will contain a link to it. +When queried, the resource will be converted back to `byte[]`. If you don't need this functionality you can disable it by setting the configuration property `EnableConvertByteArray` to `false` and gain some performance. + +![image](https://user-images.githubusercontent.com/12729184/153702334-c3b834f4-6ae4-4ddf-bd4e-e5005d5d8c6a.png) + +#### 12. Merge same cells vertically + +This functionality merges cells vertically between the tags `@merge` and `@endmerge`. +You can use `@mergelimit` to limit boundaries of merging cells vertically. + +```csharp +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); +templater.MergeSameCells(mergedFilePath, templatePath); +``` + +File content before and after merge without merge limit: + +Screenshot 2023-08-07 at 11 59 24 + +Screenshot 2023-08-07 at 11 59 57 + +File content before and after merge with merge limit: + +Screenshot 2023-08-08 at 18 21 00 + +Screenshot 2023-08-08 at 18 21 40 + +#### 13. Null values handling + +By default, null values will be treated as empty strings when exporting: + +```csharp +Dictionary[] value = +[ + new() + { + ["Name1"] = "Somebody once", + ["Name2"] = null, + ["Name3"] = "told me." + } +]; + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export("test.xlsx", value, configuration: config); +``` + +![image](https://user-images.githubusercontent.com/31481586/241419455-3c0aec8a-4e5f-4d83-b7ec-6572124c165d.png) + +If you want you can change this behaviour in the configuration: + +```csharp +var config = new OpenXmlConfiguration +{ + EnableWriteNullValueCell = false // Default value is true +}; + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export("test.xlsx", value, configuration: config); +``` + +![image](https://user-images.githubusercontent.com/31481586/241419441-c4f27e8f-3f87-46db-a10f-08665864c874.png) + +Similarly, there is an option to let empty strings be treated as null values: + +```csharp +var config = new OpenXmlConfiguration +{ + WriteEmptyStringAsNull = true // Default value is false +}; + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export("test.xlsx", value, configuration: config); +``` + +Both properties work with `null` and `DBNull` values. + +#### 14. Freeze Panes + +MiniExcel allows you to freeze both rows and columns in place: + +```csharp +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +var config = new OpenXmlConfiguration +{ + FreezeRowCount = 1, // default is 1 + FreezeColumnCount = 2 // default is 0 +}; + +exporter.Export("Book1.xlsx", dt, configuration: config); +``` + +![image](docs/images/freeze-pane-1.png) + + +### Fill Data To Excel Template + +The declarations are similar to Vue templates `{{variable_name}}` and collection renderings `{{collection_name.field_name}}`. + +Collection renderings support `IEnumerable`, `DataTable` and `DapperRow`. + +#### 1. Basic Fill + +Template: +![image](https://user-images.githubusercontent.com/12729184/114537556-ed8d2b00-9c84-11eb-8303-a69f62c41e5b.png) + +Code: +```csharp +// 1. By POCO +var value = new +{ + Name = "Jack", + CreateDate = new DateTime(2021, 01, 01), + VIP = true, + Points = 123 +}; +MiniExcel.SaveAsByTemplate(path, templatePath, value); + + +// 2. By Dictionary +var value = new Dictionary() +{ + ["Name"] = "Jack", + ["CreateDate"] = new DateTime(2021, 01, 01), + ["VIP"] = true, + ["Points"] = 123 +}; +MiniExcel.SaveAsByTemplate(path, templatePath, value); +``` + +Result: +![image](https://user-images.githubusercontent.com/12729184/114537490-d8180100-9c84-11eb-8c69-db58692f3a85.png) + + +#### 2. IEnumerable Data Fill + +> Note: The first IEnumerable of the same column is the basis for filling the template + +Template: + +![image](https://user-images.githubusercontent.com/12729184/114564652-14f2f080-9ca3-11eb-831f-09e3fedbc5fc.png) + +Code: +```csharp +//1. By POCO +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); +var value = new +{ + employees = new[] + { + new { name = "Jack", department = "HR" }, + new { name = "Lisa", department = "HR" }, + new { name = "John", department = "HR" }, + new { name = "Mike", department = "IT" }, + new { name = "Neo", department = "IT" }, + new { name = "Loan", department = "IT" } + } +}; +templater.ApplyTemplate(path, templatePath, value); + +//2. By Dictionary +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); +var value = new Dictionary() +{ + ["employees"] = new[] + { + new { name = "Jack", department = "HR" }, + new { name = "Lisa", department = "HR" }, + new { name = "John", department = "HR" }, + new { name = "Mike", department = "IT" }, + new { name = "Neo", department = "IT" }, + new { name = "Loan", department = "IT" } + } +}; +templater.ApplyTemplate(path, templatePath, value); +``` + +Result: + +![image](https://user-images.githubusercontent.com/12729184/114564204-b2015980-9ca2-11eb-900d-e21249f93f7c.png) + + +#### 3. Complex Data Fill + +Template: + +![image](https://user-images.githubusercontent.com/12729184/114565255-acf0da00-9ca3-11eb-8a7f-8131b2265ae8.png) + + +Code: + +```csharp +// 1. By POCO +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); +var value = new +{ + title = "FooCompany", + managers = new[] + { + new { name = "Jack", department = "HR" }, + new { name = "Loan", department = "IT" } + }, + employees = new[] + { + new { name = "Wade", department = "HR" }, + new { name = "Felix", department = "HR" }, + new { name = "Eric", department = "IT" }, + new { name = "Keaton", department = "IT" } + } +}; +templater.ApplyTemplate(path, templatePath, value); + +// 2. By Dictionary +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); +var value = new Dictionary() +{ + ["title"] = "FooCompany", + ["managers"] = new[] + { + new { name = "Jack", department = "HR" }, + new { name = "Loan", department = "IT" } + }, + ["employees"] = new[] + { + new { name = "Wade", department = "HR" }, + new { name = "Felix", department = "HR" }, + new { name = "Eric", department = "IT" }, + new { name = "Keaton", department = "IT" } + } +}; +templater.ApplyTemplate(path, templatePath, value); +``` + +Result: + +![image](https://user-images.githubusercontent.com/12729184/114565329-bf6b1380-9ca3-11eb-85e3-3969e8bf6378.png) + + +#### 4. Cell value auto mapping type + +Template: + +![image](https://user-images.githubusercontent.com/12729184/114802504-64830a80-9dd0-11eb-8d56-8e8c401b3ace.png) + +Model: + +```csharp +public class Poco +{ + public string @string { get; set; } + public int? @int { get; set; } + public decimal? @decimal { get; set; } + public double? @double { get; set; } + public DateTime? datetime { get; set; } + public bool? @bool { get; set; } + public Guid? Guid { get; set; } +} +``` + +Code: + +```csharp +var poco = new Poco +{ + @string = "string", + @int = 123, + @decimal = 123.45M, + @double = 123.33D, + datetime = new DateTime(2021, 4, 1), + @bool = true, + Guid = Guid.NewGuid() +}; + +var value = new +{ + Ts = new[] + { + poco, + new TestIEnumerableTypePoco{}, + null, + poco + } +}; + +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); +templater.ApplyTemplate(path, templatePath, value); +``` + +Result: + +![image](https://user-images.githubusercontent.com/12729184/114802419-43221e80-9dd0-11eb-9ffe-a2ce34fe7076.png) + + +#### 5. Example: List Github Projects + +Template + +![image](https://user-images.githubusercontent.com/12729184/115068623-12073280-9f25-11eb-9124-f4b3efcdb2a7.png) + +Code + +```csharp +var projects = new[] +{ + new { Name = "MiniExcel", Link = "https://github.com/mini-software/MiniExcel", Star=146, CreateTime = new DateTime(2021,03,01) }, + new { Name = "HtmlTableHelper", Link = "https://github.com/mini-software/HtmlTableHelper", Star=16, CreateTime = new DateTime(2020,02,01) }, + new { Name = "PocoClassGenerator", Link = "https://github.com/mini-software/PocoClassGenerator", Star=16, CreateTime = new DateTime(2019,03,17)} +}; + +var value = new +{ + User = "ITWeiHan", + Projects = projects, + TotalStar = projects.Sum(s => s.Star) +}; + +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); +templater.ApplyTemplate(path, templatePath, value); +``` + +Result: + +![image](https://user-images.githubusercontent.com/12729184/115068639-1a5f6d80-9f25-11eb-9f45-27c434d19a78.png) + + +#### 6. Grouped Data Fill + +```csharp +var value = new Dictionary() +{ + ["employees"] = new[] + { + new { name = "Jack", department = "HR" }, + new { name = "Jack", department = "HR" }, + new { name = "John", department = "HR" }, + new { name = "John", department = "IT" }, + new { name = "Neo", department = "IT" }, + new { name = "Loan", department = "IT" } + } +}; + +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); +templater.ApplyTemplate(path, templatePath, value); +``` +- With `@group` tag and with `@header` tag + +Before: + +![before_with_header](https://user-images.githubusercontent.com/38832863/218646717-21b9d57a-2be2-4e9a-801b-ae212231d2b4.PNG) + +After: + +![after_with_header](https://user-images.githubusercontent.com/38832863/218646721-58a7a340-7004-4bc2-af24-cffcb2c20737.PNG) + +- With `@group` tag and without `@header` tag + +Before: + +![before_without_header](https://user-images.githubusercontent.com/38832863/218646873-b12417fa-801b-4890-8e96-669ed3b43902.PNG) + +After; + +![after_without_header](https://user-images.githubusercontent.com/38832863/218646872-622461ba-342e-49ee-834f-b91ad9c2dac3.PNG) + +- Without `@group` tag + +Before: + +![without_group](https://user-images.githubusercontent.com/38832863/218646975-f52a68eb-e031-43b5-abaa-03b67c052d1a.PNG) + +After: + +![without_group_after](https://user-images.githubusercontent.com/38832863/218646974-4a3c0e07-7c66-4088-ad07-b4ad3695b7e1.PNG) + +#### 7. If/ElseIf/Else Statements inside cell + +Rules: +1. Supports `DateTime`, `double` and `int` with `==`, `!=`, `>`, `>=`,`<`, `<=` operators. +2. Supports `string` with `==`, `!=` operators. +3. Each statement should be on a new line. +4. A single space should be added before and after operators. +5. There shouldn't be any new lines inside of a statement. +6. Cells should be in the exact format as below: + +```csharp +@if(name == Jack) +{{employees.name}} +@elseif(name == Neo) +Test {{employees.name}} +@else +{{employees.department}} +@endif +``` + +Before: + +![if_before](https://user-images.githubusercontent.com/38832863/235360606-ca654769-ff55-4f5b-98d2-d2ec0edb8173.PNG) + +After: + +![if_after](https://user-images.githubusercontent.com/38832863/235360609-869bb960-d63d-45ae-8d64-9e8b0d0ab658.PNG) + +#### 8. DataTable as parameter + +```csharp +var managers = new DataTable(); +{ + managers.Columns.Add("name"); + managers.Columns.Add("department"); + managers.Rows.Add("Jack", "HR"); + managers.Rows.Add("Loan", "IT"); +} + +var value = new Dictionary() +{ + ["title"] = "FooCompany", + ["managers"] = managers, +}; + +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); +templater.ApplyTemplate(path, templatePath, value); +``` +#### 9. Formulas +Prefix your formula with `$` and use `$enumrowstart` and `$enumrowend` to mark references to the enumerable start and end rows: + +![image](docs/images/template-formulas-1.png) + +When the template is rendered, the `$` prefix will be removed and `$enumrowstart` and `$enumrowend` will be replaced with the start and end row numbers of the enumerable: + +![image](docs/images/template-formulas-2.png) + +_Other examples_: + +| Formula | Example | +|---------|------------------------------------------------------------------------------------------------| +| Sum | `$=SUM(C{{$enumrowstart}}:C{{$enumrowend}})` | +| Count | `COUNT(C{{$enumrowstart}}:C{{$enumrowend}})` | +| Range | `$=MAX(C{{$enumrowstart}}:C{{$enumrowend}}) - MIN(C{{$enumrowstart}}:C{{$enumrowend}})` | + + +#### 10. Checking template parameter key + +When a parameter key is missing it will be replaced with an empty string by default. +You can change this behaviour to throw an exception by setting the `IgnoreTemplateParameterMissing` configuration property: + +```csharp +var config = new OpenXmlConfiguration +{ + IgnoreTemplateParameterMissing = false, +}; + +var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); +templater.ApplyTemplate(path, templatePath, value, config) +``` + +![image](https://user-images.githubusercontent.com/12729184/157464332-e316f829-54aa-4c84-a5aa-9aef337b668d.png) + + + +### Attributes and configuration + +#### 1. Specify the column name, column index, or ignore the column entirely + +![image](https://user-images.githubusercontent.com/12729184/114230869-3e163700-99ac-11eb-9a90-2039d4b4b313.png) + +```csharp +public class ExcelAttributeDemo +{ + [MiniExcelColumnName("Column1")] + public string Test1 { get; set; } + + [MiniExcelColumnName("Column2")] + public string Test2 { get; set; } + + [MiniExcelIgnore] + public string Test3 { get; set; } + + [MiniExcelColumnIndex("I")] // "I" will be converted to index 8 + public string Test4 { get; set; } + + public string Test5 { get; } // properties wihout a setter will be ignored + public string Test6 { get; private set; } // properties with a non public setter will be ignored + + [MiniExcelColumnIndex(3)] // Indexes are 0-based + public string Test7 { get; set; } +} + +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var rows = importer.Query(path).ToList(); + +// rows[0].Test1 = "Column1" +// rows[0].Test2 = "Column2" +// rows[0].Test3 = null +// rows[0].Test4 = "Test7" +// rows[0].Test5 = null +// rows[0].Test6 = null +// rows[0].Test7 = "Test4" +``` + + +#### 2. Custom Formatting + +```csharp +public class Dto +{ + public string Name { get; set; } + + [MiniExcelFormat("MMMM dd, yyyy")] + public DateTime InDate { get; set; } +} + +var value = new Dto[] +{ + new Issue241Dto{ Name = "Jack", InDate = new DateTime(2021, 01, 04) }, + new Issue241Dto{ Name = "Henry", InDate = new DateTime(2020, 04, 05) } +}; + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export(path, value); +``` + +Result: + +![image](https://user-images.githubusercontent.com/12729184/118910788-ab2bcd80-b957-11eb-8d42-bfce36621b1b.png) + + +#### 3. Set Column Width + +```csharp +public class Dto +{ + [MiniExcelColumnWidth(20)] + public int ID { get; set; } + + [MiniExcelColumnWidth(15.50)] + public string Name { get; set; } +} +``` + +#### 4. Multiple column names mapping to the same property. + +```csharp +public class Dto +{ + public string Name { get; set; } + + [MiniExcelColumnName(columnName: "EmployeeNo", aliases: ["EmpNo", "No"])] + public string Empno { get; set; } +} +``` + +#### 5. System.ComponentModel.DisplayNameAttribute + +The `DisplayNameAttribute` has the same effect as the `MiniExcelColumnNameAttribute`: + +```C# +public class Dto +{ + public int ID { get; set; } + + public string Name { get; set; } + + [DisplayName("Specification")] + public string Spc { get; set; } + + [DisplayName("Unit Price")] + public decimal Up { get; set; } +} +``` + +#### 6. MiniExcelColumnAttribute + +Multiple attributes can be simplified using the `MiniExcelColumnAttribute`: + +```csharp +public class Dto +{ + [MiniExcelColumn(Name = "ID",Index =0)] + public string MyProperty { get; set; } + + [MiniExcelColumn(Name = "CreateDate", Index = 1, Format = "yyyy-MM", Width = 100)] + public DateTime MyProperty2 { get; set; } +} +``` + +#### 7. DynamicColumnAttribute + +Attributes can also be set on columns dynamically: +```csharp +var config = new OpenXmlConfiguration +{ + 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"} + ] +}; + +var value = new[] { new { id = 1, name = "Jack", createdate = new DateTime(2022, 04, 12), point = 123.456 } }; + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export(path, value, configuration: config); +``` + +#### 8. MiniExcelSheetAttribute + +It is possible to define the name and visibility of a sheet through the `MiniExcelSheetAttribute`: + +```csharp +[MiniExcelSheet(Name = "Departments", State = SheetState.Hidden)] +private class DepartmentDto +{ + [MiniExcelColumn(Name = "ID",Index = 0)] + public string ID { get; set; } + + [MiniExcelColumn(Name = "Name",Index = 1)] + public string Name { get; set; } +} +``` +It is also possible to do it dynamically through the `DynamicSheets` property of the `OpenXmlConfiguration`: + +```csharp +var configuration = new OpenXmlConfiguration +{ + DynamicSheets = + [ + new DynamicExcelSheet("usersSheet") { Name = "Users", State = SheetState.Visible }, + new DynamicExcelSheet("departmentSheet") { Name = "Departments", State = SheetState.Hidden } + ] +}; + +var users = new[] +{ + new { Name = "Jack", Age = 25 }, + new { Name = "Mike", Age = 44 } +}; +var department = new[] +{ + new { ID = "01", Name = "HR" }, + new { ID = "02", Name = "IT" } +}; + +var sheets = new Dictionary +{ + ["usersSheet"] = users, + ["departmentSheet"] = department +}; + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export(path, sheets, configuration: configuration); +``` + +### CSV + +> Unlike Excel queries, csv always maps values to `string` by default, unless you are querying to a strongly defined type. + +#### Custom separator + +The default separator is the comma (`,`), but you can customize it using the `CsvConfiguration.Seperator` property: + +```csharp +var config = new CsvConfiguration +{ + Seperator=';' +}; + +var exporter = MiniExcel.Exporters.GetCsvExporter(); +exporter.Export(path, values, configuration: config); +``` + +You also have the option to define a more complex separator: + +```csharp +var config = new CsvConfiguration +{ + SplitFn = row => Regex + .Split(row, $"[\t,](?=(?:[^\"]|\"[^\"]*\")*$)") + .Select(str => Regex.Replace(str.Replace("\"\"", "\""), "^\"|\"$", "")) + .ToArray() +}; + +var importer = MiniExcel.Exporters.GetCsvImporter(); +var rows = importer.Query(path, configuration: config).ToList(); +``` + +#### Custom line break + +The default line break is `\r\n`, but you can customize it using the `CsvConfiguration.NewLine`: + +```csharp +var config = new CsvConfiguration +{ + NewLine='\n' +}; + +var exporter = MiniExcel.Exporters.GetCsvExporter(); +exporter.Export(path, values,configuration: config); +``` + +#### Custom encoding + +The default encoding is UTF8 with BOM. If you have custom encoding requirements you can modify the `StreamReaderFunc` and `StreamWriterFunc` properties: + +```csharp +// Read +var config = new CsvConfiguration +{ + StreamReaderFunc = stream => new StreamReader(stream,Encoding.GetEncoding("gb2312")) +}; + +var importer = MiniExcel.Importers.GetCsvImporter(); +var rows = importer.Query(path, useHeaderRow: true, configuration: config); + +// Write +var config = new CsvConfiguration +{ + StreamWriterFunc = stream => new StreamWriter(stream, Encoding.GetEncoding("gb2312")) +}; + +var exporter = MiniExcel.Exporters.GetCsvExporter(); +exporter.Export(path, value, configuration: config); +``` + +#### Read empty string as null + +By default, empty values are mapped to `string.Empty`. +You can modify this behavior and map them to `null` using the `CsvConfiguration.ReadEmptyStringAsNull` property: + +```csharp +var config = new CsvConfiguration +{ + ReadEmptyStringAsNull = true +}; +``` + + +#### DataReader + +There is support for reading one cell at a time using a custom `IDataReader`: + +```csharp +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +using var reader = importer.GetDataReader(path, useHeaderRow: true); + +// or + +var importer = MiniExcel.Importers.GetCsvImporter(); +using var reader = importer.GetDataReader(path, useHeaderRow: true); + + +while (reader.Read()) +{ + for (int i = 0; i < reader.FieldCount; i++) + { + var value = reader.GetValue(i); + } +} +``` + + +#### Add records + +It is possible to append an arbitrary number of rows to a csv document: + +```csharp +var exporter = MiniExcel.Exporters.GetCsvExporter(); + +// Insert 1 rows after last +var value = new { ID = 3, Name = "Mike", InDate = new DateTime(2021, 04, 23) }; +exporter.Append(path, value); + +// Insert N rows after last +var value = new[] +{ + new { ID = 4, Name = "Frank", InDate = new DateTime(2021, 06, 07) }, + new { ID = 5, Name = "Gloria", InDate = new DateTime(2022, 05, 03) }, +}; +exporter.AppendToCsv(path, value); +``` + +### Other functionalities + +#### 1. Enums + +The serialization of enums is supported. Enum fields are mapped case insensitively. + +The use of the `DescriptionAttribute` is also supported to map enum properties: + +```csharp +public class Dto +{ + public string Name { get; set; } + public UserTypeEnum UserType { get; set; } +} + +public enum UserTypeEnum +{ + [Description("General User")] V1, + [Description("General Administrator")] V2, + [Description("Super Administrator")] V3 +} +``` + +![image](https://user-images.githubusercontent.com/12729184/133116630-27cc7161-099a-48b8-9784-cd1e443af3d1.png) + + +#### 2. Convert Csv to Xlsx or vice-versa + +```csharp +MiniExcel.Exporters.GetCsvExporter().ConvertXlsxToCsv(xlsxPath, csvPath); +MiniExcel.Exporters.GetCsvExporter().ConvertCsvToXlsx(csvPath, xlsxPath); + +// or + +using (var excelStream = new FileStream(path: filePath, FileMode.Open, FileAccess.Read)) +using (var csvStream = new MemoryStream()) +{ + MiniExcel.ConvertXlsxToCsv(excelStream, csvStream); +} +``` + +#### 3. Custom CultureInfo + +You can customise CultureInfo used by MiniExcel through the `Culture` configuration parameter. The default is `CultureInfo.InvariantCulture`: + +```csharp +var config = new CsvConfiguration +{ + Culture = new CultureInfo("fr-FR"), +}; +``` + +#### 4. Custom Buffer Size + +The default buffer size is 5MB, but you can easily customize it: +```csharp +var conf = new OpenXmlConfiguration { BufferSize = 1024 * 1024 * 10 }; +``` + +#### 5. FastMode + +You can set the configuration property `FastMode` to achieve faster saving speeds, but this will make the memory consumption much higher, so it's not recommended: + +```csharp +var config = new OpenXmlConfiguration { FastMode = true }; + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.Export(path, reader, configuration: config); +``` + +#### 6. Adding images in batch + +Please add pictures before batch generating the rows' data or a large amount of memory will be used when calling `AddPicture`: + +```csharp +MiniExcelPicture[] images = +[ + new() + { + ImageBytes = File.ReadAllBytes(PathHelper.GetFile("images/github_logo.png")), + SheetName = null, // when null it will default to the first sheet + CellAddress = "C3", // required + }, + new() + { + ImageBytes = File.ReadAllBytes(PathHelper.GetFile("images/google_logo.png")), + PictureType = "image/png", // image/png is the default picture type + SheetName = "Demo", + CellAddress = "C9", + WidthPx = 100, + HeightPx = 100, + }, +]; + +var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +exporter.AddPicture(path, images); +``` +![Image](https://github.com/user-attachments/assets/19c4d241-9753-4ede-96c8-f810c1a22247) + +#### 7. Get Sheets Dimensions + +You can easily retrieve the dimensions of all worksheets of an Excel file: + +```csharp +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var dim = importer.GetSheetDimensions(path); +``` + +### FAQ + +#### Q: Excel header title is not equal to my DTO class property name, how do I map it? + +A. You can use the `MiniExcelColumnName` attribute on the property you want to map: + +```csharp +class Dto +{ + [MiniExcelColumnName("ExcelPropertyName")] + public string MyPropertyName { get; set;} +} +``` + +#### Q. How do I query multiple sheets of an Excel file? + +A. You can retrieve the sheet names with the `GetSheetNames` method and then Query them using the `sheetName` parameter: + +```csharp +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var sheetNames = importer.GetSheetNames(path); + +var rows = new Dictionary>(); +foreach (var sheet in sheetNames) +{ + rows[sheet] = importer.Query(path, sheetName: sheet).ToList(); +} +``` + +#### Q. Can I retrieve informations about what sheets are visible or active? + +A. You can use the `GetSheetInformations` method: + +```csharp +var importer = MiniExcel.Importers.GetOpenXmlImporter(); +var sheets = importer.GetSheetInformations(path); + +foreach (var sheetInfo in sheets) +{ + Console.WriteLine($"sheet index : {sheetInfo.Index} "); // next sheet index - numbered from 0 + Console.WriteLine($"sheet name : {sheetInfo.Name} "); // sheet name + Console.WriteLine($"sheet state : {sheetInfo.State} "); // sheet visibility state - visible / hidden + Console.WriteLine($"sheet active : {sheetInfo.Active} "); // whether the sheet is currently marked as active +} +``` + +#### Q. Is there a way to count all rows from a sheet without having to query it first? + +A. Yes, you can use the method `GetSheetDimensions`: + +```csharp +var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); +var dimensions = excelImporter.GetSheetDimensions(path); + +Console.WriteLine($"Total rows: {dimensions[0].Rows.Count}"); +``` + +#### Q. Is it possible to use integer indexes for the columns? + +A. The default indexes of a MiniExcel Query are the strings "A", "B", "C"... +If you want to switch to a numeric index you can copy the following method for converting them: + +```csharp +IEnumerable> ConvertToIntIndexRows(IEnumerable rows) +{ + var isFirst = true; + ICollection keys = []; + foreach (IDictionary row in rows) + { + if(isFirst) + { + keys = row.Keys; + isFirst = false; + } + + var dict = new Dictionary(); + + var index = 0; + foreach (var key in keys) + { + dict[index++] = row[key]; + } + + yield return dict; + } +} +``` + +#### Q. Why is no header generated when trying to export an empty enumerable? + +A. MiniExcel uses reflection to dynamically get the type from the values. If there's no data to begin with, the header is also skipped. You can check [issue 133](https://github.com/mini-software/MiniExcel/issues/133) for details. + +#### Q. How to stop iterating after a blank row is hit? + +A. LINQ's `TakeWhile` extension method can be used for this purpose. + +#### Q. Some of the rows in my document are empty, can they be removed automatically? + +A. Yes, simply set the `IgnoreEmptyRows` property of the `OpenXmlConfiguration`. + + +#### Q. How do I overwrite an existing file when exporting a document without the operation throwing `IOException`? + +A. You have to use the `overwriteFile` parameter for overwriting an existing file on disk: + +```csharp +var excelExporter = MiniExcel.Exporters.GetOpenXmlExporter(); +excelExporter.Export(path, value, overwriteFile: true); +``` + +You can also implement your own stream for finer grained control: + +```csharp +var excelExporter = MiniExcel.Exporters.GetOpenXmlExporter(); + +using var stream = File.Create("Demo.xlsx"); +excelExporter.Export(stream,value); +``` + + +### Limitations and caveats + +- There is currently no support for the `.xls` legacy Excel format or for encrypted files +- There is only basic query support for the `.xlsm` Excel format + + +### References + +[ExcelDataReader](https://github.com/ExcelDataReader/ExcelDataReader) / [ClosedXML](https://github.com/ClosedXML/ClosedXML) / [Dapper](https://github.com/DapperLib/Dapper) / [ExcelNumberFormat](https://github.com/andersnm/ExcelNumberFormat) + + +### Thanks + +#### Jetbrains + +![jetbrains-variant-2](https://user-images.githubusercontent.com/12729184/123997015-8456c180-da02-11eb-829a-aec476fe8e94.png) + +Thanks to [**Jetbrains**](https://www.jetbrains.com/) for providing a free All product IDE for this project ([License](https://user-images.githubusercontent.com/12729184/123988233-6ab17c00-d9fa-11eb-8739-2a08c6a4a263.png)) + +#### Zomp + +![](https://avatars.githubusercontent.com/u/63680941?s=200&v=4) + +Thanks to [**Zomp**](https://github.com/zompinc) and [@virzak](https://github.com/virzak) for helping us implement a new asynchronous API +and for their [sync-method-generator](https://github.com/zompinc/sync-method-generator), a great source generator +for automating the creation of synchronous functions based on asynchronous ones. + +### Donations sharing +[Link](https://github.com/orgs/mini-software/discussions/754) + + +### Contributors + +![](https://contrib.rocks/image?repo=mini-software/MiniExcel) \ No newline at end of file diff --git a/README.md b/README.md index 370da122..f6647478 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,9 @@ @@ -29,45 +12,27 @@ [](https://www.dotnetfoundation.org/)
-

This project is part of the .NET Foundation and operates under their code of conduct.

+

This project is part of the .NET Foundation and operates under their code of conduct.

--- ---
- If MiniExcel was useful to you please star the project and consider donating. A small gesture can make a big difference in improving the library! + Your Stars or Donations can make MiniExcel better
--- +### Introduction + MiniExcel is a simple and efficient Excel processing tool for .NET, specifically designed to minimize memory usage. 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. @@ -94,117 +59,42 @@ flowchart LR - 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 DLL size under 600KB -- Simple and intuitive API to read, write, and fill Excel documents - - -## Usage -### Installation - -You can download the full package from [NuGet](https://www.nuget.org/packages/MiniExcel): - -```bash -dotnet add package MiniExcel -``` - -This package will contain the assemblies with both Excel and Csv functionalities, along with the deprecated `v1.x` methods' signatures. -If you don't care for those you can also install the Excel and Csv packages separately: - -```bash -dotnet add package MiniExcel.Core -``` - -```bash -dotnet add package MiniExcel.Csv -``` - -### Quickstart +- Lightweight, without the need for Microsoft Office or COM+ components, and a DLL size under 500KB +- Simple and intuitive API style to read/write/fill excel -#### Importing +### Version 2.0 preview -Firstly, you have to get an importer. The available ones are the `OpenXmlImporter` and the `CsvImporter`: -```csharp -var importer = MiniExcel.Importers.GetOpenXmlImporter(); - -// or - -var importer = MiniExcel.Importers.GetCsvImporter(); -``` - -You can then use it to query Excel or csv documents as dynamic objects, or map them directly to a suitable strong type: -```csharp -var query = importer.Query(excelPath); - -// or +We are working on a future MiniExcel version, with a new modular and focused API, +separate nuget packages for Core and Csv funcionalities, full support for asynchronously streamed queries through `IAsyncEnumerable`, +and more to come soon! The packages are gonna be available in pre-release, so feel free to check them out and give us some feedback! -var query = importer.Query(csvPath); -``` - -Finally, you can materialize the results or enumerate them and perform some custom logic: -```csharp -var rows = query.ToList(); - -// or +If you do, make sure to also check out the [new docs](README-V2.md) and the [upgrade notes](V2-Upgrade-Notes.md). -foreach (var row in query) -{ - // your logic here -} -``` -MiniExcel also fully supports `IAsyncEnumerable`, allowing you to perform all sorts of asynchronous operations: -```csharp -var query = importer.QueryAsync(inputPath); -await foreach (var row in query) -{ - // your asynchronous logic here -} -``` +### Get Started -#### Exporting +- [Import/Query Excel](#getstart1) -Similarly to what was described before, the first thing you need to do is getting an exporter: -```csharp -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); +- [Export/Create Excel](#getstart2) -// or +- [Excel Template](#getstart3) -var exporter = MiniExcel.Exporters.GetCsvExporter(); -``` +- [Excel Column Name/Index/Ignore Attribute](#getstart4) -You can then use it to create an Excel or csv document from a `IEnumerable` whose generic type can be some strong type, anonymous type or even a `IDictionary`: -```csharp -var values = new[] // can also be a strong type -{ - new { Column1 = "MiniExcel", Column2 = 1 }, - new { Column1 = "Github", Column2 = 2 } -} -exporter.Export(outputPath, values); +- [Examples](#getstart5) -// or -List> values = -[ - new() { { "Column1", "MiniExcel" }, { "Column2", 1 } }, - new() { { "Column1", "Github" }, { "Column2", 2 } } -]; -exporter.Export(outputPath, values); -``` +### Installation -The exporters also fully support asynchronous operations: -```csharp -await exporter.ExportAsync(outputPath, values); -``` +You can install the package [from NuGet](https://www.nuget.org/packages/MiniExcel) ### Release Notes -If you're migrating from a `1.x` version, please check the [upgrade notes](V2-upgrade-notes.md). - -You can check the full release notes [here](docs). +Please Check [Release Notes](docs) ### TODO -Check what we are planning for future versions [here](https://github.com/mini-software/MiniExcel/projects/1?fullscreen=true). +Please Check [TODO](https://github.com/mini-software/MiniExcel/projects/1?fullscreen=true) ### Performance @@ -221,21 +111,11 @@ dotnet run -project .\benchmarks\MiniExcel.Benchmarks -c Release -f net9.0 -filt You can find the benchmarks' results for the latest release [here](benchmarks/results). -## Documentation +### Excel Query/Import -- [Query/Import](#docs-import) -- [Create/Export](#docs-export) -- [Excel Template](#docs-template) -- [Attributes and configuration](#docs-attributes) -- [CSV specifics](#docs-csv) -- [Other functionalities](#docs-other) -- [FAQ](#docs-faq) -- [Limitations](#docs-limitations) +#### 1. Execute a query and map the results to a strongly typed IEnumerable [[Try it]](https://dotnetfiddle.net/w5WD1J) - -### Query/Import - -#### 1. Execute a query and map the results to a strongly typed IEnumerable +Recommand to use Stream.Query because of better efficiency. ```csharp public class UserAccount @@ -248,223 +128,235 @@ public class UserAccount public decimal Points { get; set; } } -var importer = MiniExcel.Importers.GetOpenXmlImporter(); -var rows = importer.Query(path); +var rows = MiniExcel.Query(path); -// or +// or -using var stream = File.OpenRead(path); -var rows = importer.Query(stream); +using (var stream = File.OpenRead(path)) + var rows = stream.Query(); ``` -#### 2. Execute a query and map it to a list of dynamic objects +![image](https://user-images.githubusercontent.com/12729184/111107423-c8c46b80-8591-11eb-982f-c97a2dafb379.png) + +#### 2. Execute a query and map it to a list of dynamic objects without using head [[Try it]](https://dotnetfiddle.net/w5WD1J) -By default no header will be used and the dynamic keys will be `.A`, `.B`, `.C`, etc..: +* dynamic key is `A.B.C.D..` | MiniExcel | 1 | |-----------|---| | Github | 2 | ```csharp -var importer = MiniExcel.Importers.GetOpenXmlImporter(); -var rows = importer.Query(path).ToList(); -// rows[0].A = "MiniExcel" -// rows[0].B = 1 -// rows[1].A = "Github" -// rows[1].B = 2 +var rows = MiniExcel.Query(path).ToList(); + +// or +using (var stream = File.OpenRead(path)) +{ + var rows = stream.Query().ToList(); + + Assert.Equal("MiniExcel", rows[0].A); + Assert.Equal(1, rows[0].B); + Assert.Equal("Github", rows[1].A); + Assert.Equal(2, rows[1].B); +} ``` -You can also specify that a header must be used, in which case the dynamic keys will be mapped to it: +#### 3. Execute a query with first header row [[Try it]](https://dotnetfiddle.net/w5WD1J) + +note : same column name use last right one -| Name | Value | -|-----------|-------| -| MiniExcel | 1 | -| Github | 2 | +Input Excel : + +| Column1 | Column2 | +|-----------|---------| +| MiniExcel | 1 | +| Github | 2 | ```csharp -var importer = MiniExcel.Importers.GetOpenXmlImporter(); -var rows = importer.Query(path, useHeaderRow: true).ToList(); -// rows[0].Name = "MiniExcel" -// rows[0].Value = 1 -// rows[1].Name = "Github" -// rows[1].Value = 2 -``` +var rows = MiniExcel.Query(useHeaderRow:true).ToList(); -#### 3. Query Support for LINQ extensions First/Take/Skip etc... +// or -e.g: Query the tenth row by skipping the first 9 and taking the first +using (var stream = File.OpenRead(path)) +{ + var rows = stream.Query(useHeaderRow:true).ToList(); -```csharp -var importer = MiniExcel.Importers.GetOpenXmlImporter(); -var tenthRow = importer.Query(path).Skip(9).First(); + Assert.Equal("MiniExcel", rows[0].Column1); + Assert.Equal(1, rows[0].Column2); + Assert.Equal("Github", rows[1].Column1); + Assert.Equal(2, rows[1].Column2); +} ``` -#### 4. Specify the Excel sheet to query from +#### 4. Query Support LINQ Extension First/Take/Skip ...etc +Query First ```csharp -var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); -excelImporter.Query(path, sheetName: "SheetName"); +var row = MiniExcel.Query(path).First(); +Assert.Equal("HelloWorld", row.A); + +// or + +using (var stream = File.OpenRead(path)) +{ + var row = stream.Query().First(); + Assert.Equal("HelloWorld", row.A); +} ``` -#### 5. Get the sheets' names from an Excel workbook +Performance between MiniExcel/ExcelDataReader/ClosedXML/EPPlus +![queryfirst](https://user-images.githubusercontent.com/12729184/111072392-6037a900-8515-11eb-9693-5ce2dad1e460.gif) + +#### 5. Query by sheet name ```csharp -var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); -var sheetNames = excelImporter.GetSheetNames(path); +MiniExcel.Query(path, sheetName: "SheetName"); +//or +stream.Query(sheetName: "SheetName"); ``` -#### 6. Get the columns' names from an Excel sheet +#### 6. Query all sheet name and rows ```csharp -var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); -var columns = excelImporter.GetColumnNames(path); - -// columns = [ColumnName1, ColumnName2, ...] when there is a header row -// columns = ["A","B",...] otherwise +var sheetNames = MiniExcel.GetSheetNames(path); +foreach (var sheetName in sheetNames) +{ + var rows = MiniExcel.Query(path, sheetName: sheetName); +} ``` -#### 7. Casting dynamic rows to IDictionary - -Under the hood the dynamic objects returned in a query are implemented using `ExpandoObject`, -making it possible to cast them to `IDictionary`: +#### 7. Get Columns ```csharp -var excelimporter = MiniExcel.Importers.GetOpenXmlImporter(); +var columns = MiniExcel.GetColumns(path); // e.g result : ["A","B"...] -var rows = excelImporter.Query(path).Cast>(); +var cnt = columns.Count; // get column count +``` -// or +#### 8. Dynamic Query cast row to `IDictionary` -foreach(IDictionary row in excelImporter.Query(path)) +```csharp +foreach(IDictionary row in MiniExcel.Query(path)) { - // your logic here + //.. } + +// or +var rows = MiniExcel.Query(path).Cast>(); +// or Query specified ranges (capitalized) +// A2 represents the second row of column A, C3 represents the third row of column C +// If you don't want to restrict rows, just don't include numbers +var rows = MiniExcel.QueryRange(path, startCell: "A2", endCell: "C3").Cast>(); ``` -#### 8. Query Excel sheet as a DataTable -This is not recommended, as `DataTable` will forcibly load all data into memory, effectively losing the advantages MiniExcel offers. -```csharp -var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); -var table = excelImporter.QueryAsDataTable(path); +#### 9. Query Excel return DataTable + +Not recommended, because DataTable will load all data into memory and lose MiniExcel's low memory consumption feature. + +```C# +var table = MiniExcel.QueryAsDataTable(path, useHeaderRow: true); ``` -#### 9. Specify what cell to start reading data from +![image](https://user-images.githubusercontent.com/12729184/116673475-07917200-a9d6-11eb-947e-a6f68cce58df.png) + + + +#### 10. Specify the cell to start reading data ```csharp -var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); -excelImporter.Query(path, startCell: "B3") +MiniExcel.Query(path,useHeaderRow:true,startCell:"B3") ``` + ![image](https://user-images.githubusercontent.com/12729184/117260316-8593c400-ae81-11eb-9877-c087b7ac2b01.png) -#### 10. Fill Merged Cells -If the Excel sheet being queried contains merged cells it is possble to enable the option to fill every row with the merged value. -```csharp -var config = new OpenXmlConfiguration -{ - FillMergedCells = true -}; +#### 11. Fill Merged Cells + +Note: The efficiency is slower compared to `not using merge fill` + +Reason: The OpenXml standard puts mergeCells at the bottom of the file, which leads to the need to foreach the sheetxml twice -var importer = MiniExcel.Importers.GetOpenXmlImporter(); -var rows = importer.Query(path, configuration: config); +```csharp + var config = new OpenXmlConfiguration() + { + FillMergedCells = true + }; + var rows = MiniExcel.Query(path, configuration: config); ``` ![image](https://user-images.githubusercontent.com/12729184/117973630-3527d500-b35f-11eb-95c3-bde255f8114e.png) -Filling of cells with variable width and height is also supported +support variable length and width multi-row and column filling ![image](https://user-images.githubusercontent.com/12729184/117973820-6d2f1800-b35f-11eb-88d8-555063938108.png) ->Note: The performance will take a hit when enabling the feature. ->This happens because in the OpenXml standard the `mergeCells` are indicated at the bottom of the file, which leads to the need of reading the whole sheet twice. - -#### 11. Big files and disk-based cache - -If the SharedStrings file size exceeds 5 MB, MiniExcel will default to use a local disk cache. -E.g: on the file [10x100000.xlsx](https://github.com/MiniExcel/MiniExcel/files/8403819/NotDuplicateSharedStrings_10x100000.xlsx) (one million rows of data), when disabling the disk cache the maximum memory usage is 195 MB, but with disk cache enabled only 65 MB of memory are used. -> Note: this optimization is not without cost. In the above example it increased reading times from 7 seconds to 27 seconds roughly. +#### 12. Reading big file by disk-base cache (Disk-Base Cache - SharedString) -If you prefer you can disable the disk cache with the following code: +If the SharedStrings size exceeds 5 MB, MiniExcel default will use local disk cache, e.g, [10x100000.xlsx](https://github.com/MiniExcel/MiniExcel/files/8403819/NotDuplicateSharedStrings_10x100000.xlsx)(one million rows data), when disable disk cache the maximum memory usage is 195MB, but able disk cache only needs 65MB. Note, this optimization needs some efficiency cost, so this case will increase reading time from 7.4 seconds to 27.2 seconds, If you don't need it that you can disable disk cache with the following code: ```csharp -var config = new OpenXmlConfiguration -{ - EnableSharedStringCache = false -}; +var config = new OpenXmlConfiguration { EnableSharedStringCache = false }; +MiniExcel.Query(path,configuration: config) +``` -var importer = MiniExcel.Importers.GetOpenXmlImporter(); -importer.Query(path, configuration: config) +You can use `SharedStringCacheSize ` to change the sharedString file size beyond the specified size for disk caching +```csharp +var config = new OpenXmlConfiguration { SharedStringCacheSize=500*1024*1024 }; +MiniExcel.Query(path, configuration: config); ``` -You can use also change the disk caching triggering file size beyond the default 5 MB: -```csharp -var config = new OpenXmlConfiguration -{ - // the size has to be specified in bytes - SharedStringCacheSize = 10 * 1024 * 1024 -}; +![image](https://user-images.githubusercontent.com/12729184/161411851-1c3f72a7-33b3-4944-84dc-ffc1d16747dd.png) + +![image](https://user-images.githubusercontent.com/12729184/161411825-17f53ec7-bef4-4b16-b234-e24799ea41b0.png) + + + -var importer = MiniExcel.Importers.GetOpenXmlImporter(); -importer.Query(path, configuration: config) -``` -### Create/Export Excel -There are various ways to export data to an Excel document using MiniExcel. -#### 1. From anonymous or strongly typed collections -When using an anonymous type: +### Create/Export Excel + +1. Must be a non-abstract type with a public parameterless constructor . + +2. MiniExcel support parameter IEnumerable Deferred Execution, If you want to use least memory, please do not call methods such as ToList + +e.g : ToList or not memory usage +![image](https://user-images.githubusercontent.com/12729184/112587389-752b0b00-8e38-11eb-8a52-cfb76c57e5eb.png) + + + +#### 1. Anonymous or strongly type [[Try it]](https://dotnetfiddle.net/w5WD1J) ```csharp -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -var values = new[] -{ +var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); +MiniExcel.SaveAs(path, new[] { new { Column1 = "MiniExcel", Column2 = 1 }, new { Column1 = "Github", Column2 = 2} -} -exporter.Export(path, values); +}); ``` -When using a strong type it must be non-abstract with a public parameterless constructor: +#### 2. `IEnumerable>` ```csharp -class ExportTest +var values = new List>() { - public string Column1 { get; set; } - public int Column2 { get; set; } -} - -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -ExportTest[] values = -[ - new() { Column1 = "MiniExcel", Column2 = 1 }, - new() { Column1 = "Github", Column2 = 2} -] -exporter.Export(path, values); -``` - -#### 2. From a IEnumerable> - -```csharp -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -List>() values = -[ - new() { { "Column1", "MiniExcel" }, { "Column2", 1 } }, - new() { { "Column1", "Github" }, { "Column2", 2 } } -]; -exporter.Export(path, values); + new Dictionary{{ "Column1", "MiniExcel" }, { "Column2", 1 } }, + new Dictionary{{ "Column1", "Github" }, { "Column2", 2 } } +}; +MiniExcel.SaveAs(path, values); ``` -Result: +Create File Result : | Column1 | Column2 | |-----------|---------| @@ -472,126 +364,131 @@ Result: | Github | 2 | -#### 3. IDataReader -MiniExcel supports exporting data directly from a `IDataReader` without the need to load the data into memory first. +#### 3. IDataReader +- `Recommended`, it can avoid to load all data into memory +```csharp +MiniExcel.SaveAs(path, reader); +``` -E.g. using the data reader returned by Dapper's `ExecuteReader` extension method: +![image](https://user-images.githubusercontent.com/12729184/121275378-149a5e80-c8bc-11eb-85fe-5453552134f0.png) -```csharp -using var connection = YourDbConnection(); -connection.Open(); -var reader = connection.ExecuteReader("SELECT 'MiniExcel' AS Column1, 1 as Column2 UNION ALL SELECT 'Github', 2"); +DataReader export multiple sheets (recommand by Dapper ExecuteReader) -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -exporter.Export("Demo.xlsx", reader); +```csharp +using (var cnn = Connection) +{ + cnn.Open(); + var sheets = new Dictionary(); + sheets.Add("sheet1", cnn.ExecuteReader("select 1 id")); + sheets.Add("sheet2", cnn.ExecuteReader("select 2 id")); + MiniExcel.SaveAs("Demo.xlsx", sheets); +} ``` + + #### 4. Datatable ->**WARNING**: Not recommended, this will load all data into memory +- `Not recommended`, it will load all data into memory -For `DataTable` use you have to add column names manually before adding the rows: +- DataTable use Caption for column name first, then use columname ```csharp +var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); var table = new DataTable(); +{ + table.Columns.Add("Column1", typeof(string)); + table.Columns.Add("Column2", typeof(decimal)); + table.Rows.Add("MiniExcel", 1); + table.Rows.Add("Github", 2); +} -table.Columns.Add("Column1", typeof(string)); -table.Columns.Add("Column2", typeof(decimal)); - -table.Rows.Add("MiniExcel", 1); -table.Rows.Add("Github", 2); - -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -exporter.Export("test.xlsx", table); +MiniExcel.SaveAs(path, table); ``` #### 5. Dapper Query -Thanks to @shaofing (PR #552), by instatiating a `CommandDefinition` with the flag `CommandFlags.NoCache`, you can pass a Dapper query directly in the `Export` function instead of the corresponding `IDataReader`: +Thanks @shaofing #552 , please use `CommandDefinition + CommandFlags.NoCache` ```csharp -using var connection = YourDbConnection(); - -var cmd = new CommandDefinition( - "SELECT 'MiniExcel' AS Column1, 1 as Column2 UNION ALL SELECT 'Github', 2", - flags: CommandFlags.NoCache) -); +using (var connection = GetConnection(connectionString)) +{ + var rows = connection.Query( + new CommandDefinition( + @"select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2" + , flags: CommandFlags.NoCache) + ); + // Note: QueryAsync will throw close connection exception + MiniExcel.SaveAs(path, rows); +} +``` -// Note: QueryAsync will throw a closed connection exception -var rows = connection.Query(cmd); +Below code will load all data into memory -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -exporter.Export("dapper_test.xlsx", rows); +```csharp +using (var connection = GetConnection(connectionString)) +{ + var rows = connection.Query(@"select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2"); + MiniExcel.SaveAs(path, rows); +} ``` -> **WARNING**: If you simply use `var rows = connection.Query(sql)` all data will be loaded into memory instead! - -#### 6. Create Multiple Sheets -It is possible to create multiple sheets at the same time, using a `Dictionary` or `DataSet`: +#### 6. SaveAs to MemoryStream [[Try it]](https://dotnetfiddle.net/JOen0e) ```csharp -// 1. Dictionary -var users = new[] +using (var stream = new MemoryStream()) //support FileStream,MemoryStream ect. { - new { Name = "Jack", Age = 25 }, - new { Name = "Mike", Age = 44 } -}; + stream.SaveAs(values); +} +``` -var department = new[] +e.g : api of export excel + +```csharp +public IActionResult DownloadExcel() { - new { ID = "01", Name = "HR" }, - new { ID = "02", Name = "IT" } -}; + var values = new[] { + new { Column1 = "MiniExcel", Column2 = 1 }, + new { Column1 = "Github", Column2 = 2} + }; + + var memoryStream = new MemoryStream(); + memoryStream.SaveAs(values); + memoryStream.Seek(0, SeekOrigin.Begin); + return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + { + FileDownloadName = "demo.xlsx" + }; +} +``` + + +#### 7. Create Multiple Sheets +```csharp +// 1. Dictionary +var users = new[] { new { Name = "Jack", Age = 25 }, new { Name = "Mike", Age = 44 } }; +var department = new[] { new { ID = "01", Name = "HR" }, new { ID = "02", Name = "IT" } }; var sheets = new Dictionary { ["users"] = users, ["department"] = department }; +MiniExcel.SaveAs(path, sheets); -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -exporter.Export(path, sheets); -``` - -```csharp // 2. DataSet var sheets = new DataSet(); -sheets.Tables.Add(UsersDataTable); -sheets.Tables.Add(DepartmentDataTable); - -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -exporter.Export(path, sheets); +sheets.Add(UsersDataTable); +sheets.Add(DepartmentDataTable); +//.. +MiniExcel.SaveAs(path, sheets); ``` ![image](https://user-images.githubusercontent.com/12729184/118130875-6e7c4580-b430-11eb-9b82-22f02716bd63.png) -#### 7. Inserting sheets - -MiniExcel supports the functionality of inserting a new sheet into an existing Excel workbook: - -```csharp -var config = new OpenXmlConfiguration { FastMode = true }; -var value = new { ID = 3, Name = "Mike", InDate = new DateTime(2021, 04, 23) }; - -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -exporter.InsertSheet(path, value, sheetName: "Sheet2", configuration: config); -``` -> **Note**: In order to insert worksheets FastMode must be enabled! - - -#### 8. Save to Stream - -You can export data directly to a `MemoryStream`, `FileStream`, and generally any stream that supports seeking: - -```csharp -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); - -using var stream = new MemoryStream(); -exporter.Export(stream, values); -``` -#### 9. TableStyles Options +#### 8. TableStyles Options Default style @@ -600,152 +497,187 @@ Default style Without style configuration ```csharp -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); - -var config = new OpenXmlConfiguration +var config = new OpenXmlConfiguration() { TableStyles = TableStyles.None }; - -exporter.Export(path, value, configuration: config); +MiniExcel.SaveAs(path, value,configuration:config); ``` ![image](https://user-images.githubusercontent.com/12729184/118784917-f3e57700-b8c2-11eb-8718-8d955b1bc197.png) -#### 10. AutoFilter +#### 9. AutoFilter -By default, autofilter is enabled on the headers of exported Excel documents. -You can disable this by setting the `AutoFilter` property of the configuration to `false`: +Since v0.19.0 `OpenXmlConfiguration.AutoFilter` can en/unable AutoFilter , default value is `true`, and setting AutoFilter way: ```csharp -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -var config = new OpenXmlConfiguration { AutoFilter = false }; -exporter.Export(path, value, configuration: config); +MiniExcel.SaveAs(path, value, configuration: new OpenXmlConfiguration() { AutoFilter = false }); ``` -#### 11. Creating images + + +#### 10. Create Image ```csharp -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -var value = new[] -{ - new { Name = "github", Image = File.ReadAllBytes("images/github_logo.png") }, - new { Name = "google", Image = File.ReadAllBytes("images/google_logo.png") }, - new { Name = "microsoft", Image = File.ReadAllBytes("images/microsoft_logo.png") }, - new { Name = "reddit", Image = File.ReadAllBytes("images/reddit_logo.png") }, - new { Name = "statck_overflow", Image = File.ReadAllBytes("images/statck_overflow_logo.png") } +var value = new[] { + new { Name="github",Image=File.ReadAllBytes(PathHelper.GetFile("images/github_logo.png"))}, + new { Name="google",Image=File.ReadAllBytes(PathHelper.GetFile("images/google_logo.png"))}, + new { Name="microsoft",Image=File.ReadAllBytes(PathHelper.GetFile("images/microsoft_logo.png"))}, + new { Name="reddit",Image=File.ReadAllBytes(PathHelper.GetFile("images/reddit_logo.png"))}, + new { Name="statck_overflow",Image=File.ReadAllBytes(PathHelper.GetFile("images/statck_overflow_logo.png"))}, }; -exporter.Export(path, value); +MiniExcel.SaveAs(path, value); ``` ![image](https://user-images.githubusercontent.com/12729184/150462383-ad9931b3-ed8d-4221-a1d6-66f799743433.png) -Whenever you export a property of type `byte[]` it will be archived as an internal resource and the cell will contain a link to it. -When queried, the resource will be converted back to `byte[]`. If you don't need this functionality you can disable it by setting the configuration property `EnableConvertByteArray` to `false` and gain some performance. + + +#### 11. Byte Array File Export + +Since 1.22.0, when value type is `byte[]` then system will save file path at cell by default, and when import system can be converted to `byte[]`. And if you don't want to use it, you can set `OpenXmlConfiguration.EnableConvertByteArray` to `false`, it can improve the system efficiency. + +![image](https://user-images.githubusercontent.com/12729184/153702334-c3b834f4-6ae4-4ddf-bd4e-e5005d5d8c6a.png) + +Since 1.22.0, when value type is `byte[]` then system will save file path at cell by default, and when import system can be converted to `byte[]`. And if you don't want to use it, you can set `OpenXmlConfiguration.EnableConvertByteArray` to `false`, it can improve the system efficiency. ![image](https://user-images.githubusercontent.com/12729184/153702334-c3b834f4-6ae4-4ddf-bd4e-e5005d5d8c6a.png) #### 12. Merge same cells vertically -This functionality merges cells vertically between the tags `@merge` and `@endmerge`. -You can use `@mergelimit` to limit boundaries of merging cells vertically. +This functionality is only supported in `xlsx` format and merges cells vertically between @merge and @endmerge tags. +You can use @mergelimit to limit boundaries of merging cells vertically. + +```csharp +var mergedFilePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid().ToString()}.xlsx"); + +var path = @"../../../../../samples/xlsx/TestMergeWithTag.xlsx"; + +MiniExcel.MergeSameCells(mergedFilePath, path); +``` ```csharp -var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); -templater.MergeSameCells(mergedFilePath, templatePath); +var memoryStream = new MemoryStream(); + +var path = @"../../../../../samples/xlsx/TestMergeWithTag.xlsx"; + +memoryStream.MergeSameCells(path); ``` -File content before and after merge without merge limit: +File content before and after merge: + +Without merge limit: Screenshot 2023-08-07 at 11 59 24 Screenshot 2023-08-07 at 11 59 57 -File content before and after merge with merge limit: +With merge limit: Screenshot 2023-08-08 at 18 21 00 Screenshot 2023-08-08 at 18 21 40 -#### 13. Null values handling +#### 13. Skip null values -By default, null values will be treated as empty strings when exporting: +New explicit option to write empty cells for null values: ```csharp -Dictionary[] value = -[ - new() - { - ["Name1"] = "Somebody once", - ["Name2"] = null, - ["Name3"] = "told me." - } -]; +DataTable dt = new DataTable(); -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -exporter.Export("test.xlsx", value, configuration: config); -``` +/* ... */ -![image](https://user-images.githubusercontent.com/31481586/241419455-3c0aec8a-4e5f-4d83-b7ec-6572124c165d.png) +DataRow dr = dt.NewRow(); -If you want you can change this behaviour in the configuration: +dr["Name1"] = "Somebody once"; +dr["Name2"] = null; +dr["Name3"] = "told me."; -```csharp -var config = new OpenXmlConfiguration +dt.Rows.Add(dr); + +OpenXmlConfiguration configuration = new OpenXmlConfiguration() { - EnableWriteNullValueCell = false // Default value is true + EnableWriteNullValueCell = true // Default value. }; -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -exporter.Export("test.xlsx", value, configuration: config); +MiniExcel.SaveAs(@"C:\temp\Book1.xlsx", dt, configuration: configuration); ``` -![image](https://user-images.githubusercontent.com/31481586/241419441-c4f27e8f-3f87-46db-a10f-08665864c874.png) +![image](https://user-images.githubusercontent.com/31481586/241419455-3c0aec8a-4e5f-4d83-b7ec-6572124c165d.png) + +```xml + + + Somebody once + + + + told me. + + +``` -Similarly, there is an option to let empty strings be treated as null values: +Previous behavior: ```csharp -var config = new OpenXmlConfiguration +/* ... */ + +OpenXmlConfiguration configuration = new OpenXmlConfiguration() { - WriteEmptyStringAsNull = true // Default value is false + EnableWriteNullValueCell = false // Default value is true. }; -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -exporter.Export("test.xlsx", value, configuration: config); +MiniExcel.SaveAs(@"C:\temp\Book1.xlsx", dt, configuration: configuration); ``` -Both properties work with `null` and `DBNull` values. +![image](https://user-images.githubusercontent.com/31481586/241419441-c4f27e8f-3f87-46db-a10f-08665864c874.png) -#### 14. Freeze Panes +```xml + + + Somebody once + + + + + + told me. + + +``` -MiniExcel allows you to freeze both rows and columns in place: +Works for null and DBNull values. +#### 14. Freeze Panes ```csharp -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -var config = new OpenXmlConfiguration +/* ... */ + +OpenXmlConfiguration configuration = new OpenXmlConfiguration() { FreezeRowCount = 1, // default is 1 FreezeColumnCount = 2 // default is 0 }; -exporter.Export("Book1.xlsx", dt, configuration: config); +MiniExcel.SaveAs(@"C:\temp\Book1.xlsx", dt, configuration: configuration); ``` ![image](docs/images/freeze-pane-1.png) -### Fill Data To Excel Template - -The declarations are similar to Vue templates `{{variable_name}}` and collection renderings `{{collection_name.field_name}}`. +### Fill Data To Excel Template -Collection renderings support `IEnumerable`, `DataTable` and `DapperRow`. +- The declaration is similar to Vue template `{{variable name}}`, or the collection rendering `{{collection name.field name}}` +- Collection rendering support IEnumerable/DataTable/DapperRow #### 1. Basic Fill Template: ![image](https://user-images.githubusercontent.com/12729184/114537556-ed8d2b00-9c84-11eb-8303-a69f62c41e5b.png) +Result: +![image](https://user-images.githubusercontent.com/12729184/114537490-d8180100-9c84-11eb-8c69-db58692f3a85.png) + Code: ```csharp // 1. By POCO @@ -770,121 +702,118 @@ var value = new Dictionary() MiniExcel.SaveAsByTemplate(path, templatePath, value); ``` -Result: -![image](https://user-images.githubusercontent.com/12729184/114537490-d8180100-9c84-11eb-8c69-db58692f3a85.png) #### 2. IEnumerable Data Fill -> Note: The first IEnumerable of the same column is the basis for filling the template +> Note1: Use the first IEnumerable of the same column as the basis for filling list Template: - ![image](https://user-images.githubusercontent.com/12729184/114564652-14f2f080-9ca3-11eb-831f-09e3fedbc5fc.png) +Result: +![image](https://user-images.githubusercontent.com/12729184/114564204-b2015980-9ca2-11eb-900d-e21249f93f7c.png) + Code: ```csharp //1. By POCO -var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); var value = new { - employees = new[] - { - new { name = "Jack", department = "HR" }, - new { name = "Lisa", department = "HR" }, - new { name = "John", department = "HR" }, - new { name = "Mike", department = "IT" }, - new { name = "Neo", department = "IT" }, - new { name = "Loan", department = "IT" } + employees = new[] { + new {name="Jack",department="HR"}, + new {name="Lisa",department="HR"}, + new {name="John",department="HR"}, + new {name="Mike",department="IT"}, + new {name="Neo",department="IT"}, + new {name="Loan",department="IT"} } }; -templater.ApplyTemplate(path, templatePath, value); +MiniExcel.SaveAsByTemplate(path, templatePath, value); //2. By Dictionary -var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); var value = new Dictionary() { - ["employees"] = new[] - { - new { name = "Jack", department = "HR" }, - new { name = "Lisa", department = "HR" }, - new { name = "John", department = "HR" }, - new { name = "Mike", department = "IT" }, - new { name = "Neo", department = "IT" }, - new { name = "Loan", department = "IT" } + ["employees"] = new[] { + new {name="Jack",department="HR"}, + new {name="Lisa",department="HR"}, + new {name="John",department="HR"}, + new {name="Mike",department="IT"}, + new {name="Neo",department="IT"}, + new {name="Loan",department="IT"} } }; -templater.ApplyTemplate(path, templatePath, value); +MiniExcel.SaveAsByTemplate(path, templatePath, value); ``` -Result: - -![image](https://user-images.githubusercontent.com/12729184/114564204-b2015980-9ca2-11eb-900d-e21249f93f7c.png) #### 3. Complex Data Fill +> Note: Support multi-sheets and using same varible + Template: ![image](https://user-images.githubusercontent.com/12729184/114565255-acf0da00-9ca3-11eb-8a7f-8131b2265ae8.png) +Result: -Code: +![image](https://user-images.githubusercontent.com/12729184/114565329-bf6b1380-9ca3-11eb-85e3-3969e8bf6378.png) ```csharp // 1. By POCO -var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); var value = new { title = "FooCompany", - managers = new[] - { - new { name = "Jack", department = "HR" }, - new { name = "Loan", department = "IT" } + managers = new[] { + new {name="Jack",department="HR"}, + new {name="Loan",department="IT"} }, - employees = new[] - { - new { name = "Wade", department = "HR" }, - new { name = "Felix", department = "HR" }, - new { name = "Eric", department = "IT" }, - new { name = "Keaton", department = "IT" } + employees = new[] { + new {name="Wade",department="HR"}, + new {name="Felix",department="HR"}, + new {name="Eric",department="IT"}, + new {name="Keaton",department="IT"} } }; -templater.ApplyTemplate(path, templatePath, value); +MiniExcel.SaveAsByTemplate(path, templatePath, value); // 2. By Dictionary -var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); var value = new Dictionary() { ["title"] = "FooCompany", - ["managers"] = new[] - { - new { name = "Jack", department = "HR" }, - new { name = "Loan", department = "IT" } + ["managers"] = new[] { + new {name="Jack",department="HR"}, + new {name="Loan",department="IT"} }, - ["employees"] = new[] - { - new { name = "Wade", department = "HR" }, - new { name = "Felix", department = "HR" }, - new { name = "Eric", department = "IT" }, - new { name = "Keaton", department = "IT" } + ["employees"] = new[] { + new {name="Wade",department="HR"}, + new {name="Felix",department="HR"}, + new {name="Eric",department="IT"}, + new {name="Keaton",department="IT"} } }; -templater.ApplyTemplate(path, templatePath, value); +MiniExcel.SaveAsByTemplate(path, templatePath, value); ``` -Result: +#### 4. Fill Big Data Performance -![image](https://user-images.githubusercontent.com/12729184/114565329-bf6b1380-9ca3-11eb-85e3-3969e8bf6378.png) +> NOTE: Using IEnumerable deferred execution not ToList can save max memory usage in MiniExcel +![image](https://user-images.githubusercontent.com/12729184/114577091-5046ec80-9cae-11eb-924b-087c7becf8da.png) -#### 4. Cell value auto mapping type -Template: + +#### 5. Cell value auto mapping type + +Template ![image](https://user-images.githubusercontent.com/12729184/114802504-64830a80-9dd0-11eb-8d56-8e8c401b3ace.png) -Model: +Result + +![image](https://user-images.githubusercontent.com/12729184/114802419-43221e80-9dd0-11eb-9ffe-a2ce34fe7076.png) + +Class ```csharp public class Poco @@ -899,130 +828,108 @@ public class Poco } ``` -Code: +Code ```csharp -var poco = new Poco -{ - @string = "string", - @int = 123, - @decimal = 123.45M, - @double = 123.33D, - datetime = new DateTime(2021, 4, 1), - @bool = true, - Guid = Guid.NewGuid() -}; - +var poco = new TestIEnumerableTypePoco { @string = "string", @int = 123, @decimal = decimal.Parse("123.45"), @double = (double)123.33, @datetime = new DateTime(2021, 4, 1), @bool = true, @Guid = Guid.NewGuid() }; var value = new { - Ts = new[] - { + Ts = new[] { poco, new TestIEnumerableTypePoco{}, null, poco } }; - -var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); -templater.ApplyTemplate(path, templatePath, value); +MiniExcel.SaveAsByTemplate(path, templatePath, value); ``` -Result: -![image](https://user-images.githubusercontent.com/12729184/114802419-43221e80-9dd0-11eb-9ffe-a2ce34fe7076.png) - -#### 5. Example: List Github Projects +#### 6. Example : List Github Projects Template ![image](https://user-images.githubusercontent.com/12729184/115068623-12073280-9f25-11eb-9124-f4b3efcdb2a7.png) + +Result + +![image](https://user-images.githubusercontent.com/12729184/115068639-1a5f6d80-9f25-11eb-9f45-27c434d19a78.png) + Code ```csharp var projects = new[] { - new { Name = "MiniExcel", Link = "https://github.com/mini-software/MiniExcel", Star=146, CreateTime = new DateTime(2021,03,01) }, - new { Name = "HtmlTableHelper", Link = "https://github.com/mini-software/HtmlTableHelper", Star=16, CreateTime = new DateTime(2020,02,01) }, - new { Name = "PocoClassGenerator", Link = "https://github.com/mini-software/PocoClassGenerator", Star=16, CreateTime = new DateTime(2019,03,17)} + new {Name = "MiniExcel",Link="https://github.com/mini-software/MiniExcel",Star=146, CreateTime=new DateTime(2021,03,01)}, + new {Name = "HtmlTableHelper",Link="https://github.com/mini-software/HtmlTableHelper",Star=16, CreateTime=new DateTime(2020,02,01)}, + new {Name = "PocoClassGenerator",Link="https://github.com/mini-software/PocoClassGenerator",Star=16, CreateTime=new DateTime(2019,03,17)} }; - var value = new { User = "ITWeiHan", Projects = projects, TotalStar = projects.Sum(s => s.Star) }; - -var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); -templater.ApplyTemplate(path, templatePath, value); +MiniExcel.SaveAsByTemplate(path, templatePath, value); ``` -Result: - -![image](https://user-images.githubusercontent.com/12729184/115068639-1a5f6d80-9f25-11eb-9f45-27c434d19a78.png) - - -#### 6. Grouped Data Fill +#### 7. Grouped Data Fill ```csharp var value = new Dictionary() { - ["employees"] = new[] - { - new { name = "Jack", department = "HR" }, - new { name = "Jack", department = "HR" }, - new { name = "John", department = "HR" }, - new { name = "John", department = "IT" }, - new { name = "Neo", department = "IT" }, - new { name = "Loan", department = "IT" } + ["employees"] = new[] { + new {name="Jack",department="HR"}, + new {name="Jack",department="HR"}, + new {name="John",department="HR"}, + new {name="John",department="IT"}, + new {name="Neo",department="IT"}, + new {name="Loan",department="IT"} } }; - -var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); -templater.ApplyTemplate(path, templatePath, value); +await MiniExcel.SaveAsByTemplateAsync(path, templatePath, value); ``` -- With `@group` tag and with `@header` tag +##### 1. With `@group` tag and with `@header` tag -Before: +Before ![before_with_header](https://user-images.githubusercontent.com/38832863/218646717-21b9d57a-2be2-4e9a-801b-ae212231d2b4.PNG) -After: +After ![after_with_header](https://user-images.githubusercontent.com/38832863/218646721-58a7a340-7004-4bc2-af24-cffcb2c20737.PNG) -- With `@group` tag and without `@header` tag +##### 2. With @group tag and without @header tag -Before: +Before ![before_without_header](https://user-images.githubusercontent.com/38832863/218646873-b12417fa-801b-4890-8e96-669ed3b43902.PNG) -After; +After ![after_without_header](https://user-images.githubusercontent.com/38832863/218646872-622461ba-342e-49ee-834f-b91ad9c2dac3.PNG) -- Without `@group` tag +##### 3. Without @group tag -Before: +Before ![without_group](https://user-images.githubusercontent.com/38832863/218646975-f52a68eb-e031-43b5-abaa-03b67c052d1a.PNG) -After: +After ![without_group_after](https://user-images.githubusercontent.com/38832863/218646974-4a3c0e07-7c66-4088-ad07-b4ad3695b7e1.PNG) -#### 7. If/ElseIf/Else Statements inside cell +#### 8. If/ElseIf/Else Statements inside cell Rules: -1. Supports `DateTime`, `double` and `int` with `==`, `!=`, `>`, `>=`,`<`, `<=` operators. -2. Supports `string` with `==`, `!=` operators. -3. Each statement should be on a new line. -4. A single space should be added before and after operators. -5. There shouldn't be any new lines inside of a statement. -6. Cells should be in the exact format as below: +1. Supports DateTime, Double, Int with ==, !=, >, >=, <, <= operators. +2. Supports String with ==, != operators. +3. Each statement should be new line. +4. Single space should be added before and after operators. +5. There shouldn't be new line inside of statements. +6. Cell should be in exact format as below. ```csharp @if(name == Jack) @@ -1034,15 +941,15 @@ Test {{employees.name}} @endif ``` -Before: +Before ![if_before](https://user-images.githubusercontent.com/38832863/235360606-ca654769-ff55-4f5b-98d2-d2ec0edb8173.PNG) -After: +After ![if_after](https://user-images.githubusercontent.com/38832863/235360609-869bb960-d63d-45ae-8d64-9e8b0d0ab658.PNG) -#### 8. DataTable as parameter +#### 9. DataTable as parameter ```csharp var managers = new DataTable(); @@ -1052,17 +959,16 @@ var managers = new DataTable(); managers.Rows.Add("Jack", "HR"); managers.Rows.Add("Loan", "IT"); } - var value = new Dictionary() { ["title"] = "FooCompany", ["managers"] = managers, }; - -var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); -templater.ApplyTemplate(path, templatePath, value); +MiniExcel.SaveAsByTemplate(path, templatePath, value); ``` -#### 9. Formulas +#### 10. Formulas + +##### 1. Example Prefix your formula with `$` and use `$enumrowstart` and `$enumrowend` to mark references to the enumerable start and end rows: ![image](docs/images/template-formulas-1.png) @@ -1071,110 +977,118 @@ When the template is rendered, the `$` prefix will be removed and `$enumrowstart ![image](docs/images/template-formulas-2.png) -_Other examples_: +##### 2. Other Example Formulas: -| Formula | Example | -|---------|------------------------------------------------------------------------------------------------| -| Sum | `$=SUM(C{{$enumrowstart}}:C{{$enumrowend}})` | -| Count | `COUNT(C{{$enumrowstart}}:C{{$enumrowend}})` | -| Range | `$=MAX(C{{$enumrowstart}}:C{{$enumrowend}}) - MIN(C{{$enumrowstart}}:C{{$enumrowend}})` | +| | | +|--------------|-------------------------------------------------------------------------------------------| +| Sum | `$=SUM(C{{$enumrowstart}}:C{{$enumrowend}})` | +| Alt. Average | `$=SUM(C{{$enumrowstart}}:C{{$enumrowend}}) / COUNT(C{{$enumrowstart}}:C{{$enumrowend}})` | +| Range | `$=MAX(C{{$enumrowstart}}:C{{$enumrowend}}) - MIN(C{{$enumrowstart}}:C{{$enumrowend}})` | -#### 10. Checking template parameter key +#### 11. Other -When a parameter key is missing it will be replaced with an empty string by default. -You can change this behaviour to throw an exception by setting the `IgnoreTemplateParameterMissing` configuration property: +##### 1. Checking template parameter key + +Since V1.24.0 , default ignore template missing parameter key and replace it with empty string, `IgnoreTemplateParameterMissing` can control throwing exception or not. ```csharp -var config = new OpenXmlConfiguration +var config = new OpenXmlConfiguration() { IgnoreTemplateParameterMissing = false, }; - -var templater = MiniExcel.Templaters.GetOpenXmlTemplater(); -templater.ApplyTemplate(path, templatePath, value, config) +MiniExcel.SaveAsByTemplate(path, templatePath, value, config) ``` ![image](https://user-images.githubusercontent.com/12729184/157464332-e316f829-54aa-4c84-a5aa-9aef337b668d.png) -### Attributes and configuration +### Excel Column Name/Index/Ignore Attribute -#### 1. Specify the column name, column index, or ignore the column entirely + + +#### 1. Specify the column name, column index, column ignore + +Excel Example ![image](https://user-images.githubusercontent.com/12729184/114230869-3e163700-99ac-11eb-9a90-2039d4b4b313.png) +Code + ```csharp public class ExcelAttributeDemo { - [MiniExcelColumnName("Column1")] + [ExcelColumnName("Column1")] public string Test1 { get; set; } - - [MiniExcelColumnName("Column2")] + [ExcelColumnName("Column2")] public string Test2 { get; set; } - - [MiniExcelIgnore] + [ExcelIgnore] public string Test3 { get; set; } - - [MiniExcelColumnIndex("I")] // "I" will be converted to index 8 + [ExcelColumnIndex("I")] // system will convert "I" to 8 index public string Test4 { get; set; } - - public string Test5 { get; } // properties wihout a setter will be ignored - public string Test6 { get; private set; } // properties with a non public setter will be ignored - - [MiniExcelColumnIndex(3)] // Indexes are 0-based + public string Test5 { get; } //wihout set will ignore + public string Test6 { get; private set; } //un-public set will ignore + [ExcelColumnIndex(3)] // start with 0 public string Test7 { get; set; } } -var importer = MiniExcel.Importers.GetOpenXmlImporter(); -var rows = importer.Query(path).ToList(); - -// rows[0].Test1 = "Column1" -// rows[0].Test2 = "Column2" -// rows[0].Test3 = null -// rows[0].Test4 = "Test7" -// rows[0].Test5 = null -// rows[0].Test6 = null -// rows[0].Test7 = "Test4" +var rows = MiniExcel.Query(path).ToList(); +Assert.Equal("Column1", rows[0].Test1); +Assert.Equal("Column2", rows[0].Test2); +Assert.Null(rows[0].Test3); +Assert.Equal("Test7", rows[0].Test4); +Assert.Null(rows[0].Test5); +Assert.Null(rows[0].Test6); +Assert.Equal("Test4", rows[0].Test7); ``` -#### 2. Custom Formatting + + + +#### 2. Custom Format (ExcelFormatAttribute) + +Since V0.21.0 support class which contains `ToString(string content)` method format + +Class ```csharp public class Dto { public string Name { get; set; } - [MiniExcelFormat("MMMM dd, yyyy")] + [ExcelFormat("MMMM dd, yyyy")] public DateTime InDate { get; set; } } +``` -var value = new Dto[] -{ - new Issue241Dto{ Name = "Jack", InDate = new DateTime(2021, 01, 04) }, - new Issue241Dto{ Name = "Henry", InDate = new DateTime(2020, 04, 05) } -}; +Code -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -exporter.Export(path, value); +```csharp +var value = new Dto[] { + new Issue241Dto{ Name="Jack",InDate=new DateTime(2021,01,04)}, + new Issue241Dto{ Name="Henry",InDate=new DateTime(2020,04,05)}, +}; +MiniExcel.SaveAs(path, value); ``` -Result: +Result ![image](https://user-images.githubusercontent.com/12729184/118910788-ab2bcd80-b957-11eb-8d42-bfce36621b1b.png) +Query supports custom format conversion + +![image](https://user-images.githubusercontent.com/12729184/118911286-87b55280-b958-11eb-9a88-c8ff403d240a.png) -#### 3. Set Column Width +#### 3. Set Column Width(ExcelColumnWidthAttribute) ```csharp public class Dto { - [MiniExcelColumnWidth(20)] + [ExcelColumnWidth(20)] public int ID { get; set; } - - [MiniExcelColumnWidth(15.50)] + [ExcelColumnWidth(15.50)] public string Name { get; set; } } ``` @@ -1184,278 +1098,343 @@ public class Dto ```csharp public class Dto { - public string Name { get; set; } - - [MiniExcelColumnName(columnName: "EmployeeNo", aliases: ["EmpNo", "No"])] + [ExcelColumnName(excelColumnName:"EmployeeNo",aliases:new[] { "EmpNo","No" })] public string Empno { get; set; } + public string Name { get; set; } } ``` -#### 5. System.ComponentModel.DisplayNameAttribute -The `DisplayNameAttribute` has the same effect as the `MiniExcelColumnNameAttribute`: + +#### 5. System.ComponentModel.DisplayNameAttribute = ExcelColumnName.excelColumnNameAttribute + +Since 1.24.0, system supports System.ComponentModel.DisplayNameAttribute = ExcelColumnName.excelColumnNameAttribute ```C# -public class Dto +public class TestIssueI4TXGTDto { public int ID { get; set; } - public string Name { get; set; } - [DisplayName("Specification")] public string Spc { get; set; } - [DisplayName("Unit Price")] public decimal Up { get; set; } } ``` -#### 6. MiniExcelColumnAttribute -Multiple attributes can be simplified using the `MiniExcelColumnAttribute`: +#### 6. ExcelColumnAttribute + +Since V1.26.0, multiple attributes can be simplified like : ```csharp -public class Dto -{ - [MiniExcelColumn(Name = "ID",Index =0)] - public string MyProperty { get; set; } - - [MiniExcelColumn(Name = "CreateDate", Index = 1, Format = "yyyy-MM", Width = 100)] - public DateTime MyProperty2 { get; set; } -} + public class TestIssueI4ZYUUDto + { + [ExcelColumn(Name = "ID",Index =0)] + public string MyProperty { get; set; } + [ExcelColumn(Name = "CreateDate", Index = 1,Format ="yyyy-MM",Width =100)] + public DateTime MyProperty2 { get; set; } + } ``` + + #### 7. DynamicColumnAttribute -Attributes can also be set on columns dynamically: +Since V1.26.0, we can set the attributes of Column dynamically ```csharp -var config = new OpenXmlConfiguration -{ - 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"} - ] -}; + var config = new OpenXmlConfiguration + { + DynamicColumns = new DynamicExcelColumn[] { + 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"}, + } + }; + var path = PathHelper.GetTempPath(); + var value = new[] { new { id = 1, name = "Jack", createdate = new DateTime(2022, 04, 12) ,point = 123.456} }; + MiniExcel.SaveAs(path, value, configuration: config); +``` +![image](https://user-images.githubusercontent.com/12729184/164510353-5aecbc4e-c3ce-41e8-b6cf-afd55eb23b68.png) + +#### 8. DynamicSheetAttribute + +Since V1.31.4 we can set the attributes of Sheet dynamically. We can set sheet name and state (visibility). +```csharp + var configuration = new OpenXmlConfiguration + { + DynamicSheets = new DynamicExcelSheet[] { + new DynamicExcelSheet("usersSheet") { Name = "Users", State = SheetState.Visible }, + new DynamicExcelSheet("departmentSheet") { Name = "Departments", State = SheetState.Hidden } + } + }; -var value = new[] { new { id = 1, name = "Jack", createdate = new DateTime(2022, 04, 12), point = 123.456 } }; + var users = new[] { new { Name = "Jack", Age = 25 }, new { Name = "Mike", Age = 44 } }; + var department = new[] { new { ID = "01", Name = "HR" }, new { ID = "02", Name = "IT" } }; + var sheets = new Dictionary + { + ["usersSheet"] = users, + ["departmentSheet"] = department + }; -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -exporter.Export(path, value, configuration: config); + var path = PathHelper.GetTempPath(); + MiniExcel.SaveAs(path, sheets, configuration: configuration); +``` + +We can also use new attribute ExcelSheetAttribute: + +```C# + [ExcelSheet(Name = "Departments", State = SheetState.Hidden)] + private class DepartmentDto + { + [ExcelColumn(Name = "ID",Index = 0)] + public string ID { get; set; } + [ExcelColumn(Name = "Name",Index = 1)] + public string Name { get; set; } + } ``` -#### 8. MiniExcelSheetAttribute +### Add, Delete, Update -It is possible to define the name and visibility of a sheet through the `MiniExcelSheetAttribute`: +#### Add + +v1.28.0 support CSV insert N rows data after last row ```csharp -[MiniExcelSheet(Name = "Departments", State = SheetState.Hidden)] -private class DepartmentDto +// Origin { - [MiniExcelColumn(Name = "ID",Index = 0)] - public string ID { get; set; } - - [MiniExcelColumn(Name = "Name",Index = 1)] - public string Name { get; set; } + var value = new[] { + new { ID=1,Name ="Jack",InDate=new DateTime(2021,01,03)}, + new { ID=2,Name ="Henry",InDate=new DateTime(2020,05,03)}, + }; + MiniExcel.SaveAs(path, value); +} +// Insert 1 rows after last +{ + var value = new { ID=3,Name = "Mike", InDate = new DateTime(2021, 04, 23) }; + MiniExcel.Insert(path, value); +} +// Insert N rows after last +{ + var value = new[] { + new { ID=4,Name ="Frank",InDate=new DateTime(2021,06,07)}, + new { ID=5,Name ="Gloria",InDate=new DateTime(2022,05,03)}, + }; + MiniExcel.Insert(path, value); } ``` -It is also possible to do it dynamically through the `DynamicSheets` property of the `OpenXmlConfiguration`: -```csharp -var configuration = new OpenXmlConfiguration -{ - DynamicSheets = - [ - new DynamicExcelSheet("usersSheet") { Name = "Users", State = SheetState.Visible }, - new DynamicExcelSheet("departmentSheet") { Name = "Departments", State = SheetState.Hidden } - ] -}; +![image](https://user-images.githubusercontent.com/12729184/191023733-1e2fa732-db5c-4a3a-9722-b891fe5aa069.png) + +v1.37.0 support excel insert a new sheet into an existing workbook -var users = new[] +```csharp +// Origin excel { - new { Name = "Jack", Age = 25 }, - new { Name = "Mike", Age = 44 } -}; -var department = new[] + var value = new[] { + new { ID=1,Name ="Jack",InDate=new DateTime(2021,01,03)}, + new { ID=2,Name ="Henry",InDate=new DateTime(2020,05,03)}, + }; + MiniExcel.SaveAs(path, value, sheetName: "Sheet1"); +} +// Insert a new sheet { - new { ID = "01", Name = "HR" }, - new { ID = "02", Name = "IT" } -}; + var value = new { ID=3,Name = "Mike", InDate = new DateTime(2021, 04, 23) }; + MiniExcel.Insert(path, table, sheetName: "Sheet2"); +} +``` + + + +#### Delete(waiting) + +#### Update(waiting) -var sheets = new Dictionary -{ - ["usersSheet"] = users, - ["departmentSheet"] = department -}; -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -exporter.Export(path, sheets, configuration: configuration); + +### Excel Type Auto Check + +- MiniExcel will check whether it is xlsx or csv based on the `file extension` by default, but there may be inaccuracy, please specify it manually. +- Stream cannot be know from which excel, please specify it manually. + +```csharp +stream.SaveAs(excelType:ExcelType.CSV); +//or +stream.SaveAs(excelType:ExcelType.XLSX); +//or +stream.Query(excelType:ExcelType.CSV); +//or +stream.Query(excelType:ExcelType.XLSX); ``` -### CSV -> Unlike Excel queries, csv always maps values to `string` by default, unless you are querying to a strongly defined type. + + + +### CSV + +#### Note + +- Default return `string` type, and value will not be converted to numbers or datetime, unless the type is defined by strong typing generic. + + #### Custom separator -The default separator is the comma (`,`), but you can customize it using the `CsvConfiguration.Seperator` property: +The default is `,` as the separator, you can modify the `Seperator` property for customization ```csharp -var config = new CsvConfiguration +var config = new MiniExcelLibs.Csv.CsvConfiguration() { Seperator=';' }; - -var exporter = MiniExcel.Exporters.GetCsvExporter(); -exporter.Export(path, values, configuration: config); +MiniExcel.SaveAs(path, values,configuration: config); ``` -You also have the option to define a more complex separator: +Since V1.30.1 support function to custom separator (thanks @hyzx86) ```csharp -var config = new CsvConfiguration +var config = new CsvConfiguration() { - SplitFn = row => Regex - .Split(row, $"[\t,](?=(?:[^\"]|\"[^\"]*\")*$)") - .Select(str => Regex.Replace(str.Replace("\"\"", "\""), "^\"|\"$", "")) - .ToArray() + SplitFn = (row) => Regex.Split(row, $"[\t,](?=(?:[^\"]|\"[^\"]*\")*$)") + .Select(s => Regex.Replace(s.Replace("\"\"", "\""), "^\"|\"$", "")).ToArray() }; - -var importer = MiniExcel.Exporters.GetCsvImporter(); -var rows = importer.Query(path, configuration: config).ToList(); +var rows = MiniExcel.Query(path, configuration: config).ToList(); ``` + + #### Custom line break -The default line break is `\r\n`, but you can customize it using the `CsvConfiguration.NewLine`: +The default is `\r\n` as the newline character, you can modify the `NewLine` property for customization ```csharp -var config = new CsvConfiguration +var config = new MiniExcelLibs.Csv.CsvConfiguration() { NewLine='\n' }; - -var exporter = MiniExcel.Exporters.GetCsvExporter(); -exporter.Export(path, values,configuration: config); +MiniExcel.SaveAs(path, values,configuration: config); ``` -#### Custom encoding -The default encoding is UTF8 with BOM. If you have custom encoding requirements you can modify the `StreamReaderFunc` and `StreamWriterFunc` properties: + +#### Custom coding + +- The default encoding is "Detect Encoding From Byte Order Marks" (detectEncodingFromByteOrderMarks: true) +- f you have custom encoding requirements, please modify the StreamReaderFunc / StreamWriterFunc property ```csharp // Read -var config = new CsvConfiguration +var config = new MiniExcelLibs.Csv.CsvConfiguration() { - StreamReaderFunc = stream => new StreamReader(stream,Encoding.GetEncoding("gb2312")) + StreamReaderFunc = (stream) => new StreamReader(stream,Encoding.GetEncoding("gb2312")) }; - -var importer = MiniExcel.Importers.GetCsvImporter(); -var rows = importer.Query(path, useHeaderRow: true, configuration: config); +var rows = MiniExcel.Query(path, true,excelType:ExcelType.CSV,configuration: config); // Write -var config = new CsvConfiguration +var config = new MiniExcelLibs.Csv.CsvConfiguration() { - StreamWriterFunc = stream => new StreamWriter(stream, Encoding.GetEncoding("gb2312")) + StreamWriterFunc = (stream) => new StreamWriter(stream, Encoding.GetEncoding("gb2312")) }; - -var exporter = MiniExcel.Exporters.GetCsvExporter(); -exporter.Export(path, value, configuration: config); +MiniExcel.SaveAs(path, value,excelType:ExcelType.CSV, configuration: config); ``` #### Read empty string as null -By default, empty values are mapped to `string.Empty`. -You can modify this behavior and map them to `null` using the `CsvConfiguration.ReadEmptyStringAsNull` property: +By default, empty values are mapped to string.Empty. You can modify this behavior ```csharp -var config = new CsvConfiguration +var config = new MiniExcelLibs.Csv.CsvConfiguration() { ReadEmptyStringAsNull = true }; ``` -#### DataReader +### DataReader -There is support for reading one cell at a time using a custom `IDataReader`: +#### 1. GetReader +Since 1.23.0, you can GetDataReader ```csharp -var importer = MiniExcel.Importers.GetOpenXmlImporter(); -using var reader = importer.GetDataReader(path, useHeaderRow: true); - -// or - -var importer = MiniExcel.Importers.GetCsvImporter(); -using var reader = importer.GetDataReader(path, useHeaderRow: true); - - -while (reader.Read()) -{ - for (int i = 0; i < reader.FieldCount; i++) + using (var reader = MiniExcel.GetReader(path,true)) { - var value = reader.GetValue(i); + while (reader.Read()) + { + for (int i = 0; i < reader.FieldCount; i++) + { + var value = reader.GetValue(i); + } + } } -} ``` -#### Add records -It is possible to append an arbitrary number of rows to a csv document: +### Async + +- v0.17.0 support Async (thanks isdaniel ( SHIH,BING-SIOU)](https://github.com/isdaniel)) ```csharp -var exporter = MiniExcel.Exporters.GetCsvExporter(); +public static Task SaveAsAsync(string path, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration configuration = null) +public static Task SaveAsAsync(this Stream stream, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, IConfiguration configuration = null) +public static Task> QueryAsync(string path, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) +public static Task> QueryAsync(this Stream stream, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) where T : class, new() +public static Task> QueryAsync(string path, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) where T : class, new() +public static Task>> QueryAsync(this Stream stream, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) +public static Task SaveAsByTemplateAsync(this Stream stream, string templatePath, object value) +public static Task SaveAsByTemplateAsync(this Stream stream, byte[] templateBytes, object value) +public static Task SaveAsByTemplateAsync(string path, string templatePath, object value) +public static Task SaveAsByTemplateAsync(string path, byte[] templateBytes, object value) +public static Task QueryAsDataTableAsync(string path, bool useHeaderRow = true, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) +``` + +- v1.25.0 support `cancellationToken`。 -// Insert 1 rows after last -var value = new { ID = 3, Name = "Mike", InDate = new DateTime(2021, 04, 23) }; -exporter.Append(path, value); -// Insert N rows after last -var value = new[] -{ - new { ID = 4, Name = "Frank", InDate = new DateTime(2021, 06, 07) }, - new { ID = 5, Name = "Gloria", InDate = new DateTime(2022, 05, 03) }, -}; -exporter.AppendToCsv(path, value); -``` -### Other functionalities +### Others -#### 1. Enums +#### 1. Enum -The serialization of enums is supported. Enum fields are mapped case insensitively. +Be sure excel & property name same, system will auto mapping (case insensitive) -The use of the `DescriptionAttribute` is also supported to map enum properties: +![image](https://user-images.githubusercontent.com/12729184/116210595-9784b100-a775-11eb-936f-8e7a8b435961.png) + +Since V0.18.0 support Enum Description ```csharp public class Dto { public string Name { get; set; } - public UserTypeEnum UserType { get; set; } + public I49RYZUserType UserType { get; set; } } -public enum UserTypeEnum +public enum Type { - [Description("General User")] V1, - [Description("General Administrator")] V2, - [Description("Super Administrator")] V3 + [Description("General User")] + V1, + [Description("General Administrator")] + V2, + [Description("Super Administrator")] + V3 } ``` ![image](https://user-images.githubusercontent.com/12729184/133116630-27cc7161-099a-48b8-9784-cd1e443af3d1.png) +Since 1.30.0 version support excel Description to Enum , thanks @KaneLeung -#### 2. Convert Csv to Xlsx or vice-versa +#### 2. Convert CSV to XLSX or Convert XLSX to CSV ```csharp -MiniExcel.Exporters.GetCsvExporter().ConvertXlsxToCsv(xlsxPath, csvPath); -MiniExcel.Exporters.GetCsvExporter().ConvertCsvToXlsx(csvPath, xlsxPath); - -// or - +MiniExcel.ConvertXlsxToCsv(xlsxPath, csvPath); +MiniExcel.ConvertXlsxToCsv(xlsxStream, csvStream); +MiniExcel.ConvertCsvToXlsx(csvPath, xlsxPath); +MiniExcel.ConvertCsvToXlsx(csvStream, xlsxStream); +``` +```csharp using (var excelStream = new FileStream(path: filePath, FileMode.Open, FileAccess.Read)) using (var csvStream = new MemoryStream()) { @@ -1465,224 +1444,557 @@ using (var csvStream = new MemoryStream()) #### 3. Custom CultureInfo -You can customise CultureInfo used by MiniExcel through the `Culture` configuration parameter. The default is `CultureInfo.InvariantCulture`: +Since 1.22.0, you can custom CultureInfo like below, system default `CultureInfo.InvariantCulture`. ```csharp -var config = new CsvConfiguration +var config = new CsvConfiguration() { Culture = new CultureInfo("fr-FR"), }; +MiniExcel.SaveAs(path, value, configuration: config); + +// or +MiniExcel.Query(path, configuration: config); ``` -#### 4. Custom Buffer Size -The default buffer size is 5MB, but you can easily customize it: +#### 4. Custom Buffer Size ```csharp -var conf = new OpenXmlConfiguration { BufferSize = 1024 * 1024 * 10 }; + public abstract class Configuration : IConfiguration + { + public int BufferSize { get; set; } = 1024 * 512; + } ``` #### 5. FastMode -You can set the configuration property `FastMode` to achieve faster saving speeds, but this will make the memory consumption much higher, so it's not recommended: +System will not control memory, but you can get faster save speed. ```csharp -var config = new OpenXmlConfiguration { FastMode = true }; - -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -exporter.Export(path, reader, configuration: config); +var config = new OpenXmlConfiguration() { FastMode = true }; +MiniExcel.SaveAs(path, reader,configuration:config); ``` -#### 6. Adding images in batch +#### 6. Batch Add Image (MiniExcel.AddPicture) -Please add pictures before batch generating the rows' data or a large amount of memory will be used when calling `AddPicture`: +Please add pictures before batch generate rows data, or system will load large memory usage when calling AddPicture. ```csharp -MiniExcelPicture[] images = -[ - new() +var images = new[] +{ + new MiniExcelPicture { ImageBytes = File.ReadAllBytes(PathHelper.GetFile("images/github_logo.png")), - SheetName = null, // when null it will default to the first sheet + SheetName = null, // default null is first sheet CellAddress = "C3", // required }, - new() + new MiniExcelPicture { ImageBytes = File.ReadAllBytes(PathHelper.GetFile("images/google_logo.png")), - PictureType = "image/png", // image/png is the default picture type + PictureType = "image/png", // default PictureType = image/png SheetName = "Demo", - CellAddress = "C9", + CellAddress = "C9", // required WidthPx = 100, HeightPx = 100, }, -]; - -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -exporter.AddPicture(path, images); +}; +MiniExcel.AddPicture(path, images); ``` ![Image](https://github.com/user-attachments/assets/19c4d241-9753-4ede-96c8-f810c1a22247) -#### 7. Get Sheets Dimensions +#### 7. Get Sheets Dimension + +```csharp +var dim = MiniExcel.GetSheetDimensions(path); +``` -You can easily retrieve the dimensions of all worksheets of an Excel file: +### Examples: + +#### 1. SQLite & Dapper `Large Size File` SQL Insert Avoid OOM + +note : please don't call ToList/ToArray methods after Query, it'll load all data into memory ```csharp -var importer = MiniExcel.Importers.GetOpenXmlImporter(); -var dim = importer.GetSheetDimensions(path); +using (var connection = new SQLiteConnection(connectionString)) +{ + connection.Open(); + using (var transaction = connection.BeginTransaction()) + using (var stream = File.OpenRead(path)) + { + var rows = stream.Query(); + foreach (var row in rows) + connection.Execute("insert into T (A,B) values (@A,@B)", new { row.A, row.B }, transaction: transaction); + transaction.Commit(); + } +} ``` -### FAQ +performance: +![image](https://user-images.githubusercontent.com/12729184/111072579-2dda7b80-8516-11eb-9843-c01a1edc88ec.png) + + + -#### Q: Excel header title is not equal to my DTO class property name, how do I map it? -A. You can use the `MiniExcelColumnName` attribute on the property you want to map: +#### 2. ASP.NET Core 3.1 or MVC 5 Download/Upload Excel Xlsx API Demo [Try it](tests/MiniExcel.Tests.AspNetCore) ```csharp -class Dto +public class ApiController : Controller { - [MiniExcelColumnName("ExcelPropertyName")] - public string MyPropertyName { get; set;} + public IActionResult Index() + { + return new ContentResult + { + ContentType = "text/html", + StatusCode = (int)HttpStatusCode.OK, + Content = @" +DownloadExcel
+DownloadExcelFromTemplatePath
+DownloadExcelFromTemplateBytes
+

Upload Excel

+
+
+ +
+ value = new Dictionary() + { + ["title"] = "FooCompany", + ["managers"] = new[] { + new {name="Jack",department="HR"}, + new {name="Loan",department="IT"} + }, + ["employees"] = new[] { + new {name="Wade",department="HR"}, + new {name="Felix",department="HR"}, + new {name="Eric",department="IT"}, + new {name="Keaton",department="IT"} + } + }; + + MemoryStream memoryStream = new MemoryStream(); + memoryStream.SaveAsByTemplate(templatePath, value); + memoryStream.Seek(0, SeekOrigin.Begin); + return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + { + FileDownloadName = "demo.xlsx" + }; + } + + private static Dictionary TemplateBytesCache = new Dictionary(); + + static ApiController() + { + string templatePath = "TestTemplateComplex.xlsx"; + byte[] bytes = System.IO.File.ReadAllBytes(templatePath); + TemplateBytesCache.Add(templatePath, bytes); + } + + public IActionResult DownloadExcelFromTemplateBytes() + { + byte[] bytes = TemplateBytesCache["TestTemplateComplex.xlsx"]; + + Dictionary value = new Dictionary() + { + ["title"] = "FooCompany", + ["managers"] = new[] { + new {name="Jack",department="HR"}, + new {name="Loan",department="IT"} + }, + ["employees"] = new[] { + new {name="Wade",department="HR"}, + new {name="Felix",department="HR"}, + new {name="Eric",department="IT"}, + new {name="Keaton",department="IT"} + } + }; + + MemoryStream memoryStream = new MemoryStream(); + memoryStream.SaveAsByTemplate(bytes, value); + memoryStream.Seek(0, SeekOrigin.Begin); + return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + { + FileDownloadName = "demo.xlsx" + }; + } + + public IActionResult UploadExcel(IFormFile excel) + { + var stream = new MemoryStream(); + excel.CopyTo(stream); + + foreach (var item in stream.Query(true)) + { + // do your logic etc. + } + + return Ok("File uploaded successfully"); + } } ``` -#### Q. How do I query multiple sheets of an Excel file? +#### 3. Paging Query -A. You can retrieve the sheet names with the `GetSheetNames` method and then Query them using the `sheetName` parameter: +```csharp +void Main() +{ + var rows = MiniExcel.Query(path); + + Console.WriteLine("==== No.1 Page ===="); + Console.WriteLine(Page(rows,pageSize:3,page:1)); + Console.WriteLine("==== No.50 Page ===="); + Console.WriteLine(Page(rows,pageSize:3,page:50)); + Console.WriteLine("==== No.5000 Page ===="); + Console.WriteLine(Page(rows,pageSize:3,page:5000)); +} + +public static IEnumerable Page(IEnumerable en, int pageSize, int page) +{ + return en.Skip(page * pageSize).Take(pageSize); +} +``` + +![20210419](https://user-images.githubusercontent.com/12729184/114679083-6ef4c400-9d3e-11eb-9f78-a86daa45fe46.gif) + + + +#### 4. WebForm export Excel by memorystream ```csharp -var importer = MiniExcel.Importers.GetOpenXmlImporter(); -var sheetNames = importer.GetSheetNames(path); +var fileName = "Demo.xlsx"; +var sheetName = "Sheet1"; +HttpResponse response = HttpContext.Current.Response; +response.Clear(); +response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; +response.AddHeader("Content-Disposition", $"attachment;filename=\"{fileName}\""); +var values = new[] { + new { Column1 = "MiniExcel", Column2 = 1 }, + new { Column1 = "Github", Column2 = 2} +}; +var memoryStream = new MemoryStream(); +memoryStream.SaveAs(values, sheetName: sheetName); +memoryStream.Seek(0, SeekOrigin.Begin); +memoryStream.CopyTo(Response.OutputStream); +response.End(); +``` + + + +#### 5. Dynamic i18n multi-language and role authority management -var rows = new Dictionary>(); -foreach (var sheet in sheetNames) +Like the example, create a method to handle i18n and permission management, and use `yield return to return IEnumerable>` to achieve dynamic and low-memory processing effects + +```csharp +void Main() { - rows[sheet] = importer.Query(path, sheetName: sheet).ToList(); + var value = new Order[] { + new Order(){OrderNo = "SO01",CustomerID="C001",ProductID="P001",Qty=100,Amt=500}, + new Order(){OrderNo = "SO02",CustomerID="C002",ProductID="P002",Qty=300,Amt=400}, + }; + + Console.WriteLine("en-Us and Sales role"); + { + var path = Path.GetTempPath() + Guid.NewGuid() + ".xlsx"; + var lang = "en-US"; + var role = "Sales"; + MiniExcel.SaveAs(path, GetOrders(lang, role, value)); + MiniExcel.Query(path, true).Dump(); + } + + Console.WriteLine("zh-CN and PMC role"); + { + var path = Path.GetTempPath() + Guid.NewGuid() + ".xlsx"; + var lang = "zh-CN"; + var role = "PMC"; + MiniExcel.SaveAs(path, GetOrders(lang, role, value)); + MiniExcel.Query(path, true).Dump(); + } +} + +private IEnumerable> GetOrders(string lang, string role, Order[] orders) +{ + foreach (var order in orders) + { + var newOrder = new Dictionary(); + + if (lang == "zh-CN") + { + newOrder.Add("客户编号", order.CustomerID); + newOrder.Add("订单编号", order.OrderNo); + newOrder.Add("产品编号", order.ProductID); + newOrder.Add("数量", order.Qty); + if (role == "Sales") + newOrder.Add("价格", order.Amt); + yield return newOrder; + } + else if (lang == "en-US") + { + newOrder.Add("Customer ID", order.CustomerID); + newOrder.Add("Order No", order.OrderNo); + newOrder.Add("Product ID", order.ProductID); + newOrder.Add("Quantity", order.Qty); + if (role == "Sales") + newOrder.Add("Amount", order.Amt); + yield return newOrder; + } + else + { + throw new InvalidDataException($"lang {lang} wrong"); + } + } +} + +public class Order +{ + public string OrderNo { get; set; } + public string CustomerID { get; set; } + public decimal Qty { get; set; } + public string ProductID { get; set; } + public decimal Amt { get; set; } } ``` -#### Q. Can I retrieve informations about what sheets are visible or active? +![image](https://user-images.githubusercontent.com/12729184/118939964-d24bc480-b982-11eb-88dd-f06655f6121a.png) + + + +### FAQ + +#### Q: Excel header title not equal class property name, how to mapping? + +A. Please use ExcelColumnName attribute + +![image](https://user-images.githubusercontent.com/12729184/116020475-eac50980-a678-11eb-8804-129e87200e5e.png) + +#### Q. How to query or export multiple-sheets? + +A. `GetSheetNames` method with Query sheetName parameter. + -A. You can use the `GetSheetInformations` method: ```csharp -var importer = MiniExcel.Importers.GetOpenXmlImporter(); -var sheets = importer.GetSheetInformations(path); +var sheets = MiniExcel.GetSheetNames(path); +foreach (var sheet in sheets) +{ + Console.WriteLine($"sheet name : {sheet} "); + var rows = MiniExcel.Query(path,useHeaderRow:true,sheetName:sheet); + Console.WriteLine(rows); +} +``` + +![image](https://user-images.githubusercontent.com/12729184/116199841-2a1f5300-a76a-11eb-90a3-6710561cf6db.png) + +#### Q. How to query or export information about sheet visibility? +A. `GetSheetInformations` method. + + + +```csharp +var sheets = MiniExcel.GetSheetInformations(path); foreach (var sheetInfo in sheets) { Console.WriteLine($"sheet index : {sheetInfo.Index} "); // next sheet index - numbered from 0 Console.WriteLine($"sheet name : {sheetInfo.Name} "); // sheet name Console.WriteLine($"sheet state : {sheetInfo.State} "); // sheet visibility state - visible / hidden - Console.WriteLine($"sheet active : {sheetInfo.Active} "); // whether the sheet is currently marked as active } ``` -#### Q. Is there a way to count all rows from a sheet without having to query it first? -A. Yes, you can use the method `GetSheetDimensions`: +#### Q. Whether to use Count will load all data into the memory? -```csharp -var excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); -var dimensions = excelImporter.GetSheetDimensions(path); +No, the image test has 1 million rows*10 columns of data, the maximum memory usage is <60MB, and it takes 13.65 seconds -Console.WriteLine($"Total rows: {dimensions[0].Rows.Count}"); -``` +![image](https://user-images.githubusercontent.com/12729184/117118518-70586000-adc3-11eb-9ce3-2ba76cf8b5e5.png) -#### Q. Is it possible to use integer indexes for the columns? +#### Q. How does Query use integer indexs? -A. The default indexes of a MiniExcel Query are the strings "A", "B", "C"... -If you want to switch to a numeric index you can copy the following method for converting them: +The default index of Query is the string Key: A,B,C.... If you want to change to numeric index, please create the following method to convert ```csharp -IEnumerable> ConvertToIntIndexRows(IEnumerable rows) +void Main() { + var path = @"D:\git\MiniExcel\samples\xlsx\TestTypeMapping.xlsx"; + var rows = MiniExcel.Query(path,true); + foreach (var r in ConvertToIntIndexRows(rows)) + { + Console.Write($"column 0 : {r[0]} ,column 1 : {r[1]}"); + Console.WriteLine(); + } +} + +private IEnumerable> ConvertToIntIndexRows(IEnumerable rows) +{ + ICollection keys = null; var isFirst = true; - ICollection keys = []; - foreach (IDictionary row in rows) + foreach (IDictionary r in rows) { if(isFirst) { - keys = row.Keys; + keys = r.Keys; isFirst = false; } - var dict = new Dictionary(); - + var dic = new Dictionary(); var index = 0; foreach (var key in keys) - { - dict[index++] = row[key]; - } - - yield return dict; + dic[index++] = r[key]; + yield return dic; } } ``` -#### Q. Why is no header generated when trying to export an empty enumerable? +#### Q. No title empty excel is generated when the value is empty when exporting Excel -A. MiniExcel uses reflection to dynamically get the type from the values. If there's no data to begin with, the header is also skipped. You can check [issue 133](https://github.com/mini-software/MiniExcel/issues/133) for details. +Because MiniExcel uses a logic similar to JSON.NET to dynamically get type from values to simplify API operations, type cannot be knew without data. You can check [issue #133](https://github.com/mini-software/MiniExcel/issues/133) for understanding. -#### Q. How to stop iterating after a blank row is hit? +![image](https://user-images.githubusercontent.com/12729184/122639771-546c0c00-d12e-11eb-800c-498db27889ca.png) -A. LINQ's `TakeWhile` extension method can be used for this purpose. +> Strong type & DataTable will generate headers, but Dictionary are still empty Excel -#### Q. Some of the rows in my document are empty, can they be removed automatically? +#### Q. How to stop the foreach when blank row? -![image](https://user-images.githubusercontent.com/12729184/137873865-7107d8f5-eb59-42db-903a-44e80589f1b2.png) +MiniExcel can be used with `LINQ TakeWhile` to stop foreach iterator. -A. Yes, simply set the `IgnoreEmptyRows` property of the `OpenXmlConfiguration`. +![Image](https://user-images.githubusercontent.com/12729184/130209137-162621c2-f337-4479-9996-beeac65bc4d4.png) +#### Q. How to remove empty rows? + +![image](https://user-images.githubusercontent.com/12729184/137873865-7107d8f5-eb59-42db-903a-44e80589f1b2.png) -#### Q. How SaveAs(path,value) to replace exists file and without throwing "The file ...xlsx already exists error" -A. You can use the `overwriteFile` parameter for overwriting an existing file: +IEnumerable : ```csharp -var excelExporter = MiniExcel.Exporters.GetOpenXmlExporter(); -excelExporter.Export(path, value, overwriteFile: true); +public static IEnumerable QueryWithoutEmptyRow(Stream stream, bool useHeaderRow, string sheetName, ExcelType excelType, string startCell, IConfiguration configuration) +{ + var rows = stream.Query(useHeaderRow,sheetName,excelType,startCell,configuration); + foreach (IDictionary row in rows) + { + if(row.Keys.Any(key=>row[key]!=null)) + yield return row; + } +} ``` -You can also implement your own stream for finer grained control: + + +DataTable : ```csharp -var excelExporter = MiniExcel.Exporters.GetOpenXmlExporter(); +public static DataTable QueryAsDataTableWithoutEmptyRow(Stream stream, bool useHeaderRow, string sheetName, ExcelType excelType, string startCell, IConfiguration configuration) +{ + if (sheetName == null && excelType != ExcelType.CSV) /*Issue #279*/ + sheetName = stream.GetSheetNames().First(); + + var dt = new DataTable(sheetName); + var first = true; + var rows = stream.Query(useHeaderRow,sheetName,excelType,startCell,configuration); + foreach (IDictionary row in rows) + { + if (first) + { + + foreach (var key in row.Keys) + { + var column = new DataColumn(key, typeof(object)) { Caption = key }; + dt.Columns.Add(column); + } + + dt.BeginLoadData(); + first = false; + } -using var stream = File.Create("Demo.xlsx"); -excelExporter.Export(stream,value); + var newRow = dt.NewRow(); + var isNull=true; + foreach (var key in row.Keys) + { + var _v = row[key]; + if(_v!=null) + isNull = false; + newRow[key] = _v; + } + + if(!isNull) + dt.Rows.Add(newRow); + } + + dt.EndLoadData(); + return dt; +} ``` -### Limitations and caveats -- There is currently no support for the `.xls` legacy Excel format or for encrypted files -- There is only basic query support for the `.xlsm` Excel format +#### Q. How SaveAs(path,value) to replace exists file and without throwing "The file ...xlsx already exists error" -### References +Please use Stream class to custom file creating logic, e.g: + +```C# + using (var stream = File.Create("Demo.xlsx")) + MiniExcel.SaveAs(stream,value); +``` + + + +or, since V1.25.0, SaveAs support overwriteFile parameter for enable/unable overwriting exist file + +```csharp + MiniExcel.SaveAs(path, value, overwriteFile: true); +``` + + + + +### Limitations and caveats + +- Not support xls and encrypted file now +- xlsm only support Query + + + +### Reference [ExcelDataReader](https://github.com/ExcelDataReader/ExcelDataReader) / [ClosedXML](https://github.com/ClosedXML/ClosedXML) / [Dapper](https://github.com/DapperLib/Dapper) / [ExcelNumberFormat](https://github.com/andersnm/ExcelNumberFormat) + ### Thanks -#### Jetbrains +#### [Jetbrains](https://www.jetbrains.com/) ![jetbrains-variant-2](https://user-images.githubusercontent.com/12729184/123997015-8456c180-da02-11eb-829a-aec476fe8e94.png) -Thanks to [**Jetbrains**](https://www.jetbrains.com/) for providing a free All product IDE for this project ([License](https://user-images.githubusercontent.com/12729184/123988233-6ab17c00-d9fa-11eb-8739-2a08c6a4a263.png)) - -#### Zomp - -![](https://avatars.githubusercontent.com/u/63680941?s=200&v=4) +Thanks for providing a free All product IDE for this project ([License](https://user-images.githubusercontent.com/12729184/123988233-6ab17c00-d9fa-11eb-8739-2a08c6a4a263.png)) -Thanks to [**Zomp**](https://github.com/zompinc) and [@virzak](https://github.com/virzak) for helping us implement a new asynchronous API -and for their [sync-method-generator](https://github.com/zompinc/sync-method-generator), a great source generator -for automating the creation of synchronous functions based on asynchronous ones. -### Donations sharing -[Link](https://github.com/orgs/mini-software/discussions/754) +### Contribution sharing donate +Link https://github.com/orgs/mini-software/discussions/754 ### Contributors -![](https://contrib.rocks/image?repo=mini-software/MiniExcel) \ No newline at end of file +![](https://contrib.rocks/image?repo=mini-software/MiniExcel) diff --git a/README_OLD.md b/README_OLD.md deleted file mode 100644 index becd65a2..00000000 --- a/README_OLD.md +++ /dev/null @@ -1,1992 +0,0 @@ -
-

NuGet -Build status -star GitHub stars -version -Ask DeepWiki -

-
- ---- - -[](https://www.dotnetfoundation.org/) - -
-

This project is part of the .NET Foundation and operates under their code of conduct.

-
- ---- - - - - ---- - -
- Your Stars or Donations can make MiniExcel better -
- ---- - -### Introduction - -MiniExcel is a simple and efficient Excel processing tool for .NET, specifically designed to minimize memory usage. - -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. - -```mermaid -flowchart LR - A1(["Excel analysis
process"]) --> A2{{"Unzipping
XLSX file"}} --> A3{{"Parsing
OpenXML"}} --> A4{{"Model
conversion"}} --> A5(["Output"]) - - 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"]) - - 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; - - class A1,A2,A3,A4,A5 analysis; - class B1,B2,B3,B4,B5 others; - class C1,C2,C3,C4,C5 miniexcel; -``` - -### Features - -- 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 DLL size under 500KB -- Simple and intuitive 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](benchmarks/MiniExcel.Benchmarks/Program.cs). - -The file used to test performance is [**Test1,000,000x10.xlsx**](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". - -To run all the benchmarks use: - -```bash -dotnet run -project .\benchmarks\MiniExcel.Benchmarks -c Release -f net9.0 -filter * --join -``` - -You can find the benchmarks' results for the latest release [here](benchmarks/results). - - -### Excel Query/Import - -#### 1. Execute a query and map the results to a strongly typed IEnumerable [[Try it]](https://dotnetfiddle.net/w5WD1J) - -Recommand to use Stream.Query because of better efficiency. - -```csharp -public class UserAccount -{ - public Guid ID { get; set; } - public string Name { get; set; } - public DateTime BoD { get; set; } - public int Age { get; set; } - public bool VIP { get; set; } - public decimal Points { get; set; } -} - -var rows = MiniExcel.Query(path); - -// or - -using (var stream = File.OpenRead(path)) - var rows = stream.Query(); -``` - -![image](https://user-images.githubusercontent.com/12729184/111107423-c8c46b80-8591-11eb-982f-c97a2dafb379.png) - -#### 2. Execute a query and map it to a list of dynamic objects without using head [[Try it]](https://dotnetfiddle.net/w5WD1J) - -* dynamic key is `A.B.C.D..` - -| MiniExcel | 1 | -|-----------|---| -| Github | 2 | - -```csharp - -var rows = MiniExcel.Query(path).ToList(); - -// or -using (var stream = File.OpenRead(path)) -{ - var rows = stream.Query().ToList(); - - Assert.Equal("MiniExcel", rows[0].A); - Assert.Equal(1, rows[0].B); - Assert.Equal("Github", rows[1].A); - Assert.Equal(2, rows[1].B); -} -``` - -#### 3. Execute a query with first header row [[Try it]](https://dotnetfiddle.net/w5WD1J) - -note : same column name use last right one - -Input Excel : - -| Column1 | Column2 | -|-----------|---------| -| MiniExcel | 1 | -| Github | 2 | - - -```csharp - -var rows = MiniExcel.Query(useHeaderRow:true).ToList(); - -// or - -using (var stream = File.OpenRead(path)) -{ - var rows = stream.Query(useHeaderRow:true).ToList(); - - Assert.Equal("MiniExcel", rows[0].Column1); - Assert.Equal(1, rows[0].Column2); - Assert.Equal("Github", rows[1].Column1); - Assert.Equal(2, rows[1].Column2); -} -``` - -#### 4. Query Support LINQ Extension First/Take/Skip ...etc - -Query First -```csharp -var row = MiniExcel.Query(path).First(); -Assert.Equal("HelloWorld", row.A); - -// or - -using (var stream = File.OpenRead(path)) -{ - var row = stream.Query().First(); - Assert.Equal("HelloWorld", row.A); -} -``` - -Performance between MiniExcel/ExcelDataReader/ClosedXML/EPPlus -![queryfirst](https://user-images.githubusercontent.com/12729184/111072392-6037a900-8515-11eb-9693-5ce2dad1e460.gif) - -#### 5. Query by sheet name - -```csharp -MiniExcel.Query(path, sheetName: "SheetName"); -//or -stream.Query(sheetName: "SheetName"); -``` - -#### 6. Query all sheet name and rows - -```csharp -var sheetNames = MiniExcel.GetSheetNames(path); -foreach (var sheetName in sheetNames) -{ - var rows = MiniExcel.Query(path, sheetName: sheetName); -} -``` - -#### 7. Get Columns - -```csharp -var columns = MiniExcel.GetColumns(path); // e.g result : ["A","B"...] - -var cnt = columns.Count; // get column count -``` - -#### 8. Dynamic Query cast row to `IDictionary` - -```csharp -foreach(IDictionary row in MiniExcel.Query(path)) -{ - //.. -} - -// or -var rows = MiniExcel.Query(path).Cast>(); -// or Query specified ranges (capitalized) -// A2 represents the second row of column A, C3 represents the third row of column C -// If you don't want to restrict rows, just don't include numbers -var rows = MiniExcel.QueryRange(path, startCell: "A2", endCell: "C3").Cast>(); -``` - - - -#### 9. Query Excel return DataTable - -Not recommended, because DataTable will load all data into memory and lose MiniExcel's low memory consumption feature. - -```C# -var table = MiniExcel.QueryAsDataTable(path, useHeaderRow: true); -``` - -![image](https://user-images.githubusercontent.com/12729184/116673475-07917200-a9d6-11eb-947e-a6f68cce58df.png) - - - -#### 10. Specify the cell to start reading data - -```csharp -MiniExcel.Query(path,useHeaderRow:true,startCell:"B3") -``` - -![image](https://user-images.githubusercontent.com/12729184/117260316-8593c400-ae81-11eb-9877-c087b7ac2b01.png) - - - -#### 11. Fill Merged Cells - -Note: The efficiency is slower compared to `not using merge fill` - -Reason: The OpenXml standard puts mergeCells at the bottom of the file, which leads to the need to foreach the sheetxml twice - -```csharp - var config = new OpenXmlConfiguration() - { - FillMergedCells = true - }; - var rows = MiniExcel.Query(path, configuration: config); -``` - -![image](https://user-images.githubusercontent.com/12729184/117973630-3527d500-b35f-11eb-95c3-bde255f8114e.png) - -support variable length and width multi-row and column filling - -![image](https://user-images.githubusercontent.com/12729184/117973820-6d2f1800-b35f-11eb-88d8-555063938108.png) - -#### 12. Reading big file by disk-base cache (Disk-Base Cache - SharedString) - -If the SharedStrings size exceeds 5 MB, MiniExcel default will use local disk cache, e.g, [10x100000.xlsx](https://github.com/MiniExcel/MiniExcel/files/8403819/NotDuplicateSharedStrings_10x100000.xlsx)(one million rows data), when disable disk cache the maximum memory usage is 195MB, but able disk cache only needs 65MB. Note, this optimization needs some efficiency cost, so this case will increase reading time from 7.4 seconds to 27.2 seconds, If you don't need it that you can disable disk cache with the following code: - -```csharp -var config = new OpenXmlConfiguration { EnableSharedStringCache = false }; -MiniExcel.Query(path,configuration: config) -``` - -You can use `SharedStringCacheSize ` to change the sharedString file size beyond the specified size for disk caching -```csharp -var config = new OpenXmlConfiguration { SharedStringCacheSize=500*1024*1024 }; -MiniExcel.Query(path, configuration: config); -``` - - -![image](https://user-images.githubusercontent.com/12729184/161411851-1c3f72a7-33b3-4944-84dc-ffc1d16747dd.png) - -![image](https://user-images.githubusercontent.com/12729184/161411825-17f53ec7-bef4-4b16-b234-e24799ea41b0.png) - - - - - - - - - -### Create/Export Excel - -1. Must be a non-abstract type with a public parameterless constructor . - -2. MiniExcel support parameter IEnumerable Deferred Execution, If you want to use least memory, please do not call methods such as ToList - -e.g : ToList or not memory usage -![image](https://user-images.githubusercontent.com/12729184/112587389-752b0b00-8e38-11eb-8a52-cfb76c57e5eb.png) - - - -#### 1. Anonymous or strongly type [[Try it]](https://dotnetfiddle.net/w5WD1J) - -```csharp -var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); -MiniExcel.SaveAs(path, new[] { - new { Column1 = "MiniExcel", Column2 = 1 }, - new { Column1 = "Github", Column2 = 2} -}); -``` - -#### 2. `IEnumerable>` - -```csharp -var values = new List>() -{ - new Dictionary{{ "Column1", "MiniExcel" }, { "Column2", 1 } }, - new Dictionary{{ "Column1", "Github" }, { "Column2", 2 } } -}; -MiniExcel.SaveAs(path, values); -``` - -Create File Result : - -| Column1 | Column2 | -|-----------|---------| -| MiniExcel | 1 | -| Github | 2 | - - -#### 3. IDataReader -- `Recommended`, it can avoid to load all data into memory -```csharp -MiniExcel.SaveAs(path, reader); -``` - -![image](https://user-images.githubusercontent.com/12729184/121275378-149a5e80-c8bc-11eb-85fe-5453552134f0.png) - -DataReader export multiple sheets (recommand by Dapper ExecuteReader) - -```csharp -using (var cnn = Connection) -{ - cnn.Open(); - var sheets = new Dictionary(); - sheets.Add("sheet1", cnn.ExecuteReader("select 1 id")); - sheets.Add("sheet2", cnn.ExecuteReader("select 2 id")); - MiniExcel.SaveAs("Demo.xlsx", sheets); -} -``` - - - -#### 4. Datatable - -- `Not recommended`, it will load all data into memory - -- DataTable use Caption for column name first, then use columname - -```csharp -var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); -var table = new DataTable(); -{ - table.Columns.Add("Column1", typeof(string)); - table.Columns.Add("Column2", typeof(decimal)); - table.Rows.Add("MiniExcel", 1); - table.Rows.Add("Github", 2); -} - -MiniExcel.SaveAs(path, table); -``` - -#### 5. Dapper Query - -Thanks @shaofing #552 , please use `CommandDefinition + CommandFlags.NoCache` - -```csharp -using (var connection = GetConnection(connectionString)) -{ - var rows = connection.Query( - new CommandDefinition( - @"select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2" - , flags: CommandFlags.NoCache) - ); - // Note: QueryAsync will throw close connection exception - MiniExcel.SaveAs(path, rows); -} -``` - -Below code will load all data into memory - -```csharp -using (var connection = GetConnection(connectionString)) -{ - var rows = connection.Query(@"select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2"); - MiniExcel.SaveAs(path, rows); -} -``` - - -#### 6. SaveAs to MemoryStream [[Try it]](https://dotnetfiddle.net/JOen0e) - -```csharp -using (var stream = new MemoryStream()) //support FileStream,MemoryStream ect. -{ - stream.SaveAs(values); -} -``` - -e.g : api of export excel - -```csharp -public IActionResult DownloadExcel() -{ - var values = new[] { - new { Column1 = "MiniExcel", Column2 = 1 }, - new { Column1 = "Github", Column2 = 2} - }; - - var memoryStream = new MemoryStream(); - memoryStream.SaveAs(values); - memoryStream.Seek(0, SeekOrigin.Begin); - return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") - { - FileDownloadName = "demo.xlsx" - }; -} -``` - - -#### 7. Create Multiple Sheets - -```csharp -// 1. Dictionary -var users = new[] { new { Name = "Jack", Age = 25 }, new { Name = "Mike", Age = 44 } }; -var department = new[] { new { ID = "01", Name = "HR" }, new { ID = "02", Name = "IT" } }; -var sheets = new Dictionary -{ - ["users"] = users, - ["department"] = department -}; -MiniExcel.SaveAs(path, sheets); - -// 2. DataSet -var sheets = new DataSet(); -sheets.Add(UsersDataTable); -sheets.Add(DepartmentDataTable); -//.. -MiniExcel.SaveAs(path, sheets); -``` - -![image](https://user-images.githubusercontent.com/12729184/118130875-6e7c4580-b430-11eb-9b82-22f02716bd63.png) - - -#### 8. TableStyles Options - -Default style - -![image](https://user-images.githubusercontent.com/12729184/138234373-cfa97109-b71f-4711-b7f5-0eaaa4a0a3a6.png) - -Without style configuration - -```csharp -var config = new OpenXmlConfiguration() -{ - TableStyles = TableStyles.None -}; -MiniExcel.SaveAs(path, value,configuration:config); -``` - -![image](https://user-images.githubusercontent.com/12729184/118784917-f3e57700-b8c2-11eb-8718-8d955b1bc197.png) - - -#### 9. AutoFilter - -Since v0.19.0 `OpenXmlConfiguration.AutoFilter` can en/unable AutoFilter , default value is `true`, and setting AutoFilter way: - -```csharp -MiniExcel.SaveAs(path, value, configuration: new OpenXmlConfiguration() { AutoFilter = false }); -``` - - - -#### 10. Create Image - -```csharp -var value = new[] { - new { Name="github",Image=File.ReadAllBytes(PathHelper.GetFile("images/github_logo.png"))}, - new { Name="google",Image=File.ReadAllBytes(PathHelper.GetFile("images/google_logo.png"))}, - new { Name="microsoft",Image=File.ReadAllBytes(PathHelper.GetFile("images/microsoft_logo.png"))}, - new { Name="reddit",Image=File.ReadAllBytes(PathHelper.GetFile("images/reddit_logo.png"))}, - new { Name="statck_overflow",Image=File.ReadAllBytes(PathHelper.GetFile("images/statck_overflow_logo.png"))}, -}; -MiniExcel.SaveAs(path, value); -``` - -![image](https://user-images.githubusercontent.com/12729184/150462383-ad9931b3-ed8d-4221-a1d6-66f799743433.png) - - - -#### 11. Byte Array File Export - -Since 1.22.0, when value type is `byte[]` then system will save file path at cell by default, and when import system can be converted to `byte[]`. And if you don't want to use it, you can set `OpenXmlConfiguration.EnableConvertByteArray` to `false`, it can improve the system efficiency. - -![image](https://user-images.githubusercontent.com/12729184/153702334-c3b834f4-6ae4-4ddf-bd4e-e5005d5d8c6a.png) - -Since 1.22.0, when value type is `byte[]` then system will save file path at cell by default, and when import system can be converted to `byte[]`. And if you don't want to use it, you can set `OpenXmlConfiguration.EnableConvertByteArray` to `false`, it can improve the system efficiency. - -![image](https://user-images.githubusercontent.com/12729184/153702334-c3b834f4-6ae4-4ddf-bd4e-e5005d5d8c6a.png) - -#### 12. Merge same cells vertically - -This functionality is only supported in `xlsx` format and merges cells vertically between @merge and @endmerge tags. -You can use @mergelimit to limit boundaries of merging cells vertically. - -```csharp -var mergedFilePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid().ToString()}.xlsx"); - -var path = @"../../../../../samples/xlsx/TestMergeWithTag.xlsx"; - -MiniExcel.MergeSameCells(mergedFilePath, path); -``` - -```csharp -var memoryStream = new MemoryStream(); - -var path = @"../../../../../samples/xlsx/TestMergeWithTag.xlsx"; - -memoryStream.MergeSameCells(path); -``` - -File content before and after merge: - -Without merge limit: - -Screenshot 2023-08-07 at 11 59 24 - -Screenshot 2023-08-07 at 11 59 57 - -With merge limit: - -Screenshot 2023-08-08 at 18 21 00 - -Screenshot 2023-08-08 at 18 21 40 - -#### 13. Skip null values - -New explicit option to write empty cells for null values: - -```csharp -DataTable dt = new DataTable(); - -/* ... */ - -DataRow dr = dt.NewRow(); - -dr["Name1"] = "Somebody once"; -dr["Name2"] = null; -dr["Name3"] = "told me."; - -dt.Rows.Add(dr); - -OpenXmlConfiguration configuration = new OpenXmlConfiguration() -{ - EnableWriteNullValueCell = true // Default value. -}; - -MiniExcel.SaveAs(@"C:\temp\Book1.xlsx", dt, configuration: configuration); -``` - -![image](https://user-images.githubusercontent.com/31481586/241419455-3c0aec8a-4e5f-4d83-b7ec-6572124c165d.png) - -```xml - - - Somebody once - - - - told me. - - -``` - -Previous behavior: - -```csharp -/* ... */ - -OpenXmlConfiguration configuration = new OpenXmlConfiguration() -{ - EnableWriteNullValueCell = false // Default value is true. -}; - -MiniExcel.SaveAs(@"C:\temp\Book1.xlsx", dt, configuration: configuration); -``` - -![image](https://user-images.githubusercontent.com/31481586/241419441-c4f27e8f-3f87-46db-a10f-08665864c874.png) - -```xml - - - Somebody once - - - - - - told me. - - -``` - -Works for null and DBNull values. - -#### 14. Freeze Panes -```csharp -/* ... */ - -OpenXmlConfiguration configuration = new OpenXmlConfiguration() -{ - FreezeRowCount = 1, // default is 1 - FreezeColumnCount = 2 // default is 0 -}; - -MiniExcel.SaveAs(@"C:\temp\Book1.xlsx", dt, configuration: configuration); -``` - -![image](docs/images/freeze-pane-1.png) - - -### Fill Data To Excel Template - -- The declaration is similar to Vue template `{{variable name}}`, or the collection rendering `{{collection name.field name}}` -- Collection rendering support IEnumerable/DataTable/DapperRow - -#### 1. Basic Fill - -Template: -![image](https://user-images.githubusercontent.com/12729184/114537556-ed8d2b00-9c84-11eb-8303-a69f62c41e5b.png) - -Result: -![image](https://user-images.githubusercontent.com/12729184/114537490-d8180100-9c84-11eb-8c69-db58692f3a85.png) - -Code: -```csharp -// 1. By POCO -var value = new -{ - Name = "Jack", - CreateDate = new DateTime(2021, 01, 01), - VIP = true, - Points = 123 -}; -MiniExcel.SaveAsByTemplate(path, templatePath, value); - - -// 2. By Dictionary -var value = new Dictionary() -{ - ["Name"] = "Jack", - ["CreateDate"] = new DateTime(2021, 01, 01), - ["VIP"] = true, - ["Points"] = 123 -}; -MiniExcel.SaveAsByTemplate(path, templatePath, value); -``` - - - -#### 2. IEnumerable Data Fill - -> Note1: Use the first IEnumerable of the same column as the basis for filling list - -Template: -![image](https://user-images.githubusercontent.com/12729184/114564652-14f2f080-9ca3-11eb-831f-09e3fedbc5fc.png) - -Result: -![image](https://user-images.githubusercontent.com/12729184/114564204-b2015980-9ca2-11eb-900d-e21249f93f7c.png) - -Code: -```csharp -//1. By POCO -var value = new -{ - employees = new[] { - new {name="Jack",department="HR"}, - new {name="Lisa",department="HR"}, - new {name="John",department="HR"}, - new {name="Mike",department="IT"}, - new {name="Neo",department="IT"}, - new {name="Loan",department="IT"} - } -}; -MiniExcel.SaveAsByTemplate(path, templatePath, value); - -//2. By Dictionary -var value = new Dictionary() -{ - ["employees"] = new[] { - new {name="Jack",department="HR"}, - new {name="Lisa",department="HR"}, - new {name="John",department="HR"}, - new {name="Mike",department="IT"}, - new {name="Neo",department="IT"}, - new {name="Loan",department="IT"} - } -}; -MiniExcel.SaveAsByTemplate(path, templatePath, value); -``` - - - -#### 3. Complex Data Fill - -> Note: Support multi-sheets and using same varible - -Template: - -![image](https://user-images.githubusercontent.com/12729184/114565255-acf0da00-9ca3-11eb-8a7f-8131b2265ae8.png) - -Result: - -![image](https://user-images.githubusercontent.com/12729184/114565329-bf6b1380-9ca3-11eb-85e3-3969e8bf6378.png) - -```csharp -// 1. By POCO -var value = new -{ - title = "FooCompany", - managers = new[] { - new {name="Jack",department="HR"}, - new {name="Loan",department="IT"} - }, - employees = new[] { - new {name="Wade",department="HR"}, - new {name="Felix",department="HR"}, - new {name="Eric",department="IT"}, - new {name="Keaton",department="IT"} - } -}; -MiniExcel.SaveAsByTemplate(path, templatePath, value); - -// 2. By Dictionary -var value = new Dictionary() -{ - ["title"] = "FooCompany", - ["managers"] = new[] { - new {name="Jack",department="HR"}, - new {name="Loan",department="IT"} - }, - ["employees"] = new[] { - new {name="Wade",department="HR"}, - new {name="Felix",department="HR"}, - new {name="Eric",department="IT"}, - new {name="Keaton",department="IT"} - } -}; -MiniExcel.SaveAsByTemplate(path, templatePath, value); -``` - -#### 4. Fill Big Data Performance - -> NOTE: Using IEnumerable deferred execution not ToList can save max memory usage in MiniExcel - -![image](https://user-images.githubusercontent.com/12729184/114577091-5046ec80-9cae-11eb-924b-087c7becf8da.png) - - - -#### 5. Cell value auto mapping type - -Template - -![image](https://user-images.githubusercontent.com/12729184/114802504-64830a80-9dd0-11eb-8d56-8e8c401b3ace.png) - -Result - -![image](https://user-images.githubusercontent.com/12729184/114802419-43221e80-9dd0-11eb-9ffe-a2ce34fe7076.png) - -Class - -```csharp -public class Poco -{ - public string @string { get; set; } - public int? @int { get; set; } - public decimal? @decimal { get; set; } - public double? @double { get; set; } - public DateTime? datetime { get; set; } - public bool? @bool { get; set; } - public Guid? Guid { get; set; } -} -``` - -Code - -```csharp -var poco = new TestIEnumerableTypePoco { @string = "string", @int = 123, @decimal = decimal.Parse("123.45"), @double = (double)123.33, @datetime = new DateTime(2021, 4, 1), @bool = true, @Guid = Guid.NewGuid() }; -var value = new -{ - Ts = new[] { - poco, - new TestIEnumerableTypePoco{}, - null, - poco - } -}; -MiniExcel.SaveAsByTemplate(path, templatePath, value); -``` - - - -#### 6. Example : List Github Projects - -Template - -![image](https://user-images.githubusercontent.com/12729184/115068623-12073280-9f25-11eb-9124-f4b3efcdb2a7.png) - - -Result - -![image](https://user-images.githubusercontent.com/12729184/115068639-1a5f6d80-9f25-11eb-9f45-27c434d19a78.png) - -Code - -```csharp -var projects = new[] -{ - new {Name = "MiniExcel",Link="https://github.com/mini-software/MiniExcel",Star=146, CreateTime=new DateTime(2021,03,01)}, - new {Name = "HtmlTableHelper",Link="https://github.com/mini-software/HtmlTableHelper",Star=16, CreateTime=new DateTime(2020,02,01)}, - new {Name = "PocoClassGenerator",Link="https://github.com/mini-software/PocoClassGenerator",Star=16, CreateTime=new DateTime(2019,03,17)} -}; -var value = new -{ - User = "ITWeiHan", - Projects = projects, - TotalStar = projects.Sum(s => s.Star) -}; -MiniExcel.SaveAsByTemplate(path, templatePath, value); -``` - -#### 7. Grouped Data Fill - -```csharp -var value = new Dictionary() -{ - ["employees"] = new[] { - new {name="Jack",department="HR"}, - new {name="Jack",department="HR"}, - new {name="John",department="HR"}, - new {name="John",department="IT"}, - new {name="Neo",department="IT"}, - new {name="Loan",department="IT"} - } -}; -await MiniExcel.SaveAsByTemplateAsync(path, templatePath, value); -``` -##### 1. With `@group` tag and with `@header` tag - -Before - -![before_with_header](https://user-images.githubusercontent.com/38832863/218646717-21b9d57a-2be2-4e9a-801b-ae212231d2b4.PNG) - -After - -![after_with_header](https://user-images.githubusercontent.com/38832863/218646721-58a7a340-7004-4bc2-af24-cffcb2c20737.PNG) - -##### 2. With @group tag and without @header tag - -Before - -![before_without_header](https://user-images.githubusercontent.com/38832863/218646873-b12417fa-801b-4890-8e96-669ed3b43902.PNG) - -After - -![after_without_header](https://user-images.githubusercontent.com/38832863/218646872-622461ba-342e-49ee-834f-b91ad9c2dac3.PNG) - -##### 3. Without @group tag - -Before - -![without_group](https://user-images.githubusercontent.com/38832863/218646975-f52a68eb-e031-43b5-abaa-03b67c052d1a.PNG) - -After - -![without_group_after](https://user-images.githubusercontent.com/38832863/218646974-4a3c0e07-7c66-4088-ad07-b4ad3695b7e1.PNG) - -#### 8. If/ElseIf/Else Statements inside cell - -Rules: -1. Supports DateTime, Double, Int with ==, !=, >, >=, <, <= operators. -2. Supports String with ==, != operators. -3. Each statement should be new line. -4. Single space should be added before and after operators. -5. There shouldn't be new line inside of statements. -6. Cell should be in exact format as below. - -```csharp -@if(name == Jack) -{{employees.name}} -@elseif(name == Neo) -Test {{employees.name}} -@else -{{employees.department}} -@endif -``` - -Before - -![if_before](https://user-images.githubusercontent.com/38832863/235360606-ca654769-ff55-4f5b-98d2-d2ec0edb8173.PNG) - -After - -![if_after](https://user-images.githubusercontent.com/38832863/235360609-869bb960-d63d-45ae-8d64-9e8b0d0ab658.PNG) - -#### 9. DataTable as parameter - -```csharp -var managers = new DataTable(); -{ - managers.Columns.Add("name"); - managers.Columns.Add("department"); - managers.Rows.Add("Jack", "HR"); - managers.Rows.Add("Loan", "IT"); -} -var value = new Dictionary() -{ - ["title"] = "FooCompany", - ["managers"] = managers, -}; -MiniExcel.SaveAsByTemplate(path, templatePath, value); -``` -#### 10. Formulas - -##### 1. Example -Prefix your formula with `$` and use `$enumrowstart` and `$enumrowend` to mark references to the enumerable start and end rows: - -![image](docs/images/template-formulas-1.png) - -When the template is rendered, the `$` prefix will be removed and `$enumrowstart` and `$enumrowend` will be replaced with the start and end row numbers of the enumerable: - -![image](docs/images/template-formulas-2.png) - -##### 2. Other Example Formulas: - -| | | -|--------------|-------------------------------------------------------------------------------------------| -| Sum | `$=SUM(C{{$enumrowstart}}:C{{$enumrowend}})` | -| Alt. Average | `$=SUM(C{{$enumrowstart}}:C{{$enumrowend}}) / COUNT(C{{$enumrowstart}}:C{{$enumrowend}})` | -| Range | `$=MAX(C{{$enumrowstart}}:C{{$enumrowend}}) - MIN(C{{$enumrowstart}}:C{{$enumrowend}})` | - - -#### 11. Other - -##### 1. Checking template parameter key - -Since V1.24.0 , default ignore template missing parameter key and replace it with empty string, `IgnoreTemplateParameterMissing` can control throwing exception or not. - -```csharp -var config = new OpenXmlConfiguration() -{ - IgnoreTemplateParameterMissing = false, -}; -MiniExcel.SaveAsByTemplate(path, templatePath, value, config) -``` - -![image](https://user-images.githubusercontent.com/12729184/157464332-e316f829-54aa-4c84-a5aa-9aef337b668d.png) - - - -### Excel Column Name/Index/Ignore Attribute - - - -#### 1. Specify the column name, column index, column ignore - -Excel Example - -![image](https://user-images.githubusercontent.com/12729184/114230869-3e163700-99ac-11eb-9a90-2039d4b4b313.png) - -Code - -```csharp -public class ExcelAttributeDemo -{ - [ExcelColumnName("Column1")] - public string Test1 { get; set; } - [ExcelColumnName("Column2")] - public string Test2 { get; set; } - [ExcelIgnore] - public string Test3 { get; set; } - [ExcelColumnIndex("I")] // system will convert "I" to 8 index - public string Test4 { get; set; } - public string Test5 { get; } //wihout set will ignore - public string Test6 { get; private set; } //un-public set will ignore - [ExcelColumnIndex(3)] // start with 0 - public string Test7 { get; set; } -} - -var rows = MiniExcel.Query(path).ToList(); -Assert.Equal("Column1", rows[0].Test1); -Assert.Equal("Column2", rows[0].Test2); -Assert.Null(rows[0].Test3); -Assert.Equal("Test7", rows[0].Test4); -Assert.Null(rows[0].Test5); -Assert.Null(rows[0].Test6); -Assert.Equal("Test4", rows[0].Test7); -``` - - - - - -#### 2. Custom Format (ExcelFormatAttribute) - -Since V0.21.0 support class which contains `ToString(string content)` method format - -Class - -```csharp -public class Dto -{ - public string Name { get; set; } - - [ExcelFormat("MMMM dd, yyyy")] - public DateTime InDate { get; set; } -} -``` - -Code - -```csharp -var value = new Dto[] { - new Issue241Dto{ Name="Jack",InDate=new DateTime(2021,01,04)}, - new Issue241Dto{ Name="Henry",InDate=new DateTime(2020,04,05)}, -}; -MiniExcel.SaveAs(path, value); -``` - -Result - -![image](https://user-images.githubusercontent.com/12729184/118910788-ab2bcd80-b957-11eb-8d42-bfce36621b1b.png) - -Query supports custom format conversion - -![image](https://user-images.githubusercontent.com/12729184/118911286-87b55280-b958-11eb-9a88-c8ff403d240a.png) - -#### 3. Set Column Width(ExcelColumnWidthAttribute) - -```csharp -public class Dto -{ - [ExcelColumnWidth(20)] - public int ID { get; set; } - [ExcelColumnWidth(15.50)] - public string Name { get; set; } -} -``` - -#### 4. Multiple column names mapping to the same property. - -```csharp -public class Dto -{ - [ExcelColumnName(excelColumnName:"EmployeeNo",aliases:new[] { "EmpNo","No" })] - public string Empno { get; set; } - public string Name { get; set; } -} -``` - - - -#### 5. System.ComponentModel.DisplayNameAttribute = ExcelColumnName.excelColumnNameAttribute - -Since 1.24.0, system supports System.ComponentModel.DisplayNameAttribute = ExcelColumnName.excelColumnNameAttribute - -```C# -public class TestIssueI4TXGTDto -{ - public int ID { get; set; } - public string Name { get; set; } - [DisplayName("Specification")] - public string Spc { get; set; } - [DisplayName("Unit Price")] - public decimal Up { get; set; } -} -``` - - - -#### 6. ExcelColumnAttribute - -Since V1.26.0, multiple attributes can be simplified like : -```csharp - public class TestIssueI4ZYUUDto - { - [ExcelColumn(Name = "ID",Index =0)] - public string MyProperty { get; set; } - [ExcelColumn(Name = "CreateDate", Index = 1,Format ="yyyy-MM",Width =100)] - public DateTime MyProperty2 { get; set; } - } -``` - - - -#### 7. DynamicColumnAttribute - -Since V1.26.0, we can set the attributes of Column dynamically -```csharp - var config = new OpenXmlConfiguration - { - DynamicColumns = new DynamicExcelColumn[] { - 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"}, - } - }; - var path = PathHelper.GetTempPath(); - var value = new[] { new { id = 1, name = "Jack", createdate = new DateTime(2022, 04, 12) ,point = 123.456} }; - MiniExcel.SaveAs(path, value, configuration: config); -``` -![image](https://user-images.githubusercontent.com/12729184/164510353-5aecbc4e-c3ce-41e8-b6cf-afd55eb23b68.png) - -#### 8. DynamicSheetAttribute - -Since V1.31.4 we can set the attributes of Sheet dynamically. We can set sheet name and state (visibility). -```csharp - var configuration = new OpenXmlConfiguration - { - DynamicSheets = new DynamicExcelSheet[] { - new DynamicExcelSheet("usersSheet") { Name = "Users", State = SheetState.Visible }, - new DynamicExcelSheet("departmentSheet") { Name = "Departments", State = SheetState.Hidden } - } - }; - - var users = new[] { new { Name = "Jack", Age = 25 }, new { Name = "Mike", Age = 44 } }; - var department = new[] { new { ID = "01", Name = "HR" }, new { ID = "02", Name = "IT" } }; - var sheets = new Dictionary - { - ["usersSheet"] = users, - ["departmentSheet"] = department - }; - - var path = PathHelper.GetTempPath(); - MiniExcel.SaveAs(path, sheets, configuration: configuration); -``` - -We can also use new attribute ExcelSheetAttribute: - -```C# - [ExcelSheet(Name = "Departments", State = SheetState.Hidden)] - private class DepartmentDto - { - [ExcelColumn(Name = "ID",Index = 0)] - public string ID { get; set; } - [ExcelColumn(Name = "Name",Index = 1)] - public string Name { get; set; } - } -``` - -### Add, Delete, Update - -#### Add - -v1.28.0 support CSV insert N rows data after last row - -```csharp -// Origin -{ - var value = new[] { - new { ID=1,Name ="Jack",InDate=new DateTime(2021,01,03)}, - new { ID=2,Name ="Henry",InDate=new DateTime(2020,05,03)}, - }; - MiniExcel.SaveAs(path, value); -} -// Insert 1 rows after last -{ - var value = new { ID=3,Name = "Mike", InDate = new DateTime(2021, 04, 23) }; - MiniExcel.Insert(path, value); -} -// Insert N rows after last -{ - var value = new[] { - new { ID=4,Name ="Frank",InDate=new DateTime(2021,06,07)}, - new { ID=5,Name ="Gloria",InDate=new DateTime(2022,05,03)}, - }; - MiniExcel.Insert(path, value); -} -``` - -![image](https://user-images.githubusercontent.com/12729184/191023733-1e2fa732-db5c-4a3a-9722-b891fe5aa069.png) - -v1.37.0 support excel insert a new sheet into an existing workbook - -```csharp -// Origin excel -{ - var value = new[] { - new { ID=1,Name ="Jack",InDate=new DateTime(2021,01,03)}, - new { ID=2,Name ="Henry",InDate=new DateTime(2020,05,03)}, - }; - MiniExcel.SaveAs(path, value, sheetName: "Sheet1"); -} -// Insert a new sheet -{ - var value = new { ID=3,Name = "Mike", InDate = new DateTime(2021, 04, 23) }; - MiniExcel.Insert(path, table, sheetName: "Sheet2"); -} -``` - - - -#### Delete(waiting) - -#### Update(waiting) - - - -### Excel Type Auto Check - -- MiniExcel will check whether it is xlsx or csv based on the `file extension` by default, but there may be inaccuracy, please specify it manually. -- Stream cannot be know from which excel, please specify it manually. - -```csharp -stream.SaveAs(excelType:ExcelType.CSV); -//or -stream.SaveAs(excelType:ExcelType.XLSX); -//or -stream.Query(excelType:ExcelType.CSV); -//or -stream.Query(excelType:ExcelType.XLSX); -``` - - - - - -### CSV - -#### Note - -- Default return `string` type, and value will not be converted to numbers or datetime, unless the type is defined by strong typing generic. - - - -#### Custom separator - -The default is `,` as the separator, you can modify the `Seperator` property for customization - -```csharp -var config = new MiniExcelLibs.Csv.CsvConfiguration() -{ - Seperator=';' -}; -MiniExcel.SaveAs(path, values,configuration: config); -``` - -Since V1.30.1 support function to custom separator (thanks @hyzx86) - -```csharp -var config = new CsvConfiguration() -{ - SplitFn = (row) => Regex.Split(row, $"[\t,](?=(?:[^\"]|\"[^\"]*\")*$)") - .Select(s => Regex.Replace(s.Replace("\"\"", "\""), "^\"|\"$", "")).ToArray() -}; -var rows = MiniExcel.Query(path, configuration: config).ToList(); -``` - - - -#### Custom line break - -The default is `\r\n` as the newline character, you can modify the `NewLine` property for customization - -```csharp -var config = new MiniExcelLibs.Csv.CsvConfiguration() -{ - NewLine='\n' -}; -MiniExcel.SaveAs(path, values,configuration: config); -``` - - - -#### Custom coding - -- The default encoding is "Detect Encoding From Byte Order Marks" (detectEncodingFromByteOrderMarks: true) -- f you have custom encoding requirements, please modify the StreamReaderFunc / StreamWriterFunc property - -```csharp -// Read -var config = new MiniExcelLibs.Csv.CsvConfiguration() -{ - StreamReaderFunc = (stream) => new StreamReader(stream,Encoding.GetEncoding("gb2312")) -}; -var rows = MiniExcel.Query(path, true,excelType:ExcelType.CSV,configuration: config); - -// Write -var config = new MiniExcelLibs.Csv.CsvConfiguration() -{ - StreamWriterFunc = (stream) => new StreamWriter(stream, Encoding.GetEncoding("gb2312")) -}; -MiniExcel.SaveAs(path, value,excelType:ExcelType.CSV, configuration: config); -``` - -#### Read empty string as null - -By default, empty values are mapped to string.Empty. You can modify this behavior - -```csharp -var config = new MiniExcelLibs.Csv.CsvConfiguration() -{ - ReadEmptyStringAsNull = true -}; -``` - - -### DataReader - -#### 1. GetReader -Since 1.23.0, you can GetDataReader - -```csharp - using (var reader = MiniExcel.GetReader(path,true)) - { - while (reader.Read()) - { - for (int i = 0; i < reader.FieldCount; i++) - { - var value = reader.GetValue(i); - } - } - } -``` - - - -### Async - -- v0.17.0 support Async (thanks isdaniel ( SHIH,BING-SIOU)](https://github.com/isdaniel)) - -```csharp -public static Task SaveAsAsync(string path, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration configuration = null) -public static Task SaveAsAsync(this Stream stream, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, IConfiguration configuration = null) -public static Task> QueryAsync(string path, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) -public static Task> QueryAsync(this Stream stream, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) where T : class, new() -public static Task> QueryAsync(string path, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) where T : class, new() -public static Task>> QueryAsync(this Stream stream, bool useHeaderRow = false, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) -public static Task SaveAsByTemplateAsync(this Stream stream, string templatePath, object value) -public static Task SaveAsByTemplateAsync(this Stream stream, byte[] templateBytes, object value) -public static Task SaveAsByTemplateAsync(string path, string templatePath, object value) -public static Task SaveAsByTemplateAsync(string path, byte[] templateBytes, object value) -public static Task QueryAsDataTableAsync(string path, bool useHeaderRow = true, string sheetName = null, ExcelType excelType = ExcelType.UNKNOWN, string startCell = "A1", IConfiguration configuration = null) -``` - -- v1.25.0 support `cancellationToken`。 - - - -### Others - -#### 1. Enum - -Be sure excel & property name same, system will auto mapping (case insensitive) - -![image](https://user-images.githubusercontent.com/12729184/116210595-9784b100-a775-11eb-936f-8e7a8b435961.png) - -Since V0.18.0 support Enum Description - -```csharp -public class Dto -{ - public string Name { get; set; } - public I49RYZUserType UserType { get; set; } -} - -public enum Type -{ - [Description("General User")] - V1, - [Description("General Administrator")] - V2, - [Description("Super Administrator")] - V3 -} -``` - -![image](https://user-images.githubusercontent.com/12729184/133116630-27cc7161-099a-48b8-9784-cd1e443af3d1.png) - -Since 1.30.0 version support excel Description to Enum , thanks @KaneLeung - -#### 2. Convert CSV to XLSX or Convert XLSX to CSV - -```csharp -MiniExcel.ConvertXlsxToCsv(xlsxPath, csvPath); -MiniExcel.ConvertXlsxToCsv(xlsxStream, csvStream); -MiniExcel.ConvertCsvToXlsx(csvPath, xlsxPath); -MiniExcel.ConvertCsvToXlsx(csvStream, xlsxStream); -``` -```csharp -using (var excelStream = new FileStream(path: filePath, FileMode.Open, FileAccess.Read)) -using (var csvStream = new MemoryStream()) -{ - MiniExcel.ConvertXlsxToCsv(excelStream, csvStream); -} -``` - -#### 3. Custom CultureInfo - -Since 1.22.0, you can custom CultureInfo like below, system default `CultureInfo.InvariantCulture`. - -```csharp -var config = new CsvConfiguration() -{ - Culture = new CultureInfo("fr-FR"), -}; -MiniExcel.SaveAs(path, value, configuration: config); - -// or -MiniExcel.Query(path, configuration: config); -``` - - -#### 4. Custom Buffer Size -```csharp - public abstract class Configuration : IConfiguration - { - public int BufferSize { get; set; } = 1024 * 512; - } -``` - -#### 5. FastMode - -System will not control memory, but you can get faster save speed. - -```csharp -var config = new OpenXmlConfiguration() { FastMode = true }; -MiniExcel.SaveAs(path, reader,configuration:config); -``` - -#### 6. Batch Add Image (MiniExcel.AddPicture) - -Please add pictures before batch generate rows data, or system will load large memory usage when calling AddPicture. - -```csharp -var images = new[] -{ - new MiniExcelPicture - { - ImageBytes = File.ReadAllBytes(PathHelper.GetFile("images/github_logo.png")), - SheetName = null, // default null is first sheet - CellAddress = "C3", // required - }, - new MiniExcelPicture - { - ImageBytes = File.ReadAllBytes(PathHelper.GetFile("images/google_logo.png")), - PictureType = "image/png", // default PictureType = image/png - SheetName = "Demo", - CellAddress = "C9", // required - WidthPx = 100, - HeightPx = 100, - }, -}; -MiniExcel.AddPicture(path, images); -``` -![Image](https://github.com/user-attachments/assets/19c4d241-9753-4ede-96c8-f810c1a22247) - -#### 7. Get Sheets Dimension - -```csharp -var dim = MiniExcel.GetSheetDimensions(path); -``` - -### Examples: - -#### 1. SQLite & Dapper `Large Size File` SQL Insert Avoid OOM - -note : please don't call ToList/ToArray methods after Query, it'll load all data into memory - -```csharp -using (var connection = new SQLiteConnection(connectionString)) -{ - connection.Open(); - using (var transaction = connection.BeginTransaction()) - using (var stream = File.OpenRead(path)) - { - var rows = stream.Query(); - foreach (var row in rows) - connection.Execute("insert into T (A,B) values (@A,@B)", new { row.A, row.B }, transaction: transaction); - transaction.Commit(); - } -} -``` - -performance: -![image](https://user-images.githubusercontent.com/12729184/111072579-2dda7b80-8516-11eb-9843-c01a1edc88ec.png) - - - - - -#### 2. ASP.NET Core 3.1 or MVC 5 Download/Upload Excel Xlsx API Demo [Try it](tests/MiniExcel.Tests.AspNetCore) - -```csharp -public class ApiController : Controller -{ - public IActionResult Index() - { - return new ContentResult - { - ContentType = "text/html", - StatusCode = (int)HttpStatusCode.OK, - Content = @" -DownloadExcel
-DownloadExcelFromTemplatePath
-DownloadExcelFromTemplateBytes
-

Upload Excel

-
-
- -
- value = new Dictionary() - { - ["title"] = "FooCompany", - ["managers"] = new[] { - new {name="Jack",department="HR"}, - new {name="Loan",department="IT"} - }, - ["employees"] = new[] { - new {name="Wade",department="HR"}, - new {name="Felix",department="HR"}, - new {name="Eric",department="IT"}, - new {name="Keaton",department="IT"} - } - }; - - MemoryStream memoryStream = new MemoryStream(); - memoryStream.SaveAsByTemplate(templatePath, value); - memoryStream.Seek(0, SeekOrigin.Begin); - return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") - { - FileDownloadName = "demo.xlsx" - }; - } - - private static Dictionary TemplateBytesCache = new Dictionary(); - - static ApiController() - { - string templatePath = "TestTemplateComplex.xlsx"; - byte[] bytes = System.IO.File.ReadAllBytes(templatePath); - TemplateBytesCache.Add(templatePath, bytes); - } - - public IActionResult DownloadExcelFromTemplateBytes() - { - byte[] bytes = TemplateBytesCache["TestTemplateComplex.xlsx"]; - - Dictionary value = new Dictionary() - { - ["title"] = "FooCompany", - ["managers"] = new[] { - new {name="Jack",department="HR"}, - new {name="Loan",department="IT"} - }, - ["employees"] = new[] { - new {name="Wade",department="HR"}, - new {name="Felix",department="HR"}, - new {name="Eric",department="IT"}, - new {name="Keaton",department="IT"} - } - }; - - MemoryStream memoryStream = new MemoryStream(); - memoryStream.SaveAsByTemplate(bytes, value); - memoryStream.Seek(0, SeekOrigin.Begin); - return new FileStreamResult(memoryStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") - { - FileDownloadName = "demo.xlsx" - }; - } - - public IActionResult UploadExcel(IFormFile excel) - { - var stream = new MemoryStream(); - excel.CopyTo(stream); - - foreach (var item in stream.Query(true)) - { - // do your logic etc. - } - - return Ok("File uploaded successfully"); - } -} -``` - -#### 3. Paging Query - -```csharp -void Main() -{ - var rows = MiniExcel.Query(path); - - Console.WriteLine("==== No.1 Page ===="); - Console.WriteLine(Page(rows,pageSize:3,page:1)); - Console.WriteLine("==== No.50 Page ===="); - Console.WriteLine(Page(rows,pageSize:3,page:50)); - Console.WriteLine("==== No.5000 Page ===="); - Console.WriteLine(Page(rows,pageSize:3,page:5000)); -} - -public static IEnumerable Page(IEnumerable en, int pageSize, int page) -{ - return en.Skip(page * pageSize).Take(pageSize); -} -``` - -![20210419](https://user-images.githubusercontent.com/12729184/114679083-6ef4c400-9d3e-11eb-9f78-a86daa45fe46.gif) - - - -#### 4. WebForm export Excel by memorystream - -```csharp -var fileName = "Demo.xlsx"; -var sheetName = "Sheet1"; -HttpResponse response = HttpContext.Current.Response; -response.Clear(); -response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; -response.AddHeader("Content-Disposition", $"attachment;filename=\"{fileName}\""); -var values = new[] { - new { Column1 = "MiniExcel", Column2 = 1 }, - new { Column1 = "Github", Column2 = 2} -}; -var memoryStream = new MemoryStream(); -memoryStream.SaveAs(values, sheetName: sheetName); -memoryStream.Seek(0, SeekOrigin.Begin); -memoryStream.CopyTo(Response.OutputStream); -response.End(); -``` - - - -#### 5. Dynamic i18n multi-language and role authority management - -Like the example, create a method to handle i18n and permission management, and use `yield return to return IEnumerable>` to achieve dynamic and low-memory processing effects - -```csharp -void Main() -{ - var value = new Order[] { - new Order(){OrderNo = "SO01",CustomerID="C001",ProductID="P001",Qty=100,Amt=500}, - new Order(){OrderNo = "SO02",CustomerID="C002",ProductID="P002",Qty=300,Amt=400}, - }; - - Console.WriteLine("en-Us and Sales role"); - { - var path = Path.GetTempPath() + Guid.NewGuid() + ".xlsx"; - var lang = "en-US"; - var role = "Sales"; - MiniExcel.SaveAs(path, GetOrders(lang, role, value)); - MiniExcel.Query(path, true).Dump(); - } - - Console.WriteLine("zh-CN and PMC role"); - { - var path = Path.GetTempPath() + Guid.NewGuid() + ".xlsx"; - var lang = "zh-CN"; - var role = "PMC"; - MiniExcel.SaveAs(path, GetOrders(lang, role, value)); - MiniExcel.Query(path, true).Dump(); - } -} - -private IEnumerable> GetOrders(string lang, string role, Order[] orders) -{ - foreach (var order in orders) - { - var newOrder = new Dictionary(); - - if (lang == "zh-CN") - { - newOrder.Add("客户编号", order.CustomerID); - newOrder.Add("订单编号", order.OrderNo); - newOrder.Add("产品编号", order.ProductID); - newOrder.Add("数量", order.Qty); - if (role == "Sales") - newOrder.Add("价格", order.Amt); - yield return newOrder; - } - else if (lang == "en-US") - { - newOrder.Add("Customer ID", order.CustomerID); - newOrder.Add("Order No", order.OrderNo); - newOrder.Add("Product ID", order.ProductID); - newOrder.Add("Quantity", order.Qty); - if (role == "Sales") - newOrder.Add("Amount", order.Amt); - yield return newOrder; - } - else - { - throw new InvalidDataException($"lang {lang} wrong"); - } - } -} - -public class Order -{ - public string OrderNo { get; set; } - public string CustomerID { get; set; } - public decimal Qty { get; set; } - public string ProductID { get; set; } - public decimal Amt { get; set; } -} -``` - -![image](https://user-images.githubusercontent.com/12729184/118939964-d24bc480-b982-11eb-88dd-f06655f6121a.png) - - - -### FAQ - -#### Q: Excel header title not equal class property name, how to mapping? - -A. Please use ExcelColumnName attribute - -![image](https://user-images.githubusercontent.com/12729184/116020475-eac50980-a678-11eb-8804-129e87200e5e.png) - -#### Q. How to query or export multiple-sheets? - -A. `GetSheetNames` method with Query sheetName parameter. - - - -```csharp -var sheets = MiniExcel.GetSheetNames(path); -foreach (var sheet in sheets) -{ - Console.WriteLine($"sheet name : {sheet} "); - var rows = MiniExcel.Query(path,useHeaderRow:true,sheetName:sheet); - Console.WriteLine(rows); -} -``` - -![image](https://user-images.githubusercontent.com/12729184/116199841-2a1f5300-a76a-11eb-90a3-6710561cf6db.png) - -#### Q. How to query or export information about sheet visibility? - -A. `GetSheetInformations` method. - - - -```csharp -var sheets = MiniExcel.GetSheetInformations(path); -foreach (var sheetInfo in sheets) -{ - Console.WriteLine($"sheet index : {sheetInfo.Index} "); // next sheet index - numbered from 0 - Console.WriteLine($"sheet name : {sheetInfo.Name} "); // sheet name - Console.WriteLine($"sheet state : {sheetInfo.State} "); // sheet visibility state - visible / hidden -} -``` - - -#### Q. Whether to use Count will load all data into the memory? - -No, the image test has 1 million rows*10 columns of data, the maximum memory usage is <60MB, and it takes 13.65 seconds - -![image](https://user-images.githubusercontent.com/12729184/117118518-70586000-adc3-11eb-9ce3-2ba76cf8b5e5.png) - -#### Q. How does Query use integer indexs? - -The default index of Query is the string Key: A,B,C.... If you want to change to numeric index, please create the following method to convert - -```csharp -void Main() -{ - var path = @"D:\git\MiniExcel\samples\xlsx\TestTypeMapping.xlsx"; - var rows = MiniExcel.Query(path,true); - foreach (var r in ConvertToIntIndexRows(rows)) - { - Console.Write($"column 0 : {r[0]} ,column 1 : {r[1]}"); - Console.WriteLine(); - } -} - -private IEnumerable> ConvertToIntIndexRows(IEnumerable rows) -{ - ICollection keys = null; - var isFirst = true; - foreach (IDictionary r in rows) - { - if(isFirst) - { - keys = r.Keys; - isFirst = false; - } - - var dic = new Dictionary(); - var index = 0; - foreach (var key in keys) - dic[index++] = r[key]; - yield return dic; - } -} -``` - -#### Q. No title empty excel is generated when the value is empty when exporting Excel - -Because MiniExcel uses a logic similar to JSON.NET to dynamically get type from values to simplify API operations, type cannot be knew without data. You can check [issue #133](https://github.com/mini-software/MiniExcel/issues/133) for understanding. - -![image](https://user-images.githubusercontent.com/12729184/122639771-546c0c00-d12e-11eb-800c-498db27889ca.png) - -> Strong type & DataTable will generate headers, but Dictionary are still empty Excel - -#### Q. How to stop the foreach when blank row? - -MiniExcel can be used with `LINQ TakeWhile` to stop foreach iterator. - -![Image](https://user-images.githubusercontent.com/12729184/130209137-162621c2-f337-4479-9996-beeac65bc4d4.png) - -#### Q. How to remove empty rows? - -![image](https://user-images.githubusercontent.com/12729184/137873865-7107d8f5-eb59-42db-903a-44e80589f1b2.png) - - -IEnumerable : - -```csharp -public static IEnumerable QueryWithoutEmptyRow(Stream stream, bool useHeaderRow, string sheetName, ExcelType excelType, string startCell, IConfiguration configuration) -{ - var rows = stream.Query(useHeaderRow,sheetName,excelType,startCell,configuration); - foreach (IDictionary row in rows) - { - if(row.Keys.Any(key=>row[key]!=null)) - yield return row; - } -} -``` - - - -DataTable : - -```csharp -public static DataTable QueryAsDataTableWithoutEmptyRow(Stream stream, bool useHeaderRow, string sheetName, ExcelType excelType, string startCell, IConfiguration configuration) -{ - if (sheetName == null && excelType != ExcelType.CSV) /*Issue #279*/ - sheetName = stream.GetSheetNames().First(); - - var dt = new DataTable(sheetName); - var first = true; - var rows = stream.Query(useHeaderRow,sheetName,excelType,startCell,configuration); - foreach (IDictionary row in rows) - { - if (first) - { - - foreach (var key in row.Keys) - { - var column = new DataColumn(key, typeof(object)) { Caption = key }; - dt.Columns.Add(column); - } - - dt.BeginLoadData(); - first = false; - } - - var newRow = dt.NewRow(); - var isNull=true; - foreach (var key in row.Keys) - { - var _v = row[key]; - if(_v!=null) - isNull = false; - newRow[key] = _v; - } - - if(!isNull) - dt.Rows.Add(newRow); - } - - dt.EndLoadData(); - return dt; -} -``` - - - -#### Q. How SaveAs(path,value) to replace exists file and without throwing "The file ...xlsx already exists error" - - -Please use Stream class to custom file creating logic, e.g: - -```C# - using (var stream = File.Create("Demo.xlsx")) - MiniExcel.SaveAs(stream,value); -``` - - - -or, since V1.25.0, SaveAs support overwriteFile parameter for enable/unable overwriting exist file - -```csharp - MiniExcel.SaveAs(path, value, overwriteFile: true); -``` - - - - -### Limitations and caveats - -- Not support xls and encrypted file now -- xlsm only support Query - - - -### Reference - -[ExcelDataReader](https://github.com/ExcelDataReader/ExcelDataReader) / [ClosedXML](https://github.com/ClosedXML/ClosedXML) / [Dapper](https://github.com/DapperLib/Dapper) / [ExcelNumberFormat](https://github.com/andersnm/ExcelNumberFormat) - - - -### Thanks - -#### [Jetbrains](https://www.jetbrains.com/) - -![jetbrains-variant-2](https://user-images.githubusercontent.com/12729184/123997015-8456c180-da02-11eb-829a-aec476fe8e94.png) - -Thanks for providing a free All product IDE for this project ([License](https://user-images.githubusercontent.com/12729184/123988233-6ab17c00-d9fa-11eb-8739-2a08c6a4a263.png)) - - - -### Contribution sharing donate -Link https://github.com/orgs/mini-software/discussions/754 - -### Contributors - -![](https://contrib.rocks/image?repo=mini-software/MiniExcel) diff --git a/V2-upgrade-notes.md b/V2-Upgrade-Notes.md similarity index 56% rename from V2-upgrade-notes.md rename to V2-Upgrade-Notes.md index eb7d5764..2fc7349b 100644 --- a/V2-upgrade-notes.md +++ b/V2-Upgrade-Notes.md @@ -1,12 +1,13 @@ -## MiniExcel 2.0 Migration Guide +## MiniExcel 2.0 Upgrade Notes -- The root namespace was changed from `MiniExcelLibs` to `MiniExcelLib`. If the full MiniExcel package is downloaded, the previous namespace will still exist and will contain the now old and deprecated methods' signatures -- Instead of having all methods being part of the `MiniExcel` static class, the functionalities are now split into 3 providers accessible from the same class: -`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 division was adopted for export methods with `OpenXmlExporter` and `CsvExporter` -- 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 +- Support for .NET Framework 4.5 was dropped, the minimum supported Framework version is now 4.6.2. +- The root namespace was changed from `MiniExcelLibs` to `MiniExcelLib`. +- Instead of having all methods being part of the `MiniExcel` static class, the functionalities are now split into 3 providers: +`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. +- 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 - MiniExcel now fully supports asynchronous streaming the queries, -so the return type for `OpenXmlImporter.QueryAsync` is `IAsyncEnumerable` instead of `Task>` \ No newline at end of file +so the return type for `OpenXmlImporter.QueryAsync` is `IAsyncEnumerable` instead of `Task>` \ No newline at end of file From 6db7f60f6e6a3a6b703e72732163deea5af21c7f Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Fri, 22 Aug 2025 23:41:06 +0200 Subject: [PATCH 9/9] Rebased and added tiny fix to readme --- README-V2.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README-V2.md b/README-V2.md index 653a7152..e64ffdf7 100644 --- a/README-V2.md +++ b/README-V2.md @@ -1516,8 +1516,8 @@ MiniExcelPicture[] images = }, ]; -var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); -exporter.AddPicture(path, images); +var templater = MiniExcel.Exporters.GetOpenXmlExporter(); +templater.AddPicture(path, images); ``` ![Image](https://github.com/user-attachments/assets/19c4d241-9753-4ede-96c8-f810c1a22247)