DTOs
A DTO (Data Transfer Object) is a class designed for the wire, not for the database. It decides exactly what goes in and out of your API — no more, no less.
On this page · 6 min · Beginner
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Beginner
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Explain the risks of returning entities directly
- Write separate request and response DTOs
- Map between entities and DTOs
- Prevent over-posting
Explanation#
An entity matches a database table: Id, CostPrice, CreatedAt, IsDeleted, navigation properties. Returning it directly leaks internal fields and couples your API to your schema.
A response DTO contains only what clients need — and can add computed fields (InStock instead of the raw Stock count).
A request DTO contains only what clients may set. Without it, a client could post "Id": 999 or "IsAdmin": true and have it saved — called over-posting or mass assignment.
Mapping can be written by hand (simple and explicit) or with a library such as AutoMapper. Extension methods like ToDto() keep hand-written mapping tidy.
Returning EF entities also risks serialising lazy-loaded navigation properties — slow queries or reference-loop errors.
Syntax#
public class CreateProductDto { public string Name { get; set; } public decimal Price { get; set; } }
public class ProductListItemDto { public int Id { get; set; } public string Name { get; set; } public bool InStock { get; set; } }
public static ProductListItemDto ToListItem(this Product p) =>
new ProductListItemDto { Id = p.Id, Name = p.Name, InStock = p.Stock > 0 };
Code example: Entities stay inside; DTOs go out#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using ShopApi.Models; // Product entity: Id, Name, Price, Stock, Category, CreatedAt
// What clients may send
public class CreateProductDto
{
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}
// What clients receive in lists
public class ProductListItemDto
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public bool InStock { get; set; }
}
public static class ProductMappings
{
public static ProductListItemDto ToListItem(this Product p)
{
return new ProductListItemDto { Id = p.Id, Name = p.Name, Price = p.Price, InStock = p.Stock > 0 };
}
public static Product ToEntity(this CreateProductDto dto)
{
return new Product
{
Name = dto.Name.Trim(),
Price = dto.Price,
Category = dto.Category,
Stock = 0, // server decides, not the client
CreatedAt = DateTime.UtcNow, // server decides, not the client
};
}
}
public class ProductsController : ApiController
{
private static readonly List<Product> Db = new List<Product>
{
new Product { Id = 1, Name = "Pen", Price = 20m, Stock = 40, Category = "Stationery" },
};
[HttpGet, Route("api/products")]
public IEnumerable<ProductListItemDto> Get()
{
return Db.Select(p => p.ToListItem());
}
[HttpPost, Route("api/products")]
public IHttpActionResult Post(CreateProductDto dto)
{
var entity = dto.ToEntity();
entity.Id = Db.Count + 1;
Db.Add(entity);
return Created("api/products/" + entity.Id, entity.ToListItem());
}
}
GET /api/products
→ 200 [{"Id":1,"Name":"Pen","Price":20.0,"InStock":true}] (no Stock count, no CreatedAt)
POST /api/products {"Name":" Bag ","Price":650,"Category":"Bags","Stock":9999,"Id":42}
→ 201 {"Id":2,"Name":"Bag","Price":650.0,"InStock":false}
Stock and Id in the body were ignored: CreateProductDto has no such properties.This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- The Product entity stays inside the API; controllers return ProductListItemDto.
- InStock is computed from Stock, so the exact stock level is not leaked.
- CreateProductDto has no Id, Stock or CreatedAt, so a client cannot set them.
- ToEntity sets server-controlled fields itself and trims the name.
- The 201 response is also a DTO, never the entity.
Real-world use case#
A user API has a User entity with PasswordHash and IsAdmin. Returning the entity once exposed hashes in a mobile app’s network log. Switching to UserDto and UpdateProfileDto closed both the leak and an over-posting hole.
Common mistakes#
Post(Product product) lets clients set every property, including Id and audit fields.
Create, update, list and detail usually need different fields. Separate DTOs keep each contract clear.
Calling a C# extension method inside an EF query can fail to translate. Project with Select(p => new Dto { ... }) or map after ToListAsync().
Best practices#
- Never expose entities from controllers
- Separate request and response DTOs
- Let the server set ids, timestamps and ownership
- Keep mapping in one place (extension methods or AutoMapper profiles)
Remember this#
Entity = database shape. DTO = API shape.
Request DTO = what clients may send. Response DTO = what they see.
No DTO → over-posting risk.
Practice exercise#
Your task: Write an UpdatePriceDto with only NewPrice, and an action PUT api/products/{id}/price that applies it to the entity and returns a ProductListItemDto-like anonymous object.
Need a hint?
Show solution
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using ShopApi.Models;
public class UpdatePriceDto { public decimal NewPrice { get; set; } }
public class ProductPricesController : ApiController
{
private static readonly List<Product> Db = new List<Product> { new Product { Id = 1, Name = "Pen", Price = 20m } };
[HttpPut, Route("api/products/{id:int}/price")]
public IHttpActionResult UpdatePrice(int id, UpdatePriceDto dto)
{
var product = Db.FirstOrDefault(p => p.Id == id);
if (product == null) return NotFound();
product.Price = dto.NewPrice;
return Ok(new { product.Id, product.Name, product.Price });
}
}
Mini quiz#
What is over-posting?
Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions
My notes#
Saved in this browser.