1. Introduction
The positional syntax of a record class is super handy for simple cases:
public record User(string Name, int Age);
But sometimes you get this burning desire to add extra methods to your record, custom properties, tweak access modifiers, or add logic to the constructor (like validation or "auto" data transformation). Sadly, with positional records, there's just nowhere to put that! Time to switch to explicit body syntax if:
- You want to extend your record with methods, properties, or extra logic.
- You need to control property behavior (setters, getters, inits, validation).
- You need to create multiple constructors for different ways of initializing.
- You want to add interfaces or implement special methods.
Building a record with a body
The syntax is a lot like a class. You know this form:
public class User
{
/* ... */
}
Now it's just a record:
public record User
{
// Explicitly defined properties
public string Name { get; init; }
public int Age { get; init; }
// Extra logic
public string GetGreeting()
{
return $"Hi, my name is {Name} and I'm {Age} years old!";
}
// Custom constructor
public User(string name, int age)
{
Name = name;
if (age < 0)
throw new ArgumentException("Age can't be negative!");
Age = age;
}
}
Fun fact: If you define your own properties explicitly, the compiler does NOT auto-generate properties from positional syntax. Everything's explicit, everything's under your control.
Hybrid option: mixed syntax
C# lets you mix both worlds: you can declare a positional record and add a body to it:
public record User(string Name, int Age)
{
public string GetGreeting()
{
return $"I'm {Name}, I'm {Age} years old!";
}
}
In this case, the Name and Age properties are still auto-generated via positional syntax, and your extra methods chill inside the body.
2. Differences from regular records — body nuances
- An explicit record lets you fully control constructors, properties, and methods.
- You can implement interfaces or add your own comparison logic if you have complex object identity rules.
- Unlike a simple positional record, to add new properties you just declare them in the record's body now.
// Rookie mistake!
public record User(string Name, int Age)
{
public string Name { get; init; } // ← conflict! Duplicate property declaration
}
Rookie mistakes: Some students try to write public record User(string Name, int Age) and also add public string Name { get; init; } in the body, thinking they're two different variables. Nope! That's a conflict (duplicate declaration). Either go fully positional, or define properties explicitly — don't mix them up.
Practical examples
We're still building that console app where the user creates orders. Let's say the Order class used to look like this:
public record Order(string Product, int Quantity, double Price);
Now suppose we need to validate the quantity (quantity can't be less than 1) and add an extra property for total cost:
public record Order
{
public string Product { get; init; }
public int Quantity { get; init; }
public double Price { get; init; }
public double TotalCost => Quantity * Price;
public Order(string product, int quantity, double price)
{
Product = product ?? throw new ArgumentNullException(nameof(product));
if (quantity < 1)
throw new ArgumentException("Quantity must be at least 1!");
Quantity = quantity;
Price = price;
}
public override string ToString()
=> $"Product: {Product}, Qty: {Quantity}, Total: {TotalCost}";
}
Notice we explicitly implemented the properties, made them init-only (immutable objects), and added auto-calculation of total cost — the code is way more flexible now!
Usage in the program:
var order = new Order("Bicycle", 2, 15000);
Console.WriteLine(order); // Product: Bicycle, Qty: 2, Total: 30000
3. record struct
Evolution of structs
Before record struct showed up, structs in C# were "simple workhorses" — fast to copy, stack-allocated, perfect for short “Parcel” data (like coordinates or colors). But they didn't have all the record goodies: no positional syntax, with expressions, value-based equality by default, or other “sweet treats”.
Now in C# you can declare structs record-style:
public record struct Point(int X, int Y);
So what does this actually give you?
- Auto-implemented Equals, GetHashCode, ToString — your struct can now compare and print itself nicely!
- with-cloning syntax: var p2 = p1 with { X = 10 };
- You can use positional or explicit body syntax.
Comparison: classic struct VS record struct
| struct | record struct | |
|---|---|---|
| Positional syntax | No | Yes |
| with-expression | No | Yes |
| Immutability | No (by default) | Yes (init) |
| Value-based equality | No (by default) | Yes |
| ToString | Standard | Pretty |
Explicit body syntax for record struct
It's just like a regular record, but struct:
public record struct Rectangle
{
public int Width { get; init; }
public int Height { get; init; }
public int Area => Width * Height;
public Rectangle(int width, int height)
{
Width = width > 0 ? width : throw new ArgumentException("Width > 0");
Height = height > 0 ? height : throw new ArgumentException("Height > 0");
}
public void Print()
{
Console.WriteLine($"Size: {Width} x {Height}, area: {Area}");
}
}
Usage example:
var rect = new Rectangle(10, 7);
rect.Print(); // Size: 10 x 7, area: 70
// Clone and change width, original stays the same
var wideRect = rect with { Width = 20 };
wideRect.Print(); // Size: 20 x 7, area: 140
Features of record struct
- It's still a struct — value type. It gets copied on assignment!
- All the record perks: value-based equality, with-cloning, pretty ToString.
- Recommended for small, compact, immutable data sets where you want to avoid heap allocations.
- You can declare positional parameters or explicitly "open up" the body.
4. Common mistakes and gotchas
Too much mutability: record struct doesn't make fields auto-immutable if you declare them as regular fields (like public int Value;). Use init setters for truly immutable structs!
Equality: If you add new fields manually (not in the positional syntax), only those fields in the constructor or with init setters participate in auto value-based equality.
Copying: This is a struct, so... everything gets copied! Don't confuse with reference-type records.
with-expression confusion: They always create a shallow copy, so they DON'T do deep copying of nested objects.
GO TO FULL VERSION