Public sealed record for DTOs in Modern C#

When building APIs, message contracts, or internal application boundaries, DTOs are everywhere. Traditionally, DTOs have been implemented as mutable classes, but since C# 9, records offer a far better abstraction for this use case. In this post, I’ll explain why I almost always prefer public sealed record for DTOs.

Fc77b16c 0Ebc 4Ac9 9E67 D00966da656c

DTOs are data, not behavior

DTOs exist to move data between layers or systems. They are not domain entities, and they typically don’t contain behavior.

That makes them a perfect fit for immutable, value-based types.

Value-based equality by Default

With records, equality is based on the values of the properties, not object identity.

This means two DTO instances containing the same data are considered equal, which is often exactly what we want when:

  • Comparing API responses
  • Validating messages
  • Writing unit tests
  • Using DTOs as dictionary keys or cache entries
public sealed record OrderDto(
    Guid OrderId,
    Guid CustomerId,
    decimal TotalAmount
);

Two instances with the same values will compare as equal without any boilerplate code.

Why sealed Matters

By default, records support inheritance. For DTOs, this is rarely desirable.

Marking a record as sealed:

  • Prevents accidental inheritance
  • Guarantees stable equality semantics
  • Makes intent explicit: this is a closed data contract

For API and messaging scenarios, this stability is critical.

Immutability Encourages Safer Code

Records are designed around immutability. Once created, their values don’t change.

This leads to:

  • Fewer side effects
  • Easier reasoning about code
  • Safer concurrent execution
  • More predictable behavior in distributed systems

Clear Intent in Code

public sealed record communicates intent immediately:

“This type is a data-only, immutable, value-based contract that is not meant to be extended.”

That clarity helps both the compiler and future developers.

Conclusion

For DTOs in modern C#, public sealed record is often the best possible choice:

  • Less boilerplate
  • Safer equality
  • Clear intent
  • Better defaults

I still use classes where identity, mutability, or behavior matters, but for DTOs, records have become my default.