Skip to content

Commit

Permalink
Merge pull request #1350 from autofac/feature/do-not-retain-isolated-…
Browse files Browse the repository at this point in the history
…service-info

Isolate service information computed in child scopes from the parent scope
  • Loading branch information
alistairjevans authored Mar 1, 2023
2 parents 2adc678 + 430e28b commit f446abb
Show file tree
Hide file tree
Showing 37 changed files with 864 additions and 91 deletions.
19 changes: 19 additions & 0 deletions Autofac.sln
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Autofac.CodeGen", "codegen\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Autofac.Test.CodeGen", "test\Autofac.Test.CodeGen\Autofac.Test.CodeGen.csproj", "{A651B51E-3CDE-410F-9354-6DB9A5A9B591}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Autofac.Test.Scenarios.LoadContext", "test\Autofac.Test.Scenarios.LoadContext\Autofac.Test.Scenarios.LoadContext.csproj", "{F07DB7CC-E2C8-4D77-9982-8DF25417921D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -210,6 +212,22 @@ Global
{A651B51E-3CDE-410F-9354-6DB9A5A9B591}.Release|x64.Build.0 = Release|Any CPU
{A651B51E-3CDE-410F-9354-6DB9A5A9B591}.Release|x86.ActiveCfg = Release|Any CPU
{A651B51E-3CDE-410F-9354-6DB9A5A9B591}.Release|x86.Build.0 = Release|Any CPU
{F07DB7CC-E2C8-4D77-9982-8DF25417921D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F07DB7CC-E2C8-4D77-9982-8DF25417921D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F07DB7CC-E2C8-4D77-9982-8DF25417921D}.Debug|ARM.ActiveCfg = Debug|Any CPU
{F07DB7CC-E2C8-4D77-9982-8DF25417921D}.Debug|ARM.Build.0 = Debug|Any CPU
{F07DB7CC-E2C8-4D77-9982-8DF25417921D}.Debug|x64.ActiveCfg = Debug|Any CPU
{F07DB7CC-E2C8-4D77-9982-8DF25417921D}.Debug|x64.Build.0 = Debug|Any CPU
{F07DB7CC-E2C8-4D77-9982-8DF25417921D}.Debug|x86.ActiveCfg = Debug|Any CPU
{F07DB7CC-E2C8-4D77-9982-8DF25417921D}.Debug|x86.Build.0 = Debug|Any CPU
{F07DB7CC-E2C8-4D77-9982-8DF25417921D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F07DB7CC-E2C8-4D77-9982-8DF25417921D}.Release|Any CPU.Build.0 = Release|Any CPU
{F07DB7CC-E2C8-4D77-9982-8DF25417921D}.Release|ARM.ActiveCfg = Release|Any CPU
{F07DB7CC-E2C8-4D77-9982-8DF25417921D}.Release|ARM.Build.0 = Release|Any CPU
{F07DB7CC-E2C8-4D77-9982-8DF25417921D}.Release|x64.ActiveCfg = Release|Any CPU
{F07DB7CC-E2C8-4D77-9982-8DF25417921D}.Release|x64.Build.0 = Release|Any CPU
{F07DB7CC-E2C8-4D77-9982-8DF25417921D}.Release|x86.ActiveCfg = Release|Any CPU
{F07DB7CC-E2C8-4D77-9982-8DF25417921D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -224,6 +242,7 @@ Global
{946CF4FE-DB20-4901-80F9-F7363BA06F1E} = {48F40A36-C829-4895-99B3-1634CC6594E0}
{5E86E12F-DB5A-4E96-80C7-7FC7791C5DD2} = {1FE012DB-9231-4F74-A38B-EC7B050CC0A3}
{A651B51E-3CDE-410F-9354-6DB9A5A9B591} = {DEA4A8C6-DE56-4359-A87C-472FB34132E7}
{F07DB7CC-E2C8-4D77-9982-8DF25417921D} = {DEA4A8C6-DE56-4359-A87C-472FB34132E7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2D16574C-61CB-4568-8490-AC9B85A721C0}
Expand Down
15 changes: 15 additions & 0 deletions bench/Autofac.Benchmarks/ChildScopeResolveBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ public void Resolve()
}
}

[Benchmark]
public void ResolveNeverRegisteredFromChild()
{
using (var requestScope = _container.BeginLifetimeScope("request", b => b.RegisterType<C1>()))
{
using (var unitOfWorkScope = requestScope.BeginLifetimeScope())
{
var instance = unitOfWorkScope.Resolve<IEnumerable<NeverRegistered>>();
GC.KeepAlive(instance);
}
}
}

[GlobalSetup]
public void Setup()
{
Expand Down Expand Up @@ -58,4 +71,6 @@ public C2(D1 d1, D2 d2) { }
internal class D1 { }

internal class D2 { }

internal class NeverRegistered { }
}
2 changes: 1 addition & 1 deletion build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ try {

# Test
Write-Message "Executing unit tests"
Get-DotNetProjectDirectory -RootPath $PSScriptRoot\test | Where-Object { $_ -inotlike "*Autofac.Test.Scenarios.ScannedAssembly" } | Invoke-Test
Get-DotNetProjectDirectory -RootPath $PSScriptRoot\test | Where-Object { $_ -inotlike "*Autofac.Test.Scenarios.*" } | Invoke-Test

# Benchmark
if ($Bench) {
Expand Down
3 changes: 2 additions & 1 deletion src/Autofac/Autofac.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<CodeAnalysisRuleSet>../../build/Analyzers.ruleset</CodeAnalysisRuleSet>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<ImplicitUsings>enable</ImplicitUsings>
<!-- Packaging -->
Expand Down Expand Up @@ -109,7 +110,7 @@
<EmbeddedResource Update="Core\Activators\InstanceActivatorResources.resx">
<StronglyTypedNamespace>Autofac.Core.Activators</StronglyTypedNamespace>
</EmbeddedResource>
<EmbeddedResource Update="Core\Activators\ProvidedInstance\ProvidedInstanceActivatorResources.resx">
<EmbeddedResource Update="Core\Activators\ProvidedInstance\ProvidedInstanceActivatorResources.resx">
<StronglyTypedNamespace>Autofac.Core.Activators.ProvidedInstance</StronglyTypedNamespace>
</EmbeddedResource>
<EmbeddedResource Update="Core\Activators\Reflection\BoundConstructorResources.resx">
Expand Down
17 changes: 17 additions & 0 deletions src/Autofac/Core/Container.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System.Diagnostics;
#if NET5_0_OR_GREATER
using System.Runtime.Loader;
#endif
using Autofac.Core.Lifetime;
using Autofac.Core.Resolving;
using Autofac.Util;
Expand Down Expand Up @@ -74,6 +77,20 @@ public ILifetimeScope BeginLifetimeScope(object tag, Action<ContainerBuilder> co
return _rootLifetimeScope.BeginLifetimeScope(tag, configurationAction);
}

#if NET5_0_OR_GREATER
/// <inheritdoc />
public ILifetimeScope BeginLoadContextLifetimeScope(AssemblyLoadContext loadContext, Action<ContainerBuilder> configurationAction)
{
return _rootLifetimeScope.BeginLoadContextLifetimeScope(loadContext, configurationAction);
}

/// <inheritdoc />
public ILifetimeScope BeginLoadContextLifetimeScope(object tag, AssemblyLoadContext loadContext, Action<ContainerBuilder> configurationAction)
{
return _rootLifetimeScope.BeginLoadContextLifetimeScope(tag, loadContext, configurationAction);
}
#endif

/// <inheritdoc/>
public DiagnosticListener DiagnosticSource => _rootLifetimeScope.DiagnosticSource;

Expand Down
10 changes: 5 additions & 5 deletions src/Autofac/Core/IReflectionCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ namespace Autofac.Core;
/// Delegate for predicates that can choose whether to remove a member from the
/// reflection cache.
/// </summary>
/// <param name="assembly">
/// The assembly the cache entry relates to (i.e. the source of a type of
/// member).
/// </param>
/// <param name="member">
/// The member information (will be an instance of a more-derived type). This
/// value may be null if the cache entry relates only to an assembly.
/// </param>
/// <param name="referencedAssemblies">
/// All assemblies the cache entry key references; this set includes both the direct assembly reference for the member,
/// and all indirectly-referenced assemblies via generic type arguments or array element types.
/// </param>
/// <returns>
/// True to remove the member from the cache, false to leave it.
/// </returns>
public delegate bool ReflectionCacheClearPredicate(Assembly assembly, MemberInfo? member);
public delegate bool ReflectionCacheClearPredicate(MemberInfo? member, IEnumerable<Assembly> referencedAssemblies);

/// <summary>
/// Defines an individual store of cached reflection data.
Expand Down
73 changes: 62 additions & 11 deletions src/Autofac/Core/Lifetime/LifetimeScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
#if NET5_0_OR_GREATER
using System.Runtime.Loader;
#endif
using Autofac.Builder;
using Autofac.Core.Registration;
using Autofac.Core.Resolving;
using Autofac.Features.Collections;
using Autofac.Util;

namespace Autofac.Core.Lifetime;
Expand Down Expand Up @@ -189,6 +193,49 @@ public ILifetimeScope BeginLifetimeScope(Action<ContainerBuilder> configurationA
/// </code>
/// </example>
public ILifetimeScope BeginLifetimeScope(object tag, Action<ContainerBuilder> configurationAction)
{
return InternalBeginLifetimeScope(tag, configurationAction, isolatedScope: false);
}

#if NETCOREAPP1_0_OR_GREATER
/// <inheritdoc />
public ILifetimeScope BeginLoadContextLifetimeScope(AssemblyLoadContext loadContext, Action<ContainerBuilder> configurationAction)
{
return BeginLoadContextLifetimeScope(MakeAnonymousTag(), loadContext, configurationAction);
}

/// <inheritdoc />
public ILifetimeScope BeginLoadContextLifetimeScope(object tag, AssemblyLoadContext loadContext, Action<ContainerBuilder> configurationAction)
{
if (loadContext == AssemblyLoadContext.Default)
{
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, LifetimeScopeResources.DefaultLoadContextError, nameof(BeginLoadContextLifetimeScope), nameof(BeginLifetimeScope)));
}

var newScope = InternalBeginLifetimeScope(tag, configurationAction, isolatedScope: true);

newScope.CurrentScopeEnding += (sender, args) =>
{
// Clear the reflection cache for those assemblies when the inner scope goes away.
ReflectionCacheSet.Shared.Clear((cacheKey, referencedAssemblySet) =>
{
foreach (var refAssembly in referencedAssemblySet)
{
if (AssemblyLoadContext.GetLoadContext(refAssembly) is AssemblyLoadContext assemblyContext && assemblyContext.Equals(loadContext))
{
return true;
}
}
return false;
});
};

return newScope;
}
#endif

private ILifetimeScope InternalBeginLifetimeScope(object tag, Action<ContainerBuilder> configurationAction, bool isolatedScope)
{
if (configurationAction == null)
{
Expand All @@ -198,7 +245,7 @@ public ILifetimeScope BeginLifetimeScope(object tag, Action<ContainerBuilder> co
CheckNotDisposed();
CheckTagIsUnique(tag);

var localsBuilder = CreateScopeRestrictedRegistry(tag, configurationAction);
var localsBuilder = CreateScopeRestrictedRegistry(tag, configurationAction, isolatedScope);
var scope = new LifetimeScope(localsBuilder.Build(), this, tag);
scope.Disposer.AddInstanceForDisposal(localsBuilder);

Expand All @@ -223,25 +270,26 @@ public ILifetimeScope BeginLifetimeScope(object tag, Action<ContainerBuilder> co
/// <param name="tag">The tag applied to the <see cref="ILifetimeScope"/>.</param>
/// <param name="configurationAction">Action on a <see cref="ContainerBuilder"/>
/// that adds component registrations visible only in the child scope.</param>
/// <param name="isolatedScope">
/// Indicates whether the generated registry should be 'isolated'; an isolated registry does not hold on to
/// any type information for retrieved services that do not result in registrations.
/// </param>
/// <returns>Registry to use for a child scope.</returns>
/// <remarks>It is the responsibility of the caller to make sure that the registry is properly
/// disposed of. This is generally done by adding the registry to the <see cref="Disposer"/>
/// property of the child scope.</remarks>
private IComponentRegistryBuilder CreateScopeRestrictedRegistry(object tag, Action<ContainerBuilder> configurationAction)
private IComponentRegistryBuilder CreateScopeRestrictedRegistry(object tag, Action<ContainerBuilder> configurationAction, bool isolatedScope)
{
var restrictedRootScopeLifetime = new MatchingScopeLifetime(tag);
var tracker = new ScopeRestrictedRegisteredServicesTracker(restrictedRootScopeLifetime);

var fallbackProperties = new FallbackDictionary<string, object?>(ComponentRegistry.Properties);
var registryBuilder = new ComponentRegistryBuilder(tracker, fallbackProperties);

var builder = new ContainerBuilder(fallbackProperties, registryBuilder);

foreach (var source in ComponentRegistry.Sources)
{
if (source.IsAdapterForIndividualComponents)
if (source.IsAdapterForIndividualComponents || (source is IPerScopeRegistrationSource && isolatedScope))
{
builder.RegisterSource(source);
tracker.AddRegistrationSource(source);
}
}

Expand All @@ -252,19 +300,22 @@ private IComponentRegistryBuilder CreateScopeRestrictedRegistry(object tag, Acti
{
if (parent.ComponentRegistry.HasLocalComponents)
{
var externalSource = new ExternalRegistrySource(parent.ComponentRegistry);
builder.RegisterSource(externalSource);
var externalSource = new ExternalRegistrySource(parent.ComponentRegistry, isolatedScope);
tracker.AddRegistrationSource(externalSource);

// Add a source for the service pipeline stages.
var externalServicePipelineSource = new ExternalRegistryServiceMiddlewareSource(parent.ComponentRegistry);
builder.RegisterServiceMiddlewareSource(externalServicePipelineSource);
var externalServicePipelineSource = new ExternalRegistryServiceMiddlewareSource(parent.ComponentRegistry, isolatedScope);
tracker.AddServiceMiddlewareSource(externalServicePipelineSource);

break;
}

parent = parent.ParentLifetimeScope;
}

var registryBuilder = new ComponentRegistryBuilder(tracker, fallbackProperties);
var builder = new ContainerBuilder(fallbackProperties, registryBuilder);

configurationAction(builder);

builder.UpdateRegistry(registryBuilder);
Expand Down
3 changes: 3 additions & 0 deletions src/Autofac/Core/Lifetime/LifetimeScopeResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="DefaultLoadContextError" xml:space="preserve">
<value>{0} should only be used for non-default assembly load contexts, typically when dynamically loading assemblies that will need to be unloaded later; if in doubt, use the normal {1} method instead. For further details on assembly load contexts, see https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/understanding-assemblyloadcontext.</value>
</data>
<data name="DuplicateTagDetected" xml:space="preserve">
<value>The tag '{0}' has already been assigned to a parent lifetime scope. If you are using Owned&lt;T&gt; this indicates you may have a circular dependency chain.</value>
</data>
Expand Down
31 changes: 29 additions & 2 deletions src/Autofac/Core/Registration/DefaultRegisteredServicesTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,15 @@ private void ClearRegistrations()
private ServiceRegistrationInfo GetInitializedServiceInfo(Service service)
{
var createdEphemeralSet = false;
var isScopeIsolatedService = false;

if (service is ScopeIsolatedService scopeIsolatedService)
{
// This is an isolated service query; use the wrapped service instead and
// remember that fact for later.
isScopeIsolatedService = true;
service = scopeIsolatedService.Service;
}

var info = GetServiceInfo(service);
if (info.IsInitialized)
Expand Down Expand Up @@ -325,6 +334,14 @@ private ServiceRegistrationInfo GetInitializedServiceInfo(Service service)
while (info.HasSourcesToQuery)
{
var next = info.DequeueNextSource();

// Do not query per-scope registration sources
// for isolated services.
if (isScopeIsolatedService && next is IPerScopeRegistrationSource)
{
continue;
}

foreach (var provided in next.RegistrationsFor(service, _registrationAccessor))
{
// This ensures that multiple services provided by the same
Expand Down Expand Up @@ -366,9 +383,19 @@ private ServiceRegistrationInfo GetInitializedServiceInfo(Service service)
{
info.InitializationDepth--;

if (info.InitializationDepth == 0 && succeeded)
if (info.InitializationDepth == 0)
{
info.CompleteInitialization();
if (succeeded)
{
info.CompleteInitialization();
}

if (isScopeIsolatedService && (!succeeded || (!info.IsRegistered && !info.HasCustomServiceMiddleware)))
{
// No registrations or custom middleware was found for this service, and this service enquiry is marked as "isolated",
// meaning that we shouldn't remember any info for it if it has no registrations.
_serviceInfo.TryRemove(service, out _);
}
}

if (lockTaken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,36 @@ namespace Autofac.Core.Registration;
internal class ExternalRegistryServiceMiddlewareSource : IServiceMiddlewareSource
{
private readonly IComponentRegistry _componentRegistry;
private readonly bool _isolatedScope;

/// <summary>
/// Initializes a new instance of the <see cref="ExternalRegistryServiceMiddlewareSource"/> class.
/// </summary>
/// <param name="componentRegistry">The component registry to retrieve middleware from.</param>
public ExternalRegistryServiceMiddlewareSource(IComponentRegistry componentRegistry)
/// <param name="isolatedScope">
/// Indicates whether queries to the external registry should be wrapped with
/// <see cref="ScopeIsolatedService"/>, to indicate that the destination
/// registry should not hold on to type information that does not result in a registration.
/// </param>
public ExternalRegistryServiceMiddlewareSource(IComponentRegistry componentRegistry, bool isolatedScope)
{
_componentRegistry = componentRegistry ?? throw new System.ArgumentNullException(nameof(componentRegistry));
_isolatedScope = isolatedScope;
}

/// <inheritdoc/>
public void ProvideMiddleware(Service service, IComponentRegistryServices availableServices, IResolvePipelineBuilder pipelineBuilder)
{
pipelineBuilder.UseRange(_componentRegistry.ServiceMiddlewareFor(service));
var serviceForLookup = service;

if (_isolatedScope)
{
// If we need to isolate services to a particular scope,
// we wrap the service in ScopeIsolatedService to tell the parent
// registry not to hold on to any types that don't result in implementations.
serviceForLookup = new ScopeIsolatedService(service);
}

pipelineBuilder.UseRange(_componentRegistry.ServiceMiddlewareFor(serviceForLookup));
}
}
Loading

0 comments on commit f446abb

Please sign in to comment.