Getting Started
Installation
dotnet add package ZeroAlloc.Notify
The source generator is bundled into the package, so this single PackageReference is all you need.
The standalone
ZeroAlloc.Notify.Generatorpackage is still published for backwards compatibility with existing direct PackageReferences, but new consumers should reference onlyZeroAlloc.Notify.
Your First Observable Property
1. Define a ViewModel
using ZeroAlloc.Notify;
[NotifyPropertyChangedAsync]
public partial class UserViewModel
{
[ObservableProperty]
private string _name = "Anonymous";
}
2. Subscribe to Changes
var vm = new UserViewModel();
// Subscribe to property changes
vm.PropertyChangedAsync += async (args, ct) =>
{
Console.WriteLine($"{args.PropertyName} changed to {args.NewValue}");
await Task.Delay(100, ct);
};
// Setting the property triggers handlers
await vm.SetNameAsync("Alice");
// Output: Name changed to Alice
Observable Collections
[NotifyCollectionChangedAsync]
public partial class TodoListViewModel
{
[ObservableProperty]
private ObservableCollection<string> _todos = new();
}
var vm = new TodoListViewModel();
vm.CollectionChangedAsync += async (args, ct) =>
{
Console.WriteLine($"Collection action: {args.Action}");
};
await vm.SetTodosAsync(new ObservableCollection<string> { "Task 1" });
Data Validation
using System.Collections;
using System.Collections.Concurrent;
using ZeroAlloc.Notify;
[NotifyDataErrorInfoAsync]
public partial class FormViewModel : INotifyDataErrorInfoAsync
{
private readonly ConcurrentDictionary<string, List<string>> _errors = new();
[ObservableProperty]
private string _email = "";
public bool HasErrors => !_errors.IsEmpty;
public IEnumerable GetErrors(string? propertyName)
=> propertyName is null
? _errors.Values.SelectMany(e => e)
: _errors.TryGetValue(propertyName, out var list) ? list : Enumerable.Empty<string>();
private async ValueTask ValidateEmailAsync(string value, CancellationToken ct)
{
_errors.Remove(nameof(Email), out _);
if (string.IsNullOrEmpty(value))
_errors[nameof(Email)] = new List<string> { "Email is required" };
else if (!value.Contains('@'))
_errors[nameof(Email)] = new List<string> { "Invalid email format" };
await RaiseErrorsChangedAsync(nameof(Email), ct);
}
}
var vm = new FormViewModel();
vm.PropertyChangedAsync += async (args, ct) =>
{
if (args.PropertyName == nameof(vm.Email))
await vm.ValidateEmailAsync((string)args.NewValue!, ct);
};
await vm.SetEmailAsync("not-an-email");
var hasErrors = vm.HasErrors;
var errors = vm.GetErrors(nameof(vm.Email));
Key Concepts
Async All the Way
ZeroAlloc.Notify is async-first. Even if your handlers are synchronous, the API uses ValueTask to support both blocking and non-blocking handlers without allocation.
// Synchronous handler — completes immediately
vm.PropertyChangedAsync += async (args, ct) =>
{
Console.WriteLine("Handler ran");
return; // No Task allocation needed
};
// Asynchronous handler — suspends and resumes
vm.PropertyChangedAsync += async (args, ct) =>
{
await Task.Delay(1000, ct);
Console.WriteLine("Handler resumed");
};
Sequential vs Parallel
By default, handlers run in parallel (fire-and-forget):
[NotifyPropertyChangedAsync]
[InvokeSequentially] // Enforce sequential handler execution for this class
public partial class MyViewModel
{
[ObservableProperty]
private string _name = "";
}
Source Generation
The source generator automatically creates:
- Setter methods (
SetNameAsync,SetEmailAsync) - Event raiser methods
- Implementation of
INotifyPropertyChangedAsync,INotifyCollectionChangedAsync, etc.
You apply the attributes; the generator creates the rest.
Nested and Generic Classes
A class nested in another type, or a generic class, gets its members on the class itself. The
generated file reopens every containing type, so each containing type has to be partial:
public static partial class Orders
{
[NotifyPropertyChangedAsync]
public partial class Entry<T>
{
[ObservableProperty]
private T? _value;
}
}
await new Orders.Entry<int>().SetValueAsync(7);
A nested class in a containing type that is not partial gets the warning
ZAN001 and nothing is generated for it.
Next Steps
- Observable Properties — Advanced property features
- Async Notifications — Deep dive into async event handling
- Performance — Benchmarks and optimization strategies
- Testing — Unit testing observable models