Skip to content

Port to .NET 10 SDK, rename to ICSharpCode.ReferenceAssemblyAnnotator, add GitHub Actions - #1

Merged
christophwille merged 7 commits into
masterfrom
net10-port
Nov 9, 2026
Merged

christophwille merged 7 commits into
masterfrom
net10-port

Conversation

@christophwille

@christophwille christophwille commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

Summary

This PR brings the annotator from SDK 3.1 / C# compiler 3.4 / AppVeyor to SDK 11, renames it to ICSharpCode.ReferenceAssemblyAnnotator so it can be published to nuget.org under a new ID, and adds a GitHub Actions build. The goal was the fewest changes that get there. The package's behavior for consumers stays the same: same MSBuild properties and items, same package layout, and nullable attributes are still added as source.

Earlier attempts this builds on: tunnelvisionlabs#90 (net6 port, never merged) contributed the WPF fix and the nullable code fixes. The PolySharp approach from tunnelvisionlabs#95 was evaluated and not taken (see below).

Validation:

  • The full test matrix (17 test builds) passes with both dotnet msbuild and VS 2026 msbuild.exe, locally and in CI.
  • ILSpy's ICSharpCode.Decompiler (netstandard2.0, WarningsAsErrors=nullable) builds from scratch against the new package; 112 reference assemblies were annotated.

Commits

1. Port to .NET 10 SDK and update dependencies

Tooling

  • global.json → 11.0.0, rollForward: major, allowPrerelease: true (uses the newest installed SDK 11).
  • Removed the pinned Microsoft.Net.Compilers.Toolset 3.4.0; the SDK's compiler is used.
  • The repo annotates its own references from Microsoft.NETCore.App.Ref 10.0.0 (was 3.1.0), using the published TunnelVisionLabs.ReferenceAssemblyAnnotator 1.0.0-alpha.160 (was alpha.138).

Dependencies

Package Old New Notes
Mono.Cecil 0.11.0 0.11.6
Microsoft.Build.Framework / Tasks.Core 16.0.461 17.8.43, ExcludeAssets="runtime" see below
StyleCop.Analyzers 1.2.0-beta.66 1.2.0-beta.556 latest of the 1.2 line, no new rule fallout
Nerdbank.GitVersioning 2.2.13 3.10.94

Why MSBuild 17.8 and not 18.x: the task runs inside the host's MSBuild, so the MSBuild packages are compile-time only. MSBuild 18.x on net472 depends on System.Collections.Immutable 10.0, which our task would then reference. VS 2022 and SDK 8/9 hosts carry older Immutable versions and binding redirects capped below 10, so the task would fail to load there. 17.8.43 (the latest servicing release of the VS 17.8 / SDK 8 line) brings Immutable 7.0, so the package works on SDK 8+ and VS 2022 17.8+. Bumping this later raises that floor.

Task TFMs are unchanged (netstandard2.0 + net472). netstandard2.0 is what dotnet build loads on any SDK; net472 is what VS msbuild.exe loads. Moving to net10.0 would break consumers on SDK 8/9 for no functional gain.

Bug fix in the weaver: TypeReferenceEquivalenceComparer
Mono.Cecil's TypeSpecification.GetElementType() returns the innermost element type, not the direct one. For Nullable<bool>& it returns the open Nullable1, dropping the generic arguments. As a result, every ByReference/Pointer/Arrayof a generic instance compared equal to any other instantiation. Annotating net10.0 hit this onJSMarshalerArgument.ToManaged(out bool?)/ToManaged(out byte?)/ … inSystem.Runtime.InteropServices.JavaScript: SingleOrDefaultthrew and the build failed with MSB4018. The fix comparesTypeSpecification.ElementTypefor these three cases. TheGenericInstancebranch still usesGetElementType()`, which is correct there because the element of a generic instance is never itself a type specification.

Smaller code fixes: nullable return types on FindMatchingMethod/Property/Field and string? in AnnotatorBuildTask (the newer compiler, plus the annotations we now build against, flag them; same as tunnelvisionlabs#90), and an SA1629 doc-comment fix.

WPF: NullableAttributes is added to _GeneratedCodeFiles only when IncludePackageReferencesDuringMarkupCompilation != 'true' (from tunnelvisionlabs#90). Modern SDKs import package references into the *_wpftmp project by default, so the old workaround produced a duplicate compile item.

Tests

  • Added net10.0 everywhere (net10.0-windows for WPF). WPF net40 → net35, because the SDK now provides net35 reference assemblies, so jnm2.ReferenceAssemblies.net35 is gone. The test XAML is simplified to avoid an MC1000 IAddChildInternal error on net35 (same as Test using the .NET 6 SDK and stop adding a duplicate compile item for WPF tunnelvisionlabs/ReferenceAssemblyAnnotator#90).
  • No target frameworks were dropped: netcoreapp1.0 through 3.1 and netstandard1.6 still restore and build.
  • tests/Directory.Build.props: CheckEolTargetFramework=false, NuGetAudit=false (System.Data.SqlClient 4.8.0 advisories), and NETSDK1215 (".NET Standard < 2.0 not recommended") downgraded to a message. The tests cover legacy TFMs on purpose.
  • For .NET Core 3.0+ (netcoreapp3.x, net10.0) the test projects don't declare a PackageDownload of Microsoft.NETCore.App.Ref, because those TFMs are no longer annotated (see Review fixes below).
  • build/test.ps1 -Configuration <cfg> -Engine dotnet|msbuild replaces the 60-line command list in appveyor.yml, so CI and local runs execute the same thing. It runs git clean before each case (needed because of the isolated package cache), finds msbuild.exe via vswhere if it isn't on PATH, and reports all failures at the end instead of stopping at the first.
  • appveyor.yml removed.

2. Rename to ICSharpCode.ReferenceAssemblyAnnotator and modernize packing

  • git mv of the folder, csproj, .props/.targets (NuGet only auto-imports build/<PackageId>.props/targets) and ruleset; namespace rename in all sources, the UsingTask, the .sln and the tests. The MIT copyright headers are unchanged.
  • Removed the NewIdRequiredDueToNuGetBug / RestorePackageId workaround. It existed because the project and the package the repo builds itself with had the same ID. The project is now ICSharpCode.* while it still builds itself with the published TunnelVisionLabs.*, so the IDs no longer clash.
  • Packaging modeled on ICSharpCode.Decompiler: Authors = ILSpy Contributors, Company = ic#code, copyright keeps the TVL line and adds ic#code, new project/repository URLs, PackageReadmeFile (README.md), PackageTags, PublishRepositoryUrl. Symbols: DebugType=embedded + EmbedUntrackedSources with the SDK's built-in Source Link replace the legacy IncludeSymbols/IncludeSource .symbols.nupkg. ContinuousIntegrationBuild is set on GitHub Actions.
  • version.json → 2.0.0-preview.{height}, a clear break from the 1.0.0-alpha.* TunnelVisionLabs line.
  • tests/NuGet.Config: <clear/> plus package source mapping that pins ICSharpCode.ReferenceAssemblyAnnotator to the local bin/<Configuration> output, so the tests can never pick up a published package once one exists.
  • README (package ID, [10.0.0], badges, migration note) and a new CLAUDE.md (contributor/agent notes); codecov.yml removed (unused since AppVeyor).

Package contents: build/ICSharpCode.ReferenceAssemblyAnnotator.{props,targets}, build/NullableAttributes.{cs,vb}, tools/{net472,netstandard2.0}/ (task + Mono.Cecil + Mono.Cecil.Rocks), README.md.

3. GitHub Actions (.github/workflows/build.yml) + 2 CI fixes

windows-2025-vs2026, setup-dotnet 11.0.x preview and setup-msbuild; then dotnet build -c Release (which also packs), test.ps1 with both engines, and upload of the .nupkg as the nuget-package artifact. Windows is required for the net35/net48/WPF cases and for msbuild.exe. Only Release runs; a Debug leg would double the ~34 test builds for little extra coverage.

Two problems only showed up on the runner, both because its checkout path (D:\a\<repo>\<repo>\) is deeper than a typical local one and pushes paths in the isolated test package cache past MAX_PATH:

  • git clean failed to delete the long paths → test.ps1 now passes -c core.longpaths=true.
  • NuGet inside msbuild.exe (.NET Framework) can't write them → the workflow passes test.ps1 -PackagesPath with a short path outside the checkout. This is safe in CI because one run only ever contains one annotator version; local runs keep the per-case isolated cache.

4. Review fixes (4b2d45b)

  • .NET Core 3.0+ is no longer annotated (.targets: the ResolveAssemblyReferencesDependsOn hook is conditioned on .NETCoreApp < 3.0). Whether the SDK adds its own Microsoft.NETCore.App.Ref PackageDownload for these TFMs depends on whether the targeting pack is installed. With it installed, nothing is downloaded and leaving the item out failed with RA0000; without it, a project-level item failed with NU1505. Those reference assemblies are already annotated, so skipping them removes the conflict. Trade-off: those TFMs lose the weaver's [DisallowNull] removal on EqualityComparer<T>.GetHashCode (Program.IsExcludedAnnotation) and the refresh of EOL netcoreapp3.x refs with newer annotations. The GetHashCode line in the test sources is #if !NETCOREAPP3_0_OR_GREATER; the README explains how to condition the PackageDownload when multi-targeting.
  • tests/NuGet.Config uses forward slashes. With backslashes, package source mapping doesn't match the local source on Linux/macOS and restore fails with NU1100. The version range 2.0.0-* was not the cause (bisected in WSL).
  • test.ps1 sets NUGET_PACKAGES itself (-PackagesPath, default tests/.isolatednugetcache). An inherited NUGET_PACKAGES overrides globalPackagesFolder, so on machines with a machine-wide cache the tests could reuse a stale annotator package with the same version.

Verified: the full matrix passes on both engines; net10.0 and MultiTFM pass with an empty NetCoreTargetingPackRoot (no targeting packs); netstandard2.0 and net10.0 pass on Linux; CI is green.

Not done, on purpose

Follow-ups worth considering

  1. The repo still builds itself with the old annotator, which has the comparer bug above. It works today only because this repo's own TFMs (netstandard2.0/net472) never hit the bug. Once ICSharpCode.ReferenceAssemblyAnnotator is on nuget.org, switch Directory.Build.props to it.
  2. Ambiguous matches abort the build. FindMatchingMethod re-throws InvalidOperationException after logging candidates (at message importance), so any future comparer gap turns into an MSB4018 crash for consumers. Consider an RA1xxx warning and skipping the member instead; the same pattern applies to FindMatchingType (Program.cs:259) and the attribute constructor lookup (Program.cs:204).
  3. No unit tests for the weaver. The only tests are whole-project builds, which is how a basic comparer bug went unnoticed. A small test project around EquivalenceComparers and Program.Annotate*, using in-memory Cecil modules, would be cheap and high-value.
  4. netcoreapp3.x consumers + NU1505: resolved in the review fixes (.NET Core 3.0+ is skipped).
  5. Mono.Cecil in VS msbuild.exe (net472): tasks share one AppDomain there, so a different Mono.Cecil version loaded first by another task (Fody, ILLink, …) can win. Core MSBuild isolates tasks in an AssemblyLoadContext, so this only affects VS. Consider ILMerge/ILRepack or a renamed Cecil if it ever shows up.
  6. Newer annotation attributes are not copied (MemberNotNull, MemberNotNullWhen, …); that's what Add support for AOT attributes and MemberNotNull(When) tunnelvisionlabs/ReferenceAssemblyAnnotator#95 addressed. Consumers on netstandard2.0 lose them today.
  7. NBGV publicReleaseRefSpec is ^refs/heads/master$, so branch builds get a -g<sha> suffix. Publish only from master builds, or add a release-tag ref spec plus a publish job (e.g. trusted publishing to nuget.org).
  8. CI runs on a preview SDK (11.0.x, preview). Once SDK 11 ships, drop dotnet-quality: preview and allowPrerelease.
  9. Minor: the nuspec lists <frameworkAssembly assemblyName="mscorlib"> for net472 (an artifact of NoStdLib + explicit mscorlib reference from building with the annotator itself); harmless but noise. CopyLocalLockFileAssemblies=true copies MSBuild/Immutable DLLs into bin that are never packed; only Cecil is picked explicitly in _GetFilesToPackage.

🤖 Generated with Claude Code

christophwille and others added 6 commits November 8, 2026 14:02
- global.json: SDK 11 (roll forward, prerelease allowed); drop the pinned
  Microsoft.Net.Compilers.Toolset 3.4.0 in favor of the SDK compiler.
- Annotate from Microsoft.NETCore.App.Ref 10.0.0; self-annotate with
  TunnelVisionLabs.ReferenceAssemblyAnnotator 1.0.0-alpha.160.
- Update Mono.Cecil 0.11.6, StyleCop.Analyzers 1.2.0-beta.556,
  Nerdbank.GitVersioning 3.10.94. Compile the task against MSBuild 17.8.43
  (compile-time only) so it still loads in SDK 8+ and VS 17.8+; MSBuild 18
  would require System.Collections.Immutable 10.
- Fix TypeReferenceEquivalenceComparer: Cecil's GetElementType() returns the
  innermost type, so by-ref/pointer/array element types lost their generic
  arguments (e.g. Nullable<bool>& matched Nullable<byte>&), which crashed
  annotation of net10.0 System.Runtime.InteropServices.JavaScript.
- Nullable fixes for the newer compiler (as in tunnelvisionlabs#90).
- WPF: only add NullableAttributes to _GeneratedCodeFiles when the markup
  compile project doesn't import package references (as in tunnelvisionlabs#90).
- Tests: add net10.0 / net10.0-windows, WPF net40 -> net35, drop
  jnm2.ReferenceAssemblies.net35 (SDK provides net35 refs). Skip the
  explicit PackageDownload for netcoreapp3.x, where the SDK already
  downloads Microsoft.NETCore.App.Ref (NU1505). Silence EOL, NuGet audit
  and NETSDK1215 warnings in tests. No target frameworks were dropped.
- Replace appveyor.yml with build/test.ps1 (-Engine dotnet|msbuild).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Rename project, folder, namespace, build props/targets and ruleset to
  ICSharpCode.ReferenceAssemblyAnnotator, so the package can be published to
  nuget.org under the new ID.
- Remove the NewIdRequiredDueToNuGetBug PackageId workaround: the project no
  longer shares an ID with the published TunnelVisionLabs package it uses to
  annotate itself.
- Package metadata modeled on ICSharpCode.Decompiler: ic#code / ILSpy
  Contributors, new repository URLs, packed README, tags; embedded PDBs with
  Source Link instead of a legacy symbols package; ContinuousIntegrationBuild
  on GitHub Actions.
- Version line 2.0.0-preview.{height}.
- tests/NuGet.Config: source mapping pins ICSharpCode.ReferenceAssemblyAnnotator
  to the local build output.
- README and CLAUDE.md updated; remove codecov.yml (unused since AppVeyor).

Validated by building ILSpy's ICSharpCode.Decompiler (nullable warnings as
errors) against the new package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed MAX_PATH)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread tests/SingleTFM/SingleTFM.csproj Outdated
<PackageDownload Include="Microsoft.NETCore.App.Ref" Version="[3.0.0]" />
<PackageReference Include="ICSharpCode.ReferenceAssemblyAnnotator" Version="2.0.0-*" PrivateAssets="all" />
<!-- The SDK already downloads Microsoft.NETCore.App.Ref for netcoreapp3.x; a second PackageDownload fails with NU1505. -->
<PackageDownload Condition="!$(TargetFramework.StartsWith('netcoreapp3.'))" Include="Microsoft.NETCore.App.Ref" Version="[10.0.0]" />

@siegfriedpammer siegfriedpammer Nov 8, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction: this duplicate-download failure reproduced with SDK 11.0.100-preview.4, but no longer reproduces after switching to SDK 11.0.100-rc.3. I am retracting this finding; the current SDK adds a compatible targeting-pack download.

Assisted-by: OpenCode:gpt-5.6-luna:OpenCode

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and it's broader than net10.0. Whether the SDK adds its own PackageDownload of Microsoft.NETCore.App.Ref depends on whether that targeting pack is installed. My machine and the CI image have 10.0.x installed, which is why it passed there. Forcing a download with -p:NetCoreTargetingPackRoot=<empty dir> reproduces NU1505 … [10.0.0], [10.0.14] locally.

Leaving out the explicit item alone (the first suggestion) fails the other way: with the pack installed nothing is downloaded, and the targets fail with RA0000. So no condition in the test project works on both kinds of machine.

Fixed in 4b2d45b at the source: the targets no longer annotate .NETCoreApp >= 3.0. Those reference assemblies are already annotated, so no PackageDownload is needed there. The test projects drop the item for netcoreapp3.x/net10.0, and the README shows how to condition it when multi-targeting.

Trade-off: those TFMs lose the weaver's removal of [DisallowNull] from EqualityComparer<T>.GetHashCode, and the refresh of EOL netcoreapp3.x refs with newer annotations. The test's GetHashCode line is now #if !NETCOREAPP3_0_OR_GREATER.

Verified: the full matrix passes on both engines, net10.0 and MultiTFM pass with an empty targeting-pack root, net10.0 passes on Linux with only SDK 11, and CI is green.

<PackageReference Condition="'$(TargetFramework)' != 'net35'" Include="System.Data.SqlClient" Version="4.8.0" />
<PackageReference Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" Version="1.0.0-alpha.*" PrivateAssets="all" />
<PackageDownload Include="Microsoft.NETCore.App.Ref" Version="[3.0.0]" />
<PackageReference Include="ICSharpCode.ReferenceAssemblyAnnotator" Version="2.0.0-*" PrivateAssets="all" />

@siegfriedpammer siegfriedpammer Nov 8, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction: commit 4b2d45b fixes this failure by changing the local source path in tests/NuGet.Config to forward slashes. With CONFIGURATION=Release and SDK 11.0.100-rc.3, restore now succeeds for both netstandard2.0 and net10.0. I am retracting this finding.

Assisted-by: OpenCode:gpt-5.6-luna:OpenCode

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The failure is real, but the cause is different: 2.0.0-* does float to the highest 2.0.0 prerelease. On Windows your exact repro resolves 2.0.0-preview.5.g71a08c2c76, and CI resolves preview.N the same way.

I reproduced the NU1100 on Linux (WSL Ubuntu, SDK 11 rc.2) and bisected it:

  • With source mapping removed, 2.0.0-* resolves preview.5, so the version range is fine.
  • With the mapping kept and the source path switched from ..\ICSharpCode…\bin\%CONFIGURATION% to forward slashes, it resolves as well.

On non-Windows NuGet still finds the backslash source (it's listed as "not considered"), but package source mapping doesn't match it, so the ID has no allowed source.

Fixed in 4b2d45b: forward slashes in tests/NuGet.Config, which Windows also accepts. Re-verified on Linux from the pushed branch (netstandard2.0 and net10.0). The version expressions in the tests and the README are unchanged.

Separately, dotnet pack … -p:TargetFramework=netstandard2.0 fails on its own (NU5026), because the package also expects the net472 output. Use dotnet build -c Release, which packs both.

This also turned up a bug in test.ps1: an inherited NUGET_PACKAGES overrides globalPackagesFolder, so a machine-wide cache could serve a stale package with the same version. The script now sets NUGET_PACKAGES itself.

@siegfriedpammer siegfriedpammer left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed commit 4b2d45b. The author fixes both previously reported restore failures: modern .NETCoreApp targets no longer require annotation or an explicit reference-pack download, and the local NuGet source path now works on Linux/macOS.

I verified the package build plus netstandard2.0 and net10.0 test restores/builds with SDK 11.0.100-rc.3. I found no remaining review findings.

Assisted-by: OpenCode:gpt-5.6-luna:OpenCode

…solation

- Targets: don't annotate .NETCoreApp >= 3.0. Those reference assemblies are
  already annotated, and the Microsoft.NETCore.App.Ref PackageDownload the
  annotator requires can't be declared reliably there: when the targeting
  pack isn't installed the SDK adds its own download (NU1505 on a duplicate),
  and when it is installed, leaving it out failed with RA0000.
  This drops the GetHashCode [DisallowNull] removal and the annotation refresh
  for those TFMs (netcoreapp3.x is EOL). The test sources make that line
  conditional; the README explains how to condition the PackageDownload when
  multi-targeting.
- tests/NuGet.Config: forward slashes in the local source path. With
  backslashes, package source mapping doesn't match that source on
  Linux/macOS and restore fails with NU1100.
- test.ps1: set NUGET_PACKAGES explicitly (-PackagesPath, default
  tests/.isolatednugetcache). An inherited NUGET_PACKAGES overrides
  globalPackagesFolder, so a machine-wide cache served a stale annotator
  package with the same version. CI passes its short path through it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants