Migration Guide: RxAppBuilder
This guide helps you migrate from legacy ReactiveUI initialization patterns to the modern RxAppBuilder pattern.
Why Migrate?
Benefits of RxAppBuilder
✅ Centralized Configuration: All setup in one place
✅ Type-Safe: Compile-time validation of configuration
✅ Platform-Specific Optimizations: Automatic scheduler setup
✅ Dependency Injection: Built-in service registration
✅ Testability: Easy to mock and configure for tests
✅ Modern Pattern: Aligns with .NET best practices
Legacy Limitations
❌ Scattered Configuration: Setup code spread across multiple files
❌ Static Dependencies: Hard to test and override
❌ Manual Scheduler Setup: Error-prone configuration
❌ No DI Integration: Manual service location
❌ Platform Detection: Manual platform-specific code
Prerequisites
- ReactiveUI 22.2.1+
- .NET 8.0+ or .NET Standard 2.0+
- Splat 17.1.1+ (included with ReactiveUI)
Migration Patterns
Pattern 1: Basic Initialization
Legacy Approach
// In App.xaml.cs or Program.cs
public partial class App : Application
{
public App()
{
InitializeComponent();
// Manual scheduler setup
RxApp.MainThreadScheduler = new DispatcherScheduler(Dispatcher.CurrentDispatcher);
// Manual service registration
Locator.CurrentMutable.RegisterLazySingleton<IDataService>(() => new DataService());
Locator.CurrentMutable.RegisterLazySingleton<IScreen>(() => new MainViewModel());
// Manual view registration
Locator.CurrentMutable.Register<IViewFor<MainViewModel>>(() => new MainView());
MainWindow = new MainWindow();
}
}
RxAppBuilder Approach ✅
public partial class App : Application
{
public App()
{
InitializeComponent();
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithWpf() // Automatically sets up DispatcherScheduler
.WithViewsFromAssembly(Assembly.GetExecutingAssembly())
.WithRegistration(locator =>
{
locator.RegisterLazySingleton<IDataService>(() => new DataService());
locator.RegisterLazySingleton<IScreen>(() => new MainViewModel());
})
.BuildApp();
MainWindow = new MainWindow();
}
}
Pattern 2: Platform-Specific Setup
WPF
// Legacy ❌
RxApp.MainThreadScheduler = new DispatcherScheduler(Dispatcher.CurrentDispatcher);
// RxAppBuilder ✅
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithWpf() // Automatic scheduler setup
.BuildApp();
MAUI
// Legacy ❌ (Not available)
// Manual platform detection and setup required
// RxAppBuilder ✅
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// Initialize ReactiveUI with RxAppBuilder
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithMaui()
.WithViewsFromAssembly(typeof(App).Assembly)
.WithRegistration(locator =>
{
locator.RegisterLazySingleton<IDataService>(() => new DataService());
})
.BuildApp();
return builder.Build();
}
Blazor (Server)
// Legacy ❌
// Complex manual setup required
// RxAppBuilder ✅
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithBlazor()
.WithViewsFromAssembly(Assembly.GetExecutingAssembly())
.WithRegistration(locator =>
{
locator.RegisterLazySingleton<IDataService>(() => new DataService());
})
.BuildApp();
var webApp = builder.Build();
// Configure and run...
Avalonia
// Legacy ❌
public override void OnFrameworkInitializationCompleted()
{
Locator.CurrentMutable.Register<IScreen>(() => new MainViewModel());
Locator.CurrentMutable.Register<IViewFor<MainViewModel>>(() => new MainWindow());
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}
}
// RxAppBuilder ✅
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
// Note: Avalonia handles ReactiveUI init via .UseReactiveUI(rxuiBuilder => {/* ... */})
AppLocator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetExecutingAssembly());
AppLocator.CurrentMutable.RegisterLazySingleton<IScreen>(() => new MainViewModel());
desktop.MainWindow = new MainWindow
{
DataContext = AppLocator.Current.GetService<MainViewModel>()
};
}
base.OnFrameworkInitializationCompleted();
}
Pattern 3: View Registration
Legacy Manual Registration
// One by one ❌
Locator.CurrentMutable.Register<IViewFor<MainViewModel>>(() => new MainView());
Locator.CurrentMutable.Register<IViewFor<DetailsViewModel>>(() => new DetailsView());
Locator.CurrentMutable.Register<IViewFor<SettingsViewModel>>(() => new SettingsView());
RxAppBuilder Auto-Registration ✅
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithWpf()
.WithViewsFromAssembly(Assembly.GetExecutingAssembly()) // Registers all IViewFor<T>
.BuildApp();
Pattern 4: Custom Schedulers
Legacy Approach
// Manual scheduler configuration ❌
RxApp.MainThreadScheduler = CustomScheduler.Instance;
RxApp.TaskpoolScheduler = CustomTaskScheduler.Instance;
RxAppBuilder Approach ✅
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithWpf()
.WithCustomScheduler(mainThreadScheduler: CustomScheduler.Instance)
.WithCustomScheduler(taskpoolScheduler: CustomTaskScheduler.Instance)
.BuildApp();
// Access configured schedulers
var mainScheduler = app.MainThreadScheduler;
var taskScheduler = app.TaskpoolScheduler;
// Optional: Register in locator if needed
AppLocator.CurrentMutable.RegisterConstant<IScheduler>(mainScheduler, "MainThread");
AppLocator.CurrentMutable.RegisterConstant<IScheduler>(taskScheduler, "Taskpool");
Pattern 5: Service Registration
Legacy Locator Pattern
// Scattered throughout app ❌
Locator.CurrentMutable.RegisterLazySingleton<IAuthService>(() => new AuthService());
Locator.CurrentMutable.RegisterLazySingleton<IDataService>(() => new DataService());
Locator.CurrentMutable.Register<MainViewModel>(() => new MainViewModel());
RxAppBuilder Centralized ✅
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithWpf()
.WithRegistration(locator =>
{
// All services in one place
locator.RegisterLazySingleton<IAuthService>(() => new AuthService());
locator.RegisterLazySingleton<IDataService>(() => new DataService());
locator.Register<MainViewModel>(() => new MainViewModel());
// With dependencies
locator.Register<DetailsViewModel>(() =>
new DetailsViewModel(
Locator.Current.GetService<IDataService>()));
})
.BuildApp();
Platform-Specific Migration
WPF Migration
// BEFORE - App.xaml.cs
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
RxApp.MainThreadScheduler = new DispatcherScheduler(Dispatcher.CurrentDispatcher);
Locator.CurrentMutable.RegisterLazySingleton<IScreen>(() => new MainViewModel());
Locator.CurrentMutable.Register<IViewFor<MainViewModel>>(() => new MainWindow());
new MainWindow().Show();
}
}
// AFTER - App.xaml.cs
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithWpf()
.WithViewsFromAssembly(typeof(App).Assembly)
.WithRegistration(locator =>
{
locator.RegisterLazySingleton<IScreen>(() => new MainViewModel());
})
.BuildApp();
var mainWindow = new MainWindow();
mainWindow.Show();
}
}
MAUI Migration
// BEFORE - MauiProgram.cs
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// Manual setup scattered
Locator.CurrentMutable.RegisterLazySingleton<IDataService>(() => new DataService());
return builder.Build();
}
}
// AFTER - MauiProgram.cs
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
// Centralized setup
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithMaui()
.WithViewsFromAssembly(typeof(App).Assembly)
.WithRegistration(locator =>
{
locator.RegisterLazySingleton<IDataService>(() => new DataService());
locator.RegisterLazySingleton<INavigationService>(() => new NavigationService());
})
.BuildApp();
return builder.Build();
}
}
Blazor Server Migration
// BEFORE - Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
// Manual ReactiveUI setup
Locator.CurrentMutable.RegisterLazySingleton<IDataService>(() => new DataService());
var app = builder.Build();
// ... configure and run
// AFTER - Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
// RxAppBuilder setup
var rxApp = RxAppBuilder.CreateReactiveUIBuilder()
.WithBlazor()
.WithViewsFromAssembly(Assembly.GetExecutingAssembly())
.WithRegistration(locator =>
{
locator.RegisterLazySingleton<IDataService>(() => new DataService());
})
.BuildApp();
var app = builder.Build();
// ... configure and run
Testing with RxAppBuilder
Legacy Test Setup
[Fact]
public void Test_ViewModel()
{
// Manual scheduler setup for each test ❌
var scheduler = new TestScheduler();
RxApp.MainThreadScheduler = scheduler;
RxApp.TaskpoolScheduler = scheduler;
// Manual service setup
Locator.CurrentMutable.Register<IDataService>(() => mockService);
var vm = new MainViewModel();
// ... test
}
RxAppBuilder Test Setup ✅
public class TestFixture : IDisposable
{
private readonly IResolverScope _scope;
public TestFixture()
{
var scheduler = new TestScheduler();
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithCustomScheduler(mainThreadScheduler: scheduler)
.WithCustomScheduler(taskpoolScheduler: scheduler)
.WithRegistration(locator =>
{
locator.RegisterLazySingleton<IDataService>(() => mockService);
})
.BuildApp();
_scope = Locator.Current.GetService<IResolverScope>();
}
public void Dispose()
{
_scope?.Dispose();
}
}
[Fact]
public void Test_ViewModel()
{
using var fixture = new TestFixture();
var vm = new MainViewModel();
// ... test
}
Common Migration Scenarios
Scenario 1: Existing Large Application
Strategy: Incremental migration
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// New: RxAppBuilder for new code
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithWpf()
.WithViewsFromAssembly(typeof(App).Assembly)
.WithRegistration(locator =>
{
// New services here
locator.RegisterLazySingleton<INewService>(() => new NewService());
})
.BuildApp();
// Old: Keep existing registrations until migrated
Locator.CurrentMutable.RegisterLazySingleton<ILegacyService>(() => new LegacyService());
new MainWindow().Show();
}
}
Scenario 2: Multiple Projects/Assemblies
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithWpf()
.WithViewsFromAssembly(typeof(App).Assembly) // UI assembly
.WithViewsFromAssembly(typeof(SharedView).Assembly) // Shared assembly
.WithRegistration(locator =>
{
// Register services from multiple assemblies
RegisterCoreServices(locator);
RegisterUIServices(locator);
})
.BuildApp();
void RegisterCoreServices(IMutableDependencyResolver locator)
{
locator.RegisterLazySingleton<IDataService>(() => new DataService());
}
void RegisterUIServices(IMutableDependencyResolver locator)
{
locator.Register<MainViewModel>(() => new MainViewModel());
}
Scenario 3: Dynamic Configuration
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithWpf()
.WithViewsFromAssembly(typeof(App).Assembly)
.WithRegistration(locator =>
{
// Configure based on environment
if (IsProduction())
{
locator.RegisterLazySingleton<IDataService>(() => new ProductionDataService());
}
else
{
locator.RegisterLazySingleton<IDataService>(() => new DevelopmentDataService());
}
})
.BuildApp();
Migration Checklist
Phase 1: Preparation
- [ ] Update to ReactiveUI 22.2.1+
- [ ] Review current initialization code
- [ ] Identify all service registrations
- [ ] Document custom scheduler usage
Phase 2: Implementation
- [ ] Install/update required packages
- [ ] Add RxAppBuilder initialization
- [ ] Migrate service registrations
- [ ] Update view registrations
- [ ] Configure platform-specific settings
Phase 3: Testing
- [ ] Update unit tests
- [ ] Test application startup
- [ ] Verify all views resolve
- [ ] Validate scheduler behavior
- [ ] Test dependency injection
Phase 4: Cleanup
- [ ] Remove legacy initialization code
- [ ] Remove manual scheduler setup
- [ ] Clean up scattered registrations
- [ ] Update documentation
Troubleshooting
Issue: Views Not Resolving
Problem: IViewFor<TViewModel> not found
Solution: Ensure WithViewsFromAssembly() includes all assemblies with views
.WithViewsFromAssembly(Assembly.GetExecutingAssembly())
.WithViewsFromAssembly(typeof(SharedView).Assembly)
Issue: Scheduler Not Working
Problem: UI updates not happening on main thread
Solution: Verify platform-specific method called
// Ensure you're using the right platform method
.WithWpf() // For WPF
.WithMaui() // For MAUI
.WithBlazor() // For Blazor
Issue: Services Not Found
Problem: GetService<T>() returns null
Solution: Check registration is in WithRegistration block
.WithRegistration(locator =>
{
locator.RegisterLazySingleton<IMyService>(() => new MyService());
})
Benefits After Migration
Development
- ✅ Clearer application structure
- ✅ Easier onboarding for new developers
- ✅ Better IDE support
- ✅ Compile-time validation
Testing
- ✅ Simplified test setup
- ✅ Better isolation
- ✅ Easier mocking
- ✅ Consistent test configuration
Maintenance
- ✅ Single source of truth for configuration
- ✅ Easier to update dependencies
- ✅ Clear dependency graph
- ✅ Better error messages
Additional Resources
Getting Help
If you encounter issues during migration:
- Check GitHub Discussions
- Ask on Slack
- Review Sample Code
Migration Time Estimate: 2-8 hours depending on application size Difficulty: Easy to Moderate Recommended: Yes - Modern pattern with better maintainability