Bắt Đầu Với Mapperly Trong Dự Án ASP.NET Core: Ánh Xạ Đối Tượng Hiệu Quả Với Source Generator (Lộ Trình .NET)

Chào các bạn, lại là mình đây! Chúng ta đang cùng nhau trên hành trình khám phá Lộ trình học ASP.NET Core 2025. Sau khi tìm hiểu về nhiều khía cạnh quan trọng như C#, .NET Runtime/SDK/CLI, quản lý mã nguồn với Git, kiến thức mạng HTTP/HTTPS, cấu trúc dữ liệu, cơ sở dữ liệu SQL/NoSQL, ORM như EF Core hay Dapper, các chiến lược Cache, và cách xây dựng API mạnh mẽ (RESTful, GraphQL, gRPC), hôm nay chúng ta sẽ đi sâu vào một chủ đề rất phổ biến và đôi khi gây “đau đầu”: Ánh xạ đối tượng (Object Mapping).

Trong các ứng dụng ASP.NET Core hiện đại, đặc biệt là khi làm việc với các tầng kiến trúc khác nhau (ví dụ: Entity Framework Core cho tầng truy cập dữ liệu, DTO – Data Transfer Object cho tầng API hoặc giao tiếp giữa các service), việc chuyển đổi dữ liệu giữa các đối tượng có cấu trúc tương tự là một tác vụ lặp đi lặp lại và tốn công sức. Bạn có thể có một đối tượng UserEntity từ cơ sở dữ liệu và cần chuyển nó thành UserDto để gửi về cho client, hoặc ngược lại.

Trước đây, chúng ta thường có hai lựa chọn chính (mà mình đã đề cập trong bài viết Ánh Xạ Dữ Liệu trong .NET: AutoMapper vs Ánh Xạ Thủ Công – Ưu, Nhược điểm và Những ‘Cạm Bẫy’ Cần Tránh):

  1. Ánh xạ thủ công: Tự viết code để gán từng thuộc tính một. Cách này rõ ràng, dễ kiểm soát, nhưng cực kỳ nhàm chán và dễ gây lỗi khi có nhiều thuộc tính hoặc khi cấu trúc đối tượng thay đổi. Imagine viết hàng trăm dòng code chỉ để copy data từ object này sang object khác!
  2. Sử dụng thư viện ánh xạ dựa trên Reflection: AutoMapper là ví dụ nổi tiếng nhất. AutoMapper giúp giảm đáng kể code boilerplate bằng cách tự động tìm và gán các thuộc tính có tên và kiểu dữ liệu giống nhau dựa trên Reflection tại thời điểm chạy (runtime). Tuy nhiên, cơ chế dựa trên Reflection có thể gây ra một chút overhead về hiệu năng, và các lỗi cấu hình ánh xạ chỉ xuất hiện khi ứng dụng chạy, không phải lúc biên dịch.

Vậy có giải pháp nào tốt hơn, kết hợp được ưu điểm của cả hai cách trên không? Có chứ! Đó là lý do chúng ta có Mapperly – một thư viện ánh xạ đối tượng sử dụng Source Generator.

Mapperly Là Gì và Tại Sao Nó Đặc Biệt?

Mapperly là một thư viện mã nguồn mở dành cho C# giúp tạo ra mã ánh xạ đối tượng. Điểm đặc biệt và mạnh mẽ nhất của nó là việc sử dụng **Source Generator**. Thay vì sử dụng Reflection tại thời điểm chạy như AutoMapper, Mapperly sẽ *sinh ra mã C#* để thực hiện việc ánh xạ *ngay trong quá trình biên dịch* (compile time).

Điều này có nghĩa là:

  • Mã ánh xạ được tạo ra là mã C# “thông thường”, không có chi phí Reflection. Kết quả là hiệu năng vượt trội, gần bằng với việc bạn tự viết code ánh xạ thủ công.
  • Các lỗi cấu hình ánh xạ (ví dụ: thuộc tính nguồn không tồn tại ở đích, hoặc kiểu dữ liệu không tương thích) sẽ được phát hiện ngay trong quá trình biên dịch. Trình biên dịch sẽ báo lỗi giống như bất kỳ lỗi cú pháp C# nào khác. Điều này giúp bạn phát hiện và sửa lỗi sớm hơn rất nhiều.
  • Giảm thiểu tối đa code boilerplate. Bạn chỉ cần định nghĩa các interface hoặc abstract class đơn giản, còn Mapperly sẽ lo phần implementation.

Với những lợi ích này, Mapperly đang nhanh chóng trở thành lựa chọn ưa thích cho việc ánh xạ đối tượng trong các dự án .NET, đặc biệt là ASP.NET Core, nơi hiệu năng và khả năng bảo trì là yếu tố quan trọng.

Tại Sao Chọn Mapperly Thay Vì AutoMapper Hoặc Ánh Xạ Thủ Công?

Hãy cùng điểm lại những lý do chính khiến Mapperly nổi bật:

  1. Hiệu năng vượt trội: Vì mã ánh xạ được tạo ra tại thời điểm biên dịch, nó nhanh hơn đáng kể so với các thư viện dựa trên Reflection. Trong các ứng dụng web hiệu năng cao, điều này có thể tạo ra sự khác biệt.
  2. An toàn tại thời điểm biên dịch (Compile-time Safety): Đây là lợi ích lớn nhất đối với developer. Bạn không còn phải lo lắng về các lỗi ánh xạ chỉ xuất hiện khi chạy ứng dụng hoặc trong môi trường production. Mọi thứ được kiểm tra ngay khi bạn build project.
  3. Giảm thiểu Code Boilerplate: Tương tự AutoMapper, Mapperly giúp bạn tránh việc viết đi viết lại code gán thuộc tính thủ công.
  4. Tích hợp tốt với Dependency Injection: Mapperly sinh ra các class concrete triển khai các interface/abstract class mapper mà bạn định nghĩa. Các class này rất dễ dàng để đăng ký và sử dụng trong hệ thống Dependency Injection (DI) của ASP.NET Core. (Xem lại bài Hiểu Rõ Vòng Đời Dịch Vụ: Scoped, Transient, SingletonTiêm Phụ Thuộc Nâng Cao Khả Năng Kiểm Thử).
  5. Dễ sử dụng và cấu hình: Đối với các trường hợp ánh xạ đơn giản, bạn chỉ cần vài dòng code là xong. Đối với các trường hợp phức tạp hơn, Mapperly cung cấp các thuộc tính (attribute) để tùy chỉnh linh hoạt.

Dưới đây là bảng so sánh nhanh giữa ba phương pháp phổ biến:

Đặc điểm Ánh xạ thủ công AutoMapper Mapperly
Cơ chế Viết code thủ công Reflection (Runtime) Source Generator (Compile Time)
Hiệu năng Rất cao (trực tiếp) Tốt (có overhead Reflection) Rất cao (như code thủ công)
An toàn tại thời điểm biên dịch Cao (compiler check syntax) Thấp (chủ yếu runtime) Rất cao (compiler check mapping)
Giảm Boilerplate Code Thấp (phải viết hết) Cao (tự động tìm và gán) Cao (sinh code tự động)
Cấu hình phức tạp Tự quyết định Sử dụng Profiles, Converters Sử dụng Attributes, Partial methods
Tích hợp DI N/A Tốt (đăng ký service) Tốt (đăng ký generated service)

Rõ ràng, Mapperly mang lại những lợi ích đáng kể, đặc biệt là sự kết hợp giữa hiệu năng cao, an toàn tại thời điểm biên dịch và giảm thiểu code boilerplate.

Bắt Đầu Với Mapperly Trong Dự Án ASP.NET Core

Ok, lý thuyết đủ rồi. Chúng ta cùng bắt tay vào code để xem Mapperly hoạt động như thế nào nhé!

Bước 1: Cài đặt NuGet Package

Trong dự án ASP.NET Core của bạn, mở Terminal hoặc Command Prompt và sử dụng .NET CLI (như đã học trong bài Làm Chủ .NET CLI) để thêm package Mapperly:

dotnet add package Riok.Mapperly --version 3.2.0 # Sử dụng version mới nhất nếu có

Hoặc sử dụng NuGet Package Manager trong Visual Studio.

Package này chỉ là một Source Generator, nó không chứa mã thư viện chạy lúc runtime. Điều này giải thích tại sao Mapperly lại nhẹ và nhanh.

Bước 2: Tạo các Đối Tượng Cần Ánh Xạ

Giả sử chúng ta có hai đối tượng đơn giản: một Entity đại diện cho dữ liệu từ DB và một DTO để trả về cho client.

// Models/UserEntity.cs
public class UserEntity
{
    public Guid Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string Email { get; set; }
    public bool IsActive { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }
}
// Dtos/UserDto.cs
public class UserDto
{
    public Guid Id { get; set; }
    public string FullName { get; set; } // Tên thuộc tính khác
    public string EmailAddress { get; set; } // Tên thuộc tính khác
    public int Age { get; set; } // Kiểu dữ liệu khác (tính từ DateOfBirth)
    public bool Status { get; set; } // Tên thuộc tính khác
}

Ở đây chúng ta cố tình tạo ra sự khác biệt về tên thuộc tính (FullName vs FirstName/LastName, EmailAddress vs Email, Status vs IsActive) và kiểu dữ liệu (Age từ DateOfBirth) để minh họa cách Mapperly xử lý.

Bước 3: Định nghĩa Mapper Interface/Abstract Class

Tạo một interface hoặc abstract class (thường là interface để dễ dàng mock cho unit test) và đánh dấu nó bằng attribute [Mapper]. Sau đó, định nghĩa các phương thức ánh xạ mà bạn muốn.

// Mappers/IUserMapper.cs
using Riok.Mapperly.Abstractions;

[Mapper]
public partial interface IUserMapper
{
    UserDto MapToDto(UserEntity entity);

    // Có thể thêm các phương thức ánh xạ khác nếu cần
    // UserEntity MapToEntity(UserDto dto);

    // Ánh xạ danh sách
    List<UserDto> MapToListDto(List<UserEntity> entities);
}

Lưu ý:

  • Sử dụng từ khóa partial cho interface (hoặc class) và attribute [Mapper]. Điều này cho phép Source Generator của Mapperly sinh ra phần còn lại của class/interface này.
  • Bạn chỉ cần khai báo chữ ký phương thức (method signature), Mapperly sẽ sinh ra implementation.

Sau khi bạn build project, Source Generator sẽ chạy và sinh ra một file mới (thường nằm trong thư mục `obj` hoặc `generated`) với implementation cho interface IUserMapper, ví dụ: UserMapper.g.cs (trong đó `UserMapper` là tên class được sinh ra, thường dựa trên tên interface của bạn bỏ đi chữ ‘I’).

Class được sinh ra sẽ trông giống thế này (đây chỉ là ví dụ mô tả, mã thật phức tạp hơn):

// (Code được sinh ra bởi Mapperly Source Generator)
partial class UserMapper : IUserMapper
{
    public UserDto MapToDto(UserEntity entity)
    {
        if (entity == null) return null;

        var userDto = new UserDto();
        userDto.Id = entity.Id;
        // ... các ánh xạ mặc định ...
        // userDto.EmailAddress = entity.Email; // Nếu tên thuộc tính khớp
        // userDto.Status = entity.IsActive; // Nếu kiểu dữ liệu khớp

        // Xử lý các thuộc tính khác biệt/cần logic
        // userDto.FullName = entity.FirstName + " " + entity.LastName;
        // userDto.Age = DateTime.Now.Year - entity.DateOfBirth.Year; // Cần custom logic

        return userDto;
    }

    public List<UserDto> MapToListDto(List<UserEntity> entities)
    {
        if (entities == null) return null;
        var list = new List<UserDto>(entities.Count);
        foreach (var entity in entities)
        {
            list.Add(MapToDto(entity)); // Sử dụng phương thức MapToDto đã sinh ra
        }
        return list;
    }
}

Bước 4: Tùy chỉnh Ánh Xạ (Handling Differences)

Như bạn thấy, các thuộc tính có tên và kiểu dữ liệu giống nhau (Id) sẽ được Mapperly tự động ánh xạ. Nhưng với các trường hợp như FullName, EmailAddress, Age, và Status, chúng ta cần hướng dẫn cho Mapperly.

Mapperly cung cấp nhiều cách để tùy chỉnh:

4.1. Ánh xạ thuộc tính với tên khác nhau (`[MapProperty]`)

Sử dụng attribute [MapProperty("SourcePropertyName", "TargetPropertyName")] trên phương thức ánh xạ:

using Riok.Mapperly.Abstractions;

[Mapper]
public partial interface IUserMapper
{
    [MapProperty("Email", "EmailAddress")] // Map property Email của source sang EmailAddress của target
    [MapProperty("IsActive", "Status")]   // Map property IsActive của source sang Status của target
    UserDto MapToDto(UserEntity entity);

    List<UserDto> MapToListDto(List<UserEntity> entities);
}

4.2. Bỏ qua thuộc tính (`[Ignore]`)

Nếu bạn không muốn ánh xạ một thuộc tính nào đó ở đối tượng đích, bạn có thể sử dụng [Ignore("TargetPropertyName")]:

using Riok.Mapperly.Abstractions;

[Mapper]
public partial interface IUserMapper
{
    [MapProperty("Email", "EmailAddress")]
    [MapProperty("IsActive", "Status")]
    // [Ignore("Age")] // Nếu không muốn tính toán tuổi, bỏ qua thuộc tính Age
    UserDto MapToDto(UserEntity entity);

    List<UserDto> MapToListDto(List<UserEntity> entities);
}

4.3. Ánh xạ bằng phương thức tùy chỉnh (Partial Methods)

Đây là cách mạnh mẽ nhất để xử lý logic phức tạp, ví dụ như tính Age từ DateOfBirth hoặc kết hợp FirstNameLastName thành FullName.

Bạn khai báo một phương thức `partial` trong cùng interface/class mapper của bạn. Phương thức này có thể có tên theo convention `Map + [SourceType]To[TargetType]` hoặc chỉ đơn giản là tên mà bạn muốn gọi để xử lý một thuộc tính cụ thể. Mapperly sẽ tìm các phương thức `partial` này và gọi chúng từ mã sinh ra nếu cần.

using Riok.Mapperly.Abstractions;

[Mapper]
public partial interface IUserMapper
{
    [MapProperty("Email", "EmailAddress")]
    [MapProperty("IsActive", "Status")]
    UserDto MapToDto(UserEntity entity);

    List<UserDto> MapToListDto(List<UserEntity> entities);

    // Partial method để xử lý FullName
    private partial string MapFullName(UserEntity entity)
    {
        return $"{entity.FirstName} {entity.LastName}";
    }

    // Partial method để xử lý Age
    // Cần đặt tên theo convention Map + [SourceType]To[TargetType] cho thuộc tính Age
    // Hoặc sử dụng MapProperty để chỉ định method handler
    [MapProperty(nameof(UserEntity.DateOfBirth), nameof(UserDto.Age))] // Chỉ định method handler
    private partial int CalculateAge(DateTime dateOfBirth)
    {
        var today = DateTime.Today;
        var age = today.Year - dateOfBirth.Year;
        // Trừ đi 1 nếu sinh nhật chưa đến trong năm nay
        if (dateOfBirth.Date > today.AddYears(-age)) age--;
        return age;
    }

    // Nếu bạn muốn xử lý toàn bộ object con hoặc logic phức tạp hơn nữa,
    // bạn có thể định nghĩa một phương thức partial nhận cả object nguồn và đích
    // private partial void AfterMapping(UserEntity source, UserDto target)
    // {
    //     // Thêm logic sau khi Mapperly đã gán các thuộc tính khác
    // }
}

Mapperly đủ thông minh để nhận ra phương thức MapFullName có thể được sử dụng để ánh xạ từ UserEntity (hoặc các thuộc tính con của nó như FirstName, LastName) sang thuộc tính FullName của UserDto. Đối với CalculateAge, chúng ta sử dụng [MapProperty] để chỉ rõ rằng thuộc tính DateOfBirth nguồn nên được xử lý bởi phương thức này để gán vào thuộc tính Age đích.

Khi build, Mapperly sẽ sinh code gọi các phương thức partial này tại đúng thời điểm cần thiết trong phương thức MapToDto được sinh ra.

Còn nhiều thuộc tính khác để tùy chỉnh như xử lý giá trị null, mapping các enum, mapping từ Dictionary, v.v. Bạn có thể tham khảo tài liệu chính thức của Mapperly để khám phá hết các tính năng này.

Bước 5: Tích hợp Mapperly vào Dependency Injection

Trong ứng dụng ASP.NET Core, bạn sẽ muốn inject instance của mapper vào các service hoặc controller thay vì tạo mới mỗi lần sử dụng. Mapperly sinh ra một class concrete có tên mặc định là tên của interface/abstract class mapper bỏ đi chữ ‘I’ (ví dụ: UserMapper từ IUserMapper).

Bạn chỉ cần đăng ký class này vào DI container trong file Program.cs (hoặc Startup.cs):

// Program.cs (trong phương thức ConfigureServices hoặc top-level statements)
// ... các cấu hình khác ...

// Đăng ký Mapperly mapper vào DI container
builder.Services.AddSingleton<IUserMapper, UserMapper>();

// ... build và chạy ứng dụng ...

Ở đây chúng ta đăng ký là Singleton vì mapper thường stateless và thread-safe. (Xem lại bài Hiểu Rõ Vòng Đời Dịch Vụ: Scoped, Transient, Singleton để hiểu rõ hơn về vòng đời dịch vụ).

Bước 6: Sử dụng Mapper trong Controller/Service

Bây giờ bạn có thể inject interface của mapper vào constructor của class cần sử dụng (ví dụ: một API Controller).

// Controllers/UsersController.cs
using Microsoft.AspNetCore.Mvc;
using YourAppName.Models; // Thay YourAppName bằng namespace của bạn
using YourAppName.Dtos;
using YourAppName.Mappers;

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IUserMapper _userMapper;
    // Giả sử bạn có một service để lấy dữ liệu
    // private readonly IUserRepository _userRepository;

    public UsersController(IUserMapper userMapper /*, IUserRepository userRepository */)
    {
        _userMapper = userMapper;
        // _userRepository = userRepository;
    }

    [HttpGet("{id}")]
    public IActionResult GetUser(Guid id)
    {
        // Giả sử lấy UserEntity từ database
        // var userEntity = _userRepository.GetById(id);
        var userEntity = new UserEntity // Ví dụ dữ liệu giả
        {
            Id = id,
            FirstName = "John",
            LastName = "Doe",
            DateOfBirth = new DateTime(1990, 5, 15),
            Email = "john.doe@example.com",
            IsActive = true,
            CreatedAt = DateTime.UtcNow,
            UpdatedAt = null
        };


        if (userEntity == null)
        {
            return NotFound();
        }

        // Sử dụng mapper để chuyển đổi từ Entity sang DTO
        var userDto = _userMapper.MapToDto(userEntity);

        return Ok(userDto);
    }

    [HttpGet]
    public IActionResult GetAllUsers()
    {
         // Giả sử lấy danh sách UserEntity từ database
        // var userEntities = _userRepository.GetAll();
        var userEntities = new List<UserEntity> // Ví dụ dữ liệu giả
        {
             new UserEntity { Id = Guid.NewGuid(), FirstName = "John", LastName = "Doe", DateOfBirth = new DateTime(1990, 5, 15), Email = "john.doe@example.com", IsActive = true, CreatedAt = DateTime.UtcNow },
             new UserEntity { Id = Guid.NewGuid(), FirstName = "Jane", LastName = "Smith", DateOfBirth = new DateTime(1992, 8, 22), Email = "jane.smith@example.com", IsActive = false, CreatedAt = DateTime.UtcNow }
        };


        if (userEntities == null || !userEntities.Any())
        {
            return Ok(new List<UserDto>());
        }

        // Sử dụng mapper để chuyển đổi danh sách Entity sang danh sách DTO
        var userDtos = _userMapper.MapToListDto(userEntities);

        return Ok(userDtos);
    }
}

Bây giờ khi bạn chạy ứng dụng và gọi endpoint /api/users/{id}, bạn sẽ nhận được UserDto đã được ánh xạ đúng cách, bao gồm cả FullName được ghép từ FirstNameLastName, Age được tính từ DateOfBirth, EmailAddress từ EmailStatus từ IsActive.

Một Vài Lưu Ý Khi Sử Dụng Mapperly

  • Partial Class/Interface: Luôn nhớ đánh dấu mapper của bạn là `partial`. Source Generator cần điều này để sinh mã.
  • Attributes: Khám phá các attribute khác mà Mapperly cung cấp (`[MapEnum]`, `[MapCollection]`, `[MapperIgnoreTarget]`, v.v.) để tùy chỉnh sâu hơn.
  • Partial Methods: Sử dụng partial methods cho các logic ánh xạ phức tạp hoặc các thuộc tính cần tính toán. Đảm bảo chữ ký phương thức và tên phù hợp để Mapperly có thể nhận diện hoặc sử dụng [MapProperty] để chỉ định rõ ràng.
  • Debugging: Khi có lỗi biên dịch liên quan đến ánh xạ, hãy kiểm tra cửa sổ Output trong Visual Studio hoặc thông báo lỗi từ `dotnet build`. Lỗi thường khá rõ ràng và chỉ ra thuộc tính nào đang gặp vấn đề. Bạn cũng có thể xem mã được sinh ra (trong thư mục `obj`) để hiểu cách Mapperly hoạt động và debug.
  • Tương thích: Mapperly tương thích với .NET Core 3.1 trở lên và .NET 5+.

Kết Luận

Ánh xạ đối tượng là một phần không thể thiếu trong phát triển ứng dụng ASP.NET Core hiện đại. Việc chọn một công cụ ánh xạ hiệu quả giúp giảm thiểu công sức, tăng khả năng bảo trì và cải thiện hiệu năng ứng dụng.

Mapperly, với ưu điểm vượt trội về hiệu năng và an toàn tại thời điểm biên dịch nhờ cơ chế Source Generator, đang nổi lên như một giải pháp thay thế hấp dẫn cho các thư viện dựa trên Reflection như AutoMapper hay việc ánh xạ thủ công nhàm chán.

Việc tích hợp Mapperly vào dự án ASP.NET Core của bạn là khá đơn giản: thêm package, định nghĩa interface/abstract class mapper với attribute [Mapper] và các phương thức ánh xạ, tùy chỉnh bằng các attribute hoặc partial methods khi cần, và đăng ký vào Dependency Injection.

Nếu bạn đang tìm kiếm một giải pháp ánh xạ đối tượng nhanh chóng, an toàn và dễ sử dụng trong lộ trình phát triển .NET của mình, Mapperly chắc chắn là một lựa chọn đáng để cân nhắc và trải nghiệm.

Hy vọng bài viết này cung cấp cho các bạn cái nhìn rõ nét và đầy đủ về cách bắt đầu sử dụng Mapperly trong dự án ASP.NET Core. Hãy thử áp dụng nó vào dự án của bạn và cảm nhận sự khác biệt!

Hẹn gặp lại các bạn trong những bài viết tiếp theo của serie Lộ trình học ASP.NET Core 2025!

Chỉ mục