NuGet Build status
English | 简体中文 | 繁體中文
🙌 Your Star can make MiniExcel better 🙌
--- ### Introduction MiniExcel is simple and efficient to avoid OOM's .NET processing Excel tool. At present, most popular frameworks need to load all the data into the memory to facilitate operation, but it will cause memory consumption problems. MiniExcel tries to use algorithm from a stream to reduce the original 1000 MB occupation to a few MB to avoid OOM(out of memory). ![image](https://user-images.githubusercontent.com/12729184/113086657-ab8bd000-9214-11eb-9563-c970ac1ee35e.png) ### Features - Low memory consumption, avoid OOM (out of memory) and full GC - Support `real-time` operation of each row of data ![miniexcel_lazy_load](https://user-images.githubusercontent.com/12729184/111034290-e5588a80-844f-11eb-8c84-6fdb6fb8f403.gif) - Support LINQ deferred execution, it can do low-consumption, fast paging and other complex queries - Lightweight, without Microsoft Office installed, no COM+, third-party dependencies, DLL size is less than 100KB - Easy API style to read/write/fill excel ### Get Started - [Excel Query](#getstart1) - [Create Excel](#getstart2) - [Fill Data To 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/shps951023/MiniExcel/projects/1?fullscreen=true) ### Performance [**Test1,000,000x10.xlsx**](benchmarks/MiniExcel.Benchmarks/Test1%2C000%2C000x10.xlsx) as performance test basic file,A total of 10,000,000 "HelloWorld" with a file size of 23 MB Benchmarks logic can be found in [MiniExcel.Benchmarks](benchmarks/MiniExcel.Benchmarks/Program.cs) , and test cli ``` dotnet run -p .\benchmarks\MiniExcel.Benchmarks\ -c Release -f netcoreapp3.1 -- -f * --join ``` Output from the latest run is : ``` BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 Intel Core i7-7700 CPU 3.60GHz (Kaby Lake), 1 CPU, 8 logical and 4 physical cores [Host] : .NET Framework 4.8 (4.8.4341.0), X64 RyuJIT Job-ZYYABG : .NET Framework 4.8 (4.8.4341.0), X64 RyuJIT IterationCount=3 LaunchCount=3 WarmupCount=3 ``` | Method | Max Memory Usage | Mean | Gen 0 | Gen 1 | Gen 2 | | ---------------------------- | ---------------: | ---------------: | -----------: | ----------: | ---------: | | 'MiniExcel QueryFirst' | 0.109 MB | 726.4 us | - | - | - | | 'ExcelDataReader QueryFirst' | 15.24 MB | 10,664,238.2 us | 566000.0000 | 1000.0000 | - | | 'MiniExcel Query' | 17.3 MB | 14,179,334.8 us | 367000.0000 | 96000.0000 | 7000.0000 | | 'ExcelDataReader Query' | 17.3 MB | 22,565,088.7 us | 1210000.0000 | 2000.0000 | - | | 'Epplus QueryFirst' | 1,452 MB | 18,198,015.4 us | 535000.0000 | 132000.0000 | 9000.0000 | | 'Epplus Query' | 1,451 MB | 23,647,471.1 us | 1451000.0000 | 133000.0000 | 9000.0000 | | 'OpenXmlSDK Query' | 1,412 MB | 52,003,270.1 us | 978000.0000 | 353000.0000 | 11000.0000 | | 'OpenXmlSDK QueryFirst' | 1,413 MB | 52,348,659.1 us | 978000.0000 | 353000.0000 | 11000.0000 | | 'ClosedXml QueryFirst' | 2,158 MB | 66,188,979.6 us | 2156000.0000 | 575000.0000 | 9000.0000 | | 'ClosedXml Query' | 2,184 MB | 191,434,126.6 us | 2165000.0000 | 577000.0000 | 10000.0000 | | Method | Max Memory Usage | Mean | Gen 0 | Gen 1 | Gen 2 | | ------------------------ | ---------------: | ---------------: | -----------: | -----------: | ---------: | | 'MiniExcel Create Xlsx' | 15 MB | 11,531,819.8 us | 1020000.0000 | - | - | | 'Epplus Create Xlsx' | 1,204 MB | 22,509,717.7 us | 1370000.0000 | 60000.0000 | 30000.0000 | | 'OpenXmlSdk Create Xlsx' | 2,621 MB | 42,473,998.9 us | 1370000.0000 | 460000.0000 | 50000.0000 | | 'ClosedXml Create Xlsx' | 7,141 MB | 140,939,928.6 us | 5520000.0000 | 1500000.0000 | 80000.0000 | ### Excel Query #### 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>(); ``` #### 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) ### Create 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. Datatable - 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); ``` #### 3. Dapper ```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); } ``` #### 4. `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 | #### 5. SaveAs Stream [[Try it]](https://dotnetfiddle.net/JOen0e) ```csharp using (var stream = File.Create(path)) { stream.SaveAs(values); } ``` #### 5. 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" }; } ``` #### 6. Support IDataReader value parameter ```csharp MiniExcel.SaveAs(path, reader); ``` #### 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 From the v0.15.0 version, the default style has been changed to ![image](https://user-images.githubusercontent.com/12729184/118784493-a36e1980-b8c2-11eb-8a3d-a669985aea1a.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) ### 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/shps951023/MiniExcel",Star=146, CreateTime=new DateTime(2021,03,01)}, new {Name = "HtmlTableHelper",Link="https://github.com/shps951023/HtmlTableHelper",Star=16, CreateTime=new DateTime(2020,02,01)}, new {Name = "PocoClassGenerator",Link="https://github.com/shps951023/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. 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); ``` ### 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 DateTime Format (ExcelFormatAttribute) 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) ### 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); ``` ### 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


" }; } 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" }; } public IActionResult DownloadExcelFromTemplatePath() { string templatePath = "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(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 mapping enum? A. 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) #### 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; } } ``` ### 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) - [StackExchange/Dapper](https://github.com/StackExchange/Dapper) ### Contributors ![](https://contrib.rocks/image?repo=shps951023/MiniExcel)