init-only properties

Init-only properties are a new addition to C# 9. The purpose of this blog post is to look into how it is represented in IL, as well to see how it behaves from IL verification's point of view.

What is an init-only property?

Properties, readonly properties and properties with private setter have been around for a while in C#. One use-case was still missing: setting properties with object initializers that are readonly. Previously these properties had to be read-write or values had to be passed through constructors, which involved writing a good set of constructors. The following code snippet shows an example of an init-only property. State class has a bool 'On' property, which is an init-only property.

var data = new State { On = true };
Console.WriteLine(data.On);

public class State
{
  public bool On { get; init; }
}

IL

Using ILSpy or ildasm we can read the IL instructions output by the C# compiler.

.property instance bool On()
{
  .get instance bool State::get_On()
  .set instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) State::set_On(bool)
}

The setter property has a modreq modifier requiring a IsExternalInit type, while the getter still looks the same as before. modreq seems an interesting keyword, there is not much official documentation around it at the time of writing this post.

How does the IL look like for an object initializer?

IL_0000: newobj instance void State::.ctor()
IL_0005: dup
IL_0006: ldc.i4.1
IL_0007: callvirt instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit)State::set_On(bool)
IL_000c: dup
IL_000d: ldc.i4.1
IL_000e: callvirt instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit)State::set_Enabled(bool)
IL_0013: stloc.0

With object initialization, when a new object is created, it is being duplicated on the evaluation stack until all its properties are set. In the example above 2 init-only properties are set, On and Enabled. Once all the properties are set, the object is stored in a local variable list, so it can be used later on and passed by reference.

Editing C# code, it is clear that the C# compiler enforces the rules which makes this property init-only in the code editor and at build time.

Serialization

Following the rules of the C# compiler, the runtime would need to adhere the same set of rules when populating init-only properties. In the discussion of the feature, IL verification has been discussed, with the runtime to restrict when a property is set. Running the below example, we can confirm that serialization today works without an issue.

var initalData = new State() { On = true };
var serialized = JsonSerializer.Serialize(initalData);
var deserialized = JsonSerializer.Deserialize<State>(serialized);
Console.WriteLine(deserialized.On); // True
Console.WriteLine(deserialized == initalData); // False 

The above code serializes the State object to a standard json:

{"On":true}

And deserialization creates a new object typed State, which is not in referential equality with the initialData. Still its internal 'state' is restored, the On property is set to true. This could mean that init-only properties are a C# compiler features only, or the serializers handle init-only properties. Let's see some further examples to decide.

Reflection

Can I set init-only properties with reflection?

var data = new State { On = true };
Console.WriteLine(data.On); // True
typeof(State).GetProperties().First().SetValue(data, false);
Console.WriteLine(data.On); // False

Although the design consideration of init-only properties include IL verification, the value of such a property can be modified with reflection even after the object initialization phase.

Note, we can use the First() Linq extension method on GetProperties safely, because we only have a single property on State type, and we have full control over this type.

IL Generation

Can I use IL generator to set such a property? From the example above, we could assume this is possible, but let's prove it through an example:

var data = new State { On = true };
Console.WriteLine(data.On); // True
var method = new DynamicMethod("InitOnlySetter", typeof(void), new[] { typeof(State), typeof(bool) });
var generator = method.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldarg_1);
generator.Emit(OpCodes.Callvirt, typeof(State).GetProperties().First().SetMethod);
generator.Emit(OpCodes.Ret);
var setter = method.CreateDelegate<Action<State, bool>>();
setter(data, false);
Console.WriteLine(data.On); // False

The program above is a really simple POC code. It creates a new DynamicMethod which will create a method with void return type and two input arguments typed State and bool. Then it uses the ILGenerator to emit IL code to load both load input arguments onto the evaluation stack and then to call the SetMethod of the property. Finally it creates an Action<State, bool> delegate for the emitted IL code, which can be invoked to modify the value of the State type.

As this is calling the same SetMethod method as before, it should not be a surprise that this can modify the value of an init-only property too.

Compile C# at Runtime

Lastly, let's see if we compile some C# code from string, can we modify the value? We can compile code dynamically with the help of CSharpCompilation type.

The code below creates a new string, initialized with the source code to compile. This source code has a static Set method in a static class called Helper. It parses the string into a syntax tree, then uses the CSharpCompilation to create a new in memory. For this, two references has to be passed, one to the current dll containing the State type and one to the System.Private.CoreLib.dll containing the core .NET types. Once the compilation is done and successful, we can load the stream into the current AppDomain (there is a single in .NET 5 we can use), find the respected Helper type and Set method to invoke it.

var data = new State { On = true };
string code = @"
public static class Helper
{
    public static void Set(State state, bool value) => state.On = value;
}";

Console.WriteLine(data.On);
var syntaxTree = CSharpSyntaxTree.ParseText(code);
var references = new List<PortableExecutableReference>();
references.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location));
references.Add(MetadataReference.CreateFromFile(Path.Combine(Environment.CurrentDirectory, "InitializeRecords.dll")));
var compilation = CSharpCompilation.Create("helper", new[] { syntaxTree }, references,
    new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

using var stream = new MemoryStream();
var result = compilation.Emit(stream);
if (!result.Success)
    throw new Exception(result.Diagnostics.First().ToString());
stream.Seek(0, SeekOrigin.Begin);
var assembly = Assembly.Load(stream.ToArray());
assembly.GetType("Helper").GetMethod("Set").Invoke(null, new object[] { data, false });
Console.WriteLine(data.On);

While the above program works perfectly with a regular setter, with init-only properties it returns an error. In the above code an exception is thrown upon checking if the compilation result is successful:

'(4,56): error CS8852: Init-only property or indexer 'State.On' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.'

In case of the init-only property, the C# compiler knows and enforces the rules for them, so one can be only set during the object initialization phase. As C# code is compiled the compiler will return the error.

Conclusion

Init-only properties, and respected rules are not enforced by IL Verification by the runtime, at this time it is a C# compiler feature.