Supercharging .NET MAUI with Incremental Source Generators

πŸš€ Supercharging .NET MAUI with Incremental Source Generators

Eliminating Boilerplate and Building Compile-Time Tooling for Modern Mobile Applications

One of the biggest strengths of .NET MAUI is its ability to build cross-platform applications using a single codebase. However, as applications grow, so does the amount of repetitive infrastructure code developers must maintain. Think about a typical enterprise MAUI application. For every new feature, we usually end up writing code like this:

  • Registering pages and services in Dependency Injection
  • Creating BindableProperty declarations
  • Writing repetitive ICommand implementations
  • Declaring route constants
  • Wrapping Preferences
  • Creating strongly typed accessors for resources
  • Mapping DTOs to domain models
  • Registering platform services None of this is business logic. It's infrastructure. And infrastructure tends to become boilerplate. The larger the application becomes, the more boilerplate accumulates. The unfortunate consequence is that developers spend a significant amount of time writing code that follows exactly the same pattern over and over again. For example, registering services in a medium-sized application usually looks something like this:
builder.Services.AddTransient<HomePage>();
builder.Services.AddTransient<HomeViewModel>();

builder.Services.AddTransient<SettingsPage>();
builder.Services.AddTransient<SettingsViewModel>();

builder.Services.AddTransient<LoginPage>();
builder.Services.AddTransient<LoginViewModel>();

builder.Services.AddTransient<ProductPage>();
builder.Services.AddTransient<ProductViewModel>();

builder.Services.AddTransient<CartPage>();
builder.Services.AddTransient<CartViewModel>();

Now imagine an application with:

  • 80 pages
  • 80 ViewModels
  • dozens of services
  • repositories
  • platform implementations This registration file quickly grows into hundreds of lines that developers rarely enjoy maintaining. The same problem appears in many other areas. Creating a custom control often starts with dozens of lines dedicated exclusively to BindableProperty declarations.
public static readonly BindableProperty TitleProperty =
    BindableProperty.Create(
        nameof(Title),
        typeof(string),
        typeof(MyCardView),
        default(string));

public string Title
{
    get => (string)GetValue(TitleProperty);
    set => SetValue(TitleProperty, value);
}

There is nothing inherently difficult about this code. It's simply repetitive. Likewise, wrapping application settings typically results in repetitive calls to Preferences.Get() and Preferences.Set(), while resource files generate yet another layer of boilerplate just to expose strongly typed properties. Although these patterns are common, they all share one important characteristic:

The compiler already has enough information to generate them automatically.

Instead of manually writing infrastructure code, what if the compiler could produce it for us? That's precisely where Incremental Source Generators become incredibly powerful. Rather than generating code at runtime through reflection or relying on handwritten templates, Incremental Source Generators execute during compilation, analyzing your project and producing additional C# files before the application is built. The result is cleaner projects, improved compile-time safety, fewer runtime errors, and significantly less repetitive code.


πŸ€” What Are Incremental Source Generators?

Introduced as part of the Roslyn compiler platform, Incremental Source Generators allow developers to inspect source code during compilation and generate additional C# files based on what they discover. Unlike traditional code generation tools, Incremental Source Generators are deeply integrated into the compiler itself. They don't execute after the build. They are part of the build. The overall process can be visualized like this:

              Your Source Code
                     β”‚
                     β–Ό
         Roslyn Compilation Pipeline
                     β”‚
                     β–Ό
      Incremental Source Generator
                     β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚                       β”‚
         β–Ό                       β–Ό
 Generated C# Files       Diagnostics
         β”‚
         β–Ό
      Final Compilation
         β”‚
         β–Ό
      Application

Because generators execute before the compilation finishes, the generated files behave exactly as if they had always existed inside your project. Visual Studio recognizes them. IntelliSense recognizes them. The compiler recognizes them. Even refactoring tools understand them. From the developer's perspective, generated code feels completely native.


⚑ Why Incremental?

Before Incremental Generators, Roslyn already supported traditional Source Generators. While useful, classic generators suffered from one major limitation: Every modification forced the generator to execute again over the entire compilation. Imagine changing a single line inside one ViewModel. Even though only one file changed, the generator would inspect every syntax tree again. As projects became larger, build performance deteriorated. Incremental Generators solve this problem by introducing a cache-aware pipeline. Instead of rebuilding everything, they process only the portions of the compilation affected by the latest changes. A simplified view looks like this:

            Source Files
                 β”‚
                 β–Ό
        Syntax Discovery
                 β”‚
                 β–Ό
       Semantic Analysis
                 β”‚
                 β–Ό
      Incremental Cache
                 β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚                 β”‚
 Changed Input      Cached Results
        β”‚                 β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 β–Ό
         Code Generation

If only one annotated class changes, only that branch of the pipeline executes again. Everything else comes directly from cache. The result is dramatically faster builds, especially in enterprise applications containing hundreds or even thousands of source files.


πŸ“Š Classic vs Incremental Generators

Although both approaches generate code during compilation, their internal architecture differs considerably.

Feature Classic Generator Incremental Generator
Executes on every build βœ… ❌
Incremental caching ❌ βœ…
Reuses previous computations ❌ βœ…
Better scalability ⚠️ βœ…
Optimized for large projects ❌ βœ…
Deterministic pipeline ⚠️ βœ…
Recommended by Microsoft ❌ βœ…

For most modern projects, Incremental Generators should always be the preferred choice. They are faster, easier to optimize, and designed specifically for large codebases like enterprise .NET MAUI applications.


πŸ“± Why Does This Matter in .NET MAUI?

Unlike backend applications, mobile applications typically contain a very high number of repetitive artifacts. Consider a simple feature. Adding a single page often means creating:

  • A View
  • A ViewModel
  • Dependency Injection registration
  • Commands
  • Bindable properties
  • Resource strings
  • Configuration values
  • Platform-specific implementations Most of these follow predictable conventions. That predictability makes them excellent candidates for compile-time generation. Rather than asking developers to write repetitive infrastructure, Incremental Source Generators can automatically generate large portions of it, allowing teams to focus on what truly matters:

Building features instead of maintaining boilerplate.

This becomes especially valuable in enterprise solutions, where consistency, maintainability, and compile-time validation are just as important as runtime performance.

πŸ—οΈ Project Structure

Before writing a single line of generator code, it's important to understand that Source Generators are completely independent projects. They are compiled as .NET analyzers and executed by the Roslyn compiler during the build process. Unlike a typical class library, a generator doesn't become part of your application's runtime. Instead, it acts as a compile-time extension of the compiler itself. A common solution structure looks like this:

MySolution
β”‚
β”œβ”€β”€ MauiApp
β”‚   β”œβ”€β”€ Pages
β”‚   β”œβ”€β”€ ViewModels
β”‚   β”œβ”€β”€ Services
β”‚   └── MauiProgram.cs
β”‚
β”œβ”€β”€ MauiApp.Generators
β”‚   β”œβ”€β”€ Attributes
β”‚   β”œβ”€β”€ Helpers
β”‚   β”œβ”€β”€ Pipelines
β”‚   β”œβ”€β”€ Templates
β”‚   └── MyGenerator.cs
β”‚
└── MauiApp.Generators.Tests

Each project has a very specific responsibility.

Project Responsibility
MauiApp Your .NET MAUI application
MauiApp.Generators Roslyn Incremental Source Generator
MauiApp.Generators.Tests Unit tests validating generated code

Separating responsibilities this way keeps the generator reusable across multiple MAUI applications while allowing it to evolve independently from the application itself.


πŸ”– Defining a Custom Attribute

Source Generators need a way to know what should be processed. The most common approach is to use custom attributes. For this article, we'll create a simple attribute named AutoRegister.

namespace MauiApp.Generators.Attributes;

[AttributeUsage(AttributeTargets.Class)]
public sealed class AutoRegisterAttribute : Attribute
{
    public ServiceLifetime Lifetime { get; }

    public AutoRegisterAttribute(ServiceLifetime lifetime =
        ServiceLifetime.Transient)
    {
        Lifetime = lifetime;
    }
}

The attribute is intentionally small. It only exposes a single property indicating how the service should be registered inside the Dependency Injection container. Developers can now decorate any service like this:

[AutoRegister]
public class ProductService
{
}

Or specify a different lifetime when necessary.

[AutoRegister(ServiceLifetime.Singleton)]
public class CacheService
{
}

At first glance, these attributes appear to do nothing. That's because they aren't intended to execute at runtime. Instead, they serve as markers that the Source Generator will discover during compilation.


πŸ” Discovering Annotated Types

The first responsibility of our generator is identifying every class decorated with AutoRegisterAttribute. Incremental Source Generators accomplish this through the Syntax Provider. Rather than inspecting every symbol in the compilation, Roslyn performs an initial syntax-only filtering step. A simplified implementation looks like this.

[Generator]
public sealed class AutoRegisterGenerator : IIncrementalGenerator
{
    public void Initialize(
        IncrementalGeneratorInitializationContext context)
    {
        var provider = context.SyntaxProvider
            .CreateSyntaxProvider(
                predicate: IsCandidate,
                transform: GetSemanticTarget);
    }
}

Although only a few lines of code, this pipeline is one of the biggest reasons Incremental Generators scale so well. Instead of loading semantic information for every file, Roslyn performs the work in two distinct stages.

            Source Files
                  β”‚
                  β–Ό
        Syntax Predicate Filter
                  β”‚
          Candidate Classes
                  β”‚
                  β–Ό
        Semantic Transformation
                  β”‚
                  β–Ό
        Valid AutoRegister Types

This two-phase approach dramatically reduces the amount of work performed during compilation.


⚑ Syntax Filtering

The predicate function should remain extremely lightweight. Its only responsibility is answering a simple question:

"Could this syntax node possibly be interesting?"

For our generator, we're only interested in class declarations that contain attributes.

private static bool IsCandidate(
    SyntaxNode node,
    CancellationToken token)
{
    return node is ClassDeclarationSyntax classDeclaration &&
           classDeclaration.AttributeLists.Count > 0;
}

Notice what we're not doing. We're not checking attribute names. We're not loading symbols. We're not inspecting inheritance. We're not querying assemblies. At this stage, Roslyn hasn't even created semantic information yet. We're simply reducing the search space. Out of thousands of syntax nodes, perhaps only a few dozen will continue to the next phase.


🧠 Semantic Analysis

Once Roslyn has identified possible candidates, it can safely construct the Semantic Model. This is where the compiler understands what the code actually means. Syntax alone tells us that something looks like a class. Semantic analysis tells us:

  • Which namespace it belongs to
  • Which interfaces it implements
  • Its base class
  • Generic arguments
  • Constructor parameters
  • Applied attributes
  • Accessibility
  • Whether symbols are partial
  • Whether referenced types actually exist This transformation usually resembles the following:
private static INamedTypeSymbol? GetSemanticTarget(
    GeneratorSyntaxContext context,
    CancellationToken token)
{
    var declaration =
        (ClassDeclarationSyntax)context.Node;

    return context.SemanticModel
                  .GetDeclaredSymbol(declaration)
           as INamedTypeSymbol;
}

Now we no longer work with syntax. We work directly with Roslyn symbols. That distinction is incredibly important. A syntax node only represents text. An INamedTypeSymbol represents the actual compiled type, complete with inheritance, metadata, and relationships to the rest of the project.


🎯 Finding the Attribute

Once we have the symbol, locating our custom attribute becomes straightforward.

var attribute = classSymbol
    .GetAttributes()
    .FirstOrDefault(a =>
        a.AttributeClass?.Name ==
        "AutoRegisterAttribute");

If the attribute exists, we can inspect every constructor argument and named property. For example, retrieving the configured lifetime:

var lifetime =
    attribute?.ConstructorArguments[0].Value;

This gives the generator all the information it needs to produce dependency registration code automatically. Because the compiler performs this work during the build, developers receive immediate feedback whenever the attribute is misused or configured incorrectly. No reflection. No startup scanning. No runtime discovery. Everything happens before the application is even compiled.


πŸ”„ Building the Incremental Pipeline

One of the biggest advantages of Incremental Source Generators is that they encourage a functional pipeline rather than a monolithic generation process. Instead of writing a single method that scans the entire compilation, each stage performs one small transformation. A typical pipeline looks like this:

           Syntax Nodes
                 β”‚
                 β–Ό
          Filter Classes
                 β”‚
                 β–Ό
        Semantic Symbols
                 β”‚
                 β–Ό
      Classes with AutoRegister
                 β”‚
                 β–Ό
      Extract Registration Data
                 β”‚
                 β–Ό
         Generate C# Source

Each step produces immutable data that can be cached and reused across builds. If only one service changes, Roslyn doesn't need to repeat the entire processβ€”it simply recomputes the affected branch while reusing everything else. This incremental architecture is what allows modern Source Generators to remain performant even in solutions containing hundreds of projects and thousands of source files.

πŸ”„ Generating Dependency Injection Code

At this point, our generator has successfully identified every class decorated with AutoRegisterAttribute and extracted the necessary metadata from the semantic model. Now comes the most interesting part: generating source code. Instead of asking developers to manually register every service inside MauiProgram.cs, our generator can automatically produce those registrations during compilation. Suppose our project contains the following services:

[AutoRegister]
public class ProductService
{
}

[AutoRegister(ServiceLifetime.Singleton)]
public class SettingsService
{
}

[AutoRegister]
public class OrderService
{
}

Rather than writing this manually:

builder.Services.AddTransient<ProductService>();
builder.Services.AddSingleton<SettingsService>();
builder.Services.AddTransient<OrderService>();

our generator can produce the registration code automatically. A generated file might look like this:

public static partial class GeneratedServiceCollectionExtensions
{
    public static IServiceCollection AddGeneratedServices(
        this IServiceCollection services)
    {
        services.AddTransient<ProductService>();
        services.AddSingleton<SettingsService>();
        services.AddTransient<OrderService>();

        return services;
    }
}

Because this file is generated during compilation, it behaves exactly like handwritten code. Developers only need a single line inside MauiProgram.cs.

builder.Services.AddGeneratedServices();

As new services are added or removed, the generated extension method updates automatically during the next build. No additional maintenance is required.


πŸ“„ Generated Files

One of the most common misconceptions about Source Generators is that they modify existing source files. They don't. Generators never edit developer code. Instead, Roslyn creates additional files that become part of the compilation. The resulting structure looks something like this.

MauiApp
β”‚
β”œβ”€β”€ Services
β”‚   β”œβ”€β”€ ProductService.cs
β”‚   β”œβ”€β”€ SettingsService.cs
β”‚   └── OrderService.cs
β”‚
└── Generated Files
    └── GeneratedServiceCollectionExtensions.g.cs

Notice the .g.cs extension. This naming convention clearly indicates that the file was produced automatically. Developers should never edit these files manually, since they'll be regenerated during the next compilation. This separation keeps generated code isolated while allowing the original source files to remain clean and focused on business logic.


βš™οΈ Behind the Scenes

Generating source code is surprisingly straightforward. Once the pipeline has collected all discovered services, the generator simply builds the corresponding C# source and injects it into the compilation. Conceptually, the process looks like this.

Annotated Classes
        β”‚
        β–Ό
Extract Metadata
        β”‚
        β–Ό
Build C# Source
        β”‚
        β–Ό
ctx.AddSource(...)
        β”‚
        β–Ό
Generated File

Roslyn then treats the generated file as if it had always been part of the project. This means:

  • IntelliSense recognizes it.
  • The debugger can step through it.
  • The compiler validates it.
  • Refactoring tools understand it.
  • Errors are reported normally. From the application's perspective, there is no distinction between handwritten code and generated code.

πŸ“± Practical .NET MAUI Scenarios

Automatic Dependency Injection registration is only the beginning. Once you understand how Incremental Source Generators work, you'll start noticing many repetitive patterns across every .NET MAUI application. Let's look at some of the most common candidates.


🧩 Generating Bindable Properties

Creating custom controls is one of the areas where developers write the most repetitive code. A single property usually requires a BindableProperty declaration, a CLR wrapper, and metadata describing default values and callbacks. For example:

public static readonly BindableProperty TitleProperty =
    BindableProperty.Create(
        nameof(Title),
        typeof(string),
        typeof(ProfileCard),
        default(string));

public string Title
{
    get => (string)GetValue(TitleProperty);
    set => SetValue(TitleProperty, value);
}

Now imagine a custom control exposing ten configurable properties. Most of the implementation consists of infrastructure rather than business logic. With a Source Generator, the developer could simply declare the intent.

[AutoBindable]
private string _title;

During compilation, the generator could produce the complete BindableProperty implementation automatically. The handwritten code becomes dramatically smaller while remaining fully type-safe.


🎯 Generating Commands

Another common pattern appears in MVVM applications. Developers frequently write similar ICommand implementations for simple actions. Instead of manually creating commands:

public ICommand RefreshCommand { get; }

a generator could inspect methods marked with an attribute.

[GenerateCommand]
private async Task RefreshAsync()
{
}

The compiler could automatically generate:

  • The ICommand
  • Async execution logic
  • Reentrancy protection
  • Exception forwarding
  • Cancellation support All without writing repetitive infrastructure code.

🌎 Strongly Typed Resources

Localization is another excellent candidate. Resource files are typically accessed through string keys.

AppResources["Login_Title"]

The problem is simple. Misspell the key and the application won't fail until runtime. A generator can inspect .resx files during compilation and expose every resource as a strongly typed property. Instead of this:

AppResources["Login_Title"]

developers work with:

AppResources.LoginTitle

This small improvement provides:

  • IntelliSense support
  • Compile-time validation
  • Easier refactoring
  • Better discoverability without changing how localization works internally.

βš™οΈ Strongly Typed Preferences

Application settings often rely on string identifiers as well.

Preferences.Set("Theme", "Dark");

Preferences.Get("Theme", "Light");

Although functional, this approach introduces several risks:

  • Typographical errors
  • Inconsistent naming
  • Difficult refactoring
  • Hidden dependencies A Source Generator could transform a simple configuration class into a strongly typed API. Developers declare:
[Preference]
public string Theme { get; set; }

and the generator produces the complete wrapper around Preferences, ensuring that all keys remain centralized, discoverable, and validated at compile time.


πŸš€ Eliminating Infrastructure, Not Flexibility

A common concern when discussing code generation is the fear of losing control over the generated implementation. In practice, the opposite is often true. Incremental Source Generators remove repetitive infrastructure while allowing developers to continue writing the parts that actually matter. Instead of replacing custom logic, they automate the repetitive plumbing surrounding it. The application remains fully customizableβ€”the generator simply handles the predictable patterns that developers would otherwise write manually every time. As projects grow, this balance becomes increasingly valuable, allowing teams to spend less time maintaining infrastructure and more time delivering features.

πŸ›‘οΈ Emitting Compiler Diagnostics

One of the most overlooked advantages of Incremental Source Generators is that they don't just generate codeβ€”they can also improve the developer experience by reporting compile-time diagnostics. Think about the kinds of mistakes developers make every day.

  • Forgetting to decorate a required class.
  • Applying an attribute to the wrong type.
  • Registering duplicate services.
  • Using invalid constructor parameters.
  • Missing interfaces.
  • Violating architectural conventions. Without a generator, these problems usually surface at runtime. With Roslyn, they can be detected while the project is compiling. Instead of throwing exceptions, a generator can produce compiler diagnostics that appear directly inside Visual Studio. For example, imagine a developer accidentally decorates an interface instead of a class.
[AutoRegister]
public interface IProductService
{
}

Rather than silently ignoring itβ€”or worse, generating invalid codeβ€”the generator can report a meaningful compiler error.

AUTOGEN001

The AutoRegister attribute can only be applied to concrete classes.

The error immediately appears in the Error List window, allowing developers to fix the issue before the application ever runs.


🚨 Creating Meaningful Diagnostics

Roslyn diagnostics are defined using a DiagnosticDescriptor. Each diagnostic contains structured information describing the problem.

private static readonly DiagnosticDescriptor InvalidTarget =
    new(
        id: "AUTOGEN001",
        title: "Invalid AutoRegister target",
        messageFormat:
            "The AutoRegister attribute can only be applied to concrete classes.",
        category: "SourceGenerator",
        DiagnosticSeverity.Error,
        isEnabledByDefault: true);

A well-designed diagnostic should clearly communicate:

Property Purpose
Id Unique identifier (AUTOGEN001)
Title Short description
Message Detailed explanation
Severity Info, Warning or Error
Category Logical grouping

Good diagnostics save developers time by pointing directly to the source of the problem instead of leaving them to interpret generic compiler messages.


πŸ“ Reporting Diagnostics

Once the generator identifies an invalid scenario, reporting it is straightforward.

context.ReportDiagnostic(
    Diagnostic.Create(
        InvalidTarget,
        classDeclaration.GetLocation()));

Notice that the diagnostic is associated with a specific location in the source code. This allows Visual Studio to highlight the offending line automatically. From the developer's perspective, it feels like a built-in compiler error. That level of integration is one of Roslyn's greatest strengths.


πŸ’‘ Diagnostics vs Exceptions

A common mistake among developers building their first Source Generator is throwing exceptions whenever something unexpected happens. For example:

throw new InvalidOperationException(
    "Invalid AutoRegister usage.");

Although technically correct, this approach provides a poor development experience. Instead of helping the developer, the build simply fails with an exception stack trace. Diagnostics are a much better alternative.

Exception Diagnostic
Breaks the build unexpectedly Integrated into the compiler
Difficult to understand Clear and actionable
Stack trace only Highlights the exact source location
Poor IDE experience First-class Visual Studio support

Whenever the issue originates from user code, prefer diagnostics over exceptions. Exceptions should generally be reserved for unexpected failures inside the generator itself.


πŸ§ͺ Testing Incremental Source Generators

Like any other component, Source Generators should be tested. Fortunately, Roslyn provides excellent testing APIs that allow generators to be executed entirely in memory. Rather than creating physical projects, tests can compile source code, execute the generator, and verify the generated output. A typical test validates three things:

  • Generated source files
  • Compiler diagnostics
  • Final compilation result The overall workflow looks like this.
Input Source Code
        β”‚
        β–Ό
Roslyn Compilation
        β”‚
        β–Ό
Run Generator
        β”‚
        β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β–Ό             β–Ό
Generated Files   Diagnostics
        β”‚             β”‚
        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
               β–Ό
          Assertions

This approach makes generator testing deterministic, repeatable, and independent of Visual Studio.


βœ… Verifying Generated Code

Suppose our input consists of a single service.

[AutoRegister]
public class ProductService
{
}

The test can verify that the generated source contains the expected registration.

services.AddTransient<ProductService>();

Rather than comparing every line, most tests focus on the presence of critical generated fragments. This keeps tests resilient to formatting changes while ensuring the generator behaves correctly.


🚫 Testing Invalid Scenarios

Generators shouldn't only be tested against valid input. Invalid cases are equally important. For example:

  • Missing attributes
  • Duplicate registrations
  • Unsupported generic types
  • Interfaces instead of classes
  • Abstract classes
  • Internal visibility
  • Nested types Each of these scenarios should produce predictable diagnostics. Testing these cases ensures that the generator fails gracefully rather than producing incomplete or incorrect source code.

⚑ Performance Considerations

One of the primary reasons Microsoft introduced Incremental Generators was performance. However, simply implementing IIncrementalGenerator doesn't automatically make a generator fast. Performance still depends on how the pipeline is designed. Some common recommendations include:

βœ… Filter as early as possible

The predicate should eliminate as many syntax nodes as possible before semantic analysis begins. Every unnecessary semantic lookup increases build time.


βœ… Keep transformations focused

Each pipeline stage should perform one specific task. Large monolithic transformations are harder to cache and less efficient.


βœ… Avoid repeated semantic analysis

The semantic model is one of the most expensive resources in the Roslyn pipeline. Extract only the information you actually need. Avoid querying symbols multiple times for the same declaration.


βœ… Generate deterministic output

Given the same input, a generator should always produce the same source code. Deterministic output enables Roslyn to maximize cache reuse and minimizes unnecessary recompilation.


βœ… Minimize allocations

Although generators run during compilation, large solutions may execute them thousands of times. Avoid excessive string concatenation, unnecessary collections, or repeated object creation inside hot paths. Small optimizations become significant as projects scale.


πŸ“ˆ Scaling to Enterprise Solutions

The real value of Incremental Source Generators becomes evident in large applications. Consider a .NET MAUI solution with:

  • 150 pages
  • 150 ViewModels
  • 80 services
  • 40 repositories
  • 20 platform-specific implementations
  • Thousands of resource entries Maintaining all of the surrounding infrastructure manually quickly becomes a burden. Incremental Source Generators shift that responsibility to the compiler. Instead of asking every developer to follow the same conventions repeatedly, those conventions are encoded once inside the generator. The result is a codebase that is:
  • More consistent
  • Easier to maintain
  • Less error-prone
  • Better suited for refactoring
  • Simpler to onboard new developers As the application grows, the amount of handwritten infrastructure remains relatively constant, while the generated code scales automatically alongside the project.

πŸ“Š Comparing Traditional Development with Source Generation

At first glance, writing a few extra lines of infrastructure code may not seem like a significant issue. However, the impact becomes much more noticeable as applications evolve. Every manually maintained registration, wrapper, or helper introduces another piece of code that must be reviewed, tested, and kept synchronized with the rest of the solution. Incremental Source Generators address this problem by moving predictable infrastructure from handwritten code to the compiler itself. The comparison below summarizes the difference.

Feature Traditional Approach Incremental Source Generator
Dependency Injection Registration ❌ Manual βœ… Generated
BindableProperty Declarations ❌ Manual βœ… Generated
ICommand Boilerplate ❌ Manual βœ… Generated
Strongly Typed Resources ❌ Manual βœ… Generated
Preferences Wrappers ❌ Manual βœ… Generated
Compile-time Validation ⚠️ Limited βœ… Built-in
IntelliSense Support ⚠️ Partial βœ… Excellent
Runtime Reflection ⚠️ Often Required βœ… None
Refactoring Safety ⚠️ Developer Dependent βœ… Compiler Assisted
Project Maintainability ⚠️ Degrades Over Time βœ… Consistent

Notice that the primary goal isn't simply writing fewer lines of code. The real objective is reducing the amount of infrastructure developers are responsible for maintaining throughout the application's lifetime.


πŸš€ Beyond Boilerplate

One of the biggest misconceptions surrounding Source Generators is that they're only useful for generating repetitive code. In reality, they enable an entirely different style of framework design. Instead of asking developers to configure every component manually, frameworks can expose simple, declarative APIs while leaving the repetitive implementation details to the compiler. This approach enables developers to focus on describing what they want rather than how it should be wired together. For example, a developer may simply write:

[AutoRegister]
public class NotificationService
{
}

or

[GenerateCommand]
private async Task SaveAsync()
{
}

or

[AutoBindable]
private string _title;

Each of these declarations represents intent rather than implementation. The compiler takes responsibility for producing the repetitive infrastructure that would otherwise be written by hand. As applications grow, this declarative style becomes easier to read, easier to review, and significantly easier to maintain.


πŸ“± The Future of .NET MAUI Tooling

Incremental Source Generators are already transforming many areas of the .NET ecosystem. Within .NET MAUI, they open the door to an entirely new generation of developer tooling. Some interesting possibilities include:

  • πŸ“¦ Automatic Dependency Injection registration
  • 🎨 BindableProperty generation
  • 🎯 Command generation
  • 🌍 Strongly typed localization
  • βš™οΈ Configuration wrappers
  • πŸ’Ύ Preferences APIs
  • πŸ” Permission helpers
  • 🌐 REST client generation
  • πŸ“‘ SignalR proxy generation
  • πŸ—„οΈ SQLite model helpers
  • 🧩 Resource key generation
  • πŸ“± Platform service registration Most of these patterns already exist in production applications today. The difference is that they are often maintained manually. Incremental Source Generators allow us to automate these patterns while preserving type safety, improving consistency, and reducing maintenance costs. As the Roslyn ecosystem continues to evolve, it's likely that compile-time tooling will become an increasingly common part of modern .NET MAUI development.

βœ… Key Benefits

🌟 Why Use Incremental Source Generators in .NET MAUI?

  • πŸš€ Reduce repetitive infrastructure by generating predictable code during compilation.
  • πŸ›‘οΈ Improve compile-time safety by validating conventions before the application runs.
  • ⚑ Increase productivity by allowing developers to focus on business logic instead of boilerplate.
  • 🧠 Enhance IntelliSense through strongly typed generated APIs.
  • πŸ”„ Simplify refactoring by eliminating fragile string-based patterns and repetitive registrations.
  • πŸ“¦ Keep projects organized by centralizing infrastructure generation inside reusable tooling.
  • πŸ“ˆ Scale large applications more effectively without proportionally increasing maintenance effort.
  • βš™οΈ Leverage Roslyn's incremental pipeline for efficient builds that process only the code affected by recent changes.

πŸ”š Final Thoughts

Incremental Source Generators represent much more than an elegant way to generate C# code. They fundamentally change how repetitive infrastructure is handled within modern .NET applications. Rather than relying on runtime discovery, reflection, or manually maintained conventions, developers can move predictable work into the compilation process, allowing the compiler to generate consistent, type-safe, and maintainable code automatically. For .NET MAUI applications, where repetitive patterns are common across dependency injection, custom controls, commands, configuration, and resource management, this approach can significantly reduce maintenance costs while improving the overall development experience. Perhaps the greatest advantage is that Source Generators don't replace developersβ€”they amplify them. They automate the repetitive work, enforce architectural consistency, and integrate seamlessly with the compiler, allowing development teams to focus on solving real business problems instead of maintaining infrastructure code. As the .NET ecosystem continues to embrace compile-time tooling, Incremental Source Generators are quickly becoming an essential skill for developers building scalable, modern, and enterprise-ready applications.


πŸ“š Additional Resources

If you'd like to dive deeper into Incremental Source Generators, Roslyn, and the compiler APIs behind them, here are some excellent official resources:


An unhandled error has occurred. Reload πŸ—™