Partial Types (partial)
#
The partial keyword allows for splitting a type definition across multiple files. This informs the compiler that the type definition might not be complete and to look for remaining parts in other project files. During compilation, all parts of the definition are merged into one cohesive whole. At runtime, the execution environment sees only one complete type definition.
One application is code organization. For large type definitions, we might decide to split, for example, initialization and logic into different files.
// File User.Data.cs
public partial class User
{
public string Name { get; set; }
public string Email { get; set; }
public string Pesel { get; set; }
}// File User.Logic.cs
public partial class User
{
public void Save()
{
// ...
}
}However, the partial keyword primarily allows us to separate human-written code from code automatically generated by the compiler via generators. This is most commonly used by UI systems (e.g., WPF, WinForms), which generate component initialization code based on files describing the appearance (e.g., .xaml files). Additionally, this mechanism can be used to generate repetitive code (e.g., ToString()), automatically implement interfaces, generate code checking regular expressions, or serialize types based on attributes.
Merging Rules #
When merging type parts, the compiler applies strict rules:
- Modifiers: All parts must have matching access modifiers. If you use
abstractorsealedin one part, the entire class becomes so. - Interfaces and Attributes: These are cumulative. If different files implement different interfaces, the resulting class implements all of them.
- Base Class: If specified, it must be identical in every part (or omitted).
It is worth remembering that for a generator to extend a programmer-written class, that class must also have the partial modifier.
Partial Methods (partial)
#
Partial methods allow declaring a method signature in one file and its implementation in another. They operate in two modes:
- Optional: If the method returns
void, is private, and has nooutparameters, its implementation is optional. If not provided, the compiler completely removes the method call from the resulting code. - Required (C# 9.0+): If the method has an access modifier (e.g.,
public) or returns a value, the implementation must be provided (usually by a generator).
public partial class User
{
partial void OnLoaded(); // Optional
[GeneratedRegex(".*@.*\\..*")]
private partial Regex EmailRegex(); // Required
}Source Generators #
Source Generators are a compiler mechanism introduced in C# 9.0 (significantly improved in later versions as Incremental Generators) that enables inspection of user code or other files during compilation and generation of new source files based on them. It is a form of compile-time metaprogramming.
Unlike reflection, source generators integrate directly with the compilation process. The generator first analyzes existing code and files. Based on the analysis, the generator creates C# source code text. The generated code is added to the compilation process as “virtual” files and compiled together with the rest of the project.
Generators can add new code but cannot modify existing code. This is why cooperation with partial types (partial) is so important, as they allow extending user classes with generated methods.
Generator Implementation #
A generator is a .NET Standard 2.0 library that implements the IIncrementalGenerator interface. It must be marked with the [Generator] attribute.
Below is an example of a more advanced generator that implements the Builder pattern for any class marked with the [GenerateBuilder] attribute. The generator analyzes public properties of the class and creates With... methods for them.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Linq;
using System.Text;
using System.Threading;
namespace BuilderGenerator
{
[Generator]
public class BuilderGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Add the marker attribute source code
context.RegisterPostInitializationOutput(ctx =>
{
ctx.AddSource("GenerateBuilderAttribute.g.cs", SourceText.From(
"""
using System;
namespace BuilderGenerator
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class GenerateBuilderAttribute : Attribute { }
}
""", Encoding.UTF8));
});
// Create the pipeline to find and transform classes
var pipeline = context.SyntaxProvider.ForAttributeWithMetadataName(
"BuilderGenerator.GenerateBuilderAttribute",
predicate: (node, _) => node is ClassDeclarationSyntax,
transform: (ctx, ct) => GetClassToGenerate(ctx, ct))
.Where(m => m != null);
// Register the source output
context.RegisterSourceOutput(pipeline, (ctx, data) => GenerateCode(ctx, data));
}
private static ClassModel? GetClassToGenerate(GeneratorAttributeSyntaxContext context, CancellationToken ct)
{
if (context.TargetSymbol is not INamedTypeSymbol symbol) return null;
ct.ThrowIfCancellationRequested();
var properties = symbol.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.IsReadOnly && p.DeclaredAccessibility == Accessibility.Public && p.SetMethod != null)
.Select(p => new PropertyModel(
p.Name,
p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
))
.ToList();
string ns = symbol.ContainingNamespace.IsGlobalNamespace
? string.Empty
: symbol.ContainingNamespace.ToDisplayString();
return new ClassModel(symbol.Name, ns, properties);
}
private static void GenerateCode(SourceProductionContext context, ClassModel? model)
{
if (model == null) return;
var code = new CodeWriter();
// Handle Namespace
if (!string.IsNullOrEmpty(model.Namespace))
{
code.AppendLine($"namespace {model.Namespace}");
code.StartBlock();
}
// Generate partial class
code.AppendLine($"public partial class {model.ClassName}");
using (code.Block())
{
code.AppendLine($"public static Builder CreateBuilder() => new Builder();");
code.AppendLine();
code.AppendLine($"public class Builder");
using (code.Block())
{
code.AppendLine($"private readonly {model.ClassName} _target = new {model.ClassName}();");
code.AppendLine();
foreach (var prop in model.Properties)
{
context.CancellationToken.ThrowIfCancellationRequested();
code.AppendLine($"public Builder With{prop.Name}({prop.Type} value)");
using (code.Block())
{
code.AppendLine($"_target.{prop.Name} = value;");
code.AppendLine("return this;");
}
code.AppendLine();
}
code.AppendLine($"public {model.ClassName} Build() => _target;");
}
}
if (!string.IsNullOrEmpty(model.Namespace))
{
code.EndBlock();
}
context.AddSource($"{model.ClassName}.Builder.g.cs", SourceText.From(code.ToString(), Encoding.UTF8));
}
}
}To use such a generator in an application project, we must add it as an Analyzer. This allows us to use the generated builders to create objects, even though we didn’t write them ourselves.
<ItemGroup>
<ProjectReference Include="..\BuilderGenerator\BuilderGenerator.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>using BuilderGenerator;
namespace App
{
[GenerateBuilder]
public partial class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
internal class Program
{
static void Main()
{
// Method CreateBuilder() and Builder class was generated:
var user = User.CreateBuilder()
.WithFirstName("John")
.WithLastName("Doe")
.WithAge(30)
.Build();
}
}
}Source code:
- BuilderGenerator/
- App/
- BuilderGenerator/
Applications in the .NET Platform #
System.Text.Json: Generates serialization code at compile time, allowing operation without reflection (crucial for AOT - Ahead-of-Time compilation).System.Text.RegularExpressions(.NET 7+): The[GeneratedRegex]attribute generates optimized C# code implementing regular expression logic instead of interpreting it at runtime.- Interoperability (
[LibraryImport], .NET 7+): Generates marshalling code (data passing) for P/Invoke, replacing dynamically generated stubs in[DllImport]. - Logging (
[LoggerMessage]): Allows generating strongly-typed, high-performance logging methods that avoid costly template parsing and argument boxing at runtime.