5

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?

Suraj
  • 35,905
  • 47
  • 139
  • 250
  • You can certainly tell via reflection if only a getter is implemented (just search for the setter and it won't be there). – Neil Mar 11 '20 at 13:50
  • 3
    An expression-bodied property *is* a getter-only propety, one whose getter doesn't just return the backing field value – Panagiotis Kanavos Mar 11 '20 at 13:50
  • I suspect the real question is something different. Are you trying to create a custom serializer or analyzer perhaps? A Roslyn analyzer *can* detect whether the syntax is a get-only auto property or an expression-bodied one. That's how refactorings can switch between the two. At runtime though, there's no difference between one getter and the other, except their code. Reflection can't tell where those getters came from – Panagiotis Kanavos Mar 11 '20 at 13:56
  • When you say "expression body property" do you actually mean that, or do you really mean "auto-implemented property"? Because from the IL you can't tell the difference between a normal property and an expression body property. – Matthew Watson Mar 11 '20 at 14:04
  • @MatthewWatson - I really mean "expression body property". I updated the post with an example class. – Suraj Mar 11 '20 at 14:09
  • @PanagiotisKanavos - The real question is as stated. If it's not possible to distinguish via reflection (possible using some heuristic that accounts for the fields on the class?) then can you post that as the answer? – Suraj Mar 11 '20 at 14:10
  • Then it's not possible because an expression body property is compiled to pretty much the same IL as a non-expression body property, so there's no way to tell the difference. – Matthew Watson Mar 11 '20 at 14:10
  • Instead of pure reflection, is there a heuristic that could be used that accounts for backing fields to distinguish between the two? – Suraj Mar 11 '20 at 14:12
  • 1
    *Except* an attribute and the existence of the backing field: [this sharplab.io example](https://sharplab.io/#v2:EYLgtghgzgLgpgJwDQBMQGoA+ABATARgFgAobAZgAI8KBhCgbxIuasuAHt2AbCgIU/oBzODADcAXyYsO3CgH0AHgF4YCAK5xRLClObkKMngFlOuJQD5Fo3ayoAWCkYAUASgY3JxcUA==) shows the auto-property has the `CompilerGenerated` attribute – Panagiotis Kanavos Mar 11 '20 at 14:12
  • @PanagiotisKanavos - BINGO! So I could look for CompilerGenerated on the property and use that to distinguish between the two. – Suraj Mar 11 '20 at 14:15
  • @PanagiotisKanavos But the OP is talking about expression body properties, not auto properties. At least that's what they claimed, but looking at the code I can see that the OP is actually using an auto property... – Matthew Watson Mar 11 '20 at 14:15
  • @SFun28 **why**? Why do you want that, when the two getters behave the same? Why are you asking this? It's *extremely* likely that someone already encountered the same problem. Dapper for example [handles read-only properties](https://stackoverflow.com/a/35645742/134204) by looking for the backing field's name – Panagiotis Kanavos Mar 11 '20 at 14:15
  • @SFun28 and you *shouldn't* do that without an extremely serious reason. Dapper took a calculated risk, and had some trouble when .NET Core changed the generated code a bit. What's why I keep asking *why*? – Panagiotis Kanavos Mar 11 '20 at 14:17
  • @MatthewWatson I'm reading tea leaves. There can be only so many reasons the difference can matter, and Marc Gravel was grumbling about the backing field names about a year back. – Panagiotis Kanavos Mar 11 '20 at 14:18
  • @SFun28 Your example class shows an *auto* property and an *expression body* property, so it seems that you actually *are* trying to tell the difference between an auto property and a non-auto property (albeit one implemented via an expression body). – Matthew Watson Mar 11 '20 at 14:19
  • @SFun28 the sharplab example shows that you *really can't tell* the difference reliably. The expression-bodied getter still uses a backing field, and yet, it wouldn't be detected. A serializer would work better by calling a constructor – Panagiotis Kanavos Mar 11 '20 at 14:20
  • @PanagiotisKanavos - good point about .NET Core, at some point i'll be migrating and would want the code to be future proofed, if possible. The "why" has to do with a code-gen library I'm working on. Whereas getter-only properties are are likely to be constructor arguments, expression body properties are not. That's an important distinction for some problems I'm working on. Would be a much longer writer-up! – Suraj Mar 11 '20 at 14:20
  • @SFun28 that's a job for Roslyn analyzers, which receive information directly from the compiler. That's how all analyzers, generators, fixes and refactorings work right now. If you check eg [Roslynator](https://github.com/JosefPihrt/Roslynator) you'll probably find code that detects different syntaxes and refactors between them already. – Panagiotis Kanavos Mar 11 '20 at 14:21
  • @PanagiotisKanavos - Agreed. Might be better to use Roslyn, but my hands are tied since the entire project just uses reflection and it would be a major refactoring. Anyways, reflection is actually pretty simple and it works for all of the scenarios I've encountered thus far. The code gen library automates the creation of boilerplate code (like hash codes, equality, ToString, cloning) along with the unit tests. – Suraj Mar 11 '20 at 14:23
  • @SFun28 check [Cross-Platform Code Generation with Roslyn and .NET Core](https://learn.microsoft.com/en-us/archive/msdn-magazine/2017/may/net-core-cross-platform-code-generation-with-roslyn-and-net-core) and [Language-Agnostic Code Generation with Roslyn](https://learn.microsoft.com/en-us/archive/msdn-magazine/2016/june/net-compiler-platform-language-agnostic-code-generation-with-roslyn) – Panagiotis Kanavos Mar 11 '20 at 14:24
  • @PanagiotisKanavos - I edited my prior response. Thanks for the link! Reflection is actually pretty simple, been around for a long time (so lots of examples), and doesn't require extra dependencies. I've been able to code-gen crazy amounts of useful but boilerplate code using just reflection. It works and works really well. This is the first case where I'm hitting something in the code-gen library where reflection is not able to accomodate. – Suraj Mar 11 '20 at 14:28
  • @MatthewWatson - Haven't lost sight of your comments. I'm digesting them now.... – Suraj Mar 11 '20 at 14:30
  • OK: there *is* a way to tell if something is an auto-generated property, but if it isn't then there's no way to tell if it's an expression-bodied or an function-bodied implementation (since they both generate the same IL). – Matthew Watson Mar 11 '20 at 14:33

1 Answers1

5

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

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • 1
    I apprciate the detail of this post. I had a long write-up as a comment to my question, but didn't submit it. You've given me a bunch to think about. Part of the complexity here is that there are many ways to author a property, but my developers don't use the whole universe of such options. The thing I'm really trying to get at is differentiating properties that need to be serialized because they are necessary for rehydrating an object and objects that don't need to be serialized because they are derivative of the other information. – Suraj Mar 11 '20 at 15:03
  • I think that that's a generally cumbersome problem to solve and so I'm using the fact that my devlopers only author properties in certain ways as a proxy for determining which properties are required for serialization and which are not. – Suraj Mar 11 '20 at 15:05
  • @SFun28 Could you create your own custom attribute type and ask your developers to decorate the appropriate properties with it? Then you could use reflection to see if a property has that attribute. – Matthew Watson Mar 11 '20 at 15:15
  • I don't love attributes on properties. I feel like it pollutes the models. Anyways, this gives me enough to go on! – Suraj Mar 11 '20 at 15:51
  • For `IsAutoProperty` is it sufficient to just check for `CompilerGeneratedAttribute` on the getter or setter method? Do you have to look for the backing field? Or is there some case where that attribute is found on the getter or setter method but the property is not an auto-property? – Suraj Mar 11 '20 at 17:44
  • 1
    @SFun28 I suspect it would be sufficient just to check for `CompilerGeneratedAttribute`. – Matthew Watson Mar 12 '20 at 14:29