Khi thực hiện migrate các dự án .NET Framework, việc chuyển đổi dữ liệu giữa Entity và DTO thường tốn rất nhiều thời gian. Hôm nay mình sẽ giới thiệu Mapster - một thư viện Mapping cực nhanh và nhẹ để thay thế cho việc gán tay (Manual Mapping) nhàm chán.
1. Cài đặt
Mở NuGet Package Manager Console và chạy lệnh sau:
Install-Package Mapster
# hoặc
dotnet add package Mapster
2. Khởi tạo Model
Giả sử chúng ta có cấu trúc class như sau:
public class Person
{
public string Title { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime? DateOfBirth { get; set; }
public Address Address { get; set; }
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string PostCode { get; set; }
public string Country { get; set; }
}
public class PersonDto
{
public string Title { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime? DateOfBirth { get; set; }
}
Tạo fake data
public class DemoData
{
public static Person CreatePerson()
{
return new Person()
{
Title = "Mr.",
FirstName = "Peter",
LastName = "Pan",
DateOfBirth = new DateTime(2000, 1, 1),
Address = new Address()
{
Country = "Neverland",
PostCode = "123N",
Street = "Funny Street 2",
City = "Neverwood"
},
};
}
public static List<Person> CreatePeople()
{
return new List<Person>()
{
new Person()
{
Title = "Mr.",
FirstName = "Peter",
LastName = "Pan",
DateOfBirth = new DateTime(2000, 1, 1),
Address = new Address()
{
Country = "Neverland",
PostCode = "123N",
Street = "Funny Street 2",
City = "Neverwood"
}
},
new Person()
{
Title = "Ms.",
FirstName = "Wendy",
LastName = "Darling",
DateOfBirth = new DateTime(1999, 5, 15),
Address = new Address()
{
Country = "England",
PostCode = "W1A 1AA",
Street = "Kensington Gardens 1",
City = "London"
}
},
new Person()
{
Title = "Mr.",
FirstName = "James",
LastName = "Hook",
DateOfBirth = new DateTime(1990, 3, 22),
Address = new Address()
{
Country = "Neverland",
PostCode = "456N",
Street = "Pirate Cove 7",
City = "Skull Island"
}
}
};
}
}
3. Thực hiện Mapping cơ bản
Sử dụng phương thức mở rộng Adapt để map dữ liệu một cách nhanh chóng:
public static class MappingFunctions
{
private static readonly Person _person = DemoData.CreatePerson();
public static PersonDto MapPersonToNewDto()
{
// Mapster tự động khớp các thuộc tính cùng tên
var personDto = _person.Adapt<PersonDto>();
return personDto;
}
}
4. Kiểm tra kết quả
Tại hàm Main, chúng ta gọi hàm mapping và in kết quả ra Console:
static void Main(string[] args)
{
var personDto = MappingFunctions.MapPersonToNewDto();
PrintPerson(personDto);
Console.ReadLine();
}
static void PrintPerson(PersonDto person)
{
Console.WriteLine("=== Person Information ===");
Console.WriteLine($"Title: {person.Title}");
Console.WriteLine($"Name: {person.FirstName} {person.LastName}");
Console.WriteLine($"Date of Birth: {person.DateOfBirth:yyyy-MM-dd}");
Console.WriteLine("===========================");
}
Như các bạn thấy, Mapster giúp code sạch hơn rất nhiều và cực kỳ phù hợp cho các dự án cần hiệu năng cao.
5. Cơ chế Adapt
Tại sao Mapster lại nhanh ngang ngửa code tay? Câu trả lời nằm ở quy trình Compile-to-Function. Thay vì dùng Reflection mỗi lần mapping (chậm), Mapster chỉ soi cấu trúc object một lần đầu tiên, sau đó "đúc" ra một hàm gán giá trị bằng mã máy và lưu vào bộ nhớ.
public class DebugMapster
{
public static void DebugMapping<TSource, TDestination>()
{
// Khởi tạo config cho cặp Source -> Destination
var config = TypeAdapterConfig<TSource, TDestination>.NewConfig();
// Tạo Expression Tree (Cấu trúc logic của hàm mapping)
var expression = config.Config.CreateMapExpression(
new Mapster.Models.TypeTuple(typeof(TSource), typeof(TDestination)),
Mapster.MapType.Projection
);
// In kết quả ra Console để "thẩm định"
Console.WriteLine($"=== Mapster Generated Logic: {typeof(TSource).Name} -> {typeof(TDestination).Name} ===");
Console.WriteLine(expression.ToString());
Console.WriteLine("===============================");
}
}
Cách sử dụng trong hàm Main
Bạn chèn lệnh gọi Debug trước khi thực hiện mapping để kiểm tra logic:static void Main(string[] args)
{
// 1. Soi logic mapping trước
DebugMapster.DebugMapping<Person, PersonDto>();
// 2. Thực hiện mapping như bình thường
var person = MappingFunctions.MapPersonToNewDto();
// 3. In kết quả
PrintPerson(person);
Console.ReadLine();
}
Kết quả
=== Mapster Generated Logic: Person -> PersonDto ===
Param_0 => new PersonDto() {Title = Param_0.Title, FirstName = Param_0.FirstName, LastName = Param_0.LastName, DateOfBirth = Param_0.DateOfBirth}
===============================
=== Person Information ===
Title: Mr.
Name: Peter Pan
Date of Birth: 2000-01-01
===========================
Nhìn vào dòng này, bạn sẽ thấy Mapster thực chất đã tạo ra một Lambda Expression. Nó gán thẳng p1.Title sang Title. Không có vòng lặp thừa, không có xử lý dư thừa. Đây chính là lý do Mapster là "vũ khí" tối thượng để thay thế Manual Mapping trong dự án Migrate mà vẫn đảm bảo hiệu năng tối đa.
6. Mapping nâng cao với Ignore, Private Setter, GlobalConfig và Type Config
Mapster không chỉ map tự động theo tên property mà còn cho phép chúng ta tùy chỉnh linh hoạt. Dưới đây là các kỹ thuật thường dùng trong dự án thực tế.Mapping với Ignore
Trong nhiều trường hợp, chúng ta không muốn map một số property nhạy cảm (như Password) hoặc property không tồn tại ở DTO.
Ví dụ Ignore cho một cặp type cụ thể:
Thêm property Password vào class Person
public class PersonDto
{
public string Title { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
public string Password { get; set; }
}
Cập nhật lại hàm CreatePerson
public static Person CreatePerson()
{
return new Person()
{
Title = "Mr.",
FirstName = "Peter",
LastName = "Pan",
DateOfBirth = new DateTime(2000, 1, 1),
Address = new Address()
{
Country = "Neverland",
PostCode = "123N",
Street = "Funny Street 2",
City = "Neverwood"
},
Password = "123456"
};
}
Cấu hình Ignore:
public static class MappingFunctions
{
// Static constructor to configure Mapster on startup
static MappingFunctions()
{
ConfigureMapster();
}
private static void ConfigureMapster()
{
// Configure Person -> PersonDto mapping
TypeAdapterConfig<Person, PersonDto>.NewConfig()
.Ignore(dest => dest.Password)
.Ignore(dest => dest.DateOfBirth);
}
//....
}
Chúng ta cần sửa hàm DebugMapping để in ra chính xác, thay NewConfig() => ForType()
internal class DebugMapster
{
public static void DebugMapping<TSource, TDestination>()
{
// Get the Expression Tree that Mapster uses for the specific mapping pair
var config = TypeAdapterConfig<TSource, TDestination>.ForType();
var expression = config.Config.CreateMapExpression(new Mapster.Models.TypeTuple(typeof(TSource), typeof(TDestination)),
Mapster.MapType.Projection);
// Print the string representation of the mapping logic
Console.WriteLine($"=== Mapster Generated Logic: {typeof(TSource).Name} -> {typeof(TDestination).Name} ===");
Console.WriteLine(expression.ToString());
Console.WriteLine("===============================");
}
}
Cập nhật lại hàm Print
static void PrintPerson(PersonDto person)
{
Console.WriteLine("=== Person Information ===");
Console.WriteLine($"Title: {person.Title}");
Console.WriteLine($"Name: {person.FirstName} {person.LastName}");
Console.WriteLine($"Date of Birth: {person.DateOfBirth:yyyy-MM-dd}");
Console.WriteLine($"Password: {person.Password}");
Console.WriteLine("===========================");
}
Kết quả
=== Mapster Generated Logic: Person -> PersonDto ===
Param_0 => new PersonDto() {Title = Param_0.Title, FirstName = Param_0.FirstName, LastName = Param_0.LastName}
===============================
=== Person Information ===
Title: Mr.
Name: Peter Pan
Date of Birth: 0001-01-01
Password:
===========================
Global Config
Trường hợp bạn muốn áp dụng cho toàn bộ ứng dụng, bạn dùngTypeAdapterConfig.GlobalSettings.Default.Ignore, sẽ ra kết quả tương tự như trên.
static void Main(string[] args)
{
TypeAdapterConfig.GlobalSettings.Default.Ignore("Password", "DateOfBirth");
var person = MappingFunctions.MapPersonToNewDto();
DebugMapster.DebugMapping<Person, PersonDto>();
PrintPerson(person);
}
Ignore tất cả property không được map explicit
Khi dùng IgnoreNonMapped(true), Mapster chỉ map những property bạn khai báo explicit bằng .Map() hoặc có tên giống nhauTypeAdapterConfig.GlobalSettings.Default.IgnoreNonMapped(true);
Sửa lại MappingFunctions
private static void ConfigureMapster()
{
// ===== CÁCH 1: Cấu hình GLOBAL (Global Settings) =====
// IgnoreNonMapped(true): Bỏ qua tất cả properties không được mapping
// Sử dụng khi bạn muốn áp dụng cho tất cả mapping trong ứng dụng
TypeAdapterConfig.GlobalSettings.Default.IgnoreNonMapped(true);
// ===== CÁCH 2: Cấu hình CỤ THỂ (Specific Mapping) =====
// Chỉ bỏ qua các properties cụ thể
// TypeAdapterConfig<Person, PersonDto>.NewConfig()
// .Ignore(dest => dest.Password)
// .Ignore(dest => dest.DateOfBirth);
}
Kết quả
=== Mapster Generated Logic: Person -> PersonDto ===
Param_0 => new PersonDto() {}
===============================
=== Person Information ===
Title:
Name:
Date of Birth: 0001-01-01
Password:
===========================
Trường hợp bạn chỉ rõ cụ thể mapping property nào
private static void ConfigureMapster()
{
TypeAdapterConfig.GlobalSettings.Default.IgnoreNonMapped(true);
TypeAdapterConfig<Person, PersonDto>.NewConfig()
.Map(dest => dest.FirstName, src => src.FirstName)
.Map(dest => dest.LastName, src => src.LastName);
}
Kết quả
=== Mapster Generated Logic: Person -> PersonDto ===
Param_0 => new PersonDto() {FirstName = Param_0.FirstName, LastName = Param_0.LastName}
===============================
=== Person Information ===
Title:
Name: Peter Pan
Date of Birth: 0001-01-01
Password:
===========================
7. Chiến lược kiểm soát dữ liệu: IgnoreNonMapped vs .Ignore()
Khi làm dự án migrate, một sai lầm phổ biến là để Mapster tự do mapping mọi thứ. Điều này dễ dẫn đến rò rỉ dữ liệu (ví dụ: vô tình map luôn trường PasswordHash từ Database ra API). Dưới đây là bảng so sánh giúp bạn chọn đúng công cụ:
| Tiêu chí | IgnoreNonMapped | .Ignore() |
|---|---|---|
| Phạm vi | GLOBAL (Toàn cục) | CỤ THỂ (Từng cặp) |
| Áp dụng cho | Tất cả các cặp mapping | 1 mapping pair duy nhất |
| Bỏ qua properties | Field không có trong Destination | Field được chỉ định rõ tên |
| Cảnh báo unmapped | Không (Silent) | Có (nếu bật chế độ Strict) |
| Trường hợp dùng | Simple DTO mapping | Bảo mật, logic phức tạp |
💡 KHI NÀO DÙNG GÌ?
IgnoreNonMapped(true):
- Dùng khi Mapping từ Entity → DTO (kịch bản phổ biến nhất).
- Bạn chỉ muốn map những gì có sẵn ở DTO, còn lại kệ nó.
- Cực kỳ hữu ích để tự động bỏ qua các Navigation properties (Lazy loading) hoặc Internal properties của Entity Framework.
.Ignore():
- Cần loại bỏ các trường nhạy cảm (Password, Token, Secret).
- Khi bạn cần điều khiển chi tiết từng ly từng tí cho một API cụ thể.
- Muốn hệ thống quăng lỗi nếu một ai đó vô tình thêm field mới vào Entity mà quên update DTO (khi kết hợp với Strict Mapping).
Lời khuyên từ thực tế: Trong dự án migrate, mình thường bật IgnoreNonMapped ở GlobalSettings để giữ code sạch, và chỉ dùng .Ignore() cho các trường hợp đặc biệt liên quan đến bảo mật.
Best practice cho Web Application
8. MaxDepth và property nằm sâu trong object graph
// Cấu hình mặc định để tránh vòng lặp vô hạn, giả sử MaxDepth = 5
config.Default.MaxDepth(5);
Khi thực hiện mapping một đối tượng có cấu trúc phân cấp sâu (Nested Objects), thư viện như Mapster sẽ báo lỗi ngay ở bước biên dịch cấu hình:
(Lỗi phát sinh do bộ dựng Mapping không thể xác định cách khởi tạo các object ở tầng sâu vượt mức cho phép).
Hầu hết các thư viện Mapping mặc định đặt MaxDepth ở một con số an toàn (thường là 5 hoặc 10). Lý do rất đơn giản:
- Chống lặp vô tận (Circular References): Nếu Class A chứa Class B, và Class B lại tham chiếu ngược về Class A, trình mapping sẽ chạy mãi mãi cho đến khi bị StackOverflowException.
- Hiệu năng: Việc sao chép một cây đối tượng quá sâu tiêu tốn rất nhiều tài nguyên CPU và bộ nhớ RAM.
SourceClass (depth 0)
├── PropertyA (depth 1)
│ └── PropertyB (depth 2)
│ └── PropertyC (depth 3)
│ └── Request (depth 4) <-- Target
Mapster dừng traverse object graph khi đạt MaxDepth. Property Request nằm sâu sẽ bị bỏ qua hoàn toàn — không được map, không báo lỗi runtime rõ ràng, dẫn đến các lỗi logic khó trace.
Giải pháp
Ignore property
Ignore "Request" ở global config. Ngăn Mapster tự động map property này — tránh cả lỗi circular reference lẫn mapping sai:TypeAdapterConfig.GlobalSettings.Default
.Ignore("Request");
Hoặc bạn Ignore ở từng mapping object cụ thể. Tuy nhiên, bạn cần thêm đoạn code manually mapping hoặc gọi function AfterMap để tránh bỏ sót property:
.AfterMapping((src, dest) =>
{
var request = RequestFinder.FindRequest(src);
if (request != null)
dest.Request = request;
});
Hoặc dùng AfterMapping per-type với helper
public static IMappingExpression<TSrc, TDest>
MapRequest<TSrc, TDest>(
this IMappingExpression<TSrc, TDest> config)
where TDest : IHasRequest
{
return config
.Ignore(dest => dest.Request)
.AfterMapping((src, dest) =>
{
var request = RequestFinder.FindRequest(src);
if (request != null)
dest.Request = request;
});
}
// Khai báo mapping
config.NewConfig<SourceA, DestA>().MapRequest();
config.NewConfig<SourceB, DestB>().MapRequest();
Tăng giới hạn cục bộ
Thay vì tăng cho toàn hệ thống, hãy chỉ tăng MaxDepth cho những luồng dữ liệu đặc thù cần Deep Copy sâu.
Flatting DTO
Hầu hết các thư viện Mapping mặc định đặt MaxDepth ở một con số an toàn (thường là 5 hoặc 10). Lý do rất đơn giản:
- Chống lặp vô tận (Circular References): Nếu Class A chứa Class B, và Class B lại tham chiếu ngược về Class A, trình mapping sẽ chạy mãi mãi cho đến khi bị StackOverflowException.
- Hiệu năng: Việc sao chép một cây đối tượng quá sâu tiêu tốn rất nhiều tài nguyên CPU và bộ nhớ RAM.
Circular reference causing stack overflow with Mapster
Bình thường, Mapster sẽ tạo ra một đối tượng mới cho mỗi lần nó gặp một thuộc tính. Khi có vòng lặp (A→B→A), nó sẽ tạo mới không ngừng nghỉ. Khi bạn bật .PreserveReference(true), Mapster sẽ kích hoạt một cơ chế kiểm tra nội bộ:TypeAdapterConfig<A, B>.NewConfig()
.PreserveReference(true);
Ví dụ:
public class Team
{
public int Id { get; set; }
public string Name { get; set; }
// Team tham chiếu Manager
public EmployeeTeamMember Manager { get; set; }
}
public class EmployeeTeamMember
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
// Employee tham chiếu ngược lại Team -> Vòng lặp vô hạn!
// A => B => A => B => A => B...
public Team Team { get; set; }
}
9. Tổng kết và Best Practices cho dự án thực tế
Sau khi đã đi qua từ cơ bản đến nâng cao, đây là "bộ quy tắc nằm lòng" mình đang áp dụng cho dự án migrate để đảm bảo code vừa nhanh, vừa sạch, vừa dễ bảo trì:
- 🚀 Ưu tiên dùng
.Adapt<T>(): Đối với các mapping đơn giản (trùng tên field), hãy cứ để Mapster tự làm việc. Đừng viết code thừa. - ⚙️ Sử dụng TypeAdapterConfig: Chỉ khi nào cần
.Ignore(), custom logic phức tạp, hoặc map vào các class có private setter thì mới cần định nghĩa config riêng. - 🌍 Tận dụng GlobalConfig: Thiết lập các rule chung như
IgnoreNullValueshoặcIgnoreNonMappedngay từ đầu để tránh lặp lại code ở nhiều nơi. - 📂 Quản lý tập trung: Đừng cấu hình mapping rải rác ở khắp các Controller. Hãy gom tất cả vào các class
IRegistervà dùng hàmScan()để Mapster tự động nạp cấu hình khi ứng dụng khởi chạy.
Ví dụ thực thi sau khi đã cấu hình:
Khi bạn đã cấu hình mọi thứ trong Startup, việc sử dụng ở các tầng Logic cực kỳ gọn nhẹ:
// Dù bạn có cấu hình Ignore hay Custom logic phức tạp đến đâu
// Thì lúc sử dụng vẫn chỉ đơn giản là một dòng duy nhất:
var personDto = _person.Adapt<PersonDto>();
// Mapster vẫn giữ được tốc độ "bàn thờ" nhờ cơ chế Compile-to-Function
// đã giải thích ở phần trên.
Hy vọng bài viết này giúp anh em có cái nhìn rõ nét hơn về Mapster và tự tin áp dụng vào các dự án Migrate .NET sắp tới.
Happy Coding! 💻
Nhận xét
Đăng nhận xét