init-only properties
04/20/2021
7 minutes
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.