What is a DTO, and why shouldn't you return EF entities from an API?
Quick Answer
A DTO (Data Transfer Object) is a plain class that defines exactly the shape of data crossing an API boundary. Returning EF entities instead leaks your database schema into the contract, causes serialization problems (lazy-loading, circular navigation references), exposes fields you didn't mean to, and couples the API to the model so a DB change breaks clients.
A DTO is a class whose only job is to define the shape of data going in or out of the API. I don't return EF entities directly for a few reasons: it ties the API contract to the database schema, so a column rename breaks clients; the entity might carry fields like a password hash or an internal flag that shouldn't go over the wire; and navigation properties cause circular references and surprise lazy-load queries when you serialize. Separate request DTOs also stop over-posting on writes. I usually project straight into the DTO with a LINQ Select so EF only fetches the columns I need.
- Contract stability — a DTO changes only when you decide to change the API; the entity changes whenever the schema does.
- Security — the entity might have
PasswordHash,InternalNotes,IsAdmin. A DTO exposes only what the caller should see, and a separate request DTO prevents over-posting on writes. - Serialization — navigation properties cause circular references or trigger extra lazy-load queries during serialization.
- Shape — a DTO can flatten (
CustomerNameinstead ofCustomer.Name) and combine data from several entities. - Map with a mapper (AutoMapper, Mapster) or hand-written projection. Projecting with LINQ
Selectstraight into the DTO is often best — EF pulls only those columns and tracks nothing.
- Serializing an EF entity with navigation properties — circular reference errors or N+1 lazy loads during JSON writing.
- Reusing the same class for the request body and the response — the request then accepts read-only fields (id, timestamps, computed values).
- Binding the request body straight to the entity — over-posting lets a caller set fields like
IsAdmin. - Mapping in memory (
.ToList()then map) instead of projecting in the query, pulling whole rows you don't need.