Properties
Define and use properties with custom logic or auto-implementation to control access to class fields safely.
In C#, properties are special class members that provide a flexible mechanism to read, write, or compute the value of a private field.
Define a property
Properties are created with the following syntax:
// Like other members, properties can have access modifiers[access modifiers] property_type property_name{get{// Get block contains actions to perform when returning the value of the field}set{// Set block contains actions to perform when setting the value of the field}}
Line 2: Properties usually have public access to expose data, while the underlying fields remain private.
Lines 4–12: The property body contains
getandsetaccessors, which act as methods for reading and writing data.
Properties do not store data directly. Instead, they act as intermediaries between external code and the data stored in fields.
Let’s look at the following example. Here, the Car class defines the private model and price fields that store the model and the price of the car. Along with these fields, there are two public properties called Model and Price.
Line 1: Uses a file-scoped namespace to reduce indentation.
Line 6: Initializes the
modelfield tostring.Emptyto ensure safety, as modern C# does not allow null references by default.Lines 9–13: Defines the
Modelproperty, which reads from and writes to themodelbacking field.Lines 19–23: Defines the
Priceproperty, managing access to thepricefield.
Let’s use Program.cs to execute it.
Line 3: Creates a new instance of the
Carclass.Line 4: The assignment triggers the
setblock of theModelproperty.Line 7: Accessing
car.Modeltriggers thegetblock to retrieve the value.
While usually the field and property names differ only by case, this is not required. Names can be arbitrary, but it is good practice to have matching names (e.g., model field and Model property).
Property usage
Using the Model property from the previous example, we can control access to the model field.
get { return model; } // Returns the value of the field
The get block returns the value of the field. Depending on the requirements, it can perform checks or other manipulations before returning the data.
set { model = value; } // Sets the value of the field
We set the value of the model field in the set block. The value keyword is a special placeholder for the data that is passed to the property:
car.Model = "Lexus LS 460L"; // "Lexus LS 460L" is assigned to 'value'
We work with properties just like regular fields. When we assign a value to a property, the set block is triggered. When we read the value of a property, the get block is triggered.
Validate values with properties
Properties have the capacity to control access logic. In our example, the price field shouldn’t have a negative value. We can prevent users from setting a negative value with the help of a property:
Line 12: The
ifstatement checks the incomingvalue(implicit parameter) instead of the field itself.Lines 13–15: If
valueis negative, we assign0to thepricefield to enforce data integrity.Lines 17–19: If
valueis non-negative, we assign it directly toprice.
Now, let’s verify this behavior with a Program.cs file.
Line 6: We try to assign
-5000to thePriceproperty. Thesetaccessor intercepts this and assigns0instead.Line 7: The console output will confirm that the
Priceis0, not-5000.Lines 10–11: We assign a valid positive number, which bypasses the validation correction. The console output verifies this.
Properties are not exclusive to classes. They can also be created in structs.
Access modifiers
We can control access to a property using access modifiers. We can decorate the whole property or its separate blocks:
public string Model{get { return model; }private set { model = value; }}
Line 1: The property itself is
public, meaning anyone can read it.Line 3: The
privatemodifier on thesetaccessor prevents external code from changing the value.
Making the set block private makes it impossible for an external class to change the value of the property, making it effectively read-only to the outside world.
Line 9: The
private setmodifier ensures thatModelcan only be set inside theCarclass.Line 19: The validation logic must check
value(the incoming data), notprice(the current field).Line 33: The constructor sets
Modelinternally, which is allowed because the constructor is part of theCarclass.
Now, let’s verify the private set behavior.
Line 3: We initialize the
Modelvia the constructor.Line 6: Attempting to assign
car.Modeldirectly is commented out because the compiler prevents it, proving the access modifier works.Line 10: We can still read
car.Modelbecause thegetaccessor is public.
When setting access modifiers to set and get blocks, we must consider the following constraints:
We can’t set an access modifier explicitly if the property contains only one block (
setorget):
public string Name{private get; // Invalid, must also have the set block}
Only one block can have an access modifier at a time:
public string Name{private get;private set; // Invalid, only one block can have an access modifier}
The access modifier of the
setorgetblock must be more restrictive than the modifier attached to the property.
internal string Name{public get; // Cannot be public, because the property is internalset;}
Auto-implemented properties
When we have many fields and do not require complex access-control logic, we can use auto-implemented properties.
Consider the following example:
Lines 6–24: Writing out manual backing fields and properties for every data point is repetitive and clutters the code.
If a class contains many fields, writing manual properties for each becomes tedious and repetitive. Auto-implemented properties were introduced to .NET to avoid this:
public string Model { get; set; } = string.Empty;
This auto-property has a generated backing field created by the compiler. The compiler generates code similar to the following:
[CompilerGenerated]private string <Model>k__BackingField = string.Empty;public string Model{get { return <Model>k__BackingField; }set { <Model>k__BackingField = value; }}
Line 2: The compiler creates a private backing field with a generated name that cannot be accessed directly by your code.
Lines 6–7: The
getandsetaccessors are wired to this generated field automatically.
The primary advantage of auto-properties is that they make our code concise while allowing us to expand them later if logic is needed.
Access modifiers can be used in the same way as with regular properties:
public string Model { get; private set; } = string.Empty;
If we make all properties auto-implemented, this would be our resulting Car class:
Line 6:
Modelis initialized to an empty string to satisfy null safety requirements.Lines 7–8:
PriceandYearuse default initialization (0 for numeric types).
Init-only and required properties
Modern C# introduces the init accessor and the required modifier to make object initializers safer and more robust.
An
initaccessor replaces thesetaccessor. It allows a property to be assigned only during object creation. Once the object is initialized, the property becomes fully immutable.The
requiredmodifier forces the caller to set the property during initialization. This eliminates the need for bulky constructors simply to enforce mandatory data fields.
Lines 6–7: The
requiredkeyword guarantees that any code creating anEmployeemust provide these values. Theinitkeyword ensures they cannot be modified after creation.Line 10: The
Departmentproperty is not required, but theinitaccessor ensures that if it is provided during initialization, it remains immutable thereafter.
Let’s see how the compiler enforces these rules when we create an object.
Lines 4–8: The object initializer provides all
requiredproperties, allowing the code to compile successfully.Line 13: Attempting to change
FirstNameafter initialization fails because it uses aninitaccessor.Line 16: Attempting to create an
Employeewithout providing theLastNamefails because it is marked asrequired.
Extra
.NET IDEs contain shortcuts because creating a property for a field is among the most common operations performed by software developers. For instance, in Visual Studio, we can automatically generate a property for a field by using the encapsulate field refactoring option in the Quick Actions menu (often accessed via Ctrl + .).