Skip to content

Commit 3f40cd3

Browse files
Adding fluent-mapping support (#850)
* Draft: fluent-mapping support * Addressing minor feedback and fixing the collection yielding to maintain IEnumerable without iterating * Optimizing for performance and cleanup of unnecessary allocations. * Adding support for templates with mappings * Consolidating some of the grid logic * Updating PR * Mostly style and format changes Changed == null checks to is null checks, added nullable reference types annotations and pattern matching where appropriate, moved new tests to FluentMapping folder, other minor changes * Added mapping samples to readme * Rebased and moved fluent mapping docs to new readme * Moved templating logic to MappingTemplater and renamed MappingExporter's SaveAsAsync method to ExportAsync for consistency * Added overload for exporting by path instead of stream * Fixed issue with nested collection mapping and improved test assertions * Various adjustments - Added explicit IEnumerator implementation to MappingCellEnumerator for clarity - Straightened recursive implementation of MappingCellEnumerator's MoveNext into a loop - Expanded one of the tests - Changed some checks to pattern matching - Some other minor style changes * Changes to the FluentMapping folder structure - Renamed namespace Mapping to FluentMapping - Moved mapping importer, exporter and templater to new Api folder - Moved methods GetMappingImporter, GetMappingExporter and GetMappingTemplater from MiniExcelProviders to a ProviderExtensions class as extension methods * Rebasing and fixing wrong parameter order * Repairing codeql setup --------- Co-authored-by: Michele Bastione <[email protected]>
1 parent 2443e75 commit 3f40cd3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+6453
-46
lines changed

.github/workflows/codeql-analysis.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,11 @@ jobs:
4343
- name: Setup .NET
4444
uses: actions/setup-dotnet@v4
4545
with:
46-
dotnet-version: |
47-
8.0.x
48-
10.0.x
49-
46+
dotnet-version: 8.0.x
47+
48+
- name: Restore dependencies
49+
run: dotnet restore
50+
5051
# Initializes the CodeQL tools for scanning.
5152
- name: Initialize CodeQL
5253
uses: github/codeql-action/init@v3
@@ -71,7 +72,7 @@ jobs:
7172
# uses a compiled language
7273

7374
- name: Manual build
74-
run: dotnet build
75+
run: dotnet build MiniExcel.slnx --no-restore --configuration Release
7576

7677
- name: Perform CodeQL Analysis
7778
uses: github/codeql-action/analyze@v3

README-V2.md

Lines changed: 174 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ You can find the benchmarks' results for the latest release [here](benchmarks/re
230230
- [Attributes and configuration](#docs-attributes)
231231
- [CSV specifics](#docs-csv)
232232
- [Other functionalities](#docs-other)
233+
- [Fluent Cell Mapping](#docs-mapping)
233234
- [FAQ](#docs-faq)
234235
- [Limitations](#docs-limitations)
235236

@@ -647,29 +648,8 @@ When queried, the resource will be converted back to `byte[]`. If you don't need
647648

648649
![image](https://user-images.githubusercontent.com/12729184/153702334-c3b834f4-6ae4-4ddf-bd4e-e5005d5d8c6a.png)
649650

650-
#### 12. Merge same cells vertically
651651

652-
This functionality merges cells vertically between the tags `@merge` and `@endmerge`.
653-
You can use `@mergelimit` to limit boundaries of merging cells vertically.
654-
655-
```csharp
656-
var templater = MiniExcel.Templaters.GetOpenXmlTemplater();
657-
templater.MergeSameCells(mergedFilePath, templatePath);
658-
```
659-
660-
File content before and after merge without merge limit:
661-
662-
<img width="318" alt="Screenshot 2023-08-07 at 11 59 24" src="https://github.com/mini-software/MiniExcel/assets/38832863/49cc96b9-6c35-4bf3-8d43-a9752a15b22e">
663-
664-
<img width="318" alt="Screenshot 2023-08-07 at 11 59 57" src="https://github.com/mini-software/MiniExcel/assets/38832863/3fbd529b-3ae6-4bbe-b4d8-2793a5a58010">
665-
666-
File content before and after merge with merge limit:
667-
668-
<img width="346" alt="Screenshot 2023-08-08 at 18 21 00" src="https://github.com/mini-software/MiniExcel/assets/38832863/04049d28-84d5-4c2a-bcff-5847547df5e1">
669-
670-
<img width="346" alt="Screenshot 2023-08-08 at 18 21 40" src="https://github.com/mini-software/MiniExcel/assets/38832863/f5cf8957-b0b0-4831-b8fc-8556299235c2">
671-
672-
#### 13. Null values handling
652+
#### 12. Null values handling
673653

674654
By default, null values will be treated as empty strings when exporting:
675655

@@ -718,7 +698,7 @@ exporter.Export("test.xlsx", value, configuration: config);
718698

719699
Both properties work with `null` and `DBNull` values.
720700

721-
#### 14. Freeze Panes
701+
#### 13. Freeze Panes
722702

723703
MiniExcel allows you to freeze both rows and columns in place:
724704

@@ -985,35 +965,35 @@ var value = new Dictionary<string, object>()
985965
var templater = MiniExcel.Templaters.GetOpenXmlTemplater();
986966
templater.ApplyTemplate(path, templatePath, value);
987967
```
988-
- With `@group` tag and with `@header` tag
968+
- Without `@group` tag
989969

990970
Before:
991971

992-
![before_with_header](https://user-images.githubusercontent.com/38832863/218646717-21b9d57a-2be2-4e9a-801b-ae212231d2b4.PNG)
972+
![without_group](https://user-images.githubusercontent.com/38832863/218646975-f52a68eb-e031-43b5-abaa-03b67c052d1a.PNG)
993973

994974
After:
995975

996-
![after_with_header](https://user-images.githubusercontent.com/38832863/218646721-58a7a340-7004-4bc2-af24-cffcb2c20737.PNG)
976+
![without_group_after](https://user-images.githubusercontent.com/38832863/218646974-4a3c0e07-7c66-4088-ad07-b4ad3695b7e1.PNG)
997977

998978
- With `@group` tag and without `@header` tag
999979

1000980
Before:
1001981

1002982
![before_without_header](https://user-images.githubusercontent.com/38832863/218646873-b12417fa-801b-4890-8e96-669ed3b43902.PNG)
1003983

1004-
After;
984+
After:
1005985

1006986
![after_without_header](https://user-images.githubusercontent.com/38832863/218646872-622461ba-342e-49ee-834f-b91ad9c2dac3.PNG)
1007987

1008-
- Without `@group` tag
988+
- With both `@group` and `@header` tags
1009989

1010990
Before:
1011991

1012-
![without_group](https://user-images.githubusercontent.com/38832863/218646975-f52a68eb-e031-43b5-abaa-03b67c052d1a.PNG)
992+
![before_with_header](https://user-images.githubusercontent.com/38832863/218646717-21b9d57a-2be2-4e9a-801b-ae212231d2b4.PNG)
1013993

1014994
After:
1015995

1016-
![without_group_after](https://user-images.githubusercontent.com/38832863/218646974-4a3c0e07-7c66-4088-ad07-b4ad3695b7e1.PNG)
996+
![after_with_header](https://user-images.githubusercontent.com/38832863/218646721-58a7a340-7004-4bc2-af24-cffcb2c20737.PNG)
1017997

1018998
#### 7. If/ElseIf/Else Statements inside cell
1019999

@@ -1043,7 +1023,31 @@ After:
10431023

10441024
![if_after](https://user-images.githubusercontent.com/38832863/235360609-869bb960-d63d-45ae-8d64-9e8b0d0ab658.PNG)
10451025

1046-
#### 8. DataTable as parameter
1026+
1027+
#### 8. Merge same cells vertically
1028+
1029+
This functionality merges cells vertically between the tags `@merge` and `@endmerge`.
1030+
You can use `@mergelimit` to limit boundaries of merging cells vertically.
1031+
1032+
```csharp
1033+
var templater = MiniExcel.Templaters.GetOpenXmlTemplater();
1034+
templater.MergeSameCells(mergedFilePath, templatePath);
1035+
```
1036+
1037+
File content before and after merge without merge limit:
1038+
1039+
<img width="318" alt="Screenshot 2023-08-07 at 11 59 24" src="https://github.com/mini-software/MiniExcel/assets/38832863/49cc96b9-6c35-4bf3-8d43-a9752a15b22e">
1040+
1041+
<img width="318" alt="Screenshot 2023-08-07 at 11 59 57" src="https://github.com/mini-software/MiniExcel/assets/38832863/3fbd529b-3ae6-4bbe-b4d8-2793a5a58010">
1042+
1043+
File content before and after merge with merge limit:
1044+
1045+
<img width="346" alt="Screenshot 2023-08-08 at 18 21 00" src="https://github.com/mini-software/MiniExcel/assets/38832863/04049d28-84d5-4c2a-bcff-5847547df5e1">
1046+
1047+
<img width="346" alt="Screenshot 2023-08-08 at 18 21 40" src="https://github.com/mini-software/MiniExcel/assets/38832863/f5cf8957-b0b0-4831-b8fc-8556299235c2">
1048+
1049+
1050+
#### 9. DataTable as parameter
10471051

10481052
```csharp
10491053
var managers = new DataTable();
@@ -1063,7 +1067,8 @@ var value = new Dictionary<string, object>()
10631067
var templater = MiniExcel.Templaters.GetOpenXmlTemplater();
10641068
templater.ApplyTemplate(path, templatePath, value);
10651069
```
1066-
#### 9. Formulas
1070+
1071+
#### 10. Formulas
10671072
Prefix your formula with `$` and use `$enumrowstart` and `$enumrowend` to mark references to the enumerable start and end rows:
10681073

10691074
![image](docs/images/template-formulas-1.png)
@@ -1081,7 +1086,7 @@ _Other examples_:
10811086
| Range | `$=MAX(C{{$enumrowstart}}:C{{$enumrowend}}) - MIN(C{{$enumrowstart}}:C{{$enumrowend}})` |
10821087

10831088

1084-
#### 10. Checking template parameter key
1089+
#### 11. Checking template parameter key
10851090

10861091
When a parameter key is missing it will be replaced with an empty string by default.
10871092
You can change this behaviour to throw an exception by setting the `IgnoreTemplateParameterMissing` configuration property:
@@ -1295,7 +1300,142 @@ var exporter = MiniExcel.Exporters.GetOpenXmlExporter();
12951300
exporter.Export(path, sheets, configuration: configuration);
12961301
```
12971302

1298-
### CSV <a name="docs-csv" />
1303+
1304+
### Fluent Cell Mapping <a name="docs-mapping" />
1305+
1306+
Since v2.0.0, MiniExcel supports a fluent API for precise cell-by-cell mapping, giving you complete control over Excel layout without relying on conventions or attributes.
1307+
1308+
>⚠️ **Important:** Compile mappings only once during application startup!
1309+
1310+
Mapping compilation is a one-time operation that generates optimized runtime code. Create a single `MappingRegistry` instance and configure all your mappings at startup. Reuse this registry throughout your application for optimal performance.
1311+
1312+
#### 1. Basic Property Mapping
1313+
1314+
Map properties to specific cells using the fluent configuration API:
1315+
1316+
```csharp
1317+
// Configure once at application startup
1318+
var registry = new MappingRegistry();
1319+
registry.Configure<Person>(cfg =>
1320+
{
1321+
cfg.Property(p => p.Name).ToCell("A1");
1322+
cfg.Property(p => p.Age).ToCell("B1");
1323+
cfg.Property(p => p.Email).ToCell("C1");
1324+
cfg.Property(p => p.Salary).ToCell("D1").WithFormat("#,##0.00");
1325+
cfg.Property(p => p.BirthDate).ToCell("E1").WithFormat("yyyy-MM-dd");
1326+
cfg.ToWorksheet("Employees");
1327+
});
1328+
1329+
var exporter = MiniExcel.Exporters.GetMappingExporter(registry);
1330+
await exporter.ExportAsync(stream, people);
1331+
```
1332+
1333+
#### 2. Reading with Fluent Mappings
1334+
1335+
```csharp
1336+
// Configure once at startup
1337+
var registry = new MappingRegistry();
1338+
registry.Configure<Person>(cfg =>
1339+
{
1340+
cfg.Property(p => p.Name).ToCell("A2");
1341+
cfg.Property(p => p.Age).ToCell("B2");
1342+
cfg.Property(p => p.Email).ToCell("C2");
1343+
});
1344+
1345+
// Read data using the mapping
1346+
var importer = MiniExcel.Importers.GetMappingImporter(registry);
1347+
var people = importer.Query<Person>(stream).ToList();
1348+
```
1349+
1350+
#### 3. Collection Mapping
1351+
1352+
Map collections to specific cell ranges (collections are laid out vertically by default):
1353+
1354+
```csharp
1355+
registry.Configure<Department>(cfg =>
1356+
{
1357+
cfg.Property(d => d.Name).ToCell("A1");
1358+
1359+
// Simple collections (strings, numbers, etc.) - starts at A3 and goes down
1360+
cfg.Collection(d => d.PhoneNumbers).StartAt("A3");
1361+
1362+
// Complex object collections - starts at C3 and goes down
1363+
cfg.Collection(d => d.Employees).StartAt("C3");
1364+
});
1365+
```
1366+
1367+
You can optionally add spacing between collection items:
1368+
1369+
```csharp
1370+
registry.Configure<Employee>(cfg =>
1371+
{
1372+
cfg.Property(e => e.Name).ToCell("A1");
1373+
cfg.Collection(e => e.Skills).StartAt("B1").WithSpacing(1); // 1 row spacing between items
1374+
});
1375+
```
1376+
1377+
#### 4. Formulas and Formatting
1378+
1379+
```csharp
1380+
registry.Configure<Product>(cfg =>
1381+
{
1382+
cfg.Property(p => p.Price).ToCell("B1");
1383+
cfg.Property(p => p.Stock).ToCell("C1");
1384+
1385+
// Add a formula for calculated values
1386+
cfg.Property(p => p.Price).ToCell("D1").WithFormula("=B1*C1");
1387+
1388+
// Apply custom number formatting
1389+
cfg.Property(p => p.Price).ToCell("E1").WithFormat("$#,##0.00");
1390+
});
1391+
```
1392+
1393+
#### 5. Template Support
1394+
1395+
Apply mappings to existing Excel templates:
1396+
1397+
```csharp
1398+
registry.Configure<TestEntity>(cfg =>
1399+
{
1400+
cfg.Property(x => x.Name).ToCell("A3");
1401+
cfg.Property(x => x.CreateDate).ToCell("B3");
1402+
cfg.Property(x => x.VIP).ToCell("C3");
1403+
cfg.Property(x => x.Points).ToCell("D3");
1404+
});
1405+
1406+
var data = new TestEntity
1407+
{
1408+
Name = "Jack",
1409+
CreateDate = new DateTime(2021, 01, 01),
1410+
VIP = true,
1411+
Points = 123
1412+
};
1413+
1414+
var termplater = MiniExcel.Templaters.GetMappingExporter(registry);
1415+
await termplater.ApplyTemplateAsync(outputPath, templatePath, new[] { data });
1416+
```
1417+
1418+
#### 6. Advanced: Nested Collections with Item Mapping
1419+
1420+
Configure how items within a collection should be mapped:
1421+
1422+
```csharp
1423+
registry.Configure<Company>(cfg =>
1424+
{
1425+
cfg.Property(c => c.Name).ToCell("A1");
1426+
1427+
cfg.Collection(c => c.Departments)
1428+
.StartAt("A3")
1429+
.WithItemMapping<Department>(deptCfg =>
1430+
{
1431+
deptCfg.Property(d => d.Name).ToCell("A3");
1432+
deptCfg.Collection(d => d.Employees).StartAt("B3");
1433+
});
1434+
});
1435+
```
1436+
1437+
1438+
### CSV Specifics <a name="docs-csv" />
12991439

13001440
> Unlike Excel queries, csv always maps values to `string` by default, unless you are querying to a strongly defined type.
13011441

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ If you do, make sure to also check out the [new docs](README-V2.md) and the [upg
8181

8282
- [Excel Column Name/Index/Ignore Attribute](#getstart4)
8383

84+
- [Fluent Cell Mapping](#getstart4.5)
85+
8486
- [Examples](#getstart5)
8587

8688

@@ -1105,7 +1107,6 @@ public class Dto
11051107
```
11061108

11071109

1108-
11091110
#### 5. System.ComponentModel.DisplayNameAttribute = ExcelColumnName.excelColumnNameAttribute
11101111

11111112
Since 1.24.0, system supports System.ComponentModel.DisplayNameAttribute = ExcelColumnName.excelColumnNameAttribute

benchmarks/MiniExcel.Benchmarks/BenchmarkSections/CreateExcelBenchmark.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using DocumentFormat.OpenXml.Spreadsheet;
77
using MiniExcelLib.Benchmarks.Utils;
88
using MiniExcelLib.Core;
9+
using MiniExcelLib.Core.FluentMapping;
910
using NPOI.XSSF.UserModel;
1011
using OfficeOpenXml;
1112

@@ -14,6 +15,7 @@ namespace MiniExcelLib.Benchmarks.BenchmarkSections;
1415
public class CreateExcelBenchmark : BenchmarkBase
1516
{
1617
private OpenXmlExporter _exporter;
18+
private MappingExporter _simpleMappingExporter;
1719

1820
[GlobalSetup]
1921
public void SetUp()
@@ -22,6 +24,22 @@ public void SetUp()
2224
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
2325

2426
_exporter = MiniExcel.Exporters.GetOpenXmlExporter();
27+
28+
var simpleRegistry = new MappingRegistry();
29+
simpleRegistry.Configure<DemoDto>(config =>
30+
{
31+
config.Property(x => x.Column1).ToCell("A1");
32+
config.Property(x => x.Column2).ToCell("B1");
33+
config.Property(x => x.Column3).ToCell("C1");
34+
config.Property(x => x.Column4).ToCell("D1");
35+
config.Property(x => x.Column5).ToCell("E1");
36+
config.Property(x => x.Column6).ToCell("F1");
37+
config.Property(x => x.Column7).ToCell("G1");
38+
config.Property(x => x.Column8).ToCell("H1");
39+
config.Property(x => x.Column9).ToCell("I1");
40+
config.Property(x => x.Column10).ToCell("J1");
41+
});
42+
_simpleMappingExporter = MiniExcel.Exporters.GetMappingExporter(simpleRegistry);
2543
}
2644

2745
[Benchmark(Description = "MiniExcel Create Xlsx")]
@@ -31,6 +49,14 @@ public void MiniExcelCreateTest()
3149
_exporter.Export(path.FilePath, GetValue());
3250
}
3351

52+
[Benchmark(Description = "MiniExcel Create Xlsx with Simple Mapping")]
53+
public void MiniExcelCreateWithSimpleMappingTest()
54+
{
55+
using var path = AutoDeletingPath.Create();
56+
using var stream = File.Create(path.FilePath);
57+
_simpleMappingExporter.Export(stream, GetValue());
58+
}
59+
3460
[Benchmark(Description = "ClosedXml Create Xlsx")]
3561
public void ClosedXmlCreateTest()
3662
{

0 commit comments

Comments
 (0)