Skip to content

Getting Started

This guide walks you through building your first source generator with Deepstaging.Roslyn.

Installation

Add the package to your analyzer/generator project:

dotnet add package Deepstaging.Roslyn

Your .csproj should target netstandard2.0 for Roslyn compatibility:

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

Your First Generator

Here's a complete incremental generator that finds classes with [AutoNotify] and generates property change notifications:

using Deepstaging.Roslyn.Generators;

namespace Deepstaging.Generators;

/// <inheritdoc />
public class AutoNotifyAttribute : Attribute;

/// <inheritdoc />
[Generator]
public class AutoNotifyGenerator : IIncrementalGenerator
{
    /// <inheritdoc />
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.ForEach(
            query: ctx => ctx.ForAttribute<AutoNotifyAttribute>().Map((c, _) => new AutoNotifyModel(c.TargetSymbol)),
            generate: (ctx, model) =>
            {
                var hint = new HintName(model.Namespace);

                // Build the partial class
                var code = TypeBuilder.Class(model.TypeName)
                    .InNamespace(model.Namespace)
                    .AsPartial()
                    .AddUsing("System.ComponentModel")
                    .WithEach(model.Fields, (builder, field) => builder
                        .AddProperty(field.PropertyName,
                            field.Type.FullyQualifiedName, p => p
                                .WithGetter(b => b.AddStatement($"return {field.Name}"))
                                .WithSetter(b => b
                                    .AddStatement($"{field.Name} = value")
                                    .AddStatement($"OnPropertyChanged(nameof({field.ParameterName}))"))));

                ctx.AddFromEmit(
                    hint.Filename(model.TypeName),
                    code.Emit()
                );
            });
    }
}

internal record AutoNotifyModel(ISymbol? Type)
{
    public ISymbol? Type { get; init; } = Type;
    private readonly ValidSymbol<INamedTypeSymbol> _symbol = Type.AsValidNamedType();

    public string TypeName => _symbol.Name;
    public string Namespace => _symbol.Namespace ?? "Global";

    public ImmutableArray<ValidSymbol<IFieldSymbol>> Fields => _symbol.QueryFields()
        .ThatArePrivate()
        .ThatAreInstance()
        .GetAll();
}

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 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.Value)));

Core Concepts

Queries

Find symbols with fluent, composable filters:

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

See Queries for the full API.

Projections

Work safely with nullable Roslyn data:

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

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

// Now you have guaranteed non-null access
var name = valid.NamedArg("Name").OrDefault("default");

See Projections for details.

Emit

Generate C# code with fluent builders:

var code = TypeBuilder.Class("Generated")
    .AddMethod("Execute", "void", m => m
        .AsPublic()
        .WithBody(b => b.AddStatement("Console.WriteLine(\"Hello\")")))
    .Emit();

See Emit for the full API.

Next Steps