← All tutorials

EF Core — چگونه کار می‌کند

درک Entity Framework Core و تفاوتش با ADO.NET و Dapper

بنر EF Core: سه مسیر EF Core، Dapper و ADO.NET که به SQL می‌رسند.
هر سه راه به دیتابیس می‌رسند؛ تفاوت در میزان کنترلی است که خودتان می‌نویسید.

در این آموزش چه یاد می‌گیریم؟

در این آموزش توضیح می‌دهیم Entity Framework Core (EF Core) چیست، در پشت‌صحنه چگونه کار می‌کند، و در مقایسه با دو روش رایج دیگر — ADO.NET خام و micro-ORM به نام Dapper — چه تفاوتی دارد. یک کوئری یکسان را به سه روش می‌بینید، با ردیابی تغییرات (change tracking) و واحد کار (unit of work) آشنا می‌شوید و یاد می‌گیرید هرکدام را چه زمانی انتخاب کنید.

در پایان نه‌تنها می‌دانید چگونه از EF Core استفاده کنید، بلکه می‌فهمید چرا این‌طور رفتار می‌کند — و چه زمانی ابزار سبک‌تری انتخاب بهتری است.

۱. EF Core چیست؟

Entity Framework Core ابزار رسمی ORM (نگاشت شیء به رابطه‌ای) مایکروسافت برای دات‌نت است. به زبان ساده: به‌جای نوشتن دستی SQL، با دیتابیس از طریق کلاس‌ها و شیءهای معمولی C# کار می‌کنید. کافی است db.Orders.Add(order) و SaveChanges() را بنویسید؛ EF Core خودش دستور INSERT را می‌سازد، اجرا می‌کند و نتیجه را به‌صورت شیء برمی‌گرداند.

EF Core بازنویسی مدرن، چندسکویی و متن‌باز Entity Framework کلاسیک است. از طریق providerهای دیتابیس با SQL Server، PostgreSQL، MySQL، SQLite و بیشتر کار می‌کند.

۲. ORM چیست؟

دیتابیس با جدول، ردیف و ستون فکر می‌کند. کد C# شما با کلاس، شیء و property فکر می‌کند. یک ORM مترجم بین این دو دنیاست — یک ردیف از جدول Orders را به یک نمونه از کلاس Order شما «نگاشت» می‌کند و بالعکس.

به این موضوع اغلب عدم‌تطابق شیء–رابطه‌ای (impedance mismatch) می‌گویند و ORM برای پنهان‌کردن آن وجود دارد:

شیء C# (Order { Id = 1, Total = 99 })
↕
ORM بین این دو نگاشت می‌کند
↕
ردیف دیتابیس (Id=1 | Total=99)

۳. EF Core چگونه کار می‌کند

۳.۱ DbContext و DbSet

همه‌چیز در EF Core حول DbContext می‌چرخد. این یک نشست (session) با دیتابیس را نشان می‌دهد — یک unit of work. درون آن، هر DbSet<T> نماینده‌ی یک جدول است که می‌توانید روی آن کوئری بزنید و ذخیره کنید.

public class ShopContext : DbContext
{
    public DbSet<Order> Orders => Set<Order>();
    public DbSet<Customer> Customers => Set<Customer>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer("...connection string...");
}

public class Order
{
    public int Id { get; set; }
    public decimal Total { get; set; }
    public DateTime CreatedAt { get; set; }
}

۳.۲ ردیابی تغییرات (Change Tracking)

این «جادوی» EF Core است — و بزرگ‌ترین تفاوتش با بقیه. وقتی یک موجودیت (entity) را بارگذاری می‌کنید، EF Core یک عکس فوری (snapshot) از آن در change tracker نگه می‌دارد. سپس با فراخوانی SaveChanges()، مقادیر فعلی را با snapshot مقایسه می‌کند و فقط برای فیلدهای تغییرکرده، خودکار دستور INSERT/UPDATE/DELETE مناسب را می‌سازد.

var order = db.Orders.First(o => o.Id == 1);
order.Total = 150;          // فقط یک property را تغییر می‌دهید
db.SaveChanges();           // EF خودش تشخیص می‌دهد و UPDATE می‌زند
شما هیچ UPDATEی ننوشتید. EF Core متوجه تغییر Total شد و خودش آن را اجرا کرد. این «واحد کار» (unit of work) همه‌ی تغییرات را دسته می‌کند و در یک تراکنش ذخیره می‌کند.

۳.۳ ترجمه‌ی LINQ به SQL

شما با LINQ (در C#) کوئری می‌زنید، و provider مربوط به EF Core آن expression tree را به SQL مخصوص دیتابیس شما ترجمه می‌کند. کوئری روی سرور اجرا می‌شود، نه در حافظه.

var bigOrders = db.Orders
    .Where(o => o.Total > 100)
    .OrderByDescending(o => o.CreatedAt)
    .ToList();

// EF Core این را تقریباً به این ترجمه می‌کند:
// SELECT * FROM Orders WHERE Total > 100 ORDER BY CreatedAt DESC
اگر اشتباهی اجرای کوئری را زودتر در حافظه مجبور کنید (مثلاً زود .ToList() بزنید و بعد فیلتر کنید)، EF می‌تواند اول کل جدول را به حافظه بیاورد. همیشه قبل از materialize کردن فیلتر کنید.

۳.۴ چرخه‌ی کامل یک کوئری

یک کوئری LINQ روی DbSet می‌نویسی
↓
EF یک expression tree می‌سازد
↓
provider آن را به SQL ترجمه می‌کند
↓
ADO.NET (در زیر!) آن SQL را روی دیتابیس اجرا می‌کند
↓
ردیف‌ها به شیءهای C# تبدیل (materialize) می‌شوند
↓
entity های ردیابی‌شده به change tracker اضافه می‌شوند
به این نکته‌ی کلیدی دقت کنید: EF Core در داخل خودش از ADO.NET استفاده می‌کند. یک لایه روی آن است — نه جایگزینی برای زیرساخت سطح‌پایین.

۴. سه لایه: ADO.NET، Dapper، EF Core

هر سه در نهایت SQL را روی همان connection مربوط به ADO.NET می‌فرستند. تفاوت در این است که چقدر کار را برای شما انجام می‌دهند:

EF Core — ORM کامل: LINQ، change tracking، migration (بیشترین انتزاع)
↓ ساخته شده روی
Dapper — micro-ORM: شما SQL می‌نویسید، Dapper نتیجه را به شیء نگاشت می‌کند
↓ ساخته شده روی
ADO.NET — خام: connection، command، data reader (کمترین انتزاع)

۵. ADO.NET به‌اختصار

ADO.NET همان API داده‌ی سطح‌پایینِ توکارِ دات‌نت است. شما connection را مدیریت می‌کنید، SQL را می‌نویسید، پارامتر اضافه می‌کنید، اجرا می‌کنید و هر ستون را دستی از یک DataReader می‌خوانید. بیشترین کنترل، بیشترین کدِ تکراری.

using var conn = new SqlConnection(connStr);
conn.Open();

using var cmd = new SqlCommand(
    "SELECT Id, Total FROM Orders WHERE Total > @min", conn);
cmd.Parameters.AddWithValue("@min", 100);

using var reader = cmd.ExecuteReader();
var orders = new List<Order>();
while (reader.Read())
{
    orders.Add(new Order
    {
        Id = reader.GetInt32(0),
        Total = reader.GetDecimal(1)
    });
}

۶. Dapper به‌اختصار

Dapper یک micro-ORM است: مجموعه‌ای نازک از extension method ها روی ADO.NET. شما همچنان خودتان SQL را می‌نویسید، ولی Dapper بخش خسته‌کننده را انجام می‌دهد — باز کردن reader و نگاشت هر ردیف به شیء شما. به‌خاطر سرعت بسیار بالایش معروف است، خیلی نزدیک به ADO.NET خام.

using var conn = new SqlConnection(connStr);

var orders = conn.Query<Order>(
    "SELECT Id, Total FROM Orders WHERE Total > @min",
    new { min = 100 }).ToList();
یک خط همان کاری را می‌کند که ADO.NET برایش حدود ۱۵ خط لازم داشت — ولی شما همچنان کنترل کامل روی SQL دارید. Dapper تغییرات را ردیابی نمی‌کند و SQL برایتان تولید نمی‌کند.

۷. یک کوئری، سه روش

«همه‌ی سفارش‌های بالای ۱۰۰ را بیاور.» خلاصه‌ی هر روش:

ADO.NET

// باز کردن connection، ساخت command، افزودن پارامتر،
// اجرای reader، حلقه روی ردیف‌ها، نگاشت دستی هر ستون

Dapper

conn.Query<Order>("SELECT * FROM Orders WHERE Total > @min",
                  new { min = 100 });

EF Core

db.Orders.Where(o => o.Total > 100).ToList();

روند را ببینید: ADO.NET = همه‌چیز با شماست؛ Dapper = شما SQL می‌نویسید، Dapper نگاشت می‌کند؛ EF Core = شما C# می‌نویسید، EF Core بقیه را انجام می‌دهد.

۸. هرکدام یک کوئری مشابه را چگونه اجرا می‌کنند

بیایید یک کار را برداریم — «همه‌ی سفارش‌هایی که مبلغشان از یک مقدار مشخص بیشتر است را بیاور» — و دقیقاً دنبال کنیم هر ابزار در داخل چه می‌کند. SQL ای که به دیتابیس می‌رسد در عمل یکسان است؛ چیزی که فرق می‌کند کارهای پیرامون آن است.

ADO.NET — هر مرحله را خودتان انجام می‌دهید

using var conn = new SqlConnection(connStr);
conn.Open();                                   // ۱. connection را باز می‌کنید

using var cmd = new SqlCommand(                // ۲. command را می‌سازید
    "SELECT Id, Total, CreatedAt FROM Orders WHERE Total > @min", conn);
cmd.Parameters.AddWithValue("@min", 100);     // ۳. پارامتر را اضافه می‌کنید

using var reader = cmd.ExecuteReader();        // ۴. اجرا می‌کنید و reader می‌گیرید
var orders = new List<Order>();
while (reader.Read())                           // ۵. روی هر ردیف حلقه می‌زنید
{
    orders.Add(new Order                       // ۶. هر ستون را دستی نگاشت می‌کنید
    {
        Id = reader.GetInt32(0),
        Total = reader.GetDecimal(1),
        CreatedAt = reader.GetDateTime(2)
    });
}
// ۷. همه‌چیز بسته می‌شود (اینجا using این کار را می‌کند)
چه اتفاقی می‌افتد: جادویی نیست. شما connection، متن SQL، پارامتر، حلقه‌ی خواندن و نگاشت را کنترل می‌کنید. اگر ستونی اضافه شود، باید نگاشت را دستی ویرایش کنید.

Dapper — مرحله‌های ۴ تا ۶ را برای شما انجام می‌دهد

using var conn = new SqlConnection(connStr);

var orders = conn.Query<Order>(                 // باز کردن+اجرا+خواندن+نگاشت در یک فراخوانی
    "SELECT Id, Total, CreatedAt FROM Orders WHERE Total > @min",
    new { min = 100 }).ToList();
چه اتفاقی می‌افتد: شما همچنان SQL دقیق را به Dapper می‌دهید. در داخل، در صورت نیاز connection را باز می‌کند، یک command مربوط به ADO.NET با پارامتر شما اجرا می‌کند، سپس با کدِ کامپایل‌شده‌ی سریع هر ردیف را با تطبیق نام ستون‌ها به property ها به یک Order نگاشت می‌کند. نه change tracking، نه تولید SQL — فقط نگاشت.

EF Core — حتی SQL را هم خودش می‌نویسد

var orders = db.Orders
    .Where(o => o.Total > 100)                 // یک expression tree از LINQ
    .ToList();                                  // اجرا اینجا کلید می‌خورد

در داخل، به‌ترتیب:

EF از روی LINQ شما یک expression tree می‌سازد
↓
provider آن را به SQL پارامتری ترجمه می‌کند (WHERE [Total] > @__p_0)
↓
آن SQL را از طریق ADO.NET اجرا می‌کند (در زیر، مثل بقیه)
↓
ردیف‌ها به شیءهای Order تبدیل (materialize) می‌شوند
↓
هر شیء در change tracker ثبت می‌شود (مگر AsNoTracking)
نکته‌ی کلیدی: هر سه تقریباً همان SQL را از طریق همان مسیر ADO.NET می‌فرستند. ADO.NET نگاشت را به‌عهده‌ی شما می‌گذارد؛ Dapper نگاشت را انجام می‌دهد؛ EF Core هم SQL را می‌نویسد، هم نگاشت می‌کند، هم نتیجه را ردیابی می‌کند.

۹. اجرای Stored Procedure در هر کدام

فرض کنید دیتابیس این پروسیجر را دارد:

CREATE PROCEDURE GetOrdersByMinTotal
    @min DECIMAL(18,2)
AS
BEGIN
    SELECT Id, Total, CreatedAt
    FROM Orders
    WHERE Total > @min;
END

ADO.NET — مقدار CommandType را روی StoredProcedure بگذار

using var conn = new SqlConnection(connStr);
conn.Open();

using var cmd = new SqlCommand("GetOrdersByMinTotal", conn);
cmd.CommandType = CommandType.StoredProcedure;   // خط مهم
cmd.Parameters.AddWithValue("@min", 100);

using var reader = cmd.ExecuteReader();
var orders = new List<Order>();
while (reader.Read())
{
    orders.Add(new Order
    {
        Id = reader.GetInt32(0),
        Total = reader.GetDecimal(1),
        CreatedAt = reader.GetDateTime(2)
    });
}

به‌جای نوشتن SQL، نام پروسیجر را می‌دهید، CommandType را تنظیم می‌کنید، پارامتر اضافه می‌کنید و نتیجه را خودتان نگاشت می‌کنید — دقیقاً مثل یک کوئری معمولی.

Dapper — تمیزترینِ این سه برای پروسیجر

using var conn = new SqlConnection(connStr);

var orders = conn.Query<Order>(
    "GetOrdersByMinTotal",
    new { min = 100 },
    commandType: CommandType.StoredProcedure).ToList();
کافی است commandType: CommandType.StoredProcedure را پاس دهید و Dapper نتیجه را خودکار نگاشت می‌کند. برای پارامتر خروجی (output) از DynamicParameters استفاده کنید و پس از اجرا مقدارش را بخوانید. به همین دلیل وقتی کدبیس به‌شدت به stored procedure تکیه دارد، Dapper محبوب است.

EF Core — بستگی دارد پروسیجر چه برمی‌گرداند

الف) پروسیجر ردیف‌هایی از یک entity / نوعِ نگاشت‌شده برمی‌گرداند:

var orders = db.Orders
    .FromSqlInterpolated($"EXEC GetOrdersByMinTotal {100}")
    .ToList();

ب) پروسیجر ردیف‌هایی از نوعی برمی‌گرداند که جدول نیست — ابتدا آن را به‌عنوان keyless entity تعریف کنید:

// در OnModelCreating:
modelBuilder.Entity<OrderDto>().HasNoKey();

// کوئری:
var rows = db.Set<OrderDto>()
    .FromSqlInterpolated($"EXEC GetOrdersByMinTotal {100}")
    .ToList();

ج) پروسیجر فقط کاری انجام می‌دهد / تعداد ردیف برمی‌گرداند (بدون result set):

db.Database.ExecuteSqlInterpolated(
    $"EXEC ArchiveOldOrders {cutoffDate}");
با FromSql*، EF نمی‌تواند LINQ بعدی را که دیتابیس روی نتیجه‌ی پروسیجر پشتیبانی نمی‌کند، ترجمه کند. ستون‌های برگشتی باید با propertyهای نوع مقصد همخوان باشند. برای امنیت در برابر SQL injection، همیشه FromSqlInterpolated یا پارامتر را به‌جای چسباندن رشته‌ها ترجیح دهید.
ابزارنحوه‌ی فراخوانی پروسیجرنگاشت
ADO.NETCommandType.StoredProcedureدستی
DappercommandType: StoredProcedureخودکار
EF CoreFromSqlInterpolated / ExecuteSql*خودکار (entity / نوع keyless)

۱۰. در پشت‌صحنه: تفاوت‌های فنی و کارایی

هر سه تقریباً SQL یکسانی می‌فرستند، پس دیتابیس همان کار را انجام می‌دهد. تفاوت کارایی کاملاً در سمت دات‌نت است: نحوه‌ی خواندن ردیف‌ها، ساخت شیءها و میزان سربار هر ابزار برای هر ردیف. بیایید لایه‌به‌لایه بررسی کنیم.

۱۰.۱ Connection و Command

اینجا تفاوتی نیست — هر سه از همان DbConnection مربوط به ADO.NET و connection pooling استفاده می‌کنند. SqlConnection هر بار واقعاً یک socket خام باز نمی‌کند؛ یک connection فیزیکیِ pool‌شده را قرض می‌گیرد. پس «باز کردن» connection در هر سه ارزان است. پارامترها هم در همه‌ی موارد به‌صورت پارامتر واقعی SQL فرستاده می‌شوند (تا دیتابیس بتواند execution plan را دوباره استفاده کند).

۱۰.۲ خواندن ردیف‌ها — DataReader

در زیر، هر سه نتایج را از طریق یک DbDataReader می‌خوانند که یک cursor ‏فقط-روبه‌جلو و streaming است. هر بار یک ردیف را از بافر شبکه می‌خواند — خودش به‌تنهایی کل result set را به حافظه نمی‌آورد. آنچه شما روی آن می‌سازید جایی است که حافظه مصرف می‌شود.

۱۰.۳ نگاشت ردیف به شیء — تفاوت اصلی

ابزارنگاشت از نظر فنی چطور کار می‌کندهزینه
ADO.NETشما reader.GetInt32(0) و... را دستی فراخوانی می‌کنید. مستقیم، typed، بدون reflection و بدون تخصیص حافظه‌ی اضافه.پایه — سریع‌ترین حالت
Dapperدر اولین فراخوانی برای هر (شکل کوئری + نوع)، delegate نگاشت را به‌صورت IL تولید و cache می‌کند. فراخوانی‌های بعدی همان delegate کامپایل‌شده را اجرا می‌کنند — تقریباً به‌سرعت کد دست‌نویس.یک‌بار warm-up، سپس نزدیک به ADO.NET
EF Corematerializer می‌سازد، موجودیت‌های ردیابی‌شده را ایجاد می‌کند، snapshot محاسبه می‌کند و آن‌ها را در identity map / change tracker ثبت می‌کند.CPU و حافظه‌ی بیشتر برای هر ردیف
این باور که «Dapper از reflection کُند استفاده می‌کند» قدیمی است. Dapper یک‌بار reflection می‌کند تا IL تولید کند، delegate کامپایل‌شده را cache می‌کند و از آن به بعد کد کامپایل‌شده است — نه reflection.

۱۰.۴ EF Core زمان اضافه را کجا صرف می‌کند

برای دقیقاً همان کوئری، EF Core چند کار انجام می‌دهد که بقیه نمی‌کنند:

  • ترجمه‌ی کوئری: expression tree مربوط به LINQ باید پارس و به SQL ترجمه شود. این cache می‌شود (compiled query cache که با شکل کوئری کلید می‌خورد)، پس بیشتر هزینه‌ی بار اول است — ولی خودِ جست‌وجوی cache هم رایگان نیست.
  • ردیابی تغییرات: هر entity ساخته‌شده یک tracking entry به‌علاوه یک snapshot از مقادیر اصلی‌اش می‌گیرد که در identity map ذخیره می‌شود. این یعنی تخصیص حافظه و CPU بیشتر برای هر ردیف.
  • تشخیص هویت (Identity resolution): EF بررسی می‌کند آیا entity ای با آن کلید از قبل ردیابی شده تا همان نمونه را برگرداند (برای درستی عالی است، ولی رایگان نیست).
  • Fix-up: با بارگذاری داده‌ی مرتبط، EF navigation propertyها را بین موجودیت‌های ردیابی‌شده به هم وصل می‌کند.
به همین دلیل AsNoTracking() برای کوئری‌های فقط‌خواندنی سریع‌تر است: از snapshot، ثبت در identity map و fix-up صرف‌نظر می‌کند. برای لیست بزرگ فقط‌خواندنی، هزینه و حافظه‌ی سمت دات‌نت تقریباً نصف می‌شود.

۱۰.۵ Buffered در برابر streaming و حافظه

  • ADO.NET: شما تصمیم می‌گیرید — ردیف‌به‌ردیف از reader بخوانید و حافظه را برای میلیون‌ها ردیف ثابت نگه دارید.
  • Dapper: پیش‌فرض buffered است (Query<T> همه‌چیز را در List می‌خواند). با buffered: false به‌صورت stream بخوانید و حافظه را کم نگه دارید.
  • EF Core: .ToList() همه را buffer می‌کند؛ AsAsyncEnumerable() یا پیمایش کوئری stream می‌کند. موجودیت‌های ردیابی‌شده در context باقی می‌مانند؛ پس یک کوئری بزرگ ردیابی‌شده تا dispose شدن context، حافظه را مدام افزایش می‌دهد.

۱۰.۶ شروع سرد (cold start) در برابر مسیر گرم

EF Core یک هزینه‌ی بار اولِ محسوس دارد: ساخت model، کامپایل کوئری، گرم‌کردن cache ها. بعد از warm-up خیلی سریع‌تر است. ADO.NET عملاً warm-up ندارد؛ Dapper یک warm-up کوچک برای هر (کوئری، نوع) دارد تا IL تولید کند. برای ابزارهای CLI کوتاه‌عمر یا cold start های serverless، این تفاوتِ بار اول می‌تواند مهم باشد.

۱۰.۷ کارایی به‌صورت تقریبی

ابزارسرعت خواندنِ نسبی*تخصیص حافظه / ردیفسربار CPU
ADO.NET۱.۰× (پایه، سریع‌ترین)کمترینحداقلی
Dapper~۱.۰ تا ۱.۱× (نزدیک به خام)کمبسیار کم
EF Core (با AsNoTracking)~۱.۲ تا ۱.۵×متوسطکم تا متوسط
EF Core (با tracking)~۱.۵ تا ۳ برابر کندتربیشترینبیشترین

* مرتبه‌ی بزرگیِ تقریبی برای کوئری‌های خواندنِ ساده؛ اعداد واقعی به‌شدت به شکل کوئری، تعداد ردیف و نسخه بستگی دارند. همیشه روی بار کاری خود benchmark بگیرید (مثلاً با BenchmarkDotNet).

زودهنگام بیش‌ازحد بهینه نکن. برای اغلب endpoint های CRUD، شبکه + دیتابیس بیشترِ زمان کل را می‌گیرند و سربار EF Core ناچیز است. این سربار فقط در مسیرهای خواندنِ داغ و پرحجم دیده می‌شود — دقیقاً همان‌جا که تیم‌ها به‌صورت گزینشی سراغ Dapper می‌روند.

۱۱. مقایسه‌ی کامل

جنبهADO.NETDapperEF Core
نوعAPI داده‌ی خامMicro-ORMORM کامل
چه کسی SQL می‌نویسدشماشماEF (از روی LINQ)
نگاشت شیءدستیخودکارخودکار
ردیابی تغییراتنداردندارددارد
Migrationنداردندارددارد
رابطه‌ها / joinدستیدستی (multi-mapping)navigation property و Include
کاراییسریع‌تریننزدیک به خامکمی سربار
کد تکراریزیادکمکمترین
منحنی یادگیریمفهوم ساده، کد زیادکمبیشتر (LINQ، tracking، تنظیمات)
مناسب برایکنترل کامل / موارد خاصخواندن‌محور و حساس به کاراییدامنه‌ی غنی، توسعه‌ی سریع

۱۲. اشتباهات رایج در EF Core

  • کوئری‌های N+1: حلقه روی entity ها و دست‌زدن تنبلانه به یک navigation property، برای هر ردیف یک کوئری جدا می‌زند. به‌جایش از .Include() یا projection استفاده کنید.
  • tracking وقتی لازم نیست: برای لیست‌های فقط‌خواندنی از .AsNoTracking() استفاده کنید تا snapshot نگیرد و سریع‌تر شود.
  • آوردن بیش‌ازحد داده: فقط ستون‌های موردنیاز را با projection (.Select(o => new {...})) انتخاب کنید نه کل entity را.
  • اجرای سمت کلاینت: صدا زدن یک متد C# که EF نمی‌تواند ترجمه‌اش کند، فیلتر را به حافظه می‌برد.
  • DbContext طولانی‌عمر: context نه thread-safe است و نه باید عمر طولانی داشته باشد چون entity های ردیابی‌شده انباشته می‌شوند. آن را کوتاه‌عمر نگه دارید (هر request / هر unit of work).

۱۳. چه زمانی از کدام استفاده کنیم

EF Core را انتخاب کنید وقتی:
  • مدل دامنه‌ی غنی با روابطی دارید که می‌خواهید پیمایش کنید.
  • بهره‌وری توسعه، migration و کاهش SQL برایتان مهم است.
  • بیشتر دسترسی به داده‌تان CRUD استاندارد است.
Dapper را انتخاب کنید وقتی:
  • کوئری‌های خواندن‌محور و حساس به کارایی دارید.
  • کنترل کامل روی SQL بهینه‌شده می‌خواهید ولی از نگاشت دستی خوشتان نمی‌آید.
  • گزارش‌های پیچیده یا stored procedure اجرا می‌کنید.
ADO.NET خام را انتخاب کنید وقتی:
  • به کنترل مطلق نیاز دارید (عملیات bulk، providerهای خاص، streaming).
  • کتابخانه می‌سازید و نمی‌خواهید وابستگی اضافی داشته باشید.

در عمل: بسیاری از تیم‌ها از EF Core برای نوشتن و CRUD استفاده می‌کنند و برای مسیرهای خواندن پرترافیک به Dapper می‌روند. این دو روی یک connection به‌خوبی کنار هم کار می‌کنند — لزوماً یکی از دو تاست.


جمع‌بندی نهایی

  • EF Core یک ORM کامل است: LINQ می‌نویسید، SQL دریافت می‌کنید، همراه با change tracking و migration.
  • با نگاشت کلاس‌ها به جدول‌ها، ردیابی تغییرات و ترجمه‌ی LINQ به SQL کار می‌کند — روی بستر ADO.NET.
  • ADO.NET پایه‌ی خام است؛ Dapper یک mapper نازک و سریع که در آن همچنان خودتان SQL می‌نویسید.
  • معامله: انتزاع بیشتر = کد کمتر ولی کمی سربار؛ انتزاع کمتر = کنترل و سرعت بیشتر.
  • می‌توانید — و اغلب باید — EF Core و Dapper را در یک اپ ترکیب کنید.

حالا می‌دانید EF Core چگونه کار می‌کند و ADO.NET و Dapper کجا قرار می‌گیرند. 🗄️