Skip to content

Commit 020f778

Browse files
committed
Integrate compiled factory-generator caches into ReflectionCacheSet infrastructure (#1461)
The two compiled factory-delegate generator caches (previously raw static ConcurrentDictionary fields on FactoryGenerator) now live as named entries in ReflectionCacheSet.Shared.Internal via a new ReflectionCacheTypeKeyedDictionary<TDiscriminator,TValue> type, so they participate in ReflectionCacheSet.Clear() / Clear(predicate) calls. This prevents Type references from collectible AssemblyLoadContexts being pinned forever by these caches, fixing the memory-leak scenario described in #1461.
1 parent 016b071 commit 020f778

4 files changed

Lines changed: 114 additions & 22 deletions

File tree

src/Autofac/Core/InternalReflectionCaches.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Reflection;
55
using Autofac.Core.Activators.Reflection;
6+
using Autofac.Features.GeneratedFactories;
67
using Autofac.Util;
78
using Autofac.Util.Cache;
89

@@ -48,6 +49,20 @@ public InternalReflectionCaches(ReflectionCacheSet set)
4849
ServiceKeyParameterAttributes = set.GetOrCreateCache<ReflectionCacheParameterDictionary<bool>>(nameof(ServiceKeyParameterAttributes));
4950
ServiceKeyPropertyAttributes = set.GetOrCreateCache<ReflectionCacheDictionary<PropertyInfo, bool>>(nameof(ServiceKeyPropertyAttributes));
5051
ServiceKeyUsageByType = set.GetOrCreateCache<ReflectionCacheDictionary<Type, bool>>(nameof(ServiceKeyUsageByType));
52+
53+
GeneratedFactoryServiceOnlyGenerators = set.GetOrCreateCache(
54+
nameof(GeneratedFactoryServiceOnlyGenerators),
55+
_ => new ReflectionCacheTypeKeyedDictionary<ParameterMapping, Func<Service, IComponentContext, IEnumerable<Parameter>, Delegate>>
56+
{
57+
Usage = ReflectionCacheUsage.All,
58+
});
59+
60+
GeneratedFactoryServiceRegistrationGenerators = set.GetOrCreateCache(
61+
nameof(GeneratedFactoryServiceRegistrationGenerators),
62+
_ => new ReflectionCacheTypeKeyedDictionary<ParameterMapping, Func<Service, ServiceRegistration, IComponentContext, IEnumerable<Parameter>, Delegate>>
63+
{
64+
Usage = ReflectionCacheUsage.All,
65+
});
5166
}
5267

5368
/// <summary>
@@ -179,4 +194,24 @@ public ReflectionCacheDictionary<Type, bool> ModuleOverridesAttachToRegistration
179194
{
180195
get;
181196
}
197+
198+
/// <summary>
199+
/// Gets the cache of compiled factory-delegate generators for
200+
/// <see cref="Features.GeneratedFactories.FactoryGenerator"/> instances that resolve via
201+
/// <c>ResolveService</c>. Keyed on <c>(delegateType, effectiveParameterMapping)</c>.
202+
/// </summary>
203+
public ReflectionCacheTypeKeyedDictionary<ParameterMapping, Func<Service, IComponentContext, IEnumerable<Parameter>, Delegate>> GeneratedFactoryServiceOnlyGenerators
204+
{
205+
get;
206+
}
207+
208+
/// <summary>
209+
/// Gets the cache of compiled factory-delegate generators for
210+
/// <see cref="Features.GeneratedFactories.FactoryGenerator"/> instances that resolve via
211+
/// <see cref="IComponentContext.ResolveComponent"/>. Keyed on <c>(delegateType, effectiveParameterMapping)</c>.
212+
/// </summary>
213+
public ReflectionCacheTypeKeyedDictionary<ParameterMapping, Func<Service, ServiceRegistration, IComponentContext, IEnumerable<Parameter>, Delegate>> GeneratedFactoryServiceRegistrationGenerators
214+
{
215+
get;
216+
}
182217
}

src/Autofac/Features/GeneratedFactories/FactoryGenerator.cs

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) Autofac Project. All rights reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

4-
using System.Collections.Concurrent;
54
using System.Globalization;
65
using System.Linq.Expressions;
76
using System.Reflection;
@@ -16,22 +15,6 @@ namespace Autofac.Features.GeneratedFactories;
1615
/// </summary>
1716
public class FactoryGenerator
1817
{
19-
/// <summary>
20-
/// Cached compiled generators for the <see cref="FactoryGenerator(Type, Service, ParameterMapping)"/> overload
21-
/// (resolves via <c>ResolveService</c>). Keyed on <c>(delegateType, effectiveParameterMapping)</c>.
22-
/// Exposed internally for test observability only.
23-
/// </summary>
24-
internal static readonly ConcurrentDictionary<(Type, ParameterMapping), Func<Service, IComponentContext, IEnumerable<Parameter>, Delegate>>
25-
ServiceOnlyGeneratorCache = new();
26-
27-
/// <summary>
28-
/// Cached compiled generators for the <see cref="FactoryGenerator(Type, Service, ServiceRegistration, ParameterMapping)"/> overload
29-
/// (resolves via <see cref="IComponentContext.ResolveComponent"/>). Keyed on <c>(delegateType, effectiveParameterMapping)</c>.
30-
/// Exposed internally for test observability only.
31-
/// </summary>
32-
internal static readonly ConcurrentDictionary<(Type, ParameterMapping), Func<Service, ServiceRegistration, IComponentContext, IEnumerable<Parameter>, Delegate>>
33-
ServiceRegistrationGeneratorCache = new();
34-
3518
// The explicit '!' default is ok because the code is never executed, it's just used by
3619
// the expression tree.
3720
private static readonly ConstructorInfo _requestConstructor
@@ -61,7 +44,9 @@ public FactoryGenerator(Type delegateType, Service service, ParameterMapping par
6144
// or compile it once on first use. The cached delegate is parameterised on 'service'
6245
// (passed at invocation time) so it contains no instance-specific captures and is safe
6346
// to share across all FactoryGenerator instances with the same structural signature.
64-
var compiledGenerator = ServiceOnlyGeneratorCache.GetOrAdd(
47+
// Accessing ReflectionCacheSet.Shared at point-of-use (not stored in a field) ensures
48+
// correct weak-reference collection of the cache set.
49+
var compiledGenerator = ReflectionCacheSet.Shared.Internal.GeneratedFactoryServiceOnlyGenerators.GetOrAdd(
6550
(delegateType, pm),
6651
static key => CreateServiceOnlyGenerator(key.Item1, key.Item2));
6752

@@ -88,7 +73,9 @@ public FactoryGenerator(Type delegateType, Service service, ServiceRegistration
8873
// or compile it once on first use. The cached delegate is parameterised on both 'service'
8974
// and 'productRegistration' (passed at invocation time), so it contains no instance-specific
9075
// captures and is safe to share across all FactoryGenerator instances for the same delegate type.
91-
var compiledGenerator = ServiceRegistrationGeneratorCache.GetOrAdd(
76+
// Accessing ReflectionCacheSet.Shared at point-of-use (not stored in a field) ensures
77+
// correct weak-reference collection of the cache set.
78+
var compiledGenerator = ReflectionCacheSet.Shared.Internal.GeneratedFactoryServiceRegistrationGenerators.GetOrAdd(
9279
(delegateType, pm),
9380
static key => CreateServiceRegistrationGenerator(key.Item1, key.Item2));
9481

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) Autofac Project. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using System.Collections.Concurrent;
5+
using System.Reflection;
6+
using Autofac.Core;
7+
8+
namespace Autofac.Util.Cache;
9+
10+
/// <summary>
11+
/// A reflection cache dictionary, keyed on a <c>(<see cref="Type"/>, <typeparamref name="TDiscriminator"/>)</c>
12+
/// tuple where <typeparamref name="TDiscriminator"/> is a non-assembly-bearing discriminator (e.g. an enum).
13+
/// Only the <see cref="Type"/> component of the key participates in assembly-predicate–based clearing.
14+
/// </summary>
15+
/// <typeparam name="TDiscriminator">
16+
/// The second component of the tuple key; must be non-null but need not be a <see cref="MemberInfo"/>.
17+
/// </typeparam>
18+
/// <typeparam name="TValue">The value type.</typeparam>
19+
internal sealed class ReflectionCacheTypeKeyedDictionary<TDiscriminator, TValue>
20+
: ConcurrentDictionary<(Type, TDiscriminator), TValue>, IReflectionCache
21+
where TDiscriminator : notnull
22+
{
23+
/// <inheritdoc />
24+
public ReflectionCacheUsage Usage { get; set; } = ReflectionCacheUsage.All;
25+
26+
/// <inheritdoc />
27+
public void Clear(ReflectionCacheClearPredicate predicate)
28+
{
29+
if (predicate is null)
30+
{
31+
throw new ArgumentNullException(nameof(predicate));
32+
}
33+
34+
if (Count == 0)
35+
{
36+
return;
37+
}
38+
39+
var reusableAssemblySet = new HashSet<Assembly>();
40+
41+
foreach (var kvp in this)
42+
{
43+
if (predicate(kvp.Key.Item1, TypeAssemblyReferenceProvider.GetAllReferencedAssemblies(kvp.Key.Item1, reusableAssemblySet)))
44+
{
45+
TryRemove(kvp.Key, out _);
46+
}
47+
}
48+
}
49+
}

test/Autofac.Test/Features/GeneratedFactories/GeneratedFactoriesTests.cs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -476,9 +476,9 @@ public void FactoryDelegate_CompiledGeneratorCached_SiblingScopes()
476476
scope1.Resolve<FuncConsumer1461>();
477477

478478
// After the first resolve, the compiled generator must be present in the cache.
479-
Assert.True(Autofac.Features.GeneratedFactories.FactoryGenerator.ServiceRegistrationGeneratorCache.ContainsKey(cacheKey));
479+
Assert.True(ReflectionCacheSet.Shared.Internal.GeneratedFactoryServiceRegistrationGenerators.ContainsKey(cacheKey));
480480

481-
var countAfterFirstScope = Autofac.Features.GeneratedFactories.FactoryGenerator.ServiceRegistrationGeneratorCache.Count;
481+
var countAfterFirstScope = ReflectionCacheSet.Shared.Internal.GeneratedFactoryServiceRegistrationGenerators.Count;
482482

483483
using var scope2 = container.BeginLifetimeScope(c => c.RegisterType<FuncConsumer1461>());
484484
scope2.Resolve<FuncConsumer1461>();
@@ -488,7 +488,7 @@ public void FactoryDelegate_CompiledGeneratorCached_SiblingScopes()
488488

489489
// Resolving the same Func<T> from additional sibling scopes must not add new cache entries;
490490
// the count must remain stable after the first scope warms the entry.
491-
var countAfterAllScopes = Autofac.Features.GeneratedFactories.FactoryGenerator.ServiceRegistrationGeneratorCache.Count;
491+
var countAfterAllScopes = ReflectionCacheSet.Shared.Internal.GeneratedFactoryServiceRegistrationGenerators.Count;
492492
Assert.Equal(countAfterFirstScope, countAfterAllScopes);
493493
}
494494

@@ -638,4 +638,25 @@ public void FactoryDelegate_RespectsChildScopeContext_SiblingScopes()
638638
Assert.Same(result1, f1());
639639
Assert.Same(result2, f2());
640640
}
641+
642+
[Fact]
643+
public void FactoryDelegate_CompiledGeneratorCache_ClearedByReflectionCacheSetClear()
644+
{
645+
// #1461: The generated-factory caches must participate in ReflectionCacheSet.Clear()
646+
// so that types from collectible AssemblyLoadContexts can be fully unloaded.
647+
var builder = new ContainerBuilder();
648+
builder.RegisterType<Dependency1461>();
649+
using var container = builder.Build();
650+
651+
using var scope = container.BeginLifetimeScope(c => c.RegisterType<FuncConsumer1461>());
652+
scope.Resolve<FuncConsumer1461>();
653+
654+
var serviceRegCache = ReflectionCacheSet.Shared.Internal.GeneratedFactoryServiceRegistrationGenerators;
655+
656+
Assert.NotEmpty(serviceRegCache);
657+
658+
ReflectionCacheSet.Shared.Clear();
659+
660+
Assert.Empty(serviceRegCache);
661+
}
641662
}

0 commit comments

Comments
 (0)