Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get PropertyInfo of a C# auto property given the backing field

Tags:

c#

c#-3.0

I am implementing a custom IFormatter to serialize objects into a custom format that is required by our legacy systems.

If I declare a C# auto property:

[StringLength(15)]
public MyProperty { get; set; }

And then in my custom serialize method I get the serialized fields via:

MemberInfo[] members = 
    FormatterServices.GetSerializableMembers(graph.GetType(), Context);

How can I access the StringLength attribute that decorates the auto Property?

I am currently getting the property info by taking advantage of the <PropertyName>k_backingfield naming convention. I'd rather not rely on this as it seems to be a specific detail of the C# compiler implementation. Is there a better way?

like image 653
PhilB Avatar asked Oct 09 '22 08:10

PhilB


1 Answers

The better way would be to stop relying on private fields for serialization (as FormatterServices.GetSerializableMembers returns) and only use public Properties instead.

It is a LOT cleaner and works in this specific case.

But due to legacy code you might want to continue to use FormatterServices.GetSerializableMembers and in this case, no there is no other options for you other than using the naming convention (Or a little bit of IL analysis) and it may break at each new compiler release.

Just for fun here is some code to do a little bit of IL analysis (It lack ignoring NOOPs and other niceties but should work with most current compilers. If you really adopt such a solution check the Cecil library (written by Jb Evain) as it contains a full decompiler and it's better than doing it by hand.

It's usage is like this :

void Main()
{
    var members = FormatterServices.GetSerializableMembers(typeof(Foo));
    var propertyFieldAssoc = new PropertyFieldAssociation(typeof(Foo));

    foreach(var member in members)
    {
        var attributes = member.GetCustomAttributes(false).ToList();
        if (member is FieldInfo)
        {
            var property = propertyFieldAssoc.GetProperty((FieldInfo)member);
            if (property != null)
            {
                attributes.AddRange(property.GetCustomAttributes(false));
            }
        }

        Console.WriteLine(member.Name);
        foreach(var attribute in attributes)
        {
            Console.WriteLine(" * {0}", attribute.GetType().FullName);
        }
        Console.WriteLine();
    }
}

And the code :

class PropertyFieldAssociation
{
    const byte LDARG_0 = 0x2;
    const byte LDARG_1 = 0x3;
    const byte STFLD = 0x7D;
    const byte LDFLD = 0x7B;
    const byte RET = 0x2A;

    static FieldInfo GetFieldFromGetMethod(MethodInfo getMethod)
    {
        if (getMethod == null) throw new ArgumentNullException("getMethod");

        var body = getMethod.GetMethodBody();
        if (body.LocalVariables.Count > 0) return null;
        var il = body.GetILAsByteArray();
        if (il.Length != 7) return null;

        var ilStream = new BinaryReader(new MemoryStream(il));

        if (ilStream.ReadByte() != LDARG_0) return null;
        if (ilStream.ReadByte() != LDFLD) return null;
        var fieldToken = ilStream.ReadInt32();
        var field = getMethod.Module.ResolveField(fieldToken);
        if (ilStream.ReadByte() != RET) return null;

        return field;
    }

    static FieldInfo GetFieldFromSetMethod(MethodInfo setMethod)
    {
        if (setMethod == null) throw new ArgumentNullException("setMethod");

        var body = setMethod.GetMethodBody();
        if (body.LocalVariables.Count > 0) return null;
        var il = body.GetILAsByteArray();
        if (il.Length != 8) return null;

        var ilStream = new BinaryReader(new MemoryStream(il));

        if (ilStream.ReadByte() != LDARG_0) return null;
        if (ilStream.ReadByte() != LDARG_1) return null;
        if (ilStream.ReadByte() != STFLD) return null;
        var fieldToken = ilStream.ReadInt32();
        var field = setMethod.Module.ResolveField(fieldToken);
        if (ilStream.ReadByte() != RET) return null;

        return field;
    }

    public static FieldInfo GetFieldFromProperty(PropertyInfo property)
    {
        if (property == null) throw new ArgumentNullException("property");

        var get = GetFieldFromGetMethod(property.GetGetMethod());
        var set = GetFieldFromSetMethod(property.GetSetMethod());

        if (get == set) return get;
        else return null;
    }

    Dictionary<PropertyInfo, FieldInfo> propertyToField = new Dictionary<PropertyInfo, FieldInfo>();
    Dictionary<FieldInfo, PropertyInfo> fieldToProperty = new Dictionary<FieldInfo, PropertyInfo>();

    public PropertyInfo GetProperty(FieldInfo field)
    {
        PropertyInfo result;
        fieldToProperty.TryGetValue(field, out result);
        return result;
    }

    public FieldInfo GetField(PropertyInfo property)
    {
        FieldInfo result;
        propertyToField.TryGetValue(property, out result);
        return result;
    }

    public PropertyFieldAssociation(Type t)
    {
        if (t == null) throw new ArgumentNullException("t");

        foreach(var property in t.GetProperties())
        {
            Add(property);
        }
    }

    void Add(PropertyInfo property)
    {
        if (property == null) throw new ArgumentNullException("property");

        var field = GetFieldFromProperty(property);
        if (field == null) return;
        propertyToField.Add(property, field);
        fieldToProperty.Add(field, property);
    }
}

class StringLengthAttribute : Attribute
{
    public StringLengthAttribute(int l)
    {
    }
}

[Serializable]
class Foo
{
    [StringLength(15)]
    public string MyProperty { get; set; }

    string myField;
    [StringLength(20)]
    public string OtherProperty { get { return myField; } set { myField = value; } }
}
like image 157
Julien Roncaglia Avatar answered Oct 13 '22 11:10

Julien Roncaglia