Skip to content

Fix Ably.PubSub.Device on .NET Framework and add a package-consumption test - #1373

Merged
sacOO7 merged 6 commits into
integration/v2from
fix/netframework-device-internals-access
Nov 2, 2026
Merged

sacOO7 merged 6 commits into
integration/v2from
fix/netframework-device-internals-access

Conversation

@sacOO7

@sacOO7 sacOO7 commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

2.0 release blocker. Ably.PubSub.Device didn't work in a .NET Framework app: the first call to PubSubDevice.CreateClient(...) threw System.MethodAccessException. Ably.PubSub.Server worked in the same setup.

This PR:

  • fixes the bug with a two-line grant;
  • adds a package-consumption test that installs the packed .nupkg files into real apps, so this class of bug fails CI from now on.

Root cause

NuGet picks, per package, the asset closest to the app's target framework. For a net462–net48x app:

Package lib/ assets shipped Asset a .NET Framework app gets
Ably.PubSub.Core net46, netstandard2.0, net6.0, net7.0 net46 (from src/Ably.PubSub.Core.NETFramework)
Ably.PubSub.Server net46, netstandard2.0, net6.0, net7.0 net46
Ably.PubSub.Device netstandard2.0, net6.0, net7.0 (no net46) netstandard2.0

So a .NET Framework device app runs Device netstandard2.0 + Core net46. The door factories call the core's internal client constructors, which needs InternalsVisibleTo. The netstandard core grants it to both doors. The net46 core (src/Ably.PubSub.Core.NETFramework/Properties/AssemblyInfo.cs) granted it only to Ably.PubSub.Server, so the runtime refused Device's call.

CI missed it for two reasons:

  • every test builds against project references, which bypass NuGet's asset selection and the Package-only signing;
  • no .NET Framework test references Device.

Fix

src/Ably.PubSub.Core.NETFramework/Properties/AssemblyInfo.cs now grants Ably.PubSub.Device access, mirroring src/Ably.PubSub.Core/Properties/AssemblyInfo.cs:

  • a keyless grant for non-PACKAGE builds;
  • a keyed grant for PACKAGE builds, with the same PublicKey= as the existing Server grant (Ably.PubSub.snk). I checked it against sn -Tp on the packed Device assembly, and it matches.

Other places checked:

  • Unity / UNITY_PACKAGE grants: not needed in the NETFramework core. The Unity plugin is built from the netstandard core.
  • Ably.PubSub.Server.NETFramework: grants nothing, and nothing there needs changing.

A small unit guard, src/Ably.PubSub.Tests.Shared/CoreDoorAccessTests.cs, asserts that the core under test grants both doors. It lives in the shared test project, so it runs against both the netstandard/net6+ core (Tests.DotNET) and the net46 core (Tests.NETFramework).

New package-consumption test

New Cake targets in cake-build/tasks/package-consumption.cake:

  • Test.PackageConsumption: runs Package, then consumes the packages;
  • Test.PackageConsumption.NoPack: consumes packages that are already in --packageOutput.

What they do:

  1. Copy tests/PackageConsumption/{DeviceApp,ServerApp} to test-results/package-consumption.

  2. Restore through a generated nuget.config:

    • package source mapping takes Ably.PubSub.* only from the packed .nupkg files, and everything else from nuget.org;
    • packages go into a private global packages folder, so neither a stale cache nor a published package of the same version can stand in.
  3. Build each app for net462, net48, net6.0 and net7.0.

  4. Read obj/project.assets.json and assert:

    • the lib/<tfm> asset NuGet selected from each Ably package;
    • that each Ably package was restored from the local folder.
  5. Run each app. .NET Framework apps run on the .NET Framework CLR on Windows and on Mono elsewhere. Each app:

    • creates its clients through its door;
    • calls Channels.Get;
    • makes REST calls, which fail with AblyException because a stub handler fails them. The stub also records the Ably-Agent header, so the test checks the ably-pubsub-device / ably-pubsub-server side flag;
    • attempts a realtime connection to a closed localhost port and waits for Disconnected;
    • calls Close().

    Everything runs offline, with no credentials.

One place for expectations: the expected asset pair per app and framework is a single matrix at the top of package-consumption.cake. The run fails if a row names a lib/<tfm> that the nuspec doesn't declare, so the matrix can't silently drift from the nuspecs. If the TFM layout is restructured, update that matrix and the test validates the new layout.

Isolation: the test apps aren't in any .sln, they're outside src/ (so CleanSolution ignores them), and they add no nuspec to nuget/. The release pre-flight's exact three-nuspec check is unaffected.

Where it runs in CI:

  • release-dry-run.yml (windows-2022): a new "Consume the packed packages" step runs Test.PackageConsumption.NoPack against the dry-run-packages the job has just packed. The .NET Framework apps run on the Windows .NET Framework CLR. A release can't be cut from a commit whose packages don't work when installed.
  • run-tests-macos-mono.yml (macos-14): a new step runs the full Test.PackageConsumption, with the .NET Framework apps on Mono.
  • Not added to run-tests-windows-netframework.yml: it would repeat the dry run's full Windows package build, and add no coverage the dry-run step doesn't already give.

Supporting change: Package now works on macOS/Linux. Under Mono, MSBuild builds only netstandard2.0 of the SDK-style heads, because their csproj narrows TargetFrameworks when MSBuildRuntimeType is Mono. As a result _Release_Verify_Files failed on the missing net6.0/net7.0 assets. On non-Windows hosts, _Package_Build_All now also builds the Device and Server projects with the .NET SDK, using the same Package properties; the core builds through their project references. Windows, where releases are built, is unchanged.

Validation

Package-consumption matrix (macOS, Mono 6.12.0.188, .NET 6.0.29 / 7.0.18 runtimes; packages packed at the committed version 2.0.0)

App TFM Asset pair selected (from project.assets.json) Before fix After fix
DeviceApp net462 (Mono) Device lib/netstandard2.0 + Core lib/net46 FAIL MethodAccessException PASS
DeviceApp net48 (Mono) Device lib/netstandard2.0 + Core lib/net46 FAIL MethodAccessException PASS
DeviceApp net6.0 Device lib/net6.0 + Core lib/net6.0 PASS PASS
DeviceApp net7.0 Device lib/net7.0 + Core lib/net7.0 PASS PASS
ServerApp net462 (Mono) Server lib/net46 + Core lib/net46 PASS PASS
ServerApp net48 (Mono) Server lib/net46 + Core lib/net46 PASS PASS
ServerApp net6.0 Server lib/net6.0 + Core lib/net6.0 PASS PASS
ServerApp net7.0 Server lib/net7.0 + Core lib/net7.0 PASS PASS

How each column was produced:

  • Before: packages packed without the grant (this branch minus the AssemblyInfo.cs change), then run through Test.PackageConsumption.NoPack. The independent validator reproduced the same result by reverting only that file. The target exits 1 with the two Device .NET Framework failures:
    System.MethodAccessException: Method `Ably.PubSub.PubSubRealtimeClient..ctor(Ably.PubSub.ClientOptions)' is inaccessible from method `Ably.PubSub.Device.PubSubDevice.CreateClient(Ably.PubSub.ClientOptions)'
    
  • After: ./build.sh -- --target=Test.PackageConsumption --version=2.0.0 --packageOutput=<dir> passes all 8 cases.
  • Out-of-repo check: throwaway copies of the apps, built outside the repo with a hand-written nuget.config, gave the same results before and after.

Build and unit tests (macOS)

Command Result
./build.sh --target=Build.NetStandard Exit 0: 0 warnings, 0 errors
./build.sh --target=Build.NetFramework (Mono) Exit 0
./build.sh --target=Test.NetStandard.Unit.WithRetry --framework=net6.0 Exit 0: 1474 total, 1455 passed, 0 failed, 19 skipped. New CoreDoorAccessTests (Device, Server) pass
./build.sh --target=Test.NetFramework.Unit.WithRetry (Mono) Exit 0: 1442 total, 17 failed, 20 skipped. New CoreDoorAccessTests (Device, Server) pass against the net46 core. No failures caused by this branch: see below
./build.sh -- --target=Test.PackageConsumption --version=2.0.0 --packageOutput=<dir> Exit 0: 8/8 PASS (re-run after the final commit)

About the .NET Framework unit failures on Mono:

  • The same target on origin/integration/v2 fails 16 of 1440 tests on this Mac.
  • 14 failures are common to both runs. Most are Resource not found: ...test-app-setup.json / StatsInterval.json and AgentTests.PlatformRuntime_ShouldDetectCorrectRuntime, which are local Mono environment issues.
  • The failures seen on only one side passed 3/3 when re-run in isolation with xunit.console.exe -method, so they are load-sensitive flakes.

Independent validation

An independent Opus validator, separate from the code's author, worked in its own worktrees. Every check passed:

  • Matrix re-run: it re-packed the packages and re-ran the matrix (8/8 PASS), and parsed project.assets.json itself to confirm the asset pairs.
  • Negative test: reverting only AssemblyInfo.cs brings back the MethodAccessException for DeviceApp net462/net48, and the target exits 1.
  • Signed build: the keyed Device PublicKey in the packed net46 Core is byte-identical to the packed Device assembly's public key. All 11 packed Ably assemblies have token 70c9e5c3d2c68b16.
  • Builds and unit tests: results matched the table above. It also confirmed against its own origin/integration/v2 baseline that the .NET Framework failures predate this branch or are flaky.
  • Diff review: scope and correctness, no leakage into nuget/ or the solutions, CI wiring, StyleCop/EditorConfig on added lines, and commit messages all PASS.
  • Hardening: it suggested resolving dotnet.exe on Windows and setting ContinuousIntegrationBuild in the Unix SDK pack pass. Both are applied in the last commit.

CI on this PR

  • release-dry-run (windows-2022) passed, including the new "Consume the packed packages" step: 8/8 PASS, with the .NET Framework apps on the Windows .NET Framework CLR (Runtime: .NET Framework 4.0.30319.42000). Device net462/net48 now work on the real CLR.
  • The macOS workflows (macOS and macOS (Mono)) didn't run. GitHub rejected them before any step because of a scheduled brownout of the macos-14 runner image, which is retired on November 2, 2026 ([macOS] macOS 14 Sonoma retirement: GitHub Actions and ADO arm64 on November 2; ADO Intel on November 23 actions/runner-images#13518). Other branches hit the same failure today, and a re-run hit it again. The Mono consumption leg is validated locally (table above). Re-run those workflows once the brownout window ends.

Not verified locally

  • Windows .NET Framework CLR: not available on this Mac. It is covered by the release-dry-run result above.
  • net8.0+ consumer apps: the .NET 8 SDK isn't installed here. They would select the net7.0 assets, the same pair as the net7.0 row. Adding them is a one-line matrix change.

Follow-ups

  • Move the macOS workflows off macos-14 before November 2, 2026 (e.g. to macos-15). This affects run-tests-macos.yml and run-tests-macos-mono.yml on every branch, so it belongs in its own PR.
  • Unit-test failures don't fail CI (predates this PR, found by the validator): Test.NetFramework.Unit.WithRetry exits 0 even when tests fail.
    • cake-build/helpers/test-retry.cake looks for failures with the NUnit XPath //test-case[@success='False'], but the xunit v2 report uses <test result="Fail">. So it always finds "0 failed tests to retry", and the task swallows the first run's failure.
    • As a result, neither .NET Framework workflow goes red on a unit failure, and that includes the new net46 guard test. The package-consumption step does enforce this fix.
    • Worth a separate PR: parse //test[@result='Fail'] and fail the task if failures remain after the retry.
  • Optionally add net8.0/net10.0 rows to the matrix once the CI images' SDKs are pinned. They select the net7.0 assets today.
  • Manual pre-release checks against the sandbox: connect, publish/subscribe, history, presence, token auth. These need credentials, so this offline test doesn't cover them.
  • If the TFM layout is restructured (all three packages on the same target set), update the matrix in package-consumption.cake. The test then validates the new layout.

Reviewer notes

  • The packages are packed at the committed version (2.0.0), not a -local suffix: the release pre-flight inside Package requires --version to equal src/CommonAssemblyInfo.cs. Package source mapping and the private global packages folder stop the nuget.org 2.0.0 from ever being used instead, and the test asserts each package's .nupkg.metadata source.
  • The apps set RollForward=Major, so a net6.0 app also runs where only a newer runtime is installed. Asset selection happens at restore, so this doesn't affect what is tested.
  • Related context: Fix 2.0 public API findings: clear device-push errors, restore channel untilAttach, AuthCallback token strings and migration docs #1371 (2.0 public API review, where this issue was first found).

Summary by CodeRabbit

  • New Features

    • Added automated checks that verify published packages restore, build, and run correctly across supported .NET frameworks and platforms.
    • Added offline checks of package behavior, including client creation, connection handling, and request headers.
  • Documentation

    • Documented the package-consumption checks, supported workflows, and platform requirements.
  • Chores

    • Updated macOS test workflows to use macOS 15.

A .NET Framework app that installs Ably.PubSub.Device gets the Device
netstandard2.0 asset (the Device package has no net46 asset) together with
the Core net46 asset. The net46 core granted InternalsVisibleTo only to
Ably.PubSub.Server, so PubSubDevice.CreateClient threw MethodAccessException
when it called the internal PubSubRealtimeClient constructor.

Add the keyless and keyed (Ably.PubSub.snk) Device grants, mirroring the
netstandard core, and a shared unit test asserting that each core build
grants both doors.
Under Mono MSBuild the SDK-style heads build only netstandard2.0, so a Unix
Package run failed _Release_Verify_Files on the missing net6.0 and net7.0
assets. Build the door projects (and through them the core) with the .NET
SDK as well, with the same Package properties. Windows is unchanged.
Test.PackageConsumption packs the three packages, restores small Device and
Server apps from only those .nupkg files (plus nuget.org for third-party
dependencies), asserts the lib/<tfm> asset NuGet selected for each Ably
package from project.assets.json, and runs each app on net462, net48,
net6.0 and net7.0. The apps create clients through their door and call
into the core offline. Test.PackageConsumption.NoPack does the same against
packages that are already packed.

The expected asset pairs live in one matrix in package-consumption.cake.
The release dry run consumes the packages it has just packed, with the
.NET Framework apps on the Windows CLR. The macOS Mono workflow packs and
consumes them with the .NET Framework apps on Mono.
The package-consumption runner now resolves dotnet.exe by name on Windows
rather than relying on the PATH fallback, and the Unix .NET SDK pass of
_Package_Build_All sets ContinuousIntegrationBuild the way the MSBuild pass
does.
@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f9eddc2c-7b42-4f2f-93c4-d71408154c43
Commits

Reviewing files that changed from the base of the PR and between 457848c and d81c52d.

Files selected for processing: 16
  • .github/workflows/release-dry-run.yml
  • .github/workflows/run-tests-macos-mono.yml
  • .github/workflows/run-tests-macos.yml
  • cake-build/README.md
  • cake-build/build.cake
  • cake-build/tasks/package-consumption.cake
  • cake-build/tasks/package.cake
  • src/Ably.PubSub.Core.NETFramework/Properties/AssemblyInfo.cs
  • src/Ably.PubSub.Tests.Shared/Ably.PubSub.Tests.Shared.projitems
  • src/Ably.PubSub.Tests.Shared/CoreDoorAccessTests.cs
  • tests/PackageConsumption/DeviceApp/DeviceApp.csproj
  • tests/PackageConsumption/DeviceApp/Program.cs
  • tests/PackageConsumption/Directory.Build.props
  • tests/PackageConsumption/ServerApp/Program.cs
  • tests/PackageConsumption/ServerApp/ServerApp.csproj
  • tests/PackageConsumption/Shared/ConsumptionChecks.cs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Walkthrough

This change adds Device and Server package-consumption apps and Cake targets that restore, build, validate, and run them across .NET Framework and .NET target frameworks. It also updates package build support, adds an internal-access test, and runs the package checks in release and macOS workflows.

Changes

Package Consumption Testing

Layer / File(s) Summary
Package build support
cake-build/tasks/package.cake, src/Ably.PubSub.Core.NETFramework/Properties/AssemblyInfo.cs, src/Ably.PubSub.Tests.Shared/*
Non-Windows package builds now build the Device and Server project heads. Core grants internal access to both door assemblies, and a shared test checks those grants.
Consumer apps and offline checks
tests/PackageConsumption/Directory.Build.props, tests/PackageConsumption/DeviceApp/*, tests/PackageConsumption/ServerApp/*, tests/PackageConsumption/Shared/ConsumptionChecks.cs
The Device and Server apps reference their respective packages. Shared checks cover channel access, expected offline REST behavior, REST agent headers, and realtime connection states.
Cake restore, validation, and execution
cake-build/build.cake, cake-build/tasks/package-consumption.cake
Cake adds package-consumption targets that restore and build the apps, validate package declarations, selected assets, and package sources, then run the apps for each framework case.
Workflow and usage integration
.github/workflows/release-dry-run.yml, .github/workflows/run-tests-macos*.yml, cake-build/README.md
The release dry-run and Mono workflows invoke package-consumption tests. The macOS workflows use macos-15, and the README documents the test targets and framework matrix.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Cake as Cake package-consumption target
  participant NuGet as NuGet restore and build
  participant Apps as DeviceApp and ServerApp
  participant Checks as ConsumptionChecks
  Cake->>NuGet: Restore apps from local package source
  NuGet->>Apps: Build for selected target frameworks
  Cake->>Cake: Validate package assets and restored sources
  Cake->>Apps: Run each app on its target runtime
  Apps->>Checks: Run offline package-consumption checks
  Checks-->>Apps: Return pass or failure result
Loading

Suggested reviewers: ttypic

Merge Risk: Minimal · up to d81c5

The package-consumption change is mergeable after normal checks; the investigated build dependency is satisfied by the preceding restore.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title names two real changes in the PR: the .NET Framework Device fix (InternalsVisibleTo grants for Ably.PubSub.Device in the Core assembly) and the new package-consumption test (Cake targets and…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title names two real changes in the PR: the .NET Framework Device fix (InternalsVisibleTo grants for Ably.PubSub.Device in the Core assembly) and the new package-consumption test (Cake targets and sample apps). It omits the macOS runner moves, the Unix SDK build change, and the release workflow wiring, but those are secondary to the two named changes. The title is specific and matches the main objectives.

Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 9 functions across 5 files. (11 skipped: 11 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

A rabbit checks each package seal,
Then hops through frameworks, row by row.
The apps test headers, links, and states,
While Cake records the checks below.
The packed-up paths are ready to go!

@ttypic ttypic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@sacOO7

sacOO7 commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

GitHub retires the macos-14 runner image on 2 November 2026, and its
scheduled brownouts were already cancelling these jobs before any step
ran. Move both macOS workflows to macos-15 so the .NET test legs and the
Mono leg, including the package-consumption step, run again.

This branch was successfully deployed

1 active deployment
staging/pull/1373/features — d81c52d6 Deployed Nov 2, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants