Skip to content

# Migration Guide: Fody to SourceGenerators

This guide helps you migrate from ReactiveUI.Fody to ReactiveUI.SourceGenerators for a modern, maintainable codebase.

Why Migrate?

Benefits of SourceGenerators

✅ Compile-Time Safety: Errors caught during compilation, not at runtime
✅ No IL Weaving: Simpler build process and better debugging
✅ IDE Support: Full IntelliSense and code navigation
✅ Visible Code: Generated code is accessible in your IDE
✅ Better Performance: No runtime overhead from IL weaving
✅ Future-Proof: Microsoft's recommended approach for code generation

Fody Limitations

❌ Post-Build Processing: Adds complexity to build pipeline
❌ Debugging Complexity: Hard to debug woven code
❌ Black Box: Generated code is invisible
❌ Build Time: IL weaving adds overhead
❌ Tooling Issues: Can conflict with other analyzers

Prerequisites

  • Minimum Requirements:
    • C# 12.0+
    • Visual Studio 17.8.0+ / Rider 2023.3+ / VS Code with C# Dev Kit
    • ReactiveUI 19.5.31+

Step 1: Install Source Generators

Remove Fody Packages

<!-- Remove these -->
<PackageReference Include="ReactiveUI.Fody" Version="*" />
<PackageReference Include="Fody" Version="*" />

Install SourceGenerators

<!-- Add these -->
<PackageReference Include="ReactiveUI.SourceGenerators" Version="*" PrivateAssets="all" />
<PackageReference Include="ReactiveUI.Primitives.ObservableEvents" Version="*" PrivateAssets="all" />

Remove FodyWeavers.xml

Delete the FodyWeavers.xml file from your project root.

Step 2: Migrate Reactive Properties

Fody Pattern

using ReactiveUI;
using ReactiveUI.Fody.Helpers;

public class MyViewModel : ReactiveObject
{
    [Reactive]
    public string Name { get; set; }
    
    [Reactive]
    public int Age { get; set; }
}

SourceGenerators Pattern

using ReactiveUI;
using ReactiveUI.SourceGenerators;

public partial class MyViewModel : ReactiveObject
{
    // Note: Class must be partial
    [Reactive]
    private string _name = string.Empty;
    
    [Reactive]
    private int _age;

    [Reactive]
    public partial string Address { get; set; }
}

Key Differences

AspectFodySourceGenerators
Class modifierNot requiredpartial required
Property/FieldPropertyPrivate field
Initialization{ get; set; }= defaultValue;
AccessPublic propertyGenerated public property

Step 3: Migrate ObservableAsPropertyHelper

Fody Pattern

public class MyViewModel : ReactiveObject
{
    [ObservableAsProperty]
    public string FullName { get; }
    
    public MyViewModel()
    {
        this.WhenAnyValue(x => x.FirstName, x => x.LastName,
                (f, l) => $"{f} {l}")
            .ToPropertyEx(this, x => x.FullName);
    }
}

Source Generator Pattern

ReactiveUI.SourceGenerators does not generate [ObservableAsProperty]. ReactiveUI.Binding provides it, and the ReactiveUI package brings ReactiveUI.Binding with it. Declare the property as partial and get-only. The generator writes the property body and a field named _{name}Helper, which you assign with ToProperty. Partial properties need C# 13 or later.

public partial class MyViewModel : ReactiveObject
{
    [Reactive]
    private string _firstName = string.Empty;
    
    [Reactive]
    private string _lastName = string.Empty;
    
    public MyViewModel()
    {
        _fullNameHelper = this.WhenAnyValue(static x => x.FirstName, static x => x.LastName,
                static (f, l) => $"{f} {l}")
            .ToProperty(this, static x => x.FullName, initialValue: string.Empty);
    }

    [ObservableAsProperty]
    public partial string FullName { get; }
}

Below C# 13, write the helper yourself:

private readonly ObservableAsPropertyHelper<string> _fullNameHelper;

public string FullName => _fullNameHelper.Value;

Earlier ReactiveUI.SourceGenerators releases put [ObservableAsProperty] on a field or a method and had an InitializeOAPH() method. The ReactiveUI.Binding migration guide covers moving that code.

Step 4: Migrate Reactive Commands

Fody Pattern

public class MyViewModel : ReactiveObject
{
    public ReactiveCommand<Unit, Unit> SaveCommand { get; }
    
    public MyViewModel()
    {
        SaveCommand = ReactiveCommand.CreateFromTask(SaveAsync);
    }
    
    private async Task SaveAsync()
    {
        // Implementation
    }
}

SourceGenerators Pattern

public partial class MyViewModel : ReactiveObject
{
    [ReactiveCommand]
    private async Task Save()
    {
        // Implementation
    }
    
    // Generated: public ReactiveCommand<Unit, Unit> SaveCommand { get; }
}

Command with CanExecute

public partial class MyViewModel : ReactiveObject
{
    [Reactive]
    private bool _isValid;
    
    private IObservable<bool> _canSave;
    
    public MyViewModel()
    {
        _canSave = this.WhenAnyValue(x => x.IsValid);
    }
    
    [ReactiveCommand(CanExecute = nameof(_canSave))]
    private async Task Save()
    {
        // Implementation
    }
}

Step 5: Handle Complex Scenarios

Property with Initial Value

// Fody
[Reactive]
public string Name { get; set; } = "Default";

// SourceGenerators ✅
[Reactive]
private string _name = "Default";

Property with Attributes

using System.Text.Json.Serialization;

// Fody
[Reactive]
[JsonPropertyName("user_name")]
public string UserName { get; set; }

// SourceGenerators ✅
[Reactive]
[property: JsonPropertyName("user_name")]
private string _userName = string.Empty;

Read-Only OAPH

// Fody
[ObservableAsProperty]
public string Status { get; }

// ReactiveUI.Binding ✅
[ObservableAsProperty]
public partial string Status { get; }

public MyViewModel()
{
    _statusHelper = SomeObservable
        .ToProperty(this, nameof(Status), string.Empty);
}

Command with Parameters

// Fody
public ReactiveCommand<string, Unit> ProcessCommand { get; }

public MyViewModel()
{
    ProcessCommand = ReactiveCommand.Create<string>(Process);
}

private void Process(string parameter) { }

// SourceGenerators ✅
[ReactiveCommand]
private void Process(string parameter)
{
    // Implementation
}

Step 6: Update Generic Types

Fody Limitation

Fody has issues with inline property initializers in generic types.

// Problematic with Fody ❌
public class MyViewModel<T> : ReactiveObject
{
    [Reactive]
    public string Name { get; set; } = "Default"; // May cause issues
}

SourceGenerators Solution

// Works perfectly ✅
public partial class MyViewModel<T> : ReactiveObject
{
    [Reactive]
    private string _name = "Default"; // No issues
}

Step 7: Rebuild and Test

1. Clean Solution

dotnet clean

2. Delete bin/obj Directories

# PowerShell
Get-ChildItem -Path . -Include bin,obj -Recurse -Directory | Remove-Item -Recurse -Force

# Bash
find . -type d \( -name bin -o -name obj \) -exec rm -rf {} +

3. Rebuild

dotnet build

4. View Generated Code

In Visual Studio:

  1. Expand Dependencies → Analyzers → ReactiveUI.SourceGenerators
  2. Browse generated files

Common Migration Issues

Issue 1: Class Not Partial

Error: The type 'MyViewModel' must be declared as partial

Solution: Add partial keyword to class declaration

// Before ❌
public class MyViewModel : ReactiveObject

// After ✅
public partial class MyViewModel : ReactiveObject

Issue 2: Property Instead of Field

Error: [Reactive] can only be applied to fields

Solution: Change property to field

// Before ❌
[Reactive]
public string Name { get; set; }

// After ✅
[Reactive]
private string _name = string.Empty;

Issue 3: Helper Field Not Assigned

Symptom: an [ObservableAsProperty] property always returns its type's default value.

Solution: Assign the generated _{name}Helper field with ToProperty in the constructor

public MyViewModel()
{
    _statusHelper = SomeObservable.ToProperty(this, static x => x.Status); // Add this line
}

Issue 4: Namespace Conflicts

Error: 'Reactive' is ambiguous

Solution: Remove old Fody namespaces

// Remove ❌
using ReactiveUI.Fody.Helpers;

// Keep ✅
using ReactiveUI.SourceGenerators;

Migration Checklist

  • Install ReactiveUI.SourceGenerators package
  • Remove ReactiveUI.Fody package
  • Remove Fody package
  • Delete FodyWeavers.xml
  • Add partial keyword to all reactive classes
  • Convert [Reactive] properties to fields or use partial properties
  • Make [ObservableAsProperty] properties partial and assign their _{name}Helper fields
  • Update .ToPropertyEx calls to .ToProperty
  • Migrate reactive commands using [ReactiveCommand]
  • Update using statements
  • Clean and rebuild solution
  • Run all unit tests
  • Test application functionality

Side-by-Side Comparison

Complete Example: Fody

using ReactiveUI;
using ReactiveUI.Fody.Helpers;

public class PersonViewModel : ReactiveObject
{
    [Reactive]
    public string FirstName { get; set; }
    
    [Reactive]
    public string LastName { get; set; }
    
    [ObservableAsProperty]
    public string FullName { get; }
    
    public ReactiveCommand<Unit, Unit> SaveCommand { get; }
    
    public PersonViewModel()
    {
        this.WhenAnyValue(x => x.FirstName, x => x.LastName,
                (f, l) => $"{f} {l}")
            .ToPropertyEx(this, x => x.FullName);
        
        SaveCommand = ReactiveCommand.CreateFromTask(SaveAsync);
    }
    
    private async Task SaveAsync()
    {
        // Save logic
    }
}

Complete Example: SourceGenerators

using ReactiveUI;
using ReactiveUI.SourceGenerators;

public partial class PersonViewModel : ReactiveObject
{
    [Reactive]
    private string _firstName = string.Empty;
    
    [Reactive]
    private string _lastName = string.Empty;
    
    public PersonViewModel()
    {
        _fullNameHelper = this.WhenAnyValue(static x => x.FirstName, static x => x.LastName,
                static (f, l) => $"{f} {l}")
            .ToProperty(this, static x => x.FullName, initialValue: string.Empty);
    }
    
    [ObservableAsProperty]
    public partial string FullName { get; }
    
    [ReactiveCommand]
    private async Task Save()
    {
        // Save logic
    }
}

Benefits After Migration

Code Quality

  • ✅ More explicit and readable
  • ✅ Better IDE support and IntelliSense
  • ✅ Easier code review
  • ✅ Visible generated code

Build Process

  • ✅ Faster builds (no IL weaving)
  • ✅ Simpler CI/CD pipeline
  • ✅ Better compatibility with other tools
  • ✅ Easier troubleshooting

Debugging

  • ✅ Step through generated code
  • ✅ Set breakpoints in properties
  • ✅ Inspect generated members
  • ✅ No black-box behavior

Additional Resources

Getting Help

If you encounter issues during migration:

  1. Check the GitHub Issues
  2. Ask on Slack
  3. Review Sample Code

Migration Time Estimate: 1-4 hours for a typical application Difficulty: Easy to Moderate Recommended: Yes - Modern tooling with long-term support