diff --git a/Autofac.sln b/Autofac.sln index 1ee85489b..bb34791be 100644 --- a/Autofac.sln +++ b/Autofac.sln @@ -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 @@ -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 @@ -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} diff --git a/bench/Autofac.Benchmarks/ChildScopeResolveBenchmark.cs b/bench/Autofac.Benchmarks/ChildScopeResolveBenchmark.cs index c12a8fa28..ef04a20ed 100644 --- a/bench/Autofac.Benchmarks/ChildScopeResolveBenchmark.cs +++ b/bench/Autofac.Benchmarks/ChildScopeResolveBenchmark.cs @@ -17,6 +17,19 @@ public void Resolve() } } + [Benchmark] + public void ResolveNeverRegisteredFromChild() + { + using (var requestScope = _container.BeginLifetimeScope("request", b => b.RegisterType())) + { + using (var unitOfWorkScope = requestScope.BeginLifetimeScope()) + { + var instance = unitOfWorkScope.Resolve>(); + GC.KeepAlive(instance); + } + } + } + [GlobalSetup] public void Setup() { @@ -58,4 +71,6 @@ public C2(D1 d1, D2 d2) { } internal class D1 { } internal class D2 { } + + internal class NeverRegistered { } } diff --git a/build.ps1 b/build.ps1 index 01ab215b7..d859650fa 100644 --- a/build.ps1 +++ b/build.ps1 @@ -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) { diff --git a/src/Autofac/Autofac.csproj b/src/Autofac/Autofac.csproj index c6d394b5d..c8fd66d04 100644 --- a/src/Autofac/Autofac.csproj +++ b/src/Autofac/Autofac.csproj @@ -21,6 +21,7 @@ $(NoWarn);CS1591 true ../../build/Analyzers.ruleset + true AllEnabledByDefault enable @@ -109,7 +110,7 @@ Autofac.Core.Activators - + Autofac.Core.Activators.ProvidedInstance diff --git a/src/Autofac/Core/Container.cs b/src/Autofac/Core/Container.cs index e0726e3bc..d4a1358a0 100644 --- a/src/Autofac/Core/Container.cs +++ b/src/Autofac/Core/Container.cs @@ -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; @@ -74,6 +77,20 @@ public ILifetimeScope BeginLifetimeScope(object tag, Action co return _rootLifetimeScope.BeginLifetimeScope(tag, configurationAction); } +#if NET5_0_OR_GREATER + /// + public ILifetimeScope BeginLoadContextLifetimeScope(AssemblyLoadContext loadContext, Action configurationAction) + { + return _rootLifetimeScope.BeginLoadContextLifetimeScope(loadContext, configurationAction); + } + + /// + public ILifetimeScope BeginLoadContextLifetimeScope(object tag, AssemblyLoadContext loadContext, Action configurationAction) + { + return _rootLifetimeScope.BeginLoadContextLifetimeScope(tag, loadContext, configurationAction); + } +#endif + /// public DiagnosticListener DiagnosticSource => _rootLifetimeScope.DiagnosticSource; diff --git a/src/Autofac/Core/IReflectionCache.cs b/src/Autofac/Core/IReflectionCache.cs index 598aca04d..7277e2cca 100644 --- a/src/Autofac/Core/IReflectionCache.cs +++ b/src/Autofac/Core/IReflectionCache.cs @@ -9,18 +9,18 @@ namespace Autofac.Core; /// Delegate for predicates that can choose whether to remove a member from the /// reflection cache. /// -/// -/// The assembly the cache entry relates to (i.e. the source of a type of -/// 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. /// +/// +/// 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. +/// /// /// True to remove the member from the cache, false to leave it. /// -public delegate bool ReflectionCacheClearPredicate(Assembly assembly, MemberInfo? member); +public delegate bool ReflectionCacheClearPredicate(MemberInfo? member, IEnumerable referencedAssemblies); /// /// Defines an individual store of cached reflection data. diff --git a/src/Autofac/Core/Lifetime/LifetimeScope.cs b/src/Autofac/Core/Lifetime/LifetimeScope.cs index e75414b9a..21c0ab228 100644 --- a/src/Autofac/Core/Lifetime/LifetimeScope.cs +++ b/src/Autofac/Core/Lifetime/LifetimeScope.cs @@ -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; @@ -189,6 +193,49 @@ public ILifetimeScope BeginLifetimeScope(Action configurationA /// /// public ILifetimeScope BeginLifetimeScope(object tag, Action configurationAction) + { + return InternalBeginLifetimeScope(tag, configurationAction, isolatedScope: false); + } + +#if NETCOREAPP1_0_OR_GREATER + /// + public ILifetimeScope BeginLoadContextLifetimeScope(AssemblyLoadContext loadContext, Action configurationAction) + { + return BeginLoadContextLifetimeScope(MakeAnonymousTag(), loadContext, configurationAction); + } + + /// + public ILifetimeScope BeginLoadContextLifetimeScope(object tag, AssemblyLoadContext loadContext, Action 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 configurationAction, bool isolatedScope) { if (configurationAction == null) { @@ -198,7 +245,7 @@ public ILifetimeScope BeginLifetimeScope(object tag, Action 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); @@ -223,25 +270,26 @@ public ILifetimeScope BeginLifetimeScope(object tag, Action co /// The tag applied to the . /// Action on a /// that adds component registrations visible only in the child scope. + /// + /// 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. + /// /// Registry to use for a child scope. /// 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 /// property of the child scope. - private IComponentRegistryBuilder CreateScopeRestrictedRegistry(object tag, Action configurationAction) + private IComponentRegistryBuilder CreateScopeRestrictedRegistry(object tag, Action configurationAction, bool isolatedScope) { var restrictedRootScopeLifetime = new MatchingScopeLifetime(tag); var tracker = new ScopeRestrictedRegisteredServicesTracker(restrictedRootScopeLifetime); var fallbackProperties = new FallbackDictionary(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); } } @@ -252,12 +300,12 @@ 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; } @@ -265,6 +313,9 @@ private IComponentRegistryBuilder CreateScopeRestrictedRegistry(object tag, Acti parent = parent.ParentLifetimeScope; } + var registryBuilder = new ComponentRegistryBuilder(tracker, fallbackProperties); + var builder = new ContainerBuilder(fallbackProperties, registryBuilder); + configurationAction(builder); builder.UpdateRegistry(registryBuilder); diff --git a/src/Autofac/Core/Lifetime/LifetimeScopeResources.resx b/src/Autofac/Core/Lifetime/LifetimeScopeResources.resx index 9ff7b958f..81a8d18f8 100644 --- a/src/Autofac/Core/Lifetime/LifetimeScopeResources.resx +++ b/src/Autofac/Core/Lifetime/LifetimeScopeResources.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + {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. + The tag '{0}' has already been assigned to a parent lifetime scope. If you are using Owned<T> this indicates you may have a circular dependency chain. diff --git a/src/Autofac/Core/Registration/DefaultRegisteredServicesTracker.cs b/src/Autofac/Core/Registration/DefaultRegisteredServicesTracker.cs index c4cf6d2fd..1e6419b1b 100644 --- a/src/Autofac/Core/Registration/DefaultRegisteredServicesTracker.cs +++ b/src/Autofac/Core/Registration/DefaultRegisteredServicesTracker.cs @@ -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) @@ -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 @@ -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) diff --git a/src/Autofac/Core/Registration/ExternalRegistryServiceMiddlewareSource.cs b/src/Autofac/Core/Registration/ExternalRegistryServiceMiddlewareSource.cs index 4f51c1b62..857eb1c53 100644 --- a/src/Autofac/Core/Registration/ExternalRegistryServiceMiddlewareSource.cs +++ b/src/Autofac/Core/Registration/ExternalRegistryServiceMiddlewareSource.cs @@ -11,19 +11,36 @@ namespace Autofac.Core.Registration; internal class ExternalRegistryServiceMiddlewareSource : IServiceMiddlewareSource { private readonly IComponentRegistry _componentRegistry; + private readonly bool _isolatedScope; /// /// Initializes a new instance of the class. /// /// The component registry to retrieve middleware from. - public ExternalRegistryServiceMiddlewareSource(IComponentRegistry componentRegistry) + /// + /// Indicates whether queries to the external registry should be wrapped with + /// , to indicate that the destination + /// registry should not hold on to type information that does not result in a registration. + /// + public ExternalRegistryServiceMiddlewareSource(IComponentRegistry componentRegistry, bool isolatedScope) { _componentRegistry = componentRegistry ?? throw new System.ArgumentNullException(nameof(componentRegistry)); + _isolatedScope = isolatedScope; } /// 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)); } } diff --git a/src/Autofac/Core/Registration/ExternalRegistrySource.cs b/src/Autofac/Core/Registration/ExternalRegistrySource.cs index 90f07e3ae..ef5034759 100644 --- a/src/Autofac/Core/Registration/ExternalRegistrySource.cs +++ b/src/Autofac/Core/Registration/ExternalRegistrySource.cs @@ -12,13 +12,22 @@ namespace Autofac.Core.Registration; internal class ExternalRegistrySource : IRegistrationSource { private readonly IComponentRegistry _registry; + private readonly bool _isolatedScope; /// /// Initializes a new instance of the class. /// /// Component registry to pull registrations from. - public ExternalRegistrySource(IComponentRegistry registry) - => _registry = registry ?? throw new ArgumentNullException(nameof(registry)); + /// + /// Indicates whether queries to the external registry and wrapped with + /// , to indicate that the destination + /// registry should not hold on to type information that does not result in a registration. + /// + public ExternalRegistrySource(IComponentRegistry registry, bool isolatedScope) + { + _registry = registry ?? throw new ArgumentNullException(nameof(registry)); + _isolatedScope = isolatedScope; + } /// /// Retrieve registrations for an unregistered service, to be used @@ -32,10 +41,19 @@ public IEnumerable RegistrationsFor(Service service, Fun // Issue #475: This method was refactored significantly to handle // registrations made on the fly in parent lifetime scopes to correctly // pass to child lifetime scopes. + 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); + } // Issue #272: Taking from the registry the following registrations: // - non-adapting own registrations: wrap them with ExternalComponentRegistration - foreach (var registration in _registry.RegistrationsFor(service)) + foreach (var registration in _registry.RegistrationsFor(serviceForLookup)) { if (registration is ExternalComponentRegistration || !registration.IsAdapting()) { diff --git a/src/Autofac/Core/Registration/IPerScopeRegistrationSource.cs b/src/Autofac/Core/Registration/IPerScopeRegistrationSource.cs new file mode 100644 index 000000000..f6c165a1e --- /dev/null +++ b/src/Autofac/Core/Registration/IPerScopeRegistrationSource.cs @@ -0,0 +1,13 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Autofac.Core.Registration; + +/// +/// Indicates that a registration source operates per-scope, and should not provide +/// registrations for requests from child scopes. +/// +[SuppressMessage("Design", "CA1040:Avoid empty interfaces", Justification = "Need a marker interface of per-scope registration sources")] +public interface IPerScopeRegistrationSource +{ +} diff --git a/src/Autofac/Core/Registration/ServiceRegistrationInfo.cs b/src/Autofac/Core/Registration/ServiceRegistrationInfo.cs index bd0c5e43b..8d08c2e8e 100644 --- a/src/Autofac/Core/Registration/ServiceRegistrationInfo.cs +++ b/src/Autofac/Core/Registration/ServiceRegistrationInfo.cs @@ -153,6 +153,18 @@ public bool IsRegistered } } + /// + /// Gets a value indicating whether this registration info has any custom service middleware registered. + /// + public bool HasCustomServiceMiddleware + { + get + { + RequiresInitialization(); + return _customPipelineBuilder is not null; + } + } + private bool Any => _defaultImplementations.Count > 0 || _sourceImplementations != null || diff --git a/src/Autofac/Core/ScopeIsolatedService.cs b/src/Autofac/Core/ScopeIsolatedService.cs new file mode 100644 index 000000000..7b0dc87d4 --- /dev/null +++ b/src/Autofac/Core/ScopeIsolatedService.cs @@ -0,0 +1,29 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Autofac.Core; + +/// +/// Decorator for a that indicates the service should +/// be isolated from the current scope so references to it are not +/// retained. Enables isolated services to be later unloaded. +/// +internal class ScopeIsolatedService : Service +{ + /// + /// Initializes a new instance of the class. + /// + /// The service to wrap for isolation. + public ScopeIsolatedService(Service service) + { + Service = service; + } + + /// + /// Gets the actual service that has been isolated. + /// + public Service Service { get; } + + /// + public override string Description => Service.Description; +} diff --git a/src/Autofac/Features/Collections/CollectionRegistrationSource.cs b/src/Autofac/Features/Collections/CollectionRegistrationSource.cs index 7007eaba9..4b102fed3 100644 --- a/src/Autofac/Features/Collections/CollectionRegistrationSource.cs +++ b/src/Autofac/Features/Collections/CollectionRegistrationSource.cs @@ -10,6 +10,7 @@ using Autofac.Core.Registration; using Autofac.Features.Decorators; using Autofac.Util; +using Autofac.Util.Cache; namespace Autofac.Features.Collections; @@ -40,7 +41,7 @@ namespace Autofac.Features.Collections; /// for something you don't expect to resolve". /// /// -internal class CollectionRegistrationSource : IRegistrationSource +internal class CollectionRegistrationSource : IRegistrationSource, IPerScopeRegistrationSource { /// /// Retrieve registrations for an unregistered service, to be used @@ -71,29 +72,37 @@ public IEnumerable RegistrationsFor(Service service, Fun } var serviceType = swt.ServiceType; - Type? elementType = null; - Type? limitType = null; - Func? factory = null; - if (serviceType.IsGenericTypeDefinedBy(typeof(IEnumerable<>))) - { - elementType = serviceType.GenericTypeArguments[0]; - limitType = elementType.MakeArrayType(); - factory = GenerateArrayFactory(elementType); - } - else if (serviceType.IsArray) - { - // GetElementType always non-null if IsArray is true. - elementType = serviceType.GetElementType()!; - limitType = serviceType; - factory = GenerateArrayFactory(elementType); - } - else if (serviceType.IsGenericListOrCollectionInterfaceType()) + var factoryCache = ReflectionCacheSet.Shared.GetOrCreateCache? Factory)>>(nameof(CollectionRegistrationSource)); + + var (elementType, limitType, factory) = factoryCache.GetOrAdd(serviceType, static serviceType => { - elementType = serviceType.GenericTypeArguments[0]; - limitType = typeof(List<>).MakeGenericType(elementType); - factory = GenerateListFactory(elementType); - } + Type? elementType = null; + Type? limitType = null; + Func? factory = null; + + if (serviceType.IsGenericTypeDefinedBy(typeof(IEnumerable<>))) + { + elementType = serviceType.GenericTypeArguments[0]; + limitType = elementType.MakeArrayType(); + factory = GenerateArrayFactory(elementType); + } + else if (serviceType.IsArray) + { + // GetElementType always non-null if IsArray is true. + elementType = serviceType.GetElementType()!; + limitType = serviceType; + factory = GenerateArrayFactory(elementType); + } + else if (serviceType.IsGenericListOrCollectionInterfaceType()) + { + elementType = serviceType.GenericTypeArguments[0]; + limitType = typeof(List<>).MakeGenericType(elementType); + factory = GenerateListFactory(elementType); + } + + return (elementType, limitType, factory); + }); if (elementType == null || factory == null || limitType == null) { diff --git a/src/Autofac/Features/ResolveAnything/AnyConcreteTypeNotAlreadyRegisteredSource.cs b/src/Autofac/Features/ResolveAnything/AnyConcreteTypeNotAlreadyRegisteredSource.cs index 8294411e2..f195e8f01 100644 --- a/src/Autofac/Features/ResolveAnything/AnyConcreteTypeNotAlreadyRegisteredSource.cs +++ b/src/Autofac/Features/ResolveAnything/AnyConcreteTypeNotAlreadyRegisteredSource.cs @@ -3,6 +3,7 @@ using Autofac.Builder; using Autofac.Core; +using Autofac.Core.Registration; namespace Autofac.Features.ResolveAnything; @@ -10,7 +11,7 @@ namespace Autofac.Features.ResolveAnything; /// Provides registrations on-the-fly for any concrete type not already registered with /// the container. /// -public class AnyConcreteTypeNotAlreadyRegisteredSource : IRegistrationSource +public class AnyConcreteTypeNotAlreadyRegisteredSource : IRegistrationSource, IPerScopeRegistrationSource { private readonly Func _predicate; diff --git a/src/Autofac/ILifetimeScope.cs b/src/Autofac/ILifetimeScope.cs index 198a88ad3..974562c5c0 100644 --- a/src/Autofac/ILifetimeScope.cs +++ b/src/Autofac/ILifetimeScope.cs @@ -1,6 +1,10 @@ // Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. +#if NET5_0_OR_GREATER +using System.Runtime.Loader; +#endif + using Autofac.Builder; using Autofac.Core; using Autofac.Core.Lifetime; @@ -148,6 +152,79 @@ public interface ILifetimeScope : IComponentContext, IDisposable, IAsyncDisposab /// A new lifetime scope. ILifetimeScope BeginLifetimeScope(object tag, Action configurationAction); +#if NET5_0_OR_GREATER + /// + /// Begin a new anonymous sub-scope, with additional components available to it that may be dynamically + /// loaded from the provided . + /// Component instances created via the new scope + /// will be disposed along with it. + /// + /// + /// A to associate to the created . + /// + /// + /// Action on a + /// that adds component registrations visible only in the new scope. + /// + /// A new lifetime scope. + /// + /// + /// IContainer cr = // ... + /// AssemblyLoadContext pluginContext = // ... + /// using (var lifetime = cr.BeginLoadContextLifetimeScope(pluginContext, builder => { + /// var assembly = pluginContext.LoadFromAssemblyPath("Plugins/plugin.dll"); + /// builder.RegisterType(assembly.GetType("PluginEntryPoint")).As<IPlugin>(); + /// { + /// var plugin = lifetime.Resolve<IPlugin>(); + /// } + /// + /// + /// + /// When the returned lifetime scope is disposed, the provided + /// can be unloaded, in that + /// Autofac will no longer have any references to types loaded from . + /// However if you have captured references to types from the loaded assemblies manually, you still may not be able + /// to unload. + /// + ILifetimeScope BeginLoadContextLifetimeScope(AssemblyLoadContext loadContext, Action configurationAction); + + /// + /// Begin a new tagged sub-scope, with additional components available to it that may be dynamically + /// loaded from the provided . + /// Component instances created via the new scope + /// will be disposed along with it. + /// + /// The tag applied to the . + /// + /// A to associate to the created . + /// + /// + /// Action on a + /// that adds component registrations visible only in the new scope. + /// + /// A new lifetime scope. + /// + /// + /// IContainer cr = // ... + /// AssemblyLoadContext pluginContext = // ... + /// using (var lifetime = cr.BeginLoadContextLifetimeScope(pluginContext, builder => { + /// var assembly = pluginContext.LoadFromAssemblyPath("Plugins/plugin.dll"); + /// builder.RegisterType(assembly.GetType("PluginEntryPoint")).As<IPlugin>(); + /// { + /// var plugin = lifetime.Resolve<IPlugin>(); + /// } + /// + /// + /// + /// When the returned lifetime scope is disposed, the provided + /// can be unloaded, in that + /// Autofac will no longer have any references to types loaded from . + /// However if you have captured references to types from the loaded assemblies manually, you still may not be able + /// to unload. + /// + ILifetimeScope BeginLoadContextLifetimeScope(object tag, AssemblyLoadContext loadContext, Action configurationAction); +#endif + /// /// Gets the disposer associated with this . /// Component instances can be associated with it manually if required. diff --git a/src/Autofac/Util/Cache/ReflectionCacheAssemblyDictionary.cs b/src/Autofac/Util/Cache/ReflectionCacheAssemblyDictionary.cs index 134e94e57..f632cd5f6 100644 --- a/src/Autofac/Util/Cache/ReflectionCacheAssemblyDictionary.cs +++ b/src/Autofac/Util/Cache/ReflectionCacheAssemblyDictionary.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using System.Collections.Concurrent; +using System.Linq; using System.Reflection; using Autofac.Core; @@ -28,9 +29,13 @@ public void Clear(ReflectionCacheClearPredicate? predicate) return; } + var reusableArrayForPredicate = new Assembly[1]; + foreach (var kvp in this) { - if (predicate(kvp.Key, null)) + reusableArrayForPredicate[0] = kvp.Key; + + if (predicate(null, reusableArrayForPredicate)) { TryRemove(kvp.Key, out _); } diff --git a/src/Autofac/Util/Cache/ReflectionCacheDictionary.cs b/src/Autofac/Util/Cache/ReflectionCacheDictionary.cs index e95419838..0281281f0 100644 --- a/src/Autofac/Util/Cache/ReflectionCacheDictionary.cs +++ b/src/Autofac/Util/Cache/ReflectionCacheDictionary.cs @@ -27,21 +27,18 @@ public void Clear(ReflectionCacheClearPredicate predicate) throw new ArgumentNullException(nameof(predicate)); } + if (Count == 0) + { + return; + } + + var reusableAssemblySet = new HashSet(); + foreach (var kvp in this) { - if (kvp.Key is Type keyType) - { - if (predicate(keyType.Assembly, keyType)) - { - TryRemove(kvp.Key, out _); - } - } - else if (kvp.Key.DeclaringType is Type declaredType) + if (predicate(kvp.Key, TypeAssemblyReferenceProvider.GetAllReferencedAssemblies(kvp.Key, reusableAssemblySet))) { - if (predicate(declaredType.Assembly, declaredType)) - { - TryRemove(kvp.Key, out _); - } + TryRemove(kvp.Key, out _); } } } diff --git a/src/Autofac/Util/Cache/ReflectionCacheTupleDictionary.cs b/src/Autofac/Util/Cache/ReflectionCacheTupleDictionary.cs index cb05438cb..1366f14da 100644 --- a/src/Autofac/Util/Cache/ReflectionCacheTupleDictionary.cs +++ b/src/Autofac/Util/Cache/ReflectionCacheTupleDictionary.cs @@ -23,30 +23,23 @@ internal sealed class ReflectionCacheTupleDictionary /// public void Clear(ReflectionCacheClearPredicate predicate) { + if (Count == 0) + { + return; + } + + var reusableAssemblySet = new HashSet(); + foreach (var kvp in this) { // Remove an item if *either* member of the tuple matches, // for generic implementation type caches where the closed generic // is in a different assembly to the open one. - if (predicate(GetKeyAssembly(kvp.Key.Item1), kvp.Key.Item1) || - predicate(GetKeyAssembly(kvp.Key.Item2), kvp.Key.Item2)) + if (predicate(kvp.Key.Item1, TypeAssemblyReferenceProvider.GetAllReferencedAssemblies(kvp.Key.Item1, reusableAssemblySet)) || + predicate(kvp.Key.Item2, TypeAssemblyReferenceProvider.GetAllReferencedAssemblies(kvp.Key.Item2, reusableAssemblySet))) { TryRemove(kvp.Key, out _); } } } - - private static Assembly GetKeyAssembly(TKey key) - { - if (key is Type keyType) - { - return keyType.Assembly; - } - else if (key.DeclaringType is Type declaredType) - { - return declaredType.Assembly; - } - - throw new InvalidOperationException("Impossible state"); - } } diff --git a/src/Autofac/Util/Cache/TypeAssemblyReferenceProvider.cs b/src/Autofac/Util/Cache/TypeAssemblyReferenceProvider.cs new file mode 100644 index 000000000..0d56cf39c --- /dev/null +++ b/src/Autofac/Util/Cache/TypeAssemblyReferenceProvider.cs @@ -0,0 +1,81 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Collections.Concurrent; +using System.Reflection; +using Autofac.Core; + +namespace Autofac.Util.Cache; + +/// +/// Helper class to determine the set of all assemblies referenced by a type, including indirect references via generic arguments and base classes. +/// +internal static class TypeAssemblyReferenceProvider +{ + /// + /// Get all distinct assemblies referenced directly by , if that member is a type, + /// or the owning type of that member (if it's a field or property). + /// + /// The member to retrieve references for. + /// The set of assemblies. + public static IEnumerable GetAllReferencedAssemblies(MemberInfo memberInfo) + { + var set = new HashSet(); + + GetAllReferencedAssemblies(memberInfo, set); + + return set; + } + + /// + /// Add to a provided all the assemblies referenced directly by a , if that member is a type, + /// or the owning type of that member (if it's a field or property). + /// + /// The member to retrieve references for. + /// A pre-allocated set to use for this purpose. + /// + /// The holding set is cleared each time this method is called. + /// + public static IEnumerable GetAllReferencedAssemblies(MemberInfo memberInfo, HashSet holdingSet) + { + holdingSet.Clear(); + + if (memberInfo is Type keyType) + { + PopulateAllReferencedAssemblies(keyType, holdingSet); + } + else if (memberInfo.DeclaringType is Type declaredType) + { + PopulateAllReferencedAssemblies(declaredType, holdingSet); + } + + return holdingSet; + } + + /// + /// Add to a provided all assemblies referenced by a given type. + /// + /// The type to retrieve references for. + /// A set to add any assemblies to. + private static void PopulateAllReferencedAssemblies(Type inputType, HashSet holdingSet) + { + if (inputType.IsArray && inputType.GetElementType() is Type elementType) + { + PopulateAllReferencedAssemblies(elementType, holdingSet); + } + + var genericArguments = inputType.GenericTypeArguments; + + foreach (var genericArgumentType in genericArguments) + { + PopulateAllReferencedAssemblies(genericArgumentType, holdingSet); + } + + holdingSet.Add(inputType.Assembly); + + if (inputType.BaseType is not null && inputType.BaseType != typeof(object)) + { + PopulateAllReferencedAssemblies(inputType.BaseType, holdingSet); + } + } +} diff --git a/test/Autofac.Specification.Test/Autofac.Specification.Test.csproj b/test/Autofac.Specification.Test/Autofac.Specification.Test.csproj index c80b9e7ea..bca979d60 100644 --- a/test/Autofac.Specification.Test/Autofac.Specification.Test.csproj +++ b/test/Autofac.Specification.Test/Autofac.Specification.Test.csproj @@ -5,6 +5,7 @@ $(NoWarn);CS1591 true ../../build/Test.ruleset + true false latest enable @@ -21,6 +22,9 @@ + + false + diff --git a/test/Autofac.Specification.Test/LoadContextScopeTests.cs b/test/Autofac.Specification.Test/LoadContextScopeTests.cs new file mode 100644 index 000000000..33dacac51 --- /dev/null +++ b/test/Autofac.Specification.Test/LoadContextScopeTests.cs @@ -0,0 +1,210 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Collections; +using System.Diagnostics.SymbolStore; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; +using Autofac.Core; +using Autofac.Features.ResolveAnything; + +namespace Autofac.Specification.Test; + +#if NET5_0_OR_GREATER + +public class LoadContextScopeTests +{ + [Fact] + public void CanLoadInstanceOfScanAssemblyAndUnloadIt() + { + using var rootContainer = new ContainerBuilder().Build(); + + LoadAssemblyAndTest( + rootContainer, + out var loadContextRef, + (builder, assembly) => builder.RegisterAssemblyTypes(assembly), + (scope, loadContext, assembly) => + { + var instance = scope.Resolve(assembly.GetType("A.Service1")); + + Assert.Contains(instance.GetType().Assembly, loadContext.Assemblies); + }); + + WaitForUnload(loadContextRef); + } + + [Fact] + public void CanLoadInstanceOfAssemblyAndUnloadItAfterActnars() + { + var builder = new ContainerBuilder(); + + builder.RegisterSource(new AnyConcreteTypeNotAlreadyRegisteredSource()); + + using var rootContainer = builder.Build(); + + LoadAssemblyAndTest( + rootContainer, + out var loadContextRef, + (builder, assembly) => { }, + (scope, loadContext, assembly) => + { + var instance = scope.Resolve(assembly.GetType("A.Service1")); + + Assert.Contains(instance.GetType().Assembly, loadContext.Assemblies); + }); + + WaitForUnload(loadContextRef); + } + + [Fact] + public void CanLoadInstanceOfAssemblyAndUnloadItAfterOnActivatedInModule() + { + var builder = new ContainerBuilder(); + + using var rootContainer = builder.Build(); + + LoadAssemblyAndTest( + rootContainer, + out var loadContextRef, + (builder, assembly) => + { + var module = (IModule)Activator.CreateInstance(assembly.GetType("A.OnActivatedModule"), 100); + + builder.RegisterModule(module); + }, + (scope, loadContext, assembly) => + { + var serviceType = assembly.GetType("A.Service1"); + + var instance = scope.Resolve(serviceType); + + Assert.Contains(instance.GetType().Assembly, loadContext.Assemblies); + + var valueProp = serviceType.GetProperty("Value"); + + Assert.Equal(100, valueProp.GetValue(instance)); + }); + + WaitForUnload(loadContextRef); + } + + [Fact] + public void CanLoadInstanceOfAssemblyAndUnloadItAfterLifetimeScopeEndingInModule() + { + var builder = new ContainerBuilder(); + + using var rootContainer = builder.Build(); + + bool callbackInvoked = false; + + LoadAssemblyAndTest( + rootContainer, + out var loadContextRef, + (builder, assembly) => + { + Action invoke = () => { callbackInvoked = true; }; + + var module = (IModule)Activator.CreateInstance(assembly.GetType("A.LifetimeScopeEndingModule"), invoke); + + builder.RegisterModule(module); + }, + (scope, loadContext, assembly) => + { + var serviceType = assembly.GetType("A.Service1"); + + var instance = scope.Resolve(serviceType); + + Assert.Contains(instance.GetType().Assembly, loadContext.Assemblies); + }); + + WaitForUnload(loadContextRef); + + Assert.True(callbackInvoked); + } + + [Fact] + public void CanLoadInstanceOfScanAssemblyAndUnloadItAfterEnumerable() + { + var builder = new ContainerBuilder(); + + using var rootContainer = builder.Build(); + + LoadAssemblyAndTest( + rootContainer, + out var loadContextRef, + (builder, assembly) => builder.RegisterType(assembly.GetType("A.Service1")), + (scope, loadContext, assembly) => + { + var genericEnumerable = typeof(IEnumerable<>).MakeGenericType(assembly.GetType("A.Service1")); + + var resolved = (IEnumerable)scope.Resolve(genericEnumerable); + + Assert.Collection( + resolved, + item => + { + Assert.Equal(item.GetType(), assembly.GetType("A.Service1")); + Assert.Contains(item.GetType().Assembly, loadContext.Assemblies); + }); + }); + + WaitForUnload(loadContextRef); + } + + [Fact] + public void CannotUseDefaultLoadContextForNewLifetimeScope() + { + var container = new ContainerBuilder().Build(); + + Assert.Throws(() => container.BeginLoadContextLifetimeScope(AssemblyLoadContext.Default, _ => { })); + } + + private void WaitForUnload(WeakReference loadContextRef) + { + var timeoutSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + // Monitor the generated reference to the assembly load context to make sure it finishes cleanup. + while (loadContextRef.IsAlive) + { + timeoutSource.Token.ThrowIfCancellationRequested(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + } + + private void LoadAssemblyAndTest( + IContainer container, + out WeakReference loadContextRef, + Action scopeBuilder, + Action execution) + { + var currentAssembly = Assembly.GetExecutingAssembly(); + var thisAssemblyPath = currentAssembly.Location; + + // Replace the project/assembly name in the path; this makes sure we use the same dotnet sdk and configuration + // as this assembly. + var newAssemblyPath = thisAssemblyPath.Replace(currentAssembly.GetName().Name, "Autofac.Test.Scenarios.LoadContext"); + + var loadContext = new AssemblyLoadContext("test", isCollectible: true); + loadContextRef = new WeakReference(loadContext); + + using (var scope = container.BeginLoadContextLifetimeScope(loadContext, builder => + { + var loadedAssembly = loadContext.LoadFromAssemblyPath(newAssemblyPath); + + scopeBuilder(builder, loadedAssembly); + + builder.RegisterInstance(loadedAssembly); + })) + { + execution(scope, loadContext, scope.Resolve()); + } + + loadContext.Unload(); + } +} + +#endif diff --git a/test/Autofac.Test.CodeGen/Autofac.Test.CodeGen.csproj b/test/Autofac.Test.CodeGen/Autofac.Test.CodeGen.csproj index 6e241393c..4da81676b 100644 --- a/test/Autofac.Test.CodeGen/Autofac.Test.CodeGen.csproj +++ b/test/Autofac.Test.CodeGen/Autofac.Test.CodeGen.csproj @@ -8,6 +8,7 @@ true true ../../build/Test.ruleset + true false latest enable diff --git a/test/Autofac.Test.Compilation/Autofac.Test.Compilation.csproj b/test/Autofac.Test.Compilation/Autofac.Test.Compilation.csproj index ce93b4f6c..06437f107 100644 --- a/test/Autofac.Test.Compilation/Autofac.Test.Compilation.csproj +++ b/test/Autofac.Test.Compilation/Autofac.Test.Compilation.csproj @@ -8,6 +8,7 @@ true true ../../build/Test.ruleset + true false latest enable diff --git a/test/Autofac.Test.Scenarios.LoadContext/Autofac.Test.Scenarios.LoadContext.csproj b/test/Autofac.Test.Scenarios.LoadContext/Autofac.Test.Scenarios.LoadContext.csproj new file mode 100644 index 000000000..37db0cdae --- /dev/null +++ b/test/Autofac.Test.Scenarios.LoadContext/Autofac.Test.Scenarios.LoadContext.csproj @@ -0,0 +1,29 @@ + + + + net7.0;net6.0 + $(NoWarn);CS1591 + true + false + ../../build/Test.ruleset + true + enable + enable + latest + + + + + all + + + + + + + + + + + + diff --git a/test/Autofac.Test.Scenarios.LoadContext/LifetimeScopeEndingModule.cs b/test/Autofac.Test.Scenarios.LoadContext/LifetimeScopeEndingModule.cs new file mode 100644 index 000000000..f99c13359 --- /dev/null +++ b/test/Autofac.Test.Scenarios.LoadContext/LifetimeScopeEndingModule.cs @@ -0,0 +1,26 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac; + +namespace A; + +public class LifetimeScopeEndingModule : Module +{ + private readonly Action _invokeOnEndCallback; + + public LifetimeScopeEndingModule(Action invokeOnEndCallback) + { + _invokeOnEndCallback = invokeOnEndCallback; + } + + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType(); + + builder.RegisterBuildCallback(scope => scope.CurrentScopeEnding += (sender, ev) => + { + _invokeOnEndCallback(); + }); + } +} diff --git a/test/Autofac.Test.Scenarios.LoadContext/OnActivatedModule.cs b/test/Autofac.Test.Scenarios.LoadContext/OnActivatedModule.cs new file mode 100644 index 000000000..9231b67ef --- /dev/null +++ b/test/Autofac.Test.Scenarios.LoadContext/OnActivatedModule.cs @@ -0,0 +1,21 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac; + +namespace A; + +public class OnActivatedModule : Module +{ + private readonly int _value; + + public OnActivatedModule(int value) + { + _value = value; + } + + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().OnActivated(x => x.Instance.Value = _value); + } +} diff --git a/test/Autofac.Test.Scenarios.LoadContext/Service1.cs b/test/Autofac.Test.Scenarios.LoadContext/Service1.cs new file mode 100644 index 000000000..ca5187b69 --- /dev/null +++ b/test/Autofac.Test.Scenarios.LoadContext/Service1.cs @@ -0,0 +1,9 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace A; + +public class Service1 +{ + public int Value { get; set; } +} diff --git a/test/Autofac.Test.Scenarios.ScannedAssembly/Autofac.Test.Scenarios.ScannedAssembly.csproj b/test/Autofac.Test.Scenarios.ScannedAssembly/Autofac.Test.Scenarios.ScannedAssembly.csproj index aa41ea4d7..cb08d4ce6 100644 --- a/test/Autofac.Test.Scenarios.ScannedAssembly/Autofac.Test.Scenarios.ScannedAssembly.csproj +++ b/test/Autofac.Test.Scenarios.ScannedAssembly/Autofac.Test.Scenarios.ScannedAssembly.csproj @@ -10,6 +10,7 @@ Autofac.Test.Scenarios.ScannedAssembly false ../../build/Test.ruleset + true diff --git a/test/Autofac.Test/Autofac.Test.csproj b/test/Autofac.Test/Autofac.Test.csproj index e8ac95e95..b63195f46 100644 --- a/test/Autofac.Test/Autofac.Test.csproj +++ b/test/Autofac.Test/Autofac.Test.csproj @@ -8,6 +8,7 @@ true true ../../build/Test.ruleset + true false latest enable diff --git a/test/Autofac.Test/Core/Pipeline/PipelineBuilderTests.cs b/test/Autofac.Test/Core/Pipeline/PipelineBuilderTests.cs index 3c0b14a8c..19a7c04ae 100644 --- a/test/Autofac.Test/Core/Pipeline/PipelineBuilderTests.cs +++ b/test/Autofac.Test/Core/Pipeline/PipelineBuilderTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using System.Diagnostics; +using System.Runtime.Loader; using Autofac.Core; using Autofac.Core.Lifetime; using Autofac.Core.Resolving; @@ -535,6 +536,16 @@ public ILifetimeScope BeginLifetimeScope(object tag, Action co throw new NotImplementedException(); } + public ILifetimeScope BeginLoadContextLifetimeScope(AssemblyLoadContext loadContext, Action configurationAction) + { + throw new NotImplementedException(); + } + + public ILifetimeScope BeginLoadContextLifetimeScope(object tag, AssemblyLoadContext loadContext, Action configurationAction) + { + throw new NotImplementedException(); + } + public object CreateSharedInstance(Guid id, Func creator) { throw new NotImplementedException(); diff --git a/test/Autofac.Test/Core/ReflectionCacheSetTests.cs b/test/Autofac.Test/Core/ReflectionCacheSetTests.cs index bed3cbf51..c564d1c83 100644 --- a/test/Autofac.Test/Core/ReflectionCacheSetTests.cs +++ b/test/Autofac.Test/Core/ReflectionCacheSetTests.cs @@ -50,7 +50,7 @@ public void InvokingClearWithPredicateCallsClearOnAllCaches() var externalCache = set.GetOrCreateCache>("cache"); externalCache[typeof(string)] = true; - set.Clear((assembly, member) => member == typeof(string)); + set.Clear((member, assembly) => member == typeof(string)); Assert.Collection(internalCache, item => Assert.Equal(typeof(IEnumerable<>), item.Key)); Assert.Empty(externalCache); diff --git a/test/Autofac.Test/Util/Cache/ReflectionCacheAssemblyDictionaryTests.cs b/test/Autofac.Test/Util/Cache/ReflectionCacheAssemblyDictionaryTests.cs index ea73b20ca..cecb4b872 100644 --- a/test/Autofac.Test/Util/Cache/ReflectionCacheAssemblyDictionaryTests.cs +++ b/test/Autofac.Test/Util/Cache/ReflectionCacheAssemblyDictionaryTests.cs @@ -28,11 +28,11 @@ public void CanConditionallyClearContents() cacheDict[typeof(string).Assembly] = false; cacheDict[typeof(ContainerBuilder).Assembly] = false; - cacheDict.Clear((assembly, member) => + cacheDict.Clear((member, assemblies) => { Assert.Null(member); - return assembly == typeof(string).Assembly; + return assemblies.Contains(typeof(string).Assembly); }); Assert.Collection(cacheDict, (kvp) => Assert.Equal(typeof(ContainerBuilder).Assembly, kvp.Key)); diff --git a/test/Autofac.Test/Util/Cache/ReflectionCacheDictionaryTests.cs b/test/Autofac.Test/Util/Cache/ReflectionCacheDictionaryTests.cs index 9e0152e2f..13431d72c 100644 --- a/test/Autofac.Test/Util/Cache/ReflectionCacheDictionaryTests.cs +++ b/test/Autofac.Test/Util/Cache/ReflectionCacheDictionaryTests.cs @@ -28,9 +28,9 @@ public void CanConditionallyClearContents() cacheDict[typeof(string)] = false; cacheDict[typeof(int)] = false; - cacheDict.Clear((assembly, member) => + cacheDict.Clear((member, assemblies) => { - Assert.Equal(typeof(string).Assembly, assembly); + Assert.Collection(assemblies, a => Assert.Equal(typeof(string).Assembly, a)); return member == typeof(string); }); @@ -45,11 +45,11 @@ public void MethodInfoAssemblyIsCorrect() cacheDict[typeof(string).GetMethod("IsNullOrEmpty")] = false; - cacheDict.Clear((assembly, member) => + cacheDict.Clear((member, assemblies) => { - Assert.Equal(typeof(string).Assembly, assembly); + Assert.Collection(assemblies, a => Assert.Equal(typeof(string).Assembly, a)); - return member == typeof(string); + return member == typeof(string).GetMethod("IsNullOrEmpty"); }); Assert.Empty(cacheDict); diff --git a/test/Autofac.Test/Util/Cache/ReflectionCacheTupleDictionaryTests.cs b/test/Autofac.Test/Util/Cache/ReflectionCacheTupleDictionaryTests.cs index bd4846a73..ba80fb6ae 100644 --- a/test/Autofac.Test/Util/Cache/ReflectionCacheTupleDictionaryTests.cs +++ b/test/Autofac.Test/Util/Cache/ReflectionCacheTupleDictionaryTests.cs @@ -27,7 +27,7 @@ public void ClearsContentsWhenOneOfTheTupleMatches() cacheDict[(typeof(string), typeof(ContainerBuilder))] = false; - cacheDict.Clear((assembly, member) => + cacheDict.Clear((member, assembly) => { return member == typeof(string); }); @@ -42,7 +42,7 @@ public void ClearsContentsWhenBothOfTheTupleMatches() cacheDict[(typeof(string), typeof(int))] = false; - cacheDict.Clear((assembly, member) => + cacheDict.Clear((member, assembly) => { return member == typeof(int) || member == typeof(string); }); @@ -57,9 +57,9 @@ public void MethodInfoUsesContainingTypeAssembly() cacheDict[(typeof(string).GetMethod("IsNullOrEmpty"), typeof(int).GetMethod("GetHashCode"))] = false; - cacheDict.Clear((assembly, member) => + cacheDict.Clear((member, assemblies) => { - Assert.Equal(typeof(string).Assembly, assembly); + Assert.Collection(assemblies, a => Assert.Equal(typeof(string).Assembly, a)); return true; }); @@ -74,9 +74,9 @@ public void TypeUsesTypeAssembly() cacheDict[(typeof(string), typeof(int))] = false; - cacheDict.Clear((assembly, member) => + cacheDict.Clear((member, assemblies) => { - Assert.Equal(typeof(string).Assembly, assembly); + Assert.Collection(assemblies, a => Assert.Equal(typeof(string).Assembly, a)); return true; }); diff --git a/test/Autofac.Test/Util/Cache/TypeAssemblyReferenceProviderTests.cs b/test/Autofac.Test/Util/Cache/TypeAssemblyReferenceProviderTests.cs new file mode 100644 index 000000000..a6164eabd --- /dev/null +++ b/test/Autofac.Test/Util/Cache/TypeAssemblyReferenceProviderTests.cs @@ -0,0 +1,74 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Autofac.Builder; +using Autofac.Core; +using Autofac.Features.Indexed; +using Autofac.Util.Cache; + +namespace Autofac.Test.Util.Cache; + +public class TypeAssemblyReferenceProviderTests +{ + [Theory] + [InlineData(typeof(string), new[] { typeof(string) })] + [InlineData(typeof(IEnumerable), new[] { typeof(IEnumerable<>), typeof(ContainerBuilder) })] + [InlineData(typeof(IEnumerable<>), new[] { typeof(IEnumerable<>) })] + [InlineData(typeof(IEnumerable), new[] { typeof(IEnumerable<>), typeof(ContainerBuilder) })] + [InlineData(typeof(IEnumerable>), new[] { typeof(IEnumerable<>), typeof(IIndex<,>), typeof(Assert) })] + [InlineData(typeof(DerivedClass), new[] { typeof(DerivedClass), typeof(RegistrationBuilder<,,>), typeof(Assert) })] + [InlineData(typeof(GenericDerivedClass), new[] { typeof(DerivedClass), typeof(RegistrationBuilder<,,>), typeof(Assert), typeof(Mock) })] + public void TypeReferencesCanBeDetermined(Type inputType, Type[] expandedTypeAssemblies) + { + var set = TypeAssemblyReferenceProvider.GetAllReferencedAssemblies(inputType); + + Assert.Distinct(set); + Assert.Equal(expandedTypeAssemblies.Length, set.Count()); + + foreach (var item in expandedTypeAssemblies) + { + Assert.Contains(item, expandedTypeAssemblies); + } + } + + [Fact] + public void MemberInfoReferencesCanBeDetermined() + { + var memberInfo = typeof(PropertyOwner).GetProperty(nameof(PropertyOwner.Property)); + + var expectedResults = new[] { typeof(ContainerBuilder), typeof(PropertyOwner<>) }; + + var set = TypeAssemblyReferenceProvider.GetAllReferencedAssemblies(memberInfo); + + Assert.Distinct(set); + Assert.Equal(expectedResults.Length, set.Count()); + + foreach (var item in expectedResults) + { + Assert.Contains(item, expectedResults); + } + } + + private class DerivedClass + : RegistrationBuilder + { + public DerivedClass(Service defaultService, SimpleActivatorData activatorData, SingleRegistrationStyle style) + : base(defaultService, activatorData, style) + { + } + } + + private class GenericDerivedClass + : RegistrationBuilder, SimpleActivatorData, SingleRegistrationStyle> + { + public GenericDerivedClass(Service defaultService, SimpleActivatorData activatorData, SingleRegistrationStyle style) + : base(defaultService, activatorData, style) + { + } + } + + private class PropertyOwner + { + public string Property { get; set; } + } +}