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
}
}
General syntax for defining a property in C#
  • Line 2: Properties usually have public access to expose data, while the underlying fields remain private.

  • Lines 4–12: The property body contains get and set accessors, 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.

C# 14.0
namespace Properties;
public class Car
{
// model field
private string model = string.Empty;
// Model property
public string Model
{
get { return model; }
set { model = value; }
}
// price field
private decimal price;
// Price property
public decimal Price
{
get { return price; }
set { price = value; }
}
}
  • Line 1: Uses a file-scoped namespace to reduce indentation.

  • Line 6: Initializes the model field to string.Empty to ensure safety, as modern C# does not allow null references by default.

  • Lines 9–13: Defines the Model property, which reads from and writes to the model backing field.

  • Lines 19–23: Defines the Price property, managing access to the price field.

Let’s use Program.cs to execute it.

C# 14.0
using Properties;
Car car = new Car();
car.Model = "Lexus LS 460 L";
car.Price = 65000;
Console.Write($"{car.Model} costs {car.Price} dollars");
  • Line 3: Creates a new instance of the Car class.

  • Line 4: The assignment triggers the set block of the Model property.

  • Line 7: Accessing car.Model triggers the get block 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:

C# 14.0
namespace Properties;
public class Car
{
private decimal price;
public decimal Price
{
get { return price; }
set
{
if (value < 0)
{
price = 0;
}
else
{
price = value;
}
}
}
}
  • Line 12: The if statement checks the incoming value (implicit parameter) instead of the field itself.

  • Lines 13–15: If value is negative, we assign 0 to the price field to enforce data integrity.

  • Lines 17–19: If value is non-negative, we assign it directly to price.

Now, let’s verify this behavior with a Program.cs file.

C#
using Properties;
Car car = new Car();
// Attempt to set a negative price
car.Price = -5000;
Console.WriteLine($"Price after negative assignment: {car.Price}");
// Attempt to set a valid price
car.Price = 65000;
Console.WriteLine($"Price after valid assignment: {car.Price}");
  • Line 6: We try to assign -5000 to the Price property. The set accessor intercepts this and assigns 0 instead.

  • Line 7: The console output will confirm that the Price is 0, 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; }
}
Restricting the set accessor to the class scope
  • Line 1: The property itself is public, meaning anyone can read it.

  • Line 3: The private modifier on the set accessor 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.

C# 14.0
namespace Properties;
public class Car
{
private string model = string.Empty;
public string Model
{
get { return model; }
private set { model = value; }
}
private decimal price;
public decimal Price
{
get { return price; }
set
{
// Validate the incoming value
if (value < 0)
{
price = 0;
}
else
{
price = value;
}
}
}
public Car(string model)
{
// We can access the private set block only from within this class
Model = model;
}
}
  • Line 9: The private set modifier ensures that Model can only be set inside the Car class.

  • Line 19: The validation logic must check value (the incoming data), not price (the current field).

  • Line 33: The constructor sets Model internally, which is allowed because the constructor is part of the Car class.

Now, let’s verify the private set behavior.

C#
namespace Properties;
public class Car
{
private string model = string.Empty;
public string Model
{
get { return model; }
private set { model = value; }
}
private decimal price;
public decimal Price
{
get { return price; }
set
{
// Validate the incoming value
if (value < 0)
{
price = 0;
}
else
{
price = value;
}
}
}
public Car(string model)
{
// We can access the private set block only from within this class
Model = model;
}
}
  • Line 3: We initialize the Model via the constructor.

  • Line 6: Attempting to assign car.Model directly is commented out because the compiler prevents it, proving the access modifier works.

  • Line 10: We can still read car.Model because the get accessor 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 (set or get):

public string Name
{
private get; // Invalid, must also have the set block
}
An invalid property definition
  • 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
}
Attempting to modify both accessors invalidates the property
  • The access modifier of the set or get block must be more restrictive than the modifier attached to the property.

internal string Name
{
public get; // Cannot be public, because the property is internal
set;
}
An invalid attempt to make an accessor more visible than its property

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:

C# 14.0
namespace Properties;
public class Car
{
private string model = string.Empty;
public string Model
{
get { return model; }
set { model = value; }
}
private decimal price;
public decimal Price
{
get { return price; }
set { price = value; }
}
private int year;
public int Year
{
get { return year; }
set { year = value; }
}
}
  • 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; }
}
Conceptual representation of how the compiler handles auto-properties
  • 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 get and set accessors 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:

C# 14.0
namespace Properties;
public class Car
{
// Auto-implemented properties
public string Model { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Year { get; set; }
}
  • Line 6: Model is initialized to an empty string to satisfy null safety requirements.

  • Lines 7–8: Price and Year use 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 init accessor replaces the set accessor. It allows a property to be assigned only during object creation. Once the object is initialized, the property becomes fully immutable.

  • The required modifier forces the caller to set the property during initialization. This eliminates the need for bulky constructors simply to enforce mandatory data fields.

C# 14.0
namespace Properties;
public class Employee
{
// Must be initialized and cannot be changed afterward
public required string FirstName { get; init; }
public required string LastName { get; init; }
// Optional, but immutable once set
public string Department { get; init; } = "Unassigned";
}
  • Lines 6–7: The required keyword guarantees that any code creating an Employee must provide these values. The init keyword ensures they cannot be modified after creation.

  • Line 10: The Department property is not required, but the init accessor 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.

C# 14.0
using Properties;
// Valid initialization: all required properties are provided
Employee validEmployee = new Employee
{
FirstName = "Alice",
LastName = "Smith"
};
// The following lines would cause compilation errors:
// Error: Init-only property or indexer 'Employee.FirstName' can only be assigned in an object initializer.
validEmployee.FirstName = "Bob";
// Error: Required member 'Employee.LastName' must be set in the object initializer or attribute constructor.
Employee invalidEmployee = new Employee { FirstName = "Charlie" };
  • Lines 4–8: The object initializer provides all required properties, allowing the code to compile successfully.

  • Line 13: Attempting to change FirstName after initialization fails because it uses an init accessor.

  • Line 16: Attempting to create an Employee without providing the LastName fails because it is marked as required.

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 + .).

Automatic property generation in Visual Studio
Automatic property generation in Visual Studio