Records to największa rewolucja w C# od LINQ! W 2015 roku do stworzenia immutable data class potrzebowałeś 50+ linii kodu. W 2026 roku? Jedna linia: record Person(string Name, int Age);
Records (C# 9+) to typy zaprojektowane dla immutable data - danych które się nie zmieniają. Mają wbudowane: value-based equality, with expressions, deconstruction, i ToString(). Idealne dla DTO, messages, events, API responses!
📅 Timeline - ewolucja Records w C#
C# 1.0-8.0 (do 2019) - brak records, ręczne immutable classes
C# 9 (2020) - 🔥 Records! Positional records, with expressions
// Immutable class w 2015 - 50+ linii boilerplate! 😱
public class Person
{
private readonly string _firstName;
private readonly string _lastName;
private readonly int _age;
public Person(string firstName, string lastName, int age)
{
_firstName = firstName;
_lastName = lastName;
_age = age;
}
public string FirstName => _firstName;
public string LastName => _lastName;
public int Age => _age;
// Equals - musisz ręcznie zaimplementować!
public override bool Equals(object? obj)
{
if (obj is not Person other) return false;
return _firstName == other._firstName &&
_lastName == other._lastName &&
_age == other._age;
}
// GetHashCode - musisz ręcznie!
public override int GetHashCode()
{
return HashCode.Combine(_firstName, _lastName, _age);
}
// ToString - musisz ręcznie!
public override string ToString()
{
return $"Person {{ FirstName = {_firstName}, LastName = {_lastName}, Age = {_age} }}";
}
// "With" method - do tworzenia kopii ze zmianami
public Person WithFirstName(string firstName)
{
return new Person(firstName, _lastName, _age);
}
public Person WithLastName(string lastName)
{
return new Person(_firstName, lastName, _age);
}
public Person WithAge(int age)
{
return new Person(_firstName, _lastName, age);
}
}
// 55 linii kodu! 😱 A to tylko 3 properties!
❌ Class - 55 linii boilerplate
public class Person
{
private readonly string _firstName;
private readonly string _lastName;
private readonly int _age;
public Person(string firstName, string lastName, int age)
{
_firstName = firstName;
_lastName = lastName;
_age = age;
}
public string FirstName => _firstName;
public string LastName => _lastName;
public int Age => _age;
public override bool Equals(object? obj) { /*...*/ }
public override int GetHashCode() { /*...*/ }
public override string ToString() { /*...*/ }
public Person WithFirstName(string firstName) { /*...*/ }
public Person WithLastName(string lastName) { /*...*/ }
public Person WithAge(int age) { /*...*/ }
}
// 55+ linii! 😱
✅ Record - 1 linia! 🔥
public record Person(string FirstName, string LastName, int Age);
// To WSZYSTKO! 🎉
// - Immutable properties (init-only)
// - Value-based Equals/GetHashCode
// - ToString() z nazwami properties
// - with expression dla kopii
// - Deconstruction
// 1 linia zamiast 55! ✨
🔥 Positional Records - najprostsza forma (C# 9)
Podstawowa składnia
🎉 C# 9 - Positional Records
record Person(string Name, int Age); - jedna linia tworzy kompletny immutable typ!
// Positional record - najkrótsza forma
public record Person(string FirstName, string LastName, int Age);
// Kompilator generuje AUTOMATYCZNIE:
// 1. Init-only properties: FirstName, LastName, Age
// 2. Primary constructor: Person(string, string, int)
// 3. Deconstruction: (firstName, lastName, age) = person
// 4. Value-based Equals/GetHashCode
// 5. ToString() → "Person { FirstName = Jan, LastName = Kowalski, Age = 30 }"
// 6. with expression support
// 7. IEquatable implementation
// Użycie
var person = new Person("Jan", "Kowalski", 30);
Console.WriteLine(person.FirstName); // Jan
Console.WriteLine(person.Age); // 30
Console.WriteLine(person); // Person { FirstName = Jan, LastName = Kowalski, Age = 30 }
// person.Age = 31; // ❌ BŁĄD - properties są init-only (immutable)
Value-based equality - automatyczne!
// Records mają value-based equality z automatu!
var person1 = new Person("Jan", "Kowalski", 30);
var person2 = new Person("Jan", "Kowalski", 30);
var person3 = new Person("Anna", "Nowak", 25);
// Value equality - porównują WARTOŚCI, nie referencje!
Console.WriteLine(person1 == person2); // true - te same wartości! ✨
Console.WriteLine(person1.Equals(person2)); // true
Console.WriteLine(person1 == person3); // false - inne wartości
// Klasy (bez override Equals) - reference equality
class PersonClass
{
public string Name { get; init; }
public int Age { get; init; }
}
var p1 = new PersonClass { Name = "Jan", Age = 30 };
var p2 = new PersonClass { Name = "Jan", Age = 30 };
Console.WriteLine(p1 == p2); // false - różne referencje! 😱
// Musisz ręcznie override Equals/GetHashCode w klasach!
ToString() - automatyczny i czytelny!
// ToString() jest automatycznie generowany z nazwami properties
var person = new Person("Jan", "Kowalski", 30);
Console.WriteLine(person);
// Output: Person { FirstName = Jan, LastName = Kowalski, Age = 30 }
// Idealny do debugowania! ✨
// W klasach musisz ręcznie:
class PersonClass
{
public string Name { get; init; }
public int Age { get; init; }
public override string ToString() => $"PersonClass {{ Name = {Name}, Age = {Age} }}";
// Musisz pamiętać aktualizować przy dodawaniu properties!
}
Deconstruction - automatyczny!
// Records mają automatyczną deconstruction
var person = new Person("Jan", "Kowalski", 30);
// Deconstruct do tuple
var (firstName, lastName, age) = person;
Console.WriteLine($"{firstName} {lastName}, {age}"); // Jan Kowalski, 30
// Możesz pominąć niektóre wartości
var (first, _, personAge) = person; // pomijamy lastName
// Świetne w LINQ
var people = new[]
{
new Person("Jan", "Kowalski", 30),
new Person("Anna", "Nowak", 25)
};
foreach (var (first, last, _) in people)
{
Console.WriteLine($"{first} {last}");
}
// Klasy NIE mają automatycznej deconstruction - musisz ręcznie:
class PersonClass
{
public void Deconstruct(out string name, out int age)
{
name = Name;
age = Age;
}
}
with expressions - immutable updates (C# 9)
Problem z immutable types
// Immutable typ - nie możesz modyfikować
var person = new Person("Jan", "Kowalski", 30);
// person.Age = 31; // ❌ BŁĄD - init-only
// Chcesz zmienić Age - musisz stworzyć NOWY obiekt
var olderPerson = new Person(person.FirstName, person.LastName, 31);
// Verbose - musisz przekopiować wszystkie inne properties! 😱
Rozwiązanie - with expression!
🎉 C# 9 - with expressions
with { Property = value } - tworzy kopię ze zmianami!
// with expression - "non-destructive mutation"
var person = new Person("Jan", "Kowalski", 30);
// Zmień tylko Age - reszta zostaje taka sama!
var olderPerson = person with { Age = 31 };
Console.WriteLine(person); // Person { FirstName = Jan, LastName = Kowalski, Age = 30 }
Console.WriteLine(olderPerson); // Person { FirstName = Jan, LastName = Kowalski, Age = 31 }
// Zmień wiele properties naraz
var different = person with
{
FirstName = "Anna",
Age = 25
};
Console.WriteLine(different); // Person { FirstName = Anna, LastName = Kowalski, Age = 25 }
// Łańcuchowanie with
var modified = person
.with { Age = 31 }
.with { LastName = "Nowak" };
❌ Bez with - ręczne kopiowanie
// Immutable class bez with
var person = new Person("Jan", "Kowalski", 30);
// Chcesz zmienić Age? Musisz:
var olderPerson = new Person(
person.FirstName, // przekopiuj
person.LastName, // przekopiuj
31 // zmień
);
// Dla 10 properties musisz przekopiować 9! 😱
✅ with expression - zmień tylko co trzeba
// Record z with
var person = new Person("Jan", "Kowalski", 30);
// Zmień tylko Age
var olderPerson = person with { Age = 31 };
// Reszta się kopiuje automatycznie! ✨
Praktyczne przykłady with
// Przykład 1: Update user profile
record UserProfile(string Email, string Name, string Bio, DateTime LastLogin);
var profile = new UserProfile(
"jan@example.com",
"Jan Kowalski",
"C# developer",
DateTime.Now
);
// Update tylko LastLogin
var updated = profile with { LastLogin = DateTime.Now };
// Przykład 2: Configuration changes
record AppConfig(string ApiUrl, int Timeout, bool EnableLogging, string Version);
var prodConfig = new AppConfig(
"https://api.production.com",
30,
false,
"1.0.0"
);
// Dev config - zmień tylko URL i logging
var devConfig = prodConfig with
{
ApiUrl = "https://api.dev.com",
EnableLogging = true
};
// Przykład 3: Event sourcing - history of changes
record OrderState(int Id, string Status, decimal Total, DateTime UpdatedAt);
var initialState = new OrderState(1, "Pending", 100m, DateTime.Now);
var paidState = initialState with { Status = "Paid", UpdatedAt = DateTime.Now };
var shippedState = paidState with { Status = "Shipped", UpdatedAt = DateTime.Now };
// Każdy stan jest immutable - perfekt dla event sourcing! ✨
Record types - record class vs record struct (C# 10)
Record class - reference type (domyślny)
// C# 9 - "record" = "record class" (reference type)
public record Person(string Name, int Age);
// Równoważne:
public record class Person(string Name, int Age);
// To reference type - heap allocation
var person1 = new Person("Jan", 30);
var person2 = person1; // kopiuje REFERENCJĘ
person1 = person1 with { Age = 31 };
Console.WriteLine(person1.Age); // 31
Console.WriteLine(person2.Age); // 30 (różne obiekty - person1 wskazuje na nowy)
// Może być null
Person? nullablePerson = null;
🔥 Record struct - value type (C# 10)
🎉 C# 10 - Record structs
record struct - records jako value types (stack allocation)!
// C# 10 - record struct (value type)
public record struct Point(double X, double Y);
// Value type - stack allocation (jeśli możliwe)
var p1 = new Point(10, 20);
var p2 = p1; // kopiuje WARTOŚĆ (całą strukturę)
p1 = p1 with { X = 15 };
Console.WriteLine(p1.X); // 15
Console.WriteLine(p2.X); // 10 (p2 to niezależna kopia)
// Nie może być null (chyba że Point?)
// Point? nullablePoint = null; // wymaga ?
// Kiedy używać record struct?
// - Małe typy (< 16 bajtów)
// - Często przekazywane przez wartość
// - Nie potrzebujesz null
// - Krótki lifetime
Readonly record struct - fully immutable
// Readonly record struct - w pełni immutable
public readonly record struct Vector(double X, double Y, double Z);
// Properties są read-only (get-only)
var vector = new Vector(1, 2, 3);
// vector.X = 5; // ❌ BŁĄD - readonly!
// Można używać with
var scaled = vector with { X = 2, Y = 4, Z = 6 };
// readonly struct jest bardziej wydajny (może być passed by reference bez kopiowania)
Mutable record struct (ostrożnie!)
// Record struct BEZ readonly - mutable (niezalecane!)
public record struct MutablePoint(double X, double Y)
{
// Properties mają set, nie init!
public double X { get; set; } = X;
public double Y { get; set; } = Y;
}
var point = new MutablePoint(10, 20);
point.X = 15; // ✅ OK - mutable
// ⚠️ Uważaj - value type semantics + mutability = confusion!
var p2 = point;
p2.X = 100; // modyfikuje KOPIĘ, nie oryginał
Console.WriteLine(point.X); // 15 (nie zmienione!)
// Zalecenie: używaj readonly record struct dla immutability
Feature
record class
record struct
readonly record struct
Typ
Reference type
Value type
Value type
Alokacja
Heap
Stack (zwykle)
Stack (zwykle)
Nullable domyślnie
Tak (nullable ref)
Nie
Nie
Mutability properties
init-only
Mutable (set)
Read-only (get)
with expression
✅
✅
✅
Value equality
✅
✅
✅
ToString()
✅
✅
✅
Kiedy używać
DTOs, events, messages
Małe typy, coordinates
Fully immutable small types
Nominal records - dodatkowe members
Positional vs Nominal syntax
// Positional record - kompaktowy (poznany wcześniej)
public record Person(string Name, int Age);
// Nominal record - pełna składnia z body
public record Person
{
public string Name { get; init; }
public int Age { get; init; }
}
// Możesz łączyć oba!
public record Person(string Name, int Age)
{
// Dodatkowe computed properties
public bool IsAdult => Age >= 18;
// Dodatkowe methods
public void Introduce() =>
Console.WriteLine($"Hi, I'm {Name}, {Age} years old.");
}
Dodatkowe properties i methods
// Record z dodatkowymi members
public record Product(string Name, decimal Price, string Category)
{
// Computed property
public decimal PriceWithTax => Price * 1.23m;
// Mutable property (może się zmieniać!)
public int Stock { get; set; }
// Init-only property (bez primary constructor)
public string? Description { get; init; }
// Method
public bool IsInStock() => Stock > 0;
// Static method
public static Product CreateSample() =>
new("Sample", 99.99m, "Test") { Stock = 10 };
}
var product = new Product("Laptop", 2999.99m, "Electronics")
{
Description = "High-end laptop",
Stock = 5
};
Console.WriteLine(product.PriceWithTax); // 3689.99
product.Stock = 4; // ✅ OK - mutable property
// product.Price = 2499.99m; // ❌ BŁĄD - init-only
Validation w records
// Walidacja w primary constructor nie działa bezpośrednio
// Rozwiązanie 1: Dodatkowy constructor
public record Email(string Value)
{
public Email(string value) : this(Validate(value))
{
}
private static string Validate(string value)
{
if (!value.Contains('@'))
throw new ArgumentException("Invalid email");
return value;
}
}
// Rozwiązanie 2: Property z field keyword (C# 14)
public record Email
{
public required string Value
{
get => field;
init => field = value.Contains('@')
? value
: throw new ArgumentException("Invalid email");
}
}
// Rozwiązanie 3: Property override w positional record
public record Person(string Name, int Age)
{
// Override Age z walidacją
public int Age
{
get => field;
init => field = value >= 0 && value <= 150
? value
: throw new ArgumentOutOfRangeException(nameof(Age));
} = Age; // Inicjalizuj z parametru
}
var person = new Person("Jan", 30); // ✅ OK
// var invalid = new Person("Jan", 200); // ❌ Exception!
Inheritance w records
Records mogą dziedziczyć z innych records
// Base record
public record Person(string Name, int Age);
// Derived record - dodaje property
public record Employee(string Name, int Age, string Department)
: Person(Name, Age);
var employee = new Employee("Jan", 30, "IT");
Console.WriteLine(employee.Name); // Jan (z Person)
Console.WriteLine(employee.Department); // IT (nowe)
Console.WriteLine(employee);
// Employee { Name = Jan, Age = 30, Department = IT }
// with działa z wszystkimi properties
var older = employee with { Age = 31 };
var moved = employee with { Department = "HR" };
Abstract records
// Abstract base record
public abstract record Shape(string Color)
{
public abstract double Area { get; }
}
// Derived records
public record Circle(string Color, double Radius) : Shape(Color)
{
public override double Area => Math.PI * Radius * Radius;
}
public record Rectangle(string Color, double Width, double Height) : Shape(Color)
{
public override double Area => Width * Height;
}
// Użycie
Shape circle = new Circle("Red", 5);
Shape rectangle = new Rectangle("Blue", 10, 20);
Console.WriteLine(circle.Area); // ~78.5
Console.WriteLine(rectangle.Area); // 200
// Value equality działa z inheritance
var circle2 = new Circle("Red", 5);
Console.WriteLine(circle == circle2); // true - te same wartości!
Sealed records
// Sealed record - nie można dziedziczyć
public sealed record Product(string Name, decimal Price);
// ❌ To nie zadziała:
// public record DiscountedProduct(string Name, decimal Price, decimal Discount)
// : Product(Name, Price); // BŁĄD - Product jest sealed
// Zalecenie: większość records powinna być sealed
// (chyba że jawnie projektujesz hierarchię)
Kiedy używać records vs classes?
Records - use cases
🔍 Używaj records gdy:
✅ Data Transfer Objects (DTOs) - przenoszenie danych między warstwami
✅ API requests/responses - modele HTTP
✅ Messages/Events - event sourcing, CQRS
✅ Immutable data - wartości które się nie zmieniają
✅ Value objects - Money, Email, Address
✅ Configuration - ustawienia aplikacji
✅ Test data - arrange phase w testach
✅ Potrzebujesz value equality (porównanie wartości)
✅ Chcesz with expressions
💡 Przykłady - records w praktyce
// 1. API Response DTO
public record UserResponse(
int Id,
string Email,
string Name,
DateTime CreatedAt
);
// 2. HTTP Request
public record CreateOrderRequest(
int UserId,
List Items,
string ShippingAddress
);
// 3. Domain Event (Event Sourcing)
public record OrderPlacedEvent(
Guid OrderId,
int UserId,
decimal Total,
DateTime PlacedAt
);
// 4. Value Object
public record Money(decimal Amount, string Currency)
{
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Currency mismatch");
return this with { Amount = Amount + other.Amount };
}
}
// 5. Configuration
public record DatabaseConfig(
string ConnectionString,
int MaxConnections,
int TimeoutSeconds
);
// 6. Test data
var testUser = new UserResponse(1, "test@example.com", "Test User", DateTime.Now);
var modifiedUser = testUser with { Name = "Modified" };
Classes - use cases
🔍 Używaj classes gdy:
✅ Mutable state - obiekt się zmienia w czasie
✅ Identity-based - ważna jest tożsamość, nie wartość (User, Order)
Records to największa rewolucja w C# data modeling! Od 55+ linii boilerplate do 1 linii kodu:
✅ 🔥 Positional records (C# 9) - jedna linia zamiast 55!
✅ Value equality - automatyczne Equals/GetHashCode
✅ ToString() - czytelny output z nazwami properties
✅ Deconstruction - automatyczny tuple pattern
✅ with expressions (C# 9) - immutable updates
✅ 🔥 Record structs (C# 10) - records jako value types
✅ Readonly record struct - fully immutable structs
✅ Nominal syntax - dodatkowe members w records
✅ Inheritance - records mogą dziedziczyć
✅ Use cases - DTOs, events, value objects, API models
W kolejnym wpisie poznasz Properties advanced patterns - computed properties, lazy initialization, property injection!
Zadanie dla Ciebie 🎯
Przepisz stare DTOs na records:
// STARE DTOs (2015):
public class UserDto
{
public int Id { get; set; }
public string Email { get; set; }
public string Name { get; set; }
public DateTime CreatedAt { get; set; }
public override bool Equals(object obj)
{
// ... ręczna implementacja
}
public override int GetHashCode()
{
// ... ręczna implementacja
}
}
public class OrderDto
{
public int Id { get; set; }
public int UserId { get; set; }
public List Items { get; set; }
public decimal Total { get; set; }
public string Status { get; set; }
}
public class OrderItemDto
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
}
Przepisz na records używając:
Positional records
Computed property dla TotalPrice w OrderItemDto
Computed property dla ItemCount w OrderDto
with expression do zmiany Status
Deconstruction w LINQ queries
🎯 BONUS: Event Sourcing System
Stwórz event sourcing system dla e-commerce używając records!
Events do stworzenia (jako records):
OrderEvents:
OrderPlaced(Guid OrderId, int UserId, List<OrderItem> Items, DateTime Timestamp)