بررسی دقیق عملکرد AutoMapper
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: پنج دقیقه

همانطور که اطلاع دارید، AutoMapper ابزاری برای نگاشت خودکار بین Model و Dto می‌باشد؛ که به صورت نادرست تصور کاهش سرعت در استفاده کردن از آن، بین توسعه دهندگان جا افتاده‌است. در این مقاله قصد داریم به صورت دقیق، به بررسی سرعت عملکرد استفاده از AutoMapper و مقایسه آن با نگاشت دستی بپردازیم.
کد‌های کامل این قسمت را میتوانید از اینجا clone کرده و شخصا تست نمایید.

ابتدا یک پروژه‌ی Console Application را ساخته و AutoMapper را به همراه Ef6، نصب مینماییم. سپس دو کلاس جدید را به نام‌های User و Address به صورت زیر در پوشه‌ی Models مینویسیم.
using System.Collections.Generic;

namespace AutoMapperComparison.Models
{
    public class User
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public ICollection<Address> Addresses { get; set; }
    }
}
using System.ComponentModel.DataAnnotations.Schema;

namespace AutoMapperComparison.Models
{
    public class Address
    {
        public int Id { get; set; }

        public double? Code { get; set; }

        public string Title { get; set; }

        public int UserId { get; set; }

        [ForeignKey(nameof(UserId))]
        public virtual User User { get; set; }
    }
}
بدیهی است که این دو مدل با همدیگر رابطه‌ی 1 به چند دارند. حال کافیست AppDbContext خود را به صورت زیر تعریف نماییم.
نکته: در متد Seed، برای ثبت رکورد‌های اولیه، از BulkInsert استفاده شده است (باید پکیج BulkInsert را نیز نصب نمایید)
using EntityFramework.BulkInsert.Extensions;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;

namespace AutoMapperComparison.Models
{
    public class AppDbContextInitializer : DropCreateDatabaseAlways<AppDbContext>
    {
        protected override void Seed(AppDbContext context)
        {
            User user = context.Users.Add(new User { Name = "Test" });

            context.SaveChanges();

            List<Address> addresses = new List<Address>();

            for (int i = 0; i < 500000; i++)
            {
                addresses.Add(new Address { Id = i, Code = 1, Title = "Test", UserId = user.Id });
            }

            context.BulkInsert(addresses);

            base.Seed(context);
        }
    }

    public class AppDbContext : DbContext
    {
        static AppDbContext()
        {
            Database.SetInitializer(new AppDbContextInitializer());

            //Database.SetInitializer<AppDbContext>(null);
        }

        public AppDbContext()
            : base(new SqlConnection(@"Data Source=.;Initial Catalog=AppDbContext;Integrated Security=True"), contextOwnsConnection: true)
        {
            Configuration.AutoDetectChangesEnabled = false;
            Configuration.EnsureTransactionsForFunctionsAndCommands = false;
            Configuration.LazyLoadingEnabled = false;
            Configuration.ProxyCreationEnabled = false;
            Configuration.ValidateOnSaveEnabled = false;
            Configuration.UseDatabaseNullSemantics = false;
        }

        public DbSet<User> Users { get; set; }

        public DbSet<Address> Addresses { get; set; }
    }
}
 برای اینکه مقایسه انجام شده دقیق باشد، تمامی Configuration‌های اضافی را نیز غیر فعال نموده‌ام.
فقط نیاز داریم یک Dto را برای Address نیز تعریف کنیم؛ چون قرار است نگاشت از Model به Dto از روی Address و AddressDto انجام شود.
کلاس AddressDto را به صورت زیر ایجاد میکنیم:
namespace AutoMapperComparison.Models
{
    public class AddressDto
    {
        public int Id { get; set; }

        public double? Code { get; set; }

        public string Title { get; set; }

        public int UserId { get; set; }

        public string UserName { get; set; }
    }
}
قرار است به صورت خودکار از طریق AutoMapper و همچنین به صورت دستی، نگاشت از Model به Dto مربوطه انجام شود.
 حال نیاز است فایل Program.cs را باز کرده و تغییرات زیر را اعمل نماییم:
using AutoMapper;
using AutoMapper.QueryableExtensions;
using AutoMapperComparison.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace AutoMapperComparison
{
    public class Program
    {
        public static void Main()
        {
            Mapper.Initialize(cfg =>
            {
                cfg.CreateMap<Address, AddressDto>();
            });

            Console.WriteLine($"Create Db {DateTimeOffset.UtcNow}");
            using (AppDbContext db = new AppDbContext())
            {
                db.Database.Initialize(force: true);
                db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS"); //Removes all clean buffers from the buffer pool, and columnstore objects from the columnstore object pool
                Console.WriteLine(db.Addresses.ProjectTo<AddressDto>());
                Console.WriteLine(db.Addresses.Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name }));
            }

            Console.WriteLine($"Normal Select {DateTimeOffset.UtcNow}");
            using (AppDbContext db = new AppDbContext())
            {
                db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS");
                Stopwatch watch = Stopwatch.StartNew();
                List<AddressDto> addresses = db.Addresses.AsNoTracking().Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name }).ToList();
                List<AddressDto> addresses2 = db.Addresses.AsNoTracking().Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name }).ToList();
                watch.Stop();
                Console.WriteLine($"{watch.ElapsedMilliseconds} {addresses.Count} {addresses2.Count}");
            }

            Console.WriteLine($"AutoMapper Exec {DateTimeOffset.UtcNow}");
            using (AppDbContext db = new AppDbContext())
            {
                db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS");
                Stopwatch watch = Stopwatch.StartNew();
                List<AddressDto> addresses = db.Addresses.AsNoTracking().ProjectTo<AddressDto>().ToList();
                List<AddressDto> addresses2 = db.Addresses.AsNoTracking().ProjectTo<AddressDto>().ToList();
                watch.Stop();
                Console.WriteLine($"{watch.ElapsedMilliseconds} {addresses.Count} {addresses2.Count}");
            }

            Console.ReadKey();
        }
    }
}
نکته: از دستور DBCC DROPCLEANBUFFERS جهت خالی کردن بافر sql server برای رسیدن به نتیجه‌ی هرچه دقیق‌تر استفاده شده است.
بعد از اجرا کردن، ابتدا بررسی میکنیم که کوئری اجرا شده‌ی دو نوع مختلف، هیچ تفاوتی با هم نداشته باشند.


حال نتایج بدست آمده، در قسمت پایین‌تر آن نمایان میشود:


البته نتیجه‌ی این آزمایش بسته به سخت افزار سیستم شما ممکن است کمی متفاوت باشد.

در سه آزمایش دیگر به صورت متوالی نتیجه‌ی زیر بدست آمد:

Normal   AutoMapper
 2451  2378
 2120  2111
 2202  2124

اگر این مقدار جزئی از تفاوت بین دو نوع مختلف آزمایش را مورد نظر نگیریم، میتوان گفت که هر دو روش نتیجه‌ی کاملا یکسانی خواهند داشت. فقط با استفاده از AutoMapper کد‌های کمتری نوشته شده‌است!

اما دلیل چیست؟ از آنجایی که ProjectTo از Dto به Model انجام شده و Lambda Expressionی که به سمت Entity Framework فرستاده شده‌است با روش Normal کاملا برابر است و بقیه‌ی عملیات توسط EF انجام میشود، با قاطعیت میتوان گفت که هر دو روش ذکر شده از نظر Performance کاملا یکسان خواهند بود.

نکته: البته به این موضوع باید توجه شود که اگر همین آزمایش را بطور مثال با استفاده از یک Listی از رکورد‌های درون Memory ساخته شده توسط خودمان انجام دهیم، آن موقع نتیجه‌ی یکسانی نخواهیم داشت، به دلیل اینکه EFی دیگر وجود نخواهد داشت که مسئولیت بازگشت داده‌ها را بر عهده بگیرد. از آنجائیکه اکثر کارهایی که توقع داریم AutoMapper برای ما انجام دهد، توسط ORM بازگشت داده میشود، پس میتوان گفت نکته‌ی فوق تقریبا در دنیای واقعی رخ نخواهد داد و باعث مشکل نخواهد شد.