When building clean and maintainable APIs in .NET Core, DTOs (Data Transfer Objects) play a crucial role in shaping your architecture. They help you control what data leaves your backend, improve performance, and prevent unnecessary data exposure.
In this guide, we’ll dive deep into how to implement custom DTO mapping using three practical approaches:
- Manual Mapping
- AutoMapper
- LINQ Projection (for high performance)
Each approach has its strengths — and by the end, you’ll know which one best fits your project.
What Is a DTO
A Data Transfer Object is a simple, lightweight class used to move data between layers — for example, from your Entity Framework model to your API response.
DTOs are not entities. They usually:
- Contain only required data fields.
- Flatten or simplify complex relationships.
- Prevent exposing database schema directly to the client. Here’s a simple example:
// Entity (Database Model)
public class User
{
public int Id { get; set; }
public string FullName { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
public ICollection<Order> Orders { get; set; }
}
// DTO (Data Transfer Object)
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; } // mapped from FullName
public string Email { get; set; }
public int OrderCount { get; set; } // computed from Orders.Count
}
1- Manual Mapping — Full Control, No Dependencies
Manual mapping gives you explicit control over how data is transformed.
You can write an extension method for reusability:
public static class UserMapper
{
public static UserDto ToDto(this User user)
{
return new UserDto
{
Id = user.Id,
Name = user.FullName,
Email = user.Email,
OrderCount = user.Orders?.Count ?? 0
};
}
}
Usage:
var userDto = user.ToDto();
✅ Pros
Full control and transparency.
No extra libraries.
❌ Cons
Verbose and repetitive for large models.
Harder to maintain when your app scales.
💡 When to use:
Great for small apps or specific mappings that require custom logic.
2- AutoMapper — Clean and Scalable
When your project grows, AutoMapper becomes invaluable. It handles repetitive mapping automatically and keeps your code DRY (Don’t Repeat Yourself).
Step 1: Install the package
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
Step 2: Define a mapping profile
using AutoMapper;
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<User, UserDto>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.FullName))
.ForMember(dest => dest.OrderCount, opt => opt.MapFrom(src => src.Orders.Count));
}
}
Step 3: Register AutoMapper
In your Program.cs or Startup.cs:
builder.Services.AddAutoMapper(typeof(Program));
Step 4: Use it in your service or controller
public class UserService
{
private readonly IMapper _mapper;
private readonly AppDbContext _context;
public UserService(IMapper mapper, AppDbContext context)
{
_mapper = mapper;
_context = context;
}
public async Task<IEnumerable<UserDto>> GetUsersAsync()
{
var users = await _context.Users
.Include(u => u.Orders)
.ToListAsync();
return _mapper.Map<IEnumerable<UserDto>>(users);
}
}
✅ Pros
Clean, consistent, and reusable.
Ideal for large codebases and microservices.
❌ Cons
Slight learning curve.
Harder to debug complex transformations.
When to use:
Perfect for enterprise-level APIs or when you have many entity-to-DTO transformations.
3- LINQ Projection — The Performance-First Approach
If your main concern is performance, LINQ projection is the most efficient way to map directly inside your EF Core query.
This approach fetches only the fields you need — nothing more.
var users = await _context.Users
.Select(u => new UserDto
{
Id = u.Id,
Name = u.FullName,
Email = u.Email,
OrderCount = u.Orders.Count
})
.ToListAsync();
✅ Pros
Executes mapping on the database side (SQL translation).
Extremely fast and memory-efficient.
❌ Cons
Not reusable across different parts of the app.
Manual mapping inside every query.
When to use:
Best for read-heavy APIs or endpoints that fetch large datasets.
Bonus: AutoMapper + EF Core Projection
You can combine AutoMapper with EF Core’s projection to achieve both clean code and high performance:
var users = await _context.Users
.ProjectTo<UserDto>(_mapper.ConfigurationProvider)
.ToListAsync();
This approach uses AutoMapper’s mapping configuration but executes the projection at the database level — reducing memory usage and query time.
Best Practice Tip
Always separate your entity models from your DTOs.
It’s one of the core principles of Clean Architecture and Domain-Driven Design.
DTOs make your API more stable, more secure, and easier to evolve over time.
Wrapping Up
Whether you prefer manual control, AutoMapper’s elegance, or pure LINQ performance, understanding DTO mapping is a must for any serious .NET backend developer.
The right choice depends on your use case:
- For small, focused APIs → go manual.
- For scalable, team-based projects → AutoMapper wins.
- For optimized, query-driven reads → LINQ projection is your friend.
By mastering all three, you’ll be ready for clean, high-performance, production-grade APIs.
_
I write about backend development, .NET best practices, and clean architecture.
If you found this post useful, follow me on DEV_
Top comments (0)