Exploring the Differences Between Classes and Records in C# and When to Use Each

Kratika
9 min readNov 17, 2023

In C#, classes have long been the go-to choice for defining types, encapsulating behavior, and organizing code. However, with the introduction of C# 9.0, a new kid on the block emerged — the record. In this blog post, we’ll delve into the distinctions between classes and records, when to use each, and practical examples of their syntax and implementation.

Classes in a Nutshell

Classes in C# are the fundamental building blocks for object-oriented programming. They encapsulate data and behavior, promoting encapsulation and modularity in code. Traditionally, classes have been mutable, allowing their properties to be modified after instantiation.

public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }

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

Records: The Newcomer with a Twist

C# records, introduced in C# 9.0, offer a concise syntax for defining immutable data types. Records are specifically designed for scenarios where data is the primary concern, providing built-in support for immutability, equality, and deconstruction.

public record PersonRecord(string FirstName, string LastName);

Key Differences Between Classes and Records

  1. Mutability:
  • Classes: By default, classes are mutable, allowing properties to be changed after instantiation.
  • Records: Records are immutable by default, meaning their properties are read-only and can only be set during initialization.

2. Value Equality:

  • Classes: Manual implementation of Equals and GetHashCode is required for value-based equality checks.
  • Records: Records automatically generate value-based equality methods, simplifying value comparisons.

3. Conciseness:

  • Classes: Writing classes can involve boilerplate code for properties, constructors, equality methods, etc.
  • Records: Records are more concise, automatically generating common methods based on the properties.

When to Use Classes and Records

Use Classes When:

  • Mutability is required.
  • Explicit control over equality methods is needed.
  • A complex behavior is associated with the type.
  • Backward compatibility is crucial in an existing codebase.

Use Records When:

  • Immutability is preferred.
  • Value-based equality is a priority.
  • Conciseness is important for data-centric types.
  • You want to reduce boilerplate code.

Practical Examples

Classes:

public class Rectangle
{
public double Length { get; set; }
public double Width { get; set; }

public Rectangle(double length, double width)
{
Length = length;
Width = width;
}
}

Records:

public record RectangleRecord(double Length, double Width);

How to create an object of a record?

Creation of objects of record types just like you create objects of class types in C#. Records are designed to be used similarly to classes, and you can instantiate them using the new keyword. Here's an example:

public record PersonRecord(string FirstName, string LastName);

class Program
{
static void Main()
{
// Creating an object of the record type
PersonRecord person = new PersonRecord("John", "Doe");

// Accessing properties of the record
Console.WriteLine($"First Name: {person.FirstName}, Last Name: {person.LastName}");
}
}

In this example, we define a record PersonRecord with properties FirstName and LastName. Then, in the Main method, we create an instance of the record and access its properties. The syntax for creating and using objects of record types is very similar to that of classes. Records provide a concise way to define and work with immutable data types in C#.

How to update the properties of a record as a dynamic value?

Assigning dynamic values to the properties of a record in the same way you would with a class. Since records are immutable by default, you can use the with expression to create a new record instance with updated property values. Here's an example:

public record PersonRecord(string FirstName, string LastName);

class Program
{
static void Main()
{
// Creating an object of the record type
PersonRecord person = new PersonRecord("John", "Doe");

// Displaying initial values
Console.WriteLine($"Original: {person.FirstName} {person.LastName}");

// Assigning dynamic values using 'with'
person = person with { FirstName = "Jane" };

// Displaying updated values
Console.WriteLine($"Updated: {person.FirstName} {person.LastName}");
}
}

In this example, we first create a PersonRecord instance with initial values ("John" and "Doe"). Then, we use the with expression to create a new record instance with an updated FirstName ("Jane"). The original record remains unchanged, and a new record with the modified property is created.

How to create an object of a record inside a class?

You can create an object of a record inside another class just like you would with any other class. Records, in this regard, behave similarly to classes. Here’s an example:

public record PersonRecord(string FirstName, string LastName);

public class AnotherClass
{
public void CreateRecordObject()
{
// Creating an object of the record type inside another class
PersonRecord person = new PersonRecord("John", "Doe");

// Accessing and displaying the record properties
Console.WriteLine($"First Name: {person.FirstName}, Last Name: {person.LastName}");
}
}

class Program
{
static void Main()
{
AnotherClass anotherClass = new AnotherClass();

// Calling a method in another class that creates a record object
anotherClass.CreateRecordObject();
}
}

In this example, the AnotherClass class has a method CreateRecordObject that creates an instance of the PersonRecord record and accesses its properties. In the Main method of the Program class, an object of AnotherClass is created, and its method is called to demonstrate creating a record object inside another class.

The ability to create and use record objects inside other classes allows for a modular and organized design in your C# applications.

Real-World Use Cases of Record:

C# records are well-suited for scenarios where immutable data structures, value-based equality, and concise code are beneficial. Here are some major use cases where records shine:

  1. DTOs (Data Transfer Objects):
  • Records are ideal for representing data transfer objects that carry data between different layers of an application. Their concise syntax is well-suited for scenarios where you mainly need to encapsulate data without behavior.
public record OrderDto(int OrderId, string ProductName, decimal Price);

2. API Responses:

  • When designing APIs, records can be used to represent response objects, providing a clear and immutable structure for the data returned from API endpoints.
public record ApiResponse(bool Success, string Message, object Data);

3. Immutable Models:

  • Records are a natural fit for modeling immutable data, such as geometric shapes, points, or any other scenario where the properties should not change once the instance is created.
public record Point(int X, int Y);

4. Configuration Objects:

  • Records can be used to represent configuration settings, providing a concise and immutable way to store and pass around configuration data.
public record AppConfig(string ApiKey, int TimeoutInSeconds);

5. Event Sourcing:

  • In event sourcing patterns, records can represent events that occurred in a system. The immutability and value-based equality of records align well with the idea of representing events as immutable state changes.
public record UserRegisteredEvent(string UserId, string Email, DateTime RegistrationDate);

6. Logging and Auditing:

  • When logging or auditing activities in an application, records can capture the details of an event or action in an immutable and readable manner.
public record LogEntry(DateTime Timestamp, string LogLevel, string Message);

7. Result Objects:

  • Records can be used to represent the result of an operation, providing a concise way to communicate success or failure along with additional information.
public record OperationResult(bool Success, string Message, object ResultData);

8. Value Objects in DDD (Domain-Driven Design):

  • In DDD, records can represent value objects — immutable objects without an identity. They are often used to model parts of the domain where equality is based on the values they hold.
public record Money(decimal Amount, string Currency);

9. Caching Keys:

  • When working with caching, records can be used to represent keys for caching purposes. The immutability of records ensures that the keys remain constant.
public record CacheKey(string UserId, string Operation);

10. Database Entities:

  • Records can be used to represent database entities, especially when immutability and value-based equality align with the requirements of the application.
public record Product(int ProductId, string Name, decimal Price);

These examples showcase scenarios where the simplicity and immutability provided by records make them a natural and effective choice. Records excel at modeling data-centric types and contribute to writing clean, maintainable code.

Advantages of Record

C# records bring several advantages to the table, enhancing code readability, maintainability, and expressiveness. Here are some key advantages of using records:

  1. Conciseness:
  • Records provide a concise syntax for defining immutable data types. With minimal code, you can declare properties, constructors, and other common members, reducing boilerplate code.
public record Person(string FirstName, string LastName);

2. Immutability by Default:

  • Records are designed to be immutable by default. This immutability encourages a functional programming style, where instances cannot be modified once created. This can simplify reasoning about code and prevent unintended side effects.
public record Point(int X, int Y);

3. Automatic Value-Based Equality:

  • Records automatically generate value-based equality checks, including Equals, GetHashCode, and ==/!= operators. This simplifies comparisons between instances based on their property values.
var point1 = new Point(1, 2);
var point2 = new Point(1, 2);

Console.WriteLine(point1 == point2); // true
Console.WriteLine(point1.Equals(point2)); // true

4. Deconstruction:

  • Records support deconstruction, allowing you to easily extract individual property values. This feature is especially handy when working with tuples or when you need to break down a record into its components.
var (x, y) = new Point(3, 4);

5. Built-in ToString Method:

  • Records automatically generate a meaningful ToString method that displays the property values, making debugging and logging more straightforward.
Console.WriteLine(new Point(1, 2));  // Output: Point { X = 1, Y = 2 }

6. With-Expression for Copying:

  • The with expression allows you to create new instances of a record with modified property values. This facilitates the creation of updated copies without changing the original instance.
var original = new Point(1, 2);
var modified = original with { X = 5 };

7. Pattern Matching:

  • Records seamlessly integrate with C# pattern matching, providing a powerful mechanism for working with complex data structures.
if (point is Point { X: 0, Y: 0 })
{
Console.WriteLine("Point is at the origin.");
}

8. Enhanced Readability:

  • Records improve code readability by emphasizing the intent of the type. This is especially beneficial in scenarios where data is the primary concern.
// Class
public class Coordinate
{
public int X { get; set; }
public int Y { get; set; }
}

// Record
public record Point(int X, int Y);

9. Simplified Declaration and Initialization:

  • With records, you can declare and initialize instances concisely, reducing the number of lines needed for common scenarios.
var point = new Point(1, 2);

10. Code Generation:

Records automatically generate common methods (such as equality checks and ToString) based on their properties, reducing the need for manual implementation and potential sources of errors.

Limitations of Record

While C# records offer several advantages, there are also some limitations and considerations that developers should be aware of. Here are some of the limitations of using records:

  1. Mutability of Properties:
  • Records are designed to be immutable by default, but if you explicitly declare a mutable property using init instead of readonly, that property becomes mutable. This can introduce unexpected mutability in your records.
public record PersonRecord
{
public string Name { get; init; } // Mutable
}

2. Inheritance:

  • Records support inheritance, but there are limitations. If a derived class overrides methods or properties from the base class, those overrides may not be inherited by a record that derives from the derived class. This can lead to unexpected behavior.
public class BaseClass
{
public virtual string Name => "Base";
}

public class DerivedClass : BaseClass
{
public override string Name => "Derived";
}

public record DerivedRecord : DerivedClass { }

3. Value-Based Equality:

  • While automatic value-based equality is a significant benefit, it might not always align with specific business rules. In some cases, you may need to provide custom equality logic by overriding the Equals method.
public record PersonRecord(string FirstName, string LastName)
{
public override bool Equals(object obj)
{
// Custom equality logic
}
}

4. Backward Compatibility:

  • If your project uses an older version of C# that doesn’t support records (C# 9.0 and later), you won’t be able to take advantage of this feature without upgrading your language version.

5. Tooling and Framework Support:

  • While major development tools and frameworks are likely to support records, it’s essential to ensure that your specific tooling and frameworks are up to date with the latest C# features.

6. Learning Curve:

  • Introducing records to a development team may require some learning and adjustment, especially if developers are more accustomed to traditional class-based approaches. It’s crucial to consider the team’s familiarity with records and provide appropriate training if needed.

Despite these limitations, C# records remain a powerful feature that simplifies code and enhances expressiveness, particularly in scenarios involving immutable data structures and value-based equality comparisons. Understanding these limitations and how they may impact your specific use case is essential for making informed design decisions.

Have questions or insights to share? Feel free to leave comments, engage with the community, and explore related articles on data management and .NET development.

Stay Connected

Don’t miss out on more informative articles and tutorials. Subscribe to our blog, and join the conversation. Your feedback and suggestions are always welcome as we continue to explore the world of .NET development together.

--

--

Kratika

Senior Software Developer specializing in .NET technologies, technical reviewer and a learner looking for latest concepts in all versions of .NET.