Skip to content

C# Classes — Class Syntax, Fields, Properties, Auto-Properties, and Init Setters

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about C# Classes. We cover key concepts, practical examples, and best practices to help you master this topic.

C# classes are reference types that encapsulate data (fields and properties) and behavior (methods) into reusable blueprints for creating objects in object-oriented programming.

What You'll Learn

You will master C# class design: defining classes with fields and properties, using auto-properties for concise getter/setter patterns, leveraging init-only setters for immutable object creation, expression-bodied members for compact syntax, and property validation with full property syntax.

Why It Matters

Classes are the foundation of object-oriented programming in .NET. Understanding property patterns — especially the distinction between fields, auto-properties, and init-only setters — is critical for designing clean APIs. Init-only setters, introduced in C# 9, enable immutable data models that work with object initializers, a pattern widely used in modern C# codebases.

Real-World Use

ASP.NET Core models use classes with properties for request/response DTOs. Entity Framework Core entities are classes with properties mapping to database columns. Configuration options classes use init-only setters for immutable settings. Complex business logic is organized into classes following SOLID principles.

Learning Path

graph LR
    A["08: Loops"] --> B["09: Classes"]
    B --> C["10: Constructors"]
    C --> D["11: Encapsulation"]
    D --> E["12: Inheritance"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style E fill:#4a90d9,stroke:#2c5f8a,color:#fff

Basic Class Structure

public class Person
{
    // Fields (private, backing storage)
    private string _name;
    private int _age;

    // Constructor
    public Person(string name, int age)
    {
        _name = name;
        _age = age;
    }

    // Methods
    public void Introduce()
    {
        Console.WriteLine($"Hi, I'm {_name} and I'm {_age} years old.");
    }
}

// Usage
var person = new Person("Alice", 30);
person.Introduce();

Expected output:

Hi, I'm Alice and I'm 30 years old.

Fields

Fields store data directly in a class. They are typically private to maintain Encapsulation:

public class BankAccount
{
    // Instance fields
    private string _accountNumber;
    private decimal _balance;
    private List<string> _transactions = new();

    // Static field (shared across all instances)
    private static int _nextAccountNumber = 1000;

    // Readonly field (set once in constructor)
    private readonly DateTime _createdAt;

    // Constant field (implicitly static)
    public const decimal MinimumBalance = 10m;

    public BankAccount(string accountNumber)
    {
        _accountNumber = accountNumber;
        _createdAt = DateTime.UtcNow;
    }
}

Properties

Properties expose fields with controlled access. They are the primary way to expose data in C#:

Auto-Properties (C# 3+)

The simplest form:

public class Product
{
    // Auto-properties: compiler generates backing field
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    // Read-only auto-property (no setter)
    public DateTime CreatedAt { get; } = DateTime.UtcNow;

    // Auto-property with private setter
    public int StockCount { get; private set; }
}

Full Property with Validation

public class Temperature
{
    private double _celsius;

    public double Celsius
    {
        get => _celsius;
        set
        {
            if (value < -273.15)
                throw new ArgumentException("Temperature cannot be below absolute zero");
            _celsius = value;
        }
    }

    // Computed property (no backing field)
    public double Fahrenheit => _celsius * 9 / 5 + 32;

    // Expression-bodied property (C# 6+)
    public string Description => Celsius switch
    {
        < 0 => "Freezing",
        < 20 => "Cold",
        < 30 => "Warm",
        _ => "Hot"
    };
}

Init-Only Setters (C# 9+)

Init-only setters enable immutable properties that can be set during object initialization but not afterward:

public class UserProfile
{
    public int Id { get; init; }
    public string Username { get; init; }
    public string Email { get; init; }
    public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
}

// Usage with object initializer (immutable after creation)
var profile = new UserProfile
{
    Id = 1,
    Username = "alice",
    Email = "alice@example.com"
};

// This would cause a compilation error:
// profile.Email = "new@example.com";  // Error: init-only property

Init-only properties are essential for immutable data models and work perfectly with record types.

Required Properties (C# 11+)

required modifier enforces that callers must initialize the property:

public class Config
{
    public required string ServerUrl { get; init; }
    public required int Port { get; init; }
    public int TimeoutSeconds { get; init; } = 30;
}

// Compilation error if ServerUrl or Port are omitted:
// var config = new Config();  // Error!
var config = new Config
{
    ServerUrl = "https://api.example.com",
    Port = 443
};

Expression-Bodied Members

Simplify methods and properties that consist of a single expression:

public class Circle
{
    public double Radius { get; }

    public Circle(double radius)
    {
        Radius = radius;
    }

    // Expression-bodied method
    public double Area() => Math.PI * Radius * Radius;

    // Expression-bodied property
    public double Circumference => 2 * Math.PI * Radius;

    // Expression-bodied static method
    public static Circle FromDiameter(double diameter) => new(diameter / 2);
}

Static Members

Static members belong to the type itself, not to any instance:

public class Counter
{
    // Static field
    private static int _totalCount;

    // Static property
    public static int TotalCount => _totalCount;

    // Instance method
    public int InstanceId { get; }

    public Counter()
    {
        _totalCount++;
        InstanceId = _totalCount;
    }

    // Static method
    public static void Reset() => _totalCount = 0;
}

var c1 = new Counter();
var c2 = new Counter();
Console.WriteLine($"Instance IDs: {c1.InstanceId}, {c2.InstanceId}");
Console.WriteLine($"Total: {Counter.TotalCount}");  // 2

Nested Classes

public class Library
{
    public class Book
    {
        public string Title { get; set; }
        public string Author { get; set; }
    }

    private List<Book> _books = new();
    public void AddBook(string title, string author)
    {
        _books.Add(new Book { Title = title, Author = author });
    }
}

Partial Classes

Split a class definition across multiple files (used extensively with source generators):

// File1.cs
public partial class Employee
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

// File2.cs
public partial class Employee
{
    public string FullName => $"{FirstName} {LastName}";
}

Common Mistakes

Mistake 1: Public Fields Instead of Properties

Always use properties, not public fields. Properties support validation, data binding, versioning, and can be defined in interfaces. Fields are implementation details.

Mistake 2: Throwing Exceptions From Auto-Property Getters

Getters should not throw exceptions or have side effects. Use a method if computation is expensive or can fail.

Mistake 3: Forgetting to Initialize Collection Properties

// Dangerous: _items could be null
public List<string> Items { get; set; }

// Safe: initialize inline
public List<string> Items { get; set; } = new();

Mistake 4: Using Mutable Properties for Value Objects

Value objects should be immutable. Use init setters or record types instead of mutable get; set; properties.

Mistake 5: Exposing Internal State via Reference Properties

Returning a List<T> property gives external code a reference to your internal list. Use IReadOnlyList<T> or return a copy.

Mistake 6: Overusing Static Classes

Static classes cannot implement interfaces, be passed as parameters, or be mocked for testing. Prefer instance classes with Dependency Injection.

Practice Questions

  1. What is the difference between a field and a property in C#?
  2. How does an init-only setter differ from a regular setter?
  3. Why are public fields considered bad practice in C#?
  4. Write a class Rectangle with auto-properties for Width and Height, a computed property for Area, and validation that prevents negative values.
  5. What is the required modifier and when would you use it?

Challenge

Design an immutable Order class with init-only properties (OrderId, CustomerName, Items, OrderDate). Include a computed property for Total. Demonstrate that properties cannot be modified after initialization.

FAQ

What is the difference between `{ get; set; }` and `{ get; private set; }`?

Both create auto-properties. The first allows anyone to read and write. The second allows anyone to read but only the class to write. The compiler generates different accessibility for the setter.

Can properties be virtual?

Yes. Properties can be marked virtual, abstract, or override. This enables polymorphic property behavior in derived classes.

Do init-only setters work at runtime or only compile-time?

Init-only setters are enforced at compile time, but the check is also emitted as a .NET module initializer for runtime verification. Reflection can bypass init restrictions.

What is the difference between `readonly` field and `init` property?

A readonly field can only be set in the constructor. An init property can also be set via object initializer syntax. Init properties work with records and DTO patterns.

Can I have both a getter and setter with different access levels?

Yes. public int Id { get; private set; } has a public getter and private setter. The setter must have more restrictive access than the getter.

Mini Project

Create a simple inventory management system:

public class InventoryItem
{
    public required string Sku { get; init; }
    public required string Name { get; set; }
    public string Category { get; set; } = "Uncategorized";
    public int Stock { get; private set; }
    public decimal Price { get; set; }
    public DateTime AddedDate { get; init; } = DateTime.UtcNow;

    public decimal TotalValue => Stock * Price;

    public void AddStock(int quantity)
    {
        if (quantity <= 0)
            throw new ArgumentException("Quantity must be positive");
        Stock += quantity;
    }

    public bool RemoveStock(int quantity)
    {
        if (quantity <= 0)
            throw new ArgumentException("Quantity must be positive");
        if (quantity > Stock) return false;
        Stock -= quantity;
        return true;
    }
}

public class Inventory
{
    private List<InventoryItem> _items = new();

    public void AddItem(InventoryItem item) => _items.Add(item);
    public int TotalItems => _items.Sum(i => i.Stock);
    public decimal TotalValue => _items.Sum(i => i.TotalValue);

    public void Display()
    {
        foreach (var item in _items)
        {
            Console.WriteLine(
                $"{item.Sku}: {item.Name} | Stock: {item.Stock} | " +
                $"Price: {item.Price:C} | Value: {item.TotalValue:C}");
        }
        Console.WriteLine($"\nTotal items: {TotalItems}");
        Console.WriteLine($"Total value: {TotalValue:C}");
    }
}

var inv = new Inventory();
inv.AddItem(new InventoryItem { Sku = "LAP001", Name = "Laptop", Price = 999.99m });
inv.AddItem(new InventoryItem { Sku = "MOU002", Name = "Mouse", Price = 24.99m });

inv.InventoryItems[0].AddStock(5);
inv.InventoryItems[1].AddStock(20);
inv.InventoryItems[1].RemoveStock(3);

inv.Display();

Expected output:

LAP001: Laptop | Stock: 5 | Price: $999.99 | Value: $4,999.95
MOU002: Mouse | Stock: 17 | Price: $24.99 | Value: $424.83

Total items: 22
Total value: $5,424.78

What's Next

You have mastered class design in C# including properties, init-only setters, and expression-bodied members. The next lesson covers constructors: default, parameterized, static, and primary constructors.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro