Decompiling Syntactic Sugar in C#

Over the last couple years, there have been a few new features introduced into C# that are considered “syntactic sugar”.  These types of enhancements are great as they allow code to be written in a clean and concise manner by limiting boilerplate and otherwise redundant code.  That said, these features typically do not introduce new functionality.  They offer new syntax for taking advantage of existing features.  Hence the name, syntactic sugar.

To understand when to use these new features and how to use them, it can be helpful to understand how the compiler is handling them. Recently I had an interesting discussion with a fellow developer about a few syntactic sugar features that were introduced in C# 6.  Specifically, we were discussing expression bodied properties and default value expressions.

In this article, we will be comparing these two features by looking at what the compiler is doing under the hood.

Decompiling Assemblies

There are a couple different ways to analyze compiler output.  The most in-depth approach is looking directly at the Intermediate Language (IL) that results from the compilation process. Free tools like dotPeek and ILSpy can be used to extract an assemblies IL however, if we are only interested in deconstructing the syntax sugar, there is a simpiler way.

Both dotPeek and ILSpy can also be used to decompile assemblies back into their source language. You also have the ability to see the code that is generated by the compiler. In dotPeek, this can be done by selecting the  “show compiler-generated code” setting and deselecting the “Use sources from symbol files when available”.

Expression Bodied Properties

As the name suggests, expression bodied properties allow you to define a property with a C# expression.  Using this feature can lead to cleaner and more readable code but, how does the compiler interpret it?  Lets take a look by creating an explicit example and seeing what gets generated. 

To get started, lets create the following property.

public string ExpressionBodiedProp => "Foo";

Next we need to compile our it.  Then, using dotPeek, we can open the compiled assembly and browse to our decompiled class. Looking at the compiler-generated code, we see the following

public string ExpressionBodiedProp
{
  get
  {
    return "Foo";
  }
}

Now, we can clearly see how an expression bodied property is compiled and what is behind the syntactic sugar.  This also gives us insight on what the constraints are around this feature.  For instance, it is obvious that an expression bodied property can never be assigned to when looking at the compiler generated output.

Next lets dissect another feature that looks similar but is handled quite differently by the compiler.

Auto-Property Initializers

C# 6 introduced another new way to define and initialize properties in addition to expression bodied properties.  Auto-property initializers can be used to assign default values to auto-properties.  This gives you something that looks like the following.

public string DefaultValueProp { get; set; } = "Bar";

Auto-properties can also be defined without a setter.  This prevents the property from having a value assigned to it outside of the constructor.  The goal of this feature is enabling immutable auto-properties.  Auto-properties could always have a private setter however, there was nothing preventing the property from being altered within the class after construction.

If we combine these to feature, the result is something that looks very similar to an expression bodied property.

public string DefaultValueProp { get; } = "Bar";

The code looks very similar to our previous example which can lead to some confusion as to what the difference is between the two implementations.  This is where looking at the compiler generated code can be helpful.  Following the same steps as before, below is the generated code for an immutable auto-property with an auto-property initializer assigned.

[CompilerGenerated]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly string \u003CDefaultValueProp\u003Ek__BackingField;

public string DefaultValueProp
{
  [CompilerGenerated] get
  {
    return this.\u003CDefaultValueProp\u003Ek__BackingField;
  }
}

public Properties()
{
  this.\u003CDefaultValueProp\u003Ek__BackingField = "Bar";
  base.\u002Ector();
}

Unlike the expression bodied property, we can see there is nothing in the compiled output which prevents us from assigning a value to our property in this case.  If we assign a value to the property within the constructor then we get the following output.  Remember, auto-properties without a setter can only be assigned to within the constructor.

[CompilerGenerated]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly string \u003CDefaultValueProp\u003Ek__BackingField;    

public string DefaultValueProp
{
  [CompilerGenerated] get
  {
    return this.\u003CDefaultValueProp\u003Ek__BackingField;
  }
}

public Properties()
{
  this.\u003CDefaultValueProp\u003Ek__BackingField = "Bar";
  base.\u002Ector();
  this.\u003CDefaultValueProp\u003Ek__BackingField = "Hello World";
}

This time by looking at the compiler generated code, we can see we are overwriting the default value with what is provided in the constructor.  Of course, it would not make a lot of sense to leave the code in this state.

Conclusion

Using features that contain syntactic sugar is a great way to clean up and remove boilerplate code.  That said, it is important to understand how the compiler handles these features.  Using tools like dotPeek and ILSpy are a great way to uncover code that gets generated by the compiler.