DEV Community

Mina Golzari Dalir
Mina Golzari Dalir

Posted on

Mastering Custom DTO Mapping in .NET Core (with and without AutoMapper)

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:

  1. Manual Mapping
  2. AutoMapper
  3. 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
}
Enter fullscreen mode Exit fullscreen mode

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
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage:

var userDto = user.ToDto();
Enter fullscreen mode Exit fullscreen mode

✅ 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
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Register AutoMapper

In your Program.cs or Startup.cs:

builder.Services.AddAutoMapper(typeof(Program));
Enter fullscreen mode Exit fullscreen mode

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);
    }
}

Enter fullscreen mode Exit fullscreen mode

✅ 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();

Enter fullscreen mode Exit fullscreen mode

✅ 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();

Enter fullscreen mode Exit fullscreen mode

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)