C# > Source Generators > Using Roslyn for Code Generation > Real-World Use Cases

Implementing the INotifyPropertyChanged Interface

This example shows how to automatically generate the `INotifyPropertyChanged` interface implementation for properties in a class using a source generator. This reduces the amount of boilerplate code required for data binding in WPF or other UI frameworks.

Problem: INotifyPropertyChanged Boilerplate

Implementing `INotifyPropertyChanged` often involves writing repetitive code for each property to raise the `PropertyChanged` event. This becomes tedious and error-prone.

Solution: Source Generator for INotifyPropertyChanged

A source generator can analyze properties in a class and automatically generate the necessary code to implement `INotifyPropertyChanged`. This eliminates manual coding and ensures consistency.

Code: Defining the Attribute

This simple attribute marks a class as requiring `INotifyPropertyChanged` implementation.

// Define an attribute to mark classes for INotifyPropertyChanged generation
[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false)]
public class GenerateINotifyPropertyChangedAttribute : System.Attribute
{
}

Code: Defining the Source Generator

This source generator finds classes with the `GenerateINotifyPropertyChangedAttribute` attribute. It then generates the `INotifyPropertyChanged` interface implementation, including the event and the `OnPropertyChanged` method. For each public, non-read-only property, it generates a backing field and modifies the property's getter and setter to raise the `PropertyChanged` event.

// Source generator implementation
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Text;
using System.Linq;

[Generator]
public class INotifyPropertyChangedGenerator : ISourceGenerator
{
    public void Execute(GeneratorExecutionContext context)
    {
        // Find all classes decorated with the GenerateINotifyPropertyChangedAttribute
        var classSymbols = context.Compilation.SyntaxTrees
            .SelectMany(tree => tree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>())
            .Select(cds => context.Compilation.GetSemanticModel(cds.SyntaxTree).GetDeclaredSymbol(cds) as INamedTypeSymbol)
            .Where(ns => ns != null && ns.GetAttributes().Any(attr => attr.AttributeClass?.Name == "GenerateINotifyPropertyChangedAttribute"))
            .ToList();

        foreach (var classSymbol in classSymbols)
        {
            string sourceCode = GenerateINotifyPropertyChanged(classSymbol);
            context.AddSource($"{classSymbol.Name}_INotifyPropertyChanged.g.cs", SourceText.From(sourceCode, Encoding.UTF8));
        }
    }

    public void Initialize(GeneratorInitializationContext context) { }

    private string GenerateINotifyPropertyChanged(INamedTypeSymbol classSymbol)
    {
        string className = classSymbol.Name;
        string namespaceName = classSymbol.ContainingNamespace.ToString();

        StringBuilder sb = new StringBuilder();
        sb.AppendLine("using System.ComponentModel;");
        sb.AppendLine("using System.Runtime.CompilerServices;");
        sb.AppendLine($"namespace {namespaceName}");
        sb.AppendLine("{");
        sb.AppendLine($"    public partial class {className} : INotifyPropertyChanged");
        sb.AppendLine("    {");
        sb.AppendLine("        public event PropertyChangedEventHandler PropertyChanged;");
        sb.AppendLine("        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)");
        sb.AppendLine("        {");
        sb.AppendLine("            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));");
        sb.AppendLine("        }");

        foreach (var property in classSymbol.GetMembers().OfType<IPropertySymbol>())
        {
            if (!property.IsReadOnly && property.DeclaredAccessibility == Accessibility.Public)
            {
                string propertyName = property.Name;
                string fieldName = "_" + char.ToLowerInvariant(propertyName[0]) + propertyName.Substring(1);
                string propertyType = property.Type.ToString();

                sb.AppendLine($"        private {propertyType} {fieldName}; ");
                sb.AppendLine($"        public {propertyType} {propertyName}");
                sb.AppendLine("        {");
                sb.AppendLine("            get { return " + fieldName + "; }");
                sb.AppendLine("            set");
                sb.AppendLine("            {");
                sb.AppendLine("                if (Equals(" + fieldName + ", value)) return;");
                sb.AppendLine("                " + fieldName + " = value;");
                sb.AppendLine("                OnPropertyChanged(nameof(" + propertyName + "));");
                sb.AppendLine("            }");
                sb.AppendLine("        }");
            }
        }

        sb.AppendLine("    }");
        sb.AppendLine("}");

        return sb.ToString();
    }
}

Code: Using the Attribute

Here, the `MyViewModel` class is decorated with the `GenerateINotifyPropertyChangedAttribute`. This will automatically generate the `INotifyPropertyChanged` implementation for the `Name` and `Age` properties, eliminating the need to write the boilerplate code manually.

using INotifyPropertyChangedGenerator;

namespace MyNamespace
{
    [GenerateINotifyPropertyChanged]
    public partial class MyViewModel
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

Real-Life Use Case Section

In WPF, Xamarin, or other UI frameworks that rely on data binding, `INotifyPropertyChanged` is essential for automatically updating the UI when the underlying data changes. This source generator automates the implementation, making it easier to create data-bound applications. Imagine an application with dozens of properties. This generator would save a huge amount of time.

Best Practices

  • Use partial classes to separate the generated code from the hand-written code.
  • Handle different property types and validation logic appropriately.
  • Consider using a code formatter to ensure the generated code is well-formatted.

Concepts behind the snippet

This snippet utilizes Roslyn's compiler APIs to inspect C# code at compile time. The `ISourceGenerator` interface is implemented to define a custom generator. The `GeneratorExecutionContext` provides access to the compilation information and allows adding generated source code. The `SyntaxTree` and `SemanticModel` are used to analyze the code and extract information. Diagnostic reporting is used to notify the developer of any issues during code generation. The use of `partial` classes allows separation of concerns between generated and hand-written code.

When to use them

Use source generators when you have repetitive code generation tasks, such as implementing interfaces or generating data access layers. They are particularly useful when the generated code depends on external information, such as database schema or configuration files.

Interview Tip

When discussing source generators in an interview, be prepared to explain their purpose, how they work, and their benefits. Be able to provide real-world examples of when you would use them. Discuss their advantages over traditional code generation techniques like T4 templates.

Memory Footprint

Source generators execute during compilation, therefore they don't impact the runtime memory footprint of the application. The generated code itself might have memory implications based on its implementation (in this case, the extra field for each property), but the generator itself does not persist in the running application.

Alternatives

Alternatives to source generators include:

  • T4 Templates: These are text templates that can generate code, but they are less integrated with the compiler and can be harder to debug.
  • Fody (PropertyChanged.Fody): An IL weaver that automatically injects `INotifyPropertyChanged` implementation.
  • Manual Implementation: Writing the `INotifyPropertyChanged` code by hand.

Pros

  • Reduced boilerplate code.
  • Improved code maintainability.
  • Enhanced performance (code is generated at compile time).
  • Compile-time validation of generated code.

Cons

  • Increased complexity of the build process.
  • Steeper learning curve for developers.
  • Debugging source generators can be challenging.
  • Adds backing fields to your class even if they are not needed for other purposes.

FAQ

  • How do I debug a source generator?

    You can debug a source generator by attaching a debugger to the `msbuild.exe` process during compilation. Set breakpoints in your source generator code and inspect the variables to understand the code generation process.
  • How do I install a source generator?

    Source generators are typically distributed as NuGet packages. Add the package to your project, and the source generator will automatically run during compilation.
  • Why use `partial`?

    The `partial` keyword allows you to split a class definition across multiple files. This is useful for separating generated code from manually written code, making it easier to maintain and update the codebase.