This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
- NEVER abandon work halfway through - if something gets difficult, push through it
- NEVER use
git stashto hide incomplete work - fix the problem directly - NEVER give up because a task is complex - break it down and keep going
- If a tool call is rejected, adapt your approach immediately and continue
This project uses Microsoft Testing Platform (MTP) with the TUnit testing framework. Test commands differ significantly from traditional VSTest.
See: https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test?tabs=dotnet-test-with-mtp
# Check .NET installation (.NET 8.0, 9.0, and 10.0 required)
dotnet --info
# Restore NuGet packages
cd src
dotnet restore ReactiveUI.Extensions.slnxNote: This project uses the modern .slnx (XML-based solution file) format instead of the legacy .sln format.
CRITICAL: The working folder must be ./src folder. These commands won't function properly without the correct working folder.
# Build the solution
dotnet build ReactiveUI.Extensions.slnx -c Release
# Build with warnings as errors (includes StyleCop violations)
dotnet build ReactiveUI.Extensions.slnx -c Release -warnaserror
# Clean the solution
dotnet clean ReactiveUI.Extensions.slnxCRITICAL: This repository uses MTP configured in testconfig.json. All TUnit-specific arguments must be passed after --:
The working folder must be ./src folder.
IMPORTANT:
- Do NOT use
--no-buildflag when running tests. Always build before testing to ensure all code changes are compiled. - Use
--output Detailedto see Console.WriteLine output from tests (place BEFORE any--separator).
# Run all tests in the solution
dotnet test --solution ReactiveUI.Extensions.slnx -c Release
# Run all tests in a specific project
dotnet test --project tests/ReactiveUI.Extensions.Tests/ReactiveUI.Extensions.Tests.csproj -c Release
# Run a single test method using treenode-filter
dotnet test --project tests/ReactiveUI.Extensions.Tests/ReactiveUI.Extensions.Tests.csproj -- --treenode-filter "/*/*/*/MyTestMethod"
# Run all tests in a specific class
dotnet test --project tests/ReactiveUI.Extensions.Tests/ReactiveUI.Extensions.Tests.csproj -- --treenode-filter "/*/*/ParityOperatorTests/*"
# Run tests with code coverage
dotnet test --solution ReactiveUI.Extensions.slnx -- --coverage --coverage-output-format coberturaThe --treenode-filter follows the pattern: /{AssemblyName}/{Namespace}/{ClassName}/{TestMethodName}
- Single test:
--treenode-filter "/*/*/*/MyTestMethod" - All tests in class:
--treenode-filter "/*/*/MyClassName/*" - Use single asterisks (
*) to match segments.
src/ReactiveUI.Extensions.slnx- Modern XML-based solution filesrc/testconfig.json- Configures test execution and code coveragesrc/Directory.Build.props- Common build properties, package metadata, target frameworkssrc/Directory.Packages.props- Central package managementsrc/Directory.Build.targets- Build targets
Code coverage uses Microsoft.Testing.Extensions.CodeCoverage configured in src/testconfig.json. Coverage is collected for production assemblies only (test projects are excluded).
Note: If a code coverage MCP server is available, prefer using it over manual report generation — it is far more efficient.
# Run tests with code coverage (from src/ folder).
dotnet test --solution ReactiveUI.Extensions.slnx -c Release -- --coverage --coverage-output-format cobertura
# Generate HTML report using ReportGenerator (install if needed: dotnet tool install -g dotnet-reportgenerator-globaltool)
reportgenerator \
-reports:"tests/ReactiveUI.Extensions.Tests/**/TestResults/**/*.cobertura.xml" \
-targetdir:/tmp/code_coverage \
-reporttypes:"Html;TextSummary"
# View the text summary
cat /tmp/code_coverage/Summary.txt
# Open HTML report in browser
xdg-open /tmp/code_coverage/index.html # Linux
open /tmp/code_coverage/index.html # macOSKey configuration (src/testconfig.json):
Format: "cobertura"— cobertura output for ReportGeneratorCodeCoverage.SkipAutoProperties: true— auto-properties excluded from coverage metricsCodeCoverage.ModulePaths.Exclude:.*Tests\\.dll$,.*TestRunner.*— excludes test/runner assembliesCodeCoverage.Functions.Exclude:.*__.*— single generic pattern excluding compiler- generated names (async state machines<X>d__N, lambda methods<X>b__N_M, local-function state machines<<X>g__Helper|N_M>d, and closure types<>c__DisplayClass*). None of our user code uses double underscores, so the pattern is unambiguous.- Race-only methods (Throttle.Emit, ThrottleDistinct.Emit, Result.TryThrow's post-Throw
closing brace) are marked
[ExcludeFromCodeCoverage]in source — the default Attributes.Exclude honors that attribute.
Tips:
- Always clean
bin/andobj/folders before coverage runs to avoid stale results - Put coverage reports in
/tmp/to avoid accidentally committing them
ReactiveUI.Extensions provides a focused collection of high-value Reactive Extensions (Rx) operators that complement System.Reactive, plus a fully async-native IObservableAsync<T> pipeline with its own set of operators, subjects, and coordination primitives.
src/
├── ReactiveUI.Extensions/ # Main library (net8.0;net9.0;net10.0;net462;net472;net481)
│ ├── ReactiveExtensions.cs # Synchronous Rx operator extensions
│ ├── Async/ # Async-native observable pipeline
│ │ ├── IObservableAsync.cs # Core async observable interface
│ │ ├── IObserverAsync.cs # Core async observer interface
│ │ ├── ObservableAsync.cs # Factory methods (Create, Return, Empty, etc.)
│ │ ├── Operators/ # Async operator implementations
│ │ │ ├── ParityHelpers.cs # Parity helpers (AsSignal, CatchIgnore, Pairwise, etc.)
│ │ │ ├── CombineLatestEnumerable.cs # CombineLatest for IEnumerable<IObservableAsync<T>>
│ │ │ └── ... # Select, Where, Scan, Throttle, etc.
│ │ ├── Subjects/ # Async subjects (SubjectAsync, BehaviorSubject, etc.)
│ │ ├── Disposables/ # Async disposable primitives
│ │ └── Internals/ # AsyncGate, Result, Optional<T>, etc.
│ └── Internal/ # Internal helpers (ArgumentExceptionHelper, Heartbeat, etc.)
│
└── tests/
└── ReactiveUI.Extensions.Tests/ # Unit tests (net8.0;net9.0;net10.0)
├── Async/ # Async operator tests
│ ├── ParityOperatorTests.cs
│ ├── ParityAndInfrastructureCoverageTests.cs
│ ├── CombineLatestOperatorTests.cs
│ └── ...
└── ReactiveExtensionsTests.cs # Sync operator tests
- Library:
net8.0;net9.0;net10.0;net462;net472;net481 - Tests:
net8.0;net9.0;net10.0only EnableWindowsTargetingis set so .NET Framework targets compile on all platforms (Linux, macOS, Windows)
- Async operators use
extension<T>syntax (C# preview feature) for instance-style extension methods ObservableAsyncpartial class — operators are split across files, all contributing to the same partial classstaticlambdas used throughout for zero-allocation delegates where no captures are needed- Manual
forloops overIReadOnlyList<T>preferred over LINQ in hot paths to avoid enumerator allocations IReadOnlyList<T>preferred overIList<T>for snapshot/immutable collection return types
CRITICAL: All code must comply with ReactiveUI contribution guidelines: https://www.reactiveui.net/contribute/index.html
- EditorConfig rules (
.editorconfig) - StyleCop Analyzers - builds fail on violations
- Roslynator Analyzers - additional code quality rules
- All public APIs require XML documentation comments
The full coding-style guide — Allman braces, _camelCase privates,
file-scoped namespaces, expression-bodied members, modern pattern
matching, every other Visual-Studio-default tweak — lives in
CONTRIBUTING.md. Follow that document when writing
or editing code in this repo. The performance rules below extend those
style rules; when the two could disagree, the perf rule wins inside
src/ReactiveUI.Extensions/.
- Braces: Allman style
- Indentation: 4 spaces, no tabs
- Fields:
_camelCasefor private/internal - Visibility: Always explicit, visibility first modifier
- Namespaces: File-scoped preferred
- Modern C#: Nullable reference types, pattern matching,
staticlambdas, collection expressions - Language version:
preview(enablesextension<T>syntax and latest features) - US English in identifiers, XML docs, comments, log messages, commit messages
These rules apply to production code (everything under
src/ReactiveUI.Extensions/). Test projects
(src/tests/ReactiveUI.Extensions.Tests/) are exempt from the allocation-
discipline rules — foreach, LINQ, and capacity-less List<T> are fine
in tests where readability beats micro-optimization. The pattern-
matching, switch-expression, and list-pattern rules still apply to tests
because they're style, not perf.
System.Reactiveis allowed only for BCL-level contracts —IObservable<T>,IObserver<T>,IScheduler,CompositeDisposable,SerialDisposable,SingleAssignmentDisposable,Unit,Notification<T>. Every operator we ship (Select,Where,CombineLatest,Merge,Throttle,Scan, etc.) is our own implementation underOperators/(sync) orAsync/Operators/(async).ISchedulerandUnitare unavoidable parts of Rx — do not try to replace them.IScheduleris the canonical scheduling abstraction the entire Rx ecosystem (and ourIObservable<T>consumers) interop through; rolling our own scheduler interface would fragment that contract for zero benefit.Unitis the standard "no value" token every Rx signal stream uses. Both are foundational BCL-level contracts: depend on them directly, pass them through, and don't author parallel substitutes. This is the one place the "don't rebadge" rule inverts — here the right move is to reuse theSystem.Reactivetype, not replace it.System.Reactive.Linq.Observable.*is banned in production code paths outside thin BCL bridges. If a feature feels like it needsObservable.Foo, add our own operator with the allocation profile we want.- The async pipeline is fully independent —
IObservableAsync<T>/IObserverAsync<T>/ObservableAsyncand the async subjects / disposables do not depend onSystem.Reactivefor operator semantics. Crossings into / out ofIObservable<T>live inAsync/Bridge/and are the only sanctioned bridges. - Don't rebadge
System.Reactivetypes 1:1. Replacements must be tailored, low-allocation, perf-focused, and only as thread-aware as needed. A subject that mirrorsSubject<T>in shape without specializing the locking, snapshot semantics, or async observer contract is worse than not writing it.
- Zero-LINQ policy in production code. No
System.Linqundersrc/ReactiveUI.Extensions/. Use plainforloops. foroverforeach. Indexedforover arrays /Span<T>/ReadOnlySpan<T>/IReadOnlyList<T>/List<T>.foreachonly when the type genuinely lacks an indexer (HashSet<T>,IAsyncEnumerable<T>).- Arrays over
List<T>when the final length is known up front. WhenList<T>is unavoidable, always pass a capacity —new List<T>(expectedCount), never capacity-less. IReadOnlyList<T>for snapshot return types, notIList<T>.- Avoid
ImmutableArray<T>/ImmutableList<T>on hot paths. The wrapping struct adds an indirection on every read. - Collection expressions
[..]first.[a, b, ..tail],[],[..source]overnew[] { ... }, ad-hoc array construction, and.ToArray(). staticlambdas wherever no capture is needed.static (state, x) => ...— pass captured data through a tuple-state argument, not closure capture.- Pre-size
Dictionary/HashSetwith a capacity hint. - Pool transient buffers.
ArrayPool<T>.Shared.Rentpaired with atry/finallyReturn. SearchValues<T>for repeated multi-character searches; cache asprivate static readonly.FrozenDictionary/FrozenSetonly when a table is built once at startup and read many times across pages / workers. Don't reach forFrozen*on per-subscription / per-instance / short-lived tables — the freeze cost dominates.StringComparer.Ordinal/StringComparison.Ordinalon every dictionary / set /string.Equalskeyed on identifiers, type names, operator names. Culture-aware is wrong and 5-10× slower.
ConfigureAwait(false)on every libraryawait. No exceptions in production code.- No sync-over-async — never
.GetAwaiter().GetResult(),.Result, or.Wait()inside async operators. ValueTaskfirst when zero-alloc is proven;Taskotherwise. Default return type on a new async-pipeline contract isValueTaskwhenever most implementations complete synchronously and the call site multiplies (per-emission). UseTaskonly when the path is genuinely async-dominant. Obey the consume-once rule.- Sync impls return
ValueTask.CompletedTask/Task.CompletedTask. No state machine, no allocation. - Cancellation flows through. Async operators accept a
CancellationTokenand pass it down; never swallow, never default toCancellationToken.Nonewhen a real one is in scope. CancellationTokenSource.CreateLinkedTokenSourcebelongs at subscribe time, not per-emission.Interlocked.Increment/Interlocked.Decrementfor simple counters under contention. Reservelockfor genuine multi-field invariants.System.Threading.Lock(NET9+) is the default monitor primitive for new code. Older TFMs fall back to aprivate readonly object _gate = new();— never lock onthis,typeof(X), or public fields.
- Invert
ifs to flatten the happy path. Guard clauses + earlyreturn/continue; noelseon guarded branches. - Switch expressions over
if/elsechains with property, positional, and list patterns. - List patterns for emptiness / cardinality.
is [_, ..]over.Count > 0;is []for empty;is [var single]to bind a singleton. is/is notpatterns over==/!=for null and type checks.- Avoid
while (true). Termination condition in the loop header unless the work is genuinely infinite (cancellation-bound pump).
- No default parameter values. Provide explicit overloads. Defaults bake into every caller's call-site IL and obscure refactors.
- Concrete collection types where practical —
IReadOnlyList<T>/T[]/Dictionary<K,V>/HashSet<T>overIEnumerable<T>for parameters and returns.IEnumerable<T>only when streaming genuinely avoids materializing the full sequence. - Pin the latest non-beta NuGet version when adding to
Directory.Packages.props; never-preview/-rc/-alpha/-beta.
- C# 14
fieldkeyword by default when a property needs a backing field with extra logic. Reach for an explicit_namefield only forref-passing APIs (Interlocked,Volatile,Unsafe.As), constructor bypass, or when storage is referenced outside the accessors. Document the reason with a one-line comment.
- Exception helpers compose their own messages.
ThrowIfNull-style helpers use[CallerArgumentExpression]+[CallerMemberName]internally. Call sites pass only the value — never thenameof.
sealedevery class that isn't designed for inheritance.readonly record structfor small immutable shapes (≤ 4-5 fields or only references).- Most methods static. A method that doesn't touch
thisisstatic. Reserve instance methods for outer-layer types holding per-instance state. Static-only classes getstatic class. - Singleton comparers (
public static readonly XComparer Instance) instead of allocating a fresh comparer / lambda per call. - Bundle long parameter lists into a
readonly record structorref structrather than splitting the method. - One type per file.
- Fix the code, don't silence the rule. Refactor the call site rather than reaching for a suppression. Almost every analyzer hit has a structural fix — pull a helper out, invert a guard, change a return type, drop a defensive null check, restructure the throw — that's preferable to silencing the rule.
#pragma warning disableis banned everywhere in this repo — production, tests, benchmarks, samples. There is no per-line escape hatch. If a rule fires, restructure the code. The only exception is generated files (*.g.cs,obj/), which we do not edit.[SuppressMessage]requires explicit human consultation. Do not add a new[SuppressMessage]without approval. When approved, the attribute must carry a per-symbolJustificationline that names the concrete reason (a past incident, a constraint, a language-version quirk). Treat each occurrence as a small debt — a second hit on the same rule usually means the design is wrong; fix that instead. Do not invent new[SuppressMessage]attributes to bypass an analyzer error during a Claude session; restructure, or stop and ask.- Zero
<NoWarn>policy. Project-wide<NoWarn>entries in.csproj/.props/.targetsare not acceptable without explicit human consultation and are unlikely to be approved. There are currently zero<NoWarn>entries in this repo; do not add the first one. If a rule is broadly wrong for the project, fix the rule's call sites or escalate before disabling it at the project level. - SA1201 from
extension<T>is the one accepted global false positive and is handled via the per-symbol pattern, not a<NoWarn>. Do not invent new project-wide suppressions on the same rule for other reasons.
- Prefer real implementations over mocks in integration tests. Mocked tests passing while a real implementation breaks is the failure mode we want to avoid.
- LINQ in production code paths - use manual
forloops over indexed collections System.Reactive.Linq.Observable.*operators in production - use our own operatorsIList<T>for return types - useIReadOnlyList<T>for immutable snapshotsList<T>when size is known - use arrays directly- Locking on arbitrary objects - use dedicated
Lock(NET9+) orobjectlock fields CancellationTokenSource.CreateLinkedTokenSourcein hot paths - create once at subscription time, not per-emission- Mocking in integration tests - prefer real implementations
- Default parameter values - use explicit overloads
- Capacity-less
new List<T>()- always pass an expected-count capacity
Use the Conventional Commits 1.0.0 shape on every commit. Full type table and worked examples (including the perf shape with benchmark numbers in the body) are in CONTRIBUTING.md.
- Required .NET SDKs: .NET 8.0, 9.0, and 10.0
- Library targets: net8.0;net9.0;net10.0;net462;net472;net481
- Test targets: net8.0;net9.0;net10.0
- No shallow clones: Repository requires full clone for MinVer (it walks history to the most recent
v*tag to compute the version) - Versioning: MinVer derives the version from the most recent
v*git tag. Untagged commits build as{floor}.0-alpha.0.{height}(floor lives insrc/Directory.Build.propsasMinVerMinimumMajorMinor). Releases tagvX.Y.Zand the build picks it up automatically. - SA1201 warning: The
extension<T>preview syntax causes a false-positive SA1201 from StyleCop — this is expected and unavoidable