|
| 1 | +# agent.md |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +If there is any conflict between other agent instruction files and this file, follow **agent.md**. |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## Repository Orientation |
| 10 | + |
| 11 | +- **Repository root** |
| 12 | +- **Primary working directory for build/test:** `./src` |
| 13 | +- **Main solution:** `src/reactiveui.slnx` |
| 14 | +- **Benchmarks solution:** `Benchmarks/ReactiveUI.Benchmarks.sln` |
| 15 | +- **Integration tests:** `integrationtests/` (platform-specific solutions; not required for most tasks) |
| 16 | + |
| 17 | +### Full Clone Required |
| 18 | + |
| 19 | +**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. |
| 20 | + |
| 21 | +```bash |
| 22 | +git clone --recursive https://github.com/reactiveui/reactiveui.git |
| 23 | +```` |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## Solution Format: SLNX |
| 28 | + |
| 29 | +This repository uses **SLNX** (XML-based solution format) instead of legacy `.sln`. |
| 30 | + |
| 31 | +* Introduced in Visual Studio 2022 17.10+ |
| 32 | +* Rider 2024.1+ support |
| 33 | +* Works with `dotnet build/test` the same way `.sln` does |
| 34 | +* Main file: `src/reactiveui.slnx` |
| 35 | + |
| 36 | +--- |
| 37 | + |
| 38 | +## Build Environment Requirements |
| 39 | + |
| 40 | +### Required SDKs |
| 41 | + |
| 42 | +* .NET **8.0**, **9.0**, **10.0** SDKs (all required) |
| 43 | + |
| 44 | +### Workload Restore (Required) |
| 45 | + |
| 46 | +**CRITICAL:** Platform workloads must be restored or the build will fail. Run from the `./src` directory. |
| 47 | + |
| 48 | +```powershell |
| 49 | +dotnet --info |
| 50 | +
|
| 51 | +cd src |
| 52 | +dotnet workload restore |
| 53 | +cd .. |
| 54 | +``` |
| 55 | + |
| 56 | +### Restore & Build |
| 57 | + |
| 58 | +**CRITICAL:** Run build/test commands from `./src` unless the command explicitly uses `src/`-prefixed paths. |
| 59 | + |
| 60 | +```powershell |
| 61 | +cd src |
| 62 | +
|
| 63 | +dotnet restore reactiveui.slnx |
| 64 | +
|
| 65 | +dotnet build reactiveui.slnx -c Release |
| 66 | +dotnet build reactiveui.slnx -c Release -warnaserror |
| 67 | +
|
| 68 | +dotnet clean reactiveui.slnx |
| 69 | +``` |
| 70 | + |
| 71 | +### Windows Requirements |
| 72 | + |
| 73 | +Building 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. |
| 74 | + |
| 75 | +--- |
| 76 | + |
| 77 | +## Testing: Microsoft Testing Platform (MTP) + TUnit |
| 78 | + |
| 79 | +This repo uses **Microsoft Testing Platform (MTP)** with **TUnit**. This differs from VSTest. |
| 80 | + |
| 81 | +* MTP is configured via `global.json` |
| 82 | +* Additional test settings in `testconfig.json` |
| 83 | +* Test projects enable `TestingPlatformDotnetTestSupport` in `Directory.Build.props` |
| 84 | + |
| 85 | +**Key rule:** TUnit/MTP arguments go **after** `--`. |
| 86 | + |
| 87 | +### Testing Best Practices |
| 88 | + |
| 89 | +* **Do NOT use `--no-build`**. Always build before testing to avoid stale binaries. |
| 90 | +* To see test output, use `--output Detailed` **before** `--`. |
| 91 | +* Repository configuration runs tests **non-parallel** (`"parallel": false` in `testconfig.json`) to avoid interference. |
| 92 | + |
| 93 | +### Test Commands (run from `./src`) |
| 94 | + |
| 95 | +```powershell |
| 96 | +cd src |
| 97 | +
|
| 98 | +# Run all tests |
| 99 | +dotnet test --solution reactiveui.slnx -c Release |
| 100 | +
|
| 101 | +# Run tests for a specific project |
| 102 | +dotnet test --project tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj |
| 103 | +
|
| 104 | +# Run with code coverage (Microsoft Code Coverage) |
| 105 | +dotnet test --solution reactiveui.slnx --coverage --coverage-output-format cobertura |
| 106 | +
|
| 107 | +# Detailed output (place BEFORE --) |
| 108 | +dotnet test --solution reactiveui.slnx -- --output Detailed |
| 109 | +dotnet test --solution reactiveui.slnx --coverage --coverage-output-format cobertura -- --report-trx --output Detailed |
| 110 | +
|
| 111 | +# List tests |
| 112 | +dotnet test --project tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj -- --list-tests |
| 113 | +
|
| 114 | +# Fail fast |
| 115 | +dotnet test --solution reactiveui.slnx -- --fail-fast |
| 116 | +
|
| 117 | +# Limit parallelism if needed (even though repo defaults non-parallel) |
| 118 | +dotnet test --solution reactiveui.slnx -- --maximum-parallel-tests 4 |
| 119 | +``` |
| 120 | + |
| 121 | +### TUnit `--treenode-filter` Syntax |
| 122 | + |
| 123 | +Pattern: `/{AssemblyName}/{Namespace}/{ClassName}/{TestMethodName}` |
| 124 | + |
| 125 | +Examples: |
| 126 | + |
| 127 | +```powershell |
| 128 | +# Single test |
| 129 | +dotnet test --project tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj -- --treenode-filter "/*/*/*/MyTestMethod" |
| 130 | +
|
| 131 | +# All tests in class |
| 132 | +dotnet test --project tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj -- --treenode-filter "/*/*/MyClassName/*" |
| 133 | +
|
| 134 | +# All tests in namespace |
| 135 | +dotnet test --project tests/ReactiveUI.Tests/ReactiveUI.Tests.csproj -- --treenode-filter "/*/MyNamespace/*/*" |
| 136 | +
|
| 137 | +# Filter by property (e.g., Category) |
| 138 | +dotnet test --solution reactiveui.slnx -- --treenode-filter "/*/*/*/*[Category=Integration]" |
| 139 | +``` |
| 140 | + |
| 141 | +See: [https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test?tabs=dotnet-test-with-mtp](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test?tabs=dotnet-test-with-mtp) |
| 142 | +TUnit flags reference: [https://tunit.dev/docs/reference/command-line-flags](https://tunit.dev/docs/reference/command-line-flags) |
| 143 | + |
| 144 | +--- |
| 145 | + |
| 146 | +## Key Configuration Files |
| 147 | + |
| 148 | +* `src/global.json` — sets `"Microsoft.Testing.Platform"` runner |
| 149 | +* `src/testconfig.json` — test execution settings (parallel false, coverage format, etc.) |
| 150 | +* `src/Directory.Build.props` — repository-wide build configuration (incl. `TestingPlatformDotnetTestSupport`) |
| 151 | +* `.github/copilot-instructions.md` — may exist, but should defer to this `agent.md` |
| 152 | + |
| 153 | +--- |
| 154 | + |
| 155 | +## Architecture Overview |
| 156 | + |
| 157 | +ReactiveUI is a cross-platform MVVM framework built on Rx.NET and functional reactive programming principles. |
| 158 | + |
| 159 | +### Core Library (`src/ReactiveUI/`) |
| 160 | + |
| 161 | +* `ReactiveObject/` — reactive `INotifyPropertyChanged` base |
| 162 | +* `ReactiveCommand/` — observable command pipelines |
| 163 | +* `Activation/` — view/viewmodel activation lifecycle |
| 164 | +* `Bindings/` — one-way/two-way binding infrastructure |
| 165 | +* `Expression/` — expression tree analysis for observation (`WhenAnyValue`) |
| 166 | +* `Routing/` — navigation/routing |
| 167 | +* `Interactions/` — request/response patterns |
| 168 | +* `Builder/` — DI and service registration patterns |
| 169 | + |
| 170 | +### Platform Extensions |
| 171 | + |
| 172 | +Examples: |
| 173 | + |
| 174 | +* `ReactiveUI.Wpf/`, `ReactiveUI.WinUI/`, `ReactiveUI.Maui/`, `ReactiveUI.AndroidX/`, |
| 175 | + `ReactiveUI.Blazor/`, `ReactiveUI.Winforms/`, `ReactiveUI.Testing/`, etc. |
| 176 | + |
| 177 | +### Scheduler Abstraction |
| 178 | + |
| 179 | +* Prefer `RxSchedulers` (AOT-friendly, avoids reflection/AOT attribute propagation) |
| 180 | +* Use `RxApp` only when required (e.g., unit test scheduler detection) |
| 181 | + |
| 182 | +See `docs/RxSchedulers.md`. |
| 183 | + |
| 184 | +--- |
| 185 | + |
| 186 | +## AOT Guidance (Critical) |
| 187 | + |
| 188 | +This repository targets net8.0+ and supports AOT/trimming scenarios. |
| 189 | + |
| 190 | +### Primary Rule: Avoid Reflection Paths |
| 191 | + |
| 192 | +Prefer strongly-typed and source-generator-friendly approaches. Avoid reflection-heavy patterns that require trimming/AOT attributes. |
| 193 | + |
| 194 | +### Attributes: Use Only If Necessary |
| 195 | + |
| 196 | +* Avoid introducing DAC/RDC/RUC attributes unless required. |
| 197 | +* If an attribute is required, apply it directly (no `#if NET6_0_OR_GREATER` guards). Polyfills are available. |
| 198 | + |
| 199 | +Example (only when truly needed): |
| 200 | + |
| 201 | +```csharp |
| 202 | +private static object CreateInstance( |
| 203 | + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] |
| 204 | + Type type) |
| 205 | +{ |
| 206 | + return Activator.CreateInstance(type)!; |
| 207 | +} |
| 208 | +``` |
| 209 | + |
| 210 | +### Suppressions: Last Resort Only |
| 211 | + |
| 212 | +If a warning cannot be resolved without harming design, use suppression attributes with a clear justification. Prefer minimal scope and specific suppression IDs. |
| 213 | + |
| 214 | +--- |
| 215 | + |
| 216 | +## Code Style & Quality Requirements |
| 217 | + |
| 218 | +**CRITICAL:** Follow ReactiveUI contribution guidelines: |
| 219 | +[https://www.reactiveui.net/contribute/index.html](https://www.reactiveui.net/contribute/index.html) |
| 220 | + |
| 221 | +### Enforced Tooling |
| 222 | + |
| 223 | +* `.editorconfig` formatting/naming conventions |
| 224 | +* StyleCop analyzers (build fails on violations) |
| 225 | +* Roslynator analyzers |
| 226 | +* Analysis level: latest |
| 227 | +* Warnings treated as errors (notably nullable and CS4014) |
| 228 | +* **Public APIs require XML documentation**, including protected methods on public types. |
| 229 | + |
| 230 | +### C# Style Rules (High-level) |
| 231 | + |
| 232 | +* Allman braces |
| 233 | +* 4 spaces, no tabs |
| 234 | +* Explicit visibility |
| 235 | +* Private/internal fields: `_camelCase`, `readonly` where possible, `static readonly` order |
| 236 | +* File-scoped namespaces preferred; using directives outside namespace and sorted |
| 237 | +* Use C# keywords (`int`, `string`) rather than BCL types |
| 238 | +* Prefer modern C# features where appropriate (nullable, pattern matching, switch expressions, records, init, target-typed new, etc.) |
| 239 | +* Use `nameof()` over string literals |
| 240 | +* Avoid `this.` unless necessary |
| 241 | +* Use `var` when it improves readability |
| 242 | +
|
| 243 | +If a specific file already follows a local style, adhere to existing file conventions. |
| 244 | +
|
| 245 | +--- |
| 246 | +
|
| 247 | +## Zero Pragma Policy (Critical) |
| 248 | +
|
| 249 | +**No `#pragma warning disable`** in production code. |
| 250 | + |
| 251 | +* **StyleCop warnings (SA****) must be fixed**, never suppressed. |
| 252 | +* **CA**** warnings may be suppressed only as a last resort** using `[SuppressMessage]` with clear justification. |
| 253 | + |
| 254 | +Example: |
| 255 | + |
| 256 | +```csharp |
| 257 | +// WRONG |
| 258 | +#pragma warning disable CA1062 |
| 259 | +public void MyMethod(object parameter) |
| 260 | +{ |
| 261 | + parameter.ToString(); |
| 262 | +} |
| 263 | +#pragma warning restore CA1062 |
| 264 | +
|
| 265 | +// CORRECT |
| 266 | +public void MyMethod(object parameter) |
| 267 | +{ |
| 268 | + ArgumentNullException.ThrowIfNull(parameter); |
| 269 | + parameter.ToString(); |
| 270 | +} |
| 271 | +
|
| 272 | +// LAST RESORT ONLY |
| 273 | +[SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods", |
| 274 | + Justification = "TUnit guarantees non-null parameters from data sources.")] |
| 275 | +public async Task MyTest(IConverter converter, int expectedValue) |
| 276 | +{ |
| 277 | + var result = converter.GetValue(); |
| 278 | + await Assert.That(result).IsEqualTo(expectedValue); |
| 279 | +} |
| 280 | +``` |
| 281 | + |
| 282 | +--- |
| 283 | + |
| 284 | +## Testing Guidelines |
| 285 | + |
| 286 | +* Use TUnit + Microsoft Testing Platform |
| 287 | +* Write unit tests for new features and bug fixes |
| 288 | +* Prefer existing patterns in: |
| 289 | + |
| 290 | + * `src/tests/ReactiveUI.Tests/` |
| 291 | + * `src/tests/ReactiveUI.AOTTests/` |
| 292 | +* Use `ReactiveUI.Testing` utilities for reactive code |
| 293 | + |
| 294 | +--- |
| 295 | + |
| 296 | +## Common Development Patterns |
| 297 | + |
| 298 | +### ViewModel Skeleton |
| 299 | + |
| 300 | +```csharp |
| 301 | +public class SampleViewModel : ReactiveObject |
| 302 | +{ |
| 303 | + private string? _name; |
| 304 | + private readonly ObservableAsPropertyHelper<bool> _isValid; |
| 305 | +
|
| 306 | + public SampleViewModel() |
| 307 | + { |
| 308 | + _isValid = this.WhenAnyValue(x => x.Name) |
| 309 | + .Select(name => !string.IsNullOrWhiteSpace(name)) |
| 310 | + .ToProperty(this, nameof(IsValid)); |
| 311 | +
|
| 312 | + SubmitCommand = ReactiveCommand.CreateFromTask( |
| 313 | + ExecuteSubmit, |
| 314 | + this.WhenAnyValue(x => x.IsValid)); |
| 315 | + } |
| 316 | +
|
| 317 | + public string? Name |
| 318 | + { |
| 319 | + get => _name; |
| 320 | + set => this.RaiseAndSetIfChanged(ref _name, value); |
| 321 | + } |
| 322 | +
|
| 323 | + public bool IsValid => _isValid.Value; |
| 324 | +
|
| 325 | + public ReactiveCommand<Unit, Unit> SubmitCommand { get; } |
| 326 | +
|
| 327 | + private async Task ExecuteSubmit(CancellationToken cancellationToken) |
| 328 | + { |
| 329 | + // Implementation |
| 330 | + } |
| 331 | +} |
| 332 | +``` |
| 333 | + |
| 334 | +### RxSchedulers (Preferred) |
| 335 | + |
| 336 | +```csharp |
| 337 | +public IObservable<string> GetData() |
| 338 | +{ |
| 339 | + return Observable.Return("data") |
| 340 | + .ObserveOn(RxSchedulers.MainThreadScheduler); |
| 341 | +} |
| 342 | +``` |
| 343 | + |
| 344 | +### WhenAnyValue |
| 345 | + |
| 346 | +```csharp |
| 347 | +this.WhenAnyValue( |
| 348 | + x => x.FirstName, |
| 349 | + x => x.LastName, |
| 350 | + (first, last) => $"{first} {last}") |
| 351 | + .Subscribe(fullName => { /* handle */ }); |
| 352 | +
|
| 353 | +this.WhenAnyValue(x => x.IsLoading) |
| 354 | + .Where(isLoading => !isLoading) |
| 355 | + .Subscribe(_ => { /* handle */ }); |
| 356 | +``` |
| 357 | +
|
| 358 | +### ObservableAsPropertyHelper |
| 359 | +
|
| 360 | +```csharp |
| 361 | +private readonly ObservableAsPropertyHelper<decimal> _total; |
| 362 | +public decimal Total => _total.Value; |
| 363 | +
|
| 364 | +_total = this.WhenAnyValue( |
| 365 | + x => x.Quantity, |
| 366 | + x => x.Price, |
| 367 | + (qty, price) => qty * price) |
| 368 | + .ToProperty(this, nameof(Total)); |
| 369 | +``` |
| 370 | +
|
| 371 | +--- |
| 372 | +
|
| 373 | +## What to Avoid |
| 374 | +
|
| 375 | +* Reflection-heavy implementations in core paths |
| 376 | +* Expression trees in hot paths without caching |
| 377 | +* Platform-specific code in `src/ReactiveUI/` core library |
| 378 | +* Breaking public APIs without proper versioning and documentation |
0 commit comments