autofac / autofac/Autofac.Extras.DynamicProxy

Static ProxyGenerator pins proxied types, blocking AssemblyLoadContext unload

Open
#69 0 comments 0 reactions 0 assignees View on GitHub
bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.