C# Properties: Encapsulation, init, and required Members

Learn C# Properties: Encapsulation, init, and required Members with practical examples, explanations, best practices, and real-world coding techniques.

Properties are a fundamental feature of C# that allow classes and structures to expose data through controlled accessors. They look similar to fields when used, but they provide greater flexibility because a type can validate values, calculate results, restrict modifications, or change its internal implementation without changing how callers interact with it.

For example, a product may have a name and price. A public field would allow other parts of an application to modify the price directly. A property can protect the object's state by rejecting negative prices while still allowing valid updates. This approach supports encapsulation, one of the core principles of object-oriented programming.

Understanding Properties in C#

A property is declared using a property type, a name, and one or more accessors. The get accessor retrieves a value, while the set accessor assigns a value. The contextual keyword value represents the value supplied to a setter.

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

    public decimal Salary
    {
        get;
        set;
    }
}

Employee employee = new Employee
{
    Name = "Alex",
    Salary = 45000m
};

Console.WriteLine(employee.Name);
Console.WriteLine(employee.Salary);

The Name and Salary properties can be read and assigned because both expose public getters and setters. The object initializer assigns the properties when creating the employee.

Properties are commonly used for model classes, application settings, API request and response models, and domain objects. Unlike fields, properties are accessed through accessor methods generated by the compiler, giving the implementation room to introduce behavior later.

Auto-Implemented Properties

When a property does not require custom logic, you can use an auto-implemented property. The compiler generates the backing storage automatically, so you do not need to declare a separate field.

public class Product
{
    public string Name { get; set; } = string.Empty;

    public decimal Price { get; set; }

    public bool IsAvailable { get; set; }
}

This class is simple and readable. However, an unrestricted public setter is not always appropriate. If a property represents a value that must always satisfy a business rule, consider a custom setter or a more controlled update method.

Auto-properties can also expose read-only state by providing only a getter. For example, a property with a private or constructor-assigned backing value can prevent outside callers from replacing the value directly.

Getters and Setters with Validation

A custom property accessor allows you to validate an incoming value before storing it. This is useful when a class must maintain invariants, meaning conditions that should remain true throughout the object's lifetime.

using System;

public sealed class Product
{
    private decimal _price;

    public required string Name { get; init; }

    public decimal Price
    {
        get => _price;
        set
        {
            if (value < 0)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(value),
                    "Price cannot be negative.");
            }

            _price = value;
        }
    }
}

var product = new Product
{
    Name = "Keyboard",
    Price = 49.99m
};

Console.WriteLine(product.Name);
Console.WriteLine(product.Price);

The Price setter rejects negative values instead of allowing the object to enter an invalid state. The Name property uses required and init, which are explained in the following sections.

Validation should be consistent with the domain's actual rules. Avoid silently replacing invalid input with a default value because the caller might not realize that the original value was rejected or changed.

Read-Only Properties

A read-only property exposes a value without allowing arbitrary assignment through the public API. One common approach is to use a getter-only auto-property and assign it in the constructor.

public sealed class Customer
{
    public int Id { get; }

    public string Name { get; }

    public Customer(int id, string name)
    {
        Id = id;
        Name = name;
    }
}

Customer customer = new Customer(101, "Jordan");

Console.WriteLine(customer.Id);
Console.WriteLine(customer.Name);

The constructor initializes the properties, and ordinary callers cannot assign new values to them after construction. This can be useful for identifiers and other values that should remain stable.

Read-only properties do not automatically make an entire object immutable. If a property returns a mutable collection or another mutable object, callers may still be able to change the state of that referenced object.

Init-Only Properties

The init accessor allows a property to be assigned during object initialization, including supported initialization through constructors and object initializers, but prevents ordinary reassignment after initialization is complete.

public sealed class ApiOptions
{
    public required Uri BaseUrl { get; init; }

    public int TimeoutSeconds { get; init; } = 30;
}

var options = new ApiOptions
{
    BaseUrl = new Uri("https://api.example.com"),
    TimeoutSeconds = 60
};

Console.WriteLine(options.BaseUrl);
Console.WriteLine(options.TimeoutSeconds);

// This does not compile after initialization:
// options.TimeoutSeconds = 90;

Init-only properties are useful for configuration objects, request models, and values that should be established at creation time. They allow object initializers to remain convenient without exposing unrestricted setters.

An init accessor is not a complete immutability guarantee. A property may reference a mutable object whose contents can still change, and initialization requirements do not replace runtime validation when values come from external input.

Required Properties in C#

The required modifier indicates that a member must be initialized when an object is created, subject to the language's required-member rules. It helps catch missing initialization during compilation.

public sealed class Registration
{
    public required string Username { get; init; }

    public required string Email { get; init; }

    public string Role { get; init; } = "User";
}

var registration = new Registration
{
    Username = "alex123",
    Email = "alex@example.com"
};

Console.WriteLine(registration.Username);
Console.WriteLine(registration.Role);

Both Username and Email must be initialized by the caller. The Role property has a default value and is optional in the object initializer.

The required modifier does not guarantee that a string is non-null or non-empty at runtime. Nullable reference type annotations help express nullability expectations, but external data can still violate those expectations. Use appropriate validation for user input, configuration, and deserialized payloads.

The required modifier was introduced in C# 11. Projects targeting older language versions may not support it unless their compiler configuration is updated appropriately.

Computed Properties

A computed property derives its value from other state instead of storing an independent value. Expression-bodied properties are a concise way to define simple calculations.

public sealed class Rectangle
{
    public double Width { get; init; }

    public double Height { get; init; }

    public double Area => Width * Height;

    public double Perimeter => 2 * (Width + Height);
}

var rectangle = new Rectangle
{
    Width = 5,
    Height = 3
};

Console.WriteLine(rectangle.Area);
Console.WriteLine(rectangle.Perimeter);

The Area and Perimeter properties calculate their results whenever they are accessed. This avoids maintaining duplicate state that could become inconsistent when the dimensions change.

Computed properties should generally be inexpensive and free of side effects. Database queries, network requests, and other expensive operations are better represented by explicit methods or asynchronous methods because callers should not expect a property access to trigger substantial work.

Private Setters and Controlled Updates

Sometimes a class needs to update its own properties while preventing outside callers from assigning arbitrary values. A private setter allows the containing class to perform assignments internally while exposing a public getter.

public sealed class BankAccount
{
    public decimal Balance { get; private set; }

    public BankAccount(decimal initialBalance)
    {
        if (initialBalance < 0)
        {
            throw new ArgumentOutOfRangeException(
                nameof(initialBalance));
        }

        Balance = initialBalance;
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount));
        }

        Balance += amount;
    }
}

var account = new BankAccount(100m);
account.Deposit(50m);

Console.WriteLine(account.Balance);

The balance cannot be assigned directly by external callers. Instead, callers use the Deposit method, which validates the amount before updating the account. In real financial applications, additional rules may be required for withdrawals, precision, concurrency, and transaction processing.

This illustrates an important design principle: when a state change has business meaning, a method can communicate that intention more clearly than a public setter.

Property Access Modifiers

C# allows getter and setter accessors to have different accessibility levels when the property's declaration permits it. This provides fine-grained control over who can read or modify a value.

public class Order
{
    public string Status { get; private set; } = "Pending";

    public void MarkAsShipped()
    {
        Status = "Shipped";
    }
}

The Status property is publicly readable, but only the containing class can set it directly. External callers must use an operation exposed by the class, such as MarkAsShipped, to change the state.

Other access modifiers, such as protected set and internal set, can be useful depending on inheritance and assembly boundaries. Choose the narrowest accessibility that supports the intended design.

Properties vs Fields

Feature Field Property
Purpose Stores a value directly Exposes a value through accessors
Validation No built-in assignment validation Can validate or transform assignments
Computed values Stores a value separately Can calculate a value on access
Access control Access applies to the field itself Getter and setter can have different accessibility

Fields are appropriate for internal storage, especially private backing fields. Properties are generally preferred for publicly exposed object state because they allow behavior and access rules to evolve without changing the calling syntax.

Common Mistakes and Best Practices

  • Exposing every setter publicly: Allow external assignment only when changing the value is a valid operation.
  • Confusing required with validation: Required members help enforce initialization at compile time but do not guarantee valid runtime data.
  • Using properties for expensive work: Keep getters predictable and inexpensive; use methods for substantial operations.
  • Duplicating derived state: Consider computed properties when a value can be reliably calculated from existing state.
  • Ignoring invalid states: Validate values at the boundary where they enter the object, and use methods for meaningful state transitions.
  • Assuming init makes everything immutable: Remember that referenced objects can still be mutable and that runtime validation remains important.

For a broader understanding of how properties fit into object-oriented design, explore C# classes and objects and C# interfaces and abstraction.

Key Takeaways

C# properties provide a controlled way to expose an object's state. Auto-properties reduce boilerplate, custom accessors support validation, read-only properties restrict modification, init enables initialization-only assignment, and required helps ensure important members are initialized.

Choose property accessors based on the rules of your application. Keep object invariants protected, use computed properties for simple derived values, and prefer explicit methods when state changes involve business logic. These practices make C# models safer and easier to maintain as applications grow.

Code Examples

using System;

public sealed class Product
{
    private decimal _price;

    public required string Name { get; init; }

    public decimal Price
    {
        get => _price;
        set
        {
            if (value < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(value));
            }

            _price = value;
        }
    }
}

var product = new Product
{
    Name = "Monitor",
    Price = 199.99m
};

Console.WriteLine($"{product.Name}: {product.Price:C}");
    
using System;

public sealed class ApiOptions
{
    public required Uri BaseUrl { get; init; }

    public int TimeoutSeconds { get; init; } = 30;
}

var options = new ApiOptions
{
    BaseUrl = new Uri("https://api.example.com"),
    TimeoutSeconds = 60
};

Console.WriteLine(options.BaseUrl);
Console.WriteLine(options.TimeoutSeconds);
    

Pro Tip

Use init-only properties for values that should be assigned during object creation, and use private setters or domain methods when state changes must follow business rules. Remember that required members do not replace runtime validation.

🎨 IgKnight Docs

Master HTML, CSS, JS, Python, .NET and more with easy-to-follow lessons and practical code examples.

© 2025 IgKnight Learning Platform. All rights reserved.