Back to Blog
C#

C# Init Keyword: Immutable Properties After Construction

c# init keyword: Learn the C# init keyword for creating immutable properties that support object initializers. Understand syntax, constraints, and when to use init ins...

C#object initializersimmutabilityproperty initializationreadonly
A C# init keyword concept showing a locked property after initialization.

The c# init keyword defines an accessor that allows a property to be assigned only during object initialization. Unlike a set accessor, an init accessor cannot be called after construction completes. This gives you the convenience of object initializers without sacrificing immutability.

Consider a typical mutable property:

public class Person { public string Name { get; set; } }

With the set accessor, Name can be changed at any time. If you want to prevent changes after the object is created, you might have used a readonly field assigned in a constructor. The init accessor offers a middle ground: assign during construction, then freeze.

Init Accessor Syntax

Use init in place of set when declaring a property:

public class Person { public string Name { get; init; } public int Age { get; init; } }

Now you can create an instance with an object initializer:

var person = new Person { Name = "Alice", Age = 30 };

After the initializer completes, Name and Age cannot be reassigned:

person.Name = "Bob"; // Compiler error: Init-only property can only be assigned in an object initializer or 'this' constructor

This behavior is enforced at compile time. Any attempt to modify an init-only property outside construction results in a compilation error.

How Init Differs from Set and Readonly

The set accessor allows assignment at any time. readonly fields can only be assigned in a constructor or field initializer. The init accessor sits between them: it permits assignment from within a constructor, from an object initializer, or from a with expression (for records), but not after construction.

Here is a constructor that assigns an init-only property:

public class Person { public string Name { get; init; } public Person(string name) { Name = name; } }

This is valid because the assignment happens during construction. After the constructor returns, no further assignment is allowed.

Underlying Compiler Behavior

The compiler implements init accessors by generating a readonly backing field and a special setter that can only be called from a constructor or from within the init context. The exact implementation differs, but the effect is the same: modification attempts outside initialization are rejected at compile time.

Practical Use Cases for Init-Only Properties

Use init when you want to create immutable data that is easy to construct with object initializers. Typical scenarios include:

  • Value objects that represent a snapshot of data.
  • Configuration objects that are read after creation.
  • Data transfer objects (DTOs) that should not change after being populated.
  • Entities in a domain model where identity and core attributes are fixed after creation.

For example, a configuration class:

public class AppConfig { public string ConnectionString { get; init; } public int TimeoutSeconds { get; init; } }

The app loads configuration during startup and passes it to services. Those services should not modify it.

Working with with Expressions in Records

Records support init-only properties and the with expression. The with expression creates a copy of the record with specified properties changed, but it works even with init accessors because the copy is still considered to be in construction.

public record Person { public string Name { get; init; } public int Age { get; init; } } var alice = new Person { Name = "Alice", Age = 30 }; var olderAlice = alice with { Age = 31 };

The original alice remains unchanged. olderAlice is a new instance. This maintains immutability while allowing a form of "update."

Limitations and Constraints

Init-only properties have specific limitations you should understand.

Cannot Be Used with Auto-Property Initializers in Interfaces

Before C# 9, auto-property initializers were not allowed in interfaces. Even with newer C# versions, init-only properties in interfaces must be implemented carefully. The implementing class must also declare the property with init.

public interface IHasName { string Name { get; init; } } public class Person : IHasName { public string Name { get; init; } }

No Support for ref Returns

You cannot use an init-only property as a ref or out argument because that would allow external modification.

Cannot Be Modified by Reflection? (Runtime Constraint)

While the compiler blocks direct assignment, reflection can technically set init-only properties. This is not recommended, but be aware that immutability is enforced at compile time, not as a runtime security guarantee.

Comparing Init to Alternatives

ApproachAssign in ConstructorAssign After ConstructionObject Initializer Support
set accessorYesYesYes
init accessorYesNoYes
readonly fieldYesNoNo (via property wrapper)

A readonly field is the strongest immutability guarantee, but it reduces flexibility. Init-only properties provide a balance.

Making the Right Choice

Use init when you need the simplicity of object initializers but want properties to be immutable after construction. Prefer readonly fields when you are certain the value will never change and you are willing to forgo object initializer syntax. Use set only when the property genuinely needs to be mutable throughout the object's lifetime.

For example, in a DTO that is populated from a JSON payload and then passed to business logic, init prevents accidental corruption. If you are building a mutable entity with an identity that never changes, init on the identity property (such as an Id) is appropriate.

Compatibility and Language Version

The init keyword was introduced in C# 9. It requires .NET 5 or later, or .NET Core 3.1 with the IsExternalInit polyfill. If you are working on an older codebase, you may need to update the language version setting in the project file.

<PropertyGroup> <LangVersion>9.0</LangVersion> </PropertyGroup>

Note that the .NET runtime version matters because the compiler relies on certain APIs. The IsExternalInit class is generated automatically by the compiler for .NET 5+ projects. For earlier frameworks, you might need to define your own IsExternalInit class, but that is rarely needed in modern projects.

Potential Pitfalls with Error Handling

One common mistake is trying to initialize an init-only property from a method called by the constructor. Assignments in a method called from the constructor are not within the construction context, so they are not allowed.

public class Person { public string Name { get; init; } public Person() { SetName(); } private void SetName() { Name = "Default"; // Error: cannot assign to init-only property } }

If you need to set init-only properties from helper methods, move that logic into the constructor body directly, or make the helper return the value and assign it in the constructor.

public string Name { get; init; } public Person() { Name = GetDefaultName(); } private string GetDefaultName() => "Default";

Realistic Example: A Value Object

Here is a complete value object using init-only properties:

public class Money { public decimal Amount { get; init; } public string Currency { get; init; } }

You can create a Money instance with a clear initializer:

var price = new Money { Amount = 19.99m, Currency = "USD" };

Because Money is immutable, you can safely share instances across threads without locks. This is a practical benefit in concurrent code where objects are passed between tasks.

Performance Implications in High-Frequency Code

Init-only properties themselves introduce no runtime overhead compared to ordinary properties. They are compiled to similar IL with a special setter that includes a flag. The main cost is compile-time enforcement. In high-frequency scenarios, using init-only properties does not degrade performance. However, object initializers with many properties can cause some temporary allocations, but that is true for any initializer pattern.

Maintainability Considerations

Init-only properties make the intention of the code clearer. A property that is init-only tells future maintainers that it should not be changed after creation. This reduces the chance of accidental mutation bugs. It also enables safer concurrency because immutable data can be shared without synchronization.

When Not to Use Init

If you need lazy loading or want a property to be changed as part of business logic, init is not appropriate. For instance, an entity's Status property might need to change from Pending to Approved during its lifetime. Using init would prevent such transitions, so a set accessor with additional logic is better.

Similarly, if you rely on serializers that call property setters during deserialization, init-only properties can be problematic. Some serialization frameworks require a parameterless constructor and settable properties. In those cases, either use set or configure the serializer to support init.

Advanced Scenario: Chained Construction

Init-only properties can be assigned from within a constructor, and that constructor can be called by a derived class constructor.

public class Base { public string Id { get; init; } } public class Derived : Base { public Derived(string id) { Id = id; // allowed: constructors can access init accessors } }

This is useful when you need derived classes to set base properties during construction while keeping them immutable afterward.

Conclusion

The init keyword is a precise tool for creating immutable properties that support convenient initialization. Use it when you want to prevent post-construction changes but also want the clarity of object initializers. The compiler guarantees immutability, providing a strong contract for maintainability and concurrency. By understanding its limitations and correct usage, you can apply init where it truly belongs.

c# init keyword: Practical Usage and Code Examples | RYUSLOG DEV