ASP.NET Web API (.NET Framework) · Building APIs

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.

Beginner 6 min Lesson 8 of 36Covers ASP.NET Web API 2.2 on .NET Framework 4.8.
0/36 in ASP.NET Web API
On this page · 6 min · Beginner

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#

C# Illustrative
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#

C#ProductsController.cs Illustrative
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());
    }
}
Expected output
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#

  1. The Product entity stays inside the API; controllers return ProductListItemDto.
  2. InStock is computed from Stock, so the exact stock level is not leaked.
  3. CreateProductDto has no Id, Stock or CreatedAt, so a client cannot set them.
  4. ToEntity sets server-controlled fields itself and trims the name.
  5. 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#

Binding directly to the entity

Post(Product product) lets clients set every property, including Id and audit fields.

One DTO for everything

Create, update, list and detail usually need different fields. Separate DTOs keep each contract clear.

Mapping inside the database query incorrectly

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?
Look the product up, set Price = dto.NewPrice, and return only the public fields.
Show solution
C# Illustrative
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#

DTOs quizMultiple choice Question 1 of 3

What is over-posting?

Score so far: 0/0

Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.