diff --git a/CHANGELOG.md b/CHANGELOG.md index 2957bd9f83..10048ba5e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Enhancements: - DynamicProxy supported C# `in` parameter modifiers only on the .NET Framework up until now. Adding .NET Standard 1.5 as an additional target to the NuGet package makes them work on .NET Core, too (@stakx, #339) - Replicate custom attributes on constructor parameters in the generated proxy type constructors to fulfill introspection of constructors. This does not change the proxying behavior. (@stakx, #341) - Improve performance of InvocationHelper cache lookups (@tangdf, #358) +- Improve fidelity of default value replication of optional parameters to fulfill inspection of the generated proxies. This does not change the proxying behavior. (@stakx, #356) Bugfixes: - Fix Castle.Services.Logging.Log4netIntegration assembly file name casing which breaks on Linux (@beginor, #324) diff --git a/docs/dynamicproxy-optional-parameter-value-limitations.md b/docs/dynamicproxy-optional-parameter-value-limitations.md new file mode 100644 index 0000000000..e8d105d55b --- /dev/null +++ b/docs/dynamicproxy-optional-parameter-value-limitations.md @@ -0,0 +1,43 @@ +# Optional parameter value limitations + +DynamicProxy uses `System.Reflection` and `System.Reflection.Emit` to create proxy types. Due to several bugs and limitations in these facilities (both on .NET and Mono), it is not possible in every single case to faithfully reproduce default values of optional parameters in the dynamically generated proxy types. + +This usually doesn't matter much in practice, but it may become a problem for frameworks or libraries (such as Dependency Injection containers) that reflect over the generated proxy types. + + +## When your code runs on Mono + +On Mono (up to and including at least version 5.10.1.47), DynamicProxy may not be able to correctly reproduce default parameter values in the proxy type for... + +* **Optional parameters of any nullable type `Nullable`.** Reflection will likely report (via `ParameterInfo.[Raw]DefaultValue`) a default value of `null` for such parameters, regardless of whether the actual default value in the proxied method was `null` or not. + + Therefore, you cannot trust the reported default value, *unless* you know you're running on a version of Mono where the underlying issues have been resolved, or when you've double-checked the default value in the original method of the proxied type. + + The underlying causes have been documented in [mono/mono#8504](https://github.com/mono/mono/issues/8504) and [mono/mono#8597](https://github.com/mono/mono/issues/8597). + + +## When your code runs on the .NET Framework or on .NET Core + +The .NET Framework (up to and including at least version 4.7.1) and .NET Core (up to and including at least version 2.1) are affected by several bugs or limitations regarding default parameter values. DynamicProxy may not be able to correctly reproduce default parameter values in the proxy type for... + +* **Optional parameters of any nullable type `Nullable`.** On the .NET Framework 3.5 only, reflection will likely report (via `ParameterInfo.[Raw]DefaultValue`) a default value of `Missing.Value` for such parameters. + + There is no easy way to quickly guess what the correct default value might be. Consider upgrading to the .NET Framework 4 or later, or double-check the default value in the original method of the proxied type. + +* **Optional parameters of some `struct` type `SomeStruct` having a default value of `default(SomeStruct)`.** If reflection reports (via `ParameterInfo.[Raw]DefaultValue`) a default value of `Missing.Value` for such parameters, you may safely assume that the *correct* default value is `default(SomeStruct)`. + + Note that if reflection reports a default value of `null` in such cases, this is not an error, but normal `System.Reflection` behavior that is to be expected. In this case, you may also safely assume `default(SomeStruct)` to be the correct default value. + + For .NET Core, the underlying cause of this problem has been documented in [dotnet/corefx#26164](https://github.com/dotnet/corefx/issues/26164). + +* **Optional parameters of some nullable `enum` type `SomeEnum?` having a non-`null` default value.** If reflection reports (via `ParameterInfo.[Raw]DefaultValue`) a default value of `Missing.Value` for such parameters, the only thing you may safely assume is that the actual default value is not `null`. + + You can only find out the correct default value by double-checking the default value in the original method of the proxied type. + + For .NET Core, the underlying cause of this problem has been documented in [dotnet/coreclr#17893](https://github.com/dotnet/coreclr/issues/17893). + +* **Optional parameters of a generic parameter type instantiated as some `enum` type `SomeEnum`.** For example, given a generic type `C` and a method `void M(T arg = default(T))`, if you proxy the closed generic type `C`, reflection might then report (via `ParameterInfo.[Raw]DefaultValue`) a default value of `Missing.Value` for the proxied `arg` parameter. If so, you may safely assume that the actual default value is `default(SomeEnum)`. + + Note that if reflection reports a default value of `null` in such cases, this is not an error, but normal `System.Reflection` behavior that is to be expected. In this case, you may also safely assume `default(SomeEnum)` to be the correct default value. + + For .NET Core, the underlying cause of this problem has been documented in [dotnet/coreclr#29570](https://github.com/dotnet/corefx/issues/29570). diff --git a/docs/dynamicproxy.md b/docs/dynamicproxy.md index cb22ae0a83..b46b8d1ec1 100644 --- a/docs/dynamicproxy.md +++ b/docs/dynamicproxy.md @@ -18,6 +18,7 @@ If you're new to DynamicProxy you can read a [quick introduction](dynamicproxy-i * [Use proxy generation hooks and interceptor selectors for fine grained control](dynamicproxy-fine-grained-control.md) * [SRP applies to interceptors](dynamicproxy-srp-applies-to-interceptors.md) * [Behavior of by-reference parameters during interception](dynamicproxy-by-ref-parameters.md) +* [Optional parameter value limitations](dynamicproxy-optional-parameter-value-limitations.md) :information_source: **Where is `Castle.DynamicProxy.dll`?:** DynamicProxy used to live in its own assembly. As part of changes in version 2.5 it was merged into `Castle.Core.dll` and that's where you'll find it. diff --git a/src/Castle.Core.Tests/Core.Tests/Logging/TraceLoggerTests.cs b/src/Castle.Core.Tests/Core.Tests/Logging/TraceLoggerTests.cs index 8ecb045d7a..70e2af3ef1 100644 --- a/src/Castle.Core.Tests/Core.Tests/Logging/TraceLoggerTests.cs +++ b/src/Castle.Core.Tests/Core.Tests/Logging/TraceLoggerTests.cs @@ -41,7 +41,7 @@ public void Cleanup() #if FEATURE_SYSTEM_CONFIGURATION [Test] - [ExcludeOnMono("Mono has a bug that causes the listeners to not fully work.")] + [ExcludeOnFramework(Framework.Mono, "Mono has a bug that causes the listeners to not fully work.")] public void WritingToLoggerByType() { TraceLoggerFactory factory = new TraceLoggerFactory(); @@ -85,7 +85,7 @@ public void TracingErrorInformation() } [Test] - [ExcludeOnMono("Mono has a bug that causes the listeners to not fully work.")] + [ExcludeOnFramework(Framework.Mono, "Mono has a bug that causes the listeners to not fully work.")] public void FallUpToShorterSourceName() { TraceLoggerFactory factory = new TraceLoggerFactory(); @@ -97,7 +97,7 @@ public void FallUpToShorterSourceName() } [Test] - [ExcludeOnMono("Mono has a bug that causes the listeners to not fully work.")] + [ExcludeOnFramework(Framework.Mono, "Mono has a bug that causes the listeners to not fully work.")] public void FallUpToDefaultSource() { TraceLoggerFactory factory = new TraceLoggerFactory(); diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/BaseTestCaseTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/BaseTestCaseTestCase.cs index 42e4432960..b510db3648 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/BaseTestCaseTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/BaseTestCaseTestCase.cs @@ -86,7 +86,7 @@ private void FindVerificationErrors() #if FEATURE_ASSEMBLYBUILDER_SAVE [Test] - [ExcludeOnMono("Mono doesn't have peverify, so we can't perform verification.")] + [ExcludeOnFramework(Framework.Mono, "Mono doesn't have peverify, so we can't perform verification.")] public void TearDown_FindsVerificationErrors() { var ex = Assert.Throws(() => FindVerificationErrors()); diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ClassProxyWithDefaultValuesTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ClassProxyWithDefaultValuesTestCase.cs deleted file mode 100644 index b246935d81..0000000000 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ClassProxyWithDefaultValuesTestCase.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2004-2013 Castle Project - http://www.castleproject.org/ -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Castle.DynamicProxy.Tests -{ - using System.Linq; - - using Castle.DynamicProxy.Tests.Classes; - using Castle.DynamicProxy.Tests.Interceptors; - using Castle.DynamicProxy.Tests.Interfaces; - - using NUnit.Framework; - - [TestFixture] - public class ClassProxyWithDefaultValuesTestCase : BasePEVerifyTestCase - { -#if DOTNET45 - [Test] - [ExcludeOnMono("This test relies on ParameterInfo.HasDefaultValue, which works differently on Mono than on the CLR. See https://github.com/mono/mono/issues/8513.")] - public void MethodParameterWithDefaultValue_DefaultValueIsNotSetOnProxiedMethod() - { - var proxiedType = generator.CreateClassProxy().GetType(); - - var parameter = proxiedType.GetMethod("Method").GetParameters().Single(paramInfo => paramInfo.Name == "value"); - - Assert.False(parameter.HasDefaultValue); - } - - [Test] - [ExcludeOnMono("This test relies on ParameterInfo.HasDefaultValue, which works differently on Mono than on the CLR. See https://github.com/mono/mono/issues/8513.")] - public void MethodParameterWithDefaultValue_DefaultValueNullIsSetOnProxiedMethodAsWell() - { - var proxiedType = generator.CreateClassProxy().GetType(); - - var parameter = proxiedType.GetMethod("Method").GetParameters().Single(paramInfo => paramInfo.Name == "value"); - - Assert.False(parameter.HasDefaultValue); - } - - [Test] - public void MethodParameterWithDefaultValue_UseNullDefaultValue_class_proxy() - { - var proxy = generator.CreateClassProxy(); - var result = proxy.Method(); - - Assert.IsTrue(result); - } - - [Test] - public void MethodParameterWithDefaultValue_UseNullDefaultValue_interface_proxy() - { - var proxy = generator.CreateInterfaceProxyWithoutTarget( - new SetReturnValueInterceptor(true)); - var result = proxy.Method(); - - Assert.IsTrue(result); - } -#endif - } -} \ No newline at end of file diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ClassProxyWithMethodsWithOptionalParametersTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ClassProxyWithMethodsWithOptionalParametersTestCase.cs deleted file mode 100644 index 6ecf25654a..0000000000 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ClassProxyWithMethodsWithOptionalParametersTestCase.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2004-2015 Castle Project - http://www.castleproject.org/ -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#if !DOTNET35 -namespace Castle.DynamicProxy.Tests -{ - using Castle.DynamicProxy.Tests.Classes; - - using NUnit.Framework; - - [TestFixture] - public class ClassProxyWithMethodsWithOptionalParametersTestCase - { - [Test] - // This previously failed on Mono because Mono doesn't handle nullable default parameters in ParameterBuilder. - // It appears that in the meantime, DynamicProxy's handling of default values was made more error-tolerant, - // so this test now passes even on Mono. - public void CanCreateClassProxy() - { - var proxyGenerator = new ProxyGenerator(); - proxyGenerator.CreateClassProxy(); - } - } -} -#endif \ No newline at end of file diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ClassWithAttributesTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ClassWithAttributesTestCase.cs index b1d78fc5d9..2779aa63dc 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ClassWithAttributesTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ClassWithAttributesTestCase.cs @@ -104,7 +104,7 @@ public void EnsureProxyHasAttributesOnParameter() } [Test] - [ExcludeOnMono("Mono does not currently emit custom attributes on generic type parameters of methods. See https://github.com/mono/mono/issues/8512.")] + [ExcludeOnFramework(Framework.Mono, "Mono does not currently emit custom attributes on generic type parameters of methods. See https://github.com/mono/mono/issues/8512.")] public void EnsureProxyHasAttributesOnGenericArgument() { var proxy = generator.CreateClassProxy(); diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/Classes/ClassWithMethodWithParameterWithDefaultValue.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/Classes/ClassWithMethodWithParameterWithDefaultValue.cs deleted file mode 100644 index 51f7a2a00b..0000000000 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/Classes/ClassWithMethodWithParameterWithDefaultValue.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2004-2013 Castle Project - http://www.castleproject.org/ -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Castle.DynamicProxy.Tests.Classes -{ - public class ClassWithMethodWithParameterWithDefaultValue - { - public virtual void Method(int? value = 3) - { - - } - } -} diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/Classes/ClassWithMethodWithParameterWithNullDefaultValue.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/Classes/ClassWithMethodWithParameterWithNullDefaultValue.cs deleted file mode 100644 index 06250e90dd..0000000000 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/Classes/ClassWithMethodWithParameterWithNullDefaultValue.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2004-2013 Castle Project - http://www.castleproject.org/ -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Castle.DynamicProxy.Tests.Classes -{ - public class ClassWithMethodWithParameterWithNullDefaultValue - { - public virtual bool Method(int? value = null) - { - return value == null; - } - } -} \ No newline at end of file diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/InterfaceProxyWithMethodsWithOptionalParametersTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/InterfaceProxyWithMethodsWithOptionalParametersTestCase.cs deleted file mode 100644 index 2b1e60a7f2..0000000000 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/InterfaceProxyWithMethodsWithOptionalParametersTestCase.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2004-2015 Castle Project - http://www.castleproject.org/ -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#if !DOTNET35 -namespace Castle.DynamicProxy.Tests -{ - using Castle.DynamicProxy.Tests.Interfaces; - - using NUnit.Framework; - - [TestFixture] - public class InterfaceProxyWithMethodsWithOptionalParametersTestCase - { - [Test] - // This previously failed on Mono because Mono doesn't handle nullable default parameters in ParameterBuilder. - // It appears that in the meantime, DynamicProxy's handling of default values was made more error-tolerant, - // so this test now passes even on Mono. - public void CanCreateInterfaceProxy() - { - var proxyGenerator = new ProxyGenerator(); - proxyGenerator.CreateInterfaceProxyWithoutTarget(); - } - } -} -#endif \ No newline at end of file diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/Interfaces/InterfaceWithMethodWithParameterWithNullDefaultValue.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/Interfaces/InterfaceWithMethodWithParameterWithNullDefaultValue.cs deleted file mode 100644 index dfe3f00f39..0000000000 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/Interfaces/InterfaceWithMethodWithParameterWithNullDefaultValue.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2004-2013 Castle Project - http://www.castleproject.org/ -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Castle.DynamicProxy.Tests.Interfaces -{ - public interface InterfaceWithMethodWithParameterWithNullDefaultValue - { - bool Method(int? value = null); - } -} \ No newline at end of file diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ModuleScopeTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ModuleScopeTestCase.cs index 60e2e4f76b..204cb0d7be 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ModuleScopeTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ModuleScopeTestCase.cs @@ -71,7 +71,7 @@ public void ModuleScopeCanHandleSignedAndUnsignedInParallel() #if FEATURE_ASSEMBLYBUILDER_SAVE [Test] - [ExcludeOnMono("On Mono, `ModuleBuilder.FullyQualifiedName` does not return a fully qualified name including a path. See https://github.com/mono/mono/issues/8503.")] + [ExcludeOnFramework(Framework.Mono, "On Mono, `ModuleBuilder.FullyQualifiedName` does not return a fully qualified name including a path. See https://github.com/mono/mono/issues/8503.")] public void ImplicitModulePaths() { var scope = new ModuleScope(true); @@ -87,7 +87,7 @@ public void ImplicitModulePaths() } [Test] - [ExcludeOnMono("On Mono, `ModuleBuilder.FullyQualifiedName` does not return a fully qualified name including a path. See https://github.com/mono/mono/issues/8503.")] + [ExcludeOnFramework(Framework.Mono, "On Mono, `ModuleBuilder.FullyQualifiedName` does not return a fully qualified name including a path. See https://github.com/mono/mono/issues/8503.")] public void ExplicitModulePaths() { var scope = new ModuleScope(true, false, "Strong", "StrongModule.dll", "Weak", "WeakModule.dll"); @@ -130,7 +130,7 @@ private static void CheckSignedSavedAssembly(string path) } [Test] - [ExcludeOnMono("On Mono, `ModuleBuilder.FullyQualifiedName` does not return a fully qualified name including a path. See https://github.com/mono/mono/issues/8503.")] + [ExcludeOnFramework(Framework.Mono, "On Mono, `ModuleBuilder.FullyQualifiedName` does not return a fully qualified name including a path. See https://github.com/mono/mono/issues/8503.")] public void SaveSigned() { var scope = new ModuleScope(true); @@ -152,7 +152,7 @@ public void SaveSigned() #if FEATURE_ASSEMBLYBUILDER_SAVE [Test] - [ExcludeOnMono("On Mono, `ModuleBuilder.FullyQualifiedName` does not return a fully qualified name including a path. See https://github.com/mono/mono/issues/8503.")] + [ExcludeOnFramework(Framework.Mono, "On Mono, `ModuleBuilder.FullyQualifiedName` does not return a fully qualified name including a path. See https://github.com/mono/mono/issues/8503.")] public void SaveUnsigned() { var scope = new ModuleScope(true); diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ParameterDefaultValuesTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ParameterDefaultValuesTestCase.cs new file mode 100644 index 0000000000..af45b316d5 --- /dev/null +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ParameterDefaultValuesTestCase.cs @@ -0,0 +1,407 @@ +// Copyright 2004-2018 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.f +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Castle.DynamicProxy.Tests +{ + using System; + using System.Reflection; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + + using Castle.DynamicProxy.Tests.Classes; + using Castle.DynamicProxy.Tests.Interceptors; + using Castle.DynamicProxy.Tests.Interfaces; + + using NUnit.Framework; + + [TestFixture] + public class ParameterDefaultValuesTestCase : BasePEVerifyTestCase + { + [TestCase(typeof(ClassWithMethodsWithAllKindsOfOptionalParameters))] + [TestCase(typeof(HasDefaultValues))] + public void Proxying_class_with_all_kinds_of_default_parameter_values_succeeds(Type classType) + { + generator.CreateClassProxy(classType, new DoNothingInterceptor()); + } + + [TestCase(typeof(InterfaceWithMethodsWithAllKindsOfOptionalParameters))] + public void Proxying_interface_with_all_kinds_of_default_parameter_values_succeeds(Type interfaceType) + { + generator.CreateInterfaceProxyWithoutTarget(interfaceType, new DoNothingInterceptor()); + } + + [Test] + public void Fully_supported_No_default() + { + var originalParameter = GetOriginalParameter(typeof(HasNoDefaultValues), nameof(HasNoDefaultValues.No_default)); + Assert.True(originalParameter.IsOptional); + Assert.False(originalParameter.HasDefaultValue); + Assert.IsAssignableFrom(originalParameter.DefaultValue); + + var proxiedParameter = GetProxiedParameter(typeof(HasNoDefaultValues), nameof(HasNoDefaultValues.No_default)); + Assert.True(proxiedParameter.IsOptional); + Assert.False(proxiedParameter.HasDefaultValue); + Assert.IsAssignableFrom(proxiedParameter.DefaultValue); + } + + [Test] + public void Fully_supported_Not_optional() + { + // On .NET Core 1.1, we have two different types called `DBNull`: one from the NuGet package + // System.Data.Common, and another (shadowed / non-exposed) from mscorlib. We need the latter. + // The `Type.GetType` detour can be dropped after upgrading the test project to .NET Core 2.0. + var coreLibDBNullType = Type.GetType("System.DBNull"); + + var originalParameter = GetOriginalParameter(typeof(HasNoDefaultValues), nameof(HasNoDefaultValues.Not_optional)); + Assert.False(originalParameter.IsOptional); + Assert.False(originalParameter.HasDefaultValue); + Assert.IsAssignableFrom(coreLibDBNullType, originalParameter.DefaultValue); + + var proxiedParameter = GetProxiedParameter(typeof(HasNoDefaultValues), nameof(HasNoDefaultValues.Not_optional)); + Assert.False(proxiedParameter.IsOptional); + Assert.False(proxiedParameter.HasDefaultValue); + Assert.IsAssignableFrom(coreLibDBNullType, proxiedParameter.DefaultValue); + } + + [TestCase(nameof(HasDefaultValues.Bool_default))] + [TestCase(nameof(HasDefaultValues.Bool_non_default))] + [TestCase(nameof(HasDefaultValues.Bool_nullable_null))] + [TestCase(nameof(HasDefaultValues.Byte_default))] + [TestCase(nameof(HasDefaultValues.Byte_non_default))] + [TestCase(nameof(HasDefaultValues.Byte_nullable_null))] + [TestCase(nameof(HasDefaultValues.Char_default))] + [TestCase(nameof(HasDefaultValues.Char_non_default))] + [TestCase(nameof(HasDefaultValues.Char_nullable_null))] + [TestCase(nameof(HasDefaultValues.DateTime_default_from_attribute))] + [TestCase(nameof(HasDefaultValues.DateTime_non_default_from_attribute))] + [TestCase(nameof(HasDefaultValues.DateTime_nullable_null))] + [TestCase(nameof(HasDefaultValues.Decimal_default))] + [TestCase(nameof(HasDefaultValues.Decimal_default_from_attribute))] + [TestCase(nameof(HasDefaultValues.Decimal_non_default))] + [TestCase(nameof(HasDefaultValues.Decimal_non_default_from_attribute))] + [TestCase(nameof(HasDefaultValues.Decimal_nullable_null))] + [TestCase(nameof(HasDefaultValues.Double_default))] + [TestCase(nameof(HasDefaultValues.Double_non_default))] + [TestCase(nameof(HasDefaultValues.Double_nullable_null))] + [TestCase(nameof(HasDefaultValues.Float_default))] + [TestCase(nameof(HasDefaultValues.Float_non_default))] + [TestCase(nameof(HasDefaultValues.Float_nullable_null))] + [TestCase(nameof(HasDefaultValues.Int_default))] + [TestCase(nameof(HasDefaultValues.Int_non_default))] + [TestCase(nameof(HasDefaultValues.Int_nullable_null))] + [TestCase(nameof(HasDefaultValues.Long_default))] + [TestCase(nameof(HasDefaultValues.Long_non_default))] + [TestCase(nameof(HasDefaultValues.Long_nullable_null))] + [TestCase(nameof(HasDefaultValues.Object_default))] + [TestCase(nameof(HasDefaultValues.Object_null))] + [TestCase(nameof(HasDefaultValues.SByte_default))] + [TestCase(nameof(HasDefaultValues.SByte_non_default))] + [TestCase(nameof(HasDefaultValues.SByte_nullable_null))] + [TestCase(nameof(HasDefaultValues.Short_default))] + [TestCase(nameof(HasDefaultValues.Short_non_default))] + [TestCase(nameof(HasDefaultValues.Short_nullable_null))] + [TestCase(nameof(HasDefaultValues.String_default))] + [TestCase(nameof(HasDefaultValues.String_non_default))] + [TestCase(nameof(HasDefaultValues.String_null))] + [TestCase(nameof(HasDefaultValues.UInt_default))] + [TestCase(nameof(HasDefaultValues.UInt_non_default))] + [TestCase(nameof(HasDefaultValues.UInt_nullable_null))] + [TestCase(nameof(HasDefaultValues.ULong_default))] + [TestCase(nameof(HasDefaultValues.ULong_non_default))] + [TestCase(nameof(HasDefaultValues.ULong_nullable_null))] + [TestCase(nameof(HasDefaultValues.UserDefinedClass_default))] + [TestCase(nameof(HasDefaultValues.UserDefinedClass_null))] + [TestCase(nameof(HasDefaultValues.UserDefinedEnum_default))] + [TestCase(nameof(HasDefaultValues.UserDefinedEnum_non_default))] + [TestCase(nameof(HasDefaultValues.UserDefinedEnum_nullable_null))] + [TestCase(nameof(HasDefaultValues.UserDefinedStruct_nullable_null))] + [TestCase(nameof(HasDefaultValues.UShort_default))] + [TestCase(nameof(HasDefaultValues.UShort_non_default))] + [TestCase(nameof(HasDefaultValues.UShort_nullable_null))] + public void Fully_supported(string methodName) + { + AssertParameter(typeof(HasDefaultValues), methodName); + } + + [TestCase(typeof(bool?))] + [TestCase(typeof(decimal?))] + [TestCase(typeof(double?))] + [TestCase(typeof(float?))] + [TestCase(typeof(DateTime?))] + [TestCase(typeof(DateTime?))] + [TestCase(typeof(int?))] + [TestCase(typeof(object))] + [TestCase(typeof(string))] + [TestCase(typeof(UserDefinedClass))] + [TestCase(typeof(UserDefinedEnum?))] + [TestCase(typeof(UserDefinedStruct?))] + public void Fully_supported_Generics(Type parameterType) + { + AssertParameter(typeof(HasDefaultValue<>).MakeGenericType(parameterType), nameof(HasDefaultValue.Method)); + } + + [ExcludeOnFramework(Framework.NetCore | Framework.NetFramework, "ParameterBuilder.SetConstant does not accept a null default value for value type parameters. See https://github.com/dotnet/corefx/issues/26164.")] + [TestCase(typeof(bool))] + [TestCase(typeof(decimal))] + [TestCase(typeof(double))] + [TestCase(typeof(float))] + [TestCase(typeof(DateTime))] + [TestCase(typeof(int))] + [TestCase(typeof(UserDefinedEnum))] + [TestCase(typeof(UserDefinedStruct))] + public void Not_supported_on_the_CLR_Generics(Type parameterType) + { + AssertParameter(typeof(HasDefaultValue<>).MakeGenericType(parameterType), nameof(HasDefaultValue.Method)); + } + + [ExcludeOnFramework(Framework.NetCore | Framework.NetFramework, "ParameterBuilder.SetConstant does not accept a null default value for value type parameters. See https://github.com/dotnet/corefx/issues/26164.")] + [TestCase(nameof(HasDefaultValues.DateTime_default))] + [TestCase(nameof(HasDefaultValues.UserDefinedStruct_default))] + public void Not_supported_on_the_CLR_Struct_default(string methodName) + { + AssertParameter(typeof(HasDefaultValues), methodName); + } + + [ExcludeOnFramework(Framework.Mono, "ParameterBuilder.SetConstant does not accept non-null default values for nullable parameters. See https://github.com/mono/mono/issues/8597.")] + [TestCase(nameof(HasDefaultValues.Bool_nullable_default))] + [TestCase(nameof(HasDefaultValues.Bool_nullable_non_default))] + [TestCase(nameof(HasDefaultValues.Byte_nullable_default))] + [TestCase(nameof(HasDefaultValues.Byte_nullable_non_default))] + [TestCase(nameof(HasDefaultValues.Char_nullable_default))] + [TestCase(nameof(HasDefaultValues.Char_nullable_non_default))] + [TestCase(nameof(HasDefaultValues.DateTime_nullable_default_from_attribute))] + [TestCase(nameof(HasDefaultValues.DateTime_nullable_non_default_from_attribute))] + [TestCase(nameof(HasDefaultValues.Decimal_nullable_default))] + [TestCase(nameof(HasDefaultValues.Decimal_nullable_default_from_attribute))] + [TestCase(nameof(HasDefaultValues.Decimal_nullable_non_default))] + [TestCase(nameof(HasDefaultValues.Decimal_nullable_non_default_from_attribute))] + [TestCase(nameof(HasDefaultValues.Double_nullable_default))] + [TestCase(nameof(HasDefaultValues.Double_nullable_non_default))] + [TestCase(nameof(HasDefaultValues.Float_nullable_default))] + [TestCase(nameof(HasDefaultValues.Float_nullable_non_default))] + [TestCase(nameof(HasDefaultValues.Int_nullable_default))] + [TestCase(nameof(HasDefaultValues.Int_nullable_non_default))] + [TestCase(nameof(HasDefaultValues.Long_nullable_default))] + [TestCase(nameof(HasDefaultValues.Long_nullable_non_default))] + [TestCase(nameof(HasDefaultValues.SByte_nullable_default))] + [TestCase(nameof(HasDefaultValues.SByte_nullable_non_default))] + [TestCase(nameof(HasDefaultValues.Short_nullable_default))] + [TestCase(nameof(HasDefaultValues.Short_nullable_non_default))] + [TestCase(nameof(HasDefaultValues.UInt_nullable_default))] + [TestCase(nameof(HasDefaultValues.UInt_nullable_non_default))] + [TestCase(nameof(HasDefaultValues.ULong_nullable_default))] + [TestCase(nameof(HasDefaultValues.ULong_nullable_non_default))] + [TestCase(nameof(HasDefaultValues.UShort_nullable_default))] + [TestCase(nameof(HasDefaultValues.UShort_nullable_non_default))] + public void Not_supported_on_Mono_Nullable_non_null(string methodName) + { + AssertParameter(typeof(HasDefaultValues), methodName); + } + + [ExcludeOnFramework(Framework.NetCore | Framework.NetFramework, "ParameterBuilder.SetConstant does not accept non-null default values for nullable enum parameters. See https://github.com/dotnet/coreclr/issues/17893.")] + [ExcludeOnFramework(Framework.Mono, "ParameterBuilder.SetConstant does not accept non-null default values for nullable parameters. See https://github.com/mono/mono/issues/8597.")] + [TestCase(nameof(HasDefaultValues.UserDefinedEnum_nullable_default))] + [TestCase(nameof(HasDefaultValues.UserDefinedEnum_nullable_non_default))] + public void Not_supported_UserDefinedEnum_nullable_non_null(string methodName) + { + AssertParameter(typeof(HasDefaultValues), methodName); + } + + private void AssertParameter(Type type, string methodName) + { + var method = type.GetMethod(methodName); + var expectedDefaultValue = method.Invoke(Activator.CreateInstance(type), new object[1]); + + var proxiedParameter = GetProxiedParameter(type, methodName); +#if DOTNET45 + Assert.True(proxiedParameter.HasDefaultValue); +#endif + Assert.AreEqual(expectedDefaultValue, proxiedParameter.DefaultValue); + } + + private static ParameterInfo GetOriginalParameter(Type type, string methodName = "Method") + { + return type.GetMethod(methodName).GetParameters()[0]; + } + + private ParameterInfo GetProxiedParameter(Type type, string methodName = "Method") + { + var proxy = generator.CreateClassProxy(type, new DoNothingInterceptor()); + return proxy.GetType().GetMethod(methodName).GetParameters()[0]; + } + + public class HasDefaultValues + { + // This class has methods with optional parameters having all kinds of default values. + // + // Method name nomenclature used: `{parameter type}[_nullable]_{kind of default value}` + // + // The methods must return the default value that reflection is expected to report for the + // parameter. Typically this matches the declared default value, but there are exceptions! + // + // This method list is intended to be exhaustive, i.e. it should cover *all* possible + // kinds of default values. If you find a case that is missing, please add it. + // + // (Some lines are commented out. These are not valid C#. We've left them here to show + // that the cases have at least been considered.) + + public virtual object Bool_default(bool arg = default) => default(bool); + public virtual object Bool_non_default(bool arg = true) => true; + public virtual object Bool_nullable_null(bool? arg = null) => null; + public virtual object Bool_nullable_default(bool? arg = default(bool)) => default(bool); + public virtual object Bool_nullable_non_default(bool? arg = true) => true; + + public virtual object Byte_default(byte arg = default) => default(byte); + public virtual object Byte_non_default(byte arg = (byte)1) => (byte)1; + public virtual object Byte_nullable_null(byte? arg = null) => null; + public virtual object Byte_nullable_default(byte? arg = default(byte)) => default(byte); + public virtual object Byte_nullable_non_default(byte? arg = (byte)1) => (byte)1; + + public virtual object Char_default(char arg = default) => default(char); + public virtual object Char_non_default(char arg = '1') => '1'; + public virtual object Char_nullable_null(char? arg = null) => null; + public virtual object Char_nullable_default(char? arg = default(char)) => default(char); + public virtual object Char_nullable_non_default(char? arg = '1') => '1'; + + public virtual object DateTime_default(DateTime arg = default) => null; + public virtual object DateTime_default_from_attribute([Optional, DateTimeConstant(0L)] DateTime arg) => new DateTime(0L); + public virtual object DateTime_non_default_from_attribute([Optional, DateTimeConstant(1L)] DateTime arg) => new DateTime(1L); + public virtual object DateTime_nullable_null(DateTime? arg = null) => null; + //public virtual object DateTime_nullable_default(DateTime? arg = default(DateTime)) => default(DateTime); + public virtual object DateTime_nullable_default_from_attribute([Optional, DateTimeConstant(0L)] DateTime? arg) => new DateTime(0L); + public virtual object DateTime_nullable_non_default_from_attribute([Optional, DateTimeConstant(1L)] DateTime? arg) => new DateTime(1L); + + public virtual object Decimal_default(decimal arg = default) => default(decimal); + public virtual object Decimal_default_from_attribute([Optional, DecimalConstant(0, 0, 0, 0, 0)] decimal arg) => new decimal(0, 0, 0, false, 0); + public virtual object Decimal_non_default(decimal arg = 1.00m) => 1.00m; + public virtual object Decimal_non_default_from_attribute([Optional, DecimalConstant(0, 0, 0, 0, 1)] decimal arg) => new decimal(1, 0, 0, false, 0); + public virtual object Decimal_nullable_null(decimal? arg = null) => null; + public virtual object Decimal_nullable_default(decimal? arg = default(decimal)) => default(decimal); + public virtual object Decimal_nullable_default_from_attribute([Optional, DecimalConstant(0, 0, 0, 0, 0)] decimal? arg) => new decimal(0, 0, 0, false, 0); + public virtual object Decimal_nullable_non_default(decimal? arg = 1.00m) => 1.00m; + public virtual object Decimal_nullable_non_default_from_attribute([Optional, DecimalConstant(0, 0, 0, 0, 1)] decimal? arg) => new decimal(1, 0, 0, false, 0); + + public virtual object Double_default(double arg = default) => default(double); + public virtual object Double_non_default(double arg = 1.0) => 1.0; + public virtual object Double_nullable_null(double? arg = null) => null; + public virtual object Double_nullable_default(double? arg = default(double)) => default(double); + public virtual object Double_nullable_non_default(double? arg = 1.0) => 1.0; + + public virtual object Float_default(float arg = default) => default(float); + public virtual object Float_non_default(float arg = 1.0f) => 1.0f; + public virtual object Float_nullable_null(float? arg = null) => null; + public virtual object Float_nullable_default(float? arg = default(float)) => default(float); + public virtual object Float_nullable_non_default(float? arg = 1.0f) => 1.0f; + + public virtual object Int_default(int arg = default) => default(int); + public virtual object Int_non_default(int arg = 1) => 1; + public virtual object Int_nullable_null(int? arg = null) => null; + public virtual object Int_nullable_default(int? arg = default(int)) => default(int); + public virtual object Int_nullable_non_default(int? arg = 1) => 1; + + public virtual object Long_default(long arg = default) => default(long); + public virtual object Long_non_default(long arg = 1L) => 1L; + public virtual object Long_nullable_null(long? arg = null) => null; + public virtual object Long_nullable_default(long? arg = default(long)) => default(long); + public virtual object Long_nullable_non_default(long? arg = 1L) => 1L; + + public virtual object Object_null(object arg = null) => null; + public virtual object Object_default(object arg = default) => default(object); + //public virtual object Object_non_default(object arg = new object()) => new object(); + + public virtual object SByte_default(sbyte arg = default) => default(sbyte); + public virtual object SByte_non_default(sbyte arg = (sbyte)1) => (sbyte)1; + public virtual object SByte_nullable_null(sbyte? arg = null) => null; + public virtual object SByte_nullable_default(sbyte? arg = default(sbyte)) => default(sbyte); + public virtual object SByte_nullable_non_default(sbyte? arg = (sbyte)1) => (sbyte)1; + + public virtual object Short_default(short arg = default) => default(short); + public virtual object Short_non_default(short arg = (short)1) => (short)1; + public virtual object Short_nullable_null(short? arg = null) => null; + public virtual object Short_nullable_default(short? arg = default(short)) => default(short); + public virtual object Short_nullable_non_default(short? arg = (short)1) => (short)1; + + public virtual object String_null(string arg = null) => null; + public virtual object String_default(string arg = default) => default(string); + public virtual object String_non_default(string arg = "1") => "1"; + + public virtual object UInt_default(uint arg = default) => default(uint); + public virtual object UInt_non_default(uint arg = 1u) => 1u; + public virtual object UInt_nullable_null(uint? arg = null) => null; + public virtual object UInt_nullable_default(uint? arg = default(uint)) => default(uint); + public virtual object UInt_nullable_non_default(uint? arg = 1u) => 1u; + + public virtual object ULong_default(ulong arg = default) => default(ulong); + public virtual object ULong_non_default(ulong arg = 1ul) => 1ul; + public virtual object ULong_nullable_null(ulong? arg = null) => null; + public virtual object ULong_nullable_default(ulong? arg = default(ulong)) => default(ulong); + public virtual object ULong_nullable_non_default(ulong? arg = 1ul) => 1ul; + + public virtual object UserDefinedClass_null(UserDefinedClass arg = null) => null; + public virtual object UserDefinedClass_default(UserDefinedClass arg = default) => default(UserDefinedClass); + //public virtual object UserDefinedClass_non_default(UserDefinedClass arg = new UserDefinedClass(...)) => new UserDefinedClass(...); + + public virtual object UserDefinedEnum_default(UserDefinedEnum arg = default) => default(UserDefinedEnum); + public virtual object UserDefinedEnum_non_default(UserDefinedEnum arg = (UserDefinedEnum)1) => (UserDefinedEnum)1; + public virtual object UserDefinedEnum_nullable_null(UserDefinedEnum? arg = null) => null; + public virtual object UserDefinedEnum_nullable_default(UserDefinedEnum? arg = default(UserDefinedEnum)) => (int)default(UserDefinedEnum); + public virtual object UserDefinedEnum_nullable_non_default(UserDefinedEnum? arg = (UserDefinedEnum)1) => 1; + + public virtual object UserDefinedStruct_default(UserDefinedStruct arg = default) => null; + //public virtual object UserDefinedStruct_non_default(UserDefinedStruct arg = new UserDefinedStruct(...)) => new UserDefinedStruct(...); + public virtual object UserDefinedStruct_nullable_null(UserDefinedStruct? arg = null) => null; + //public virtual object UserDefinedStruct_nullable_default(UserDefinedStruct? arg = default(UserDefinedStruct)) => default(UserDefinedStruct); + //public virtual object UserDefinedStruct_nullable_non_default(UserDefinedStruct? arg = new UserDefinedStruct(...)) => new UserDefinedStruct(...); + + public virtual object UShort_default(ushort arg = default) => default(ushort); + public virtual object UShort_non_default(ushort arg = (ushort)1) => (ushort)1; + public virtual object UShort_nullable_null(ushort? arg = null) => null; + public virtual object UShort_nullable_default(ushort? arg = default(ushort)) => default(ushort); + public virtual object UShort_nullable_non_default(ushort? arg = (ushort)1) => (ushort)1; + } + + public class HasDefaultValue + { + // This class has methods with optional parameters of a generic type + // whose default value is the default value of the generic type parameter. + // Note that in these cases, reflection is always expected to report a default + // value of `null`! + + public virtual object Method(TParameter arg = default) => null; + } + + public class HasNoDefaultValues + { + // This class has methods with parameters having no default values. + // This is here mostly to demonstrate what the special values `DBNull.Value` and + // `Missing.Value` mean that reflection sometimes reports. + + public virtual object Not_optional(object arg) => DBNull.Value; + + public virtual object No_default([Optional] object arg) => Missing.Value; + } + } + + public class UserDefinedClass + { + } + + public enum UserDefinedEnum + { + } + + public struct UserDefinedStruct + { + } +} diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/PersistentProxyBuilderTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/PersistentProxyBuilderTestCase.cs index 778ca0df24..7542629ba2 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/PersistentProxyBuilderTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/PersistentProxyBuilderTestCase.cs @@ -32,7 +32,7 @@ public void PersistentProxyBuilder_NullIfNothingSaved() } [Test] - [ExcludeOnMono("On Mono, `ModuleBuilder.FullyQualifiedName` does not return a fully qualified name including a path. See https://github.com/mono/mono/issues/8503.")] + [ExcludeOnFramework(Framework.Mono, "On Mono, `ModuleBuilder.FullyQualifiedName` does not return a fully qualified name including a path. See https://github.com/mono/mono/issues/8503.")] public void PersistentProxyBuilder_SavesSignedFile() { PersistentProxyBuilder builder = new PersistentProxyBuilder(); diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ValueTypeReferenceSemanticsTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ValueTypeReferenceSemanticsTestCase.cs index b44d44f071..c74babb824 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/ValueTypeReferenceSemanticsTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ValueTypeReferenceSemanticsTestCase.cs @@ -66,7 +66,7 @@ public void Can_intercept_method_having_valuetypes_parameter_with_in_modifier() } [Test] - [Ignore("Fails on both the CLR and CoreCLR with a MissingMethodException due to a bug in System.Reflection.Emit. See https://github.com/dotnet/corefx/issues/29254.")] + [ExcludeOnFramework(Framework.NetCore | Framework.NetFramework, "Fails with a MissingMethodException due to a bug in System.Reflection.Emit. See https://github.com/dotnet/corefx/issues/29254.")] public void Can_proxy_method_in_generic_type_having_valuetyped_parameter_with_in_modifier() { var proxy = this.generator.CreateInterfaceProxyWithoutTarget>(new DoNothingInterceptor()); @@ -75,7 +75,7 @@ public void Can_proxy_method_in_generic_type_having_valuetyped_parameter_with_in } [Test] - [Ignore("Fails on both the CLR and CoreCLR with a MissingMethodException due to a bug in System.Reflection.Emit. See https://github.com/dotnet/corefx/issues/29254.")] + [ExcludeOnFramework(Framework.NetCore | Framework.NetFramework, "Fails with a MissingMethodException due to a bug in System.Reflection.Emit. See https://github.com/dotnet/corefx/issues/29254.")] public void Can_proxy_generic_method_in_nongeneric_type_having_valuetyped_parameter_with_in_modifier() { var proxy = this.generator.CreateInterfaceProxyWithoutTarget(new DoNothingInterceptor()); @@ -84,7 +84,7 @@ public void Can_proxy_generic_method_in_nongeneric_type_having_valuetyped_parame } [Test] - [Ignore("Fails on both the CLR and CoreCLR with a MissingMethodException due to a bug in System.Reflection.Emit. See https://github.com/dotnet/corefx/issues/29254.")] + [ExcludeOnFramework(Framework.NetCore | Framework.NetFramework, "Fails with a MissingMethodException due to a bug in System.Reflection.Emit. See https://github.com/dotnet/corefx/issues/29254.")] public void Can_proxy_generic_method_in_generic_type_having_valuetyped_parameter_with_in_modifier() { var proxy = this.generator.CreateInterfaceProxyWithoutTarget>(new DoNothingInterceptor()); diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/WinFormsTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/WinFormsTestCase.cs index bfac68c0bb..0481f25d27 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/WinFormsTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/WinFormsTestCase.cs @@ -24,7 +24,7 @@ namespace Castle.DynamicProxy.Tests public class WinFormsTestCase : BasePEVerifyTestCase { [Test] - [ExcludeOnMono("Disabled on Mono to remove the need to have X installed.")] + [ExcludeOnFramework(Framework.Mono, "Disabled on Mono to remove the need to have X installed.")] public void Can_proxy_windows_forms_control() { var proxy = generator.CreateClassProxy(); diff --git a/src/Castle.Core.Tests/ExcludeOnFrameworkAttribute.cs b/src/Castle.Core.Tests/ExcludeOnFrameworkAttribute.cs new file mode 100644 index 0000000000..d68a151125 --- /dev/null +++ b/src/Castle.Core.Tests/ExcludeOnFrameworkAttribute.cs @@ -0,0 +1,66 @@ +// Copyright 2004-2018 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Castle +{ + using System; + + using NUnit.Framework; + using NUnit.Framework.Interfaces; + using NUnit.Framework.Internal; + + /// + /// Excludes a test method from test runs on the specified framework(s). + /// This attribute may be placed on the same method more than once. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] + public sealed class ExcludeOnFrameworkAttribute : NUnitAttribute, IApplyToTest + { + private Framework oneOrMoreFrameworks; + private string reason; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The framework(s) on which the test should be excluded. + /// + /// + /// The reason why the test should excluded. + /// + public ExcludeOnFrameworkAttribute(Framework oneOrMoreFrameworks, string reason) + { + this.oneOrMoreFrameworks = oneOrMoreFrameworks; + this.reason = reason; + } + + public void ApplyToTest(Test test) + { + if (test.RunState == RunState.NotRunnable || test.RunState == RunState.Skipped) + { + return; + } + + foreach (Framework framework in Enum.GetValues(typeof(Framework))) + { + if ((this.oneOrMoreFrameworks & framework) != 0 && framework.IsRunning()) + { + test.RunState = RunState.Skipped; + test.Properties.Add(PropertyNames.SkipReason, $"Not supported on {framework.GetName()}. ({this.reason})"); + break; + } + } + } + } +} diff --git a/src/Castle.Core.Tests/ExcludeOnFrameworkAttributeTestCase.cs b/src/Castle.Core.Tests/ExcludeOnFrameworkAttributeTestCase.cs new file mode 100644 index 0000000000..9fdc444d21 --- /dev/null +++ b/src/Castle.Core.Tests/ExcludeOnFrameworkAttributeTestCase.cs @@ -0,0 +1,68 @@ +// Copyright 2004-2018 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Castle +{ + using System; + using System.Collections.Generic; + using System.Linq; + + using NUnit.Framework; + using NUnit.Framework.Interfaces; + using NUnit.Framework.Internal; + + [TestFixture] + public class ExcludeOnFrameworkAttributeTestCase + { + [Test] + public void ExcludeOnFramework_marks_test_as_skipped_when_specified_framework_equal_to_current_framework() + { + var couldIdentifyRunningFramework = FrameworkUtil.TryGetRunningFramework(out var runningFramework); + Assume.That(couldIdentifyRunningFramework); + + var attribute = new ExcludeOnFrameworkAttribute(runningFramework, "Blah."); + var test = new MockTest(); + + attribute.ApplyToTest(test); + + Assert.AreEqual(RunState.Skipped, test.RunState); + Assert.True(test.Properties.ContainsKey(PropertyNames.SkipReason)); + Assert.True(test.Properties[PropertyNames.SkipReason].OfType().Any(sr => sr.Contains("Blah."))); + } + + [Test] + public void ExcludeOnFramework_does_not_mark_test_as_skipped_when_specified_framework_not_equal_to_current_framework() + { + const Framework noFramework = 0; + + var attribute = new ExcludeOnFrameworkAttribute(noFramework, "Blah."); + var test = new MockTest(); + + attribute.ApplyToTest(test); + + Assert.AreNotEqual(RunState.Skipped, test.RunState); + Assert.False(test.Properties.ContainsKey(PropertyNames.SkipReason)); + } + + private sealed class MockTest : Test + { + public MockTest() : base("TestName") { } + public override string XmlElementName => throw new NotImplementedException(); + public override bool HasChildren => throw new NotImplementedException(); + public override IList Tests => throw new NotImplementedException(); + public override TNode AddToXml(TNode parentNode, bool recursive) => throw new NotImplementedException(); + public override TestResult MakeTestResult() => throw new NotImplementedException(); + } + } +} diff --git a/src/Castle.Core.Tests/ExcludeOnMonoAttribute.cs b/src/Castle.Core.Tests/ExcludeOnMonoAttribute.cs deleted file mode 100644 index 292d971e36..0000000000 --- a/src/Castle.Core.Tests/ExcludeOnMonoAttribute.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2004-2018 Castle Project - http://www.castleproject.org/ -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.f -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Castle -{ - using System; - - using NUnit.Framework; - using NUnit.Framework.Interfaces; - using NUnit.Framework.Internal; - - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - public sealed class ExcludeOnMonoAttribute : NUnitAttribute, IApplyToTest - { - private static bool IsRunningOnMono() => Type.GetType("Mono.Runtime", throwOnError: false) != null; - - private string reason; - - public ExcludeOnMonoAttribute(string reason) - { - this.reason = reason; - } - - public void ApplyToTest(Test test) - { - if (test.RunState != RunState.NotRunnable && test.RunState != RunState.Skipped && IsRunningOnMono()) - { - test.RunState = RunState.Skipped; - test.Properties.Add(PropertyNames.SkipReason, $"Not supported on Mono. ({reason})"); - } - } - } -} diff --git a/src/Castle.Core.Tests/Framework.cs b/src/Castle.Core.Tests/Framework.cs new file mode 100644 index 0000000000..d1d4e8d35d --- /dev/null +++ b/src/Castle.Core.Tests/Framework.cs @@ -0,0 +1,107 @@ +// Copyright 2004-2018 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Castle +{ + using System; + using System.Diagnostics; + using System.Runtime.InteropServices; + + /// + /// Flags enumeration used to specify a framework, or a combination of frameworks. + /// + [Flags] + public enum Framework + { + /// + /// Mono. + /// + Mono = 1, + + /// + /// .NET Core. + /// + NetCore = 2, + + /// + /// The .NET Framework. + /// + NetFramework = 4, + } + + internal static class FrameworkUtil + { + public static string GetName(this Framework framework) + { + switch (framework) + { + case Framework.Mono: + return "Mono"; + + case Framework.NetCore: + return ".NET Core"; + + case Framework.NetFramework: + return "the .NET Framework"; + + default: + throw new ArgumentOutOfRangeException(nameof(framework)); + } + } + + public static bool TryGetRunningFramework(out Framework runningFramework) + { + runningFramework = 0; + + if (Framework.Mono.IsRunning()) + { + runningFramework = Framework.Mono; + } + + if (Framework.NetCore.IsRunning()) + { + Debug.Assert(runningFramework == 0); + runningFramework = Framework.NetCore; + } + + if (Framework.NetFramework.IsRunning()) + { + Debug.Assert(runningFramework == 0); + runningFramework = Framework.NetFramework; + } + + return runningFramework != 0; + } + + public static bool IsRunning(this Framework framework) + { + var frameworkDescription = RuntimeInformation.FrameworkDescription; + + switch (framework) + { + case Framework.Mono: + return frameworkDescription.StartsWith("Mono"); + + case Framework.NetCore: + return frameworkDescription.StartsWith(".NET Core"); + + case Framework.NetFramework: + return frameworkDescription.StartsWith(".NET Framework"); + + default: + throw new ArgumentOutOfRangeException(nameof(framework)); + } + } + } +} diff --git a/src/Castle.Core/DynamicProxy/Generators/Emitters/MethodEmitter.cs b/src/Castle.Core/DynamicProxy/Generators/Emitters/MethodEmitter.cs index 82d5c1e096..9c0f03c2e1 100644 --- a/src/Castle.Core/DynamicProxy/Generators/Emitters/MethodEmitter.cs +++ b/src/Castle.Core/DynamicProxy/Generators/Emitters/MethodEmitter.cs @@ -160,6 +160,126 @@ private void DefineParameters(ParameterInfo[] parameters) { parameterBuilder.SetCustomAttribute(attribute.Builder); } + + // If a parameter has a default value, that default value needs to be replicated. + // Default values as reported by `ParameterInfo.[Raw]DefaultValue` have two possible origins: + // + // 1. A `[DecimalConstant]` or `[DateTimeConstant]` custom attribute attached to the parameter. + // Attribute-based default values have already been copied above. + // + // 2. A `Constant` metadata table entry whose parent is the parameter. + // Constant-based default values need more work. We can detect this case by checking + // whether the `ParameterAttributes.HasDefault` flag is set. (NB: This is not the same + // as querying `ParameterInfo.HasDefault`, which would also return true for case (1)!) + if ((parameter.Attributes & ParameterAttributes.HasDefault) != 0) + { + CopyDefaultValueConstant(from: parameter, to: parameterBuilder); + } + } + } + + private void CopyDefaultValueConstant(ParameterInfo from, ParameterBuilder to) + { + Debug.Assert(from != null); + Debug.Assert(to != null); + Debug.Assert((from.Attributes & ParameterAttributes.HasDefault) != 0); + + object defaultValue; + try + { + defaultValue = from.DefaultValue; + } + catch (FormatException) when (from.ParameterType == typeof(DateTime)) + { + // This catch clause guards against a CLR bug that makes it impossible to query + // the default value of an optional DateTime parameter. For the CoreCLR, see + // https://github.com/dotnet/corefx/issues/26164. + + // If this bug is present, it is caused by a `null` default value: + defaultValue = null; + } + catch (FormatException) when (from.ParameterType.GetTypeInfo().IsEnum) + { + // This catch clause guards against a CLR bug that makes it impossible to query + // the default value of a (closed generic) enum parameter. For the CoreCLR, see + // https://github.com/dotnet/corefx/issues/29570. + + // If this bug is present, it is caused by a `null` default value: + defaultValue = null; + } + + if (defaultValue is Missing) + { + // It is likely that we are reflecting over invalid metadata if we end up here. + // At this point, `to.Attributes` will have the `HasDefault` flag set. If we do + // not call `to.SetConstant`, that flag will be reset when creating the dynamic + // type, so `to` will at least end up having valid metadata. It is quite likely + // that the `Missing.Value` will still be reproduced because the `Parameter- + // Builder`'s `ParameterAttributes.Optional` is likely set. (If it isn't set, + // we'll be causing a default value of `DBNull.Value`, but there's nothing that + // can be done about that, short of recreating a new `ParameterBuilder`.) + return; + } + + try + { + to.SetConstant(defaultValue); + } + catch (ArgumentException) + { + var parameterType = from.ParameterType; + + if (defaultValue == null) + { + if (parameterType.GetTypeInfo().IsGenericType && parameterType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + // This guards against a Mono bug that prohibits setting default value `null` + // for a `Nullable` parameter. See https://github.com/mono/mono/issues/8504. + // + // If this bug is present, luckily we still get `null` as the default value if + // we do nothing more (which is probably itself yet another bug, as the CLR + // would "produce" a default value of `Missing.Value` in this situation). + return; + } + else if (parameterType.GetTypeInfo().IsValueType) + { + // This guards against a CLR bug that prohibits replicating `null` default + // values for non-nullable value types (which, despite the apparent type + // mismatch, is perfectly legal and something that the Roslyn compilers do). + // For the CoreCLR, see https://github.com/dotnet/corefx/issues/26184. + + // If this bug is present, the best we can do is to not set the default value. + // This will cause a default value of `Missing.Value` (if `ParameterAttributes- + // .Optional` is set) or `DBNull.Value` (otherwise, unlikely). + return; + } + } + else if (parameterType.GetTypeInfo().IsGenericType && parameterType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + var genericArg = from.ParameterType.GetGenericArguments()[0]; + if (genericArg.GetTypeInfo().IsEnum || genericArg.IsAssignableFrom(defaultValue.GetType())) + { + // This guards against two bugs: + // + // * On the CLR and CoreCLR, a bug that makes it impossible to use `ParameterBuilder- + // .SetConstant` on parameters of a nullable enum type. For CoreCLR, see + // https://github.com/dotnet/coreclr/issues/17893. + // + // If this bug is present, there is no way to faithfully reproduce the default + // value. This will most likely cause a default value of `Missing.Value` or + // `DBNull.Value`. (To better understand which of these, see comment above). + // + // * On Mono, a bug that performs a too-strict type check for nullable types. The + // value passed to `ParameterBuilder.SetConstant` must have a type matching that + // of the parameter precisely. See https://github.com/mono/mono/issues/8597. + // + // If this bug is present, there's no way to reproduce the default value because + // we cannot actually create a value of type `Nullable<>`. + return; + } + } + + throw; } }