Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.28.0.143324">
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.32.0.713">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public void Setup()
builder.RegisterType<ClassWithoutInterceptAttribute>()
.EnableClassInterceptors()
.InterceptedBy(typeof(StringMethodInterceptor));
builder.RegisterType<ClassWithOptionalParameter>()
.EnableClassInterceptors()
.InterceptedBy(typeof(StringMethodInterceptor));
builder.RegisterType<StringMethodInterceptor>();
_container = builder.Build();
}
Expand All @@ -38,4 +41,11 @@ public string WiredUsingInterceptedBy()
var instance = _container.Resolve<ClassWithoutInterceptAttribute>();
return instance.Test();
}

[Benchmark]
public string WithOptionalConstructorParameter()
{
var instance = _container.Resolve<ClassWithOptionalParameter>();
return instance.Test();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 1 addition & 1 deletion default.proj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project DefaultTargets="All" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="Current">
<PropertyGroup>
<!-- Increment the overall semantic version here. -->
<Version>8.0.1</Version>
<Version>8.1.0</Version>
<SolutionName>Autofac.Extras.DynamicProxy</SolutionName>
<Configuration Condition="'$(Configuration)'==''">Release</Configuration>
<ArtifactDirectory>$([System.IO.Path]::Combine($(MSBuildProjectDirectory),"artifacts"))</ArtifactDirectory>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@
<AdditionalFiles Include="../../build/stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="9.3.1" />
<PackageReference Include="Autofac" Version="9.3.2" />
<PackageReference Include="Castle.Core" Version="5.2.1" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.300" Condition="Exists('$(MSBuildThisFileDirectory)../../.git')">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.400" Condition="Exists('$(MSBuildThisFileDirectory)../../.git')">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.28.0.143324">
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.32.0.713">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
33 changes: 33 additions & 0 deletions src/Autofac.Extras.DynamicProxy/Polyfills/NotNullWhenAttribute.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Polyfill for <see cref="NotNullWhenAttribute"/> which is not available in netstandard2.0.
/// Specifies that when a method returns <see cref="ReturnValue"/>,
/// the parameter will not be null even if the corresponding type allows it.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class NotNullWhenAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="NotNullWhenAttribute"/> class.
/// </summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;

/// <summary>
/// Gets a value indicating whether the return value should be true or false for the parameter to be non-null.
/// </summary>
public bool ReturnValue
{
get;
}
}

#endif
241 changes: 241 additions & 0 deletions src/Autofac.Extras.DynamicProxy/ProxiedDefaultValueParameter.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Supplies optional constructor argument values that are lost when a class
/// proxy is generated.
/// </summary>
/// <remarks>
/// <para>
/// 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
/// <see cref="Autofac.Core.Activators.Reflection.DefaultValueParameter"/> 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.
/// </para>
/// <para>
/// The values are read once, when this parameter is created, so resolving costs
/// a dictionary lookup rather than a walk over the constructors.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
internal sealed class ProxiedDefaultValueParameter : Parameter
{
private readonly IEnumerable<Parameter> _configuredParameters;

private readonly Dictionary<ParameterInfo, Func<object?>> _defaultValues;

/// <summary>
/// Initializes a new instance of the
/// <see cref="ProxiedDefaultValueParameter"/> class.
/// </summary>
/// <param name="proxyType">
/// The generated proxy type, whose constructor parameters are the ones being
/// supplied.
/// </param>
/// <param name="proxiedType">
/// The type that was proxied; the source of the default values.
/// </param>
/// <param name="configuredParameters">
/// The parameters configured on the registration. These take precedence
/// over default values, so they're checked before one is supplied.
/// </param>
/// <param name="proxyArgumentCount">
/// 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.
/// </param>
public ProxiedDefaultValueParameter(Type proxyType, Type proxiedType, IEnumerable<Parameter> configuredParameters, int proxyArgumentCount)
{
_configuredParameters = configuredParameters;
_defaultValues = FindDefaultValues(proxyType, proxiedType, proxyArgumentCount);
}

/// <inheritdoc/>
public override bool CanSupplyValue(ParameterInfo pi, IComponentContext context, [NotNullWhen(returnValue: true)] out Func<object?>? 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;
}

/// <summary>
/// Reads the default values the generated constructors dropped, keyed by the
/// proxy constructor parameter each one belongs to.
/// </summary>
/// <param name="proxyType">The generated proxy type.</param>
/// <param name="proxiedType">The type that was proxied.</param>
/// <param name="proxyArgumentCount">
/// The number of leading arguments the generated constructors take for the
/// proxy itself.
/// </param>
/// <returns>
/// The default value providers for the parameters that have one.
/// </returns>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// </remarks>
private static Dictionary<ParameterInfo, Func<object?>> FindDefaultValues(Type proxyType, Type proxiedType, int proxyArgumentCount)
{
var defaultValues = new Dictionary<ParameterInfo, Func<object?>>();

// 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;
}

/// <summary>
/// Determines whether the parameters of a constructor on the proxied type
/// are the ones a generated constructor mirrors.
/// </summary>
/// <param name="proxied">
/// The parameters of a constructor on the proxied type.
/// </param>
/// <param name="mirrored">
/// The parameters of the generated proxy constructor.
/// </param>
/// <param name="proxyArgumentCount">
/// The number of leading arguments the generated constructor takes for the
/// proxy itself.
/// </param>
/// <returns>
/// <see langword="true" /> if the generated constructor mirrors
/// <paramref name="proxied" />; otherwise, <see langword="false" />.
/// </returns>
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;
}

/// <summary>
/// Records the default values declared on a constructor of the proxied type
/// against the parameters of the generated constructor mirroring it.
/// </summary>
/// <param name="defaultValues">The set of default values being built.</param>
/// <param name="proxied">
/// The parameters of the constructor on the proxied type.
/// </param>
/// <param name="mirrored">
/// The parameters of the generated proxy constructor.
/// </param>
/// <param name="proxyArgumentCount">
/// The number of leading arguments the generated constructor takes for the
/// proxy itself.
/// </param>
private static void AddDefaultValues(Dictionary<ParameterInfo, Func<object?>> 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);
}
}
}

/// <summary>
/// Reads the default value declared on a parameter of the proxied type.
/// </summary>
/// <param name="proxied">The parameter on the proxied type.</param>
/// <param name="defaultValue">
/// The default value, if the parameter declares one.
/// </param>
/// <returns>
/// <see langword="true" /> if the parameter declares a default value;
/// otherwise, <see langword="false" />.
/// </returns>
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;
}
}
Loading
Loading