From 6bc65ce15a4022eb0778ad50c4faba4c0a1bf285 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Fri, 14 Aug 2026 14:32:06 -0700 Subject: [PATCH 1/7] Preserve optional constructor argument defaults on class proxies. Class interception rewrites the registered implementation type to a Castle-generated proxy subclass. The generated constructors mirror the parameters of the proxied type but do not carry their default values, so Autofac's DefaultValueParameter cannot see them and optional arguments fail to bind with "None of the constructors found ... can be invoked with the available services and parameters." Supply the missing defaults from the type that was proxied. This is a last resort: resolve-time parameters, parameters configured on the registration, and services available from the container all still take precedence, so binding behavior matches an unproxied registration. Reported downstream as autofac/Autofac.ServiceFabric#41, where every service registration is class-intercepted and any optional constructor argument therefore fails. --- .../Polyfills/NotNullWhenAttribute.cs | 33 ++++ .../ProxiedDefaultValueParameter.cs | 133 +++++++++++++ .../RegistrationExtensions.cs | 12 +- ...terceptorsWithOptionalParametersFixture.cs | 181 ++++++++++++++++++ 4 files changed, 357 insertions(+), 2 deletions(-) create mode 100644 src/Autofac.Extras.DynamicProxy/Polyfills/NotNullWhenAttribute.cs create mode 100644 src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs create mode 100644 test/Autofac.Extras.DynamicProxy.Test/ClassInterceptorsWithOptionalParametersFixture.cs diff --git a/src/Autofac.Extras.DynamicProxy/Polyfills/NotNullWhenAttribute.cs b/src/Autofac.Extras.DynamicProxy/Polyfills/NotNullWhenAttribute.cs new file mode 100644 index 0000000..1a20085 --- /dev/null +++ b/src/Autofac.Extras.DynamicProxy/Polyfills/NotNullWhenAttribute.cs @@ -0,0 +1,33 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#if NETSTANDARD2_0 + +namespace System.Diagnostics.CodeAnalysis; + +/// +/// Polyfill for which is not available in netstandard2.0. +/// Specifies that when a method returns , +/// the parameter will not be null even if the corresponding type allows it. +/// +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +internal sealed class NotNullWhenAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// + /// Gets a value indicating whether the return value should be true or false for the parameter to be non-null. + /// + public bool ReturnValue + { + get; + } +} + +#endif diff --git a/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs b/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs new file mode 100644 index 0000000..79771f8 --- /dev/null +++ b/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs @@ -0,0 +1,133 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System.Reflection; +using Autofac.Core; + +namespace Autofac.Extras.DynamicProxy; + +/// +/// Supplies optional constructor argument values that are lost when a class proxy +/// is generated. +/// +/// +/// +/// Class interception replaces the registered implementation type with a generated +/// proxy subclass. The generated constructors mirror the parameters of the type +/// being proxied, but they don't carry the default values of those parameters, so +/// can't see +/// them and optional arguments fail to bind. This parameter reads the default values +/// from the type that was proxied and supplies them on the proxy's behalf. +/// +/// +/// This is a last resort. Values passed to the resolve operation, values configured +/// on the registration, and services available from the container all take +/// precedence, which keeps binding behavior the same as it would be without a proxy. +/// +/// +internal sealed class ProxiedDefaultValueParameter : Parameter +{ + private readonly Type _proxiedType; + + private readonly IEnumerable _configuredParameters; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The type that was proxied; the source of the default values. + /// + /// + /// The parameters configured on the registration. These take precedence over + /// default values, so they're checked before one is supplied. + /// + public ProxiedDefaultValueParameter(Type proxiedType, IEnumerable configuredParameters) + { + _proxiedType = proxiedType; + _configuredParameters = configuredParameters; + } + + /// + public override bool CanSupplyValue(ParameterInfo pi, IComponentContext context, [NotNullWhen(returnValue: true)] out Func? valueProvider) + { + if (pi == null) + { + throw new ArgumentNullException(nameof(pi)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + valueProvider = null; + + // Only generated proxy constructors are missing default values. Anything + // else already binds correctly on its own. + if (pi.Member is not ConstructorInfo || !_proxiedType.IsAssignableFrom(pi.Member.DeclaringType)) + { + return false; + } + + // Defer to the container when the service is genuinely available; autowiring + // wins over a default value on an unproxied type too. + if (context.ComponentRegistry.TryGetServiceRegistration(new TypedService(pi.ParameterType), out _)) + { + return false; + } + + // Defer to anything explicitly configured on the registration. + foreach (var configured in _configuredParameters) + { + if (configured.CanSupplyValue(pi, context, out _)) + { + return false; + } + } + + var proxied = FindProxiedParameter(pi); + + if (proxied is null || !proxied.HasDefaultValue) + { + return false; + } + + var defaultValue = proxied.DefaultValue; + + // Workaround for https://github.com/dotnet/corefx/issues/11797, mirroring + // the handling in Autofac's DefaultValueParameter. + if (defaultValue is null && pi.ParameterType.IsValueType) + { + defaultValue = Activator.CreateInstance(pi.ParameterType); + } + + valueProvider = () => defaultValue; + return true; + } + + /// + /// Locates the parameter on the proxied type that corresponds to a parameter on + /// the generated proxy constructor. + /// + /// The proxy constructor parameter. + /// + /// The matching parameter on the proxied type, or if + /// there isn't one. + /// + private ParameterInfo? FindProxiedParameter(ParameterInfo pi) + { + foreach (var constructor in _proxiedType.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + foreach (var parameter in constructor.GetParameters()) + { + if (string.Equals(parameter.Name, pi.Name, StringComparison.Ordinal) && + parameter.ParameterType == pi.ParameterType) + { + return parameter; + } + } + } + + return null; + } +} diff --git a/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs b/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs index 966f875..169176b 100644 --- a/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs +++ b/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs @@ -311,9 +311,14 @@ public static IRegistrationBuilder(); + + var instance = container.Resolve(); + + Assert.Null(instance.Dependency); + } + + [Fact] + public void OptionalValueParameterUsesDefaultWhenNotRegistered() + { + var container = BuildContainer(); + + var instance = container.Resolve(); + + Assert.Equal(42, instance.Count); + } + + [Fact] + public void InterceptionStillAppliesWhenOptionalParameterIsDefaulted() + { + var builder = new ContainerBuilder(); + builder.RegisterType() + .EnableClassInterceptors() + .InterceptedBy(typeof(AddOneInterceptor)); + builder.RegisterType(); + var container = builder.Build(); + + var instance = container.Resolve(); + + Assert.Equal(43, instance.GetCountByMethod()); + } + + [Fact] + public void RegisteredServiceTakesPrecedenceOverDefault() + { + var builder = new ContainerBuilder(); + builder.RegisterType() + .EnableClassInterceptors() + .InterceptedBy(typeof(DoNothingInterceptor)); + builder.RegisterType(); + builder.RegisterType().As(); + var container = builder.Build(); + + var instance = container.Resolve(); + + Assert.IsType(instance.Dependency); + } + + [Fact] + public void ConfiguredParameterTakesPrecedenceOverDefault() + { + var expected = new Dependency(); + var builder = new ContainerBuilder(); + builder.RegisterType() + .EnableClassInterceptors() + .InterceptedBy(typeof(DoNothingInterceptor)) + .WithParameter(TypedParameter.From(expected)); + builder.RegisterType(); + var container = builder.Build(); + + var instance = container.Resolve(); + + Assert.Same(expected, instance.Dependency); + } + + [Fact] + public void ResolveParameterTakesPrecedenceOverDefault() + { + var container = BuildContainer(); + var expected = new Dependency(); + + var instance = container.Resolve(TypedParameter.From(expected)); + + Assert.Same(expected, instance.Dependency); + } + + [Fact] + public void RequiredParameterStillThrowsWhenMissing() + { + var container = BuildContainer(); + + Assert.Throws(() => container.Resolve()); + } + + private static IContainer BuildContainer() + where TService : class + { + var builder = new ContainerBuilder(); + builder.RegisterType() + .EnableClassInterceptors() + .InterceptedBy(typeof(DoNothingInterceptor)); + builder.RegisterType(); + return builder.Build(); + } + + public interface IDependency + { + } + + public class Dependency : IDependency + { + } + + public class HasOptionalDependency + { + public HasOptionalDependency(IDependency? dependency = null) + { + Dependency = dependency; + } + + public IDependency? Dependency + { + get; + } + } + + public class HasOptionalValue + { + public HasOptionalValue(int count = 42) + { + Count = count; + } + + public int Count + { + get; + } + + public virtual int GetCountByMethod() + { + return Count; + } + } + + public class HasRequiredDependency + { + public HasRequiredDependency(IDependency dependency) + { + Dependency = dependency; + } + + public IDependency Dependency + { + get; + } + } + + private class DoNothingInterceptor : IInterceptor + { + public void Intercept(IInvocation invocation) + { + invocation.Proceed(); + } + } + + private class AddOneInterceptor : IInterceptor + { + public void Intercept(IInvocation invocation) + { + invocation.Proceed(); + + if (invocation.Method.ReturnType == typeof(int)) + { + invocation.ReturnValue = ((int)invocation.ReturnValue!) + 1; + } + } + } +} From d8111dc12b0f978c4c5dd64b5d47d1114969fee5 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Fri, 14 Aug 2026 14:43:30 -0700 Subject: [PATCH 2/7] Fix file encoding on new files to satisfy dotnet format. --- .../Polyfills/NotNullWhenAttribute.cs | 2 +- src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs | 2 +- .../ClassInterceptorsWithOptionalParametersFixture.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Autofac.Extras.DynamicProxy/Polyfills/NotNullWhenAttribute.cs b/src/Autofac.Extras.DynamicProxy/Polyfills/NotNullWhenAttribute.cs index 1a20085..0e77e6a 100644 --- a/src/Autofac.Extras.DynamicProxy/Polyfills/NotNullWhenAttribute.cs +++ b/src/Autofac.Extras.DynamicProxy/Polyfills/NotNullWhenAttribute.cs @@ -1,4 +1,4 @@ -// Copyright (c) Autofac Project. All rights reserved. +// Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. #if NETSTANDARD2_0 diff --git a/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs b/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs index 79771f8..70c4ffa 100644 --- a/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs +++ b/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs @@ -1,4 +1,4 @@ -// Copyright (c) Autofac Project. All rights reserved. +// Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. using System.Reflection; diff --git a/test/Autofac.Extras.DynamicProxy.Test/ClassInterceptorsWithOptionalParametersFixture.cs b/test/Autofac.Extras.DynamicProxy.Test/ClassInterceptorsWithOptionalParametersFixture.cs index 73d4a78..1f9066b 100644 --- a/test/Autofac.Extras.DynamicProxy.Test/ClassInterceptorsWithOptionalParametersFixture.cs +++ b/test/Autofac.Extras.DynamicProxy.Test/ClassInterceptorsWithOptionalParametersFixture.cs @@ -1,4 +1,4 @@ -// Copyright (c) Autofac Project. All rights reserved. +// Copyright (c) Autofac Project. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. using Autofac.Core; From 641b00e6b34a908465f5c849657aae956954db41 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Fri, 14 Aug 2026 15:17:24 -0700 Subject: [PATCH 3/7] Bind proxied defaults from the constructor actually being used. Matching a proxy constructor parameter to the proxied type by name and type alone searched every constructor, so overloads sharing a parameter name and type but declaring different defaults could supply the wrong value. Match the whole mirrored signature instead, using the count of leading arguments the proxy takes for itself. Also guard the default value read against the DateTime FormatException that Autofac's DefaultValueParameter handles. --- .../ProxiedDefaultValueParameter.cs | 96 +++++++++++++-- .../RegistrationExtensions.cs | 2 +- ...terceptorsWithOptionalParametersFixture.cs | 110 ++++++++++++++++++ 3 files changed, 199 insertions(+), 9 deletions(-) diff --git a/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs b/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs index 70c4ffa..592449a 100644 --- a/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs +++ b/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs @@ -31,6 +31,8 @@ internal sealed class ProxiedDefaultValueParameter : Parameter private readonly IEnumerable _configuredParameters; + private readonly int _proxyArgumentCount; + /// /// Initializes a new instance of the class. /// @@ -41,10 +43,16 @@ internal sealed class ProxiedDefaultValueParameter : Parameter /// The parameters configured on the registration. These take precedence over /// default values, so they're checked before one is supplied. /// - public ProxiedDefaultValueParameter(Type proxiedType, IEnumerable configuredParameters) + /// + /// The number of leading arguments the generated constructors take for the proxy + /// itself - the mixins, the interceptor array, and the selector. The parameters + /// mirrored from the proxied type start after these. + /// + public ProxiedDefaultValueParameter(Type proxiedType, IEnumerable configuredParameters, int proxyArgumentCount) { _proxiedType = proxiedType; _configuredParameters = configuredParameters; + _proxyArgumentCount = proxyArgumentCount; } /// @@ -87,7 +95,28 @@ public override bool CanSupplyValue(ParameterInfo pi, IComponentContext context, var proxied = FindProxiedParameter(pi); - if (proxied is null || !proxied.HasDefaultValue) + if (proxied is null) + { + return false; + } + + bool hasDefaultValue; + + try + { + hasDefaultValue = proxied.HasDefaultValue; + } + catch (FormatException) when (proxied.ParameterType == typeof(DateTime)) + { + // Workaround for https://github.com/dotnet/corefx/issues/12338, mirroring + // the handling in Autofac's DefaultValueParameter. Reading the default + // value of a DateTime parameter can throw, in which case the parameter is + // known to have one. + valueProvider = () => default(DateTime); + return true; + } + + if (!hasDefaultValue) { return false; } @@ -114,20 +143,71 @@ public override bool CanSupplyValue(ParameterInfo pi, IComponentContext context, /// The matching parameter on the proxied type, or if /// there isn't one. /// + /// + /// + /// A generated constructor takes the arguments the proxy itself needs and then + /// mirrors, in order, the parameters of the one constructor it chains to. The + /// whole mirrored signature has to be matched to find that constructor: + /// overloads can share a parameter name and type while declaring different + /// default values, so matching a single parameter across all of them picks up + /// the wrong default. + /// + /// private ParameterInfo? FindProxiedParameter(ParameterInfo pi) { + var mirroredPosition = pi.Position - _proxyArgumentCount; + + if (mirroredPosition < 0) + { + // An argument belonging to the proxy rather than to the proxied type. + return null; + } + + var mirrored = ((ConstructorInfo)pi.Member).GetParameters(); + + // Non-public constructors are included because a protected constructor is + // mirrored by a public one on the proxy, which the container can then select. foreach (var constructor in _proxiedType.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { - foreach (var parameter in constructor.GetParameters()) + var candidates = constructor.GetParameters(); + + if (candidates.Length != mirrored.Length - _proxyArgumentCount) { - if (string.Equals(parameter.Name, pi.Name, StringComparison.Ordinal) && - parameter.ParameterType == pi.ParameterType) - { - return parameter; - } + continue; + } + + if (IsMirroredBy(candidates, mirrored)) + { + return candidates[mirroredPosition]; } } return null; } + + /// + /// Determines whether the parameters of a constructor on the proxied type are the + /// ones a generated constructor mirrors. + /// + /// The parameters of a constructor on the proxied type. + /// The parameters of the generated proxy constructor. + /// + /// if the generated constructor mirrors + /// ; otherwise, . + /// + private bool IsMirroredBy(ParameterInfo[] candidates, ParameterInfo[] mirrored) + { + for (var i = 0; i < candidates.Length; i++) + { + var proxyParameter = mirrored[i + _proxyArgumentCount]; + + if (!string.Equals(candidates[i].Name, proxyParameter.Name, StringComparison.Ordinal) || + candidates[i].ParameterType != proxyParameter.ParameterType) + { + return false; + } + } + + return true; + } } diff --git a/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs b/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs index 169176b..431de1d 100644 --- a/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs +++ b/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs @@ -350,7 +350,7 @@ public static IRegistrationBuilder(() => container.Resolve()); } + [Fact] + public void DefaultComesFromTheSelectedConstructorOverload() + { + var builder = new ContainerBuilder(); + builder.RegisterType() + .EnableClassInterceptors() + .InterceptedBy(typeof(DoNothingInterceptor)) + .WithParameter(TypedParameter.From(new Dependency())); + builder.RegisterType(); + var container = builder.Build(); + + var instance = container.Resolve(); + + Assert.Equal(99, instance.Count); + } + + [Fact] + public void DefaultComesFromTheShorterConstructorWhenItIsTheOneSelected() + { + var container = BuildContainer(); + + var instance = container.Resolve(); + + Assert.Equal(1, instance.Count); + } + + [Fact] + public void ProtectedConstructorDefaultIsUsed() + { + // A protected constructor is mirrored by a public one on the proxy, so the + // container can select it where it couldn't on the unproxied type. + var builder = new ContainerBuilder(); + builder.RegisterType() + .EnableClassInterceptors() + .InterceptedBy(typeof(DoNothingInterceptor)) + .WithParameter(TypedParameter.From("named")); + builder.RegisterType(); + var container = builder.Build(); + + var instance = container.Resolve(); + + Assert.Equal(7, instance.Count); + } + + [Fact] + public void OptionalDateTimeParameterUsesDefault() + { + var container = BuildContainer(); + + var instance = container.Resolve(); + + Assert.Equal(default, instance.When); + } + private static IContainer BuildContainer() where TService : class { @@ -145,6 +199,62 @@ public virtual int GetCountByMethod() } } + public class HasOptionalDateTime + { + public HasOptionalDateTime(DateTime when = default) + { + When = when; + } + + public DateTime When + { + get; + } + } + + public class HasOverloadedConstructors + { + public HasOverloadedConstructors(int count = 1) + { + Count = count; + } + + public HasOverloadedConstructors(IDependency dependency, int count = 99) + { + Dependency = dependency; + Count = count; + } + + public int Count + { + get; + } + + public IDependency? Dependency + { + get; + } + } + + public class HasProtectedConstructor + { + protected HasProtectedConstructor(string name, int count = 7) + { + Name = name; + Count = count; + } + + public string Name + { + get; + } + + public int Count + { + get; + } + } + public class HasRequiredDependency { public HasRequiredDependency(IDependency dependency) From 0d4f379c7ec7168e19420b5a4343afe30e97c3fb Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Fri, 14 Aug 2026 15:23:05 -0700 Subject: [PATCH 4/7] Formatting updates. --- .../ProxiedDefaultValueParameter.cs | 102 ++++++++++-------- .../RegistrationExtensions.cs | 13 +-- ...terceptorsWithOptionalParametersFixture.cs | 25 +---- 3 files changed, 66 insertions(+), 74 deletions(-) diff --git a/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs b/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs index 592449a..e8b8f4a 100644 --- a/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs +++ b/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs @@ -7,22 +7,25 @@ namespace Autofac.Extras.DynamicProxy; /// -/// Supplies optional constructor argument values that are lost when a class proxy -/// is generated. +/// Supplies optional constructor argument values that are lost when a class +/// proxy is generated. /// /// /// -/// Class interception replaces the registered implementation type with a generated -/// proxy subclass. The generated constructors mirror the parameters of the type -/// being proxied, but they don't carry the default values of those parameters, so -/// can't see -/// them and optional arguments fail to bind. This parameter reads the default values -/// from the type that was proxied and supplies them on the proxy's behalf. +/// Class interception replaces the registered implementation type with a +/// generated proxy subclass. The generated constructors mirror the parameters +/// of the type being proxied, but they don't carry the default values of those +/// parameters, so +/// can't +/// see them and optional arguments fail to bind. This parameter reads the +/// default values from the type that was proxied and supplies them on the +/// proxy's behalf. /// /// -/// This is a last resort. Values passed to the resolve operation, values configured -/// on the registration, and services available from the container all take -/// precedence, which keeps binding behavior the same as it would be without a proxy. +/// This is a last resort. Values passed to the resolve operation, values +/// configured on the registration, and services available from the container +/// all take precedence, which keeps binding behavior the same as it would be +/// without a proxy. /// /// internal sealed class ProxiedDefaultValueParameter : Parameter @@ -34,19 +37,20 @@ internal sealed class ProxiedDefaultValueParameter : Parameter private readonly int _proxyArgumentCount; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the + /// class. /// /// /// The type that was proxied; the source of the default values. /// /// - /// The parameters configured on the registration. These take precedence over - /// default values, so they're checked before one is supplied. + /// The parameters configured on the registration. These take precedence + /// over default values, so they're checked before one is supplied. /// /// - /// The number of leading arguments the generated constructors take for the proxy - /// itself - the mixins, the interceptor array, and the selector. The parameters - /// mirrored from the proxied type start after these. + /// The number of leading arguments the generated constructors take for the + /// proxy itself - the mixins, the interceptor array, and the selector. The + /// parameters mirrored from the proxied type start after these. /// public ProxiedDefaultValueParameter(Type proxiedType, IEnumerable configuredParameters, int proxyArgumentCount) { @@ -70,15 +74,15 @@ public override bool CanSupplyValue(ParameterInfo pi, IComponentContext context, valueProvider = null; - // Only generated proxy constructors are missing default values. Anything - // else already binds correctly on its own. + // Only generated proxy constructors are missing default values. + // Anything else already binds correctly on its own. if (pi.Member is not ConstructorInfo || !_proxiedType.IsAssignableFrom(pi.Member.DeclaringType)) { return false; } - // Defer to the container when the service is genuinely available; autowiring - // wins over a default value on an unproxied type too. + // Defer to the container when the service is genuinely available; + // autowiring wins over a default value on an unproxied type too. if (context.ComponentRegistry.TryGetServiceRegistration(new TypedService(pi.ParameterType), out _)) { return false; @@ -108,10 +112,10 @@ public override bool CanSupplyValue(ParameterInfo pi, IComponentContext context, } catch (FormatException) when (proxied.ParameterType == typeof(DateTime)) { - // Workaround for https://github.com/dotnet/corefx/issues/12338, mirroring - // the handling in Autofac's DefaultValueParameter. Reading the default - // value of a DateTime parameter can throw, in which case the parameter is - // known to have one. + // Workaround for https://github.com/dotnet/corefx/issues/12338, + // mirroring the handling in Autofac's DefaultValueParameter. + // Reading the default value of a DateTime parameter can throw, in + // which case the parameter is known to have one. valueProvider = () => default(DateTime); return true; } @@ -123,8 +127,8 @@ public override bool CanSupplyValue(ParameterInfo pi, IComponentContext context, var defaultValue = proxied.DefaultValue; - // Workaround for https://github.com/dotnet/corefx/issues/11797, mirroring - // the handling in Autofac's DefaultValueParameter. + // Workaround for https://github.com/dotnet/corefx/issues/11797, + // mirroring the handling in Autofac's DefaultValueParameter. if (defaultValue is null && pi.ParameterType.IsValueType) { defaultValue = Activator.CreateInstance(pi.ParameterType); @@ -135,22 +139,24 @@ public override bool CanSupplyValue(ParameterInfo pi, IComponentContext context, } /// - /// Locates the parameter on the proxied type that corresponds to a parameter on - /// the generated proxy constructor. + /// Locates the parameter on the proxied type that corresponds to a + /// parameter on the generated proxy constructor. /// - /// The proxy constructor parameter. + /// + /// The proxy constructor parameter. + /// /// - /// The matching parameter on the proxied type, or if - /// there isn't one. + /// The matching parameter on the proxied type, or + /// if there isn't one. /// /// /// - /// A generated constructor takes the arguments the proxy itself needs and then - /// mirrors, in order, the parameters of the one constructor it chains to. The - /// whole mirrored signature has to be matched to find that constructor: - /// overloads can share a parameter name and type while declaring different - /// default values, so matching a single parameter across all of them picks up - /// the wrong default. + /// A generated constructor takes the arguments the proxy itself needs and + /// then mirrors, in order, the parameters of the one constructor it chains + /// to. The whole mirrored signature has to be matched to find that + /// constructor: overloads can share a parameter name and type while + /// declaring different default values, so matching a single parameter + /// across all of them picks up the wrong default. /// /// private ParameterInfo? FindProxiedParameter(ParameterInfo pi) @@ -159,14 +165,16 @@ public override bool CanSupplyValue(ParameterInfo pi, IComponentContext context, if (mirroredPosition < 0) { - // An argument belonging to the proxy rather than to the proxied type. + // An argument belonging to the proxy rather than to the proxied + // type. return null; } var mirrored = ((ConstructorInfo)pi.Member).GetParameters(); - // Non-public constructors are included because a protected constructor is - // mirrored by a public one on the proxy, which the container can then select. + // Non-public constructors are included because a protected constructor + // is mirrored by a public one on the proxy, which the container can + // then select. foreach (var constructor in _proxiedType.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { var candidates = constructor.GetParameters(); @@ -186,11 +194,15 @@ public override bool CanSupplyValue(ParameterInfo pi, IComponentContext context, } /// - /// Determines whether the parameters of a constructor on the proxied type are the - /// ones a generated constructor mirrors. + /// Determines whether the parameters of a constructor on the proxied type + /// are the ones a generated constructor mirrors. /// - /// The parameters of a constructor on the proxied type. - /// The parameters of the generated proxy constructor. + /// + /// The parameters of a constructor on the proxied type. + /// + /// + /// The parameters of the generated proxy constructor. + /// /// /// if the generated constructor mirrors /// ; otherwise, . diff --git a/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs b/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs index 431de1d..1b3c0f2 100644 --- a/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs +++ b/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs @@ -303,17 +303,18 @@ public static IRegistrationBuilder(); - var instance = container.Resolve(); - Assert.Null(instance.Dependency); } @@ -22,9 +20,7 @@ public void OptionalReferenceParameterUsesDefaultWhenNotRegistered() public void OptionalValueParameterUsesDefaultWhenNotRegistered() { var container = BuildContainer(); - var instance = container.Resolve(); - Assert.Equal(42, instance.Count); } @@ -37,9 +33,7 @@ public void InterceptionStillAppliesWhenOptionalParameterIsDefaulted() .InterceptedBy(typeof(AddOneInterceptor)); builder.RegisterType(); var container = builder.Build(); - var instance = container.Resolve(); - Assert.Equal(43, instance.GetCountByMethod()); } @@ -53,9 +47,7 @@ public void RegisteredServiceTakesPrecedenceOverDefault() builder.RegisterType(); builder.RegisterType().As(); var container = builder.Build(); - var instance = container.Resolve(); - Assert.IsType(instance.Dependency); } @@ -70,9 +62,7 @@ public void ConfiguredParameterTakesPrecedenceOverDefault() .WithParameter(TypedParameter.From(expected)); builder.RegisterType(); var container = builder.Build(); - var instance = container.Resolve(); - Assert.Same(expected, instance.Dependency); } @@ -81,9 +71,7 @@ public void ResolveParameterTakesPrecedenceOverDefault() { var container = BuildContainer(); var expected = new Dependency(); - var instance = container.Resolve(TypedParameter.From(expected)); - Assert.Same(expected, instance.Dependency); } @@ -91,7 +79,6 @@ public void ResolveParameterTakesPrecedenceOverDefault() public void RequiredParameterStillThrowsWhenMissing() { var container = BuildContainer(); - Assert.Throws(() => container.Resolve()); } @@ -105,9 +92,7 @@ public void DefaultComesFromTheSelectedConstructorOverload() .WithParameter(TypedParameter.From(new Dependency())); builder.RegisterType(); var container = builder.Build(); - var instance = container.Resolve(); - Assert.Equal(99, instance.Count); } @@ -115,17 +100,15 @@ public void DefaultComesFromTheSelectedConstructorOverload() public void DefaultComesFromTheShorterConstructorWhenItIsTheOneSelected() { var container = BuildContainer(); - var instance = container.Resolve(); - Assert.Equal(1, instance.Count); } [Fact] public void ProtectedConstructorDefaultIsUsed() { - // A protected constructor is mirrored by a public one on the proxy, so the - // container can select it where it couldn't on the unproxied type. + // A protected constructor is mirrored by a public one on the proxy, so + // the container can select it where it couldn't on the unproxied type. var builder = new ContainerBuilder(); builder.RegisterType() .EnableClassInterceptors() @@ -133,9 +116,7 @@ public void ProtectedConstructorDefaultIsUsed() .WithParameter(TypedParameter.From("named")); builder.RegisterType(); var container = builder.Build(); - var instance = container.Resolve(); - Assert.Equal(7, instance.Count); } @@ -143,9 +124,7 @@ public void ProtectedConstructorDefaultIsUsed() public void OptionalDateTimeParameterUsesDefault() { var container = BuildContainer(); - var instance = container.Resolve(); - Assert.Equal(default, instance.When); } From e95bb7aab7e62ef5ce8abde06bc2765644ba9d39 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Fri, 14 Aug 2026 15:38:09 -0700 Subject: [PATCH 5/7] Update all dependencies. --- .../Autofac.Extras.DynamicProxy.Benchmarks.csproj | 2 +- .../Autofac.Extras.DynamicProxy.csproj | 6 +++--- ...utofac.Extras.DynamicProxy.Test.SatelliteAssembly.csproj | 2 +- .../Autofac.Extras.DynamicProxy.Test.csproj | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bench/Autofac.Extras.DynamicProxy.Benchmarks/Autofac.Extras.DynamicProxy.Benchmarks.csproj b/bench/Autofac.Extras.DynamicProxy.Benchmarks/Autofac.Extras.DynamicProxy.Benchmarks.csproj index fe4acad..8cbad49 100644 --- a/bench/Autofac.Extras.DynamicProxy.Benchmarks/Autofac.Extras.DynamicProxy.Benchmarks.csproj +++ b/bench/Autofac.Extras.DynamicProxy.Benchmarks/Autofac.Extras.DynamicProxy.Benchmarks.csproj @@ -27,7 +27,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Autofac.Extras.DynamicProxy/Autofac.Extras.DynamicProxy.csproj b/src/Autofac.Extras.DynamicProxy/Autofac.Extras.DynamicProxy.csproj index b3f403d..8955c5a 100644 --- a/src/Autofac.Extras.DynamicProxy/Autofac.Extras.DynamicProxy.csproj +++ b/src/Autofac.Extras.DynamicProxy/Autofac.Extras.DynamicProxy.csproj @@ -54,12 +54,12 @@ - + - + all - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Autofac.Extras.DynamicProxy.Test.SatelliteAssembly/Autofac.Extras.DynamicProxy.Test.SatelliteAssembly.csproj b/test/Autofac.Extras.DynamicProxy.Test.SatelliteAssembly/Autofac.Extras.DynamicProxy.Test.SatelliteAssembly.csproj index cfc1fc4..a3750b6 100644 --- a/test/Autofac.Extras.DynamicProxy.Test.SatelliteAssembly/Autofac.Extras.DynamicProxy.Test.SatelliteAssembly.csproj +++ b/test/Autofac.Extras.DynamicProxy.Test.SatelliteAssembly/Autofac.Extras.DynamicProxy.Test.SatelliteAssembly.csproj @@ -18,7 +18,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Autofac.Extras.DynamicProxy.Test/Autofac.Extras.DynamicProxy.Test.csproj b/test/Autofac.Extras.DynamicProxy.Test/Autofac.Extras.DynamicProxy.Test.csproj index c41b098..2dcd197 100644 --- a/test/Autofac.Extras.DynamicProxy.Test/Autofac.Extras.DynamicProxy.Test.csproj +++ b/test/Autofac.Extras.DynamicProxy.Test/Autofac.Extras.DynamicProxy.Test.csproj @@ -30,8 +30,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From e2dbddf20a567b2b805629cc0df7d9d468d74a99 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Fri, 14 Aug 2026 15:51:38 -0700 Subject: [PATCH 6/7] Read the proxied default values once instead of per resolve. Walking the proxied type's constructors inside CanSupplyValue meant reflecting over them on every resolve of a class-intercepted component, and allocating a fresh parameter, wrapper array and closure each time. Read the defaults into a lookup keyed by the proxy constructor parameter, once, on the first resolve, so binding is a dictionary hit. For a class-intercepted type with an optional argument this drops a resolve from 835 ns / 2.78 KB to 759 ns / 2.48 KB; the overhead on an intercepted type with no arguments falls from 133 to 72 bytes. Adds a benchmark covering the scenario. Drops the DateTime FormatException guard along the way: it covers a .NET Core 1.x reflection bug that no framework this package targets can hit, and the block can't be exercised by a test. Tests cover the constructor matching that used to live in the resolve path, including overloads that take the same number of arguments. --- .../ClassInterceptionBenchmark.cs | 10 + .../Scenario/ClassWithOptionalParameter.cs | 19 ++ .../ProxiedDefaultValueParameter.cs | 210 ++++++++++-------- .../RegistrationExtensions.cs | 18 +- ...terceptorsWithOptionalParametersFixture.cs | 46 ++++ 5 files changed, 202 insertions(+), 101 deletions(-) create mode 100644 bench/Autofac.Extras.DynamicProxy.Benchmarks/Scenario/ClassWithOptionalParameter.cs diff --git a/bench/Autofac.Extras.DynamicProxy.Benchmarks/ClassInterceptionBenchmark.cs b/bench/Autofac.Extras.DynamicProxy.Benchmarks/ClassInterceptionBenchmark.cs index 2bd9b48..8d4898d 100644 --- a/bench/Autofac.Extras.DynamicProxy.Benchmarks/ClassInterceptionBenchmark.cs +++ b/bench/Autofac.Extras.DynamicProxy.Benchmarks/ClassInterceptionBenchmark.cs @@ -21,6 +21,9 @@ public void Setup() builder.RegisterType() .EnableClassInterceptors() .InterceptedBy(typeof(StringMethodInterceptor)); + builder.RegisterType() + .EnableClassInterceptors() + .InterceptedBy(typeof(StringMethodInterceptor)); builder.RegisterType(); _container = builder.Build(); } @@ -38,4 +41,11 @@ public string WiredUsingInterceptedBy() var instance = _container.Resolve(); return instance.Test(); } + + [Benchmark] + public string WithOptionalConstructorParameter() + { + var instance = _container.Resolve(); + return instance.Test(); + } } diff --git a/bench/Autofac.Extras.DynamicProxy.Benchmarks/Scenario/ClassWithOptionalParameter.cs b/bench/Autofac.Extras.DynamicProxy.Benchmarks/Scenario/ClassWithOptionalParameter.cs new file mode 100644 index 0000000..d90490e --- /dev/null +++ b/bench/Autofac.Extras.DynamicProxy.Benchmarks/Scenario/ClassWithOptionalParameter.cs @@ -0,0 +1,19 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Autofac.Extras.DynamicProxy.Benchmarks.Scenario; + +public class ClassWithOptionalParameter : ITest +{ + private readonly int _count; + + public ClassWithOptionalParameter(int count = 42) + { + _count = count; + } + + public virtual string Test() + { + return _count.ToString(System.Globalization.CultureInfo.InvariantCulture); + } +} diff --git a/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs b/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs index e8b8f4a..f0490fa 100644 --- a/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs +++ b/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs @@ -22,6 +22,10 @@ namespace Autofac.Extras.DynamicProxy; /// proxy's behalf. /// /// +/// The values are read once, when this parameter is created, so resolving costs +/// a dictionary lookup rather than a walk over the constructors. +/// +/// /// This is a last resort. Values passed to the resolve operation, values /// configured on the registration, and services available from the container /// all take precedence, which keeps binding behavior the same as it would be @@ -30,16 +34,18 @@ namespace Autofac.Extras.DynamicProxy; /// internal sealed class ProxiedDefaultValueParameter : Parameter { - private readonly Type _proxiedType; - private readonly IEnumerable _configuredParameters; - private readonly int _proxyArgumentCount; + private readonly Dictionary> _defaultValues; /// /// Initializes a new instance of the /// class. /// + /// + /// The generated proxy type, whose constructor parameters are the ones being + /// supplied. + /// /// /// The type that was proxied; the source of the default values. /// @@ -52,31 +58,18 @@ internal sealed class ProxiedDefaultValueParameter : Parameter /// proxy itself - the mixins, the interceptor array, and the selector. The /// parameters mirrored from the proxied type start after these. /// - public ProxiedDefaultValueParameter(Type proxiedType, IEnumerable configuredParameters, int proxyArgumentCount) + public ProxiedDefaultValueParameter(Type proxyType, Type proxiedType, IEnumerable configuredParameters, int proxyArgumentCount) { - _proxiedType = proxiedType; _configuredParameters = configuredParameters; - _proxyArgumentCount = proxyArgumentCount; + _defaultValues = FindDefaultValues(proxyType, proxiedType, proxyArgumentCount); } /// public override bool CanSupplyValue(ParameterInfo pi, IComponentContext context, [NotNullWhen(returnValue: true)] out Func? valueProvider) { - if (pi == null) - { - throw new ArgumentNullException(nameof(pi)); - } - - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - valueProvider = null; - // Only generated proxy constructors are missing default values. - // Anything else already binds correctly on its own. - if (pi.Member is not ConstructorInfo || !_proxiedType.IsAssignableFrom(pi.Member.DeclaringType)) + if (!_defaultValues.TryGetValue(pi, out var defaultValueProvider)) { return false; } @@ -97,124 +90,89 @@ public override bool CanSupplyValue(ParameterInfo pi, IComponentContext context, } } - var proxied = FindProxiedParameter(pi); - - if (proxied is null) - { - return false; - } - - bool hasDefaultValue; - - try - { - hasDefaultValue = proxied.HasDefaultValue; - } - catch (FormatException) when (proxied.ParameterType == typeof(DateTime)) - { - // Workaround for https://github.com/dotnet/corefx/issues/12338, - // mirroring the handling in Autofac's DefaultValueParameter. - // Reading the default value of a DateTime parameter can throw, in - // which case the parameter is known to have one. - valueProvider = () => default(DateTime); - return true; - } - - if (!hasDefaultValue) - { - return false; - } - - var defaultValue = proxied.DefaultValue; - - // Workaround for https://github.com/dotnet/corefx/issues/11797, - // mirroring the handling in Autofac's DefaultValueParameter. - if (defaultValue is null && pi.ParameterType.IsValueType) - { - defaultValue = Activator.CreateInstance(pi.ParameterType); - } - - valueProvider = () => defaultValue; + valueProvider = defaultValueProvider; return true; } /// - /// Locates the parameter on the proxied type that corresponds to a - /// parameter on the generated proxy constructor. + /// Reads the default values the generated constructors dropped, keyed by the + /// proxy constructor parameter each one belongs to. /// - /// - /// The proxy constructor parameter. + /// The generated proxy type. + /// The type that was proxied. + /// + /// The number of leading arguments the generated constructors take for the + /// proxy itself. /// /// - /// The matching parameter on the proxied type, or - /// if there isn't one. + /// The default value providers for the parameters that have one. /// /// /// - /// A generated constructor takes the arguments the proxy itself needs and - /// then mirrors, in order, the parameters of the one constructor it chains - /// to. The whole mirrored signature has to be matched to find that - /// constructor: overloads can share a parameter name and type while - /// declaring different default values, so matching a single parameter + /// A generated constructor mirrors, in order, the parameters of the one + /// constructor it chains to. The whole mirrored signature has to be matched + /// to find that constructor: overloads can share a parameter name and type + /// while declaring different default values, so matching a single parameter /// across all of them picks up the wrong default. /// /// - private ParameterInfo? FindProxiedParameter(ParameterInfo pi) + private static Dictionary> FindDefaultValues(Type proxyType, Type proxiedType, int proxyArgumentCount) { - var mirroredPosition = pi.Position - _proxyArgumentCount; - - if (mirroredPosition < 0) - { - // An argument belonging to the proxy rather than to the proxied - // type. - return null; - } - - var mirrored = ((ConstructorInfo)pi.Member).GetParameters(); + var defaultValues = new Dictionary>(); // Non-public constructors are included because a protected constructor - // is mirrored by a public one on the proxy, which the container can - // then select. - foreach (var constructor in _proxiedType.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + // is mirrored by a public one on the proxy, which the container can then + // select. + var proxiedConstructors = proxiedType.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + foreach (var proxyConstructor in proxyType.GetConstructors()) { - var candidates = constructor.GetParameters(); + var mirrored = proxyConstructor.GetParameters(); - if (candidates.Length != mirrored.Length - _proxyArgumentCount) + foreach (var proxiedConstructor in proxiedConstructors) { - continue; - } + var proxied = proxiedConstructor.GetParameters(); - if (IsMirroredBy(candidates, mirrored)) - { - return candidates[mirroredPosition]; + if (proxied.Length != mirrored.Length - proxyArgumentCount || + !IsMirroredBy(proxied, mirrored, proxyArgumentCount)) + { + continue; + } + + AddDefaultValues(defaultValues, proxied, mirrored, proxyArgumentCount); + break; } } - return null; + return defaultValues; } /// /// Determines whether the parameters of a constructor on the proxied type /// are the ones a generated constructor mirrors. /// - /// + /// /// The parameters of a constructor on the proxied type. /// /// /// The parameters of the generated proxy constructor. /// + /// + /// The number of leading arguments the generated constructor takes for the + /// proxy itself. + /// /// /// if the generated constructor mirrors - /// ; otherwise, . + /// ; otherwise, . /// - private bool IsMirroredBy(ParameterInfo[] candidates, ParameterInfo[] mirrored) + private static bool IsMirroredBy(ParameterInfo[] proxied, ParameterInfo[] mirrored, int proxyArgumentCount) { - for (var i = 0; i < candidates.Length; i++) + for (var i = 0; i < proxied.Length; i++) { - var proxyParameter = mirrored[i + _proxyArgumentCount]; + var proxyParameter = mirrored[i + proxyArgumentCount]; - if (!string.Equals(candidates[i].Name, proxyParameter.Name, StringComparison.Ordinal) || - candidates[i].ParameterType != proxyParameter.ParameterType) + if (!string.Equals(proxied[i].Name, proxyParameter.Name, StringComparison.Ordinal) || + proxied[i].ParameterType != proxyParameter.ParameterType) { return false; } @@ -222,4 +180,62 @@ private bool IsMirroredBy(ParameterInfo[] candidates, ParameterInfo[] mirrored) return true; } + + /// + /// Records the default values declared on a constructor of the proxied type + /// against the parameters of the generated constructor mirroring it. + /// + /// The set of default values being built. + /// + /// The parameters of the constructor on the proxied type. + /// + /// + /// The parameters of the generated proxy constructor. + /// + /// + /// The number of leading arguments the generated constructor takes for the + /// proxy itself. + /// + private static void AddDefaultValues(Dictionary> defaultValues, ParameterInfo[] proxied, ParameterInfo[] mirrored, int proxyArgumentCount) + { + for (var i = 0; i < proxied.Length; i++) + { + if (TryGetDefaultValue(proxied[i], out var defaultValue)) + { + defaultValues.Add(mirrored[i + proxyArgumentCount], () => defaultValue); + } + } + } + + /// + /// Reads the default value declared on a parameter of the proxied type. + /// + /// The parameter on the proxied type. + /// + /// The default value, if the parameter declares one. + /// + /// + /// if the parameter declares a default value; + /// otherwise, . + /// + private static bool TryGetDefaultValue(ParameterInfo proxied, out object? defaultValue) + { + defaultValue = null; + + if (!proxied.HasDefaultValue) + { + return false; + } + + defaultValue = proxied.DefaultValue; + + // Workaround for https://github.com/dotnet/corefx/issues/11797, + // mirroring the handling in Autofac's DefaultValueParameter. + if (defaultValue is null && proxied.ParameterType.IsValueType) + { + defaultValue = Activator.CreateInstance(proxied.ParameterType); + } + + return true; + } } diff --git a/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs b/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs index 1b3c0f2..66ff55e 100644 --- a/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs +++ b/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs @@ -312,9 +312,6 @@ public static IRegistrationBuilder { var proxyParameters = new List(); @@ -349,9 +353,15 @@ public static IRegistrationBuilder() + .EnableClassInterceptors() + .InterceptedBy(typeof(DoNothingInterceptor)); + builder.RegisterType(); + builder.RegisterType().As(); + var container = builder.Build(); + + var instance = container.Resolve(); + + Assert.Equal(3, instance.Count); + } + [Fact] public void ProtectedConstructorDefaultIsUsed() { @@ -215,6 +231,36 @@ public IDependency? Dependency } } + public class HasSameArityConstructors + { + public HasSameArityConstructors(IDependency dependency, int count = 3) + { + Dependency = dependency; + Count = count; + } + + public HasSameArityConstructors(string name, int count = 4) + { + Name = name; + Count = count; + } + + public int Count + { + get; + } + + public IDependency? Dependency + { + get; + } + + public string? Name + { + get; + } + } + public class HasProtectedConstructor { protected HasProtectedConstructor(string name, int count = 7) From 7b9186d2d42496b9542e27d3d9ed09e2124fe3fe Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Fri, 14 Aug 2026 16:19:59 -0700 Subject: [PATCH 7/7] Bump version to 8.1.0. Optional constructor arguments on class proxies bind to their defaults now, so a constructor that previously couldn't bind can be selected where a narrower overload used to win. That changes behavior for applications resolving successfully today, which is more than a patch should carry. --- default.proj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.proj b/default.proj index 7cbb8f5..a3a8bf8 100644 --- a/default.proj +++ b/default.proj @@ -2,7 +2,7 @@ - 8.0.1 + 8.1.0 Autofac.Extras.DynamicProxy Release $([System.IO.Path]::Combine($(MSBuildProjectDirectory),"artifacts"))