Skip to content

Can't create a DynamicProxy for an interface containing method with optional, nullable enum parameter using net45 library #141

@balefrost

Description

@balefrost

The problem seems to stem from how ParameterBuilder handles default values for nullable enums. The value reported by ParameterInfo.DefaultValue for such a parameter is an int, but trying to use ParmeterBuilder.SetConstant for a parameter of a nullable enum type doesn't work (and, as far as I can tell, this is a bug in ParameterBuilder.SetConstant itself).

As a larger question, why does DynamicProxy even try to set default values for parameters on the generated class? Normally, default values are inserted by the compiler at the call site - default parameters are mostly compile-time, call-site syntactic sugar. In fact, it's possible for the interface and the implementation to disagree on what the default parameter value is and even on whether a particular parameter is optional or not. The compiler will use whichever default values correspond to the method on the most-specific type that the compiler knows statically. If you're calling through the concrete implementation class, it will use the defaults defined for the concrete method. If you're calling through the interface, it will use the defaults defined for the interface method.

But when using DynamicProxy, the compiler can't see the generated class. Call sites are always going to be invoking through the interface or base class, so it shouldn't be necessary to copy the default parameter values down to the parameters on the generated class. The only place where it would even be visible would be via runtime reflection on the generated type... which, while possible, doesn't seem like something that should be relied upon (especially since the net40-client and earlier libraries doesn't behave this way).

Given that libraries compiled for net40-client and earlier don't exhibit this behavior, my workaround is to just run against one of those. But this net45-specific codepath already caused problems with optional decimals in issue #87. Maybe the call to SetConstant should be wrapped in a try/catch and exceptions should be ignored. Or maybe the whole behavior should be removed... it seems weird that the net40-client and net45 libraries behave differently.

Here's a test case:

[Test]
public void CanCreateProxyForInterfaceWithNullableEnumMember()
{
    Assert.DoesNotThrow(() =>
    {
        var proxyGenerator = new ProxyGenerator();
        proxyGenerator.CreateInterfaceProxyWithTarget<IContainsMethodWithDefaultEnumParameter>(
            new ConcreteContainsMethodWithDefaultEnumParameter());
    });
}

public class ConcreteContainsMethodWithDefaultEnumParameter : IContainsMethodWithDefaultEnumParameter
{
    public void Do(ConsoleColor? value = ConsoleColor.Cyan)
    {
    }
}

public interface IContainsMethodWithDefaultEnumParameter
{
    void Do(ConsoleColor? value = ConsoleColor.Cyan);
}

And here's the exception:

System.ArgumentException: Constant does not match the defined type.
   at System.Reflection.Emit.TypeBuilder.SetConstantValue(ModuleBuilder module, Int32 tk, Type destType, Object value)
   at Castle.DynamicProxy.Generators.Emitters.MethodEmitter.DefineParameters(ParameterInfo[] parameters)
   at Castle.DynamicProxy.Generators.Emitters.AbstractTypeEmitter.CreateMethod(String name, MethodAttributes attributes, MethodInfo methodToUseAsATemplate)
   at Castle.DynamicProxy.Generators.MethodGenerator.Generate(ClassEmitter class, ProxyGenerationOptions options, INamingScope namingScope)
   at Castle.DynamicProxy.Contributors.CompositeTypeContributor.ImplementMethod(MetaMethod method, ClassEmitter class, ProxyGenerationOptions options, OverrideMethodDelegate overrideMethod)
   at Castle.DynamicProxy.Contributors.CompositeTypeContributor.Generate(ClassEmitter class, ProxyGenerationOptions options)
   at Castle.DynamicProxy.Generators.InterfaceProxyWithTargetGenerator.GenerateType(String typeName, Type proxyTargetType, Type[] interfaces, INamingScope namingScope)
   at Castle.DynamicProxy.Generators.InterfaceProxyWithTargetGenerator.<>c__DisplayClass1.<GenerateCode>b__0(String n, INamingScope s)
   at Castle.DynamicProxy.Generators.BaseProxyGenerator.ObtainProxyType(CacheKey cacheKey, Func`3 factory)
   at Castle.DynamicProxy.ProxyGenerator.CreateInterfaceProxyWithTarget(Type interfaceToProxy, Type[] additionalInterfacesToProxy, Object target, ProxyGenerationOptions options, IInterceptor[] interceptors)
   at Castle.DynamicProxy.ProxyGenerator.CreateInterfaceProxyWithTarget[TInterface](TInterface target, IInterceptor[] interceptors)
   at ReflectionEmitLibrary.ReflectionEmitTest.<>c.<CanCreateProxyForInterfaceWithNullableEnumMember>b__0_0() in C:\src\Sandbox\ReflectionEmitLibrary\ReflectionEmitTest.cs:line 18
   at NUnit.Framework.Constraints.ThrowsConstraint.VoidInvocationDescriptor.Invoke()
   at NUnit.Framework.Constraints.ThrowsConstraint.ExceptionInterceptor.Intercept(Object invocation)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions