What is a DTO, and why shouldn't you return EF entities from an API?

Web API & REST API Design Intermediate 2–3 Yrs3–5 Yrs Architecture 1 min read

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 (CustomerName instead of Customer.Name) and combine data from several entities.
  • Map with a mapper (AutoMapper, Mapster) or hand-written projection. Projecting with LINQ Select straight 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.