C# > Source Generators > Using Roslyn for Code Generation > Creating a Source Generator

Attribute-Driven Property Generator

This example demonstrates a source generator that automatically creates properties based on a custom attribute applied to a class. This simplifies the process of defining properties and reduces boilerplate code.

Concepts Behind the Snippet

This source generator utilizes a custom attribute, `GeneratePropertyAttribute`, to mark fields that should have properties automatically generated. The generator then analyzes the code, identifies fields marked with the attribute, and generates the corresponding properties with getter and setter implementations. This showcases a more practical use of source generators for reducing boilerplate and enhancing code maintainability.

Define the Attribute

First, we define the `GeneratePropertyAttribute`. This attribute can be applied to fields. It has an optional `PropertyName` property which allows the user to specify the name of the generated property. If not specified, the property name will be derived from the field name.

using System;

namespace MySourceGenerator
{
    [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
    public class GeneratePropertyAttribute : Attribute
    {
        public string PropertyName { get; set; }
        public GeneratePropertyAttribute(string propertyName = null)
        {
            PropertyName = propertyName;
        }
    }
}

Implement the Source Generator

This code defines the `PropertyGenerator`. The `Execute` method finds classes containing fields decorated with `GeneratePropertyAttribute`. For each such field, it generates a corresponding property. It extracts the property name either from the attribute's argument or by capitalizing the field name. The generated property is then added to the compilation. It handles the case where the `GenerateProperty` attribute may or may not have an argument.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Linq;
using System.Text;

namespace MySourceGenerator
{
    [Generator]
    public class PropertyGenerator : ISourceGenerator
    {
        public void Execute(GeneratorExecutionContext context)
        {
            // Find all classes with fields marked with the GenerateProperty attribute
            var classes = context.Compilation.SyntaxTrees
                .SelectMany(tree => tree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>())
                .Where(c => c.Members.Any(m => m.Kind() == Microsoft.CodeAnalysis.CSharp.SyntaxKind.FieldDeclaration && ((FieldDeclarationSyntax)m).AttributeLists.Any(a => a.ToString().Contains("GenerateProperty"))));

            foreach (var classDeclaration in classes)
            {
                var namespaceName = (classDeclaration.Parent as NamespaceDeclarationSyntax)?.Name.ToString() ?? "";
                var className = classDeclaration.Identifier.Text;

                var fieldsToGenerate = classDeclaration.Members
                    .OfType<FieldDeclarationSyntax>()
                    .Where(f => f.AttributeLists.Any(a => a.ToString().Contains("GenerateProperty"))) 
                    .Select(f => new
                    {
                        FieldDeclaration = f,
                        FieldName = f.Declaration.Variables.First().Identifier.Text,
                        FieldType = f.Declaration.Type.ToString(),
                        Attribute = f.AttributeLists.SelectMany(a => a.Attributes).FirstOrDefault(attr => attr.Name.ToString() == "GenerateProperty")
                    }).ToList();

                var sourceBuilder = new StringBuilder($"""
                    namespace {namespaceName}
                    {{
                        public partial class {className}
                        {{
                """);

                foreach (var field in fieldsToGenerate)
                {
                    string propertyName;
                    if (field.Attribute?.ArgumentList != null && field.Attribute.ArgumentList.Arguments.Any())
                    {
                       propertyName = field.Attribute.ArgumentList.Arguments.First().Expression.ToString().Trim('"');
                    }
                    else
                    {
                        // Create property name from field name
                        propertyName = char.ToUpper(field.FieldName[0]) + field.FieldName.Substring(1);
                    }

                    sourceBuilder.AppendLine($"            public {field.FieldType} {propertyName} {{ get; set; }}");
                }

                sourceBuilder.AppendLine($"""
                        }}
                    }}
                    """);

                context.AddSource($"{className}.Generated.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
            }
        }

        public void Initialize(GeneratorInitializationContext context)
        {
            // No initialization required for this example.
        }
    }
}

Example Usage

Here's how you would use the attribute. `myField` will get a property named `MyField`. `anotherField` will get a property named `CustomName`. Note the use of `partial class` which is important so the generated code can be merged with the existing class definition.

namespace MyNamespace
{
    public partial class MyClass
    {
        [GenerateProperty]
        private string myField;

        [GenerateProperty("CustomName")]
        private int anotherField;
    }
}

Real-Life Use Case

This attribute-driven property generator streamlines the process of creating properties for data transfer objects (DTOs) or view models. By simply adding the `GenerateProperty` attribute to fields, developers can automatically generate the necessary properties, reducing boilerplate code and improving code maintainability.

Best Practices

  • Provide clear and informative error messages if the generator encounters issues, such as invalid attribute usage or missing dependencies.
  • Use partial classes to allow users to extend the generated code without modifying the generated files directly.
  • Ensure the generated code follows coding conventions and best practices to maintain code quality.
  • Consider providing options for customizing the generated code, such as specifying property access modifiers or adding validation logic.

Interview Tip

Be prepared to discuss how attribute-driven code generation can improve code maintainability and reduce boilerplate. Explain the benefits of using source generators for tasks that involve repetitive code generation.

When to Use Them

Use attribute-driven code generation when you need to generate code based on specific attributes or metadata associated with code elements. This is particularly useful for scenarios where you want to automate the creation of properties, methods, or other code constructs based on specific configurations or annotations.

Memory Footprint

The memory footprint of the generator itself is minimal. The generated properties will increase the overall size of the class but typically not by a significant amount unless a large number of properties are generated. Optimize the generated code to avoid unnecessary memory allocations.

Alternatives

Alternatives include:

  • Manual Coding: Writing the properties manually, which is time-consuming and error-prone.
  • Code Snippets: Using code snippets to generate property templates, but this still requires manual intervention.
  • T4 Templates: Older technology for generating code, less integrated with the compiler, and can be harder to debug.

Pros

  • Reduced boilerplate code: Automatically generates properties, reducing manual coding effort.
  • Improved code maintainability: Simplifies the process of defining properties and reduces errors.
  • Increased productivity: Allows developers to focus on more important tasks.

Cons

  • Debugging complexity: Debugging generated code can be challenging.
  • Learning curve: Requires understanding of the Roslyn API and compiler concepts.
  • Potential for code bloat: Generating too many properties can increase the size of the class.

FAQ

  • How do I handle errors in my source generator?

    Use the `GeneratorExecutionContext.ReportDiagnostic` method to report errors or warnings to the user. Provide informative messages that help the user understand and resolve the issue.
  • How can I customize the generated code?

    Provide options for customizing the generated code, such as allowing users to specify property access modifiers, add validation logic, or modify the naming conventions. Use attributes or configuration files to allow users to configure the generator.
  • How do I test my source generator?

    Create unit tests that verify the generated code under various conditions. Use the `CSharpGeneratorDriver` class to simulate the compilation process and assert that the generated code is correct.