Stop Using Classes for DTOs, Use record Types Instead
When working with data-centric structures, such as DTOs (Data Transfer Objects), API models, configuration classes, or response wrappers, developers often fall back on regular C# class types. However, since the introduction of the record type in C# 9, we now have a much better, more suitable way to model these use cases. In this post, I am explaining why C# record types are ideal for these scenarios, with code examples to help you use them effectively.
Perfect for API Requests and Responses
When working in ASP.NET Core, you are constantly defining models for incoming requests and outgoing responses. Record types are perfect here because they are immutable (which avoids unexpected bugs), they serialize and deserialize just like classes, and they are more concise.
public record LoginRequest(string Email, string Password);
public record LoginResponse(bool Success, string Message);
This makes your controllers and services more expressive, with less clutter.
Great for Config Snapshots
You can use records for app configuration models as well.
public record AppSettings(string SiteName, bool IsLive);
This works perfectly with IOptions<AppSettings>
or IOptionsSnapshot<AppSettings>
in ASP.NET Core.
Cleaner, More Maintainable Code
With record types, you avoid a lot of repetitive code that comes with traditional POCO (Plain Old CLR Object) classes. You don’t need to write constructors, ToString()
, or implement equality logic. That’s all generated automatically.
Compare this verbose class:
public class Product
{
public string Name { get; }
public decimal Price { get; }
public Product(string name, decimal price)
{
Name = name;
Price = price;
}
public override string ToString() => $"Product: {Name}, Price: {Price}";
}
With this equivalent record:
public record Product(string Name, decimal Price);
The result? Less boilerplate, easier maintenance, and a more expressive data model.
Better Debugging and Logging
Record types also generate a helpful ToString() method automatically, which includes property names and values. This makes debugging, logging, and testing easier.
var user = new UserDto("Usman", "usman@example.com");
Console.WriteLine(user);
// Output: UserDto { Name = Usman, Email = usman@example.com
}
This built-in string representation is more informative than the default class output, which is typically just the type name.
Support for Non-Destructive Mutation
One of the most powerful features of records is the with expression, which allows you to clone an object while modifying some of its properties, without altering the original.
var original = new Product("Laptop", 999.99m);
var discounted = original with { Price = 899.99m };
This functional style of updating data is clean, expressive, and helps avoid side effects, especially in multi-threaded environments.
When NOT to Use Records
While records are great for data, you should not put business logic inside them. Records are designed to model what data is, not what it does. For example, don’t include methods like ApplyDiscount()
or domain logic inside a record. That logic belongs in domain entities or services. Also, if you’re using EF Core and need mutable entities with identity tracking, stick with class
, record doesn’t play well with those use cases.
That was all for this tip, until the next time, stay curious, and keep coding.