عنوان:

‫نبرد نگاشت‌گرها در .NET: مقایسه عمیق AutoMapper و Mapster


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۶/۱۷ ۰۹:۳۶
آدرس: www.dntips.ir
مقدمه: چرا به نگاشت‌گرها نیاز داریم؟

در دنیای برنامه‌نویسی مدرن، داده‌ها اغلب بین لایه‌های مختلف یک نرم‌افزار، مانند مدل‌های پایگاه داده، اشیای منطق کسب‌وکار، و اشیای نمایش داده‌ها (DTOها)، منتقل و تبدیل می‌شوند. این فرآیند تبدیل یا "نگاشت" (Mapping)، اگر به صورت دستی انجام شود، می‌تواند بسیار تکراری، وقت‌گیر و مستعد خطا باشد. نگاشت‌گرها (Mappers) ابزارهایی هستند که این فرآیند را به صورت خودکار انجام می‌دهند و به توسعه‌دهندگان اجازه می‌دهند بر روی منطق اصلی برنامه تمرکز کنند.
در اکوسیستم دات‌نت، AutoMapper سال‌هاست که به عنوان یکی از محبوب‌ترین و تثبیت‌شده‌ترین نگاشت‌گرها شناخته می‌شود. با این حال، اخیراً با تغییر مدل کسب‌وکار آن به سمت طرح‌های تجاری، بحث‌ها و جستجو برای جایگزین‌های کارآمد، به‌ویژه Mapster، افزایش یافته است. این مقاله به بررسی عمیق و مقایسه‌ای جامع بین این دو کتابخانه می‌پردازد و مزایا و معایب هر یک را روشن می‌سازد.

AutoMapper: پیشگام و بازیگر قدیمی

AutoMapper با ارائه یک رویکرد مبتنی بر پیکربندی برای نگاشت اشیاء، توانسته است حجم زیادی از کدهای تکراری را حذف کند. این کتابخانه از بازتاب (Reflection) در زمان اجرا برای نگاشت اشیاء استفاده می‌کند. این بدان معناست که در زمان اجرای برنامه، AutoMapper ساختار اشیاء مبدأ و مقصد را بررسی کرده و عملیات نگاشت را انجام می‌دهد.
با وجود اکوسیستم بالغ و جامعه کاربری گسترده‌ای که دارد، AutoMapper با چالش‌هایی نیز روبرو است. استفاده از بازتاب می‌تواند باعث کاهش عملکرد و افزایش مصرف منابع در سناریوهای با حجم بالا شود. همچنین، به دلیل ماهیت پویا و مبتنی بر زمان اجرای خود، خطاهای مربوط به نگاشت اغلب در زمان اجرا (Runtime) شناسایی می‌شوند که دیباگ کردن (Debugging) آن‌ها می‌تواند دشوار باشد.
با این حال، AutoMapper همچنان برای پروژه‌های بزرگ و پیچیده که به انعطاف‌پذیری و پیکربندی‌های سفارشی گسترده نیاز دارند، گزینه‌ای قدرتمند محسوب می‌شود. مدل تجاری جدید آن نیز با ارائه پشتیبانی مستقیم و به‌روزرسانی‌های منظم، ارزش بیشتری برای کاربران سازمانی ایجاد می‌کند.

Mapster: ظهور یک جایگزین مدرن

در مقابل AutoMapper، Mapster به عنوان یک نگاشت‌گر مدرن و با کارایی بالا قد علم کرده است. مزیت اصلی Mapster در رویکردش به نگاشت است: به جای بازتاب در زمان اجرا، از تولید کد در زمان کامپایل (Compile-time code generation) استفاده می‌کند. به عبارت ساده، Mapster کد لازم برای عملیات نگاشت را در زمان کامپایل برنامه تولید می‌کند. این رویکرد به چند دلیل حیاتی، Mapster را از AutoMapper متمایز می‌کند:
  • عملکرد سریع‌تر و مصرف کمتر منابع: تولید کد در زمان کامپایل، نیاز به بررسی ساختار اشیاء در زمان اجرا را از بین می‌برد. این امر باعث کاهش چشمگیر سربار پردازنده (CPU overhead)، نگاشت سریع‌تر و مصرف کمتر حافظه می‌شود. این ویژگی برای برنامه‌های با کارایی بالا (High-performance)، ریزسرویس‌ها (Microservices) و استقرار در محیط‌های ابری بسیار مناسب است.
  • ایمنی در زمان کامپایل (Compile-Time Safety): از آنجا که نگاشت در زمان کامپایل بررسی می‌شود، هرگونه ناسازگاری یا خطای نگاشتی، بلافاصله توسط کامپایلر تشخیص داده می‌شود. این ویژگی به توسعه‌دهندگان کمک می‌کند تا خطاهای بالقوه را قبل از رسیدن به محیط تولید، رفع کنند و فرآیند دیباگینگ را ساده‌تر می‌سازد.
  • سینتکس ساده و شهودی: Mapster با ارائه یک سینتکس ساده و سرراست، یادگیری آن را برای توسعه‌دهندگان جدید بسیار آسان می‌کند. در بسیاری از موارد نگاشت ساده، تنها با یک خط کد می‌توان عملیات نگاشت را انجام داد: var dest = source.Adapt.
  • انعطاف‌پذیری در پیکربندی: Mapster با وجود سادگی، یک API روان (Fluent API) برای پیکربندی‌های پیچیده ارائه می‌دهد. این API امکان نگاشت شرطی، نگاشت اشیاء تودرتو (Nested Objects) و سناریوهای پیشرفته دیگر را فراهم می‌کند.
  • سازگاری با اکوسیستم مدرن .NET: کتابخانه Mapster به طور بومی برای کار با نسخه‌های جدیدتر .NET طراحی شده و به خوبی با تکنولوژی‌هایی مانند LINQ، Entity Framework Core و الگوهای برنامه‌نویسی ناهمزمان (Async Scenarios) سازگار است.

مقایسه عملی: کدنویسی در AutoMapper و Mapster

برای درک بهتر تفاوت‌های این دو کتابخانه، به مثال‌های عملی نگاهی می‌اندازیم. فرض کنید دو کلاس User و UserDto داریم و می‌خواهیم مقادیر را بین آن‌ها نگاشت کنیم.

نگاشت ساده

// Automapper
// 1. Define mapping configuration
var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<User, UserDto>();
});

// 2. Create mapper instance
var mapper = config.CreateMapper();

// 3. Perform mapping
UserDto dto = mapper.Map<UserDto>(user);

// Mapster
// No setup required for simple mappings
UserDto dto = user.Adapt<UserDto>();

توضیح: در AutoMapper، حتی برای نگاشت‌های ساده، نیاز به تعریف پیکربندی و ایجاد نمونه‌ای از Mapper دارید. در مقابل، Mapster به دلیل رویکرد تولید کد، هیچ تنظیمات اولیه ای نیاز ندارد و سینتکس بسیار کوتاه‌تری ارائه می‌دهد.

نگاشت ویژگی‌های سفارشی

توضیح: هر دو کتابخانه قابلیت نگاشت سفارشی را ارائه می‌دهند. در AutoMapper از متد ForMember و در Mapster از Map استفاده می‌شود. هرچند سینتکس در این بخش کمی شبیه است، اما رویکرد کلی Mapster برای پیکربندی‌های سفارشی نیز همچنان سبک و ساده است.

// AutoMapper
var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<User, UserDto>()
        .ForMember(dest => dest.FullName,
                   opt => opt.MapFrom(src => src.FirstName + " " + src.LastName));
});

var mapper = config.CreateMapper();
UserDto dto = mapper.Map<UserDto>(user);

// Mapster
TypeAdapterConfig<User, UserDto>.NewConfig()
    .Map(dest => dest.FullName,
         src => src.FirstName + " " + src.LastName);

UserDto dto = user.Adapt<UserDto>();

نگاشت لیست‌ها

توضیح: نگاشت لیست‌ها در هر دو کتابخانه بسیار آسان است. با این حال، سینتکس Mapster به دلیل استفاده از متد Adapt به صورت مستقیم بر روی لیست، کمی شهودی‌تر به نظر می‌رسد.

// AutoMapper
List<UserDto> dtoList = mapper.Map<List<UserDto>>(userList);

// Mapster
List<UserDto> dtoList = userList.Adapt<List<UserDto>>();

پروجکشن (Projection) با Entity Framework Core

توضیح: پروجکشن قابلیتی است که به نگاشتگر اجازه می‌دهد تا یک پرس‌وجو (Query) را از یک مدل داده به یک DTO تبدیل کند، بدون اینکه تمام داده‌ها را از پایگاه داده به حافظه بارگذاری کند. هر دو کتابخانه از این قابلیت پشتیبانی می‌کنند، اما سینتکس Mapster با متد ProjectToType ساده‌تر و روان‌تر است.

// AutoMapper
var users = dbContext.Users
    .ProjectTo<UserDto>(config)
    .ToList();

// Mapster
var users = dbContext.Users
    .ProjectToType<UserDto>()
    .ToList();

خودآزمایی

کدام یک از ویژگی‌های زیر، مزیت اصلی Mapster نسبت به AutoMapper در نظر گرفته می‌شود؟
الف) اکوسیستم بسیار بزرگ و بالغ
ب) استفاده از بازتاب در زمان اجرا
ج) ایمنی در زمان کامپایل و عملکرد بالا
د) مدل کسب‌وکار مبتنی بر لایسنس پولی برای شرکت‌ها


نتیجه‌گیری: انتخاب نگاشت‌گر مناسب

AutoMapper با سال‌ها سابقه، مستندات جامع و جامعه کاربری گسترده، همچنان یک انتخاب مطمئن است و برای بسیاری از پروژه‌ها کارایی دارد. با این حال، تغییر مدل تجاری و رویکرد مبتنی بر بازتاب آن، باعث شده تا توسعه‌دهندگان به دنبال جایگزین‌هایی باشند که هم کارآمدترند و هم با نیازهای مدرن برنامه‌نویسی همخوانی دارند.
در این میان، Mapster با رویکرد نوین تولید کد در زمان کامپایل، عملکرد فوق‌العاده، سینتکس ساده و ایمنی بالا در زمان کامپایل، به سرعت در حال جلب توجه است. این کتابخانه به خصوص برای پروژه‌هایی که کارایی و مصرف بهینه منابع برایشان اولویت دارد، یک گزینه بسیار جذاب و قوی محسوب می‌شود.
در نهایت، انتخاب بین این دو به نیازهای پروژه و تیم توسعه‌دهنده بستگی دارد. اما با توجه به مزایای فنی و اقتصادی Mapster، حتماً ارزش بررسی و امتحان کردن را دارد.


پاسخ خودآزمایی
گزینه صحیح ج) ایمنی در زمان کامپایل و عملکرد بالا است. Mapster با استفاده از تولید کد در زمان کامپایل، خطاهای نگاشت را قبل از اجرای برنامه شناسایی کرده و به دلیل عدم استفاده از بازتاب، عملکرد بهتری ارائه می‌دهد.


مشاهده مطلب اصلی

نظرات

  • علی یگانه مقدم در ۱۴۰۴/۰۶/۲۵ ۰۸:۴۵
    نکته تکمیلی در مورد مپستر
    امکان استفاده از Imapper برای تزریق وابستگی ها نیز فراهم هست و همچنین میتوان با افزودن کانفیگ ها به صورت سینگلتون به سیستم در ابتدا امر اجرای اپلیکیشن، کانفیگ ها را استفاده کرد:

    var config = new TypeAdapterConfig();
    config.NewConfig<User, UserDto>()
        .Map(dest => dest.SerialNumber, src => src.SerialNumber)
        .Ignore(dest => dest.Password);
    
    services.AddSingleton(config);
    services.AddSingleton<IMapper, ServiceMapper>();

    در کد بالا به عنوان نمونه عملیات نگاشت از طریق متد Map و یک عملیات جلوگیری از نگاشت یک فیلد خاص توسط ignore را مشاهده میکنید.نحوه استفاده از آن به صورت زیر میباشد:
    app.MapPost("/test", (IMapper mapper) =>
    {
        var user = new User();
        var dto=mapper.Map<UserDto>(user);
        
        //or
    
        var dto2 = new UserDto();
        mapper.Map(dto, dto2);
    });

    برای بهینه تر شدن این حالت و خودکار سازی فرایندها میتوان از اینترفیس مشترک برای پیاه سازی کانفیگ ها استفاده کرد و با استفاده از رفلکشن تماما آن ها را پیدا کرده به سیستم اضافه کنیم:
    public interface IMapConfiguration
    {
        TypeAdapterConfig AddConfig(TypeAdapterConfig config);
    }

    public static class MapConfigurationService
    {
        public static IServiceCollection ScanMapConfigurations(this IServiceCollection services, Assembly assembly)
        {
            var types = System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
                .Where(t => t.IsClass && t.IsPublic && t.GetInterfaces().Contains(typeof(IMapConfiguration)))
                .Select(t => (IMapConfiguration)Activator.CreateInstance(t))
                .ToList();
            
            
            var config = new TypeAdapterConfig();
    
            foreach (var type in types)
            {
                type.AddConfig(config);
            }
    
    
            services.AddSingleton(config);
            services.AddSingleton<IMapper, ServiceMapper>();
            
            return services;
    
    
        }
    }

    سپس این سرویس را به سیستم اضافه میکنیم:
    builder.Services.ScanMapConfigurations(typeof(UserConfig).Assembly);


    سپس برای هر پیکربندی یک نمونه از کلاس زیر را درست میکنیم:
    public class UserConfig:IMapConfiguration
    {
        public TypeAdapterConfig AddConfig(TypeAdapterConfig config)
        {
            config.NewConfig<User, UserDto>()
                .Map(dest => dest.SerialNumber, src => src.SerialNumber)
                .Ignore(dest => dest.Password);
    
            
            return config;
        }
    }
    در این حالت سیستم، ابتدای اجرا از طریق رفلکشن در اسمبلی که کانفیگ های مپستر در آن قرار دارند به دنبال کلاس هایی میگردد که پیاده سازی اینترفیس IMapConfiguration را داشته باشند تا آن هارا به صورت سینگلتون به سیستم اضافه کند.

    همچنین مپستر توانایی مپ کردن نوع های خاص به صورت عمومی را شامل میشود مثلا نحوه تبدیل رشته به dateonly را یکبار تعریف کنید و از این پس سیستم در همه جا رفتار یکسانی را پیش میگرد یا برای تبدیل نوع های خاص به یک ValueObject میتوان از آن استفاده کرد که با توجه به سلسله مقالات اخیر DDD میتواند بسیار کاربردی باشد:
    public class EmailConfig:IMapConfiguration
    {
        public TypeAdapterConfig AddConfig(TypeAdapterConfig config)
        {
            config.NewConfig<Email, string>()
                .MapWith(src => src.EmailAddress);
            
            config.NewConfig<string, Email>()
                .MapWith(src => Email.Create(src));
            return config;
        }
    }
    با استفاده از متد MapWith میتوان نحوه تعریف type ها به یکدیگر را مشخص کرد و نیاز نیست برای هر نگاشت کانفیگ اختصاصی و تکراری داشته باشیم. همچنین خود مپستر هم شامل پیش فرض هایی مثلا برای Enum میباشد که در صورتی که در طرف مقابل نوع عددی باشد معادل عددی و در صورتی که رشته باشد رشته عبارت تعیین شده در Enum را نگاشت میکند.