C# 6 behind the scenes (part #1)

C# 6 brings a lot of new language features that can ease up a developer’s life. Technically, these are pure compiler features, meaning that no new version of the .NET framework is necessary – but what is going on behind the scenes, and what does the compiler actually do when it encounters code lines that contain the new syntax?

This question is especially interesting since C# code may be analysed or changed at runtime (using reflection) or after build (e.g., using libraries such as Mono.Cecil). This means that either

  • your code might be deconstructed by some framework you are using,
  • or (even more interesting), if you create libraries that can be used by other developers, you might want to parse parts of their code.

I came across this exact situation as part of my implementation of the ICommand interface: Any developer that makes use of this implementation may define certain conditions under which a command can be executed. These conditions are defined as lambda expressions, passed to the ICommand library, and then parsed and analysed by the code contained in this library (which, in course, has been written by me) – so I was wondering whether I needed to change the parsing process in order to cope with C# 6 expressions any developer might have used to express their condition.

The simple answer – fortunately – was no, although it’s good to know that there are certain restrictions. But let’s take a look at some examples to discuss this topic in detail:

Auto-property initializers:

C# 6 allows to add property initializers as part of the property declaration syntax. This applies to both “standard” and getter-only auto-properties:

public string GetterOnlyProperty { get; } = "SomeValue";

public string GetterSetterProperty { get; set; } = "AnotherValue";

Let’s take a look at how these expressions are handled by the compiler! In general, .NET code is not compiled into machine code directly. Instead, the C# compiler generates CIL (Common Intermediate Language) code which is interpreted by a virtual machine at runtime. One of the most convenient ways of analysing CIL code is the ILSpy application, as it lets you even decompile CIL code to C#.

Let’s copy the two lines of C# code shown above to an (otherwise empty) console application project, compile it, and load the resulting .exe file into ILSpy. As mentioned before, its possible to show the exact CIL code contained in our compiled application, but I recommend to switch the dropdown box in the menu bar from IL to C# – this activates decompilation mode and allows to directly compare input (the two lines shown above, as entered into Visual Studio) and output (CIL code converted back to C#).

What we see is the following:

public string GetterOnlyProperty
{
	[CompilerGenerated]
	get
	{
		return this.<GetterOnlyProperty>k__BackingField;
	}
}

public string GetterSetterProperty
{
	get;
	set;
}

public Program()
{
	this.<GetterOnlyProperty>k__BackingField = "SomeValue";
	this.<GetterSetterProperty>k__BackingField = "AnotherValue";
	base..ctor();
}

Since C# 6 is only understood by the compiler while the .NET framework itself has not changed, this means that there is no new version of the .NET virtual machine that could interpret the new language expressions. Instead, the C# compiler included with Visual Studio 2015 converts the extended auto-property initializers to conventional CIL expressions: The two auto-properties are complemented with traditional initializers called in the program’s constructor, working on the properties’ backing fields.

Expression initializers:

Similarly, initializers may be defined for methods, operators, conversions, and method-like getter-only properties. To test this, we could add the following method and property to our sample application…

public int AutoProperty => GetterOnlyProperty.Length;

public int Multiply(int a, int b) => a * b;

…and again compile and inspect it – ILSpy will show the following result:

public int AutoProperty
{
	get
	{
		return this.GetterOnlyProperty.Length;
	}
}

public int Multiply(int a, int b)
{
	return a * b;
}

Even in this case, C# 6 language features are converted to traditional expressions by the compiler.

No surprises so far, anyone parsing through our code after compilation will not stumble across any new language expressions, as everything is converted to conventional IL syntax. However, what about live code analysis during runtime using reflection? We’ll examine this use case and discuss a few examples in the second part of this artilce!