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
INotifyPropertyChangedinterface - The
PropertyChangedevent - A protected
OnPropertyChangedhelper 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: Query → Project → Emit.
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¶
-
Scaffold a full five-layer project with
dotnet new roslynkit— generator, analyzers, code fixes, tests, CI, and docs. -
Trace a real production feature (
[StrongId]) across all five layers. -
Full reference for
TypeQuery,MethodQuery,PropertyQuery,FieldQuery, and more. -
Snapshot testing, analyzer assertions, and code fix verification with
RoslynTestBase.