Skip to content

feat!: ship the attributes in a public library instead of generating them into each project - #503

Merged
glennawatson merged 4 commits into
mainfrom
feat/public-attributes-library
Sep 26, 2026
Merged

glennawatson merged 4 commits into
mainfrom
feat/public-attributes-library

Conversation

@glennawatson

@glennawatson glennawatson commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The attributes are public types in the package's own library, so the generators no longer declare them in every consuming project; task commands can also run in the background, and [BindableDerivedList] output no longer needs DynamicData.

  • [Reactive], [ReactiveCommand] and the other attributes, with the AccessModifier, PropertyAccessModifier and InheritanceModifier enums, ship in ReactiveUI.SourceGenerators.dll. It targets net8.0-net11.0 and net462-net481, keeps the existing namespaces, and is referenced by consumers from the package's lib/ folder.
  • The attributes are [Conditional], so a consuming assembly keeps no reference to the library. The generators read them from source; defining REACTIVEUI_SOURCEGENERATORS_KEEP_ATTRIBUTES keeps them in metadata.
  • The generators no longer register post-initialization output. Nothing is declared into a consumer's assembly.
  • The package bundles the generators and code fixes under analyzers/. The Roslyn 4.8, 4.14 and 5.0 builds are now ReactiveUI.SourceGenerators.Roslyn.dll, and each band carries the code fixes, which previously arrived only through a dependency that excluded its analyzers. Consumers now see RXUISG0016 and RXUISG0020.
  • [ReactiveCommand(RunInBackground = true)] starts a task-returning method with Task.Run. The command parameter and CancellationToken are passed through, so the code before the first await no longer runs on the calling thread; previously the option was ignored for tasks.
  • [ReactiveCommand(BackgroundScheduler = ...)] chooses the scheduler a synchronous background command runs on. It accepts a scheduler member or a built-in ReactiveUI scheduler, as OutputScheduler does, and implies RunInBackground.
  • [BindableDerivedList] output no longer opens with using DynamicData;. The generated property only exposes the field, so a project without DynamicData or System.Reactive can use it.

Why

Projects that share internals through InternalsVisibleTo failed with CS0436 when both used the generators, and the attribute namespace only existed when the generator ran.

  • Every consuming assembly received its own internal ReactiveUI.SourceGenerators.ReactiveAttribute and friends, so an assembly granted access to another's internals saw two copies of each.
  • Because the namespace was generated, an IDE that did not run the generator reported ReactiveUI.SourceGenerators as missing; it now comes from a referenced assembly.
  • The injected attributes also used init accessors, which do not compile on .NET Framework without an IsExternalInit polyfill.
  • Task commands had no way to leave the calling thread without wrapping each method in Task.Run, and the background scheduler could not be chosen.
  • [BindableDerivedList] forced a DynamicData reference, and with it System.Reactive, on projects that fill the collection by other means.

Closes #371
Closes #347
Closes #489

Breaking changes

The package is no longer a development dependency, because consumers compile against its attributes.

  • A PackageReference with an <IncludeAssets> line that omits compile (older installs added one) must drop that line: <PackageReference Include="ReactiveUI.SourceGenerators" Version="..." PrivateAssets="all" />.
  • The attributes' named properties have set accessors instead of init; attribute syntax is unchanged.
  • The code-fix analyzers now run in consuming projects, so misplaced [Reactive] attributes report RXUISG0020 as a warning.
  • A task-returning method with RunInBackground = true now starts on the thread pool; remove the option to keep it on the calling thread.

How this was verified

The generator tests compile against the attributes library, as a consumer does; new tests run generated task commands to check the thread they start on, and compile [BindableDerivedList] without DynamicData; the packed nupkg was exercised by hand.

Notes for the reviewer

Start with src/ReactiveUI.SourceGenerators/ and its project file, then ReactiveCommandGenerator.Execute.cs; most of the rest is mechanical.

  • Hand-written: the attribute sources and ReactiveUI.SourceGenerators.csproj, AttributeDefinitions.cs, the generators' Initialize methods, TestHelper.cs, and the command generator's WriteExecuteArgument and WriteSchedulerArguments.
  • Mechanical: the deleted *Attr.verified.cs and *AccMod.verified.cs snapshots of the injected files, the one-line using DynamicData; removal across the DERIVEDLIST snapshots, the project references added to the test, sample and benchmark projects, and the Roslyn projects' AssemblyName.
  • Subtle: ReactiveUI 24 has no CreateRunInBackground(execute, backgroundScheduler) overload, so a background scheduler without can-execute or output scheduler is written with canExecute: null, which also binds on ReactiveUI 23.
  • BackgroundScheduler does not apply to task-returning methods, which always start on the thread pool, or to observable-returning methods.
  • Out of scope: the separate ReactiveUI.SourceGenerators.Analyzers.CodeFixes package is still packable on its own; nothing depends on it any more.

Checklist

  • I have read the Contribute guide
  • The PR title follows Conventional Commits
  • Tests cover this change, or the summary says why they do not
  • New or changed public API has XML documentation

…them into each project

Every generator injected its attributes and enums into each consuming
project as internal types. Two projects that share internals through
InternalsVisibleTo then saw two copies of ReactiveUI.SourceGenerators.ReactiveAttribute
and failed with CS0436.

The attributes and the AccessModifier, PropertyAccessModifier and
InheritanceModifier enums are now public types in ReactiveUI.SourceGenerators.dll,
built for net8.0-net11.0 and net462-net481. They are [Conditional], so a
consuming assembly keeps no reference to it. The generators no longer
register post-initialization output.

The ReactiveUI.SourceGenerators project is now that library and the package
consumers install. The Roslyn 4.8, 4.14 and 5.0 builds are renamed
ReactiveUI.SourceGenerators.Roslyn.dll and bundled under analyzers/, each band
with the code fixes, which the package previously referenced only as a
dependency that excluded its analyzers.

BREAKING CHANGE: the package is no longer a development dependency, because
consumers compile against its attributes. A PackageReference whose
IncludeAssets omits compile must drop that line. The attributes' named
properties have set accessors instead of init.
@reactiveui reactiveui deleted a comment from coderabbitai Bot Sep 26, 2026
@codecov

codecov Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.45%. Comparing base (d2e6f72) to head (f4a0c8b).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #503      +/-   ##
==========================================
- Coverage   96.94%   96.45%   -0.50%     
==========================================
  Files          69       71       +2     
  Lines        3857     3578     -279     
  Branches      503      506       +3     
==========================================
- Hits         3739     3451     -288     
- Misses        118      127       +9     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The generated file opened with `using DynamicData;` although the property it
writes is fully qualified and only exposes the ReadOnlyObservableCollection<T>
field. A project without DynamicData, and so without System.Reactive, failed
with CS0246. The using is gone, so the field can be filled by any means.

Closes #489
…scheduler

[ReactiveCommand(RunInBackground = true)] now starts a task-returning method
with Task.Run, passing the command parameter and CancellationToken through,
so the code before its first await no longer runs on the calling thread. It
previously applied to synchronous methods only and was ignored for tasks.

BackgroundScheduler names the scheduler a synchronous background command runs
on: a scheduler member of the class or a built-in ReactiveUI scheduler, as for
OutputScheduler. Setting it implies RunInBackground. Without an output
scheduler or can-execute, the call names `canExecute: null`, since ReactiveUI
24 has no overload taking only the execute delegate and a background scheduler.

Closes #347
The generators read the attributes from source and a consuming assembly never
instantiates them, so no generator test ran their constructors. The tests
construct each attribute and check what it holds.
@glennawatson
glennawatson merged commit 2a33b09 into main Sep 26, 2026
11 of 12 checks passed
@glennawatson
glennawatson deleted the feat/public-attributes-library branch September 26, 2026 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants