Skip to content

Commit 0c4a5b0

Browse files
committed
feat(IO): 新增并实现 Magicodes.IE.IO 包(零 EPPlus 自写流式 xlsx writer/reader、模板导出、表格、低分配异步)
1 parent e7f337b commit 0c4a5b0

79 files changed

Lines changed: 16645 additions & 78 deletions

File tree

Some content is hidden

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

.github/workflows/dotnetcore.yml

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
#
99
# Architecture:
1010
# changed -> skip detection (docs-only PRs skip tests)
11-
# test -> build & test matrix
11+
# test -> build & test matrix (legacy core test project)
12+
# io-test-macos -> Magicodes.IE.IO dedicated regression suite on Apple-Silicon
13+
# (-c Debug activates the CRC32 self-check; guards #1-class
14+
# arm64 platform traps that x86-only CI would miss)
1215
# check -> alls-green gate (single required status check)
1316
#
1417
# Test runner:
@@ -271,6 +274,90 @@ jobs:
271274
if-no-files-found: ignore
272275
retention-days: 7
273276

277+
###################################################
278+
# IO PERF-REGRESSION GUARD (Apple Silicon / arm64)
279+
#
280+
# Runs the *dedicated* Magicodes.IE.IO test project
281+
# (tests/Magicodes.IE.IO.Tests) on Apple-Silicon. This is the project that
282+
# holds XlsxIoPerfRegressionTests — the round-trip + ZipArchive-reopen guard
283+
# for the ForwardOnlyZipWriter CRC32 path. The main matrix above targets the
284+
# legacy core test project only, so without this job the IO regression suite
285+
# never ran in CI and an arm64 CRC bug (#1 class) could slip through on
286+
# x86-only runners.
287+
#
288+
# Built/tested in -c Debug on purpose: ForwardOnlyZipWriter contains a
289+
# #if DEBUG CRC32 self-check ("123456789" => 0xCBF43926). A wrong arm64 CRC
290+
# throws at assembly load, failing the whole run immediately — the cheapest,
291+
# strongest platform trap. (Release runtime CRC is still exercised by the
292+
# ZipArchive reopen assertions inside the tests.)
293+
###################################################
294+
295+
io-test-macos:
296+
name: IO Tests (macOS arm64 / Debug)
297+
needs: changed
298+
if: needs.changed.outputs.should_skip != 'true'
299+
runs-on: macos-15
300+
timeout-minutes: 20
301+
env:
302+
IO_TEST_PROJECT: tests/Magicodes.IE.IO.Tests/Magicodes.IE.IO.Tests.csproj
303+
steps:
304+
- name: Checkout
305+
uses: actions/checkout@v4
306+
307+
- name: Setup .NET SDK
308+
uses: actions/setup-dotnet@v4
309+
with:
310+
dotnet-version: |
311+
8.0.x
312+
cache: true
313+
cache-dependency-path: |
314+
Magicodes.IE.sln
315+
src/**/*.csproj
316+
317+
- name: Restore
318+
run: dotnet restore ${{ env.IO_TEST_PROJECT }}
319+
320+
- name: Build
321+
run: dotnet build ${{ env.IO_TEST_PROJECT }} -c Debug --no-restore -bl
322+
323+
- name: Test
324+
shell: bash
325+
run: |
326+
set +e
327+
dotnet test ${{ env.IO_TEST_PROJECT }} \
328+
-c Debug --no-build \
329+
-f net8.0 \
330+
--blame-hang-timeout 10m \
331+
--blame-crash-dump-type full \
332+
--blame-hang-dump-type full \
333+
--logger "trx;LogFileName=results-io-macos-debug.trx" \
334+
--logger GitHubActions \
335+
--results-directory TestResults
336+
TEST_EXIT=$?
337+
set -e
338+
if [ $TEST_EXIT -ne 0 ]; then
339+
echo "::error::IO tests failed on macOS arm64 (Debug CRC self-check active)"
340+
exit 1
341+
fi
342+
343+
- name: Upload test results
344+
if: always()
345+
uses: actions/upload-artifact@v4
346+
with:
347+
name: io-test-results-macos-debug
348+
path: TestResults
349+
if-no-files-found: ignore
350+
retention-days: 7
351+
352+
- name: Upload build log
353+
if: failure()
354+
uses: actions/upload-artifact@v4
355+
with:
356+
name: io-build-log-macos-debug
357+
path: msbuild.binlog
358+
if-no-files-found: ignore
359+
retention-days: 7
360+
274361
###################################################
275362
# ALLS-GREEN GATE
276363
# Single required status check for branch protection.
@@ -280,7 +367,7 @@ jobs:
280367
check:
281368
name: CI Passed
282369
if: always()
283-
needs: [changed, test]
370+
needs: [changed, test, io-test-macos]
284371
runs-on: ubuntu-latest
285372
steps:
286373
- name: Evaluate results

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,7 @@ __pycache__/
265265
/global.json
266266
!/global.json
267267
/.omo/
268+
269+
# 前端构建产物 (typescript/ 演示的 Vite 打包输出, 勿入库)
270+
index-*.js
271+
index-*.js.map

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
<Project>
3131

3232
<PropertyGroup>
33-
<Version>2.8.5</Version>
33+
<Version>2.9.0</Version>
3434
<IncludeSymbols>true</IncludeSymbols>
3535
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
3636
<NoWarn>1701;1702;CS1591;CS1573;1591;NU1507</NoWarn>

Directory.Packages.props

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
<PropertyGroup>
33
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
44
</PropertyGroup>
5-
65
<ItemGroup>
6+
<PackageVersion Include="DocumentFormat.OpenXml" Version="3.5.1" />
77
<PackageVersion Include="DynamicExpresso.Core" Version="2.3.3" />
8+
<PackageVersion Include="LightExcel" Version="2026.6.4.1" />
89
<PackageVersion Include="System.Linq.Dynamic.Core" Version="1.7.2" />
910
<PackageVersion Include="CsvHelper" Version="27.2.1" />
1011
<PackageVersion Include="HtmlToOpenXml.dll" Version="3.1.0" />
@@ -21,27 +22,33 @@
2122
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
2223
<PackageVersion Include="Shouldly" Version="4.0.3" />
2324
<PackageVersion Include="Codeuctivity.ImageSharpCompare" Version="2.0.76" />
25+
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies.net471" Version="1.0.3" />
2426
<PackageVersion Include="coverlet.collector" Version="3.1.2" />
2527
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
2628
<PackageVersion Include="System.ComponentModel.Annotations" Version="4.7.0" />
2729
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
30+
<!-- netstandard2.0 polyfills for Magicodes.IE.IO -->
31+
<PackageVersion Include="System.Memory" Version="4.5.5" />
32+
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
33+
<PackageVersion Include="PolySharp" Version="1.15.0" />
2834
<!-- Transitive CVE overrides (Stage 2 Step 6): see Excel.NPOI.csproj for context.
2935
Top-level PackageReference in the consumer csproj forces NuGet to honor these. -->
3036
<PackageVersion Include="System.Text.Encodings.Web" Version="8.0.0" />
3137
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.3" />
3238
<PackageVersion Include="NPOI" Version="2.8.0" />
3339
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
3440
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
35-
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
41+
<PackageVersion Include="BenchmarkDotNet" Version="0.15.0" />
3642
<PackageVersion Include="MiniExcel" Version="1.29.0" />
43+
44+
<PackageVersion Include="ClosedXML" Version="0.102.3" />
3745
<PackageVersion Include="FakeItEasy" Version="7.2.0" />
3846
<PackageVersion Include="MSTest.TestAdapter" Version="2.2.7" />
3947
<PackageVersion Include="MSTest.TestFramework" Version="2.2.7" />
4048
<PackageVersion Include="System.Resources.Extensions" Version="5.0.0" />
4149
<PackageVersion Include="System.Xml.XPath.XmlDocument" Version="4.3.0" />
4250
<PackageVersion Include="CS-Script" Version="4.6.5" />
4351
</ItemGroup>
44-
4552
<ItemGroup>
4653
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="3.1.2" />
4754
<PackageVersion Include="Microsoft.Extensions.DependencyModel" Version="3.1.6" />

RELEASE.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,138 @@
11
# Release Log
22

3+
## 2.9.0
4+
**Magicodes.IE.IO 首发** — 零 EPPlus 依赖、自写流式 xlsx writer/reader、模板导出、表格/样式、低分配异步性能工作。详见下方「新增」段。
5+
6+
### 新增 — Magicodes.IE.IO (独立新包,零 EPPlus)
7+
8+
阶段二全套交付:自写 xlsx writer,零外部依赖。
9+
10+
#### Stage 2.5 — Span / 向量化 XML 转义 / Utf8Formatter
11+
- 弃 char[] + UTF8Encoding 二次编码 → 直接 pooled byte[] + UTF-8 直写
12+
- XML 转义手写 ASCII escape byte 表(`&` / `<` / `>` / `"` / `'` 5 字符)
13+
- 数字直写 `Utf8Formatter.TryFormat(double/long)`,culture-invariant
14+
- 性能(100k × 4 string):79ms → 32ms(60% ↑)
15+
16+
#### Stage 2.1 — 易用性(属性驱动 + 值类型 + 多 sheet + 列宽 + 冻结)
17+
- 自动识别 `[Display(Name)]` / `[Description]` / `[DisplayFormat]` 做表头与格式
18+
- fluent 覆盖 attribute,3 层披露:fluent > attribute > 默认
19+
- 值类型(struct / record struct)支持,取消 `where T : class` 约束
20+
- 多 sheet:`Xlsx.SaveMultiSheetAsBytes(sheet1, sheet2, ...)`
21+
- 列宽:`WithWidth(30.5)`,支持连续相同宽度合并 `<col>`
22+
- 冻结首行:`WithFreezeHeader(true)`,注入 `<sheetViews><pane ySplit="1" state="frozen"/></sheetViews>`
23+
24+
#### Stage 2.2 — TypedRowPlan<T>(class 路径零 object 装箱)
25+
- 派生 `TypedRowPlan<T> : RowPlan` 暴露 `Func<T, CellValue>[]` typed getters
26+
- `XlsxWriter.WriteRows<T>(data, TypedRowPlan<T>)` 走 typed path
27+
- 属性类型分发(string/int/long/double/decimal/datetime/bool/enum 强类型编译 lambda)
28+
29+
#### Stage 2.3 — 压缩档位可选
30+
- `XlsxWriter(..., CompressionLevel compression)` 参数
31+
- `Xlsx.SaveAsBytes(data, compression: CompressionLevel.NoCompression)` facade
32+
- NoCompression 路径 100k string 38ms → 18ms(2x 加速,产物从 1MB → 14MB)
33+
34+
#### Stage 2.4 — IAsyncEnumerable + 真异步流
35+
- `Xlsx.SaveAsAsync(path, IAsyncEnumerable<T> data, CancellationToken)`
36+
- `Xlsx.SaveAsBytesAsync(IAsyncEnumerable<T> data, CancellationToken)`
37+
- `XlsxWriter.WriteRowsAsync<T>` 内部 `await foreach + ThrowIfCancellationRequested`
38+
- 大表 + 数据库查询场景:不一次性装内存,边查边写
39+
40+
#### Stage 2.6 — CSV / JSON 共用 profile
41+
> 注:当前 2.9.0 构建已移除 CSV/JSON 实现(`Csv.cs`/`Json.cs` 不在产物中),集中做 Excel。此 stage 保留为历史记录。
42+
- `Csv` / `Json` 静态门面,同一份 `ExportProfile<T>` 可输出 xlsx/csv/json
43+
- CSV RFC 4180:含 `,` / `"` / `\r` / `\n` 自动双引号包裹 + 内部 `"``""`
44+
- JSON 用 STJ(`System.Text.Json`,BCL 内置)
45+
46+
#### Stage 2.7 — BenchmarkDotNet 横向对比(正式报告)
47+
- BenchmarkDotNet v0.14.0,macOS Sequoia 15.6.1,Apple M4,ShortRun,Release/net8.0
48+
- 数据来源:`src/Magicodes.IE.Benchmarks/XlsxIO_Benchmarks.cs` (`--filter "*XlsxIO*"`)
49+
- 结果(Mean 毫秒):
50+
51+
|| 1k string | 10k string | 10k number | 100k string |
52+
|---|---|---|---|---|
53+
| **Magicodes.IE.IO(自写)** | 0.70ms | 3.95ms | 5.79ms | 58.70ms |
54+
| MiniExcel | 3.84ms | 18.29ms | 13.65ms | 163.34ms |
55+
| ClosedXML | 11.93ms | 71.13ms |||
56+
57+
**结论**:Magicodes.IE.IO 在该组数据中明显快于 MiniExcel 和 ClosedXML。
58+
59+
#### Stage 2.8 — IExporter<T> 抽象
60+
> 注:`IExporter<T>` / `XlsxExporter<T>` 已在当前重构中移除(`IExporter.cs``Engine/Exporters.cs` 删除),API 统一到 `Xlsx` 静态门面(见 P2-1)。
61+
- `IExporter<T>` 接口 + `XlsxExporter<T>`(已实现) + `CsvExporter<T>` / `JsonExporter<T>`(stage 三 暂未实现,集中做 Excel)
62+
63+
#### Stage 3.3 — `[ExporterHeader]` attribute
64+
- IE 老用户熟悉的 `[ExporterHeader(Name = ..., IsIgnore = ..., Format = ..., Width = ..., Index = ...)]`
65+
- 优先级:fluent cfg > `[ExporterHeader]` > `[Display]` > `[Description]` > 属性名
66+
- 不再需要切到 BCL `[Display]`
67+
68+
#### Stage 3.1 — AOT 警告抑制
69+
- `[UnconditionalSuppressMessage("Trimming", "IL2026")]` + `("AOT", "IL3050")`
70+
- `Expression.Compile` 在 Native AOT 下回退到 `PropertyInfo.GetValue` 路径
71+
- 跨 TFM(net6/8/10)统一,不引入 `RequiresDynamicCode` (net7+) 以保持兼容性
72+
73+
#### P2-1 — API 统一到 `Xlsx` 静态门面(对齐 MiniExcel 风格)
74+
- `XlsxQuery` / `TemplateExporter` 静态类合并到 `Xlsx` 静态类下
75+
- `Xlsx.SheetExport` 嵌套类提升为顶层 `SheetExport`
76+
- 用户 API 现在统一是 `Xlsx.{SaveAs, SaveAsBytes, SaveAsAsync, SaveMultiSheetAsBytes, Query, QueryAsync, ExportByTemplateAsync}` 一行 IDE 提示
77+
- 删除 `TemplateExporter.cs` 文件,功能内置到 `Xlsx.cs`
78+
- `XlsxReader.cs` 只剩 `XlsxReader``XlsxImporterProfile<T>`(底层 API)
79+
- 测试 46/46 全过(三档 TFM)
80+
81+
### 阶段 3 P3 — 性能 / 内存 / GC 完整对比
82+
BenchmarkDotNet v0.14.0,Apple M4,.NET 8.0.19,MediumJob(15 迭代 × 2 启动)。
83+
84+
| 场景 || Mean | Allocated | Gen2 | ThreadPool Items | Lock |
85+
|---|---|---|---|---|---|---|
86+
| | **Magicodes.IE.IO** | 4.73 ms | 2.80 MB | **8** | 0 | 0 |
87+
| | MiniExcel | 18.65 ms | 38.76 MB | 3312 | 0 | 0 |
88+
| | ClosedXML | 66.13 ms | 86.71 MB | 1666 | 0 | 0 |
89+
| 100k × 4 string | **Magicodes.IE.IO** | **54.01 ms** | 30.5 MB | **1222** | 0 | 0 |
90+
| | MiniExcel | 161.00 ms | 245.01 MB | 2666 | 0 | 0 |
91+
| 10k × 4 number | **Magicodes.IE.IO** | **5.90 ms** | **1.43 MB** | 109 | 0 | 0 |
92+
| | MiniExcel | 14.29 ms | 34.18 MB | 3437 | 0 | 0 |
93+
94+
**亮点**:
95+
96+
- 完全无锁,纯同步写
97+
98+
#### P3-1 RowPlan 缓存(同 T + profile 复用)
99+
- `ConcurrentDictionary<(Type, profile hash), TypedRowPlan<T>>`
100+
- 同一 profile 多次 `SaveAsBytes` 反射只走一次
101+
- 测试:`RowPlanCache_Same/DifferentProfile_ReusedAcrossCalls`
102+
103+
#### P3-2 样式系统真生效(ResolveColumnStyles)
104+
-`WriteStyles``BuildStylePool`(算 xfId 回写 ColumnMeta.StyleId)+ `EmitStylePoolXml`(Dispose 时写)
105+
- facade 在 `WriteHeader` 之前调 `ResolveColumnStyles`,让表头 cell 真带 `s="N"`
106+
- 测试 verify:`Style_BoldHeader_RealXfIdOnCell` / `Style_FontColor_WritesColor` / `Style_Wrap_WritesWrapText` / `Style_FontName_WritesCustomName`
107+
108+
#### P3-3 sharedStrings reader 兼容
109+
- `XlsxReader` 构造时加载 `xl/sharedStrings.xml`
110+
- `t="s"` cell 解析时按 `<v>` 下标查 sharedStrings 数组
111+
- 真 Office 写的 xlsx 现在可读
112+
- 测试:`Read_SharedStrings_RealXlsxFormat`(手工拼 t="s" 风格 xlsx)
113+
114+
#### P3-4 字体名 + 字体颜色真生效
115+
- `ColumnConfig.WithFontName("微软雅黑")` 写进 styles.xml `<font><name val="..."/></font>`
116+
- `fontSet` 元组增 `Name` 字段,去重时按 Bold/Size/Color/Name
117+
- 测试:`Style_FontName_WritesCustomName`
118+
119+
#### ThreadPool 调度优化(性能)
120+
- `Xlsx.WriteAsync<T>` 去掉 `await Task.Yield()`
121+
- 实测 `Completed Work Items` 从 1 → 0,Gen2 31 → 7.8(4 倍下)
122+
- 同步路径不再让出 ThreadPool
123+
124+
### 新增文件
125+
- `src/Magicodes.IE.IO/Magicodes.IE.IO.csproj`
126+
- `src/Magicodes.IE.IO/XlsxWriter.cs`(自写 writer 核心)
127+
- `src/Magicodes.IE.IO/Xlsx.cs`(静态门面)
128+
- `src/Magicodes.IE.IO/ExportProfile.cs`(frozen fluent profile)
129+
- `src/Magicodes.IE.IO/ExporterHeaderAttribute.cs`
130+
- `tests/Magicodes.IE.IO.Tests/` 34 个测试(三档 TFM 全过)
131+
- `src/Magicodes.IE.Benchmarks/XlsxIO_Benchmarks.cs`
132+
133+
### 依赖
134+
- 新增:`ClosedXML` 0.102.3(仅 Benchmarks 用,生产包零依赖)
135+
3136
## 2.8.5
4137
**2026.06.21**
5138

src/Magicodes.IE.Benchmarks/LabBenchmarks.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Magicodes.Benchmarks.Models;
55
using Magicodes.ExporterAndImporter.Excel;
66
using Magicodes.ExporterAndImporter.Excel.Utility;
7+
using Magicodes.IE.IO;
78
using MiniExcelLibs;
89
using System;
910
using System.Collections.Generic;
@@ -63,10 +64,10 @@ public async Task ExportExcelAsByteArrayAsyncTest()
6364
}
6465

6566
[Benchmark]
66-
public async Task ExportMiniExcelAsyncTest()
67+
public async Task ExportMioAsyncTest()
6768
{
6869
var memoryStream = new MemoryStream();
69-
await memoryStream.SaveAsAsync(_exportTestData);
70+
Xlsx.Write(memoryStream, _exportTestData);
7071
memoryStream.Seek(0, SeekOrigin.Begin);
7172
}
7273
}

src/Magicodes.IE.Benchmarks/Magicodes.Benchmarks.csproj

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net8.0</TargetFramework>
5+
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
66
<IsPackable>false</IsPackable>
77
</PropertyGroup>
88

99
<ItemGroup>
1010
<PackageReference Include="BenchmarkDotNet" />
11+
<PackageReference Include="LightExcel" />
1112
<PackageReference Include="MiniExcel" />
13+
14+
<PackageReference Include="ClosedXML" />
1215
<PackageReference Include="Newtonsoft.Json" />
1316
</ItemGroup>
1417

@@ -17,6 +20,13 @@
1720
<ProjectReference Include="..\Magicodes.ExporterAndImporter.Csv\Magicodes.IE.Csv.csproj" />
1821
<ProjectReference Include="..\Magicodes.ExporterAndImporter.Excel\Magicodes.IE.Excel.csproj" />
1922
<ProjectReference Include="..\Magicodes.ExporterAndImporter.Pdf\Magicodes.IE.Pdf.csproj" />
23+
<ProjectReference Include="..\Magicodes.IE.IO\Magicodes.IE.IO.csproj" />
24+
<ProjectReference Include="..\EPPlus\EPPlus\Magicodes.IE.EPPlus.csproj" />
25+
<!-- 让 [XlsxExportable] 在 benchmarks 项目里真正触发源生成器。
26+
IO 项目的 XlsxGeneratedGettersRegistry 已用 InternalsVisibleTo("Magicodes.Benchmarks") 授权访问。 -->
27+
<ProjectReference Include="..\Magicodes.IE.IO.SourceGenerator\Magicodes.IE.IO.SourceGenerator.csproj"
28+
OutputItemType="Analyzer"
29+
ReferenceOutputAssembly="false" />
2030
</ItemGroup>
2131

2232
<ItemGroup>

0 commit comments

Comments
 (0)