ASP.NET Core  

Reducing API Response Size Using AutoMapper and DTOs

Introduction

In modern web applications, APIs are the backbone of data exchange between the backend and the frontend. However, sending entire database entities directly as API responses often leads to:

  • Large payloads

  • Unnecessary data exposure

  • Performance bottlenecks in slow networks

A common solution in ASP.NET Core applications is to use DTOs (Data Transfer Objects) combined with AutoMapper. This approach allows developers to:

  • Return only the required fields

  • Map complex entities to simpler shapes

  • Reduce bandwidth consumption

  • Maintain separation of concerns

In this article, we will demonstrate a complete workflow with ASP.NET Core APIs, AutoMapper, and Angular clients, ensuring production-ready implementation.

1. Understanding DTOs

DTO (Data Transfer Object) is a simple object designed to transfer data between layers, especially between backend and frontend.

Benefits of using DTOs:

  1. Minimized API payloads – Send only the fields required by the client.

  2. Encapsulation – Hide internal fields like passwords or audit logs.

  3. Decoupling – Backend can change entity structure without breaking the API contract.

  4. Improved performance – Smaller JSON payloads reduce latency and bandwidth.

Example

Suppose you have a User entity:

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string PasswordHash { get; set; }
    public DateTime CreatedAt { get; set; }
    public string Role { get; set; }
}

You don’t want to send PasswordHash or internal timestamps to the frontend. Instead, define a DTO:

public class UserDto
{
    public int Id { get; set; }
    public string FullName { get; set; }
    public string Email { get; set; }
}

2. Introducing AutoMapper

AutoMapper is a library for object-to-object mapping. It automatically maps properties from a source object to a target object, reducing boilerplate code.

Installation

dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

Configuration

Create a Mapping Profile:

using AutoMapper;

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<User, UserDto>()
            .ForMember(dest => dest.FullName, 
                       opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"));
    }
}

Register AutoMapper in Program.cs:

builder.Services.AddAutoMapper(typeof(Program));

3. Using DTOs in ASP.NET Core Controllers

Example: User API

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly AppDbContext _context;
    private readonly IMapper _mapper;

    public UsersController(AppDbContext context, IMapper mapper)
    {
        _context = context;
        _mapper = mapper;
    }

    [HttpGet]
    public async Task<IActionResult> GetUsers()
    {
        var users = await _context.Users.ToListAsync();

        // Map entities to DTOs
        var userDtos = _mapper.Map<List<UserDto>>(users);

        return Ok(userDtos);
    }
}

Key Points

  • The frontend receives only Id, FullName, and Email.

  • PasswordHash and internal fields are never exposed.

  • Mapping is automatic and maintainable even with nested objects.

4. Mapping Nested Objects

Many real-world entities have relationships. AutoMapper can handle nested properties.

Example: Product and Category

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public Category Category { get; set; }
}

public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
}

DTO

public class ProductDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string CategoryName { get; set; }
}

Mapping Profile

CreateMap<Product, ProductDto>()
    .ForMember(dest => dest.CategoryName, opt => opt.MapFrom(src => src.Category.Name));

Now, the API will send a flat, lightweight object instead of embedding the entire Category.

5. Reducing API Response Size With Select Queries

Instead of fetching full entities and then mapping, project directly to DTOs using LINQ. This is more efficient for large datasets.

[HttpGet]
public async Task<IActionResult> GetProducts()
{
    var products = await _context.Products
        .Select(p => new ProductDto
        {
            Id = p.Id,
            Name = p.Name,
            CategoryName = p.Category.Name
        })
        .ToListAsync();

    return Ok(products);
}

Why this matters

  • Only the required columns are selected.

  • Database load and network transfer are reduced.

  • Ideal for large tables or nested objects.

6. Pagination, Filtering, and Sorting

Reducing response size is not only about selecting fields. For large datasets, implement:

  • Pagination

  • Filtering

  • Sorting

Example

[HttpGet]
public async Task<IActionResult> GetProducts([FromQuery] int page = 1, [FromQuery] int pageSize = 20)
{
    var query = _context.Products.AsQueryable();

    // Example filter
    query = query.Where(p => p.Price > 100);

    // Sorting
    query = query.OrderBy(p => p.Name);

    var total = await query.CountAsync();

    var products = await query
        .Skip((page - 1) * pageSize)
        .Take(pageSize)
        .Select(p => new ProductDto
        {
            Id = p.Id,
            Name = p.Name,
            CategoryName = p.Category.Name
        })
        .ToListAsync();

    return Ok(new { Total = total, Data = products });
}

7. Angular Integration

On the Angular side, define interfaces that match DTOs.

export interface ProductDto {
  id: number;
  name: string;
  categoryName: string;
}

Service Example

@Injectable({ providedIn: 'root' })
export class ProductService {
  constructor(private http: HttpClient) {}

  getProducts(page: number, pageSize: number): Observable<{ total: number; data: ProductDto[] }> {
    return this.http.get<{ total: number; data: ProductDto[] }>(`/api/products?page=${page}&pageSize=${pageSize}`);
  }
}

Component Example

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html'
})
export class ProductListComponent implements OnInit {
  products: ProductDto[] = [];
  total = 0;
  page = 1;
  pageSize = 20;

  constructor(private productService: ProductService) {}

  ngOnInit() {
    this.loadProducts();
  }

  loadProducts() {
    this.productService.getProducts(this.page, this.pageSize)
      .subscribe(res => {
        this.products = res.data;
        this.total = res.total;
      });
  }
}

8. Benefits of Combining DTOs, AutoMapper, and Select Projections

TechniqueBenefits
DTOsLimits data exposure, reduces payload size
AutoMapperSimplifies mapping, reduces boilerplate
LINQ .Select() projectionFetch only required columns, improves performance
Pagination & FilteringLimits records returned, reduces API load

Tip: Use all these techniques together in production for maximum efficiency.

9. Best Practices

  1. Keep DTOs lean: Include only necessary fields.

  2. Use projection for large datasets: Avoid fetching full entities if only a few fields are needed.

  3. Map nested objects carefully: AutoMapper can flatten complex relationships.

  4. Avoid sending sensitive data: Never include passwords, tokens, or internal IDs.

  5. Version your APIs: Avoid breaking frontend when entities change.

  6. Test payload sizes: Measure network payload to confirm optimization.

  7. Document DTOs: Use Swagger/OpenAPI to describe the API contract.

10. Advanced Scenarios

10.1 Multiple DTOs for Different Clients

You can define multiple DTOs for different use cases:

  • UserSummaryDto – lightweight overview

  • UserDetailDto – full profile

Mapping profiles

CreateMap<User, UserSummaryDto>()
    .ForMember(d => d.FullName, opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"));

CreateMap<User, UserDetailDto>()
    .ForMember(d => d.FullName, opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
    .ForMember(d => d.Role, opt => opt.MapFrom(src => src.Role));

10.2 Conditional Mapping

Use AutoMapper conditions to map only when necessary:

CreateMap<User, UserDto>()
    .ForMember(dest => dest.Email, opt => opt.Condition(src => !string.IsNullOrEmpty(src.Email)));

10.3 Projection to IQueryable

For very large datasets:

var userDtos = await _context.Users
    .ProjectTo<UserDto>(_mapper.ConfigurationProvider)
    .ToListAsync();
  • This generates SQL with only selected columns, improving efficiency.

Conclusion

Reducing API response size is essential for modern web applications. Using DTOs, AutoMapper, and selective projections ensures:

  • Lightweight responses

  • Improved performance

  • Secure data exposure

  • Maintainable code

Angular clients benefit from receiving exactly the data they need, enabling smooth dashboards, tables, and charts without unnecessary payloads.

By combining DTOs, AutoMapper, LINQ projection, and pagination, developers can build scalable, production-ready APIs.