Examine automatically implemented properties

Completed

The standard syntax for defining a property in C# includes a get accessor and a set accessor. However, when no other logic is required in the accessors, you can use automatically implemented properties.

Automatically implemented properties

Automatically implemented properties make property-declaration more concise when no other logic is required in the property accessors. They also enable client code to create objects. When you declare a property as shown in the following example, the compiler creates a private, anonymous backing field that can only be accessed through the property's get and set accessors. init accessors can also be declared as automatically implemented properties.

The following example shows a simple class that has some automatically implemented properties:


// This class is mutable. Its data can be modified from
// outside the class.
public class Customer
{
    // Auto-implemented properties for trivial get and set
    public double TotalPurchases { get; set; }
    public string Name { get; set; }
    public int CustomerId { get; set; }

    // Constructor
    public Customer(double purchases, string name, int id)
    {
        TotalPurchases = purchases;
        Name = name;
        CustomerId = id;
    }

    // Methods
    public string GetContactInfo() { return "ContactInfo"; }
    public string GetTransactionHistory() { return "History"; }

    // .. Other methods, events, etc.
}

class Program
{
    static void Main()
    {
        // Initialize a new object.
        Customer cust1 = new Customer(4987.63, "Northwind", 90108);

        // Modify a property.
        cust1.TotalPurchases += 499.99;
    }
}

Automatically implemented and field backed properties declare a private instance backing field. You can initialize automatically implemented properties similarly to fields:


public string FirstName { get; set; } = "FirstName";

The Customer class shown in the previous example is mutable. Client code can change the values in objects after creation. In complex classes that contain significant behavior (methods) and data, it's often necessary to have public properties. However, for small classes or structs that just encapsulate a set of values (data) and have little or no behaviors, you should use one of the following options for making the objects immutable:

  • Declare only a get accessor (immutable everywhere except the constructor).
  • Declare a get accessor and an init accessor (immutable everywhere except during object construction).
  • Declare the set accessor as private (immutable to consumers).

You might need to add validation to an automatically implemented property. C# 13 adds field backed properties as a preview feature. You use the field keyword to access the compiler synthesized backing field of an automatically implemented property. For example, you could ensure that the FirstName property in the preceding example can't be set to null or the empty string:


public string FirstName 
{ 
    get; 
    set 
    { 
        field = (string.IsNullOrWhiteSpace(value) is false
            ? value
            : throw new ArgumentException(nameof(value), "First name can't be whitespace or null"));
    }
} = "FirstName";

This feature enables you to add logic to accessors without requiring you to explicitly declare the backing field. You use the field keyword to access the backing field generated by the compiler.

Important

The field keyword is a preview feature in C# 13. You must be using .NET 9 and set your <LangVersion> element to preview in your project file in order to use the field contextual keyword.

You should be careful using the field keyword feature in a class that has a field named field. The new field keyword shadows a field named field in the scope of a property accessor. You can either change the name of the field variable, or use the @ token to reference the field identifier as @field.

Properties with backing fields

You can mix the concept of a computed property with a private field and create a cached evaluated property. For example, update the FullName property so that the string formatting happens on the first access:


public class Person
{
    public Person() { }

    [SetsRequiredMembers]
    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public required string FirstName { get; init; }
    public required string LastName { get; init; }

    private string? _fullName;
    public string FullName
    {
        get
        {
            if (_fullName is null)
                _fullName = $"{FirstName} {LastName}";
            return _fullName;
        }
    }
}

This implementation works because the FirstName and LastName properties are readonly. People can change their name. Updating the FirstName and LastName properties to allow set accessors requires you to invalidate any cached value for fullName. You modify the set accessors of the FirstName and LastName property so the fullName field is calculated again:


public class Person
{
    private string? _firstName;
    public string? FirstName
    {
        get => _firstName;
        set
        {
            _firstName = value;
            _fullName = null;
        }
    }

    private string? _lastName;
    public string? LastName
    {
        get => _lastName;
        set
        {
            _lastName = value;
            _fullName = null;
        }
    }

    private string? _fullName;
    public string FullName
    {
        get
        {
            if (_fullName is null)
                _fullName = $"{FirstName} {LastName}";
            return _fullName;
        }
    }
}

This final version evaluates the FullName property only when needed. The previously calculated version is used if valid. Otherwise, the calculation updates the cached value. Developers using this class don't need to know the details of the implementation. None of these internal changes affect the use of the Person object.

Beginning with C# 13, you can create partial properties in partial classes. The implementing declaration for a partial property can't be an automatically implemented property. An automatically implemented property uses the same syntax as a declaring partial property declaration.

Implement a lightweight class with automatically implemented properties

You may encounter situations when you need to create an immutable lightweight class that serves only to encapsulate a set of automatically implemented properties. Use this kind of construct instead of a struct when you must use reference type semantics.

You can make an immutable property in the following ways:

  • Declare only the get accessor, which makes the property immutable everywhere except in the type's constructor.
  • Declare an init accessor instead of a set accessor, which makes the property settable only in the constructor or by using an object initializer.
  • Declare the set accessor to be private. The property is settable within the type, but it's immutable to consumers.

You can add the required modifier to the property declaration to force callers to set the property as part of initializing a new object.

The following example shows how a property with only get accessor differs than one with get and private set.


class Contact
{
    public string Name { get; }
    public string Address { get; private set; }

    public Contact(string contactName, string contactAddress)
    {
        // Both properties are accessible in the constructor.
        Name = contactName;
        Address = contactAddress;
    }

    // Name isn't assignable here. This will generate a compile error.
    //public void ChangeName(string newName) => Name = newName;

    // Address is assignable here.
    public void ChangeAddress(string newAddress) => Address = newAddress;
}

The following example shows two ways to implement an immutable class using automatically implemented properties. Each way declares one of the properties with a private set and one of the properties with a get only. The first class uses a constructor only to initialize the properties, and the second class uses a static factory method that calls a constructor.


// This class is immutable. After an object is created,
// it can't be modified from outside the class. It uses a
// constructor to initialize its properties.
class Contact
{
    // Read-only property.
    public string Name { get; }

    // Read-write property with a private set accessor.
    public string Address { get; private set; }

    // Public constructor.
    public Contact(string contactName, string contactAddress)
    {
        Name = contactName;
        Address = contactAddress;
    }
}

// This class is immutable. After an object is created,
// it can't be modified from outside the class. It uses a
// static method and private constructor to initialize its properties.
public class Contact2
{
    // Read-write property with a private set accessor.
    public string Name { get; private set; }

    // Read-only property.
    public string Address { get; }

    // Private constructor.
    private Contact2(string contactName, string contactAddress)
    {
        Name = contactName;
        Address = contactAddress;
    }

    // Public factory method.
    public static Contact2 CreateContact(string name, string address)
    {
        return new Contact2(name, address);
    }
}

public class Program
{
    static void Main()
    {
        // Some simple data sources.
        string[] names = ["Person One","Person Two", "Person Three",
                            "Person Four", "Person Five"];
        string[] addresses = ["123 Main St.", "345 Cypress Ave.", "678 1st Ave",
                                "12 108th St.", "89 E. 42nd St."];

        // Simple query to demonstrate object creation in select clause.
        // Create Contact objects by using a constructor.
        var query1 = from i in Enumerable.Range(0, 5)
                    select new Contact(names[i], addresses[i]);

        // List elements can't be modified by client code.
        var list = query1.ToList();
        foreach (var contact in list)
        {
            Console.WriteLine("{0}, {1}", contact.Name, contact.Address);
        }

        // Create Contact2 objects by using a static factory method.
        var query2 = from i in Enumerable.Range(0, 5)
                        select Contact2.CreateContact(names[i], addresses[i]);

        // Console output is identical to query1.
        var list2 = query2.ToList();

        // List elements can't be modified by client code.
        // CS0272:
        // list2[0].Name = "Person Six";
    }
}

/* Output:
    Person One, 123 Main St.
    Person Two, 345 Cypress Ave.
    Person Three, 678 1st Ave
    Person Four, 12 108th St.
    Person Five, 89 E. 42nd St.
*/

The compiler creates backing fields for each automatically implemented property. The fields aren't accessible directly from source code.