BrandGhost
C# Source Generator Attributes: Generating Code with ForAttributeWithMetadataName

C# Source Generator Attributes: Generating Code with ForAttributeWithMetadataName

C# Source Generator Attributes: Generating Code with ForAttributeWithMetadataName

If you have ever looked at libraries like AutoMapper, Refit, or the built-in System.Text.Json serialization in .NET and wondered "how does it know which types to process?" -- the answer is usually C# source generator attributes. Attributes are the most ergonomic, intention-revealing way to tell a source generator exactly which types, methods, or properties it should target for code generation. Once you understand this pattern, you gain the ability to eliminate entire categories of boilerplate from your codebase at compile time, with zero runtime overhead.

This guide walks through generating code from attributes step by step using the modern IIncrementalGenerator API and the ForAttributeWithMetadataName method, which is the recommended approach from the .NET 7 SDK onward. By the end you will have a fully working source generator that automatically produces code for any type you decorate with a custom marker attribute.

Why Attributes Are the Natural Trigger for Source Generators

Source generators have several ways to discover what they should process. They can scan all syntax nodes for patterns, use conventions based on class names, or look for interface implementations. But none of those approaches are as explicit or as developer-friendly as attributes.

When a developer writes [GenerateToString] above a class, that is a clear, opt-in declaration of intent. It is instantly readable, grep-able, and self-documenting. Other discovery approaches create implicit contracts that are easy to violate accidentally -- a naming convention is silently skipped if someone renames a class without knowing the convention existed in the first place.

Attributes also integrate naturally into IDE tooling. Code analysis, "Go to Definition" navigation, and completion all work for attributes exactly as they work for any other type. There is no magic, just a well-understood .NET construct that developers already know how to use.

Consider what it means to mark a class for automatic code generation of something like the factory method pattern. Instead of manually wiring up factory logic or using runtime reflection to discover types at startup, a source generator with a c# source generator attribute trigger produces the factory registration code at compile time. The result is zero-overhead, refactor-safe, and visible right at the type definition.

The Marker Attribute Pattern

A marker attribute is an attribute class that carries no data -- or minimal data -- and whose only purpose is to flag a type for processing. The pattern is clean precisely because it separates declaration ("this type needs generation") from configuration ("here are the options for that generation").

The canonical marker attribute targets a single declaration kind, disallows multiple applications on the same symbol, and opts out of inheritance. That last point matters especially for source generators. When Inherited = false, a derived class does not automatically inherit the attribute, so the source generator only processes the class where the attribute is explicitly applied. This prevents accidental code generation on subclasses and keeps the source generator behavior predictable and auditable.

You do not need to ship the marker attribute as a separate NuGet package. Instead, source generators inject the attribute source directly into the compilation using RegisterPostInitializationOutput. Consumers get the attribute automatically -- no extra package reference, no version mismatch, and no risk of the attribute type being defined twice. This self-contained approach is one of the most convenient aspects of attribute-driven code generation in .NET.

Step API Purpose
1. Inject marker attribute RegisterPostInitializationOutput Emits the attribute source into the compilation at build time
2. Discover decorated types ForAttributeWithMetadataName Finds all types using the source generator attribute efficiently
3. Extract attribute data GeneratorAttributeSyntaxContext Reads arguments and semantic info into a lean, cacheable model
4. Emit generated output ctx.AddSource Writes the partial class code into the compilation

Step 1: Injecting Your Marker Attribute via RegisterPostInitializationOutput

The first thing any attribute-driven source generator should do is register the attribute source code as a post-initialization output. This hooks into the Roslyn compilation pipeline before user code is analyzed, ensuring the attribute type is visible to both the generator and the IDE at design time.

context.RegisterPostInitializationOutput(static ctx =>
{
    ctx.AddSource("GenerateToStringAttribute.g.cs", """
        namespace MyGenerators;

        [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
        internal sealed class GenerateToStringAttribute : Attribute { }
        """);
});

A few things are worth pointing out here. The static keyword on the lambda is deliberate -- it prevents accidental closure captures that would break the incremental generator's caching. The attribute is placed in a dedicated namespace (MyGenerators) so it does not pollute the global namespace or collide with user-defined types. And because it is internal sealed, it cannot be subclassed or referenced across assembly boundaries.

The AllowMultiple = false constraint means if a developer accidentally applies the attribute twice to the same class, the compiler reports an error immediately -- catching the mistake at build time rather than producing confusing duplicate generated output.

Step 2: Discovering Attribute-Decorated Types with ForAttributeWithMetadataName

Once the attribute exists in the compilation, you need to find all types that use it. The ForAttributeWithMetadataName method on SyntaxProvider is specifically designed for this task. It is the recommended API for c# source generator attribute discovery from the .NET 7 SDK onward because it is both incremental and efficient -- Roslyn only re-runs it for syntax nodes that have actually changed between builds.

Here is the complete pipeline setup for the IIncrementalGenerator:

[Generator]
public sealed class GenerateToStringGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // Step 1: Inject the marker attribute into the compilation
        context.RegisterPostInitializationOutput(static ctx =>
        {
            ctx.AddSource("GenerateToStringAttribute.g.cs", """
                namespace MyGenerators;

                [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
                internal sealed class GenerateToStringAttribute : Attribute { }
                """);
        });

        // Step 2: Discover all classes decorated with [GenerateToString]
        var provider = context.SyntaxProvider
            .ForAttributeWithMetadataName(
                fullyQualifiedMetadataName: "MyGenerators.GenerateToStringAttribute",
                predicate: static (node, _) => node is ClassDeclarationSyntax,
                transform: static (ctx, _) => ExtractModel(ctx));

        // Step 3: Register source output for each discovered type
        context.RegisterSourceOutput(provider, static (spc, model) =>
            Execute(spc, model));
    }

    private static void Execute(
        SourceProductionContext ctx,
        ToStringModel model)
    {
        // Code generation logic -- see Step 4
    }
}

The fullyQualifiedMetadataName argument must match the fully qualified type name exactly, using + for nested types rather than .. Roslyn uses this string for a targeted lookup in the symbol table rather than scanning every node in the syntax tree. The predicate lambda is a fast, syntax-only filter that runs on the AST without semantic analysis, making it extremely cheap. Only when the predicate returns true does Roslyn invoke the transform lambda with full semantic context.

This two-phase approach -- cheap syntactic filter first, expensive semantic work only when needed -- is the core of what makes IIncrementalGenerator pipelines perform well even in large solutions with many types.

Step 3: Reading Attribute Arguments from GeneratorAttributeSyntaxContext

C# source generator attributes can carry data through constructor arguments and named properties. Reading those values lets your generator adapt its output based on user-supplied configuration. The GeneratorAttributeSyntaxContext gives you access to everything: the attribute's constructor arguments, named arguments, and the full SemanticModel for deeper analysis.

Suppose the marker attribute has an optional constructor argument and a named property:

// Reading attribute constructor arguments and named arguments
private static ToStringModel ExtractModel(GeneratorAttributeSyntaxContext ctx)
{
    var attributeData = ctx.Attributes[0];

    // Read the positional constructor argument (e.g., [GenerateToString("verbose")])
    string format = "default";
    if (attributeData.ConstructorArguments.Length > 0)
    {
        format = (string?)attributeData.ConstructorArguments[0].Value ?? "default";
    }

    // Read a named argument (e.g., [GenerateToString(IncludePrivate = true)])
    bool includePrivate = false;
    foreach (var (key, value) in attributeData.NamedArguments)
    {
        if (key == "IncludePrivate")
        {
            includePrivate = (bool)(value.Value ?? false);
        }
    }

    // Extract only plain, value-comparable data from the semantic symbol
    var symbol = (INamedTypeSymbol)ctx.TargetSymbol;
    var className = symbol.Name;
    var namespaceName = symbol.ContainingNamespace.IsGlobalNamespace
        ? null
        : symbol.ContainingNamespace.ToDisplayString();
    var propertyNames = symbol.GetMembers()
        .OfType<IPropertySymbol>()
        .Where(static p =>
            p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
        .Select(static p => p.Name)
        .ToList();
    return new ToStringModel(className, namespaceName, propertyNames, format, includePrivate);
}

One important discipline here is to extract all the information you need from ctx inside the transform lambda and return a plain, equatable data model rather than passing the GeneratorAttributeSyntaxContext itself downstream. The context holds references to large Roslyn objects that prevent incremental caching from working correctly. A lean model means Roslyn can skip re-running downstream steps when the extracted data has not changed -- this is the difference between a generator that keeps the IDE fast and one that causes lag on every keystroke.

The SemanticModel accessible through syntaxCtx.SemanticModel is available for cases where you need to resolve types beyond the attribute's own arguments, for example when inspecting property types that the generated code needs to interact with.

Step 4: Generating the Output Code

With a clean data model in hand, the final step is emitting source text into the compilation. The generated class must be partial to allow your generator to add members without replacing the user's own code. This is a fundamental pattern in C# source generators using attributes -- the user defines the shell, the generator fills it in.

private static void Execute(
    SourceProductionContext ctx,
    ToStringModel model)
{
    var namespaceName = model.Namespace;
    var className = model.ClassName;

    // Build interpolated segments: "Name = {Name}, Age = {Age}"
    var segments = string.Join(", ", model.PropertyNames.Select(p => $"{p} = {{{p}}}"));

    var sb = new StringBuilder();

    if (namespaceName is not null)
    {
        sb.AppendLine($"namespace {namespaceName};");
        sb.AppendLine();
    }

    sb.AppendLine($"partial class {className}");
    sb.AppendLine("{");
    sb.AppendLine("    public override string ToString() =>");
    sb.AppendLine($"        $"{className} {{{{ {segments} }}}}";");
    sb.AppendLine("}");

    ctx.AddSource(
        $"{className}.g.cs",
        SourceText.From(sb.ToString(), Encoding.UTF8));
}

Each generated file is added via ctx.AddSource. The hint name must be unique within the compilation -- using the class name as a suffix is a simple, reliable convention. For generators that may encounter multiple types with the same name in different namespaces, prefix the hint name with a hash of the fully qualified name to guarantee uniqueness.

Complete End-to-End Example: [GenerateToString]

Putting all four steps together, here is what consumer code looks like and what the source generator produces at compile time. The developer decorates any class with [GenerateToString] and marks it as partial:

using MyGenerators;

[GenerateToString]
public partial class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
}

// Generated code (added to the compilation automatically):
// partial class Person
// {
//     public override string ToString() =>
//         $"Person {{ Name = {Name}, Age = {Age} }}";
// }

// At runtime:
// new Person { Name = "Alice", Age = 30 }.ToString()
// => "Person { Name = Alice, Age = 30 }"

There is no runtime reflection, no code to maintain manually, and no risk of the ToString() output drifting out of sync with the class's properties. When the developer adds a new property to Person, the generator re-runs on the next build and the new property appears in the output automatically.

This same pattern scales naturally to more sophisticated scenarios. You can use c# source generator attributes to trigger generation of the builder pattern -- imagine [GenerateBuilder] producing a full fluent builder class for any POCO. The decorator pattern is another natural fit: an attribute on an interface can trigger generation of a logging or caching decorator, keeping the decorator in sync with the interface contract automatically as the interface evolves.

For attribute-driven service registration in DI frameworks, the same approach is used by libraries that produce plugin architectures and modular applications. An attribute on a class becomes a compile-time instruction to register it with the dependency injection container -- no assembly scanning at startup, no startup-time surprises.

Attribute Inheritance and Type Hierarchies

The Inherited = false setting on AttributeUsage is a deliberate choice, not a passive default. With Inherited = true on AttributeUsage, the runtime reflection behaviour for derived types changes -- but ForAttributeWithMetadataName is not affected. Roslyn's ISymbol.GetAttributes() only returns attributes that are directly declared on a symbol, regardless of the Inherited setting.

For most marker attribute scenarios, Inherited = false is the safer default. If you genuinely need generation across a type hierarchy -- for example, generating code for every concrete type that inherits from an attributed abstract base -- you can intentionally set Inherited = true and handle the full hierarchy in your transform logic.

ForAttributeWithMetadataName is built on ISymbol.GetAttributes() and shares its behaviour -- only symbols with the attribute directly declared are returned, regardless of AttributeUsage.Inherited. If your generator needs to process an entire type hierarchy starting from an attributed base class, the transform lambda is the right place to walk the hierarchy using the INamedTypeSymbol.BaseType chain. Setting Inherited = true on AttributeUsage will NOT cause derived classes to flow through the pipeline.

Using Multiple Attributes on the Same Type

A single type can be decorated with multiple generator attributes. Each attribute corresponds to a separate ForAttributeWithMetadataName pipeline, and each pipeline runs independently. This composability is one of the most powerful aspects of c# source generator attributes -- you can have [GenerateToString], [GenerateEquals], and [GenerateBuilder] all on the same class, with each generator producing its own partial class output file without any knowledge of the others.

The strategy pattern is a good example of where multiple attributes compose well. A [RegisterStrategy] attribute can trigger DI registration generation while a separate [GenerateStrategyValidator] attribute triggers input validation code -- both on the same strategy implementation class, with neither generator needing awareness of the other.

When multiple instances of the same attribute apply to the same type (AllowMultiple = true), ctx.Attributes in GeneratorAttributeSyntaxContext contains all of them as a sequence. Iterate all instances and generate a distinct output block for each. For generators with AllowMultiple = false, ctx.Attributes[0] is always the single instance.

For generators that enforce architectural rules at compile time -- such as ensuring that a [Singleton] service is never mistakenly registered with a scoped lifetime -- attributes carry that intent right into the type definition. This is the principle behind compile-time singleton enforcement where a marker attribute combined with a source generator analyzer can report diagnostics or emit registration code that makes a violation a build error rather than a runtime surprise.


Frequently Asked Questions

What is the difference between a marker attribute and a configuration attribute in source generators?

A marker attribute signals that a type should be processed -- it carries no data and its presence alone is the meaningful signal. A configuration attribute carries arguments that tell the generator how to process the type. In practice, many c# source generator attributes blend both roles: they mark the type for processing while also accepting optional configuration through constructor arguments or named properties. The distinction matters for design. If all arguments are optional, the attribute is primarily a marker with optional tuning. If arguments are required, it is primarily a configuration attribute. Starting with a pure marker and adding optional arguments incrementally as needs emerge is usually the cleanest evolutionary path.

Why use ForAttributeWithMetadataName instead of a general SyntaxProvider scan?

ForAttributeWithMetadataName is purpose-built for c# source generator attribute discovery and is significantly more efficient than a general node scan. It performs a targeted lookup in Roslyn's symbol table rather than visiting every syntax node in the compilation tree. It also integrates with the incremental generator caching model correctly, re-running only when the attribute usage or the decorated type changes. A general scan with a predicate that checks attribute names is both slower and more error-prone -- attribute names can be aliased using using directives, which a metadata name lookup handles correctly while a raw string comparison does not.

Can I use ForAttributeWithMetadataName on methods or properties, not just classes?

Yes. The predicate lambda filters syntax nodes, so you can check for MethodDeclarationSyntax, PropertyDeclarationSyntax, FieldDeclarationSyntax, or any other node type. You also need to update the AttributeUsage on your marker attribute to match -- AttributeTargets.Method or AttributeTargets.Property instead of AttributeTargets.Class. The rest of the pipeline works identically. The GeneratorAttributeSyntaxContext.TargetSymbol will be an IMethodSymbol or IPropertySymbol respectively, and ctx.TargetNode will be the corresponding syntax node, but all the same attribute argument reading APIs apply.

Why does my generator not trigger after I add the attribute to a class?

The most common cause is a fully qualified name mismatch in the fullyQualifiedMetadataName argument of ForAttributeWithMetadataName. The string must be the complete metadata name -- Namespace.AttributeName -- using + for nested types rather than .. If the attribute lives inside a nested class, write OuterClass+InnerAttribute. Also verify that your generator project references Microsoft.CodeAnalysis.CSharp and targets netstandard2.0, which is required for all Roslyn generator projects regardless of what framework the consuming project targets. Mismatched target frameworks are a common second cause of silent pipeline failures.

How do I test a source generator that reads attribute arguments?

The recommended approach is the Roslyn source generator test framework (Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing), which lets you pass synthetic C# source code as input and assert on the generated output strings without running a full build. Write your attribute argument extraction logic into small static helper methods that are easy to test in isolation from the Roslyn pipeline. For real-project debugging, add a Debugger.Launch() call at the entry point of Initialize and attach a debugger from Visual Studio -- or set <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> in the consuming project's .csproj to capture generator output to disk. Generated files will appear under obj/GeneratedFiles/ (or the path specified by <CompilerGeneratedFilesOutputPath>) and can be opened and inspected directly in the IDE.

Should the generated partial class use the same namespace as the user's class?

Yes, always. The generated file must use the same namespace as the declaring type so the partial class merges correctly with the user's code. Use symbol.ContainingNamespace.ToDisplayString() to retrieve the fully qualified namespace string from the INamedTypeSymbol. If the type is in the global namespace (symbol.ContainingNamespace.IsGlobalNamespace returns true), omit the namespace declaration from the generated file entirely. Never hardcode a namespace in your generator output -- it will break for any consumer who places their class in a different namespace.

Do C# source generator attributes affect runtime performance?

No. Attributes in .NET are metadata stored in the assembly manifest and are only loaded on demand via reflection. A source generator reads attribute data at compile time through Roslyn's semantic model and produces plain C# output with no reference to the attribute at all. The generated code runs without any awareness that an attribute triggered its creation. If you want to minimise the marker attribute's visibility at runtime, declare it as internal. It won't appear in the public API surface and cannot be referenced by type name from external assemblies -- though the attribute metadata is still present in the assembly IL. For compile-time-only concerns, this is typically sufficient.


Conclusion

C# source generator attributes are the cleanest, most intention-revealing way to drive compile-time code generation in .NET 7 and beyond. The pattern is consistent across every use case: inject the attribute via RegisterPostInitializationOutput, discover decorated types with ForAttributeWithMetadataName, extract a lean equatable model from GeneratorAttributeSyntaxContext, and emit partial class output via ctx.AddSource.

The real power of this approach is composability. Whether you are generating ToString overrides, builder classes, DI registrations, or cross-cutting concerns like the decorator pattern in C# -- the same four-step structure applies every time. The attribute is always the clear, opt-in declaration of intent, making the generated code discoverable, traceable, and safe to refactor.

Start with the simplest possible marker attribute and a straightforward Execute method. Ship it, let your team use it, and expand it with configuration arguments as real requirements emerge. The incremental generator infrastructure ensures that growing complexity in your generator does not come at the cost of IDE responsiveness or build performance for the developers who depend on it.

C# Source Generators: Reading the Roslyn Syntax Tree

Master reading and traversing a Roslyn syntax tree in a C# source generator. Learn SyntaxTree, SemanticModel, and ForAttributeWithMetadataName for .NET 6+.

C# Source Generators: A Complete Guide to Compile-Time Code Generation

Master C# source generators with this complete guide. Learn how compile-time code generation works in .NET 10, why it beats reflection, and how to get started.

How to Create Your First C# Source Generator (Step-by-Step)

Learn how to create a C# source generator with .NET 10. Step-by-step guide covering project setup, IIncrementalGenerator implementation, and packaging.

An error has occurred. This application may no longer respond until reloaded. Reload