C# Records — A Practical Guide for ASP.NET
Clear, factual, and example-driven introduction to C# records with ASP.NET samples.
What is a record?
A record in C# is a reference type that provides built-in value-based equality, concise syntax for immutable data containers, and convenient features such as with-expressions and positional syntax. Records are ideal for data-transfer objects (DTOs), configuration payloads, and any place you want structural equality instead of reference equality.
Basic syntax
Here are a few common ways to declare records.
public record Person(string FirstName, string LastName);Explanation: This creates an immutable record with two positional properties: FirstName and LastName. The compiler generates equality, a primary constructor, and deconstruct support.
public record Person {string FirstName : get; init;string LastName : get; init;}Explanation: This style uses explicit properties and init-only setters, giving immutability after construction while still allowing object initializers at creation time.
Examples
var a = new Person("Ada", "Lovelace");
var b = new Person("Ada", "Lovelace");
Console.WriteLine(a == b); // True — structural equality
var (first, last) = a; // deconstructionExplanation: Two records with the same property values compare equal. You can also deconstruct positional records directly into variables.
var original = new Person("Grace", "Hopper");
var updated = original with { LastName = "M."); // note: example shows with-expressionExplanation: with creates a shallow copy with specified property changes. (In real code ensure property names and types match.)
Using records in ASP.NET
Records work well as DTOs for controllers, request/response models, and minimal API endpoints. They reduce boilerplate and make it clear that the data shape matters, not object identity.
// DTO as record
public record CreateUserRequest(string Username, string Email);
// Minimal API endpoint (Program.cs)
app.MapPost("/users", (CreateUserRequest req) =>
{
// req is automatically bound from the JSON body
var id = Guid.NewGuid();
var response = new { Id = id, req.Username, req.Email };
return Results.Created($"/users/{id}", response);
});Explanation: The record CreateUserRequest is a compact DTO. ASP.NET model binding will populate it from JSON body automatically.
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpPost]
public IActionResult Create([FromBody] CreateUserRequest req)
{
// Use req.Username / req.Email
return CreatedAtAction(nameof(Get), new { id = 1 }, req);
}
[HttpGet("{id}")]
public IActionResult Get(int id) => Ok(new { id, Username = "demo" });
}Explanation: Records are fully compatible with controller model-binding. They can be used anywhere a class would be used for binding.
Pitfalls & recommendations
- Immutability vs ORM tracking: Some ORMs (like EF Core) expect mutable entities. While you can use records with EF Core, prefer classes for domain entities and records for DTOs.
- Shallow copy from with-expressions: The
withexpression performs a shallow copy. For nested mutable objects, you must copy them manually if deep immutability is desired. - Reference semantics warnings: Records are reference types; copying references to mutable members still shares the same underlying objects.
- Versioning: Adding/removing properties changes equality and serialized shape — consider backward compatibility for public APIs.
Conclusion
C# records are a powerful, concise tool for modeling data in ASP.NET applications. They give you structural equality, clear intent about immutability, and reduce boilerplate for DTOs and request/response models.
Why it's important: Using records where appropriate makes code easier to reason about, reduces bugs related to accidental identity checks, and clarifies which types are simple data carriers vs. behavior-rich domain objects.
What if ignored: If you ignore records and use mutable classes everywhere, you risk accidental mutations, confusing equality semantics (reference vs value), and more boilerplate. That increases maintenance cost and the chance of subtle bugs in APIs and tests.