Friday, October 19, 2007

CustomAttributesData and NamedParameter of an Array type


After submitting a piece of work to Spring.NET, which was included in the release candidate 2 of version 1.1 following issue was raised on the Spring.NET forums http://forum.springframework.net/showthread.php?p=9286:

[InvalidCastException: Unable to cast object of type 'System.Collections.ObjectModel.ReadOnlyCollection `1[System.Reflection.CustomAttributeTypedArgument]' to type 'System.Array'.]
System.Reflection.Emit.CustomAttributeBuilder.Emit Value(BinaryWriter writer, Type type, Object value) +701
System.Reflection.Emit.CustomAttributeBuilder.Init CustomAttributeBuilder(ConstructorInfo con, Object[] constructorArgs, PropertyInfo[] namedProperties, Object[] propertyValues, FieldInfo[] namedFields, Object[] fieldValues) +1448
System.Reflection.Emit.CustomAttributeBuilder..cto r(ConstructorInfo con, Object[] constructorArgs, PropertyInfo[] namedProperties, Object[] propertyValues, FieldInfo[] namedFields, Object[] fieldValues) +24

Issue happened for the custom attribute of type:

[Transaction(ReadOnly = false, NoRollbackFor = new Type[] {typeof (...)})]

The reason behind exception is how CustomAttributeData handles this "array" type of metadata when it creates attribute arguments:

internal CustomAttributeTypedArgument(Module scope, CustomAttributeEncodedArgument encodedArg)
{
    CustomAttributeEncoding encodedType = encodedArg.CustomAttributeType.EncodedType;
    switch (encodedType)
    {
// Many cases omitted, see the code with Reflector ...

//Bringing attention to this case, as this is exactly what is not resolved yet in the Mono.Cecil at the moment of this writing (Though trivial to work around for)
case CustomAttributeEncoding.Type:
            this.m_argumentType = typeof(Type);
            this.m_value = null;
            if (encodedArg.StringValue != null)
            {
                this.m_value = ResolveType(scope, encodedArg.StringValue);
                return;
            }
            break;

// And bellow is the point that explains the issue with the arrays  in the custom attributes       
case CustomAttributeEncoding.Array:
        {
            Type type;
            encodedType = encodedArg.CustomAttributeType.EncodedArrayType;
            if (encodedType == CustomAttributeEncoding.Enum)
            {

                // This case should be checked - no Spring.NET init tests for this case now
                type = ResolveType(scope, encodedArg.CustomAttributeType.EnumName);


            }
            else
            {

                // This case should be checked - no Spring.NET init tests for this case now
                type = CustomAttributeEncodingToType(encodedType);
            }
            this.m_argumentType = type.MakeArrayType();
            if (encodedArg.ArrayValue == null)
            {
                this.m_value = null;
                return;
            }
            CustomAttributeTypedArgument[] array = new CustomAttributeTypedArgument[encodedArg.ArrayValue.Length];
            for (int i = 0; i < array.Length; i++)
            {
                array[i] = new CustomAttributeTypedArgument(scope, encodedArg.ArrayValue[i]);
            }

           And this is the point that gives us troubles now, can't say that returning it as a

           ReadOnly is a bad idea, guys are right, so we will need to cope with such a case with a bit of a work around.
            this.m_value = Array.AsReadOnly<CustomAttributeTypedArgument>(array);
            return;
        }
        default:
            this.m_argumentType = CustomAttributeEncodingToType(encodedType);
            this.m_value = EncodedValueToRawValue(encodedArg.PrimitiveValue, encodedType);
            break;
    }
}

It is quite relevant to check though what is the Array.AsReadOnly<T> implementation.

public static ReadOnlyCollection<T> AsReadOnly<T>(T[] array)
{
    if (array == null)
    {
        throw new ArgumentNullException("array");
    }
    return new ReadOnlyCollection<T>(array);
}

Great! This is quite straightforward and deterministic to build a workaround around :).

Solution was to add extra Resolve method with the argument value parameter that is transforming the System.Collections.ObjectModel.ReadOnlyCollection `1[System.Reflection.CustomAttributeTypedArgument] to the array of the original type.

As we can pass the same through the constructor argument, same resolution should be applied to the constructor arguments. Subject to create tests for and apply.

No comments: