# 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–10.0 or .NET Framework 4.6.2–4.8.1
- 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()
.WithMainThreadScheduler(CustomScheduler.Instance)
.WithTaskPoolScheduler(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()
.WithMainThreadScheduler(scheduler)
.WithTaskPoolScheduler(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: Hybrid Bootstrap (existing DI container + existing host)¶
The most common real-world case: your app already builds an IServiceProvider (or Autofac container, or DryIoc container) via its own host/bootstrap. You want RxAppBuilder to register into that container instead of standing up a parallel one.
The flow is:
- 1. Hook the Splat adapter for your container so
AppLocatorforwards into it. - 2. Drive
RxAppBuilderthrough the resolver-bound extension:resolver.CreateReactiveUIBuilder(). - 3. Re-bind
AppLocatoronce the container is built (only matters for containers that distinguish "configure" from "built", e.g. MSDI / Autofac / SimpleInjector).
Generic Host + Microsoft.Extensions.DependencyInjection¶
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ReactiveUI.Builder;
using Splat;
using Splat.Microsoft.Extensions.DependencyInjection;
var hostBuilder = Host.CreateApplicationBuilder(args);
// Your existing service registrations
hostBuilder.Services.AddSingleton<IDataService, DataService>();
hostBuilder.Services.AddTransient<MainViewModel>();
hostBuilder.Services.AddTransient<IViewFor<MainViewModel>, MainView>();
// Bind Splat's AppLocator to the IServiceCollection.
hostBuilder.Services.UseMicrosoftDependencyResolver();
// Drive ReactiveUI through the same container.
((IMutableDependencyResolver)AppLocator.CurrentMutable)
.CreateReactiveUIBuilder()
.WithWpf()
.WithViewsFromAssembly(typeof(App).Assembly)
.BuildApp();
var host = hostBuilder.Build();
// Re-bind AppLocator to the built IServiceProvider.
host.Services.UseMicrosoftDependencyResolver();
host.Run();
Existing Autofac bootstrap¶
var builder = new ContainerBuilder();
builder.RegisterModule<MyExistingAutofacModule>(); // your existing modules
builder.RegisterType<MainViewModel>();
builder.RegisterType<MainView>().As<IViewFor<MainViewModel>>();
var autofacResolver = builder.UseAutofacDependencyResolver();
autofacResolver.CreateReactiveUIBuilder()
.WithWpf()
.WithViewsFromAssembly(typeof(App).Assembly)
.BuildApp();
var container = builder.Build();
autofacResolver.SetLifetimeScope(container); // Autofac v5+ requires this
> See custom-dependency-inversion for the same pattern against DryIoc and the bare IMutableDependencyResolver route, plus a verification checklist.
Scenario 4: 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:
- 1. Check GitHub Discussions
- 2. Ask on Slack
- 3. Review Sample Code
Migration Time Estimate: 2-8 hours depending on application size Difficulty: Easy to Moderate Recommended: Yes - Modern pattern with better maintainability