Skip to content

Getting Started

Build a complete [AutoNotify] source generator — from attribute to generated code — in one sitting. By the end you'll have a working generator, an analyzer that catches mistakes, and tests for both.

Prefer scaffolding?

Skip the manual setup — dotnet new roslynkit -n MyLib gives you all of this pre-wired with CI, docs, and packaging. See Templates.

Installation

dotnet new classlib -n MyProject.Generators -f netstandard2.0
cd MyProject.Generators
dotnet add package Deepstaging.Roslyn --prerelease
dotnet add package PolySharp

PolySharp provides compiler polyfills (record, init, etc.) for netstandard2.0 targets.

Then configure MyProject.Generators.csproj:

<PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>preview</LangVersion>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

Your First Generator

Delete Class1.cs and create AutoNotifyGenerator.cs — a complete incremental generator that finds classes with [AutoNotify] and generates INotifyPropertyChanged implementations from private backing fields:

using System;
using Deepstaging.Roslyn;
using Deepstaging.Roslyn.Emit;
using Deepstaging.Roslyn.Emit.Interfaces.Observable;
using Deepstaging.Roslyn.Generators;
using Microsoft.CodeAnalysis;

namespace MyProject;

[AttributeUsage(AttributeTargets.Class)]
public class AutoNotifyAttribute : Attribute;

[Generator]
public sealed class AutoNotifyGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var models = context.ForAttribute<AutoNotifyAttribute>()
            .Map(static (ctx, _) => ctx.TargetSymbol.AsValidNamedType().QueryAutoNotify());

        context.RegisterSourceOutput(models, static (ctx, model) => model
            .WriteAutoNotifyClass()
            .AddSourceTo(ctx, HintName.From(model.Namespace, model.TypeName)));
    }
}

[PipelineModel]
internal sealed record AutoNotifyModel(string Namespace, string TypeName, EquatableArray<FieldModel> Fields);

[PipelineModel]
internal sealed record FieldModel(string FieldName, string PropertyName, string TypeName);

file static class AutoNotifyExtensions
{
    extension(ValidSymbol<INamedTypeSymbol> symbol)
    {
        public AutoNotifyModel QueryAutoNotify() => new(
            symbol.Namespace ?? "Global",
            symbol.Name,
            symbol.QueryFields()
                .ThatArePrivate()
                .ThatAreInstance()
                .Where(f => f.Name.StartsWith("_"))
                .Select(f => new FieldModel(
                    f.Name,
                    f.Name.TrimStart('_').ToPascalCase(),
                    f.Type.FullyQualifiedName))
                .ToEquatableArray());
    }

    extension(AutoNotifyModel model)
    {
        public OptionalEmit WriteAutoNotifyClass() => TypeBuilder
            .Class(model.TypeName)
            .AsPartial()
            .InNamespace(model.Namespace)
            .ImplementsINotifyPropertyChanged()
            .WithEach(model.Fields, (type, field) => type
                .AddProperty(field.PropertyName, field.TypeName, p => p
                    .WithGetter(b => b.AddStatement($"return {field.FieldName}"))
                    .WithSetter(b => b
                        .AddStatement($"{field.FieldName} = value")
                        .AddStatement($"OnPropertyChanged()"))))
            .Emit();
    }
}

The ImplementsINotifyPropertyChanged() extension adds:

  • The INotifyPropertyChanged interface
  • The PropertyChanged event
  • A protected OnPropertyChanged helper with [CallerMemberName] support

Generator Context Extensions

The ForAttribute extension simplifies attribute-based symbol discovery:

// By generic type (requires attribute assembly reference)
context.ForAttribute<MyAttribute>()
    .Map((ctx, ct) => BuildModel(ctx));

// By Type — useful for generic attributes without backtick-arity strings
context.ForAttribute(typeof(MyAttribute<>))
    .Map((ctx, ct) => BuildModel(ctx));

// By fully qualified name (no assembly reference needed)
context.ForAttribute("MyNamespace.MyAttribute")
    .Map((ctx, ct) => BuildModel(ctx));

// With syntax predicate for filtering
context.ForAttribute<MyAttribute>()
    .Where(
        syntaxPredicate: (node, ct) => node is ClassDeclarationSyntax,
        builder: (ctx, ct) => BuildModel(ctx));

For non-attribute-based discovery, use MapTypes:

context.MapTypes((compilation, ct) =>
    TypeQuery.From(compilation)
        .ThatAreClasses()
        .ThatArePartial()
        .WithName("*Repository")
        .Select(t => new RepositoryModel(t.Name, t.Namespace ?? "")));

Core Concepts

These three layers compose to form the generator pipeline: QueryProjectEmit.

Queries — Find What You Need

Fluent filters over Roslyn symbols. Every query returns real ISymbol objects — no wrappers.

// Find all public async methods on a type
var methods = type.QueryMethods()
    .ThatArePublic()
    .ThatAreAsync()
    .GetAll();

// Find types by attribute + name pattern
var repos = TypeQuery.From(compilation)
    .ThatAreClasses()
    .ThatArePartial()
    .WithAttribute("RepositoryAttribute")
    .WithName("*Repository")
    .GetAll();

Projections — Safe Access to Nullable Data

Roslyn hands you nullable data at every turn. Projections wrap it in a type-safe container.

var attr = symbol.GetAttribute("MyAttribute");

// Early-exit pattern — no null checks
if (attr.IsNotValid(out var valid))
    return;

// Guaranteed non-null access to attribute arguments
var name = valid.NamedArg("Name").OrDefault("default");
var maxRetries = valid.NamedArg<int>("MaxRetries").OrDefault(3);
var targetType = valid.NamedArg<INamedTypeSymbol>("TargetType").OrNull();

Emit — Generate C# with Fluent Builders

Composable builders for classes, methods, properties, and more. Parse complex signatures from strings or build them piece by piece.

var code = TypeBuilder.Class("CustomerService")
    .InNamespace("MyApp.Services")
    .AsPartial()
    .AddMethod("GetById", "Task<Customer?>", m => m
        .AsPublic()
        .AsAsync()
        .AddParameter("id", "CustomerId")
        .WithBody(b => b
            .AddStatement("var entity = await _repo.FindAsync(id)")
            .AddStatement("return entity?.ToDomain()")))
    .Emit();

Next Steps

  • Templates

    Scaffold a full five-layer project with dotnet new roslynkit — generator, analyzers, code fixes, tests, CI, and docs.

  • End-to-End Walkthrough

    Trace a real production feature ([StrongId]) across all five layers.

  • Queries API

    Full reference for TypeQuery, MethodQuery, PropertyQuery, FieldQuery, and more.

  • Testing

    Snapshot testing, analyzer assertions, and code fix verification with RoslynTestBase.