مقالات › EF Core و دیتابیس

آموزش Entity Framework Core از صفر؛ Code-First، DbContext و Migration

EF Core را اصولی یاد بگیرید: DbContext، موجودیت‌ها، پیکربندی با Fluent API، مایگریشن، عملیات CRUD، ردیابی تغییرات و عادت‌هایی که لایه‌ی داده را سریع نگه می‌دارند.

Entity Framework Core یا EF Core، ORM رسمی دات‌نت است. با آن می‌توانید به‌جای نوشتن دستی SQL، با کلاس‌های C# و LINQ با دیتابیس رابطه‌ای کار کنید و همچنان کوئری‌های بهینه تحویل بگیرید. در این آموزش روش Code-First را از ابتدا تا انتها یاد می‌گیرید؛ به‌همراه مفاهیمی که فرق «کار می‌کند» با «درست و سریع کار می‌کند» را مشخص می‌کنند.

ORM چیست و چرا به آن نیاز داریم؟

نگاشت‌گر شیء-رابطه‌ای (ORM) بین دو دنیا ترجمه می‌کند: اشیای داخل حافظه و ردیف‌های جدول. بدون ORM باید رشته‌های SQL بنویسید، اتصال باز کنید، DataReader بخوانید و ستون‌ها را دستی به Propertyها نگاشت کنید. EF Core این کارها را خودکار می‌کند:

  • کوئری‌ها با LINQ نوشته و به SQL ترجمه می‌شوند.
  • نتایج به اشیای C# تبدیل می‌شوند.
  • تغییرات اشیا تشخیص داده و با SaveChanges() ذخیره می‌شوند.
  • تغییرات ساختار دیتابیس با Migration نسخه‌بندی می‌شوند.

همچنان باید SQL بلد باشید — EF Core این نیاز را حذف نمی‌کند — اما کد تکراری خیلی کمتری می‌نویسید.

نصب پکیج‌ها

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet tool install --global dotnet-ef

برای دیتابیس‌های دیگر کافی است به‌جای SqlServer از Sqlite، Npgsql.EntityFrameworkCore.PostgreSQL یا Pomelo.EntityFrameworkCore.MySql استفاده کنید. موجودیت‌ها و کوئری‌ها تغییری نمی‌کنند.

تعریف موجودیت‌ها (Entity)

موجودیت‌ها کلاس‌های ساده‌ی C# هستند. EF Core با قراردادها آن‌ها را می‌فهمد: Propertyی با نام Id (یا BlogId) کلید اصلی می‌شود، string به nvarchar تبدیل می‌شود و غیره.

public class Blog
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

    public List<Post> Posts { get; set; } = [];
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Body { get; set; } = string.Empty;
    public bool IsPublished { get; set; }

    public int BlogId { get; set; }        // کلید خارجی
    public Blog Blog { get; set; } = null!; // Navigation Property
}

ساخت DbContext

DbContext جلسه‌ی کاری شما با دیتابیس است. برای هر جدول یک DbSet<T> دارد و پیکربندی مدل را نگه می‌دارد.

public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
    public DbSet<Blog> Blogs => Set<Blog>();
    public DbSet<Post> Posts => Set<Post>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
    }
}

پیکربندی با Fluent API

قراردادها موارد پایه را پوشش می‌دهند؛ Fluent API کنترل کامل به شما می‌دهد. نگه داشتن پیکربندی هر موجودیت در کلاس جداگانه، DbContext را تمیز نگه می‌دارد:

public class PostConfiguration : IEntityTypeConfiguration<Post>
{
    public void Configure(EntityTypeBuilder<Post> builder)
    {
        builder.Property(p => p.Title).HasMaxLength(200).IsRequired();
        builder.HasIndex(p => new { p.BlogId, p.IsPublished });

        builder.HasOne(p => p.Blog)
               .WithMany(b => b.Posts)
               .HasForeignKey(p => p.BlogId)
               .OnDelete(DeleteBehavior.Cascade);
    }
}

نکته: روی ستون‌های رشته‌ای که در آن‌ها جستجو می‌کنید یا ایندکس دارند، حتماً HasMaxLength بگذارید. بدون آن SQL Server از nvarchar(max) استفاده می‌کند که ایندکس‌گذاری مؤثر روی آن ممکن نیست.

ثبت در ASP.NET Core

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

AddDbContext آن را با طول عمر Scoped ثبت می‌کند: یک نمونه برای هر درخواست HTTP. دقیقاً همان چیزی که می‌خواهیم؛ DbContext سبک است، Thread-Safe نیست و باید عمر کوتاهی داشته باشد.

Migration؛ نسخه‌بندی ساختار دیتابیس

هر Migration یک فایل C# است که توضیح می‌دهد دیتابیس چطور از یک نسخه‌ی مدل به نسخه‌ی بعدی برود.

dotnet ef migrations add InitialCreate
dotnet ef database update

هر بار که موجودیت‌ها را تغییر می‌دهید، یک Migration جدید با نام گویا بسازید:

dotnet ef migrations add AddPostSlug
dotnet ef database update

دستورهای کاربردی:

دستور کاربرد
dotnet ef migrations list نمایش همه‌ی Migrationها و وضعیت اعمال‌شدن
dotnet ef migrations remove حذف آخرین Migration (اعمال‌نشده)
dotnet ef database update AddPostSlug رفتن به یک Migration مشخص (حتی به عقب)
dotnet ef migrations script --idempotent تولید اسکریپت SQL برای سرور اصلی

هشدار: در محیط Production بهتر است یک اسکریپت SQL Idempotent (یا Migration Bundle) را بررسی و در فرایند انتشار اجرا کنید؛ نه اینکه چند نمونه از برنامه هم‌زمان در شروع کار Database.Migrate() را صدا بزنند.

عملیات CRUD با EF Core

ایجاد (Create)

var blog = new Blog { Title = "Engineering notes" };
blog.Posts.Add(new Post { Title = "Hello EF Core", Body = "..." });

db.Blogs.Add(blog);
await db.SaveChangesAsync();   // درج وبلاگ و پست در یک تراکنش

خواندن (Read)

var published = await db.Posts
    .AsNoTracking()
    .Where(p => p.IsPublished && p.Blog.Title.StartsWith("Eng"))
    .OrderByDescending(p => p.Id)
    .Select(p => new { p.Id, p.Title, Blog = p.Blog.Title })
    .ToListAsync();

کوئری LINQ تا وقتی با متدهایی مثل ToListAsync، FirstOrDefaultAsync یا CountAsync اجرا (Materialize) نشود، اجرا نمی‌شود. تا آن لحظه فقط یک عبارت می‌سازید که EF Core آن را به یک کوئری SQL ترجمه می‌کند.

به‌روزرسانی (Update)

var post = await db.Posts.FirstAsync(p => p.Id == id);
post.Title = "Updated title";
await db.SaveChangesAsync();   // فقط ستون تغییرکرده UPDATE می‌شود

برای به‌روزرسانی گروهی، اصلاً موجودیت‌ها را بارگذاری نکنید:

await db.Posts
    .Where(p => p.BlogId == blogId)
    .ExecuteUpdateAsync(s => s.SetProperty(p => p.IsPublished, true));

حذف (Delete)

await db.Posts.Where(p => p.Id == id).ExecuteDeleteAsync();

ردیابی تغییرات (Change Tracking) را بشناسید

وقتی موجودیت‌ها را به‌صورت معمولی بارگذاری می‌کنید، DbContext یک تصویر (Snapshot) از آن‌ها نگه می‌دارد. هنگام SaveChanges() مقادیر فعلی را با تصویر مقایسه می‌کند و برای تغییرات دستور UPDATE می‌سازد. هر موجودیت یک وضعیت دارد:

  • Added ← درج می‌شود
  • Modified ← به‌روزرسانی می‌شود
  • Deleted ← حذف می‌شود
  • Unchanged ← کاری لازم نیست
  • Detached ← ردیابی نمی‌شود

ردیابی هزینه‌ی حافظه و پردازش دارد. برای کوئری‌های فقط‌خواندنی — که بخش عمده‌ی یک API معمولی است — از AsNoTracking() استفاده کنید.

بارگذاری داده‌های مرتبط

// Eager Loading: یک کوئری با JOIN
var blogs = await db.Blogs.Include(b => b.Posts).ToListAsync();

// Projection: معمولاً بهترین گزینه — فقط چیزی که نمایش می‌دهید
var summaries = await db.Blogs
    .Select(b => new { b.Title, PostCount = b.Posts.Count })
    .ToListAsync();

در Web APIها از Lazy Loading پرهیز کنید: با هر دسترسی به Navigation بی‌صدا یک کوئری جدید می‌فرستد و عامل کلاسیک مشکل N+1 است.

داده‌ی اولیه (Seed)

modelBuilder.Entity<Blog>().HasData(
    new Blog { Id = 1, Title = "Welcome", CreatedAt = new DateTime(2025, 1, 1) });

HasData برای داده‌های مرجع ثابت (کشورها، وضعیت‌ها) عالی است. برای داده‌های بزرگ یا وابسته به محیط، از کد در زمان شروع برنامه استفاده کنید.

خلاصه‌ی بهترین روش‌ها

  1. DbContext را Scoped و کوتاه‌عمر نگه دارید.
  2. برای خواندن از AsNoTracking() و Projection با Select استفاده کنید.
  3. طول رشته‌ها، ایندکس‌ها و روابط را صریح پیکربندی کنید.
  4. برای Migrationها نام معنادار بگذارید و کد تولیدشده را بررسی کنید.
  5. برای عملیات گروهی از ExecuteUpdate و ExecuteDelete استفاده کنید.
  6. در محیط توسعه SQL تولیدشده را لاگ کنید (options.LogTo(Console.WriteLine)) و آن را بخوانید.

جمع‌بندی

EF Core اجازه می‌دهد دیتابیس را با کلاس‌های C# مدل کنید، با LINQ کوئری بزنید و با Migration به‌شکل امن تغییرش دهید. اگر طول عمر DbContext، ردیابی تغییرات و نحوه‌ی ترجمه‌ی کوئری‌ها را خوب بفهمید، کد دسترسی به داده‌ای می‌نویسید که هم سریع نوشته می‌شود و هم سریع اجرا می‌شود.

پرسش‌های متداول

آیا EF Core از SQL خام یا Dapper کندتر است؟

در کوئری‌های معمولی تفاوت کم است، مخصوصاً با AsNoTracking و Projection. برای مسیرهای بسیار پرتکرار می‌توانید SQL خام (FromSql) یا Dapper را کنار EF Core استفاده کنید؛ این دو بدون مشکل با هم کار می‌کنند.

Code-First بهتر است یا Database-First؟

برای پروژه‌های جدید Code-First ایده‌آل است: مدل داخل سورس‌کنترل قرار می‌گیرد و Migrationها همه‌ی تغییرات را مستند می‌کنند. برای دیتابیس موجود از dotnet ef dbcontext scaffold استفاده کنید.

آیا Database.Migrate() را در شروع برنامه صدا بزنم؟

برای برنامه‌های کوچک و محیط توسعه راحت است. در انتشار چندنمونه‌ای (Multi-instance)، Migration را به‌عنوان یک مرحله‌ی کنترل‌شده در فرایند انتشار اعمال کنید.