autofac / autofac/Autofac.Extras.DynamicProxy
Static ProxyGenerator pins proxied types, blocking AssemblyLoadContext unload
- Dominant language
- C#
- Stars
- 108
- Forks
- 32
- PR merge metrics
- No merged PRs in 30d
Description
## Problem
`RegistrationExtensions` holds the proxy generator in a static field:
```csharp
private static readonly ProxyGenerator _proxyGenerator = new();
```
Castle's `ModuleScope` keeps a `SynchronizedDictionary 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.
Contributor guide
Research direction
Start in RegistrationExtensions at the static ProxyGenerator field, then inspect Castle's ModuleScope typeCache and the existing ReflectionCacheSet integration mentioned in the issue. Reproduce the cache-retention behavior with a collectible AssemblyLoadContext and a generated proxy. Done means an agreed scoped or opt-in design prevents unintended type pinning without silently changing proxy-sharing behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- backend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100