|
| 1 | +# Copilot Instructions for ReactiveUI |
| 2 | + |
| 3 | +You are working on the ReactiveUI repository - a composable, cross-platform model-view-viewmodel framework for all .NET platforms inspired by functional reactive programming. |
| 4 | + |
| 5 | +## 🏗️ Build Environment Requirements |
| 6 | + |
| 7 | +**IMPORTANT**: This repository requires Windows for building. Linux/macOS are not supported. |
| 8 | + |
| 9 | +### Required Tools |
| 10 | +- Windows 10/11 with Visual Studio 2019+ |
| 11 | +- PowerShell or Command Prompt |
| 12 | +- .NET 9.0 SDK (required for AOT tests) or .NET 8.0 SDK minimum |
| 13 | +- Windows-specific target frameworks: `net8.0-windows10.0.17763.0`, `net9.0-windows10.0.17763.0` |
| 14 | + |
| 15 | +### Solution Files |
| 16 | +- Main solution: `src/ReactiveUI.sln` (repository root/src directory) |
| 17 | +- Integration tests: `integrationtests/` directory contains platform-specific solutions |
| 18 | + |
| 19 | +## 🛠️ Build & Test Commands |
| 20 | + |
| 21 | +**Run these commands in Windows PowerShell or CMD from the repository root:** |
| 22 | + |
| 23 | +```powershell |
| 24 | +# Check .NET installation (requires .NET 9.0 SDK for full build including AOT tests) |
| 25 | +dotnet --info |
| 26 | +
|
| 27 | +# Restore packages (may fail on non-Windows due to Windows-specific target frameworks) |
| 28 | +dotnet restore src/ReactiveUI.sln |
| 29 | +
|
| 30 | +# Build the solution (requires Windows for platform-specific targets) |
| 31 | +dotnet build src/ReactiveUI.sln -c Release -warnaserror |
| 32 | +
|
| 33 | +# Run tests (includes AOT tests that require .NET 9.0) |
| 34 | +dotnet test src/ReactiveUI.sln -c Release --no-build |
| 35 | +``` |
| 36 | + |
| 37 | +**Note**: The repository contains Windows-specific target frameworks (`net8.0-windows`, `net9.0-windows`) and AOT tests that require .NET 9.0. Building on Linux/macOS will fail due to these platform dependencies. |
| 38 | + |
| 39 | +**For non-Windows environments**: If working in this repository on Linux/macOS (such as in GitHub Codespaces), focus on documentation changes, code analysis, and understanding patterns rather than attempting to build. The build failure is expected and normal. |
| 40 | + |
| 41 | +If any step fails due to Windows-specific tooling requirements, create a draft PR describing the minimal change needed. |
| 42 | + |
| 43 | +## 🎯 AOT-Friendly Development Patterns |
| 44 | + |
| 45 | +ReactiveUI supports Ahead-of-Time (AOT) compilation. Follow these patterns: |
| 46 | + |
| 47 | +### ✅ Preferred AOT-Friendly Patterns |
| 48 | + |
| 49 | +1. **String-based property observation** (AOT-safe when using nameof): |
| 50 | + ```csharp |
| 51 | + // Use ObservableForProperty with nameof - AOT-safe |
| 52 | + obj.ObservableForProperty<TestReactiveObject, string?>( |
| 53 | + nameof(TestReactiveObject.TestProperty), |
| 54 | + beforeChange: false, |
| 55 | + skipInitial: false) |
| 56 | + .Select(x => x.Value) |
| 57 | + .Subscribe(HandlePropertyChange); |
| 58 | + |
| 59 | + // Method that needs to access properties via reflection |
| 60 | +#if NET6_0_OR_GREATER |
| 61 | + private static PropertyInfo? GetPropertyInfo( |
| 62 | + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] |
| 63 | + Type type, string propertyName) |
| 64 | +#else |
| 65 | + private static PropertyInfo? GetPropertyInfo(Type type, string propertyName) |
| 66 | +#endif |
| 67 | + { |
| 68 | + return type.GetProperty(propertyName); |
| 69 | + } |
| 70 | + ``` |
| 71 | + |
| 72 | +2. **ReactiveCommand creation**: |
| 73 | + ```csharp |
| 74 | + // Preferred: Async command with explicit typing |
| 75 | + public ReactiveCommand<Unit, Unit> SubmitCommand { get; } |
| 76 | + |
| 77 | + public ViewModel() |
| 78 | + { |
| 79 | + SubmitCommand = ReactiveCommand.CreateFromTask(ExecuteSubmit); |
| 80 | + } |
| 81 | + |
| 82 | + private async Task ExecuteSubmit(CancellationToken cancellationToken) |
| 83 | + { |
| 84 | + // Implementation |
| 85 | + } |
| 86 | + ``` |
| 87 | + |
| 88 | +3. **Observable property helpers**: |
| 89 | + ```csharp |
| 90 | + private readonly ObservableAsPropertyHelper<string> _computedValue; |
| 91 | + |
| 92 | + public string ComputedValue => _computedValue.Value; |
| 93 | + |
| 94 | + // In constructor - when using strongly-typed expressions, no AOT attributes needed |
| 95 | + public ViewModel() |
| 96 | + { |
| 97 | + _computedValue = this.WhenAnyValue(x => x.InputValue) |
| 98 | + .Select(x => $"Computed: {x}") |
| 99 | + .ToProperty(this, nameof(ComputedValue)); |
| 100 | + } |
| 101 | + |
| 102 | + // For methods that register view models with dependency injection |
| 103 | +#if NET6_0_OR_GREATER |
| 104 | + public void RegisterViewModel<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TViewModel>() |
| 105 | +#else |
| 106 | + public void RegisterViewModel<TViewModel>() |
| 107 | +#endif |
| 108 | + { |
| 109 | + // Registration logic that uses reflection on TViewModel |
| 110 | + } |
| 111 | + ``` |
| 112 | + |
| 113 | +4. **Property change notification**: |
| 114 | + ```csharp |
| 115 | + private string? _myProperty; |
| 116 | + |
| 117 | + public string? MyProperty |
| 118 | + { |
| 119 | + get => _myProperty; |
| 120 | + set => this.RaiseAndSetIfChanged(ref _myProperty, value); |
| 121 | + } |
| 122 | + ``` |
| 123 | + |
| 124 | +### ⚠️ AOT Considerations |
| 125 | + |
| 126 | +**Preferred Approach**: Use `DynamicallyAccessedMembersAttribute` to inform the AOT compiler about required members: |
| 127 | + |
| 128 | +```csharp |
| 129 | +// For methods that access type constructors |
| 130 | +private static object CreateInstance( |
| 131 | +#if NET6_0_OR_GREATER |
| 132 | + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] |
| 133 | +#endif |
| 134 | + Type type) |
| 135 | +{ |
| 136 | + return Activator.CreateInstance(type); |
| 137 | +} |
| 138 | + |
| 139 | +// For methods that access properties via reflection |
| 140 | +private static PropertyInfo? GetProperty( |
| 141 | +#if NET6_0_OR_GREATER |
| 142 | + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] |
| 143 | +#endif |
| 144 | + Type type, string propertyName) |
| 145 | +{ |
| 146 | + return type.GetProperty(propertyName); |
| 147 | +} |
| 148 | + |
| 149 | +// For methods that need all members (like view model registration) |
| 150 | +public void RegisterViewModel< |
| 151 | +#if NET6_0_OR_GREATER |
| 152 | + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] |
| 153 | +#endif |
| 154 | + TViewModel>() |
| 155 | +{ |
| 156 | + // Registration logic |
| 157 | +} |
| 158 | +``` |
| 159 | + |
| 160 | +**Fallback Approach**: When `DynamicallyAccessedMembersAttribute` isn't sufficient, use suppression attributes: |
| 161 | +```csharp |
| 162 | +[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Description of why this is safe")] |
| 163 | +[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Description of why this is safe")] |
| 164 | +public void SetupObservation() |
| 165 | +{ |
| 166 | + // Reflection-based code that can't be made AOT-safe with DynamicallyAccessedMembersAttribute |
| 167 | +} |
| 168 | +``` |
| 169 | + |
| 170 | +**Best Practices**: |
| 171 | +- Prefer `DynamicallyAccessedMembersAttribute` over `UnconditionalSuppressMessage` when possible |
| 172 | +- Use specific `DynamicallyAccessedMemberTypes` values rather than `All` when you know what's needed |
| 173 | +- Prefer strongly-typed expressions over string-based property names when possible |
| 174 | +- Use `nameof()` for compile-time property name checking |
| 175 | + |
| 176 | +**ReactiveUI Codebase Examples**: |
| 177 | +```csharp |
| 178 | +// From ReactiveUIBuilder.cs - View model registration |
| 179 | +public IReactiveUIBuilder RegisterSingletonViewModel< |
| 180 | +#if NET6_0_OR_GREATER |
| 181 | + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] |
| 182 | +#endif |
| 183 | + TViewModel>() |
| 184 | + |
| 185 | +// From ExpressionRewriter.cs - Property access |
| 186 | +private static PropertyInfo? GetItemProperty( |
| 187 | +#if NET6_0_OR_GREATER |
| 188 | + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] |
| 189 | +#endif |
| 190 | + Type type) |
| 191 | + |
| 192 | +// From DependencyResolverMixins.cs - Constructor access |
| 193 | +private static Func<object> TypeFactory( |
| 194 | +#if NET6_0_OR_GREATER |
| 195 | + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] |
| 196 | +#endif |
| 197 | + TypeInfo typeInfo) |
| 198 | +``` |
| 199 | + |
| 200 | +## 📚 Reference Materials |
| 201 | + |
| 202 | +### Official Samples & Documentation |
| 203 | +Use these as references for patterns and implementation examples: |
| 204 | + |
| 205 | +- **ReactiveUI Documentation**: https://reactiveui.net/docs/ |
| 206 | +- **Official Samples Repository**: Various platform examples (WPF, Avalonia, Uno, etc.) |
| 207 | +- **Getting Started Guide**: https://reactiveui.net/docs/getting-started/ |
| 208 | +- **ViewModels Documentation**: https://reactiveui.net/docs/handbook/view-models/ |
| 209 | + |
| 210 | +### In-Repository Examples |
| 211 | +- `src/ReactiveUI.AOTTests/` - AOT compatibility test examples showing proper attribute usage |
| 212 | +- `src/ReactiveUI.Tests/` - Comprehensive test patterns and API usage examples |
| 213 | +- `integrationtests/` - Platform-specific integration examples |
| 214 | +- `src/ReactiveUI.Builder.Tests/` - Builder pattern examples for dependency injection |
| 215 | + |
| 216 | +### Key Project Structure |
| 217 | +- `src/ReactiveUI/` - Core ReactiveUI library |
| 218 | +- `src/ReactiveUI.WPF/` - WPF-specific extensions |
| 219 | +- `src/ReactiveUI.WinUI/` - WinUI-specific extensions |
| 220 | +- `src/ReactiveUI.Maui/` - MAUI-specific extensions |
| 221 | +- `src/ReactiveUI.Testing/` - Testing utilities |
| 222 | + |
| 223 | +## 🎨 Good Development Suggestions |
| 224 | + |
| 225 | +### Creating ViewModels |
| 226 | +```csharp |
| 227 | +public class SampleViewModel : ReactiveObject |
| 228 | +{ |
| 229 | + private string? _name; |
| 230 | + private readonly ObservableAsPropertyHelper<bool> _isValid; |
| 231 | + |
| 232 | + public SampleViewModel() |
| 233 | + { |
| 234 | + // Setup validation |
| 235 | + _isValid = this.WhenAnyValue(x => x.Name) |
| 236 | + .Select(name => !string.IsNullOrWhiteSpace(name)) |
| 237 | + .ToProperty(this, nameof(IsValid)); |
| 238 | + |
| 239 | + // Setup commands |
| 240 | + SubmitCommand = ReactiveCommand.CreateFromTask( |
| 241 | + ExecuteSubmit, |
| 242 | + this.WhenAnyValue(x => x.IsValid)); |
| 243 | + } |
| 244 | + |
| 245 | + public string? Name |
| 246 | + { |
| 247 | + get => _name; |
| 248 | + set => this.RaiseAndSetIfChanged(ref _name, value); |
| 249 | + } |
| 250 | + |
| 251 | + public bool IsValid => _isValid.Value; |
| 252 | + |
| 253 | + public ReactiveCommand<Unit, Unit> SubmitCommand { get; } |
| 254 | + |
| 255 | + private async Task ExecuteSubmit(CancellationToken cancellationToken) |
| 256 | + { |
| 257 | + // Submit logic here |
| 258 | + } |
| 259 | +} |
| 260 | +``` |
| 261 | + |
| 262 | +### Using WhenAnyValue / WhenAny |
| 263 | +```csharp |
| 264 | +// Combine multiple properties |
| 265 | +this.WhenAnyValue( |
| 266 | + x => x.FirstName, |
| 267 | + x => x.LastName, |
| 268 | + (first, last) => $"{first} {last}") |
| 269 | + .Subscribe(fullName => { /* Handle full name */ }); |
| 270 | + |
| 271 | +// React to property changes |
| 272 | +this.WhenAnyValue(x => x.IsLoading) |
| 273 | + .Where(isLoading => !isLoading) |
| 274 | + .Subscribe(_ => { /* Handle loaded state */ }); |
| 275 | + |
| 276 | +// String-based observation (AOT-friendly with nameof) |
| 277 | +obj.ObservableForProperty<MyClass, string>( |
| 278 | + nameof(MyClass.PropertyName), |
| 279 | + beforeChange: false, |
| 280 | + skipInitial: false) |
| 281 | + .Select(x => x.Value) |
| 282 | + .Subscribe(HandleChange); |
| 283 | +``` |
| 284 | + |
| 285 | +### ReactiveUI Builder Pattern |
| 286 | +```csharp |
| 287 | +// Platform-specific builder configuration |
| 288 | +public static class MauiProgram |
| 289 | +{ |
| 290 | + public static MauiApp CreateMauiApp() |
| 291 | + { |
| 292 | + var builder = MauiApp.CreateBuilder(); |
| 293 | + |
| 294 | + builder |
| 295 | + .UseMauiApp<App>() |
| 296 | + .UseReactiveUI() // Extension for MAUI |
| 297 | + .ConfigureFonts(fonts => |
| 298 | + { |
| 299 | + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); |
| 300 | + }); |
| 301 | + |
| 302 | + return builder.Build(); |
| 303 | + } |
| 304 | +} |
| 305 | + |
| 306 | +// WinUI builder configuration |
| 307 | +public void ConfigureServices(IServiceCollection services) |
| 308 | +{ |
| 309 | + services.AddReactiveUI(builder => |
| 310 | + { |
| 311 | + builder.WithWinUI(); // Platform-specific setup |
| 312 | + }); |
| 313 | +} |
| 314 | +``` |
| 315 | + |
| 316 | +### ObservableAsPropertyHelper |
| 317 | +```csharp |
| 318 | +// Computed property that reacts to changes |
| 319 | +private readonly ObservableAsPropertyHelper<decimal> _total; |
| 320 | + |
| 321 | +public decimal Total => _total.Value; |
| 322 | + |
| 323 | +// In constructor |
| 324 | +_total = this.WhenAnyValue( |
| 325 | + x => x.Quantity, |
| 326 | + x => x.Price, |
| 327 | + (qty, price) => qty * price) |
| 328 | + .ToProperty(this, nameof(Total)); |
| 329 | +``` |
| 330 | + |
| 331 | +## 🎨 Code Style & Formatting |
| 332 | + |
| 333 | +ReactiveUI enforces strict code style and formatting guidelines: |
| 334 | + |
| 335 | +### EditorConfig & StyleCop Compliance |
| 336 | +- **EditorConfig**: The repository has a comprehensive `.editorconfig` file with detailed formatting rules |
| 337 | +- **StyleCop Analyzers**: Extensive StyleCop analyzer rules are configured with error-level enforcement |
| 338 | +- **Code Analysis**: Over 200 analyzer rules are configured for code quality and consistency |
| 339 | + |
| 340 | +### Key Style Requirements |
| 341 | +- **Indentation**: 4 spaces (configured in `.editorconfig`) |
| 342 | +- **Braces**: Allman style (opening braces on new lines) |
| 343 | +- **Field naming**: Private fields use `_camelCase` prefix, static fields use `s_` prefix |
| 344 | +- **Var usage**: Prefer `var` when type is apparent |
| 345 | +- **Documentation**: All public APIs require XML documentation |
| 346 | +- **Null checking**: Use modern C# null-conditional operators |
| 347 | + |
| 348 | +### Official Style Guide |
| 349 | +Follow the comprehensive ReactiveUI style guide: |
| 350 | +- **Code Style Guide**: https://www.reactiveui.net/contribute/software-style-guide/code-style.html |
| 351 | +- **Commit Message Convention**: https://reactiveui.net/contribute/software-style-guide/commit-message-convention |
| 352 | + |
| 353 | +### Build with Style Enforcement |
| 354 | +```powershell |
| 355 | +# Build with warnings as errors (includes StyleCop violations) |
| 356 | +dotnet build src/ReactiveUI.sln -c Release -warnaserror |
| 357 | +``` |
| 358 | + |
| 359 | +**Important**: Style violations will cause build failures. Use an IDE with EditorConfig support (Visual Studio, VS Code, Rider) to automatically format code according to project standards. |
| 360 | + |
| 361 | +## 📋 Testing Guidelines |
| 362 | + |
| 363 | +- Always write unit tests for new features or bug fixes |
| 364 | +- Use `ReactiveUI.Testing` package for testing reactive code |
| 365 | +- Follow existing test patterns in `src/ReactiveUI.Tests/` |
| 366 | +- For AOT scenarios, reference patterns in `src/ReactiveUI.AOTTests/` |
| 367 | + |
| 368 | +## 🚫 What to Avoid |
| 369 | + |
| 370 | +- **Reflection-heavy patterns** without proper AOT suppression |
| 371 | +- **Expression trees** in hot paths without caching |
| 372 | +- **Platform-specific code** in the core ReactiveUI library |
| 373 | +- **Breaking changes** to public APIs without proper versioning |
| 374 | + |
| 375 | +## 🔄 Common Tasks |
| 376 | + |
| 377 | +### Adding a new feature: |
| 378 | +1. Create failing tests first |
| 379 | +2. Implement minimal functionality |
| 380 | +3. Ensure AOT compatibility |
| 381 | +4. Update documentation if needed |
| 382 | +5. Add XML documentation to public APIs |
| 383 | + |
| 384 | +### Fixing bugs: |
| 385 | +1. Create reproduction test |
| 386 | +2. Fix with minimal changes |
| 387 | +3. Verify AOT compatibility |
| 388 | +4. Ensure no regression in existing tests |
| 389 | + |
| 390 | +Remember: ReactiveUI emphasizes functional reactive programming patterns, immutability where possible, and clean separation of concerns through the MVVM pattern. When in doubt, prefer reactive streams over imperative code, and always consider the AOT implications of your changes. |
0 commit comments