How the Ceres Source Generator Uses Partial Classes and GenerateFlowAttribute
The Ceres source generator scans compilation for partial classes marked with [GenerateFlow] and emits companion .gen.cs files that implement IFlowGraphContainer and/or IFlowGraphRuntime interfaces, merging seamlessly with user code at compile time.
The Ceres library (akikurisu/ceres) eliminates boilerplate when building flow-graph systems by leveraging Roslyn source generators. Understanding how the Ceres Source Generator works with partial classes and the GenerateFlowAttribute is essential for implementing custom flow containers without writing repetitive interface implementations.
Why Partial Classes Are Required for the Ceres Source Generator
The generator emits a second source file containing another part of the same class definition. C# merges multiple partial class declarations into a single type at compile time, allowing generated members to coexist with user-written members. If the class were not declared partial, the generator would create a duplicate type definition, causing a compilation error.
This requirement is enforced in CeresSyntaxReceiver (registered in FlowGraphGenerator.Initialize), which explicitly checks for the partial modifier before adding a class to the candidate list:
if (!classNode.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) return;
Candidates.Add(classNode);
How the GenerateFlowAttribute Controls Code Generation
The GenerateFlowAttribute (defined in Runtime/Flow/Annotations/GenerateFlowAttribute.cs) acts as the generation trigger and provides configuration via two boolean properties: GenerateImplementation and GenerateRuntime.
Syntax Collection and Candidate Detection
The pipeline begins with CeresSyntaxReceiver walking the syntax tree to collect every class declaration that has a base list and carries the partial modifier. This filtering ensures only valid candidates proceed to semantic analysis.
Attribute Detection and Argument Parsing
For each candidate, the generator obtains the INamedTypeSymbol and searches for the attribute using its fully qualified name Ceres.Graph.Flow.Annotations.GenerateFlowAttribute:
var generateAttribute = classSymbol.GetAttributes()
.FirstOrDefault(x => x.AttributeClass?.ToDisplayString()
== "Ceres.Graph.Flow.Annotations.GenerateFlowAttribute");
The generator then reads the named arguments to determine which interfaces to implement:
foreach (var arg in generateAttribute.NamedArguments)
{
if (arg.Key == nameof(FlowGraphGeneratorTemplate.GenerateRuntime))
generateRuntime = (bool)arg.Value.Value;
else if (arg.Key == nameof(FlowGraphGeneratorTemplate.GenerateImplementation))
generateImplementation = (bool)arg.Value.Value;
}
Template Selection Based on Generation Flags
FlowGraphGeneratorTemplate (located in Runtime/SourceGenerators/Source~/Ceres.SourceGenerator/Generators/FlowGraphGeneratorTemplate.cs) selects from four string templates based on the flag values:
- Implementation + Runtime:
ImplementationRuntimeTemplate - Implementation only:
ImplementationNonRuntimeTemplate - Runtime only:
NonImplementationRuntimeTemplate - Neither: Minimal stub
The generator creates a FlowGraphGeneratorTemplate instance, sets Namespace, ClassName, and the flag values, then calls GenerateCode() to concatenate the selected templates into the final source.
Source Generator Implementation Details
The core logic resides in Runtime/SourceGenerators/Source~/Ceres.SourceGenerator/Generators/FlowGraphGenerator.cs. After template selection, the generator creates a file named {ClassName}.gen.cs, adds it to the GeneratedFile list, and writes it to the compilation context via GenerateFiles(context, generatedFiles).
The generated code implements IFlowGraphContainer (when GenerateImplementation is true) and/or IFlowGraphRuntime (when GenerateRuntime is true), providing properties like Graph, methods like GetFlowGraph(), and backing fields such as _graph and graphData.
Practical Usage Examples
Container Implementation Without Runtime
To generate only the container interface without runtime execution logic:
using Ceres.Graph.Flow;
using Ceres.Graph.Flow.Annotations;
namespace MyGame.Flow
{
[GenerateFlow(GenerateRuntime = false, GenerateImplementation = true)]
public partial class MyGraph : FlowGraphObjectBase
{
// Custom logic here
}
}
The generator produces MyGraph.gen.cs using ImplementationNonRuntimeTemplate, adding IFlowGraphContainer implementation while omitting runtime-specific members.
Full Generation with Both Interfaces
For classes requiring both container and runtime capabilities:
[GenerateFlow(GenerateRuntime = true, GenerateImplementation = true)]
public partial class EnemyAI : FlowGraphObjectBase { }
The generated code implements both interfaces with lazy initialization:
public partial class EnemyAI : IFlowGraphContainer, IFlowGraphRuntime
{
[NonSerialized] private FlowGraph _graph;
[SerializeField] private FlowGraphData graphData;
public UObject Object => this;
public FlowGraph Graph
{
get
{
if (_graph == null)
{
_graph = GetFlowGraph();
using var ctx = FlowGraphCompilationContext.GetPooled();
using var comp = CeresGraphCompiler.GetPooled(_graph, ctx);
_graph.Compile(comp);
}
return _graph;
}
}
public FlowGraph GetFlowGraph() => graphData.CreateFlowGraphInstance();
FlowGraphData IFlowGraphContainer.GetFlowGraphData() => graphData;
public void SetGraphData(CeresGraphData graph) => graphData = (FlowGraphData)graph;
protected FlowGraphData GetGraphData() => graphData;
}
Runtime-Only Generation
For scenarios requiring only the runtime accessor:
[GenerateFlow(GenerateRuntime = true, GenerateImplementation = false)]
public partial class UIFlow : MonoBehaviour { }
This emits only the Graph property and related runtime logic, omitting IFlowGraphContainer implementation.
Summary
- The Ceres Source Generator requires partial classes to merge generated code with user code without type conflicts.
- The
[GenerateFlow]attribute triggers generation and controls output viaGenerateImplementationandGenerateRuntimeproperties. CeresSyntaxReceiverfilters candidates by checking for thepartialmodifier and base type inheritance.FlowGraphGeneratorTemplateselects from four code templates based on attribute arguments to implementIFlowGraphContainerand/orIFlowGraphRuntime.- Generated files follow the convention
{ClassName}.gen.csand are compiled alongside the original partial class.
Frequently Asked Questions
Why must Ceres flow-graph classes be declared as partial?
The generator emits a separate source file containing the second part of the class definition. C# merges multiple partial declarations into a single type at compile time, allowing generated members to coexist with user-written code. Without the partial modifier, the generator would produce a conflicting type definition and compilation would fail. The CeresSyntaxReceiver explicitly filters out non-partial classes by checking for SyntaxKind.PartialKeyword.
What is the difference between GenerateImplementation and GenerateRuntime?
GenerateImplementation controls whether the generated code includes the IFlowGraphContainer interface and its members such as GetFlowGraphData and SetGraphData. GenerateRuntime controls whether the class implements IFlowGraphRuntime and includes the lazy-initialized Graph property with compilation logic. You can enable either flag independently, both together, or neither, allowing you to generate exactly the boilerplate your class requires.
Where does the generated code get saved in the Ceres repository?
The generator creates in-memory source files named {ClassName}.gen.cs during the compilation process. These files are not written to disk in the repository source tree; instead, they are added to the compilation context via GenerateFiles(context, generatedFiles) and compiled alongside your partial class. You can view the generated code in your IDE's "Generated Files" or "Analyzers" node under the project dependencies.
Can I use GenerateFlowAttribute on classes that don't inherit from FlowGraphObjectBase?
Yes, provided the class is declared as partial and inherits from a base type compatible with the Ceres runtime (such as MonoBehaviour or ScriptableObject in Unity environments). The CeresSyntaxReceiver checks for a base list to ensure the class has inheritance, but the specific base type is flexible. The generated code will implement the requested interfaces regardless of the concrete base class, as long as the type system allows the interface implementations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →