DEV Community

Maria
Maria

Posted on

C# Records and Pattern Matching: Modern Data Modeling

C# Records and Pattern Matching: Modern Data Modeling

When it comes to building robust, maintainable software, how we model our data matters. In the past, crafting immutable, expressive, and concise domain models in C# often required extra boilerplate code and workarounds. But with the advent of C# 9 and subsequent releases, the introduction of records and improved pattern matching features has revolutionized how developers approach data modeling. These modern tools allow us to write clean, elegant, and powerful code with ease.

In this post, we’ll dive deep into C# records and pattern matching, exploring how they can transform your development process. You’ll learn how to build immutable data structures, leverage pattern matching for expressive domain logic, and avoid common pitfalls along the way.


Why Should You Care About Records and Pattern Matching?

Imagine you're developing an application to manage orders for an e-commerce platform. You need to model domain entities like Customer, Order, and Product. These entities should:

  1. Be immutable to ensure thread safety and predictability.
  2. Have value-based equality to compare objects based on their data rather than references.
  3. Be expressive and concise to reduce boilerplate code.

Traditionally, achieving these goals in C# meant implementing a lot of repetitive code: manually overriding Equals, GetHashCode, and ToString, and writing constructors to enforce immutability. But with records, this boilerplate becomes a thing of the past.

Couple that with pattern matching, and you gain the ability to write expressive, declarative logic for handling complex scenarios—no more nested if statements or verbose type checks.


What Are C# Records?

At their core, records are reference types designed for immutability and value-based equality. Think of a record as a lightweight way to define a data-centric object. Unlike traditional classes, records automatically generate essential boilerplate code like equality members and ToString out of the box.

A Simple Example

Here’s how you can define and use a record:

public record Product(string Name, decimal Price);

public class Program
{
    public static void Main()
    {
        var product1 = new Product("Laptop", 999.99m);
        var product2 = new Product("Laptop", 999.99m);

        Console.WriteLine(product1); // Output: Product { Name = Laptop, Price = 999.99 }
        Console.WriteLine(product1 == product2); // Output: True (value-based equality)

        // Immutability
        // product1.Name = "Tablet"; // Not allowed: Records are immutable by default
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  • The Product record automatically generates:
    • A constructor (Product(string Name, decimal Price)).
    • Equals and GetHashCode for value-based equality.
    • A ToString method for human-readable output.
  • Records are immutable by default, meaning their properties cannot be modified once initialized.

Positional vs. Non-Positional Records

The above example used a positional record, where the properties are defined in the record's constructor. You can also define records in a more traditional way, using property declarations:

public record Customer
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

The init accessor ensures that the properties can only be set during object initialization, preserving immutability.


Advanced Features of Records

with Expressions for Non-Destructive Mutation

Sometimes, you might need to create a modified copy of a record. Records support non-destructive mutation via with expressions:

var originalProduct = new Product("Laptop", 999.99m);
var discountedProduct = originalProduct with { Price = 899.99m };

Console.WriteLine(originalProduct); // Output: Product { Name = Laptop, Price = 999.99 }
Console.WriteLine(discountedProduct); // Output: Product { Name = Laptop, Price = 899.99 }
Enter fullscreen mode Exit fullscreen mode

The with expression creates a new instance while preserving the original record's immutability.


Inheritance with Records

Records support inheritance, making them ideal for polymorphic domain models:

public record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
Enter fullscreen mode Exit fullscreen mode

This becomes especially powerful when combined with pattern matching, as we’ll see shortly.


Pattern Matching in C#: Writing Expressive Logic

Pattern matching allows you to simplify complex conditional logic by matching on object types, properties, or values in a declarative way. Combined with records, it becomes a natural companion for modeling and processing domain data.

Type Patterns

Let’s revisit our Shape hierarchy. Using pattern matching, we can process shapes in a clean and expressive way:

public static double CalculateArea(Shape shape) => shape switch
{
    Circle c => Math.PI * c.Radius * c.Radius,
    Rectangle r => r.Width * r.Height,
    _ => throw new ArgumentException("Unknown shape")
};
Enter fullscreen mode Exit fullscreen mode

The switch expression here evaluates the type of shape and applies the correct logic for each derived type. No need for cumbersome if-else chains or manual type checks!


Property Patterns

You can also match on specific properties of an object:

public static string GetProductCategory(Product product) => product switch
{
    { Price: < 50 } => "Budget",
    { Price: >= 50 and < 500 } => "Standard",
    { Price: >= 500 } => "Premium",
    _ => "Unknown"
};
Enter fullscreen mode Exit fullscreen mode

This approach is both concise and expressive, making it easy to see the intent of the logic.


Combining Patterns

Patterns can be combined to handle more complex scenarios. Here’s an example using a tuple pattern:

public static string DescribeOrderStatus(string status, bool isPaid) => (status, isPaid) switch
{
    ("Pending", false) => "Order is pending payment.",
    ("Pending", true) => "Order is pending shipment.",
    ("Shipped", _) => "Order has been shipped.",
    _ => "Unknown order status."
};
Enter fullscreen mode Exit fullscreen mode

By combining multiple conditions, you can handle intricate logic in a clean and maintainable way.


Common Pitfalls and How to Avoid Them

While records and pattern matching are powerful tools, there are some pitfalls to watch out for:

  1. Misusing Immutability: Records are immutable by default, but if you accidentally use mutable types (e.g., List<T>), you might break immutability. Use immutable collections like ImmutableList<T> to avoid this.

  2. Overusing Inheritance: While records support inheritance, excessive use can lead to tight coupling. Prefer composition over inheritance where possible.

  3. Performance Concerns with Large Records: Records implement value-based equality, which can be expensive for large objects. Be cautious when using records with many properties or deeply nested structures.

  4. Overcomplicating Patterns: Pattern matching is meant to simplify logic. Avoid nesting patterns too deeply, as it can hurt readability.


Key Takeaways

C# records and pattern matching are a game-changer for modern data modeling. Here’s what you should remember:

  • Records simplify immutable, value-based data modeling, drastically reducing boilerplate code.
  • Use with expressions for non-destructive mutation and init for immutability.
  • Pattern matching enables declarative and expressive logic, making your code easier to read and maintain.
  • Avoid common pitfalls like breaking immutability and overcomplicating patterns.

Next Steps

To deepen your understanding of these features, try applying them in a real-world project. Experiment with:

  • Building a domain model using records.
  • Refactoring existing if-else chains with pattern matching.
  • Combining records and pattern matching in advanced scenarios like parsing or processing data streams.

You can also explore the official C# documentation for more details on records and pattern matching.

Happy coding! 🎉

Top comments (0)