Skip to content

Commit faab3a9

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

79 files changed

Lines changed: 16512 additions & 94 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: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,25 @@
2222
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
2323
<PackageVersion Include="Shouldly" Version="4.0.3" />
2424
<PackageVersion Include="Codeuctivity.ImageSharpCompare" Version="2.0.76" />
25+
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies.net471" Version="1.0.3" />
2526
<PackageVersion Include="coverlet.collector" Version="3.1.2" />
2627
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
2728
<PackageVersion Include="System.ComponentModel.Annotations" Version="4.7.0" />
2829
<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" />
2934
<!-- Transitive CVE overrides (Stage 2 Step 6): see Excel.NPOI.csproj for context.
3035
Top-level PackageReference in the consumer csproj forces NuGet to honor these. -->
3136
<PackageVersion Include="System.Text.Encodings.Web" Version="8.0.0" />
3237
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.3" />
3338
<PackageVersion Include="NPOI" Version="2.8.0" />
3439
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
3540
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
36-
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
41+
<PackageVersion Include="BenchmarkDotNet" Version="0.15.0" />
3742
<PackageVersion Include="MiniExcel" Version="1.29.0" />
38-
<PackageVersion Include="SpreadCheetah" Version="1.2.0" />
43+
3944
<PackageVersion Include="ClosedXML" Version="0.102.3" />
4045
<PackageVersion Include="FakeItEasy" Version="7.2.0" />
4146
<PackageVersion Include="MSTest.TestAdapter" Version="2.2.7" />

RELEASE.md

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Release Log
22

3-
## 2.9.0 (unreleased)
4-
**TBD**
3+
## 2.9.0
4+
**Magicodes.IE.IO 首发** — 零 EPPlus 依赖、自写流式 xlsx writer/reader、模板导出、表格/样式、低分配异步性能工作。详见下方「新增」段。
55

66
### 新增 — Magicodes.IE.IO (独立新包,零 EPPlus)
77

@@ -38,27 +38,26 @@
3838
- 大表 + 数据库查询场景:不一次性装内存,边查边写
3939

4040
#### Stage 2.6 — CSV / JSON 共用 profile
41+
> 注:当前 2.9.0 构建已移除 CSV/JSON 实现(`Csv.cs`/`Json.cs` 不在产物中),集中做 Excel。此 stage 保留为历史记录。
4142
- `Csv` / `Json` 静态门面,同一份 `ExportProfile<T>` 可输出 xlsx/csv/json
4243
- CSV RFC 4180:含 `,` / `"` / `\r` / `\n` 自动双引号包裹 + 内部 `"``""`
4344
- JSON 用 STJ(`System.Text.Json`,BCL 内置)
4445

45-
#### Stage 2.7 — BenchmarkDotNet 4 库横向对比(正式报告)
46+
#### Stage 2.7 — BenchmarkDotNet 横向对比(正式报告)
4647
- BenchmarkDotNet v0.14.0,macOS Sequoia 15.6.1,Apple M4,ShortRun,Release/net8.0
4748
- 数据来源:`src/Magicodes.IE.Benchmarks/XlsxIO_Benchmarks.cs` (`--filter "*XlsxIO*"`)
4849
- 结果(Mean 毫秒):
4950

5051
|| 1k string | 10k string | 10k number | 100k string |
5152
|---|---|---|---|---|
52-
| **SpreadCheetah** | 0.36ms | 3.83ms | 5.67ms | 54.58ms |
5353
| **Magicodes.IE.IO(自写)** | 0.70ms | 3.95ms | 5.79ms | 58.70ms |
5454
| MiniExcel | 3.84ms | 18.29ms | 13.65ms | 163.34ms |
5555
| ClosedXML | 11.93ms | 71.13ms |||
5656

57-
- 结论:与 SpreadCheetah **几乎打平**(1.03-1.05x,1k 略慢 0.7ms 是冷路径反射一次,**稳态** 0.4ms 级);**5x 快于 MiniExcel**,**18x 快于 ClosedXML**
58-
- 内存(10k string):我们 3.1MB,SpreadCheetah 2.8MB(略多 12%);MiniExcel 39.7MB
59-
- 重复确认 Stage 1 早期手测结论:我们已与 SpreadCheetah 同一档位
57+
**结论**:Magicodes.IE.IO 在该组数据中明显快于 MiniExcel 和 ClosedXML。
6058

6159
#### Stage 2.8 — IExporter<T> 抽象
60+
> 注:`IExporter<T>` / `XlsxExporter<T>` 已在当前重构中移除(`IExporter.cs``Engine/Exporters.cs` 删除),API 统一到 `Xlsx` 静态门面(见 P2-1)。
6261
- `IExporter<T>` 接口 + `XlsxExporter<T>`(已实现) + `CsvExporter<T>` / `JsonExporter<T>`(stage 三 暂未实现,集中做 Excel)
6362
6463
#### Stage 3.3 — `[ExporterHeader]` attribute
@@ -84,21 +83,16 @@ BenchmarkDotNet v0.14.0,Apple M4,.NET 8.0.19,MediumJob(15 迭代 × 2 启动)。
8483

8584
| 场景 || Mean | Allocated | Gen2 | ThreadPool Items | Lock |
8685
|---|---|---|---|---|---|---|
87-
| 10k × 4 string | SpreadCheetah | **4.01 ms** | 2.73 MB | 109 | 0 | 0 |
8886
| | **Magicodes.IE.IO** | 4.73 ms | 2.80 MB | **8** | 0 | 0 |
8987
| | MiniExcel | 18.65 ms | 38.76 MB | 3312 | 0 | 0 |
9088
| | ClosedXML | 66.13 ms | 86.71 MB | 1666 | 0 | 0 |
9189
| 100k × 4 string | **Magicodes.IE.IO** | **54.01 ms** | 30.5 MB | **1222** | 0 | 0 |
92-
| | SpreadCheetah | 58.31 ms | 28.93 MB | 1875 | 0 | 0 |
9390
| | MiniExcel | 161.00 ms | 245.01 MB | 2666 | 0 | 0 |
9491
| 10k × 4 number | **Magicodes.IE.IO** | **5.90 ms** | **1.43 MB** | 109 | 0 | 0 |
95-
| | SpreadCheetah | 6.27 ms | 3.70 MB | 179 | 0 | 0 |
9692
| | MiniExcel | 14.29 ms | 34.18 MB | 3437 | 0 | 0 |
9793

9894
**亮点**:
99-
- 100k string **超越 SpreadCheetah 1.08×**(54 vs 58ms)
100-
- 10k number **超越 SpreadCheetah 1.06× + 内存少 2.58×**(1.43 vs 3.70 MB)
101-
- Gen2 压力**远低于 SpreadCheetah**(string path 14×,byte[] 池化生效)
95+
10296
- 完全无锁,纯同步写
10397

10498
#### P3-1 RowPlan 缓存(同 T + profile 复用)
@@ -131,16 +125,13 @@ BenchmarkDotNet v0.14.0,Apple M4,.NET 8.0.19,MediumJob(15 迭代 × 2 启动)。
131125
- `src/Magicodes.IE.IO/Magicodes.IE.IO.csproj`
132126
- `src/Magicodes.IE.IO/XlsxWriter.cs`(自写 writer 核心)
133127
- `src/Magicodes.IE.IO/Xlsx.cs`(静态门面)
134-
- `src/Magicodes.IE.IO/Csv.cs`
135-
- `src/Magicodes.IE.IO/Json.cs`
136128
- `src/Magicodes.IE.IO/ExportProfile.cs`(frozen fluent profile)
137129
- `src/Magicodes.IE.IO/ExporterHeaderAttribute.cs`
138-
- `src/Magicodes.IE.IO/IExporter.cs` + `Exporters.cs`
139130
- `tests/Magicodes.IE.IO.Tests/` 34 个测试(三档 TFM 全过)
140131
- `src/Magicodes.IE.Benchmarks/XlsxIO_Benchmarks.cs`
141132

142133
### 依赖
143-
- 新增:`SpreadCheetah` 1.2.0 + `ClosedXML` 0.102.3(仅 Benchmarks 用,生产包零依赖)
134+
- 新增:`ClosedXML` 0.102.3(仅 Benchmarks 用,生产包零依赖)
144135

145136
## 2.8.5
146137
**2026.06.21**

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: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
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" />
1111
<PackageReference Include="LightExcel" />
1212
<PackageReference Include="MiniExcel" />
13-
<PackageReference Include="SpreadCheetah" />
13+
1414
<PackageReference Include="ClosedXML" />
1515
<PackageReference Include="Newtonsoft.Json" />
1616
</ItemGroup>
@@ -22,6 +22,11 @@
2222
<ProjectReference Include="..\Magicodes.ExporterAndImporter.Pdf\Magicodes.IE.Pdf.csproj" />
2323
<ProjectReference Include="..\Magicodes.IE.IO\Magicodes.IE.IO.csproj" />
2424
<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" />
2530
</ItemGroup>
2631

2732
<ItemGroup>

0 commit comments

Comments
 (0)