Skip to Content
Course content

234: Positional Records vs Property-Based Records

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I once worked with a developer who was absolutely enamored with the brevity of C# records. He converted our entire Domain Transfer Object layer to positional records because, as he put it, "why write ten lines when you can write one?" It was great for a week. Then, the business requirements changed, and we needed to add an optional DiscountCode to our Order record. Because he used the positional syntax, that one addition broke every single line of code where an Order was instantiated across the rest of the application. He spent an entire afternoon hunting down constructor calls just to add a single optional field.

The Speed of Positional Records

When you write a record like public record User(string Username, string Email);, you're using what we call a positional record. You aren't just defining a data structure; you're telling the compiler to generate a primary constructor, a set of public init-only properties, and a deconstructor—all in one line. It's incredibly satisfying for small, immutable data packets.

public record Point(double X, double Y);

// Usage is fast and clean
var start = new Point(0, 0);
var (x, y) = start; // Deconstruction works out of the box

I love these for internal state or simple coordinates. However, the "magic" here is that the primary constructor is the only way to create the object by default. If you add a third parameter to that record, every new Point(...) call in your codebase now has a compiler error. This is the "rigidity" that caught my colleague off guard.

Regaining Control with Property-Based Records

If you find yourself needing optional parameters, default values, or specific logic during initialization, you should switch to property-based records. This looks much more like a traditional class, but you keep all the value-based equality and the with expression capabilities that make records special.

public record Order
{
    public int OrderId { get; init; }
    public string CustomerName { get; init; } = "Guest"; // Default value
    public decimal Total { get; init; }
    public string? DiscountCode { get; init; } // Optional field
}

// Now I can instantiate it without worrying about constructor order
var myOrder = new Order 
{ 
    OrderId = 101, 
    Total = 59.99m 
};

By using the { get; init; } syntax, you're still ensuring immutability—the object cannot be changed after it's created—but you've decoupled the object's creation from a strict constructor signature. I generally lean toward this approach for any record that is likely to evolve over the course of a project. It saves you from the "refactoring nightmare" I mentioned earlier.

Which One Should You Actually Use?

You might be wondering if you can mix them. You can, but it usually gets confusing. The general rule of thumb I follow is based on the "stability" of the data. If you are representing a mathematical concept (like a ComplexNumber or a Vector3) where the properties are fixed and essential, go positional. It's concise and expresses the intent perfectly.

For everything else—API responses, database entities, or configuration objects—use property-based records. The slight increase in verbosity is a small price to pay for the flexibility of object initializers. Remember, the with expression works identically for both: var updatedOrder = myOrder with { Total = 49.99m };. You don't lose any of the "record magic" by choosing the more explicit syntax.




📋 Practical Task

Refactoring the Warehouse Inventory DTO

You have been handed a legacy piece of code where a Product record was defined positionally. The team is complaining because adding a new optional WarehouseLocation field is causing too many breaking changes across the system. Your task is to refactor the Product record from a positional record to a property-based record to allow for optional initialization.

Requirements:

  • Convert the Product record from public record Product(string Sku, string Name, decimal Price); to a property-based record.
  • Ensure all properties remain immutable (use init).
  • Add an optional property string? WarehouseLocation that defaults to null.
  • Add a property int StockQuantity that defaults to 0.
  • In your Main method, instantiate a Product using object initializer syntax, providing only the Sku and Price, leaving the others to their defaults.
// Start with this
public record Product(string Sku, string Name, decimal Price);

public class Program
{
    public static void Main()
    {
        // Your implementation here
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.