Bizarre behavior with custom attributes and GetCustomAttributes

2.4k Views Asked by At

I've been fighting this problem for hours and I could not find anything related on SO (or google for that matter).

Here's my problem: I have a custom attribute that contains an object array property.

[System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple = false)]
public class Property : System.Attribute
{
    public object[] Parameters { get; set; }

    public JsonProperty(object[] prms = null)
    {
        Parameters = prms;
    }
}

And then I use the following code to read it from the properties:

var customProperties = (Property[])currentProperty.GetCustomAttributes(typeof(Property), false);

This all works fine for the following:

[Property(Parameters = new object[]{}]
<...property...>

However, if I set it to null ([Property(Parameters = null]), I get this error:

System.Reflection.CustomAttributeFormatException:
'Parameters' property specified was not found.

Which is absurd, because the property is defined inside my custom attribute. I really don't get it.

So my question is: what is going on?

--Edit

If I change the type of the property from object[] to object, assigning null works just fine.

--Edit to add the code

Attribute:

[System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple = false)]
public class JsonProperty : System.Attribute
{
    public object[] Parameters { get; set; }

    public JsonProperty(object[] prms = null)
    {
        Parameters = prms;
    }
}

Class:

public class MyClass
{
    [JsonProperty(Parameters = null)]
    public DateTime Start { get; set; }
}

Method:

public string getAttributes()
{
    Type t = MyClass.GetType();

     // Get only public properties and that have been set.
     var properties = t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
                       .Where(prop => prop.GetValue(this, null) != null);

     foreach (var prop in properties)
     {
          //The error occur on the next line.
          var jsonProperties =
              (JsonProperty[])prop.GetCustomAttributes(typeof(JsonProperty), false);

--If you didn't understand, try reading this:

https://social.msdn.microsoft.com/Forums/vstudio/en-US/ddebbec6-1653-4502-9802-0b421efec60d/an-unexplicable-customattributeformatexception-from-getcustomattributes?forum=csharpgeneral

I asked the question there too.

2

There are 2 best solutions below

0
On

I had the same error with System.Runtime.Serialization.OptionalFieldAttribute. To reproduce it it's enough to add a field, mark it with attribute and later call FieldInfo.GetCustomAttributes().

[OptionalField(VersionAdded = 0)]
private int _testA = 0;
private void SomeMethod()
{
    FieldInfo fieldInfo = GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Where(p => p.Name == "_testA").FirstOrDefault();

    fieldInfo.GetCustomAttributes();
    //You will get a CustomAttributeFormatException('OptionalField' property specified was not found) for VersionAdded values less then 1.
}

The good question is why! The answer as I see we can get from code of GetCustomAttributes(), where there is a try{}catch{}, which catches all errors and throws CustomAttributeFormatException, so I think the error in my case comes from setter of OptionalFieldAttribute:

[AttributeUsage(AttributeTargets.Field, Inherited=false)]
[System.Runtime.InteropServices.ComVisible(true)]
public sealed class OptionalFieldAttribute : Attribute 
{
    int versionAdded = 1;
    public OptionalFieldAttribute() { }
    
    public int VersionAdded 
    {
        get {
            return this.versionAdded;
        }
        set {
            if (value < 1)
                throw new ArgumentException(Environment.GetResourceString("Serialization_OptionalFieldVersionValue"));
            Contract.EndContractBlock();
            this.versionAdded = value;
        }
    }
}


[System.Security.SecurityCritical]
private unsafe static object[] GetCustomAttributes(
    RuntimeModule decoratedModule, int decoratedMetadataToken, int pcaCount, 
    RuntimeType attributeFilterType, bool mustBeInheritable, IList derivedAttributes, bool isDecoratedTargetSecurityTransparent)
{
    if (decoratedModule.Assembly.ReflectionOnly)
        throw new InvalidOperationException(Environment.GetResourceString("Arg_ReflectionOnlyCA"));
    Contract.EndContractBlock();

    MetadataImport scope = decoratedModule.MetadataImport;
    CustomAttributeRecord[] car = CustomAttributeData.GetCustomAttributeRecords(decoratedModule, decoratedMetadataToken);

    bool useObjectArray = (attributeFilterType == null || attributeFilterType.IsValueType || attributeFilterType.ContainsGenericParameters);
    Type arrayType = useObjectArray ? typeof(object) : attributeFilterType;

    if (attributeFilterType == null && car.Length == 0)
        return CreateAttributeArrayHelper(arrayType, 0);

    object[] attributes = CreateAttributeArrayHelper(arrayType, car.Length);
    int cAttributes = 0;

    // Custom attribute security checks are done with respect to the assembly *decorated* with the 
    // custom attribute as opposed to the *caller of GetCustomAttributes*.
    // Since this assembly might not be on the stack and the attribute ctor or property setters we're about to invoke may
    // make security demands, we push a frame on the stack as a proxy for the decorated assembly (this frame will be picked
    // up an interpreted by the security stackwalker).
    // Once we push the frame it will be automatically popped in the event of an exception, so no need to use CERs or the
    // like.
    SecurityContextFrame frame = new SecurityContextFrame();
    frame.Push(decoratedModule.GetRuntimeAssembly());

    // Optimization for the case where attributes decorate entities in the same assembly in which case 
    // we can cache the successful APTCA check between the decorated and the declared assembly.
    Assembly lastAptcaOkAssembly = null;

    for (int i = 0; i < car.Length; i++)
    {
        object attribute = null;
        CustomAttributeRecord caRecord = car[i];

        IRuntimeMethodInfo ctor = null;
        RuntimeType attributeType = null;
        bool ctorHasParameters, isVarArg;
        int cNamedArgs = 0;

        IntPtr blobStart = caRecord.blob.Signature;
        IntPtr blobEnd = (IntPtr)((byte*)blobStart + caRecord.blob.Length);
        int blobLen = (int)((byte*)blobEnd - (byte*)blobStart);

        if (!FilterCustomAttributeRecord(caRecord, scope, ref lastAptcaOkAssembly, 
                                         decoratedModule, decoratedMetadataToken, attributeFilterType, mustBeInheritable, 
                                         attributes, derivedAttributes,
                                         out attributeType, out ctor, out ctorHasParameters, out isVarArg))
            continue;

        if (ctor != null)
        {
            // Linktime demand checks 
            // decoratedMetadataToken needed as it may be "transparent" in which case we do a full stack walk
            RuntimeMethodHandle.CheckLinktimeDemands(ctor, decoratedModule, isDecoratedTargetSecurityTransparent);
        }
        else
        {
            // 


        }

        // Leverage RuntimeConstructorInfo standard .ctor verfication
        RuntimeConstructorInfo.CheckCanCreateInstance(attributeType, isVarArg); 

        // Create custom attribute object
        if (ctorHasParameters)
        {
            attribute = CreateCaObject(decoratedModule, ctor, ref blobStart, blobEnd, out cNamedArgs); 
        }
        else
        {
            attribute = RuntimeTypeHandle.CreateCaInstance(attributeType, ctor);

            // It is allowed by the ECMA spec to have an empty signature blob
            if (blobLen == 0)
                cNamedArgs = 0;
            else
            {
                // Metadata is always written in little-endian format. Must account for this on
                // big-endian platforms.
#if BIGENDIAN
                const int CustomAttributeVersion = 0x0100;
#else
                const int CustomAttributeVersion = 0x0001;
#endif
                if (Marshal.ReadInt16(blobStart) != CustomAttributeVersion)
                    throw new CustomAttributeFormatException();
                blobStart = (IntPtr)((byte*)blobStart + 2); // skip version prefix

                cNamedArgs = Marshal.ReadInt16(blobStart);
                blobStart = (IntPtr)((byte*)blobStart + 2); // skip namedArgs count
#if BIGENDIAN
                cNamedArgs = ((cNamedArgs & 0xff00) >> 8) | ((cNamedArgs & 0x00ff) << 8);
#endif
            }
        }

        for (int j = 0; j < cNamedArgs; j++)
        {
            #region // Initialize named properties and fields
            string name;
            bool isProperty;
            RuntimeType type;
            object value;
            
            IntPtr blobItr = caRecord.blob.Signature;

            GetPropertyOrFieldData(decoratedModule, ref blobStart, blobEnd, out name, out isProperty, out type, out value);

            try
            {
                if (isProperty)
                {
                    #region // Initialize property
                    if (type == null && value != null)
                    {
                        type = (RuntimeType)value.GetType();
                        if (type == Type_RuntimeType)
                            type = Type_Type;
                    }

                    RuntimePropertyInfo property = null;

                    if (type == null)
                        property = attributeType.GetProperty(name) as RuntimePropertyInfo;
                    else
                        property = attributeType.GetProperty(name, type, Type.EmptyTypes) as RuntimePropertyInfo;

                    // Did we get a valid property reference?
                    if (property == null)
                    {
                        throw new CustomAttributeFormatException(
                            String.Format(CultureInfo.CurrentUICulture, Environment.GetResourceString(
                                isProperty ? "RFLCT.InvalidPropFail" : "RFLCT.InvalidFieldFail"), name));
                    }

                    RuntimeMethodInfo setMethod = property.GetSetMethod(true) as RuntimeMethodInfo;
                    
                    // Public properties may have non-public setter methods
                    if (!setMethod.IsPublic)
                        continue;

                    RuntimeMethodHandle.CheckLinktimeDemands(setMethod, decoratedModule, isDecoratedTargetSecurityTransparent);

                    setMethod.UnsafeInvoke(attribute, BindingFlags.Default, null, new object[] { value }, null);
                    #endregion
                }
                else
                {
                    RtFieldInfo field = attributeType.GetField(name) as RtFieldInfo;

                    if (isDecoratedTargetSecurityTransparent)
                    {
                        RuntimeFieldHandle.CheckAttributeAccess(field.FieldHandle, decoratedModule.GetNativeHandle());
                    }

                    field.CheckConsistency(attribute);
                    field.UnsafeSetValue(attribute, value, BindingFlags.Default, Type.DefaultBinder, null);
                }
            }
            catch (Exception e)
            {
                throw new CustomAttributeFormatException(
                    String.Format(CultureInfo.CurrentUICulture, Environment.GetResourceString(
                        isProperty ? "RFLCT.InvalidPropFail" : "RFLCT.InvalidFieldFail"), name), e);
            }
            #endregion
        }

        if (!blobStart.Equals(blobEnd))
            throw new CustomAttributeFormatException();

        attributes[cAttributes++] = attribute;
    }

    // The frame will be popped automatically if we take an exception any time after we pushed it. So no need of a catch or
    // finally or CERs here.
    frame.Pop();

    if (cAttributes == car.Length && pcaCount == 0)
        return attributes;

    object[] result = CreateAttributeArrayHelper(arrayType, cAttributes + pcaCount);
    Array.Copy(attributes, 0, result, 0, cAttributes);
    return result;
}

The message of exception is definitely not helpful, because it gives not a root cause!

0
On

Old post I know but there is a work-around. I had a similar problem when using Reflection and Custom Attributes. I changed the property to update the set value if it was Null like below.

public object[] Parameters { get; set; }

was changed to:

private object[] _Parameters = new object[0];

public object[] Parameters {
  get {
    return _Parameters;
  }
  set {
    _Parameters = value ?? new object[0];
  }
}

So now, even if you assign Null or fail to assign a value it will work but you may need to update your logic elsewhere if you are expecting to skip the attribute if it is set as Null.

Also in my case this was when handling an array of int[] not object[].