Skip to content

# 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 AppLocator forwards into it.
  • 2. Drive RxAppBuilder through the resolver-bound extension: resolver.CreateReactiveUIBuilder().
  • 3. Re-bind AppLocator once 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:


Migration Time Estimate: 2-8 hours depending on application size Difficulty: Easy to Moderate Recommended: Yes - Modern pattern with better maintainability