diff --git a/MiniExcel.slnx b/MiniExcel.slnx index 92c62e02..f111dbe4 100644 --- a/MiniExcel.slnx +++ b/MiniExcel.slnx @@ -15,9 +15,11 @@ + + diff --git a/README-V2.md b/README-V2.md new file mode 100644 index 00000000..e64ffdf7 --- /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 templater = MiniExcel.Exporters.GetOpenXmlExporter(); +templater.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 becd65a2..f6647478 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,15 @@ flowchart LR - 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 +### Version 2.0 preview + +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! + +If you do, make sure to also check out the [new docs](README-V2.md) and the [upgrade notes](V2-Upgrade-Notes.md). + + ### Get Started - [Import/Query Excel](#getstart1) @@ -75,7 +84,6 @@ flowchart LR - [Examples](#getstart5) - ### Installation You can install the package [from NuGet](https://www.nuget.org/packages/MiniExcel) diff --git a/V2-Upgrade-Notes.md b/V2-Upgrade-Notes.md new file mode 100644 index 00000000..2fc7349b --- /dev/null +++ b/V2-Upgrade-Notes.md @@ -0,0 +1,13 @@ +## MiniExcel 2.0 Upgrade Notes + +- 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 diff --git a/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs b/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs index 80b5c268..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; @@ -103,7 +102,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 +442,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 +457,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 +473,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 +628,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/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/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 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,