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 @@
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
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/default.proj b/default.proj
index 7cbb8f5..a3a8bf8 100644
--- a/default.proj
+++ b/default.proj
@@ -2,7 +2,7 @@
- 8.0.1
+ 8.1.0Autofac.Extras.DynamicProxyRelease$([System.IO.Path]::Combine($(MSBuildProjectDirectory),"artifacts"))
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
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/Autofac.Extras.DynamicProxy/Polyfills/NotNullWhenAttribute.cs b/src/Autofac.Extras.DynamicProxy/Polyfills/NotNullWhenAttribute.cs
new file mode 100644
index 0000000..0e77e6a
--- /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..f0490fa
--- /dev/null
+++ b/src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs
@@ -0,0 +1,241 @@
+// 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.
+///
+///
+/// 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
+/// without a proxy.
+///
+///
+internal sealed class ProxiedDefaultValueParameter : Parameter
+{
+ private readonly IEnumerable _configuredParameters;
+
+ 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.
+ ///
+ ///
+ /// 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.
+ ///
+ public ProxiedDefaultValueParameter(Type proxyType, Type proxiedType, IEnumerable configuredParameters, int proxyArgumentCount)
+ {
+ _configuredParameters = configuredParameters;
+ _defaultValues = FindDefaultValues(proxyType, proxiedType, proxyArgumentCount);
+ }
+
+ ///
+ public override bool CanSupplyValue(ParameterInfo pi, IComponentContext context, [NotNullWhen(returnValue: true)] out Func
-
+ allruntime; 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 @@
allruntime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/test/Autofac.Extras.DynamicProxy.Test/ClassInterceptorsWithOptionalParametersFixture.cs b/test/Autofac.Extras.DynamicProxy.Test/ClassInterceptorsWithOptionalParametersFixture.cs
new file mode 100644
index 0000000..5daadd4
--- /dev/null
+++ b/test/Autofac.Extras.DynamicProxy.Test/ClassInterceptorsWithOptionalParametersFixture.cs
@@ -0,0 +1,316 @@
+// Copyright (c) Autofac Project. All rights reserved.
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using Autofac.Core;
+using Castle.DynamicProxy;
+
+namespace Autofac.Extras.DynamicProxy.Test;
+
+public class ClassInterceptorsWithOptionalParametersFixture
+{
+ [Fact]
+ public void OptionalReferenceParameterUsesDefaultWhenNotRegistered()
+ {
+ var container = BuildContainer();
+ 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());
+ }
+
+ [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 DefaultComesFromTheRightConstructorWhenOverloadsTakeTheSameCount()
+ {
+ var builder = new ContainerBuilder();
+ builder.RegisterType()
+ .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()
+ {
+ // 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
+ {
+ 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 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 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)
+ {
+ Name = name;
+ Count = count;
+ }
+
+ public string Name
+ {
+ get;
+ }
+
+ public int Count
+ {
+ get;
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+}