Source Generator Compile-time Engine
July 24, 2026 · View on GitHub
The Source Generator is AspectCore's compile-time proxy engine, based on a Roslyn incremental generator (IIncrementalGenerator) that generates C# proxy source code for annotated types at compile time, so that the runtime directly uses these compiled proxy types without Reflection.Emit. It shares the same set of interception semantics as the DynamicProxy Runtime Engine; for a comparison of the two, see Engine Comparison and Selection.
The code is located in src/AspectCore.SourceGenerator/. This project is a netstandard2.0 Roslyn analyzer (IsRoslynComponent, LangVersion=latest) with no project references, and its DLL is packaged into analyzers/dotnet/cs; it depends on Microsoft.CodeAnalysis.CSharp and Microsoft.CodeAnalysis.Analyzers.
1. How It Is Triggered
Generation is triggered by the [AspectCore.DynamicProxy.AspectCoreGenerateProxy] attribute (AspectCoreProxyGenerator.cs:13), supporting three placements:
- Type level: applied to a concrete class or interface. An interface must specify its implementation type (
[AspectCoreGenerateProxy(typeof(Impl))]). - Assembly level (current compilation):
[assembly: AspectCoreGenerateProxy]automatically discovers qualifying types within this assembly. - Assembly level (referenced assembly): if a referenced assembly is annotated at the assembly level, its qualifying types are included.
2. Incremental Generation Flow
In Initialize, AspectCoreProxyGenerator (AspectCoreProxyGenerator.cs:10) hooks up two candidate sources and merges them (:15):
- Syntax fast path:
CreateSyntaxProvideruses a predicate to match "type declarations with an attribute list", and thenGetCandidateconfirms via the symbol's fully qualified attribute name (:125). - Referenced assembly discovery:
CompilationProvider.SelectMany(GetReferencedAssemblyCandidates)(:44).
After the two paths are merged, RegisterSourceOutput executes Execute (:151): for each candidate, it validates, decides interface/class proxy, and calls ProxyEmitter to generate {ProxyTypeName}.g.cs; as long as anything is generated, it additionally generates AspectCoreSourceGeneratedProxyRegistry.g.cs.
Candidate Filtering
IsProxyableClassMethod(:460):Ordinary && !static && virtual && !sealed, accessibility ∈ {public, protected, protected internal}, and not a record synthesized member.IsProxyableClassProperty(:470): same as above (property version).- Auto-discovery (assembly level) additionally skips types containing event members, as well as types that are already explicitly annotated.
3. Diagnostics (ACSGxxx)
The generator reports diagnostics when it encounters unsupported situations (Emit/GeneratorDiagnostics.cs, category AspectCore.SourceGenerator):
| ID | Level | Meaning |
|---|---|---|
| ACSG002 | Warning | Nested types are not supported |
| ACSG003 | Warning | Event members are not supported |
| ACSG005 | Error | Cannot proxy a sealed type |
| ACSG006 | Error | The type is not visible to the generated code (requires public/internal) |
| ACSG007 | Error | A class proxy lacks an accessible constructor |
| ACSG008 | Error | Cannot proxy a ref struct |
| ACSG009 | Warning | A byref-like params parameter is not supported |
| ACSG010 | Warning | A byref-like parameter is not supported |
| ACSG011 | Warning | A byref-like return value is not supported |
(ACSG001/ACSG004 are historically retained descriptors for "open generic type/method not supported"; generics are now supported and these are no longer actively triggered. The diagnostic titles/messages are in Chinese.)
4. Proxy Source Code Generation (ProxyEmitter)
Emit/ProxyEmitter.cs has two entry points: EmitInterfaceProxy (:14) and EmitClassProxy (:112). The generated proxy is a sealed type, marked with [NonAspect] + [Dynamically], and its fields include _activatorFactory, _aspectContextFactory, _aspectBuilderFactory, _aspectConfiguration, _serviceProvider, _implementation, _validator, and _cachedActivator (used only on the async path).
- Targetless interface proxy: additionally generates an internal
{proxy}__Stubimplementing the interface, and provides two constructors (with and without a target). - Class proxy: forwards the real base class constructors via
EmitClassConstructors(skipping record copy constructors). - record: generated as a
sealed record class(letting the compiler synthesize the copy constructor /withsupport); non-records usesealed class. For the difference between the two engines on records, see Record Type Support. __Metareflection cache: each proxy embeds aprivate static class __Metacaching theMethodInfoofService_*/Impl_*/Proxy_*, and marked with trimming/AOT suppression attributes.
Return Type Dispatch (ReturnKindKind)
ReturnKind.Determine (ProxyEmitter.cs:1515) maps to ReturnKindKind (Void/Sync/Task/TaskOfT/ValueTask/ValueTaskOfT/AsyncEnumerable/RefSync), with semantics aligned to DynamicProxy's ReturnKind.
Inlined Activation (Performance-critical)
The method body generated by EmitProxyInvokeBody (:858):
- Takes the cached service/implementation/proxy methods; re-resolves the implementation method by the runtime instance signature when necessary.
if (!ShouldIntercept(...))→ directly calls the target (ref/ref readonlyretains therefprefix, preserving true aliasing).- Constructs
object[] __argsandAspectActivatorContext. - The synchronous path fully inlines activation: directly
CreateContext→GetBuilderto take the cached pipeline →Build()→ execute, propagating failures viaExceptionDispatchInfoand running incomplete tasks withNoSyncContextScope.Run— skipping theAspectActivatorallocation. - The asynchronous path: reuses
_cachedActivator(AspectActivatoris stateless and reusable), callingInvokeTask<T>/InvokeValueTask<T>/InvokeAsyncEnumerable<T>. ref/outparameters are written back from__args[i]after the pipeline.ref/ref readonlyreturns: the value-semantic pipeline result is first stored into aStrongBox<T>, thenreturn ref __refBox.Value;(for details, see C# Language Feature Adaptation).
Interface Stubs
EmitInterfaceStubMembers/EmitStubMethod (:256) generate minimal members for the interface and its inherited interfaces: stubs are generated only for abstract methods (default interface methods are omitted so they go through DIM); out is set to default, and non-void returns default(T); a non-generic stub with a ref return returns a ref pointing to a private static slot, while a generic ref return stub throws NotSupportedException.
5. Runtime Discovery (RegistryEmitter)
Emit/RegistryEmitter.cs generates AspectCoreSourceGeneratedProxyRegistry.g.cs:
- An assembly attribute
[assembly: AspectCoreSourceGeneratedProxyRegistryAttribute(typeof(AspectCoreSourceGeneratedProxyRegistry))](:17), for the runtime to scan and discover. - A
public sealed class AspectCoreSourceGeneratedProxyRegistry : ISourceGeneratedProxyRegistry, implementingTryGetProxyType(serviceType, implementationType, kind, out proxyType)(:27), which internally matches entry by entry by kind + service key (generics normalized to their open definition).
On the runtime side, once enabled via ProxyEngineOptions, SourceGeneratedProxyTypeGenerator (in AspectCore.Core) has ScanRegistries reflectively scan the registry attributes on the assemblies in the AppDomain and instantiate them, thereby looking up the table to obtain the proxy types generated at compile time. For engine enabling and selection, see Engine Comparison and Selection.
6. Applicability and Limitations
- Reduces the dependency on dynamic code during proxy generation: proxies are generated at compile time, and at runtime proxy generation does not need
Reflection.Emit; combined with manual registry registration (AddSourceGeneratedProxyRegistry<T>()), it can work in scenarios without assembly scanning. Note that this is not equivalent to end-to-end NativeAOT — during interception the target call still goes throughMethodReflector(DynamicMethod), and proxy construction also retains[RequiresDynamicCode]paths; for the boundary, see Engine Comparison and Selection. - Requires explicit annotation: only types with
[AspectCoreGenerateProxy](or assembly-level auto-discovery) will have proxies generated. - Not supported:
sealedclasses,ref struct, nested types, event members, and byref-likeparamsparameters (corresponding to the diagnostics in the table above). - The behavioral differences from DynamicProxy are concentrated on records (equality /
initsetters) and the boundary of targetless interface stubs, both explained in the corresponding documents.