Problem
RegistrationExtensions holds the proxy generator in a static field:
private static readonly ProxyGenerator _proxyGenerator = new();
Castle's ModuleScope keeps a SynchronizedDictionary<CacheKey, Type> typeCache that strongly holds every generated proxy type, keyed on the type that was proxied. Because the generator is static, those entries are rooted for the life of the process and survive container disposal, so any type that has ever been class- or interface-intercepted can never be unloaded. DynamicProxyGenAssembly2 is also a non-collectible dynamic assembly.
The practical effect: a plugin host that loads types into a collectible AssemblyLoadContext and intercepts any of them cannot unload that context, even after disposing the container and calling ReflectionCacheSet.Shared.Clear(). Autofac core went to some trouble to make its reflection caches clearable for exactly this scenario (IReflectionCache, ReflectionCacheSet, weak rooting of Shared and of externally-registered caches); this package doesn't participate in that at all.
Evidence
Reflecting into ModuleScope.typeCache after generating a single class proxy, entries persist across a full GC:
before: typeCache holds 0 entries
after class proxy: typeCache holds 1 entries
value=Castle.Proxies.SampleProxy (assembly DynamicProxyGenAssembly2)
after GC: typeCache holds 1 entries
value=Castle.Proxies.SampleProxy (assembly DynamicProxyGenAssembly2)
Not urgent
No one has reported this. Opening it to track a limitation we know about.
Tradeoffs to weigh if we do address it
- A per-container (or otherwise scoped)
ModuleScope would stop sharing generated proxy types across containers. Sharing is a deliberate performance choice - proxy type generation is expensive - so this would be a real regression for anyone building containers repeatedly in one process.
- It is observable behavior: today two containers proxying the same type get the same generated type. Changing that is arguably breaking.
- An opt-in escape hatch (a way to supply the
ProxyGenerator/ModuleScope, or to participate in ReflectionCacheSet clearing) may be the cheaper path than changing the default.
Not related
Per-registration state added in #68 (ProxiedDefaultValueParameter) is not part of this. That dictionary is keyed on the same ParameterInfo instances ConstructorBinder already holds per registration, and its lifetime is the container, so it does not pin anything the container isn't pinning already.
Problem
RegistrationExtensionsholds the proxy generator in a static field:Castle's
ModuleScopekeeps aSynchronizedDictionary<CacheKey, Type> typeCachethat strongly holds every generated proxy type, keyed on the type that was proxied. Because the generator is static, those entries are rooted for the life of the process and survive container disposal, so any type that has ever been class- or interface-intercepted can never be unloaded.DynamicProxyGenAssembly2is also a non-collectible dynamic assembly.The practical effect: a plugin host that loads types into a collectible
AssemblyLoadContextand intercepts any of them cannot unload that context, even after disposing the container and callingReflectionCacheSet.Shared.Clear(). Autofac core went to some trouble to make its reflection caches clearable for exactly this scenario (IReflectionCache,ReflectionCacheSet, weak rooting ofSharedand of externally-registered caches); this package doesn't participate in that at all.Evidence
Reflecting into
ModuleScope.typeCacheafter generating a single class proxy, entries persist across a full GC:Not urgent
No one has reported this. Opening it to track a limitation we know about.
Tradeoffs to weigh if we do address it
ModuleScopewould stop sharing generated proxy types across containers. Sharing is a deliberate performance choice - proxy type generation is expensive - so this would be a real regression for anyone building containers repeatedly in one process.ProxyGenerator/ModuleScope, or to participate inReflectionCacheSetclearing) may be the cheaper path than changing the default.Not related
Per-registration state added in #68 (
ProxiedDefaultValueParameter) is not part of this. That dictionary is keyed on the sameParameterInfoinstancesConstructorBinderalready holds per registration, and its lifetime is the container, so it does not pin anything the container isn't pinning already.