diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index dc595af9..ae439987 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -6,9 +6,9 @@ on:
workflow_dispatch:
inputs:
version:
- description: 'Version to bump to (e.g. 2.6.0). Only used by manual trigger.'
+ description: 'Version to bump to (e.g. 3.0.0). Only used by manual trigger.'
required: true
- default: '2.6.0'
+ default: '3.0.0'
permissions:
contents: write
@@ -16,7 +16,200 @@ permissions:
id-token: write
jobs:
+ # ---------------------------------------------------------------------------
+ # Quality gates. These MUST pass before any package is published or any
+ # GitHub Release is created. Every downstream publish job depends on the
+ # `quality-gate` aggregation job via `needs:`, so a failure in any gate
+ # short-circuits the whole release: no NuGet/MyGet push, no GitHub Release.
+ #
+ # The gates mirror the PR CI pipeline (.github/workflows/build-pr-ci.yml) and
+ # reuse the exact same coverage script (.github/scripts/check-coverage.sh)
+ # so thresholds stay identical and cannot drift:
+ # Unit 95% / E2E 80% / NativeAOT Unit 100% / NativeAOT E2E 95%.
+ # ---------------------------------------------------------------------------
+ lint:
+ name: Lint (Release gate)
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+ 9.0.x
+ 10.0.x
+ - name: Lint (dotnet format)
+ shell: bash
+ run: |
+ dotnet format AspectCore-Framework.sln --verify-no-changes --verbosity diagnostic
+
+ unit-coverage:
+ name: Unit Coverage (Release gate)
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+ 9.0.x
+ 10.0.x
+ - name: Collect unit test coverage
+ shell: bash
+ run: |
+ chmod +x ./.github/scripts/check-coverage.sh
+ ./.github/scripts/check-coverage.sh collect unit --output coverage-results/unit.env
+ - name: Assert unit coverage (min 95%)
+ shell: bash
+ run: |
+ ./.github/scripts/check-coverage.sh assert unit --input coverage-results/unit.env
+
+ e2e-coverage:
+ name: E2E Coverage (Release gate)
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+ 9.0.x
+ 10.0.x
+ - name: Collect E2E test coverage
+ shell: bash
+ run: |
+ chmod +x ./.github/scripts/check-coverage.sh
+ ./.github/scripts/check-coverage.sh collect e2e --output coverage-results/e2e.env
+ - name: Assert E2E coverage (min 80%)
+ shell: bash
+ run: |
+ ./.github/scripts/check-coverage.sh assert e2e --input coverage-results/e2e.env
+
+ nativeaot-coverage:
+ name: NativeAOT Coverage (Release gate)
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+ 9.0.x
+ 10.0.x
+ - name: Collect NativeAOT unit coverage
+ shell: bash
+ run: |
+ chmod +x ./.github/scripts/check-coverage.sh
+ ./.github/scripts/check-coverage.sh collect nativeaot-unit --output coverage-results/nativeaot-unit.env
+ - name: Collect NativeAOT E2E coverage
+ shell: bash
+ run: |
+ ./.github/scripts/check-coverage.sh collect nativeaot-e2e --output coverage-results/nativeaot-e2e.env
+ - name: Assert NativeAOT unit coverage (min 100%)
+ shell: bash
+ run: |
+ ./.github/scripts/check-coverage.sh assert nativeaot-unit --input coverage-results/nativeaot-unit.env
+ - name: Assert NativeAOT E2E coverage (min 95%)
+ shell: bash
+ run: |
+ ./.github/scripts/check-coverage.sh assert nativeaot-e2e --input coverage-results/nativeaot-e2e.env
+
+ nativeaot-verify:
+ name: NativeAOT Publish + Run (Release gate)
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+ 9.0.x
+ 10.0.x
+ - name: Restore dependencies
+ run: dotnet restore AspectCore-Framework.sln
+ - name: Build solution
+ run: dotnet build AspectCore-Framework.sln -c Release --no-restore
+ - name: Publish NativeAOT E2E (self-contained AOT binary)
+ run: dotnet publish tests/AspectCore.NativeAot.E2E/AspectCore.NativeAot.E2E.csproj -c Release -r linux-x64 -o ./publish-aot --no-restore
+ - name: Run NativeAOT E2E binary
+ run: ./publish-aot/AspectCore.NativeAot.E2E
+
+ codeql:
+ name: CodeQL (Release gate)
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ security-events: write
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 6.0.x
+ 8.0.x
+ 9.0.x
+ 10.0.x
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: csharp
+ - name: Build for CodeQL
+ shell: bash
+ run: |
+ for project in $(find ./src -name "*.csproj"); do
+ dotnet build --configuration Release $project
+ done
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:csharp"
+
+ quality-gate:
+ name: Quality Gate (all checks passed)
+ runs-on: ubuntu-latest
+ needs:
+ - lint
+ - unit-coverage
+ - e2e-coverage
+ - nativeaot-coverage
+ - nativeaot-verify
+ - codeql
+ steps:
+ - name: All quality gates passed
+ run: echo "All release quality gates passed. Proceeding to package and publish."
+
build-and-test:
+ # Publishing is gated: this job (and therefore NuGet/MyGet push and the
+ # GitHub Release created within it) only runs after every quality gate
+ # succeeds. If quality-gate fails, this job is skipped and nothing ships.
+ needs: quality-gate
permissions:
id-token: write
contents: write
@@ -197,8 +390,12 @@ jobs:
generate_release_notes: true
update-version:
+ # Runs only after a successful publish (build-and-test). Combined with the
+ # quality-gate chain, the version bump PR is never opened when a release
+ # was blocked by a failing gate or a failed publish.
# For tag pushes: only bump version for stable releases (not pre-releases like beta/rc/alpha)
- # For manual trigger: always run
+ # For manual trigger: always run (subject to build-and-test success)
+ needs: build-and-test
if: ${{ github.event_name == 'workflow_dispatch' || !contains(github.ref_name, '-') }}
runs-on: ubuntu-latest
steps:
diff --git a/.gitignore b/.gitignore
index 70984902..f620231d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -146,6 +146,9 @@ DocProject/Help/html
# Click-Once directory
publish/
+# NativeAOT publish output
+publish-aot/
+
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
diff --git a/build/common.props b/build/common.props
index dd08e207..cc5a5af9 100644
--- a/build/common.props
+++ b/build/common.props
@@ -4,9 +4,10 @@
Lemon
AspectCore Framework
- https://avatars1.githubusercontent.com/u/19426425?v=3&s=200
+ icon.png
+ README.md
https://github.com/dotnetcore/AspectCore-Framework
- https://github.com/dotnetcore/AspectCore-Framework/blob/dev/LICENSE
+ MIT
git
https://github.com/dotnetcore/AspectCore-Framework
false
@@ -18,4 +19,8 @@
needed for NativeAOT AOP support while remaining compatible with net6.0 as the lowest TFM -->
13.0
+
+
+
+
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
index dd34221f..a3cc33b6 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -12,6 +12,7 @@
- [安装](./getting-started/installation.md) — NuGet 包与目标框架
- [快速上手](./getting-started/quick-start.md) — 五分钟跑通第一个拦截器
- [核心概念](./getting-started/concepts.md) — 拦截器、代理、切面上下文等术语
+- [NativeAOT 上手](./getting-started/nativeaot.md) — 把使用 AspectCore 的应用发布成 NativeAOT 原生二进制
### 📖 使用指南(guide)
面向日常开发的功能说明。
@@ -26,6 +27,8 @@
- [数据校验](./guide/data-validation.md) — DataAnnotations 校验拦截
- [反射扩展](./guide/reflection-extensions.md) — AspectCore.Extensions.Reflection 高性能反射
- [常见场景](./guide/common-scenarios.md) — 日志、缓存、重试、性能监控等
+- [从 2.x 升级到 3.0](./guide/upgrade-to-3.0.md) — 升级前提、破坏性变更、升级步骤与常见问题
+- [Source Generator 诊断目录](./guide/source-generator-diagnostics.md) — ACSGxxx 诊断的含义、触发条件与修复
### 🏛 架构设计(architecture)
面向贡献者与深度使用者的设计文档。
@@ -46,11 +49,18 @@
- [贡献指南](./development/contributing.md) — 分支、提交、PR 流程
- [开发规范](./development/development-guidelines.md) — 工程结构识别、命令粒度、测试、性能、设计原则
- [Code Review 规范](./development/code-review-guidelines.md) — Review 维度、BLOCKING 问题、自查清单
+- [GA 发布流程](./development/release-process.md) — 正式版发布的步骤与门禁
### ✅ 测试(testing)
- [测试策略](./testing/testing-strategy.md) — 单元、双引擎一致性、E2E、覆盖率门槛
- [运行测试](./testing/running-tests.md) — 如何运行与筛选测试
+### 📦 发布说明(release-notes)
+每个大版本的发布记录。
+
+- [v3.0.0 变更日志](./release-notes/v3.0.0-changelog.md) — 结构化变更清单(特性 / 性能 / 修复 / 破坏性变更 / 工程)
+- [v3.0.0 发布叙事](./release-notes/v3.0.0.md) — 发布背景与研发过程的叙事版本
+
## 版本说明
本文档基于源码扫描生成,力求与代码一致。文中区分「已实现能力」与「设计/提案」,未实现的内容会显式标注。产品版本见 `build/version.props`。
diff --git a/docs/development/release-process.md b/docs/development/release-process.md
new file mode 100644
index 00000000..a7d070c5
--- /dev/null
+++ b/docs/development/release-process.md
@@ -0,0 +1,122 @@
+# 3.0.0 GA 发布流程
+
+本文说明如何把 AspectCore 从 `3.0.0-rc.1` 正式发布为 `3.0.0` GA。发布由 `.github/workflows/release.yml` 在收到 `v*` tag(或手动 `workflow_dispatch`)后自动执行:先过质量门禁,再打包、推 NuGet/MyGet、建 GitHub Release,最后回写版本号。所有命令与机制均以仓库中 `.github/workflows/release.yml`、`.github/scripts/check-coverage.sh`、`build/version.props` 的实际实现为准。
+
+> ⚠️ 发布是不可逆的对外操作(NuGet 包一旦发布无法删除,只能 unlist)。请务必先走完「发布前检查清单」,确认无误后再打 tag。
+
+---
+
+## 1. 发布机制总览
+
+release.yml 的 job 依赖链如下(前者全绿,后者才运行):
+
+```
+lint ┐
+unit-coverage ┐
+e2e-coverage ├─► quality-gate ─► build-and-test ─► update-version
+nativeaot-coverage ┤ (打包 + 推 NuGet/MyGet (回写版本号 +
+nativeaot-verify ┤ + 建 GitHub Release) 开 version bump PR)
+codeql ┘
+```
+
+关键点:
+
+- **质量门禁前置且强制**。`quality-gate` 通过 `needs:` 依赖全部 6 个门禁 job,且不使用 `if: always()`。只要任一门禁失败,`quality-gate` 会被跳过 → `build-and-test` 随之被跳过 → **不会 push NuGet/MyGet,也不会创建 GitHub Release**。
+- **门禁与 PR CI 同源**。release 门禁复用与 `build-pr-ci.yml` 完全相同的 `.github/scripts/check-coverage.sh`,覆盖率阈值硬编码在脚本内,两条流水线共用,不会漂移:
+ - 单元测试覆盖率 ≥ **95%**
+ - E2E 覆盖率 ≥ **80%**
+ - NativeAOT 单测覆盖率 ≥ **100%**
+ - NativeAOT E2E 覆盖率 ≥ **95%**
+ - 另有 lint(`dotnet format --verify-no-changes`)、CodeQL 安全分析、NativeAOT `publish` + 运行原生二进制。
+- **VersionQuality 由 GA tag 自动清空**,无需手工提交。`build/version.props` 当前为 `rc.1`。发布成功后,`update-version` job 会在**稳定版 tag**(tag 名不含 `-`)时把 `VersionQuality` 置空并开一个 version bump PR。因此**不要手动提交清空 VersionQuality 的改动**——那是 tag 触发后由流水线自动完成的。
+
+---
+
+## 2. 发布前检查清单
+
+打 tag 之前逐项确认:
+
+- [ ] **PR CI 全绿**:待发布的 master HEAD 对应的最后一个 PR,`build-pr-ci.yml` 的全部 9 个 job(lint / 双 OS build / 单测覆盖率 / E2E 覆盖率 / NativeAOT 覆盖率 / CodeQL)均通过。
+- [ ] **NativeAOT publish + run 通过**:`nativeaot-verify.yml` 在 master 上绿(publish 出的 `linux-x64` 原生二进制能正常运行退出)。
+- [ ] **覆盖率达标**:单测 ≥ 95%、E2E ≥ 80%、NativeAOT 单测 = 100%、NativeAOT E2E ≥ 95%(与门禁一致)。
+- [ ] **发布说明就绪**:`docs/release-notes/v3.0.0.md` 内容已定稿,GitHub Release 采用 `generate_release_notes: true` 自动汇总 PR,可与该文档互补。
+- [ ] **版本号正确**:`build/version.props` 为 `3 / 0 / 0`,`VersionQuality` 仍为 `rc.1`(**保持不动**,由 tag 自动清空)。
+- [ ] **本地干净**:准备打 tag 的 commit 就是要发布的 master HEAD,无未合并的关键改动。
+
+> 快速自检覆盖率(可选,在本地或 CI 上运行,与门禁同脚本):
+>
+> ```bash
+> ./.github/scripts/check-coverage.sh collect unit --output /tmp/unit.env
+> ./.github/scripts/check-coverage.sh assert unit --input /tmp/unit.env
+> ```
+
+---
+
+## 3. 执行发布(确切命令序列)
+
+确认清单全绿后,在 master HEAD 上执行:
+
+```bash
+# 1. 确认当前在 master 且是要发布的提交
+git checkout master
+git pull --ff-only origin master
+
+# 2. 打 GA tag —— 注意:tag 名不带任何 "-" 后缀
+git tag v3.0.0
+
+# 3. 推送 tag,触发 release.yml
+git push origin v3.0.0
+```
+
+`git push origin v3.0.0` 会触发 `release.yml`,自动完成:
+
+1. 跑质量门禁(lint / coverage / CodeQL / NativeAOT publish+run);全绿后
+2. `build-and-test`:按 `FULL_VERSION=3.0.0`(从 tag 去掉 `v` 前缀得到)编译、`dotnet pack`、校验包数量;
+3. 推包到 **NuGet.org**(OIDC 换取临时 API key,带重试)与 **MyGet**;
+4. 用 `softprops/action-gh-release` 创建 **GitHub Release**(附 `.nupkg`/`.snupkg`,自动生成 release notes);
+5. `update-version`:因 tag `v3.0.0` **不含 `-`**,识别为稳定版 → 把 `build/version.props` 的 `VersionQuality` 置空、版本 bump 到下一个 minor(`3.1.0`),并开一个 version bump PR 供合并。
+
+### ⚠️ tag 命名硬性要求:不能带 `-`
+
+release.yml 的 `update-version` job 用 `!contains(github.ref_name, '-')` 判断是否为稳定版:
+
+- `v3.0.0` ✅ —— 稳定版,发布后自动清空 `VersionQuality`。
+- `v3.0.0-rc.2`、`v3.0.0-preview1` ❌ —— 含 `-`,被识别为**预发布**,`update-version` 不运行,**不会清空 VersionQuality**,只做打包发布。
+
+因此 GA 发布**必须**用 `v3.0.0` 这种不含 `-` 的 tag,否则版本后缀不会被清空。
+
+---
+
+## 4. 发布后验证
+
+- 访问 GitHub Actions,确认 `Release` workflow 全部 job 成功(尤其 `quality-gate` → `build-and-test` → `update-version`)。
+- 在 [NuGet.org](https://www.nuget.org/) 搜索 `AspectCore.*`,确认 `3.0.0` 版本已上架(NuGet 索引可能有几分钟延迟)。
+- 确认 GitHub Releases 页面出现 `v3.0.0`。
+- Review 并合并 `update-version` 自动开的 version bump PR(把仓库基线推进到下一个开发版本)。
+
+---
+
+## 5. 回滚 / 应急
+
+发布是对外不可逆操作,出问题时按以下顺序处理:
+
+- **门禁未过 → 发布被自动拦截**:这是预期行为,不会有包被推出。修复问题后重新走流程即可。若同名 tag 已存在但发布被拦截,可先删本地/远端 tag 再重打:
+ ```bash
+ git tag -d v3.0.0
+ git push origin :refs/tags/v3.0.0 # 删除远端 tag
+ ```
+ > 删除远端 tag 属于对外动作,确认无正在进行的发布后再执行。
+- **包已推到 NuGet 但发现问题**:NuGet.org 的包**不能删除**,只能 **unlist**(隐藏,不影响已依赖它的用户),然后发布修订版本 `3.0.1`。unlist 在 NuGet.org 包管理页操作。
+- **GitHub Release 有误**:可在 Releases 页面编辑或删除该 Release(不影响已发布的 NuGet 包)。
+- **紧急修复走 patch 版本**:按 `build/version.props` 注释说明,patch 版本(如 `3.0.1`)需手动更新 `build/version.props`,再打 `v3.0.1` tag 走同一发布流程。
+
+---
+
+## 相关文件
+
+- 发布流水线:`.github/workflows/release.yml`
+- 覆盖率门禁脚本(阈值来源):`.github/scripts/check-coverage.sh`
+- PR CI(门禁同源参照):`.github/workflows/build-pr-ci.yml`
+- NativeAOT 验证:`.github/workflows/nativeaot-verify.yml`
+- 版本定义:`build/version.props`
+- 发布说明:`docs/release-notes/v3.0.0.md`
diff --git a/docs/getting-started/nativeaot.md b/docs/getting-started/nativeaot.md
new file mode 100644
index 00000000..dc4879d5
--- /dev/null
+++ b/docs/getting-started/nativeaot.md
@@ -0,0 +1,257 @@
+# NativeAOT 上手指南
+
+本页带你把一个使用 AspectCore 的应用发布成 **NativeAOT 原生二进制**,从空项目一路走到可运行。NativeAOT 下 AspectCore 的能力有明确边界,配置也和普通运行时不同,因此请先读完「支持边界」再动手,避免走上默认路径后在发布或运行阶段才发现不兼容。
+
+> 本页面向使用者。如果你想理解 NativeAOT 支持背后的引擎改造与设计取舍,见[NativeAOT AOP 设计方案](../architecture/nativeaot-design.md)(面向贡献者)。
+
+## 支持边界(先读这一节)
+
+AspectCore 有两套生成代理的引擎,它们对 NativeAOT 的支持完全不同:
+
+- **DynamicProxy(默认引擎)在 NativeAOT 下不可用。** 它在运行时用 `Reflection.Emit` / `DynamicMethod` 生成代理,而这类动态代码生成正是 NativeAOT 所禁止的,发布后会在运行阶段失败。
+- **NativeAOT 支持仅覆盖 Source Generator 路径。** Source Generator 在编译期生成代理类型与调度委托,不依赖运行时 Emit,因此可以随 NativeAOT 一起发布并运行。
+
+引擎与 NativeAOT 的兼容关系:
+
+| 引擎 | NativeAOT 兼容 | 是否默认 | 说明 |
+|------|----------------|----------|------|
+| DynamicProxy | 否 | 是 | 运行时织入,功能最完整;依赖 Emit,AOT 下不可用。 |
+| SourceGenerator | 是 | 否 | 编译期生成代理,需显式 opt-in。 |
+| Auto | 条件兼容 | 否 | 优先用 SG,缺生成物时回退 DynamicProxy;在 NativeAOT 下该回退不可用,会抛异常。 |
+
+**结论:要用 NativeAOT,必须显式把引擎切换到 Source Generator。** 保持默认(DynamicProxy)或依赖 `Auto` 的运行时回退,都会在 AOT 环境下失败。下面的步骤会完成这一切换。
+
+## 前置条件
+
+- **.NET 7 及以上**(NativeAOT 的最低要求)。本页示例统一用 `net9.0`,与仓库内的官方 E2E 工程一致。
+- 已安装 NativeAOT 工具链所需的本机构建环境(编译器/链接器),具体见 [.NET NativeAOT 官方部署文档](https://learn.microsoft.com/dotnet/core/deploying/native-aot/)。
+- 了解 AspectCore 的基本用法(拦截器、代理、容器接管)。如果还不熟悉,先看[快速上手](./quick-start.md)。
+
+## 从零到可运行
+
+下面以一个控制台程序为例,给出一套可直接复制的完整配置。
+
+### 1. 目标框架
+
+在 `.csproj` 中把目标框架设为 .NET 7 及以上,示例用 `net9.0`:
+
+```xml
+net9.0
+```
+
+### 2. 开启 NativeAOT 相关的项目属性
+
+在 `.csproj` 的 `` 中加入以下属性(取自官方 E2E 工程 `tests/AspectCore.NativeAot.E2E/AspectCore.NativeAot.E2E.csproj`):
+
+```xml
+
+ Exe
+ net9.0
+ enable
+
+
+ true
+
+ true
+
+
+ true
+
+ false
+
+```
+
+- `PublishAot` 和 `IsAotCompatible` 是必需项。
+- `SuppressTrimAnalysisWarnings` 与 `IlcTrimMetadata` 是「视需要」的配套项:Source Generator 路径仍保留了少量基于标准反射的解析(如代理构造函数查找),关闭元数据裁剪能避免运行期找不到构造函数;若你的场景不涉及这类反射解析,可按需收紧。
+
+### 3. 引用运行时包与 Source Generator
+
+运行时依赖照常安装(与[安装](./installation.md)一致):
+
+```bash
+dotnet add package AspectCore.Extensions.DependencyInjection
+```
+
+Source Generator 需要以 **analyzer** 方式引用,不能当普通库引用。手动在 `.csproj` 中加入:
+
+```xml
+
+
+
+```
+
+`OutputItemType="Analyzer"` 让编译器在编译期加载它生成代理代码,`ReferenceOutputAssembly="false"` 表示不把它作为运行时程序集引用。如果你在本仓库内直接引用源码,则改用等价的 `ProjectReference`(E2E 工程即如此):
+
+```xml
+
+```
+
+### 4. 切换到 Source Generator 引擎
+
+在注册服务时,除了照常调用 `ConfigureDynamicProxy()`,再调用 `ConfigureDynamicProxyEngine(...)` 把引擎切到 Source Generator:
+
+```csharp
+using AspectCore.DynamicProxy;
+
+services.ConfigureDynamicProxy();
+services.ConfigureDynamicProxyEngine(o =>
+{
+ o.Engine = ProxyEngine.SourceGenerator;
+ o.Strict = true; // 可选:严格模式下,遇到无法 AOT 的路径直接抛异常,而不是回退反射
+});
+```
+
+`ProxyEngineOptions`(`src/AspectCore.Abstractions/DynamicProxy/ProxyEngineOptions.cs`)上有三个开关:
+
+| 选项 | 含义 |
+|------|------|
+| `Engine` | 引擎选择:`DynamicProxy`(默认)/ `SourceGenerator` / `Auto`。NativeAOT 必须设为 `SourceGenerator`。 |
+| `AllowRuntimeFallback` | 缺失生成物时是否回退到运行时 DynamicProxy。`Engine=Auto` 时默认 `true`,`Engine=SourceGenerator` 时默认 `false`。NativeAOT 下回退不可用,应保持关闭。 |
+| `Strict` | 为 `true` 时缺失生成物直接抛异常,适合用来在 CI/开发期强约束覆盖率,尽早暴露不兼容路径。 |
+
+`Strict` 与 `AllowRuntimeFallback` 的取舍见下文[已知限制与诊断](#已知限制与诊断)。
+
+### 5. 手动注册 Source Generator 代理注册表
+
+NativeAOT 开启裁剪后,靠程序集扫描自动发现生成的代理**可能不可靠**。为确保原生二进制能稳定找到代理,用工厂方式手动注册代理注册表(取自 E2E 工程 `Program.cs`):
+
+```csharp
+services.AddSingleton(
+ _ => new AspectCore.SourceGenerated.AspectCoreSourceGeneratedProxyRegistry());
+```
+
+`AspectCoreSourceGeneratedProxyRegistry` 由 Source Generator 在编译期生成,用工厂注册可以绕开 AOT 下的 DI 构造函数解析问题。这一步在 NativeAOT 场景下是**推荐做法**;在非 AOT 环境下可省略。
+
+把第 4、5 步合到一起,一个最小可运行的控制台入口如下:
+
+```csharp
+using AspectCore.DynamicProxy;
+using AspectCore.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
+
+var services = new ServiceCollection();
+services.AddTransient();
+
+services.ConfigureDynamicProxy();
+services.ConfigureDynamicProxyEngine(o =>
+{
+ o.Engine = ProxyEngine.SourceGenerator;
+ o.Strict = true;
+});
+services.AddSingleton(
+ _ => new AspectCore.SourceGenerated.AspectCoreSourceGeneratedProxyRegistry());
+
+var provider = services.BuildDynamicProxyProvider();
+provider.GetRequiredService().Call();
+```
+
+拦截器与服务的写法和普通场景完全一样(见[快速上手](./quick-start.md)),Source Generator 会在编译期为它们生成代理。
+
+### 6. (按需)用 rd.xml 保留反射元数据
+
+如果你的代码涉及需要运行期反射的场景(例如动态类型、部分泛型路径),可以用 `rd.xml` 显式声明要保留元数据的程序集,避免被裁剪。参考 E2E 工程的 `tests/AspectCore.NativeAot.E2E/rd.xml`:
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+在 `.csproj` 中通过 `RdXmlFile` 引入:
+
+```xml
+
+
+
+```
+
+`Dynamic="Required All"` 会保留对应程序集的完整反射元数据。把 `YourApp` 换成你自己的程序集名。如果你的场景不涉及运行期反射,这一步可以跳过。
+
+### 7. 发布并运行
+
+用 `dotnet publish` 发布成对应平台(RID)的原生二进制,然后直接运行验证:
+
+```bash
+dotnet publish -c Release -r linux-x64
+```
+
+把 `linux-x64` 换成你的目标 RID(如 `win-x64`、`osx-arm64`)。发布产物是一个自包含的原生可执行文件,不依赖 .NET 运行时,直接运行即可看到拦截器生效。
+
+## 已知限制与诊断
+
+即使切到 Source Generator,NativeAOT 下仍有一些编译期无法完全静态化的签名。Source Generator 会在编译期发出 `ACSG` 系列诊断提示这些情况。
+
+### 开放泛型方法可能回退反射
+
+对开放泛型方法(如 `T Process(T input)`),Source Generator 无法为所有可能的 `T` 预生成强类型委托,只能覆盖它在编译期能发现的具体类型实参。对未覆盖到的类型:
+
+- `Strict = false`:回退到 `MethodInfo.Invoke()`(标准反射,NativeAOT 兼容但更慢,且依赖元数据被保留)。
+- `Strict = true`:直接抛 `InvalidOperationException`,提示你补充类型提示。
+
+Source Generator 会对这类方法发出诊断 **ACSG0101**。要让某个开放泛型方法获得完整的 AOT 委托覆盖,用 `[AspectCoreGenericHint]` 在方法上声明已知的闭合类型(`src/AspectCore.Abstractions/DynamicProxy/AspectCoreGenericHintAttribute.cs`):
+
+```csharp
+[AspectCoreGenericHint(typeof(int), typeof(string))]
+T Process(T input);
+```
+
+上面告诉 Source Generator 为 `Process` 和 `Process` 生成强类型委托。该特性可在同一方法上多次标注,每次提供一组类型实参。
+
+### ref struct / byref-like 参数与返回值不支持代理
+
+代理管道以 `object[]` 传参、以 `object` 承载返回值,因此 `ref struct` 等 byref-like 类型无法参与代理。Source Generator 会对这类签名在编译期报诊断,避免生成后在运行期失败:
+
+| 诊断码 | 场景 |
+|--------|------|
+| `ACSG008` | 代理目标本身是 byref-like 类型。 |
+| `ACSG009` | byref-like 的 `params` 参数。 |
+| `ACSG010` | 非 `params` 的 byref-like 参数。 |
+| `ACSG011` | byref-like 返回值。 |
+
+遇到这些诊断时,通常需要调整签名,或用 `[NonAspect]` 显式排除该成员(见[核心概念](./concepts.md#nonaspect))。
+
+> `ACSG` 系列诊断的完整含义、触发条件与修复建议,见 [Source Generator 诊断目录](../guide/source-generator-diagnostics.md)。
+
+### Strict 模式 vs 运行时回退的取舍
+
+- 想要**尽早在编译/CI 阶段暴露所有不兼容路径**:设 `Strict = true`,任何缺失生成物或命中反射回退的地方都会直接失败,不会悄悄退化。推荐在 NativeAOT 目标工程和 CI 中开启。
+- 想要**在非 AOT 环境保留兼容性、AOT 环境按需收紧**:可用 `Auto` + `AllowRuntimeFallback`,但要清楚在 NativeAOT 下运行时回退到 DynamicProxy 是不可用的——回退一旦被触发就会抛异常。因此 NativeAOT 目标工程应保持 `SourceGenerator` + 不回退。
+
+## 验证与参考
+
+### 官方 E2E 示例工程
+
+仓库内的 `tests/AspectCore.NativeAot.E2E/` 是一个最小可发布、可运行的完整示例,建议直接参考:
+
+- `AspectCore.NativeAot.E2E.csproj` — 本页所有 `.csproj` 配置的来源。
+- `Program.cs` — 引擎切换、手动注册代理注册表,以及覆盖同步/异步/`ValueTask`/`IAsyncEnumerable`/`ref`·`out` 参数/多拦截器堆叠/keyed 服务/接口与类代理等场景的断言。
+- `rd.xml` — 反射元数据保留声明。
+
+### 在本地照做验证
+
+仓库通过 `.github/workflows/nativeaot-verify.yml` 做**双重验证**:先 `dotnet publish` 发布原生二进制,再直接运行该二进制并以退出码判定成败。你可以在本地照同样的方式验证自己的工程:
+
+```bash
+# 1. 发布原生二进制
+dotnet publish path/to/YourApp.csproj -c Release -r linux-x64 -o ./publish-aot
+
+# 2. 直接运行原生二进制
+./publish-aot/YourApp
+```
+
+发布成功且运行时拦截器行为符合预期,即说明你的 NativeAOT 配置可用。
+
+## 下一步
+
+- [Source Generator 编译时引擎](../architecture/source-generator.md) — 编译期代理的生成机制。
+- [两套引擎对比与选型](../architecture/engine-comparison.md) — DynamicProxy / SourceGenerator / Auto 的整体取舍。
+- [NativeAOT AOP 设计方案](../architecture/nativeaot-design.md) — NativeAOT 支持的设计背景与实现细节(面向贡献者)。
diff --git a/docs/guide/source-generator-diagnostics.md b/docs/guide/source-generator-diagnostics.md
new file mode 100644
index 00000000..e598292e
--- /dev/null
+++ b/docs/guide/source-generator-diagnostics.md
@@ -0,0 +1,277 @@
+# Source Generator 诊断目录(ACSGxxx)
+
+AspectCore 的 [Source Generator 编译时引擎](../architecture/source-generator.md) 在编译期为标注了 `[AspectCoreGenerateProxy]` 的类型生成代理源码。当目标类型或其成员触及生成器无法支持的形态时,生成器会报告一条诊断,编译器输出中以 `ACSGxxx` 编号呈现。
+
+- **诊断类别**:所有诊断的 `category` 均为 `AspectCore.SourceGenerator`,默认启用。
+- **编号规则**:`ACSG001`–`ACSG011` 为通用生成限制;`ACSG0101` 为 NativeAOT 专项诊断。
+- **两类严重级别**:
+ - **Error**:直接阻断该类型的代理生成,编译报错。纯 NativeAOT / 裁剪场景下没有可用的编译期代理,因此这些错误会阻断该类型的 AOP 能力。
+ - **Warning**:跳过该类型或成员的代理生成(不阻断编译),或提示运行时会发生降级(如反射回退)。
+- **本文档诊断文案取自源码** `src/AspectCore.SourceGenerator/Emit/GeneratorDiagnostics.cs`,消息模板与源码逐字一致;标题为便于阅读省略了部分诊断的 `AspectCore SourceGenerator ` 前缀。消息模板中的 `{0}`、`{1}` 是运行时填入的类型名 / 成员名占位符。
+
+## 总览
+
+| ID | 标题 | 级别 | 触发形态 | 状态 | NativeAOT 影响 |
+|----|------|------|----------|------|----------------|
+| [ACSG001](#acsg001) | 暂不支持开放泛型类型 | Warning | 开放泛型类型 | **保留(当前未发出)** | 间接 |
+| [ACSG002](#acsg002) | 暂不支持嵌套类型 | Warning | 嵌套类型 | 生效 | 间接 |
+| [ACSG003](#acsg003) | 暂不支持事件成员 | Warning | 含 `event` 成员 | 生效 | 间接 |
+| [ACSG004](#acsg004) | 暂不支持开放泛型方法 | Warning | 开放泛型方法 | **保留(当前未发出)** | 间接 |
+| [ACSG005](#acsg005) | 无法为 sealed 类型生成代理 | **Error** | `sealed` 类 | 生效 | 是(阻断) |
+| [ACSG006](#acsg006) | 类型对 Source Generator 不可见 | **Error** | 非 public/internal | 生效 | 是(阻断) |
+| [ACSG007](#acsg007) | 类型没有可访问的构造函数 | **Error** | 缺可访问构造函数 | 生效 | 是(阻断) |
+| [ACSG008](#acsg008) | 无法为 ref struct 类型生成代理 | **Error** | `ref struct` | 生效 | 是(阻断) |
+| [ACSG009](#acsg009) | 暂不支持 byref-like params 参数 | Warning | `params` 的 byref-like 参数 | 生效 | 是 |
+| [ACSG010](#acsg010) | 暂不支持 byref-like 参数 | Warning | byref-like 参数 | 生效 | 是 |
+| [ACSG011](#acsg011) | 暂不支持 byref-like 返回值 | Warning | byref-like 返回值 | 生效 | 是 |
+| [ACSG0101](#acsg0101) | 开放泛型方法在 NativeAOT 下回退反射 | Warning | 无提示的开放泛型方法 | 生效 | 是(直接) |
+
+> **关于"保留(当前未发出)"**:`ACSG001` 与 `ACSG004` 在 `GeneratorDiagnostics.cs` 中有描述符与工厂方法,但全仓没有任何 `ReportDiagnostic` 调用点。当前版本**开放泛型类型与开放泛型方法均已受支持**(见 `AspectCoreProxyGenerator.cs:190` 与 `ProxyEmitter.cs` 的 "Generic types/methods are supported" 注释,代理会转发泛型参数)。开放泛型方法在 NativeAOT 下的降级由 [`ACSG0101`](#acsg0101) 负责提示。因此这两条诊断当前不会触发,仅作保留。
+
+---
+
+## ACSG001
+
+**暂不支持开放泛型类型** · Warning · **保留(当前未发出)**
+
+- **消息模板**:`类型 '{0}' 为开放泛型,当前版本的 Source Generator 暂不支持生成代理。`
+- **状态说明**:保留项。当前版本的开放泛型类型**受支持**——生成器会把类型参数转发给代理类型(`AspectCoreProxyGenerator.cs:190` 注释 "Generic types are supported")。该描述符已定义但没有发出点,编译时不会出现此诊断。
+- **示例**:以下开放泛型类型可正常生成代理,**不会**触发本诊断:
+
+ ```csharp
+ [AspectCoreGenerateProxy]
+ public class Repository { public virtual T Get(int id) => default!; }
+ ```
+
+- **修复**:无需处理。若在旧版本或历史构建中遇到,升级到支持泛型的版本即可。
+- **NativeAOT 影响**:间接。作为保留项当前不影响 AOT。
+
+## ACSG002
+
+**暂不支持嵌套类型** · Warning
+
+- **消息模板**:`类型 '{0}' 为嵌套类型,当前版本的 Source Generator 暂不支持生成代理。`
+- **说明**:目标类型定义在另一个类型内部(`type.ContainingType is not null`)。生成器检测到后跳过该类型(`AspectCoreProxyGenerator.cs:195`,随后 `continue`)。
+- **示例**:
+
+ ```csharp
+ public class Outer
+ {
+ [AspectCoreGenerateProxy]
+ public class Inner { public virtual void Do() { } } // 触发 ACSG002
+ }
+ ```
+
+- **修复**:将需要代理的类型提升为顶层(命名空间级)类型。
+- **NativeAOT 影响**:间接。该类型不会生成编译期代理,纯 AOT 场景下将缺少可用代理。
+
+## ACSG003
+
+**暂不支持事件成员** · Warning
+
+- **消息模板**:`类型 '{0}' 包含事件成员 '{1}',当前版本的 Source Generator 暂不支持生成代理。`
+- **说明**:目标类型(或其继承的接口)声明了 `event` 成员。接口路径检查全部继承接口的事件(`ProxyEmitter.cs:25`),类路径检查自身事件(`ProxyEmitter.cs:122`),命中即中止该类型代理生成(`return null`)。
+- **示例**:
+
+ ```csharp
+ [AspectCoreGenerateProxy(typeof(NotifierImpl))]
+ public interface INotifier
+ {
+ event EventHandler Changed; // 触发 ACSG003
+ void Notify();
+ }
+ ```
+
+- **修复**:从需要代理的类型中移除事件成员,或将事件拆分到不参与代理的类型上。
+- **NativeAOT 影响**:间接。该类型不生成编译期代理。
+
+## ACSG004
+
+**暂不支持开放泛型方法** · Warning · **保留(当前未发出)**
+
+- **消息模板**:`类型 '{0}' 包含开放泛型方法 '{1}',当前版本的 Source Generator 暂不支持生成代理。`
+- **状态说明**:保留项。当前版本的**开放泛型方法受支持**——代理方法会保留泛型元数(generic arity)并在调用时 `MakeGenericMethod`(`ProxyEmitter.cs` 注释 "Generic methods are supported")。该描述符已定义但没有发出点,编译时不会出现此诊断。开放泛型方法在 NativeAOT 下的委托降级改由 [`ACSG0101`](#acsg0101) 提示。
+- **示例**:以下开放泛型方法可正常生成代理,**不会**触发本诊断(但在 NativeAOT 下可能触发 `ACSG0101`):
+
+ ```csharp
+ [AspectCoreGenerateProxy(typeof(ConverterImpl))]
+ public interface IConverter { T Process(T input); }
+ ```
+
+- **修复**:无需处理。若关注 NativeAOT 下的反射回退,参见 [`ACSG0101`](#acsg0101)。
+- **NativeAOT 影响**:间接。作为保留项当前不影响 AOT;实际的 AOT 提示见 `ACSG0101`。
+
+## ACSG005
+
+**无法为 sealed 类型生成代理** · **Error**
+
+- **消息模板**:`无法为 sealed 类型 '{0}' 生成代理。请移除 sealed 修饰符或使用接口代理。`
+- **说明**:类代理通过继承目标类实现,`sealed` 类无法被继承。生成器对非抽象的 `sealed class` 报错并跳过(`AspectCoreProxyGenerator.cs:202`)。
+- **示例**:
+
+ ```csharp
+ [AspectCoreGenerateProxy]
+ public sealed class OrderService // 触发 ACSG005
+ {
+ public virtual void Submit() { }
+ }
+ ```
+
+- **修复**:移除 `sealed` 修饰符;或抽出接口并改用接口代理(`[AspectCoreGenerateProxy(typeof(OrderService))]` 标注在接口上)。
+- **NativeAOT 影响**:是(阻断)。该类型不会生成任何编译期代理,纯 AOT 场景下无法对其进行 AOP。
+
+## ACSG006
+
+**类型对 Source Generator 不可见** · **Error**
+
+- **消息模板**:`类型 '{0}' 对 Source Generator 不可见。请确保类型具有 public 或 internal 可访问性。`
+- **说明**:目标类型或其实现类型的可访问性不足,生成的代理代码无法引用它。生成器对目标类型(`AspectCoreProxyGenerator.cs:216`)与 `[AspectCoreGenerateProxy(typeof(Impl))]` 指定的实现类型(`:264`)分别校验可见性。
+- **示例**:
+
+ ```csharp
+ public interface IFoo { void Run(); }
+
+ // 实现类型可访问性低于 public/internal,代理无法引用它
+ [AspectCoreGenerateProxy(typeof(FooImpl))] // FooImpl 不可见时触发 ACSG006
+ public class FooProxyMarker : IFoo { public virtual void Run() { } }
+ ```
+
+- **修复**:将目标类型与实现类型的可访问性提升到 `public` 或 `internal`。
+- **NativeAOT 影响**:是(阻断)。该类型不生成编译期代理。
+
+## ACSG007
+
+**类型没有可访问的构造函数** · **Error**
+
+- **消息模板**:`类型 '{0}' 没有可访问的构造函数。类代理要求目标类型具有 public 或 protected 构造函数。`
+- **说明**:类代理需要转发目标类的构造器,若目标类只有 `private` 构造函数则无法继承调用。该检查仅在类代理路径进行(`AspectCoreProxyGenerator.cs:277`)。
+- **示例**:
+
+ ```csharp
+ [AspectCoreGenerateProxy]
+ public class Cache
+ {
+ private Cache() { } // 只有私有构造函数 → 触发 ACSG007
+ public virtual object? Get(string key) => null;
+ }
+ ```
+
+- **修复**:为目标类型添加 `public` 或 `protected` 构造函数。
+- **NativeAOT 影响**:是(阻断)。该类型不生成编译期代理。
+
+## ACSG008
+
+**无法为 ref struct 类型生成代理** · **Error**
+
+- **消息模板**:`无法为 ref struct 类型 '{0}' 生成代理。ref struct(如 Span、ReadOnlySpan)不能装箱、不能实现接口、不能作为类字段,因此无法进行 AOP 代理。`
+- **说明**:`ref struct` 存在生命周期约束,不能装箱、不能实现接口、不能作为类字段,无法承载 AOP 代理结构。生成器检测到 `type.IsRefLikeType` 后报错并跳过(`AspectCoreProxyGenerator.cs:209`)。
+- **示例**:
+
+ ```csharp
+ [AspectCoreGenerateProxy]
+ public ref struct SpanHolder // 触发 ACSG008
+ {
+ public void Use() { }
+ }
+ ```
+
+- **修复**:不要对 `ref struct` 应用 AOP。将需要拦截的逻辑迁移到普通类 / 接口上。
+- **NativeAOT 影响**:是。属于 NativeAOT 相关限制,该类型无法生成代理。
+
+## ACSG009
+
+**暂不支持 byref-like params 参数** · Warning
+
+- **消息模板**:`成员 '{0}' 包含 byref-like params 参数 '{1}',当前版本的 Source Generator 暂不支持生成代理。`
+- **说明**:成员含一个 `params` 且类型为 byref-like 的参数(如 C# 13 的 `params ReadOnlySpan`)。由 `NativeAotSignatureDiagnosticRules.Analyze` 识别、经 `ProxyEmitter.cs` 分发(`TryReportUnsupportedByRefLikeMembers`),命中则跳过该类型代理生成。
+- **示例**:
+
+ ```csharp
+ [AspectCoreGenerateProxy(typeof(WriterImpl))]
+ public interface IWriter
+ {
+ void Write(params ReadOnlySpan data); // 触发 ACSG009
+ }
+ ```
+
+- **修复**:改用普通数组 `params`(如 `params byte[]`)或非 byref-like 的集合参数。
+- **NativeAOT 影响**:是。byref-like 类型无法进入 AspectCore 的 `object[]` 参数管道;属 NativeAOT 相关限制。
+
+## ACSG010
+
+**暂不支持 byref-like 参数** · Warning
+
+- **消息模板**:`成员 '{0}' 包含 byref-like 参数 '{1}',当前版本的 Source Generator 暂不支持生成代理。byref-like 类型(如 Span、ReadOnlySpan)无法进入 AspectCore 的 object[] 参数管道。`
+- **说明**:成员含一个 byref-like 类型的普通参数。AspectCore 的拦截管道把参数装箱进 `object[]`,而 byref-like 类型不能装箱。识别与分发路径同 `ACSG009`。
+- **示例**:
+
+ ```csharp
+ [AspectCoreGenerateProxy(typeof(ParserImpl))]
+ public interface IParser
+ {
+ int Parse(ReadOnlySpan text); // 触发 ACSG010
+ }
+ ```
+
+- **修复**:将 byref-like 参数替换为可装箱的类型(如 `string`、`byte[]`、`Memory`)。
+- **NativeAOT 影响**:是。属 NativeAOT 相关限制,该类型不生成代理。
+
+## ACSG011
+
+**暂不支持 byref-like 返回值** · Warning
+
+- **消息模板**:`成员 '{0}' 返回 byref-like 类型 '{1}',当前版本的 Source Generator 暂不支持生成代理。byref-like 类型(如 Span、ReadOnlySpan)无法进入 AspectCore 的 object ReturnValue 管道。`
+- **说明**:成员返回 byref-like 类型。拦截管道以 `object ReturnValue` 承载返回值,byref-like 类型不能装箱。返回值检查先于参数检查执行(`NativeAotSignatureDiagnostic.cs:36`)。
+- **示例**:
+
+ ```csharp
+ [AspectCoreGenerateProxy(typeof(BufferImpl))]
+ public interface IBuffer
+ {
+ Span Rent(int size); // 触发 ACSG011
+ }
+ ```
+
+- **修复**:将返回类型替换为可装箱的类型(如 `int[]`、`Memory`)。
+- **NativeAOT 影响**:是。属 NativeAOT 相关限制,该类型不生成代理。
+
+## ACSG0101
+
+**开放泛型方法在 NativeAOT 下回退反射** · Warning · 直接 NativeAOT 诊断
+
+- **标题(源码原文,英文)**:`Open generic method falls back to reflection for NativeAOT`
+- **消息模板(源码原文,英文)**:`Method '{0}.{1}' is an open generic. The NativeAOT delegate falls back to reflection for unclosed type parameters. Add [AspectCoreGenericHint] to specify concrete type arguments for full NativeAOT safety.`
+- **说明**:开放泛型方法**可以**正常生成代理,但为其生成的 NativeAOT 委托对未闭合的类型参数会回退到反射调用。生成器对每个 `IsGenericMethod` 的方法发出此提示(接口路径 `ProxyEmitter.cs:452`,类路径 `ProxyEmitter.cs:617`)。**这是提示而非阻断**,代理仍会生成。
+- **示例**:
+
+ ```csharp
+ [AspectCoreGenerateProxy(typeof(ConverterImpl))]
+ public interface IConverter
+ {
+ T Process(T input); // 未加提示 → 触发 ACSG0101(代理仍生成)
+ }
+ ```
+
+- **修复**:在方法上添加 `[AspectCoreGenericHint]`(命名空间 `AspectCore.DynamicProxy`),为常用的类型参数组合声明具体类型,生成器会为这些组合产出完全类型化的委托,消除反射回退:
+
+ ```csharp
+ using AspectCore.DynamicProxy;
+
+ public interface IConverter
+ {
+ [AspectCoreGenericHint(typeof(int), typeof(string))]
+ T Process(T input);
+ }
+ ```
+
+ 该特性 `AllowMultiple = true`,可叠加多组类型参数;每组生成一个类型化委托(如上例生成 `Process` 与 `Process` 的委托)。
+
+- **NativeAOT 影响**:是(直接)。这是唯一一条直接针对 NativeAOT 安全性的诊断。不处理时,未提示的类型参数在 AOT 运行时走反射,可能与裁剪 / AOT 约束冲突。
+
+---
+
+## 相关文档
+
+- [Source Generator 编译时引擎](../architecture/source-generator.md) — 触发方式、增量生成流程、候选过滤
+- [两套引擎对比与选型](../architecture/engine-comparison.md) — DynamicProxy vs SourceGenerator vs Auto
+- [C# 语言特性适配](../architecture/language-features.md) — 各 C# 特性在 AOP Emit 中的适配情况
diff --git a/docs/guide/upgrade-to-3.0.md b/docs/guide/upgrade-to-3.0.md
new file mode 100644
index 00000000..3f61a190
--- /dev/null
+++ b/docs/guide/upgrade-to-3.0.md
@@ -0,0 +1,168 @@
+# 从 2.x 升级到 3.0
+
+本页面向已经在使用 AspectCore 2.x 的项目,说明升级到 3.0 需要注意什么、怎么升、以及升级后遇到问题如何排查。核心结论先给出来:**目标框架在 `net6.0` 及以上的项目,绝大多数只需升级包版本,代码零改动**;真正的门槛只有一个——目标框架收窄。
+
+> 本页说的是 **AspectCore 自身的大版本升级(2.x → 3.0)**。如果你要从 Castle DynamicProxy 迁移到 AspectCore,那是另一件事,见[Castle 迁移指南](./castle-migration/migration-guide.md),不要和本页混淆。
+
+## 先判断:你能不能升
+
+3.0 最大的变化是目标框架(TFM)收窄。先对照下表确认你的项目是否满足升级前提:
+
+| 你的项目目标框架 | 能否升级到 3.0 | 说明 |
+|------------------|----------------|------|
+| `net8.0` / `net9.0` / `net10.0` | ✅ 可以 | 推荐路径,功能完整 |
+| `net6.0` | ✅ 可以 | 最低支持;核心库与大部分集成包保留了 `net6.0` |
+| `net7.0` | ⚠️ 需先升框架 | 3.0 不再提供 `net7.0`,需先把项目升到 `net8.0` 及以上 |
+| `.NET Framework`(`net461` 等) | ❌ 不能 | 3.0 移除了 `net461`,请停留在 2.x |
+| 以 `netstandard2.0` / `netstandard2.1` 消费的库 | ❌ 不能 | 3.0 移除了 netstandard 目标,请停留在 2.x |
+
+如果这一步就卡住了(.NET Framework 或 netstandard 消费方),先看[常见升级问题](#常见升级问题)里的替代方案,不必往下读升级步骤。
+
+## Breaking Changes
+
+### 1. 目标框架收窄(最主要的破坏性变更)
+
+所有包的目标框架从 2.4.0 的多目标收窄为 3.0 的现代 .NET:
+
+| 版本 | 目标框架 |
+|------|----------|
+| 2.4.0 | `net7.0;net6.0;netstandard2.1;netstandard2.0;net461` |
+| 3.0 | `net10.0;net9.0;net8.0;net6.0` |
+
+**被移除的框架**:`net461`(.NET Framework)、`netstandard2.0`、`netstandard2.1`、`net7.0`。
+
+**为什么收窄**:
+
+- NativeAOT 要求 .NET 7+,而 `System.Reflection.Emit` 在 AOT 下不可用;AspectCore 的编译期引擎要落地就必须放弃老框架。
+- Default Interface Method(3.0 内部用来保证 DynamicProxy 路径兼容)要求 .NET Core 3.0+,netstandard2.0 无法编译。
+- netstandard2.0/2.1 的实际使用场景基本是 .NET Framework 遗留项目,这类项目不会用到 SG 引擎和 NativeAOT,收窄对它们没有价值损失。
+- `net7.0` 已 EOL,`net6.0` 虽也已 EOL,但作为 AOP 框架的最低门槛仍覆盖大量存量项目,故保留。
+
+(依据:`docs/architecture/nativeaot-design.md` 的「TFM 变更」一节。)
+
+**二进制兼容影响**:对 `netstandard2.0` 消费者而言,`AspectCore.Abstractions` 的 TFM 收窄是破坏性的(无法再被引用);对 `net6.0` 及以上的消费者,`AspectCore.Abstractions` 新增了接口与默认接口方法,属于**非破坏性**变更。
+
+### 2. 各包目标框架的两个特例
+
+大部分包都是 `net10.0;net9.0;net8.0;net6.0`,但有两个包例外,升级时注意:
+
+| 包 | 目标框架 | 注意点 |
+|----|----------|--------|
+| `AspectCore.Extensions.CastleCompat` | `net10.0;net9.0;net8.0`(**不含 `net6.0`**) | 如果你的项目停在 `net6.0` 又想用 Castle 兼容层,用不了;需升到 `net8.0` 及以上 |
+| `AspectCore.SourceGenerator` | `netstandard2.0` | 这是 Roslyn 分析器的约定要求,以 analyzer 形式被编译器加载,不代表你的项目要支持 netstandard2.0 |
+
+### 3. 新增的包
+
+3.0 引入两个新包,都属于 opt-in,不装就不影响现有行为:
+
+| 包 | 作用 |
+|----|------|
+| `AspectCore.SourceGenerator` | 编译期代理引擎,在编译时生成代理类型,是 NativeAOT 支持的基础。默认不启用。 |
+| `AspectCore.Extensions.CastleCompat` | Castle DynamicProxy 兼容垫片,供存量 Castle 代码渐进迁移到 AspectCore。 |
+
+包的完整清单与选包建议见[安装](../getting-started/installation.md)。
+
+### 4. 默认行为保持不变
+
+这是本次升级最需要明确的一点:**3.0 没有改变默认运行时行为**。
+
+- 默认代理引擎仍然是 `DynamicProxy`。`ProxyEngineOptions.Engine` 的默认值就是 `ProxyEngine.DynamicProxy`,不显式配置就走原来的运行时织入路径。
+- DynamicProxy 路径做了「零变更保证」:`AspectActivatorContext`(struct)、`IAspectContextFactory` 的原有方法、`RuntimeAspectContext`、`MethodReflector` 等均保持不变,不改签名、不改行为。
+- 结论:**目标框架在 `net6.0` 及以上的项目,从 2.x 升到 3.0 通常代码零改动,只是把 NuGet 包版本升上去**。你现有的拦截器、配置方式、DI 注册都照旧工作。
+
+(依据:`ProxyEngineOptions.cs` 中 `Engine` 的默认值;`docs/architecture/nativeaot-design.md` 的「DynamicProxy 路径兼容性保证」一节。)
+
+### 5. 移除的 API
+
+提交历史中可见的一处公开 API 移除:`ObjectExtensions.cs`(提交 `cbbaf24`,#347)。
+
+> **诚实说明**:更细粒度的 public API 增删本指南没有逐条核对。如果你的代码依赖了某些不常用的公开类型,升级后出现编译错误,属正常范围。需要精确的 API 差异清单时,建议对 `v2.4.0..HEAD` 做一次符号级 diff(例如借助 API 对比工具),本指南不臆造一份删除清单。
+
+## 升级步骤
+
+### 第 1 步:前置检查(目标框架)
+
+先确认目标框架落在 `net6.0` / `net8.0` / `net9.0` / `net10.0`。若当前是 `net7.0` 或更老,先改 `.csproj` 的 `TargetFramework(s)`:
+
+```xml
+
+net8.0
+```
+
+若是 .NET Framework 或 netstandard 消费方,无法升级到 3.0,见[常见升级问题](#常见升级问题)。
+
+### 第 2 步:更新 NuGet 包版本
+
+把用到的 AspectCore 包统一升到 3.0:
+
+```bash
+# 最常见的入口包,会带上 AspectCore.Core / AspectCore.Abstractions
+dotnet add package AspectCore.Extensions.DependencyInjection --version 3.0.0
+
+# 按需升级其他用到的包,例如
+dotnet add package AspectCore.Extensions.Autofac --version 3.0.0
+dotnet add package AspectCore.Extensions.Configuration --version 3.0.0
+```
+
+> 3.0 目前为预览阶段(`3.0.0-rc.1`)。正式版发布后把版本号换成对应的稳定版即可;升级方式一致。
+
+### 第 3 步:验证现有拦截行为
+
+由于默认引擎不变,升级后**不需要改拦截器代码**。构建并运行你的测试,确认拦截行为与升级前一致即可。若拦截"突然不生效",多半是目标框架或引擎配置问题,见[常见升级问题](#常见升级问题)。
+
+### 第 4 步(可选):启用 Source Generator 引擎
+
+如果你想用编译期代理(例如为了 NativeAOT),显式切换引擎:
+
+```csharp
+using AspectCore.DynamicProxy;
+
+services.AddDynamicProxy();
+services.ConfigureDynamicProxyEngine(options =>
+{
+ options.Engine = ProxyEngine.SourceGenerator;
+ // options.Strict = true; // 缺失生成物时直接抛异常,适合在 CI 强约束
+});
+```
+
+`ProxyEngine` 有三个取值:`DynamicProxy`(默认,运行时)、`SourceGenerator`(编译期)、`Auto`(优先 SG,缺失时按 `AllowRuntimeFallback` 策略回退 DynamicProxy)。两套引擎的差异与选型见[两套引擎对比与选型](../architecture/engine-comparison.md)。
+
+(依据:`ProxyEngine.cs` 枚举定义;`ServiceCollectionExtensions.ConfigureDynamicProxyEngine`;`docs/architecture/nativeaot-design.md` 的使用示例。)
+
+### 第 5 步(按需):NativeAOT
+
+如果目标是发布 NativeAOT 应用,需切换到 Source Generator 引擎,并遵循 NativeAOT 的额外约束。设计与限制见[NativeAOT 设计文档](../architecture/nativeaot-design.md);面向使用者的上手步骤见 [NativeAOT 上手指南](../getting-started/nativeaot.md)。
+
+> 注意:NativeAOT 目前只覆盖 Source Generator 路径,DynamicProxy 路径在 AOT 下仍不可用。
+
+### 第 6 步(按需):从 Castle 迁移
+
+如果你的项目同时还在用 Castle DynamicProxy,想借这次升级一并迁到 AspectCore,那是一条独立的迁移路径,见 [Castle 迁移指南](./castle-migration/migration-guide.md)、[功能对比](./castle-migration/feature-comparison.md)、[迁移检查清单](./castle-migration/checklist.md)。再次强调:Castle 迁移和本页的 2.x → 3.0 版本升级是两回事。
+
+## 常见升级问题
+
+### 我的项目是 .NET Framework / netstandard2.0,怎么办?
+
+3.0 已移除 `net461`、`netstandard2.0`、`netstandard2.1`,这类项目无法升级。可选方案:
+
+- **停留在 2.x**:2.4.0 仍支持这些框架,功能不变,继续可用。
+- **升级运行时后再升 AspectCore**:如果条件允许把项目迁到 `net6.0`+,迁移完成后即可升级到 3.0。
+
+### 升级后拦截"不生效"了?
+
+按顺序排查:
+
+1. **目标框架**:确认项目实际编译到的是 `net6.0`+,而不是意外落回了不受支持的框架。
+2. **引擎配置**:如果显式设置了 `ProxyEngine.SourceGenerator` 或 `Auto`,确认生成物已正确产出;否则先去掉引擎配置回到默认 `DynamicProxy` 验证一遍,隔离问题。
+3. **包版本一致**:确认所有 AspectCore 包都升到了同一大版本,避免新旧混用。
+
+### 启用 Source Generator 后编译报诊断(ACSGxxx)?
+
+Source Generator 引擎在编译期会产出以 `ACSG` 开头的诊断信息(提示哪些方法/类型无法被编译期代理等)。诊断编号的含义与处理办法见 [Source Generator 诊断参考](./source-generator-diagnostics.md)。
+
+## 相关文档
+
+- [安装](../getting-started/installation.md) — 各包用途、目标框架、选包建议
+- [两套引擎对比与选型](../architecture/engine-comparison.md) — DynamicProxy vs SourceGenerator vs Auto
+- [NativeAOT 设计文档](../architecture/nativeaot-design.md) — TFM 变更、兼容性保证、引擎选择矩阵
+- [Castle 迁移指南](./castle-migration/migration-guide.md) — 从 Castle DynamicProxy 迁移到 AspectCore(区别于本页的版本升级)
diff --git a/docs/release-notes/v3.0.0-changelog.md b/docs/release-notes/v3.0.0-changelog.md
new file mode 100644
index 00000000..de25e30a
--- /dev/null
+++ b/docs/release-notes/v3.0.0-changelog.md
@@ -0,0 +1,144 @@
+# AspectCore 3.0.0 变更日志(Changelog)
+
+> 覆盖范围:**v2.4.0 → v3.0.0**。本篇是结构化的变更清单,按 特性 / 性能 / 修复 / 破坏性变更 / 工程 分节列出。
+> 发布背景与研发过程的叙事版本见同目录的 [v3.0.0.md](./v3.0.0.md),两篇互补。
+
+> ⚠️ **升级须知**:3.0.0 是大版本升级,**目标框架显著收窄**——移除了 .NET Framework(net461)与 netstandard2.0/2.1 支持。仍运行在这些框架上的消费者**无法直接升级**。升级前请先阅读下方 [💥 Breaking Changes](#-breaking-changes升级前必读) 一节。
+
+---
+
+## 版本坐标
+
+| 版本 | 提交 | 日期 | 说明 |
+|------|------|------|------|
+| v2.4.0 | `dfd5cf6` | 2023-05-25 | netstandard2.0 时代的最后一个稳定版(本次变更的基线) |
+| v2.6.3 | `635a6a5` | 2026-07-15 | 中间版本 |
+| v3.0.0-beta.1 | `88a11d8` | 2026-07-21 | 首个 3.0 预览 |
+| v3.0.0-rc.1 | `ed93236` | 2026-07-25 | 候选发布 |
+
+自 v2.4.0 至 HEAD 共 **137 个提交**。
+
+---
+
+## 🚀 新特性
+
+### Source Generator 编译期代理引擎(全新引擎)
+
+在既有的 DynamicProxy 运行时引擎之外新增一套基于 Roslyn 的编译期代理引擎,在编译时为被拦截方法生成强类型代理代码,是 NativeAOT 支持的基础。
+
+- `bff678b` — 以 AST 架构重构 proxy emit(#341)
+- `05c19b8` — Source Generator GA 改进:多程序集、泛型、AOT、inline activation(#348)
+
+### NativeAOT AOP 支持(编译期调度委托)
+
+Source Generator 在编译期生成调度委托,运行时直接调用,使 AOP 拦截能够在 NativeAOT 发布下工作。
+
+- `38f71bc` — NativeAOT AOP 支持(#392)
+- `d43c4b3` — 强化 NativeAOT 下的 Source Generator 覆盖(#402)
+
+### Castle DynamicProxy 迁移工具包 + 竞品 benchmark
+
+- `3cc2087` — 面向 Castle DynamicProxy 用户的迁移工具包,附竞品 benchmark(#397)
+
+### C# 语言特性代理支持
+
+补齐 C# 9 ~ C# 13 语言特性在代理生成中的适配:
+
+- `2733202` — C# 语言特性 Emit 适配(基础,#375)
+- `11ad20b` — C# 12 主构造函数 + C# 13 params collections(#376)
+- `3f8669d` — ref struct 拒绝(rejection)+ scoped 参数(#377)
+- `a748efc` — C# 13 partial properties(#378)
+- `4bf323d` — init-only / required members(#381)
+- `5770dbb` — async enumerable(`IAsyncEnumerable`,#382)
+- `fe319f9` — interpolated string handler + `Index` / `Range`(#383)
+- `ea68a9c` — C# 9 record 类型(#384)
+- `bbece5a` — `ref` / `ref readonly` 返回(#385)
+
+---
+
+## ⚡ 性能优化
+
+### 多 TFM 综合性能优化(Phase 1-3)
+
+- `47c5a83` — 分三个阶段的综合优化(#400):
+ - **Phase 1**:消除热路径分配(适用于全部目标框架)
+ - **Phase 2**:net8.0+ 条件优化 —— `ObjectPool`、`FrozenDictionary`、`UnsafeAccessor`
+ - **Phase 3**:`PoolingAsyncValueTaskMethodBuilder` + 泛型方法缓存
+ - net6.0 走 fallback 路径,不使用上述新 API
+- `69eb544` — 缓存 pipeline delegate,Source Generator 代理改用 `Array.Empty`(#399)
+
+### 对外性能数字
+
+| 场景 | v2.x(DynamicProxy) | v3.0(Source Generator) |
+|------|:---:|:---:|
+| 同步方法拦截 | 220 ns / 360 B | 112 ns / 112 B |
+| `ValueTask` | 1,975 ns / 929 B | 1,514 ns / 232 B |
+| NativeAOT 冷启动 | 不可用 | 18 ms |
+
+经三轮优化后,Source Generator 引擎相较 DynamicProxy **约快 49%、内存分配约少 69%**。
+
+> 📊 **数据来源**:以上数字来自项目自述与 BenchmarkDotNet 产物,并非本文档独立复现的测量结果;实际收益随运行时、目标框架与工作负载而异。
+
+---
+
+## 🐛 Bug 修复
+
+### 依赖注入(DI)
+
+- `b7769fa` — 修复内置 DI 容器的多个问题(#331 / #271 / #254,经由 #401 合并)
+- `6df4161` — 修复 keyed service 在核心 DI 管道中的解析(#389)
+- `d7750bf` — 修复 `IServiceResolver` 的 keyed service 解析(#387)
+- `dbccd10` — 修复 `ServiceValidator` 的 keyed service 解析(#326)
+- `f1b6d9b` — 服务 scope 校验与 Microsoft DI 对齐兼容(#336)
+
+### 拦截
+
+- `8b991e0` — 修复调用 internal 类的非代理方法时抛出 `MethodAccessException`(#274)
+- `9467eb1` — 修复工厂(factory)注册服务的拦截(#235)
+- `f34af55` — 修复 `ServiceInterceptor` 的 `AllowMultiple` 因 `.Distinct()` 未生效的问题
+
+---
+
+## 💥 Breaking Changes(升级前必读)
+
+### 1. 目标框架收窄(影响最大)
+
+3.0.0 移除了旧运行时的目标框架:
+
+| | 目标框架(TFM) |
+|---|---|
+| v2.4.0 | `net7.0;net6.0;netstandard2.1;netstandard2.0;net461` |
+| **3.0.0** | `net10.0;net9.0;net8.0;net6.0` |
+
+**被移除的目标框架**:`net461`(.NET Framework 4.6.1)、`netstandard2.0`、`netstandard2.1`。
+
+- **受影响者**:仍运行在 .NET Framework 或仅依赖 netstandard2.0/2.1 的项目**无法升级到 3.0.0**,需要停留在 2.x 分支,或先迁移到 .NET 6+ 运行时。
+- **原因**:NativeAOT 支持要求 .NET 7+;默认接口方法(DIM,Default Interface Members)要求 .NET Core 3.0+。这两项能力无法在被移除的框架上实现。
+
+### 2. 新增 NuGet 包
+
+- `AspectCore.SourceGenerator` — 编译期代理引擎
+- `AspectCore.Extensions.CastleCompat` — Castle DynamicProxy 兼容 / 迁移支持
+
+### 3. 默认引擎不变
+
+默认代理引擎仍为 **DynamicProxy**,以降低现有用户的迁移成本;Source Generator 引擎为 **opt-in**(按需启用)。因此从 2.x 升级到 3.0(且已在受支持的目标框架上)时,拦截行为保持一致,不会被动切换到新引擎。
+
+### 4. 迁移步骤
+
+目标框架收窄与引擎选型的详细迁移步骤,详见[升级指南](../guide/upgrade-to-3.0.md)。
+
+---
+
+## 🏗️ 工程与 CI
+
+- `7587d88` — CI 从 AppVeyor 迁移到 GitHub Actions(#349)
+- `16d097a` — 测试覆盖率提升至 core 98% / extensions 90%,新增 2462 个测试(#353);当前测试总数 2800+
+- `b0133a3` — CI 增加 coverage、lint、CodeQL、.NET analyzers 门禁(#369)
+
+---
+
+## 相关链接
+
+- 发布叙事与研发过程:[v3.0.0.md](./v3.0.0.md)
+- 项目主页:
diff --git a/icon.png b/icon.png
new file mode 100644
index 00000000..c748a32c
Binary files /dev/null and b/icon.png differ
diff --git a/publish-aot/AspectCore.NativeAot.E2E b/publish-aot/AspectCore.NativeAot.E2E
deleted file mode 100755
index 3bfc117b..00000000
Binary files a/publish-aot/AspectCore.NativeAot.E2E and /dev/null differ
diff --git a/publish-aot/AspectCore.NativeAot.E2E.dbg b/publish-aot/AspectCore.NativeAot.E2E.dbg
deleted file mode 100755
index 65c5374d..00000000
Binary files a/publish-aot/AspectCore.NativeAot.E2E.dbg and /dev/null differ
diff --git a/tests/AspectCore.Core.Tests/EngineParity/SourceGeneratorDiagnosticVerificationTests.cs b/tests/AspectCore.Core.Tests/EngineParity/SourceGeneratorDiagnosticVerificationTests.cs
index 99ea37e4..ee161ff3 100644
--- a/tests/AspectCore.Core.Tests/EngineParity/SourceGeneratorDiagnosticVerificationTests.cs
+++ b/tests/AspectCore.Core.Tests/EngineParity/SourceGeneratorDiagnosticVerificationTests.cs
@@ -1,46 +1,51 @@
#nullable enable
using System;
-using AspectCore.DynamicProxy;
+using System.Linq;
+using AspectCore.SourceGenerator;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
using Xunit;
namespace AspectCore.Core.Tests.EngineParity;
///
-/// 编译期诊断验证测试(文档性质)
+/// 编译期诊断验证测试
///
-/// 这些测试验证 Source Generator 在编译时报告的诊断信息。
-/// 由于诊断测试需要 Roslyn 编译测试框架,这里以文档形式记录预期行为。
+/// 这些测试通过真实驱动 来验证 Source Generator
+/// 在编译时报告的诊断信息:构造会触发对应场景的最小源码片段,运行 Source Generator,
+/// 断言产出的诊断包含期望的 ACSGxxx id 与 severity(或按预期不产生诊断)。
///
-/// 实际的诊断验证应该在单独的编译测试项目中进行,或者手动验证编译输出。
+/// 编译驱动范式与 保持一致(构造
+/// + ,检查 GetRunResult().Diagnostics)。
///
public class SourceGeneratorDiagnosticVerificationTests
{
#region ACSG005: Sealed 类型诊断
///
- /// 验证:尝试为 sealed 类型生成代理应该报告 ACSG005 错误
+ /// 验证:尝试为 sealed 类型生成代理应该报告 ACSG005 错误。
///
- /// 测试代码:
- ///
- /// [AspectCoreGenerateProxy]
- /// public sealed class SealedService
- /// {
- /// public virtual void DoWork() { }
- /// }
- ///
- ///
- /// 预期诊断:
- /// - Id: ACSG005
- /// - Severity: Error
- /// - Message: "无法为 sealed 类型 'SealedService' 生成代理。请移除 sealed 修饰符或使用接口代理。"
+ /// 预期诊断:Id=ACSG005,Severity=Error,消息包含目标类型名。
///
[Fact]
public void SealedType_Should_Report_ACSG005_Error_Documentation()
{
- // 这个测试作为文档,记录预期的诊断行为
- // 实际验证需要在编译测试项目中进行
- Assert.True(true, "此测试作为文档,记录 ACSG005 诊断的预期行为");
+ const string source = """
+ using AspectCore.DynamicProxy;
+
+ [AspectCoreGenerateProxy]
+ public sealed class SealedService
+ {
+ public void DoWork() { }
+ }
+ """;
+
+ var runResult = RunGenerator(source, "SealedTypeDiagnostic");
+
+ var diagnostic = Assert.Single(runResult.Diagnostics.Where(d => d.Id == "ACSG005"));
+ Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity);
+ Assert.Contains("SealedService", diagnostic.GetMessage());
}
#endregion
@@ -48,28 +53,29 @@ public void SealedType_Should_Report_ACSG005_Error_Documentation()
#region ACSG007: 无构造函数诊断
///
- /// 验证:尝试为没有可访问构造函数的类型生成代理应该报告 ACSG007 错误
+ /// 验证:尝试为没有可访问构造函数的类型生成代理应该报告 ACSG007 错误。
///
- /// 测试代码:
- ///
- /// [AspectCoreGenerateProxy]
- /// public class NoPublicCtorService
- /// {
- /// private NoPublicCtorService() { }
- /// public virtual void DoWork() { }
- /// }
- ///
- ///
- /// 预期诊断:
- /// - Id: ACSG007
- /// - Severity: Error
- /// - Message: "类型 'NoPublicCtorService' 没有可访问的构造函数。类代理要求目标类型具有 public 或 protected 构造函数。"
+ /// 预期诊断:Id=ACSG007,Severity=Error,消息包含目标类型名。
///
[Fact]
public void NoAccessibleConstructor_Should_Report_ACSG007_Error_Documentation()
{
- // 这个测试作为文档,记录预期的诊断行为
- Assert.True(true, "此测试作为文档,记录 ACSG007 诊断的预期行为");
+ const string source = """
+ using AspectCore.DynamicProxy;
+
+ [AspectCoreGenerateProxy]
+ public class NoPublicCtorService
+ {
+ private NoPublicCtorService() { }
+ public virtual void DoWork() { }
+ }
+ """;
+
+ var runResult = RunGenerator(source, "NoAccessibleConstructorDiagnostic");
+
+ var diagnostic = Assert.Single(runResult.Diagnostics.Where(d => d.Id == "ACSG007"));
+ Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity);
+ Assert.Contains("NoPublicCtorService", diagnostic.GetMessage());
}
#endregion
@@ -77,26 +83,29 @@ public void NoAccessibleConstructor_Should_Report_ACSG007_Error_Documentation()
#region ACSG006: 类型可见性诊断
///
- /// 验证:internal 类型应该可以正常生成代理(Source Generator 在同一个编译上下文中)
- ///
- /// 测试代码:
- ///
- /// [AspectCoreGenerateProxy]
- /// internal class InternalService
- /// {
- /// public virtual void DoWork() { }
- /// }
- ///
+ /// 验证:internal 类型应该可以正常生成代理(Source Generator 生成的代码位于同一编译上下文)。
///
- /// 预期结果:
- /// - 不应该报告 ACSG006 错误
- /// - 应该成功生成代理
+ /// 预期结果:不报告 ACSG006(类型不可见)错误,不产生任何 Error 级别诊断,并且成功生成代理源码。
///
[Fact]
public void InternalType_Should_Not_Report_Error_Documentation()
{
- // 这个测试作为文档,记录预期的诊断行为
- Assert.True(true, "此测试作为文档,记录 internal 类型的预期行为");
+ const string source = """
+ using AspectCore.DynamicProxy;
+
+ [AspectCoreGenerateProxy]
+ internal class InternalService
+ {
+ public virtual void DoWork() { }
+ }
+ """;
+
+ var runResult = RunGenerator(source, "InternalTypeDiagnostic");
+
+ Assert.Empty(runResult.Diagnostics.Where(d => d.Id == "ACSG006"));
+ Assert.DoesNotContain(runResult.Diagnostics, d => d.Severity == DiagnosticSeverity.Error);
+ // internal 类型对生成器可见,应实际产出代理源码。
+ Assert.NotEmpty(runResult.GeneratedTrees);
}
#endregion
@@ -104,27 +113,34 @@ public void InternalType_Should_Not_Report_Error_Documentation()
#region ACSG001: 开放泛型类型诊断
///
- /// 验证:尝试为开放泛型类型生成代理应该报告 ACSG001 警告
+ /// 验证:开放泛型类型的实际行为。
///
- /// 测试代码:
- ///
- /// [AspectCoreGenerateProxy]
- /// public class GenericService<T>
- /// {
- /// public virtual void DoWork(T value) { }
- /// }
- ///
+ /// 注意:当前版本的 Source Generator **支持**开放泛型类型的类代理(泛型参数会被转发到代理类型,
+ /// 参见 与 ProxyEmitter 中的泛型处理逻辑)。
+ /// ACSG001(UnsupportedGenericType)描述符虽已定义,但在生产代码中没有任何发出点,
+ /// 因此开放泛型类型不会触发 ACSG001。
///
- /// 预期诊断:
- /// - Id: ACSG001
- /// - Severity: Warning
- /// - Message: "类型 'GenericService<T>' 为开放泛型,当前版本的 Source Generator 暂不支持生成代理。"
+ /// 预期结果:不报告 ACSG001,不产生任何 Error 级别诊断,并且成功生成代理源码。
///
[Fact]
public void OpenGenericType_Should_Report_ACSG001_Warning_Documentation()
{
- // 这个测试作为文档,记录预期的诊断行为
- Assert.True(true, "此测试作为文档,记录 ACSG001 诊断的预期行为");
+ const string source = """
+ using AspectCore.DynamicProxy;
+
+ [AspectCoreGenerateProxy]
+ public class GenericService
+ {
+ public virtual void DoWork(T value) { }
+ }
+ """;
+
+ var runResult = RunGenerator(source, "OpenGenericTypeDiagnostic");
+
+ Assert.Empty(runResult.Diagnostics.Where(d => d.Id == "ACSG001"));
+ Assert.DoesNotContain(runResult.Diagnostics, d => d.Severity == DiagnosticSeverity.Error);
+ // 开放泛型类型受支持,应实际产出代理源码。
+ Assert.NotEmpty(runResult.GeneratedTrees);
}
#endregion
@@ -132,30 +148,31 @@ public void OpenGenericType_Should_Report_ACSG001_Warning_Documentation()
#region ACSG002: 嵌套类型诊断
///
- /// 验证:尝试为嵌套类型生成代理应该报告 ACSG002 警告
- ///
- /// 测试代码:
- ///
- /// public class OuterClass
- /// {
- /// [AspectCoreGenerateProxy]
- /// public class NestedService
- /// {
- /// public virtual void DoWork() { }
- /// }
- /// }
- ///
+ /// 验证:尝试为嵌套类型生成代理应该报告 ACSG002 警告。
///
- /// 预期诊断:
- /// - Id: ACSG002
- /// - Severity: Warning
- /// - Message: "类型 'NestedService' 为嵌套类型,当前版本的 Source Generator 暂不支持生成代理。"
+ /// 预期诊断:Id=ACSG002,Severity=Warning,消息包含嵌套类型名。
///
[Fact]
public void NestedType_Should_Report_ACSG002_Warning_Documentation()
{
- // 这个测试作为文档,记录预期的诊断行为
- Assert.True(true, "此测试作为文档,记录 ACSG002 诊断的预期行为");
+ const string source = """
+ using AspectCore.DynamicProxy;
+
+ public class OuterClass
+ {
+ [AspectCoreGenerateProxy]
+ public class NestedService
+ {
+ public virtual void DoWork() { }
+ }
+ }
+ """;
+
+ var runResult = RunGenerator(source, "NestedTypeDiagnostic");
+
+ var diagnostic = Assert.Single(runResult.Diagnostics.Where(d => d.Id == "ACSG002"));
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
+ Assert.Contains("NestedService", diagnostic.GetMessage());
}
#endregion
@@ -163,29 +180,57 @@ public void NestedType_Should_Report_ACSG002_Warning_Documentation()
#region ACSG003: 事件成员诊断
///
- /// 验证:尝试为包含事件的类型生成代理应该报告 ACSG003 警告
+ /// 验证:尝试为包含事件成员的类型生成代理应该报告 ACSG003 警告。
///
- /// 测试代码:
- ///
- /// [AspectCoreGenerateProxy]
- /// public class ServiceWithEvent
- /// {
- /// public virtual event EventHandler MyEvent;
- /// public virtual void DoWork() { }
- /// }
- ///
- ///
- /// 预期诊断:
- /// - Id: ACSG003
- /// - Severity: Warning
- /// - Message: "类型 'ServiceWithEvent' 包含事件成员 'MyEvent',当前版本的 Source Generator 暂不支持生成代理。"
+ /// 预期诊断:Id=ACSG003,Severity=Warning,消息包含类型名与事件成员名。
///
[Fact]
public void TypeWithEvent_Should_Report_ACSG003_Warning_Documentation()
{
- // 这个测试作为文档,记录预期的诊断行为
- Assert.True(true, "此测试作为文档,记录 ACSG003 诊断的预期行为");
+ const string source = """
+ using System;
+ using AspectCore.DynamicProxy;
+
+ [AspectCoreGenerateProxy]
+ public class ServiceWithEvent
+ {
+ public virtual event EventHandler MyEvent;
+ public virtual void DoWork() { }
+ }
+ """;
+
+ var runResult = RunGenerator(source, "TypeWithEventDiagnostic");
+
+ var diagnostic = Assert.Single(runResult.Diagnostics.Where(d => d.Id == "ACSG003"));
+ Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
+ Assert.Contains("ServiceWithEvent", diagnostic.GetMessage());
+ Assert.Contains("MyEvent", diagnostic.GetMessage());
}
#endregion
+
+ ///
+ /// 编译给定源码并运行 ,返回生成器运行结果
+ /// (包含诊断与生成的语法树)。编译驱动范式与 一致。
+ ///
+ private static GeneratorDriverRunResult RunGenerator(
+ string source,
+ string assemblyName,
+ LanguageVersion languageVersion = LanguageVersion.Latest)
+ {
+ var compilation = CSharpCompilation.Create(
+ assemblyName: assemblyName,
+ syntaxTrees: new[] { CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(languageVersion)) },
+ references: AppDomain.CurrentDomain.GetAssemblies()
+ .Where(assembly => !assembly.IsDynamic
+ && !string.IsNullOrEmpty(assembly.Location)
+ && assembly != typeof(SourceGeneratorDiagnosticVerificationTests).Assembly)
+ .Select(assembly => MetadataReference.CreateFromFile(assembly.Location)),
+ options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(new AspectCoreProxyGenerator().AsSourceGenerator());
+ driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _);
+
+ return driver.GetRunResult();
+ }
}
diff --git a/tests/AspectCore.E2E.Tests/Scenarios/AdditionalCoverageScenarios.cs b/tests/AspectCore.E2E.Tests/Scenarios/AdditionalCoverageScenarios.cs
index 73c637d5..b1141294 100644
--- a/tests/AspectCore.E2E.Tests/Scenarios/AdditionalCoverageScenarios.cs
+++ b/tests/AspectCore.E2E.Tests/Scenarios/AdditionalCoverageScenarios.cs
@@ -286,17 +286,25 @@ public async Task Async_TaskVoidReturn_Chain_Works()
using var host = new TestHost();
host.Add();
+ InterceptorLog.Clear();
var service = host.Resolve(config =>
{
config.Interceptors.AddDelegate(async (ctx, next) =>
{
+ InterceptorLog.Entries.Add("Chain.Before");
await ctx.Invoke(next);
+ InterceptorLog.Entries.Add("Chain.After");
}, Predicates.Implement(typeof(IAsyncService)));
});
await service.ChainAsync();
- // If we get here without exception, the chain worked
- Assert.True(true);
+
+ // A Task (void-result) async method must flow through the interceptor
+ // chain: the Before entry is recorded, the inner invocation is awaited,
+ // then the After entry is recorded in order.
+ Assert.Equal(
+ new[] { "Chain.Before", "Chain.After" },
+ InterceptorLog.Entries.ToArray());
}
[Fact]