Chuyển đến nội dung chính

Mapster từ cơ bản đến nâng cao

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ớ.

graph TD A[Source: Person] -->|Call .Adapt| B{Mapster Engine} B --> C{Check Cache?} C -- No --> D[Inspect Properties via Reflection] D --> E[Build Expression Tree] E --> F[Compile to IL Code / Function] F --> G[Store in Cache] C -- Yes --> H[Get Compiled Function from Cache] G --> I[Execute: target.Name = source.Name] H --> I I --> J[Destination: PersonDto] subgraph "Deep Dive: Compile Step" E -.-> K["Logic: dest.FirstName = src.FirstName"] end
Đôi khi bạn sẽ thắc mắc: "Tại sao field này không map được?" hoặc "Mapster nó có đang làm đúng ý mình không?". Thay vì đoán mò, chúng ta sẽ bắt Mapster "in" ra đoạn logic mà nó đã biên dịch.
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ùng TypeAdapterConfig.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 nhau
TypeAdapterConfig.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
Dưới đây là flowchart minh họa
graph TD Start[Bắt đầu cấu hình Mapping] --> Q1{Bạn muốn áp dụng cho?} Q1 -- "Tất cả các cặp (Global)" --> Q2{Mục tiêu?} Q1 -- "Chỉ 1 cặp Entity-DTO cụ thể" --> Q3{Mục tiêu?} Q2 -- "Tự động bỏ qua các field dư thừa" --> A[IgnoreNonMapped-true] Q2 -- "Mapping chặt chẽ, báo lỗi nếu thiếu" --> B[Mặc định: Require Destination Member] Q3 -- "Ẩn các thông tin nhạy cảm: Password, Token" --> C[.Ignore-dest.Field] Q3 -- "Tùy chỉnh logic phức tạp" --> D[.Map-dest.Field, src.OtherField] subgraph "Chiến lược an toàn" A C end

💡 KHI NÀO DÙNG GÌ?

1️⃣ Dùng 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.
2️⃣ Dùng .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

graph TD A[Program Startup / Global.asax] --> B[MappingConfig.Register] B --> C[Scan Assembly] C --> D{Apply Rules} D --> E[Global: IgnoreNonMapped, IgnoreNull] D --> F[Specific: Entity to DTO Custom Rules] subgraph "Usage in Services/Controllers" G[Source Object] -->|.Adapt<T>| H[Destination Object] H -.->|Uses| D end

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:
Error: "Error while compiling mapping for Type X to Type Y..."
(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.
Giả sử bạn có một class với property Request nằm ở depth 4–5:

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ư IgnoreNullValues hoặc IgnoreNonMapped ngay 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 IRegister và dùng hàm Scan() để 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

Bài đăng phổ biến từ blog này

[ASP.NET MVC] Authentication và Authorize

Một trong những vấn đề bảo mật cơ bản nhất là đảm bảo những người dùng hợp lệ truy cập vào hệ thống. ASP.NET đưa ra 2 khái niệm: Authentication và Authorize Authentication xác nhận bạn là ai. Ví dụ: Bạn có thể đăng nhập vào hệ thống bằng username và password hoặc bằng ssh. Authorization xác nhận những gì bạn có thể làm. Ví dụ: Bạn được phép truy cập vào website, đăng thông tin lên diễn đàn nhưng bạn không được phép truy cập vào trang mod và admin.

ASP.NET MVC: Cơ bản về Validation

Validation (chứng thực) là một tính năng quan trọng trong ASP.NET MVC và được phát triển trong một thời gian dài. Validation vắng mặt trong phiên bản đầu tiên của asp.net mvc và thật khó để tích hợp 1 framework validation của một bên thứ 3 vì không có khả năng mở rộng. ASP.NET MVC2 đã hỗ trợ framework validation do Microsoft phát triển, tên là Data Annotations. Và trong phiên bản 3, framework validation đã hỗ trợ tốt hơn việc xác thực phía máy khách, và đây là một xu hướng của việc phát triển ứng dụng web ngày nay.

Tổng hợp một số kiến thức lập trình về Amibroker

Giới thiệu về Amibroker Amibroker theo developer Tomasz Janeczko được xây dựng dựa trên ngôn ngữ C. Vì vậy bộ code Amibroker Formula Language sử dụng có syntax khá tương đồng với C, ví dụ như câu lệnh #include để import hay cách gói các object, hàm trong các block {} và kết thúc câu lệnh bằng dấu “;”. AFL trong Amibroker là ngôn ngữ xử lý mảng (an array processing language). Nó hoạt động dựa trên các mảng (các dòng/vector) số liệu, khá giống với cách hoạt động của spreadsheet trên excel.