diff --git a/src/Autofac/Core/Activators/InstanceActivator.cs b/src/Autofac/Core/Activators/InstanceActivator.cs index c36726eee..62f9b7d93 100644 --- a/src/Autofac/Core/Activators/InstanceActivator.cs +++ b/src/Autofac/Core/Activators/InstanceActivator.cs @@ -42,7 +42,14 @@ protected void CheckNotDisposed() { if (IsDisposed) { - throw new ObjectDisposedException(InstanceActivatorResources.InstanceActivatorDisposed, innerException: null); + // Call separate throw method to allow inlining of the disposal check. + ThrowDisposedException(); } } + + [DoesNotReturn] + private static void ThrowDisposedException() + { + throw new ObjectDisposedException(InstanceActivatorResources.InstanceActivatorDisposed, innerException: null); + } } diff --git a/src/Autofac/Core/Activators/Reflection/BoundConstructor.cs b/src/Autofac/Core/Activators/Reflection/BoundConstructor.cs index 00d45d8a1..3488b2e12 100644 --- a/src/Autofac/Core/Activators/Reflection/BoundConstructor.cs +++ b/src/Autofac/Core/Activators/Reflection/BoundConstructor.cs @@ -98,10 +98,19 @@ public object Instantiate() throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, BoundConstructorResources.CannotInstantitate, Description)); } - var values = new object?[_valueRetrievers!.Length]; - for (var i = 0; i < _valueRetrievers.Length; ++i) + object?[] values; + + if (_valueRetrievers!.Length != 0) + { + values = new object?[_valueRetrievers!.Length]; + for (var i = 0; i < _valueRetrievers.Length; ++i) + { + values[i] = _valueRetrievers[i](); + } + } + else { - values[i] = _valueRetrievers[i](); + values = Array.Empty(); } try diff --git a/src/Autofac/Core/Activators/Reflection/ConstructorBinder.cs b/src/Autofac/Core/Activators/Reflection/ConstructorBinder.cs index 5ea2ac98f..09b11cef3 100644 --- a/src/Autofac/Core/Activators/Reflection/ConstructorBinder.cs +++ b/src/Autofac/Core/Activators/Reflection/ConstructorBinder.cs @@ -36,7 +36,7 @@ public ConstructorBinder(ConstructorInfo constructorInfo) if (_illegalParameter is null) { // Build the invoker. - _factory = FactoryCache.GetOrAdd(constructorInfo, FactoryBuilder); + _factory = FactoryCache.GetOrAdd(Constructor, FactoryBuilder); } } @@ -113,6 +113,15 @@ public BoundConstructor Bind(IEnumerable availableParameters, ICompon return BoundConstructor.ForBindSuccess(this, _factory!, valueRetrievers); } + /// + /// Get the constructor factory delegate. + /// + /// Will return null if the constructor contains an invalid parameter. + internal Func? GetConstructorInvoker() + { + return _factory; + } + private static Func GetConstructorInvoker(ConstructorInfo constructorInfo) { var paramsInfo = constructorInfo.GetParameters(); diff --git a/src/Autofac/Core/Activators/Reflection/IConstructorSelectorWithEarlyBinding.cs b/src/Autofac/Core/Activators/Reflection/IConstructorSelectorWithEarlyBinding.cs new file mode 100644 index 000000000..287b1c44e --- /dev/null +++ b/src/Autofac/Core/Activators/Reflection/IConstructorSelectorWithEarlyBinding.cs @@ -0,0 +1,28 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Autofac.Core.Activators.Reflection; + +/// +/// Defines an interface that indicates a constructor selector can attempt to +/// determine the correct constructor early, as the container is built, +/// without needing to understand the set of parameters in each resolve. +/// +public interface IConstructorSelectorWithEarlyBinding : IConstructorSelector +{ + /// + /// Given the set of all found constructors for a registration, try and + /// select the correct single to use. + /// + /// + /// The set of binders for all constructors found by + /// on the registration. + /// + /// + /// The single, correct binder to use. If the method returns null, this + /// indicates the selector requires resolve-time parameters, and the default + /// method will + /// be invoked. + /// + ConstructorBinder? SelectConstructorBinder(ConstructorBinder[] constructorBinders); +} diff --git a/src/Autofac/Core/Activators/Reflection/MatchingSignatureConstructorSelector.cs b/src/Autofac/Core/Activators/Reflection/MatchingSignatureConstructorSelector.cs index e8297fe10..9c952e198 100644 --- a/src/Autofac/Core/Activators/Reflection/MatchingSignatureConstructorSelector.cs +++ b/src/Autofac/Core/Activators/Reflection/MatchingSignatureConstructorSelector.cs @@ -8,7 +8,7 @@ namespace Autofac.Core.Activators.Reflection; /// /// Selects a constructor based on its signature. /// -public class MatchingSignatureConstructorSelector : IConstructorSelector +public class MatchingSignatureConstructorSelector : IConstructorSelector, IConstructorSelectorWithEarlyBinding { private readonly Type[] _signature; @@ -77,4 +77,49 @@ public BoundConstructor SelectConstructorBinding(BoundConstructor[] constructorB throw new DependencyResolutionException(string.Format(CultureInfo.CurrentCulture, MatchingSignatureConstructorSelectorResources.TooManyConstructorsMatch, signature)); } + + /// + /// Selects the best constructor from the available constructor bindings. + /// + /// Available constructors. + /// The best constructor. + public ConstructorBinder SelectConstructorBinder(ConstructorBinder[] constructorBinders) + { + if (constructorBinders == null) + { + throw new ArgumentNullException(nameof(constructorBinders)); + } + + var matchingCount = 0; + ConstructorBinder? chosen = null; + + for (var idx = 0; idx < constructorBinders.Length; idx++) + { + var binding = constructorBinders[idx]; + + // Concievably could store the set of parameter types in the binder as well, but + // that's yet more memory up-front, for a less used constructor selector. + if (binding.Parameters.Select(p => p.ParameterType).SequenceEqual(_signature)) + { + chosen = binding; + matchingCount++; + } + } + + if (matchingCount == 1) + { + return chosen!; + } + + // DeclaringType will be non-null for constructors. + var targetTypeName = constructorBinders[0].Constructor.DeclaringType!.Name; + var signature = string.Join(", ", _signature.Select(t => t.Name).ToArray()); + + if (matchingCount == 0) + { + throw new DependencyResolutionException(string.Format(CultureInfo.CurrentCulture, MatchingSignatureConstructorSelectorResources.RequiredConstructorNotAvailable, targetTypeName, signature)); + } + + throw new DependencyResolutionException(string.Format(CultureInfo.CurrentCulture, MatchingSignatureConstructorSelectorResources.TooManyConstructorsMatch, signature)); + } } diff --git a/src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs b/src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs index 15bcd6f9a..84b3fc46d 100644 --- a/src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs +++ b/src/Autofac/Core/Activators/Reflection/ReflectionActivator.cs @@ -3,7 +3,9 @@ using System.Globalization; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text; +using Autofac.Core.Resolving; using Autofac.Core.Resolving.Pipeline; namespace Autofac.Core.Activators.Reflection; @@ -91,6 +93,23 @@ public void ConfigurePipeline(IComponentRegistryServices componentRegistryServic binders[idx] = new ConstructorBinder(availableConstructors[idx]); } + if (binders.Length == 1) + { + UseSingleConstructorActivation(pipelineBuilder, binders[0]); + return; + } + else if (ConstructorSelector is IConstructorSelectorWithEarlyBinding earlyBindingSelector) + { + var matchedConstructor = earlyBindingSelector.SelectConstructorBinder(binders); + + if (matchedConstructor is not null) + { + UseSingleConstructorActivation(pipelineBuilder, matchedConstructor); + + return; + } + } + _constructorBinders = binders; pipelineBuilder.Use(ToString(), PipelinePhase.Activation, MiddlewareInsertionMode.EndOfPhase, (ctxt, next) => @@ -101,6 +120,66 @@ public void ConfigurePipeline(IComponentRegistryServices componentRegistryServic }); } + private void UseSingleConstructorActivation(IResolvePipelineBuilder pipelineBuilder, ConstructorBinder singleConstructor) + { + if (singleConstructor.ParameterCount == 0) + { + var constructorInvoker = singleConstructor.GetConstructorInvoker(); + + if (constructorInvoker is null) + { + // This is not going to happen, because there is only 1 constructor, that constructor has no parameters, + // so there are no conditions under which GetConstructorInvoker will return null in this path. + // Throw an error here just in case (and to satisfy nullability checks). + throw new NoConstructorsFoundException(_implementationType, string.Format(CultureInfo.CurrentCulture, ReflectionActivatorResources.NoConstructorsAvailable, _implementationType, ConstructorFinder)); + } + + // If there are no arguments to the constructor, bypass all argument binding and pre-bind the constructor. + var boundConstructor = BoundConstructor.ForBindSuccess( + singleConstructor, + constructorInvoker, + Array.Empty>()); + + // Fast-path to just create an instance. + pipelineBuilder.Use(ToString(), PipelinePhase.Activation, MiddlewareInsertionMode.EndOfPhase, (ctxt, next) => + { + CheckNotDisposed(); + + var instance = boundConstructor.Instantiate(); + + InjectProperties(instance, ctxt); + + ctxt.Instance = instance; + + next(ctxt); + }); + } + else + { + pipelineBuilder.Use(ToString(), PipelinePhase.Activation, MiddlewareInsertionMode.EndOfPhase, (ctxt, next) => + { + CheckNotDisposed(); + + var prioritisedParameters = GetAllParameters(ctxt.Parameters); + + var bound = singleConstructor.Bind(prioritisedParameters, ctxt); + + if (!bound.CanInstantiate) + { + throw new DependencyResolutionException(GetBindingFailureMessage(new[] { bound })); + } + + var instance = bound.Instantiate(); + + InjectProperties(instance, ctxt); + + ctxt.Instance = instance; + + next(ctxt); + }); + } + } + /// /// Activate an instance in the provided context. /// @@ -146,8 +225,7 @@ private object ActivateInstance(IComponentContext context, IEnumerable parameters) { - // Most often, there will be no `parameters` and/or no `_defaultParameters`; in both of those cases we can avoid allocating. - var prioritisedParameters = parameters.Any() ? EnumerateParameters(parameters) : _defaultParameters; + var prioritisedParameters = GetAllParameters(parameters); var boundConstructors = new BoundConstructor[availableConstructors.Length]; var validBindings = availableConstructors.Length; @@ -172,6 +250,23 @@ private BoundConstructor[] GetAllBindings(ConstructorBinder[] availableConstruct return boundConstructors; } + private IEnumerable GetAllParameters(IEnumerable parameters) + { + // Most often, there will be no `parameters` and/or no `_defaultParameters`; in both of those cases we can avoid allocating. + // Do a reference compare with the NoParameters constant; faster than an Any() check for the common case. + if (ReferenceEquals(ResolveRequest.NoParameters, parameters)) + { + return _defaultParameters; + } + + if (parameters.Any()) + { + return EnumerateParameters(parameters); + } + + return _defaultParameters; + } + private IEnumerable EnumerateParameters(IEnumerable parameters) { foreach (var param in parameters) diff --git a/src/Autofac/Core/Activators/Reflection/ReflectionActivatorResources.Designer.cs b/src/Autofac/Core/Activators/Reflection/ReflectionActivatorResources.Designer.cs index 928433cc1..e010e10bc 100644 --- a/src/Autofac/Core/Activators/Reflection/ReflectionActivatorResources.Designer.cs +++ b/src/Autofac/Core/Activators/Reflection/ReflectionActivatorResources.Designer.cs @@ -19,7 +19,7 @@ namespace Autofac.Core.Activators.Reflection { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class ReflectionActivatorResources { diff --git a/src/Autofac/Core/Lifetime/LifetimeScope.cs b/src/Autofac/Core/Lifetime/LifetimeScope.cs index a688cb397..e75414b9a 100644 --- a/src/Autofac/Core/Lifetime/LifetimeScope.cs +++ b/src/Autofac/Core/Lifetime/LifetimeScope.cs @@ -443,7 +443,8 @@ private void CheckNotDisposed() { if (IsTreeDisposed()) { - throw new ObjectDisposedException(LifetimeScopeResources.ScopeIsDisposed, innerException: null); + // Throw in a separate method to allow this check to be inlined. + ThrowDisposedException(); } } @@ -490,4 +491,10 @@ private bool IsTreeDisposed() /// Fired when a resolve operation is beginning in this scope. /// public event EventHandler? ResolveOperationBeginning; + + [DoesNotReturn] + private static void ThrowDisposedException() + { + throw new ObjectDisposedException(LifetimeScopeResources.ScopeIsDisposed, innerException: null); + } } diff --git a/test/Autofac.Test/Core/Activators/Reflection/MatchingSignatureConstructorSelectorTests.cs b/test/Autofac.Test/Core/Activators/Reflection/MatchingSignatureConstructorSelectorTests.cs index 5cb2a8f28..ea131e01a 100644 --- a/test/Autofac.Test/Core/Activators/Reflection/MatchingSignatureConstructorSelectorTests.cs +++ b/test/Autofac.Test/Core/Activators/Reflection/MatchingSignatureConstructorSelectorTests.cs @@ -28,13 +28,11 @@ public FourConstructors(int i, string s, double d) } } - private readonly BoundConstructor[] _ctors = GetConstructors(); - [Fact] public void SelectsEmptyConstructor() { var target0 = new MatchingSignatureConstructorSelector(); - var c0 = target0.SelectConstructorBinding(_ctors, Enumerable.Empty()); + var c0 = target0.SelectConstructorBinding(GetConstructors(), Enumerable.Empty()); Assert.Empty(c0.TargetConstructor.GetParameters()); } @@ -42,7 +40,7 @@ public void SelectsEmptyConstructor() public void SelectsConstructorWithParameters() { var target2 = new MatchingSignatureConstructorSelector(typeof(int), typeof(string)); - var c2 = target2.SelectConstructorBinding(_ctors, Enumerable.Empty()); + var c2 = target2.SelectConstructorBinding(GetConstructors(), Enumerable.Empty()); Assert.Equal(2, c2.TargetConstructor.GetParameters().Length); } @@ -50,7 +48,7 @@ public void SelectsConstructorWithParameters() public void IgnoresInvalidBindings() { var target2 = new MatchingSignatureConstructorSelector(typeof(int), typeof(string), typeof(double)); - Assert.Throws(() => target2.SelectConstructorBinding(_ctors, Enumerable.Empty())); + Assert.Throws(() => target2.SelectConstructorBinding(GetConstructors(), Enumerable.Empty())); } [Fact] @@ -59,7 +57,35 @@ public void WhenNoMatchingConstructorsAvailable_ExceptionDescribesTargetTypeAndS var target = new MatchingSignatureConstructorSelector(typeof(string)); var dx = Assert.Throws(() => - target.SelectConstructorBinding(_ctors, Enumerable.Empty())); + target.SelectConstructorBinding(GetConstructors(), Enumerable.Empty())); + + Assert.Contains(typeof(FourConstructors).Name, dx.Message); + Assert.Contains(typeof(string).Name, dx.Message); + } + + [Fact] + public void SelectsConstructorInEarlyBinding() + { + var target0 = new MatchingSignatureConstructorSelector(); + var c0 = target0.SelectConstructorBinder(GetConstructorBinders()); + Assert.Empty(c0.Constructor.GetParameters()); + } + + [Fact] + public void SelectsConstructorWithParametersInEarlyBinding() + { + var target2 = new MatchingSignatureConstructorSelector(typeof(int), typeof(string)); + var c2 = target2.SelectConstructorBinder(GetConstructorBinders()); + Assert.Equal(2, c2.Constructor.GetParameters().Length); + } + + [Fact] + public void WhenNoMatchingConstructorsAvailable_ExceptionDescribesTargetTypeAndSignature_InEarlyBinding() + { + var target = new MatchingSignatureConstructorSelector(typeof(string)); + + var dx = Assert.Throws(() => + target.SelectConstructorBinder(GetConstructorBinders())); Assert.Contains(typeof(FourConstructors).Name, dx.Message); Assert.Contains(typeof(string).Name, dx.Message); @@ -72,9 +98,16 @@ private static BoundConstructor[] GetConstructors() builder.Register(ctx => "test"); var container = builder.Build(); + return GetConstructorBinders() + .Select(cb => cb.Bind(new[] { new AutowiringParameter() }, container)) + .ToArray(); + } + + private static ConstructorBinder[] GetConstructorBinders() + { return typeof(FourConstructors) .GetTypeInfo().DeclaredConstructors - .Select(ci => new ConstructorBinder(ci).Bind(new[] { new AutowiringParameter() }, container)) + .Select(ci => new ConstructorBinder(ci)) .ToArray(); } }