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/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.0 Autofac.Extras.DynamicProxy Release $([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 - + all runtime; 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? valueProvider) + { + valueProvider = null; + + if (!_defaultValues.TryGetValue(pi, out var defaultValueProvider)) + { + 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; + } + } + + valueProvider = defaultValueProvider; + return true; + } + + /// + /// Reads the default values the generated constructors dropped, keyed by the + /// proxy constructor parameter each one belongs to. + /// + /// The generated proxy type. + /// The type that was proxied. + /// + /// The number of leading arguments the generated constructors take for the + /// proxy itself. + /// + /// + /// The default value providers for the parameters that have one. + /// + /// + /// + /// 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 static Dictionary> FindDefaultValues(Type proxyType, Type proxiedType, int proxyArgumentCount) + { + 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. + var proxiedConstructors = proxiedType.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + foreach (var proxyConstructor in proxyType.GetConstructors()) + { + var mirrored = proxyConstructor.GetParameters(); + + foreach (var proxiedConstructor in proxiedConstructors) + { + var proxied = proxiedConstructor.GetParameters(); + + if (proxied.Length != mirrored.Length - proxyArgumentCount || + !IsMirroredBy(proxied, mirrored, proxyArgumentCount)) + { + continue; + } + + AddDefaultValues(defaultValues, proxied, mirrored, proxyArgumentCount); + break; + } + } + + 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, . + /// + private static bool IsMirroredBy(ParameterInfo[] proxied, ParameterInfo[] mirrored, int proxyArgumentCount) + { + for (var i = 0; i < proxied.Length; i++) + { + var proxyParameter = mirrored[i + proxyArgumentCount]; + + if (!string.Equals(proxied[i].Name, proxyParameter.Name, StringComparison.Ordinal) || + proxied[i].ParameterType != proxyParameter.ParameterType) + { + return false; + } + } + + 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 966f875..66ff55e 100644 --- a/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs +++ b/src/Autofac.Extras.DynamicProxy/RegistrationExtensions.cs @@ -303,23 +303,33 @@ public static IRegistrationBuilder { var proxyParameters = new List(); @@ -343,7 +353,16 @@ public static IRegistrationBuilder - + 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 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; + } + } + } +}