This file is the single source of truth for AI/agent assistance in this repository (Claude Code, GitHub Copilot, and other coding agents). It consolidates build/test commands, architecture context, coding standards, and AOT guidance.
If there is any conflict between other agent instruction files and this file, follow CLAUDE.md.
- Repository root
- Primary working directory for build/test:
./src - Main solution:
src/reactiveui.slnx - Benchmarks solution:
Benchmarks/ReactiveUI.Benchmarks.sln - Integration tests:
integrationtests/(platform-specific solutions; not required for most tasks)
CRITICAL: Use a full, recursive clone. Shallow clones can fail because build/versioning relies on git history. If a clone has already been done you must use the unshallow commit command in git.
git clone --recursive https://github.com/reactiveui/reactiveui.gitThis repository uses SLNX (XML-based solution format) instead of legacy .sln.
- Introduced in Visual Studio 2022 17.10+
- Rider 2024.1+ support
- Works with
dotnet build/testthe same way.slndoes - Main file:
src/reactiveui.slnx
- .NET 8.0, 9.0, 10.0 SDKs (all required)
CRITICAL: Platform workloads must be restored or the build will fail. Run from the ./src directory.
dotnet --info
cd src
dotnet workload restore
cd ..CRITICAL: Run build/test commands from ./src unless the command explicitly uses src/-prefixed paths.
cd src
dotnet restore reactiveui.slnx
dotnet build reactiveui.slnx -c Release
dotnet build reactiveui.slnx -c Release -warnaserror
dotnet clean reactiveui.slnxBuilding the full solution requires Windows due to Windows-only target frameworks (WPF, WinUI, .NET Framework). Non-Windows builds may fail; this is expected. In non-Windows environments, focus on documentation, targeted library changes, or analysis that does not require full compilation.
The Windows-only test projects (ReactiveUI.Wpf.Tests, ReactiveUI.WinForms.Tests) can be built and run on Linux through Wine, giving a fast local feedback loop without a Windows VM or waiting on CI. This works because the Windows targeting/runtime packs restore cross-platform (EnableWindowsTargeting=true) and Wine can host the .NET Desktop runtime.
Treat results as a strong signal, not gospel: this is Wine, not Windows. CI on
windows-latestis authoritative. A green run here is high-confidence; investigate a red one before assuming a product bug.Known Wine limitation — dispatcher thread-affinity: Wine does not enforce WPF
Dispatcherthread affinity the way Windows does, soDispatcherObject.CheckAccess()can returntrueon a non-dispatcher thread. Tests that marshal work from a background thread (e.g.*FromBackgroundThread*, anything assertingDispatcher.BeginInvoke/scheduler hand-off afterDispatcherUtilities.DoEvents()) may behave differently under Wine than on Windows — a Wine pass or fail for those is not conclusive. Verify background-thread/marshalling tests on CI. Wine is reliable for the large majority of WPF tests that run on a single (dispatcher) thread.
Assemble a Windows .NET Desktop runtime (base runtime gives dotnet.exe + host/fxr + Microsoft.NETCore.App; the desktop pack adds Microsoft.WindowsDesktop.App for WPF/WinForms). Match the version to the net8 windows TFM (bump as the SDK moves):
VER=8.0.27
mkdir -p ~/wine-dotnet8 && cd ~/wine-dotnet8
curl -fsSL -o /tmp/dnr.zip https://builds.dotnet.microsoft.com/dotnet/Runtime/$VER/dotnet-runtime-$VER-win-x64.zip
curl -fsSL -o /tmp/wdr.zip https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/$VER/windowsdesktop-runtime-$VER-win-x64.zip
unzip -oq /tmp/dnr.zip # dotnet.exe + host/ + shared/Microsoft.NETCore.App
unzip -oq /tmp/wdr.zip # + shared/Microsoft.WindowsDesktop.App
export WINEPREFIX=~/.wine-rxui WINEARCH=win64
wineboot -i
wine ~/wine-dotnet8/dotnet.exe --list-runtimes # must list NETCore.App AND WindowsDesktop.AppThe UI test TFMs are gated to Windows in Directory.Build.props (ReactiveUITestingUITargets), so force a single Windows TFM with a global property. Clear obj/bin first — stale non-Windows assets break the WPF _wpftmp markup pass with NETSDK1005:
cd src
rm -rf tests/ReactiveUI.Wpf.Tests/obj tests/ReactiveUI.Wpf.Tests/bin
dotnet build tests/ReactiveUI.Wpf.Tests/ReactiveUI.Wpf.Tests.csproj -c Release \
-p:ReactiveUITestingUITargets=net8.0-windows10.0.19041.0 -p:CheckEolTargetFramework=falsecd src/tests/ReactiveUI.Wpf.Tests/bin/Release/net8.0-windows10.0.19041.0
export WINEPREFIX=~/.wine-rxui WINEARCH=win64 WINEDEBUG=-all
wine ~/wine-dotnet8/dotnet.exe ReactiveUI.Wpf.Tests.dll \
--treenode-filter "/*/*/*/ViewModelToViewBindingFromBackgroundThreadDoesNotTouchWpfControlDirectly"WINEDEBUG=-all silences fixme:/err: chatter; pipe through grep -viE 'fixme|^err:|wine:' if needed. WinForms tests work identically (ReactiveUI.WinForms.Tests, same TFM override).
This repo uses Microsoft Testing Platform (MTP) with TUnit. This differs from VSTest.
- MTP is configured via
global.json - Additional test settings in
testconfig.json - Test projects enable
TestingPlatformDotnetTestSupportinDirectory.Build.props
Key rule: TUnit/MTP arguments go after --.
- Do NOT use
--no-build. Always build before testing to avoid stale binaries. - To see test output, use
--output Detailedbefore--. - Repository configuration runs tests non-parallel (
"parallel": falseintestconfig.json) to avoid interference.
cd src
# Run all tests
dotnet test --solution reactiveui.slnx -c Release
# Run tests for a specific project
dotnet test --project tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj
# Run with code coverage (Microsoft Code Coverage)
dotnet test --solution reactiveui.slnx --coverage --coverage-output-format cobertura
# Detailed output (place BEFORE --)
dotnet test --solution reactiveui.slnx -- --output Detailed
dotnet test --solution reactiveui.slnx --coverage --coverage-output-format cobertura -- --report-trx --output Detailed
# List tests
dotnet test --project tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj -- --list-tests
# Fail fast
dotnet test --solution reactiveui.slnx -- --fail-fast
# Limit parallelism if needed (even though repo defaults non-parallel)
dotnet test --solution reactiveui.slnx -- --maximum-parallel-tests 4Pattern: /{AssemblyName}/{Namespace}/{ClassName}/{TestMethodName}
Examples:
# Single test
dotnet test --project tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj -- --treenode-filter "/*/*/*/MyTestMethod"
# All tests in class
dotnet test --project tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj -- --treenode-filter "/*/*/MyClassName/*"
# All tests in namespace
dotnet test --project tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj -- --treenode-filter "/*/MyNamespace/*/*"
# Filter by property (e.g., Category)
dotnet test --solution reactiveui.slnx -- --treenode-filter "/*/*/*/*[Category=Integration]"See: https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test?tabs=dotnet-test-with-mtp TUnit flags reference: https://tunit.dev/docs/reference/command-line-flags
src/global.json— sets"Microsoft.Testing.Platform"runnersrc/testconfig.json— test execution settings (parallel false, coverage format, etc.)src/Directory.Build.props— repository-wide build configuration (incl.TestingPlatformDotnetTestSupport).github/copilot-instructions.md— may exist, but should defer to thisagent.md
ReactiveUI is a cross-platform MVVM framework built on Rx.NET and functional reactive programming principles.
ReactiveObject/— reactiveINotifyPropertyChangedbaseReactiveCommand/— observable command pipelinesActivation/— view/viewmodel activation lifecycleBindings/— one-way/two-way binding infrastructureExpression/— expression tree analysis for observation (WhenAnyValue)Routing/— navigation/routingInteractions/— request/response patternsBuilder/— DI and service registration patterns
Examples:
ReactiveUI.Wpf/,ReactiveUI.WinUI/,ReactiveUI.Maui/,ReactiveUI.AndroidX/,ReactiveUI.Blazor/,ReactiveUI.Winforms/,ReactiveUI.Testing/, etc.
- Prefer
RxSchedulers(AOT-friendly, avoids reflection/AOT attribute propagation) - Use
RxApponly when required (e.g., unit test scheduler detection)
See docs/RxSchedulers.md.
This repository targets net8.0+ and supports AOT/trimming scenarios.
Prefer strongly-typed and source-generator-friendly approaches. Avoid reflection-heavy patterns that require trimming/AOT attributes.
- Avoid introducing DAC/RDC/RUC attributes unless required.
- If an attribute is required, apply it directly (no
#if NET6_0_OR_GREATERguards). Polyfills are available.
Example (only when truly needed):
private static object CreateInstance(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
Type type)
{
return Activator.CreateInstance(type)!;
}Do NOT add any suppression without explicit human approval. This covers [SuppressMessage], [UnconditionalSuppressMessage], #pragma warning, and .editorconfig severity changes. A suppression is a true last resort, used only when a warning genuinely cannot be resolved by fixing the code without harming the design.
Before suppressing anything:
- Fix the underlying issue first. Most analyzer warnings indicate a real fix. For example,
S3398("method should be moved") means move the method into the type that uses it — never suppress it.SA****(StyleCop) must always be fixed, never suppressed. - If you believe a suppression is genuinely unavoidable, stop and ask the human. Present the specific analyzer ID, why it cannot be fixed in code, and the proposed justification. Wait for explicit approval.
- Only after approval, apply it with minimal scope, the specific ID, and a clear
Justification.
CRITICAL: Follow ReactiveUI contribution guidelines: https://www.reactiveui.net/contribute/index.html
.editorconfigformatting/naming conventions- StyleCop analyzers (build fails on violations)
- Roslynator analyzers
- Analysis level: latest
- Warnings treated as errors (notably nullable and CS4014)
- Public APIs require XML documentation, including protected methods on public types.
- Allman braces
- 4 spaces, no tabs
- Explicit visibility
- Private/internal fields:
_camelCase,readonlywhere possible,static readonlyorder - File-scoped namespaces preferred; using directives outside namespace and sorted
- Use C# keywords (
int,string) rather than BCL types - Prefer modern C# features where appropriate (nullable, pattern matching, switch expressions, records, init, target-typed new, etc.)
- Use
nameof()over string literals - Avoid
this.unless necessary - Use
varwhen it improves readability
If a specific file already follows a local style, adhere to existing file conventions.
No #pragma warning disable in production code.
- StyleCop warnings (SA**) must be fixed**, never suppressed.
- No analyzer warning (CA**, S**** Sonar, RCS**** Roslynator, IL**** trimming/AOT, etc.) may be suppressed without explicit human approval** — see "Suppressions: Last Resort Only" above. Fix the code first; if a suppression seems unavoidable, stop and ask.
Example:
// WRONG
#pragma warning disable CA1062
public void MyMethod(object parameter)
{
parameter.ToString();
}
#pragma warning restore CA1062
// CORRECT
public void MyMethod(object parameter)
{
ArgumentNullException.ThrowIfNull(parameter);
parameter.ToString();
}
// LAST RESORT ONLY
[SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods",
Justification = "TUnit guarantees non-null parameters from data sources.")]
public async Task MyTest(IConverter converter, int expectedValue)
{
var result = converter.GetValue();
await Assert.That(result).IsEqualTo(expectedValue);
}-
Use TUnit + Microsoft Testing Platform
-
Write unit tests for new features and bug fixes
-
Prefer existing patterns in:
src/tests/ReactiveUI.Tests/src/tests/ReactiveUI.AOTTests/
-
Use
ReactiveUI.Testingutilities for reactive code
public class SampleViewModel : ReactiveObject
{
private string? _name;
private readonly ObservableAsPropertyHelper<bool> _isValid;
public SampleViewModel()
{
_isValid = this.WhenAnyValue(x => x.Name)
.Select(name => !string.IsNullOrWhiteSpace(name))
.ToProperty(this, nameof(IsValid));
SubmitCommand = ReactiveCommand.CreateFromTask(
ExecuteSubmit,
this.WhenAnyValue(x => x.IsValid));
}
public string? Name
{
get => _name;
set => this.RaiseAndSetIfChanged(ref _name, value);
}
public bool IsValid => _isValid.Value;
public ReactiveCommand<Unit, Unit> SubmitCommand { get; }
private async Task ExecuteSubmit(CancellationToken cancellationToken)
{
// Implementation
}
}public IObservable<string> GetData()
{
return Observable.Return("data")
.ObserveOn(RxSchedulers.MainThreadScheduler);
}this.WhenAnyValue(
x => x.FirstName,
x => x.LastName,
(first, last) => $"{first} {last}")
.Subscribe(fullName => { /* handle */ });
this.WhenAnyValue(x => x.IsLoading)
.Where(isLoading => !isLoading)
.Subscribe(_ => { /* handle */ });private readonly ObservableAsPropertyHelper<decimal> _total;
public decimal Total => _total.Value;
_total = this.WhenAnyValue(
x => x.Quantity,
x => x.Price,
(qty, price) => qty * price)
.ToProperty(this, nameof(Total));- Reflection-heavy implementations in core paths
- Expression trees in hot paths without caching
- Platform-specific code in
src/ReactiveUI/core library - Breaking public APIs without proper versioning and documentation