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]