Skip to content

Interceptors and Proxies: Diagrams

Diagrams for the concepts introduced in Interceptors and Proxies.

Proxy Call Flow

How a method call flows through the proxy system:

Proxy Call Flow

Proxy Generation at Compile Time

Proxy Generation at Compile Time

Proxy Type Hierarchy

Proxy Type Hierarchy

Generated Proxy Fields

FieldTypePurpose
__methodTableProxyMethodTable (static)Slot-indexed MethodInfo table shared by all instances of the proxy type
__bindingInterceptorBinding?Binds the interceptor to the method table; shares resolved handlers across proxy instances
__handler0Func<Invocation, object?>?Per-slot handler cache, filled on the first call
__cachedIntercepted0Func<ArgumentList, Task<string>>Cached delegate to target
ProxyTargetobject?Real service (from InterfaceProxy)

Invocation Structure

FieldDescription
ProxyThe proxy instance (e.g., IGreetingServiceProxy)
MethodTable, MethodIndexTable-qualified slot of the called method (ProxyMethodRef via MethodRef); the index is validated against the table in the constructor
MethodMethodInfo of the called method, resolved as MethodTable.Methods[MethodIndex]
ArgumentsArgumentList containing method arguments
InterceptedDelegateDelegate to call the real implementation (for pass-through)
InterfaceProxyTargetThe real service instance

ArgumentList Variants

ArgumentList Variants
CountGeneric TypeSimple Type
0ArgumentList0ArgumentList0
1ArgumentListG1<>ArgumentListS1
2ArgumentListG2<,>ArgumentListS2
3ArgumentListG3<,,>ArgumentListS3
4ArgumentListG4<,,,>ArgumentListS4
5-10(uses Simple)ArgumentListS5-S10

ArgumentList Methods

MethodDescription
.Get<T>(index)Get argument at index
.GetCancellationToken(index)Get cancellation token
.Set<T>(index, val)Set argument value
.LengthNumber of arguments

Handler Caching

Handlers are cached at two levels: each proxy instance caches the resolved handler per method slot in a dedicated field, and the InterceptorBinding shares resolved handlers across all proxy instances using the same (interceptor, method table) pair. The interceptor is bound just once, right after the proxy construction, so both caches only ever go from unresolved to resolved. Interceptor.SelectHandler runs at most once per slot; a slot the interceptor leaves unhandled caches the InterceptorBinding.NoHandler marker, which generated proxies detect by reference to invoke their typed original-call delegate directly (no boxed results).

Handler Caching

Interceptor Chain

Multiple interceptors can be chained together:

Interceptor Chain

Pass-Through vs Virtual Proxy

AspectPass-Through ProxyVirtual Proxy
CreationProxies.New(typeof(IService), interceptor, proxyTarget: realService)Proxies.New(typeof(IService), interceptor)
ProxyTarget!= null== null
FlowProxy → Interceptor → InvokeIntercepted() → Real ServiceProxy → Interceptor → Return default/mock
Use casesLogging, Metrics, Caching, Retry logicMocking/Stubs, Default values, RPC client proxies, Lazy initialization
Pass-Through vs Virtual Proxy

Typed vs Untyped Handlers

AspectTyped Handlers (Default)Untyped Handlers
MethodCreateTypedHandler<TUnwrapped>(invocation, methodDef)CreateUntypedHandler(invocation, methodDef)
SetupDefault behaviorUsesUntypedHandlers = true in constructor
Return typeTUnwrapped (e.g., string for Task<string>)object?
PerformanceOne handler instantiation per unique return typeNo generic instantiation overhead
Use caseMost use casesComputeServiceInterceptor for max performance

MethodDef Key Properties

For Task<string> GreetAsync(string name, CancellationToken ct):

PropertyValue
MethodInfoGreetAsync
FullName"MyNamespace.IGreetingService.GreetAsync"
ReturnTypetypeof(Task<string>)
UnwrappedReturnTypetypeof(string)
IsAsyncMethodtrue
ReturnsTasktrue
ReturnsValueTaskfalse
IsAsyncVoidMethodfalse
CancellationTokenIndex1
Parameters[name: string, ct: CancellationToken]

Helper Methods

MethodDescription
DefaultResultCompleted Task with default(T)
WrapResult(value)Task.FromResult(value)
WrapAsyncInvokerResult(task)Proper Task<T> or ValueTask<T>
InterceptedAsyncInvokerFunc<Invocation, Task<T>>
TargetAsyncInvokerFunc<object, Args, Task<T>>

See Also