Skip to content

C# Records — Record Class, Record Struct, Positional Syntax, and With Expressions

DodaTech Updated 2026-06-28 8 min read

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

C# records are reference types designed for immutable data modeling, providing built-in value-based equality, concise positional construction syntax, and non-destructive mutation through with expressions.

What You'll Learn

You will master records in C#: record class syntax for immutable data models, record structs for value-type immutability, positional records for concise construction, with expressions for non-destructive mutation, value-based equality semantics, and the integration of records with pattern matching in .NET.

Why It Matters

Records solve a common problem in C#: creating simple data containers that are immutable, comparable by value, and easy to clone with modifications. Before records, developers wrote dozens of lines of boilerplate for Equals, GetHashCode, ToString, and copy constructors. Records eliminate this boilerplate and are now the standard way to model data in modern C#.

Real-World Use

DTOs (Data Transfer Objects) in ASP.NET Core APIs use records for request/response models. Domain events in event-driven architectures use records for immutable event data. Configuration objects use records for immutable settings. Functional Programming patterns in C# use records for discriminated unions. Entity Framework Core query results are often projected to records.

Learning Path

graph LR
    A["14: Interfaces"] --> B["15: Records"]
    B --> C["16: Structs"]
    C --> D["17: Strings"]
    D --> E["18: Arrays & Collections"]
    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

Record Class (Reference Type)

public record Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
    public int Age { get; init; }
}

// Object initializer syntax
var person1 = new Person
{
    FirstName = "Alice",
    LastName = "Smith",
    Age = 30
};

Positional Records

The most concise syntax for records:

public record Person(string FirstName, string LastName, int Age);

// Construction
var person1 = new Person("Alice", "Smith", 30);
var person2 = new Person("Bob", "Jones", 25);

// Deconstruction
var (first, last, age) = person1;
Console.WriteLine($"{first} {last}, {age}");  // Alice Smith, 30

// Accessing properties
Console.WriteLine(person1.FirstName);  // Alice

Positional records automatically generate:

  • Init-only properties for each parameter
  • A primary constructor
  • Deconstruct method
  • Value-based Equals, GetHashCode
  • ToString implementation
  • IEquatable<T> implementation

With Expressions (Non-Destructive Mutation)

Since records are immutable, with creates a new record with modified properties:

var alice = new Person("Alice", "Smith", 30);
var olderAlice = alice with { Age = 31 };

Console.WriteLine(alice);        // Person { FirstName = Alice, LastName = Smith, Age = 30 }
Console.WriteLine(olderAlice);   // Person { FirstName = Alice, LastName = Smith, Age = 31 }
Console.WriteLine(alice == olderAlice);  // False (different age)

// For positional records, you can also use named arguments
var bob = alice with { FirstName = "Bob", Age = 25 };
Console.WriteLine(bob);  // Person { FirstName = Bob, LastName = Smith, Age = 25 }

Value-Based Equality

Records compare by value, not by reference:

var p1 = new Person("Alice", "Smith", 30);
var p2 = new Person("Alice", "Smith", 30);

Console.WriteLine(p1 == p2);  // True (value equality)
Console.WriteLine(ReferenceEquals(p1, p2));  // False (different objects)

// Records implement IEquatable<T>
Console.WriteLine(p1.Equals(p2));  // True

// Records work with dictionaries/hashsets
var set = new HashSet<Person> { p1 };
Console.WriteLine(set.Contains(p2));  // True

Record Struct (C# 10)

Value-type records for performance-sensitive scenarios:

public readonly record struct Point(double X, double Y);

// Mutable record struct (rarely needed)
public record struct Vector2(double X, double Y)
{
    public double Magnitude => Math.Sqrt(X * X + Y * Y);
}

// Usage
var p1 = new Point(3, 4);
var p2 = p1 with { X = 5 };
Console.WriteLine(p1);  // Point { X = 3, Y = 4 }
Console.WriteLine(p2);  // Point { X = 5, Y = 4 }

Adding Methods and Properties

public record Circle(double Radius)
{
    // Computed property
    public double Area => Math.PI * Radius * Radius;
    public double Circumference => 2 * Math.PI * Radius;

    // Method
    public bool Contains(Point point) =>
        Math.Sqrt(point.X * point.X + point.Y * point.Y) <= Radius;
}

var circle = new Circle(5);
Console.WriteLine($"Area: {circle.Area:F2}");          // 78.54
Console.WriteLine(circle.Contains(new Point(3, 4)));   // True

Inheritance with Records

Records support inheritance hierarchies:

public abstract record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);
public record Cat(string Name, bool IsIndoor) : Animal(Name);

var pets = new Animal[]
{
    new Dog("Rex", "German Shepherd"),
    new Cat("Whiskers", true)
};

foreach (var pet in pets)
{
    Console.WriteLine(pet);
}

Expected output:

Dog { Name = Rex, Breed = German Shepherd }
Cat { Name = Whiskers, IsIndoor = True }

Records vs Classes vs Structs

// Class: mutable, reference type, reference equality
public class PersonClass
{
    public string Name { get; set; }
}

// Record: immutable, reference type, value equality
public record PersonRecord(string Name);

// Struct: mutable/immutable, value type, value equality
public struct PersonStruct
{
    public string Name { get; set; }
}

Customizing Records

public record Employee
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
    public decimal Salary { get; init; }

    // Custom constructor
    public Employee(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
        Salary = 50000m;
    }

    // Override auto-generated ToString
    public override string ToString()
    {
        return $"Employee: {FirstName} {LastName} (${Salary:N0})";
    }
}

Records with Validation

public record Temperature
{
    private double _celsius;

    public required double Celsius
    {
        get => _celsius;
        init => _celsius = value < -273.15
            ? throw new ArgumentException("Below absolute zero")
            : value;
    }

    public double Fahrenheit => Celsius * 9 / 5 + 32;
}

Common Mistakes

Mistake 1: Treating Records as Mutable

Records are designed for immutability. Using { get; set; } on record properties defeats their purpose. Use { get; init; } or positional syntax.

Mistake 2: Forgetting That Records Are Reference Types

Record class instances are stored on the heap and passed by reference. Use record struct for stack-allocated value types when performance matters.

Mistake 3: Assuming Positional Records Are Tuples

Records generate full types with names, methods, and metadata. Tuples are lightweight anonymous types. Choose records for formal data models, tuples for quick grouping.

Mistake 4: Modifying Record Properties After Construction

Init-only setters prevent modification after construction. Use with expressions to create modified copies instead.

Mistake 5: Not Overriding ToString When Needed

The auto-generated ToString includes all properties. For sensitive data (passwords, PII), override ToString to exclude or mask such properties.

Mistake 6: Deep Inheritance with Records

Records support inheritance but deep hierarchies violate the principle of simple data modeling. Keep record hierarchies shallow (1-2 levels max).

Practice Questions

  1. What are the key differences between a record class and a regular class?
  2. How does a with expression work? What does it create?
  3. What is the difference between positional record syntax and property-based record syntax?
  4. When would you choose a record struct over a record class?
  5. Write a record Order(int Id, string Customer, decimal Total, DateTime OrderDate) and demonstrate with expressions.

Challenge

Design a record hierarchy for a geometric shapes library with Shape, Circle(double Radius), Rectangle(double Width, double Height), and Triangle(double Base, double Height). Add computed Area properties using the init accessor pattern or expression-bodied members.

FAQ

Are records the same as DTOs?

Records are excellent for DTOs because of their immutability, value equality, and concise syntax. Many ASP.NET Core projects use records for request/response models.

Can records have private fields?

Yes. Records can have private fields, methods, and other members just like regular classes. The auto-generated members are for the positional parameters.

Do records work with Entity Framework Core?

Yes. EF Core supports records as entity types. However, records must have init-only properties, not positional properties, for EF Core to materialize them properly from queries.

How does serialization work with records?

System.Text.Json handles records like regular classes. Both property-based and positional records serialize correctly. Deserialization works because of the init-only setters.

Can I make a record mutable?

Yes, by using { get; set; } instead of { get; init; }, or by using a positional record struct without readonly. However, this contradicts the design intent of records.

Mini Project

Create an e-commerce order system using records:

public record Address(string Street, string City, string State, string ZipCode);
public record Product(string Sku, string Name, decimal UnitPrice);
public record OrderItem(Product Product, int Quantity)
{
    public decimal Total => Product.UnitPrice * Quantity;
}

public record Order
{
    public required int Id { get; init; }
    public required string CustomerName { get; init; }
    public required Address ShippingAddress { get; init; }
    public List<OrderItem> Items { get; init; } = new();
    public DateTime OrderDate { get; init; } = DateTime.UtcNow;
    public string Status { get; init; } = "Pending";

    public decimal Subtotal => Items.Sum(i => i.Total);
    public decimal Tax => Subtotal * 0.08m;
    public decimal Total => Subtotal + Tax;
}

// Creating an order
var address = new Address("123 Main St", "Portland", "OR", "97201");
var product1 = new Product("WIDG-001", "Widget", 19.99m);
var product2 = new Product("GADG-001", "Gadget", 49.99m);

var order = new Order
{
    Id = 1001,
    CustomerName = "Alice Johnson",
    ShippingAddress = address,
    Items = new List<OrderItem>
    {
        new(product1, 3),
        new(product2, 1)
    }
};

Console.WriteLine($"Order #{order.Id}");
Console.WriteLine($"Customer: {order.CustomerName}");
Console.WriteLine($"Status: {order.Status}");
Console.WriteLine($"Items: {order.Items.Count}");
Console.WriteLine($"Subtotal: {order.Subtotal:C}");
Console.WriteLine($"Tax: {order.Tax:C}");
Console.WriteLine($"Total: {order.Total:C}");

// Non-destructive mutation: update status
var shippedOrder = order with { Status = "Shipped" };
Console.WriteLine($"\nUpdated status: {shippedOrder.Status}");
Console.WriteLine($"Original status still: {order.Status}");

// Value equality
var sameOrder = order with { };
Console.WriteLine($"\nEqual to original: {order == sameOrder}");

// Deconstruction
var (id, customer, _, _, _, _) = order;
Console.WriteLine($"Deconstructed: #{id}, {customer}");

Expected output:

Order #1001
Customer: Alice Johnson
Status: Pending
Items: 2
Subtotal: $109.96
Tax: $8.80
Total: $118.76

Updated status: Shipped
Original status still: Pending

Equal to original: True
Deconstructed: #1001, Alice Johnson

What's Next

You have mastered records in C#. The next lesson covers structs: value types, readonly struct, ref struct, and comparison with record structs.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro