How It Works
How It Works
Section titled “How It Works”Indago replaces runtime reflection-based assembly scanning with a Roslyn IIncrementalGenerator that runs during your build. By the time your application starts, all type and assembly scan results are pre-computed and baked into generated C# — there is nothing to resolve at runtime.
Overview
Section titled “Overview”When you write code like this:
provider.GetTypes(s => s.FromAssemblyOf<MyService>().AddClasses().AsMatchingInterface())Indago’s generator finds that call during compilation, evaluates the selector expression against the Roslyn symbol graph, and emits a concrete implementation that returns the pre-computed results directly. No reflection, no startup cost, and full AOT/trimming compatibility.
The Three Syntax Providers
Section titled “The Three Syntax Providers”IndagoProviderGenerator (in CompiledTypeProviderGenerator.cs) registers three distinct syntax providers during Initialize, one for each method on IIndagoProvider:
| Syntax Provider | Handles |
|---|---|
AssemblyCollection | provider.GetAssemblies(…) call sites |
ReflectionCollection | provider.GetTypes(…) call sites |
ServiceDescriptorCollection | provider.Scan(…) call sites |
Each provider scans the syntax tree for its corresponding method invocation, extracts the selector expression, and passes it downstream for resolution.
Call Site Detection
Section titled “Call Site Detection”Each IIndagoProvider method carries hidden compiler-injected parameters:
IEnumerable<Type> GetTypes( Func<IReflectionTypeSelector, IEnumerable<Type>> selector, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string filePath = "", [CallerArgumentExpression(nameof(selector))] string argumentExpression = "");The [CallerArgumentExpression] attribute causes the compiler to pass the literal text of the selector lambda as a string. The generator reads that text and hashes it with GetArgumentExpressionHash, which normalises whitespace before computing an MD5 hash. This hash is the stable key that links a specific call site to its generated output — change the lambda text and the hash changes, triggering re-generation.
Symbol Visitor Walk
Section titled “Symbol Visitor Walk”For each call site, the generator resolves which types satisfy the selector by walking the Roslyn ITypeSymbol graph. The AssemblyProviders/ directory contains the visitors and compiled filters that do this work:
TypeSymbolVisitor— walks the type hierarchy of an assembly, accumulating matchingINamedTypeSymbolinstances. It exposesGetReferencedTypes(walks all referenced assemblies) andGetCompilationTypes(walks the current compilation only).CompiledTypeFilter/CompiledAssemblyFilter— evaluate individual filter predicates: namespace scoping, type kind (class, interface, enum…), attribute presence, assignability to a base type or interface, and name patterns.FindTypeVisitor/FindTypeInAssembly— locate specific types needed for assignability checks across assembly boundaries.
The net result is a precise, build-time enumeration of every type that would have been returned by the equivalent runtime reflection call.
Code Emission
Section titled “Code Emission”After resolving all call sites, the generator builds a single IndagoProvider.g.cs file containing:
- A concrete class that implements
IIndagoProvider, with each method returning a hard-coded collection for its call-site hash. - An
[assembly: IndagoHashAttribute("…hash…")]attribute that embeds aGeneratedHashfor cross-assembly cache validation.
The generated provider is exposed through the static IndagoProvider.Instance property — you call into it directly, with zero reflection scanning.
Incremental Rebuild
Section titled “Incremental Rebuild”Because Indago uses Roslyn’s IIncrementalGenerator API, the pipeline is fully incremental. Call sites whose selector expression text has not changed produce no re-computation. Only modified selectors, or changes to the types a selector matches, cause re-emission. In large solutions this means the generator adds negligible time to incremental builds.
Pipeline Diagram
Section titled “Pipeline Diagram”flowchart TD A[User calls provider.GetTypes(selector)] --> B[Roslyn IIncrementalGenerator] B --> C{Three Syntax Providers} C -->|GetAssemblies| D[AssemblyCollection] C -->|GetTypes| E[ReflectionCollection] C -->|Scan| F[ServiceDescriptorCollection] D & E & F --> G[Hash selector expression at call site] G --> H[Symbol visitor walks ITypeSymbol graph] H --> I[Apply compiled filters] I --> J[Emit IndagoProvider.g.cs] J --> K[Zero reflection at runtime]Next Steps
Section titled “Next Steps”To understand how scan results are shared across project references without redundant symbol resolution, see Cross-Assembly Caching.