[CS] CARLOS SEIDL PT
Articles

Records in C#: Less Boilerplate, More Meaning!

C# records for Value Objects, DTOs, and messages: immutability, value equality, with expressions, and pattern matching.

By 4 min read
  • C#
  • Records
  • DDD
Article cover: Records in C#

Why use C# records to cut boilerplate

In many .NET codebases, a large share of the effort goes into creating simple data types (DTOs, Value Objects, event messages) with repetitive getters, setters, Equals, GetHashCode, and constructors. C# records cut down that boilerplate and make the domain’s intent more explicit, especially when immutability is a rule of the game.

In this article—continuing our series and keeping the same visual and textual style—we’ll focus on the practical use of records to build cleaner APIs, safer models, and more expressive domains, setting the stage for integrations, testing, and architectural evolution.


Record concepts: immutability and value equality

  • Record: a reference type with value equality (by default), ideal for representing immutable, meaningful data in the domain.
  • Immutability: state doesn’t change after creation; updates happen via with (non-destructive copy).
  • Record class vs record struct:
    • record class: reference type, value equality (by components).
    • record struct: value type, useful for high-performance and stack-friendly scenarios.
  • Positional records: concise syntax for defining properties in the type header.
  • Value equality: two records with the same components are equal, regardless of reference.
  • Deconstruct: built-in support for deconstructing components.
  • Architectural fit: works well with Domain-Driven Design (Value Objects), DTOs, and message contracts.

Where to use records in a layered architecture

In a layered architecture (API → Application → Domain → Infrastructure), records shine in:

  • Domain: modeling immutable Value Objects (e.g., Email, Money, Document), with validations and no identity of their own.
  • Application: DTOs and Commands/Queries (CQRS) with value equality, simplifying tests and mapping.
  • API: concise input/output contracts (request/response) that are easy to serialize.
  • Infrastructure: mapping to persistence/integrations while preserving the domain’s invariants.

The typical flow: the API receives a DTO (record), the Application layer orchestrates use cases and turns DTOs into Value Objects (records), which are then used by the domain’s entities/services.


Records in code: Value Objects, DTOs, with and pattern matching

5.1. Domain Value Object with record

Key points

  • Immutable: no set;. State is defined in the constructor.
  • Invariants in the factory (Create): guarantees validity from the moment of creation.
  • Value equality: two Email instances with the same Address are equal without manually overriding Equals/GetHashCode.

5.2. Request/Response DTO with record (Positional)

Key points

  • Concise syntax to reduce boilerplate.
  • record struct for Money: economical, with no extra allocation when passed by value (illustrative scenario).

5.3. Using with and pattern matching


5.4. Integration with the Application Layer

Key points

  • Explicit conversion from DTO → Value Object ensures invariants.
  • The response contract stays concise and expressive.

5.5. Serialization Considerations (System.Text.Json)

Note: positional and nominal records work well with System.Text.Json when they have public read accessors; adjust explicit constructors if you need custom parsing.


Best practices and caveats when using records

  • Use records for data and meaning, not for rich behavior. Domain behavior belongs in Entities/Services; records are great for Value Objects and contracts.
  • Build invariants at creation time (static factory, or private constructor + static method): avoids invalid instances.
  • Watch out for accidental mutability: avoid exposing init when the semantics call for true immutability.
  • Contract versioning: when evolving records used in APIs/events, prefer new types (CustomerCreatedV2) over breaking changes.
  • EF Core interoperability: for Value Objects, map them as owned types; make sure constructors are compatible with the infrastructure when needed (e.g., private constructor + configuration).
  • Perf: record struct can reduce allocations in hot-path scenarios; validate with benchmarks before adopting it.

When to use records and when to prefer classes

Use when

  • Modeling Value Objects (e.g., Email, CPF, Money).
  • Defining DTOs, Commands/Queries, and event payloads.
  • Prioritizing immutability and value equality for clarity and testability.

Avoid when

  • The type requires identity and a lifecycle (rich entities): prefer class.
  • There is a heavy dependency on ORMs that require public setters or proxies (mitigated with owned types/configuration).
  • The domain demands frequent mutations that are intrinsic to the type (consider traditional classes).

C# records: less boilerplate, clearer intent

C# records reduce boilerplate and make the domain’s intent explicit, offering immutability, value equality, and lean syntax. In layered architectures, they make DTOs and Value Objects more expressive, simplify tests, and favor clear contracts between components. Apply them where there is data with strong semantics and well-defined invariants; reserve traditional classes for entities and scenarios with inherent identity and mutability.

Need highly available .NET APIs? See the backend and .NET architecture service.

← all articles