Modern INotifyPropertyChanged in C#: Generic Equality Checks and Best Practices
Understanding CS0019: How to Compare Generic Types in C#
When attempting to write a generic helper method for INotifyPropertyChanged, using the standard inequality operator (!=) on an unconstrained generic type T triggers compiler error CS0019 (Operator '!=' cannot be applied to operands of type 'T' and 'T'). This occurs because C# cannot guarantee at compile time that type T overrides the inequality operator.
To check for inequality safely across both value types (like int or custom structs) and reference types (handling null values gracefully), use EqualityComparer<T>.Default.
protected bool SetProperty<T>(ref T backingField, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(backingField, value))
{
return false;
}
backingField = value;
NotifyPropertyChanged(propertyName);
return true;
}Classic Boilerplate: The SetProperty Pattern
By returning a bool from your helper method, you can easily execute additional logic when a property actually changes. Here is how a custom base ViewModel class traditionally looks:
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T backingField, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(backingField, value))
return false;
backingField = value;
NotifyPropertyChanged(propertyName);
return true;
}
}And here is how you use it inside a property setter:
private string _name = string.Empty;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}The Modern Approach: CommunityToolkit.Mvvm
If you are building modern .NET applications—especially in .NET MAUI, WPF, or WinUI 3—you generally should not write `INotifyPropertyChanged` boilerplate manually anymore. The official Microsoft solution is the CommunityToolkit.Mvvm package (formerly MVVM Light/Toolkit).
It uses C# Source Generators to eliminate backing fields and property boilerplate entirely during compilation.
1. Inheriting from ObservableObject
Using the toolkit, your base class functionality is provided by ObservableObject:
using CommunityToolkit.Mvvm.ComponentModel;
public class UserViewModel : ObservableObject
{
private string _name = string.Empty;
public string Name
{
get => _name;
set => SetProperty(ref _name, value); // Built-in SetProperty
}
}2. Ultimate Syntactic Sugar: [ObservableProperty]
By applying the [ObservableProperty] attribute to a private field, the toolkit automatically generates the public property, generic inequality check, and property changed notifications for you at compile time:
using CommunityToolkit.Mvvm.ComponentModel;
public partial class UserViewModel : ObservableObject
{
[ObservableProperty]
private string _name;
}The source generator converts the code above into a complete public Name property with full notification support behind the scenes.
Summary & Recommendations
- To compare generic types safely: Use
EqualityComparer<T>.Default.Equals(field, value)instead of operator!=. - For custom implementations: Implement the
SetProperty(ref T storage, T value)pattern in your base class. - For modern .NET/MAUI projects: Install
CommunityToolkit.Mvvmand use the[ObservableProperty]source generator attribute to keep code clean and maintainable.