record 🆚 record struct in dotnet

A C# record is a special type of class or struct (introduced in C# 9) designed primarily for working with data models, offering a concise syntax and automatically implementing essential features like value-based equality, where two instances are equal if all their corresponding property values are the same. It is intended to simplify the creation of immutable or data-centric types by automatically generating boilerplate code, including the constructor, properties, equality members (Equals, GetHashCode, and ==/!= operators), a helpful ToString() method, and support for non-destructive mutation via the with expression

Records make it easy to create immutable types by auto-generating:
✓ constructors
✓ properties
✓ equality members (Equals, GetHashCode, ==, !=)
✓ a friendly ToString()
✓ and non-destructive mutation using the with expression.

The primary distinction between C# records lies in their underlying type semantics: reference type vs. value type.

record

  1. Reference Type (similar to a class)
  2. Allocated on the managed heap
  3. Value-based equality (compiler-synthesized)
  4. Positional properties are immutable (init accessors)
  5. Supports inheritance from other record classes
  6. Data Transfer Objects (DTOs), immutable data models where reference type behavior (e.g., possibility of null) is acceptable.

record struct

  1. Value Type (similar to a struct)
  2. Allocated on the stack (or inline in containing objects)
  3. Value-based equality (compiler-synthesized, faster than regular struct reflection)
  4. Positional properties are mutable (read-write)
  5. Does not support inheritance
  6. Small, lightweight data models, performance-critical scenarios, or when avoiding heap allocation/garbage collection pressure is paramount.

 

record vs. record class

The two declarations are functionally equivalent. The class Keyword in record class is optional and is provided only for explicit clarity to remind the developer that the type is a reference type.The two declarations are functionally equivalent. The class keyword in record class is optional and is provided only for explicit clarity to remind the developer that the type is a reference type.

 

record vs. record struct

The addition of the struct keyword changes the fundamental type semantics.

record class (Reference Type):

    • Sits on the heap.
    • Has reference semantics (assignment copies the reference).
    • Supports inheritance.
    • Positional properties are immutable by default (using init).
    • Can be null.

record struct (Value Type):

    • Sits on the stack (or inline in containing types).
    • Has value semantics (assignment copies the entire value).
    • Does not support inheritance.
    • Positional properties are mutable by default (can be made immutable using readonly record struct).
    • Cannot be null.

When to Use Which

  • record / record class: Use when you need a data model that is typically immutable, benefits from value equality, and you are okay with it being a reference type (heap allocation) and potentially inheriting from other records.
  • record struct: Use when your data type:
    1. Logically represents a single value (like DateTime or Point).
    2. Is small (ideally 16 bytes or less).
    3. You want to benefit from stack allocation and minimize garbage collector pressure.
    4. You still want the features records offer, like compiler-synthesized value equality and with expressions.

👉The key decision is similar to choosing between a regular class and a regular struct but with the added benefits of value-based equality and non-destructive mutation for both record types.

 

Can record struct be used in EF Core for value objects ?

Yes, you can, and it's an excellent choice for Value Objects in modern EF Core.

record struct is the preferred way to model simple, immutable Value Objects (Phone, Email, Money) that are small and do not need to be reference types.

Here's why and how:

 

 EF Core Support for record struct

Complex Types

Yes (Recommended)

This feature (introduced in EF Core 7, officially named Complex Types in EF Core 8) is the standard way to map value objects whose properties are stored as columns in the owner's table. record struct is ideal because it is a value type and is often immutable. You typically configure them using the [ComplexType] attribute or Fluent API.

Value Conversions

Yes

You can also use Value Converters to map a single record struct to a single column in the database (e.g., storing a record struct Email(string Value) as just a string column). This is best for very simple value objects.

Owned Entities

No

While a popular older pattern, the "Owned Entity" feature in EF Core only supports reference types (class / record class), not value types like record struct. However, the newer Complex Type feature is generally better suited for simple Value Objects.

 

Example

// The Value Object (record struct)
public record struct EmailAddress(string Value);
// The Entity (using [ComplexType] attribute from EF Core 8+)
using System.ComponentModel.DataAnnotations.Schema;

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }

    // Configure as a Complex Type
    [ComplexType]
    public EmailAddress Email { get; set; }
}

This configuration tells EF Core to map the EmailAddress.Value property directly to a column in the Customer table (e.g., Email_Value), making it an excellent way to implement DDD Value Objects without the performance overhead of reference types.

 

why not using regular struct instead of record struct?

While you can use a regular struct, you should generally prefer record struct for Value Objects because it significantly reduces boilerplate code and provides robust value semantics out-of-the-box.

Feature

record struct

struct

Value Equality

Automatic: Compiler generates a fast, efficient Equals() and GetHashCode() based on all members.

Manual/Slow: Default Equals() uses slow reflection to compare fields. You must manually override Equals(), GetHashCode(), operator ==, and operator != for correct, performant behavior.

ToString()

Automatic: Compiler generates a formatted string showing all member names and values (e.g., { X = 5, Y = 10 }).

Basic: Default is just the type name (e.g., Point).

Positional Syntax

Yes: Concise, single-line definition for properties (e.g., public record struct Point(int X, int Y);).

No: Requires separate property/field declarations and a verbose constructor.

Immutability

Easy: Use readonly record struct to enforce immutability with one keyword.

Manual: Requires marking every property/field as readonly and ensuring no setters exist.

Non-Destructive Mutation

Yes: Supports the with expression to create a copy with one or more modified properties (e.g., var p2 = p1 with { Y = 6 };).

No: Must manually create a copy method or constructor.