Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Distinguish between getter-only property and expression body property?

Is it possible, using reflection, to distinguish between a getter-only property and an expression body property?

class MyClass
{
    DateTime GetterOnly { get; }

    DateTime ExpressionBody => DateTime.Now;
}

For example, how could the method below be completed?

enum PropertyKind
{
    NotInteresting,

    GetterOnly,

    ExpressionBody,
}

PropertyKind GetPropertyKind(PropertyInfo propertyInfo)
{
    if (propertyInfo.GetSetMethod(true) == null)
    {
        // what goes here??
    }

    return PropertyKind.NotInteresting;
}

Related post: What is the difference between getter-only auto properties and expression body properties?

like image 883
SFun28 Avatar asked Mar 11 '20 13:03

SFun28


1 Answers

First we must define our terms:

  • Auto-Property - One with a backing field automatically generated by the compiler.
  • Expression-bodied property - One implemented using the => (lambda) syntax.
  • Function-bodied property - One implemented using the normal {...} syntax.

It is important to note that it is not possible to differentiate between an expression-bodied property and a function-bodied property, because effectively the same IL will be generated for both.

However, I believe that what you actually want is to be able to tell the difference between an auto-property and a non-auto-property.

This is possible because the compiler generates a backing field decorated with [CompilerGeneratedAttribute] and with a name derived from the property, which can be tested for.

The backing field name is currently always "<PropertyName>k__BackingField" (where PropertyName is the name of the property), and this is true for both .Net 4.6 and .Net Core 3.1 - but of course this is in no way guaranteed to never change, so any code that relies on this is likely to break for future versions of the C# compiler.

Notwithstanding that rather large caveat, you can write a method to check if a PropertyInfo implements an auto-property like so:

public static bool IsAutoProperty(PropertyInfo property)
{
    string backingFieldName = $"<{property.Name}>k__BackingField";
    var    backingField     = property.DeclaringType.GetField(backingFieldName, BindingFlags.NonPublic | BindingFlags.Instance);

    return backingField != null && backingField.GetCustomAttribute(typeof(CompilerGeneratedAttribute)) != null;
}

This inspects the property to see if (a) it has a backing field with a specific name derived from the property name and (b) that backing field is compiler generated.

I don't think this is a good idea, because it relies on undocumented and empirically-determined compiler behaviour, so caution is required!

Here's a compilable console app to demonstrate:

using System;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace ConsoleApplication1
{
    static class Program
    {
        static void Main(string[] args)
        {
            var type = typeof(MyClass);

            foreach (var property in type.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance))
            {
                if (IsAutoProperty(property))
                    Console.WriteLine($"{property.Name} is an auto-property");
            }
        }

        public static bool IsAutoProperty(PropertyInfo property)
        {
            string backingFieldName = $"<{property.Name}>k__BackingField";
            var    backingField     = property.DeclaringType.GetField(backingFieldName, BindingFlags.NonPublic | BindingFlags.Instance);

            return backingField != null && backingField.GetCustomAttribute(typeof(CompilerGeneratedAttribute)) != null;
        }
    }

    class MyClass
    {
        DateTime GetterOnly { get; }

        DateTime ExpressionBody => DateTime.Now;
    }
}                                                                                                 

This outputs:

GetterOnly is an auto-property

like image 179
Matthew Watson Avatar answered Sep 29 '22 07:09

Matthew Watson