EffectSharp.SourceGenerators
1.4.0
dotnet add package EffectSharp.SourceGenerators --version 1.4.0
NuGet\Install-Package EffectSharp.SourceGenerators -Version 1.4.0
<PackageReference Include="EffectSharp.SourceGenerators" Version="1.4.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="EffectSharp.SourceGenerators" Version="1.4.0" />
<PackageReference Include="EffectSharp.SourceGenerators"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add EffectSharp.SourceGenerators --version 1.4.0
#r "nuget: EffectSharp.SourceGenerators, 1.4.0"
#:package EffectSharp.SourceGenerators@1.4.0
#addin nuget:?package=EffectSharp.SourceGenerators&version=1.4.0
#tool nuget:?package=EffectSharp.SourceGenerators&version=1.4.0
EffectSharp.SourceGenerators
Incremental Roslyn source generator for EffectSharp that turns simple C# classes into reactive view models using attributes. It generates:
- Reactive properties from fields annotated with
[ReactiveField] - Read-only computed properties from methods annotated with
[Computed] ReactiveCollection<T>computed from methods annotated with[ComputedList]with minimal updates- Command properties from methods annotated with
[FunctionCommand] - Watch/effect subscriptions from methods annotated with
[Watch] - Boilerplate:
INotifyPropertyChanging,INotifyPropertyChanged,IReactive,InitializeReactiveModel()and a deep-tracking method (TrackDeep()orTrackDeepReactiveModel())
This project is implemented as an Incremental Generator for fast, scalable builds and a smooth IDE experience.
Installation
Add package references to your project:
<ItemGroup>
<PackageReference Include="EffectSharp" Version="<latest>" />
<PackageReference Include="EffectSharp.SourceGenerators" Version="<latest>" PrivateAssets="all" />
</ItemGroup>
Notes:
PrivateAssets="all"prevents the generator from flowing transitively to consumers of your library.- Works with SDK-style projects using .NET 5+ (source generator support). The generator itself targets
netstandard2.0.
Quick Start
Annotate your view model with attributes and call the generated initializer in the constructor:
using EffectSharp;
using EffectSharp.SourceGenerators;
[ReactiveModel]
public partial class CounterViewModel : IDisposable
{
[ReactiveField]
private int _count = 0;
[FunctionCommand]
public void Increment() => Count++; // Generated property
[FunctionCommand(CanExecute = nameof(CanDecrement))]
public void Decrement() => Count--;
public bool CanDecrement() => Count > 0;
[Computed]
public string ComputeDisplayCount() => $"Current Count: {Count}";
[ReactiveField(EqualsMethod = null)] // disable equality check
private int _maxCount = 0;
private ReactiveCollection<(int Count, DateTime Timestamp)> _records = new();
[Watch(nameof(Count))]
private void OnCountChanged(int newCount, int oldCount)
{
if (MaxCount < newCount)
{
MaxCount = newCount;
records.Add((newCount, DateTime.Now));
}
}
[ReactiveField]
private bool _orderByCount = true;
[ComputedList(KeySelector = "x => x.Count")]
public List<(int Count, DateTime Timestamp)> CurrentRecords()
{
if (OrderByCount)
{
return records.OrderBy(r => r.Count).ToList();
}
else
{
return records.OrderBy(r => r.Timestamp).ToList();
}
}
public CounterViewModel()
{
InitializeReactiveModel(); // Generated method
}
public void Dispose()
{
DisposeReactiveModel(); // Generated method
}
}
What gets generated (conceptually):
- Implementation of
INotifyPropertyChangingandINotifyPropertyChanged - Implementation of
IReactiveand a deep-tracking method for dependency tracking:- If your type (or any base type) does not declare a parameterless
TrackDeepmethod, the generator emitspublic void TrackDeep(). - If your type does declare a parameterless
TrackDeepmethod (any accessibility and any return type), the generator instead emitspublic void TrackDeepReactiveModel()containing the default deep-tracking logic. You can call this helper from your ownTrackDeepimplementation to reuse the generated behavior.
- If your type (or any base type) does not declare a parameterless
public void InitializeReactiveModel()that creates computed values, subscribes watchers, and hooks change notificationspublic void DisposeReactiveModel()that disposes computed values and watcherspublic int Count { get; set; }withPropertyChanging/Changednotification and reactive dependency trackingpublic int IFunctionCommand<object> IncrementCommand { get; }for theIncrementmethod withCanExecutesupportpublic string DisplayCount { get; }computed fromComputeDisplayCount()public int MaxCount { get; set; }with no equality check on setReactive.Watchsubscription forOnCountChangedinInitializeReactiveModel()public bool OrderByCount { get; set; }with notificationpublic ReactiveCollection<(int Count, DateTime Timestamp)> CurrentRecords { get; }computed list fromCurrentRecords()method with minimal updates.
Attributes
[ReactiveModel] (class)
Marks a partial class as a reactive model. The generator adds interfaces and generated members to this partial type
with an InitializeReactiveModel() method to wire up reactive properties, computed values, commands, and watchers
and a DisposeReactiveModel() method to release resources.
[ReactiveField] (field)
Generates a reactive property for the field.
Options:
EqualsMethod(string): custom equality method used to short-circuit unchanged sets.- Must be a fully resolvable callable returning
boolwith signature compatible with(oldValue, newValue). - Default is
EqualityComparer<T>.Default.
- Must be a fully resolvable callable returning
Example:
[ReactiveField(EqualsMethod = "MyEqualityComparer.Equals")]
private string _name;
[Computed] (method)
Generates a read-only property computed from the method.
Naming:
- If the method name starts with
Compute, the property name is the method name withoutCompute(e.g.,ComputeTotal→Total). - Otherwise the property name is prefixed with
Computed(e.g.,Total→ComputedTotal).
Options:
Setter(string): optional setter callback invoked when the computed value is assigned via the generatedComputed<T>wrapper.
[ComputedList] (method)
Generates a read-only ReactiveCollection<T> property computed from the method returning an IList<T>.
Naming:
- If the method name starts with
Compute, the property name is the method name withoutCompute(e.g.,ComputeItems→Items). - Otherwise the property name is prefixed with
Computed(e.g.,Items→ComputedItems).
Options:
KeySelector(string): optional key selector expression used to identify items for minimal updates.- Must be a fully resolvable expression of type
Func<T, TKey>. - If not provided, items are compared by reference.
- Must be a fully resolvable expression of type
EqualityComparer(string): optional equality comparer expression used to compare items for minimal updates.- Must be a fully resolvable expression of type
EqualityComparer<T>. - If not provided,
EqualityComparer<T>.Defaultis used.
- Must be a fully resolvable expression of type
[FunctionCommand] (method)
Generates a command property exposing either IFunctionCommand or IAsyncFunctionCommand depending on the method signature.
Supported method shapes:
- Sync:
TResult Method(TParam param)orvoid Method() - Async:
async Task<TResult> Method(TParam param, CancellationToken ct)orTask Method()
Options:
CanExecute(string): expression of a parameterless method returningboolused forCanExecute.AllowConcurrentExecution(bool, defaulttrue): set tofalseto serialize command executions.ExecutionScheduler(string): scheduler expression used only for async commands.
[Watch] (method)
Creates an effect that re-runs when the specified properties change.
Options:
Values(string[]): array of value expressions to watch; more than one creates a tuple(v1, v2, ...).Immediate(bool, defaultfalse): iftrue, runs the watcher immediately upon initialization.Deep(bool, defaultfalse): iftrue, tracks deep changes onIReactiveproperties.Once(bool, defaultfalse): iftrue, runs the watcher only once when any of the values change.Scheduler(string):Action<Effect>scheduler expression to schedule watcher execution.SuppressEquality(bool, defaulttrue): iftrue, the watcher will not run if the new and old values are equal.EqualityComparer(string, defaultnull):EqualityComparer<T>expression used to compare new and old values whenSuppressEqualityistrue.
Supported method shapes:
(),(newValue)or(newValue, oldValue)- When multiple values are watched, use a tuple for the value parameters.
[Deep] (field, property or computed/computed-list method)
Marks a member whose value should participate in deep tracking; the value must be or may be IReactive.
Resource Disposal
The generator emits a unified disposal method to release reactive resources created during initialization:
- Generated method:
public void DisposeReactiveModel() - What it does:
- Disposes all generated
Computed<T>instances andWatcheffects (Effect) created inInitializeReactiveModel(). - Clears corresponding backing fields to
nullfor idempotent repeated calls.
- Disposes all generated
- When to call:
- When the reactive model is no longer needed (e.g., view deactivation, window closing).
- Typical integration points: implement
IDisposableon your view model and callDisposeReactiveModel()fromDispose(), or call it from lifecycle hooks.
- Idempotency:
- Safe to call multiple times; calling
DisposeReactiveModel()afterInitializeReactiveModel()will release resources. If you need the model again, callInitializeReactiveModel()to re-wire computations and watchers.
- Safe to call multiple times; calling
Diagnostics
The generator ships analyzers that validate attribute usage.
See AnalyzerReleases.Unshipped.md and AnalyzerReleases.Shipped.md for release tracking.
Build and Try
Build the solution:
# from the repository root
dotnet build -c Debug
Run the WPF counter example (Windows):
dotnet build Examples/Example.Wpf.Counter/Example.Wpf.Counter.csproj -c Debug
Inspect generated sources (optional):
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
Generated files will appear under obj/generated/. Look for *.ReactiveModel.g.cs next to your annotated types.
Notes
- Always invoke
InitializeReactiveModel()once (e.g., in the constructor) to initialize computed values and watchers. - Deep tracking:
- By default the generator implements
TrackDeep()which callsTrackDeep()on nestedIReactivemembers to propagate dependency tracking. - If you define your own parameterless
TrackDeepmethod on the model (or a base type), the generator will emitTrackDeepReactiveModel()instead; your implementation can call this helper to compose custom logic with the default behavior.
- By default the generator implements
- This project is an Incremental Generator, making it efficient for large solutions and responsive in the IDE.
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.