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="ReactiveMarbles.ObservableEvents.SourceGenerator" 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
| Aspect | Fody | SourceGenerators |
|---|---|---|
| Class modifier | Not required | partial required |
| Property/Field | Property | Private field |
| Initialization | { get; set; } |
= defaultValue; |
| Access | Public property | Generated 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);
}
}
SourceGenerators Pattern
public partial class MyViewModel : ReactiveObject
{
[Reactive]
private string _firstName = string.Empty;
[Reactive]
private string _lastName = string.Empty;
[ObservableAsProperty]
private string _fullName = string.Empty;
public MyViewModel()
{
_fullNameHelper = this.WhenAnyValue(x => x.FirstName, x => x.LastName,
(f, l) => $"{f} {l}")
.ToProperty(this, nameof(FullName));
}
}
Alternative: Observable Property (New Feature)
public partial class MyViewModel : ReactiveObject
{
[Reactive]
private string _firstName = string.Empty;
[Reactive]
private string _lastName = string.Empty;
public MyViewModel()
{
// Simpler - InitializeOAPH() called automatically
InitializeOAPH();
}
[ObservableAsProperty]
private IObservable<string> FullName =>
this.WhenAnyValue(x => x.FirstName, x => x.LastName,
(f, l) => $"{f} {l}");
}
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; }
// SourceGenerators ✅
[ObservableAsProperty]
private string _status = string.Empty;
public MyViewModel()
{
_statusHelper = SomeObservable
.ToProperty(this, nameof(Status));
}
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:
- Expand Dependencies → Analyzers → ReactiveUI.SourceGenerators
- 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: Missing InitializeOAPH Call
Error: ObservableAsPropertyHelper not initialized
Solution: Call InitializeOAPH() in constructor
public MyViewModel()
{
InitializeOAPH(); // 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
partialkeyword to all reactive classes - [ ] Convert
[Reactive]properties to fields or usepartialproperties - [ ] Update
[ObservableAsProperty]properties - [ ] Update
.ToPropertyExcalls 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;
[ObservableAsProperty]
private string _fullName = string.Empty;
public PersonViewModel()
{
_fullNameHelper = this.WhenAnyValue(x => x.FirstName, x => x.LastName,
(f, l) => $"{f} {l}")
.ToProperty(this, nameof(FullName));
}
[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:
- Check the GitHub Issues
- Ask on Slack
- 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