آموزش Entity Framework Core در C#: ORM قدرتمند برای دات‌نت

فهرست مطالب

مقدمه: در دنیای توسعه نرم‌افزار، مدیریت داده‌ها و تعامل با پایگاه‌های داده، یکی از هسته‌های اصلی هر برنامه کاربردی است. توسعه‌دهندگان همواره به دنبال راه‌حل‌هایی بوده‌اند که این فرآیند را ساده‌تر، کارآمدتر و قابل نگهداری‌تر کند. در اکوسیستم دات‌نت، چارچوب‌هایی مانند ADO.NET از دیرباز پایه‌ای برای این تعاملات بوده‌اند. اما با پیچیده‌تر شدن سیستم‌ها و نیاز به انتزاع بیشتر، مفهوم ORM (Object-Relational Mapper) مطرح شد.

Entity Framework Core (EF Core) به عنوان نسل جدیدی از ORM محبوب مایکروسافت، تحولی بزرگ در نحوه کار توسعه‌دهندگان C# با پایگاه‌های داده ایجاد کرده است. این ORM قدرتمند، به توسعه‌دهندگان اجازه می‌دهد تا با استفاده از مدل‌های دامنه‌ای (Domain Models) و اشیاء C#، با پایگاه داده تعامل داشته باشند، بدون اینکه نیاز به نوشتن کوئری‌های SQL به صورت مستقیم داشته باشند. EF Core وظیفه نگاشت اشیاء به جداول، مدیریت تغییرات، و اجرای دستورات SQL را به صورت خودکار بر عهده می‌گیرد.

این مقاله جامع با هدف آموزش Entity Framework Core در C# برای جامعه تخصصی برنامه‌نویسان دات‌نت طراحی شده است. ما از مفاهیم پایه‌ای شروع کرده و به تدریج به مباحث پیشرفته‌تر از جمله مدل‌سازی داده، کوئری‌نویسی پیشرفته، بهینه‌سازی عملکرد و بهترین شیوه‌ها خواهیم پرداخت. هدف این است که خوانندگان پس از مطالعه این مطلب، درک عمیقی از EF Core پیدا کرده و بتوانند از آن به صورت مؤثر در پروژه‌های واقعی خود استفاده کنند.

مقدمه‌ای بر Entity Framework Core: چرا ORM؟

قبل از اینکه به جزئیات EF Core بپردازیم، ضروری است که درک کنیم چرا یک ORM مانند Entity Framework Core تا این حد برای توسعه مدرن ضروری است. توسعه‌دهندگان به صورت سنتی برای تعامل با پایگاه داده، از ADO.NET و دستورات SQL استفاده می‌کردند. این رویکرد اگرچه کنترل بالایی را فراهم می‌آورد، اما با چالش‌هایی همراه بود:

  • **عدم تطابق امپدانس (Impedance Mismatch):** داده‌ها در پایگاه داده به صورت جدولی (Relational) ذخیره می‌شوند، در حالی که در برنامه‌های شی‌گرا، با اشیاء و روابط بین آن‌ها سروکار داریم. نگاشت دستی این دو ساختار به یکدیگر، کاری تکراری، مستعد خطا و زمان‌بر است.
  • **کد تکراری (Boilerplate Code):** برای هر عملیات CRUD (ایجاد، خواندن، به‌روزرسانی، حذف) نیاز به نوشتن دستورات SQL، باز کردن اتصال، اجرای دستور، خواندن نتایج و بستن اتصال بود. این حجم بالای کد تکراری، بهره‌وری را کاهش می‌داد.
  • **آسیب‌پذیری‌های امنیتی:** نگارش دستی SQL، مخصوصاً در ورودی‌های کاربر، می‌تواند منجر به آسیب‌پذیری‌های SQL Injection شود.
  • **وابستگی به پایگاه داده:** تغییر نوع پایگاه داده (مثلاً از SQL Server به PostgreSQL) اغلب مستلزم بازنویسی بخش قابل توجهی از کدهای دسترسی به داده بود.

یک ORM مانند Entity Framework Core این چالش‌ها را با ایجاد یک لایه انتزاعی بین برنامه و پایگاه داده حل می‌کند. این لایه به توسعه‌دهندگان اجازه می‌دهد تا با استفاده از اشیاء C# خود (که به آن‌ها Entity گفته می‌شود)، داده‌ها را ذخیره، بازیابی و به‌روزرسانی کنند. EF Core مسئول ترجمه عملیات روی اشیاء به دستورات SQL مناسب و اجرای آن‌ها در پایگاه داده است. این امر باعث افزایش بهره‌وری، کاهش کدنویسی تکراری، و قابلیت نگهداری بهتر کد می‌شود.

تکامل Entity Framework: از Classic تا Core

Entity Framework تاریخی طولانی در اکوسیستم دات‌نت دارد. نسخه اولیه آن در سال 2008 به عنوان بخشی از .NET Framework 3.5 SP1 منتشر شد. با گذشت زمان و رسیدن به EF6، قابلیت‌های آن به بلوغ قابل توجهی رسید. اما با ظهور دات‌نت کور (Dotnet Core)، نیاز به یک ORM Cross-Platform و سبک‌تر احساس شد. اینجاست که Entity Framework Core پا به میدان گذاشت.

EF Core با بازطراحی کامل، به یک فریم‌ورک سبک، ماژولار و قابل توسعه تبدیل شد که از ابتدا برای کار با دات‌نت کور (و بعدها دات‌نت 5، 6، 7 و بالاتر) طراحی شده بود. تفاوت‌های کلیدی EF Core با EF6 شامل موارد زیر است:

  • **Cross-Platform:** EF Core می‌تواند روی ویندوز، لینوکس و macOS اجرا شود.
  • **عملکرد بهبود یافته:** با بهینه‌سازی‌های داخلی، EF Core معمولاً عملکرد بهتری نسبت به EF6 ارائه می‌دهد.
  • **ماژولار بودن:** EF Core از بسته‌های NuGet کوچکتر و متمرکزتر تشکیل شده که امکان انتخاب قابلیت‌های مورد نیاز را فراهم می‌کند.
  • **پشتیبانی از الگوهای توسعه مدرن:** هماهنگی بهتر با Dependency Injection و تست‌پذیری بهبود یافته.
  • **قابلیت‌های جدید:** اضافه شدن قابلیت‌هایی مانند Global Query Filters، Owned Entities، Interceptors و غیره.

در حال حاضر، EF Core انتخاب استاندارد برای توسعه برنامه‌های کاربردی دات‌نت است که نیاز به تعامل با پایگاه داده‌های رابطه‌ای دارند.

شروع به کار با Entity Framework Core در C#

برای شروع کار با EF Core، ابتدا نیاز به نصب بسته‌های NuGet مربوطه دارید. سپس باید مدل داده‌ای خود (اشیاء C#) را تعریف کرده و یک کلاس DbContext برای مدیریت تعاملات با پایگاه داده ایجاد کنید.

نصب Entity Framework Core

EF Core به صورت بسته‌های NuGet در دسترس است. حداقل دو بسته اصلی مورد نیاز است:

  • Microsoft.EntityFrameworkCore: شامل هسته EF Core.
  • بسته فراهم‌کننده پایگاه داده (Database Provider): این بسته به EF Core می‌گوید که چگونه با یک نوع خاص از پایگاه داده (مثلاً SQL Server، PostgreSQL، MySQL، SQLite) ارتباط برقرار کند.

به عنوان مثال، برای SQL Server، بسته‌های زیر را باید نصب کنید:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools

بسته Microsoft.EntityFrameworkCore.Tools برای ابزارهای خط فرمان (CLI) EF Core مانند مدیریت Migrationها ضروری است.

رویکرد Code-First در EF Core

EF Core از دو رویکرد اصلی پشتیبانی می‌کند: Code-First و Database-First. در رویکرد Code-First، شما ابتدا مدل‌های دامنه‌ای خود را در C# تعریف می‌کنید و EF Core بر اساس این مدل‌ها، ساختار پایگاه داده را برای شما ایجاد یا به‌روزرسانی می‌کند. این رویکرد به دلیل مزایایی مانند کنترل کامل بر کد و امکان استفاده از Git برای مدیریت تغییرات شمای پایگاه داده، بسیار محبوب است. در این مقاله ما بر رویکرد Code-First تمرکز خواهیم کرد.

تعریف مدل‌های (Entities)

Entities یا موجودیت‌ها، کلاس‌های POCO (Plain Old CLR Objects) هستند که نماینده داده‌ها در دامنه‌ی برنامه و جداول در پایگاه داده‌اند. هر ویژگی (Property) در کلاس معمولاً به یک ستون در جدول نگاشت می‌شود.

public class Blog
{
    public int BlogId { get; set; }
    public string Name { get; set; }
    public string Url { get; set; }

    public ICollection<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int BlogId { get; set; }
    public Blog Blog { get; set; }
}

در این مثال، BlogId و PostId به عنوان کلیدهای اصلی (Primary Key) شناسایی می‌شوند (به دلیل قرارداد نام‌گذاری EF Core). BlogId در کلاس Post یک کلید خارجی (Foreign Key) است که به Blog مربوط می‌شود، و Blog یک ویژگی ناوبری (Navigation Property) است که نشان‌دهنده رابطه یک به چند بین Blog و Post است.

ایجاد کلاس DbContext

DbContext هسته EF Core است. این کلاس نماینده یک جلسه با پایگاه داده است و امکان کوئری‌نویسی، ردیابی تغییرات و ذخیره داده‌ها را فراهم می‌کند. هر کلاس DbSet<TEntity> در DbContext به یک جدول در پایگاه داده نگاشت می‌شود.

public class MyDbContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Fluent API configurations can go here
        modelBuilder.Entity<Blog>()
            .HasMany(b => b.Posts)
            .WithOne(p => p.Blog)
            .HasForeignKey(p => p.BlogId);
    }
}

سازنده DbContext با پارامتر DbContextOptions<T> امکان پیکربندی پایگاه داده (مانند رشته اتصال) را فراهم می‌کند. متد OnModelCreating برای پیکربندی پیشرفته مدل با استفاده از Fluent API استفاده می‌شود.

پیکربندی DbContext و رشته اتصال

رشته اتصال پایگاه داده و نوع فراهم‌کننده پایگاه داده معمولاً در متد ConfigureServices در فایل Startup.cs (برای ASP.NET Core) یا در فایل Program.cs (برای دات‌نت 6 به بعد) پیکربندی می‌شوند.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<MyDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
}

رشته اتصال DefaultConnection باید در فایل appsettings.json تعریف شود:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MyDatabase;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

Migrations: مدیریت تغییرات شمای پایگاه داده

Migrations ابزار قدرتمند EF Core برای مدیریت تکامل شمای پایگاه داده با تغییرات مدل Code-First است. با هر تغییر در مدل‌های C#، می‌توانید یک Migration جدید ایجاد کنید که تغییرات لازم را برای به‌روزرسانی پایگاه داده شما تعریف می‌کند.

برای ایجاد اولین Migration:

dotnet ef migrations add InitialCreate

این دستور یک فایل Migration در پوشه Migrations ایجاد می‌کند که شامل متدهای Up (برای اعمال تغییرات) و Down (برای بازگرداندن تغییرات) است.

برای اعمال Migration به پایگاه داده:

dotnet ef database update

این دستور پایگاه داده را ایجاد کرده یا به‌روزرسانی می‌کند تا با مدل شما همگام شود.

مفاهیم اصلی در Entity Framework Core

برای استفاده موثر از EF Core، درک عمیق از مفاهیم اصلی آن حیاتی است.

Entities و نگاشت

همانطور که قبلاً اشاره شد، Entities کلاس‌های POCO (Plain Old CLR Objects) هستند که نماینده داده‌ها در دامنه‌ی برنامه و جداول در پایگاه داده‌اند. EF Core از طریق یک سری قراردادها (Conventions) سعی می‌کند به صورت خودکار کلاس‌ها و ویژگی‌ها را به جداول و ستون‌ها نگاشت کند. به عنوان مثال:

  • **نام جدول:** نام کلاس به صورت پیش‌فرض نام جدول می‌شود (مثلاً کلاس Blog به جدول Blogs).
  • **کلید اصلی:** ویژگی با نام Id یا <ClassName>Id به عنوان کلید اصلی شناسایی می‌شود (مثلاً BlogId).
  • **نوع داده:** نوع داده ویژگی C# به مناسب‌ترین نوع داده در پایگاه داده نگاشت می‌شود (مثلاً string به nvarchar(max)، int به int).

پیکربندی با Data Annotations و Fluent API

زمانی که قراردادها کافی نیستند یا نیاز به سفارشی‌سازی بیشتری دارید، EF Core دو راه برای پیکربندی نگاشت‌ها ارائه می‌دهد:

  • **Data Annotations:** ویژگی‌های (Attributes) C# که به کلاس‌ها یا ویژگی‌ها اضافه می‌شوند. سریع و ساده برای سناریوهای معمول.
  • public class Blog
    {
        [Key]
        public int BlogId { get; set; }
    
        [Required]
        [MaxLength(200)]
        public string Name { get; set; }
    
        [Column("BlogUrl")]
        public string Url { get; set; }
    }
        
  • **Fluent API:** یک رویکرد مبتنی بر کد برای پیکربندی مدل در متد OnModelCreating از DbContext. قدرتمندتر و انعطاف‌پذیرتر برای سناریوهای پیچیده، مانند روابط Many-to-Many، انواع خاص ستون‌ها، و فیلترهای کوئری گلوبال.
  • protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .ToTable("BlogsData") // Custom table name
            .HasKey(b => b.BlogId);
    
        modelBuilder.Entity<Blog>()
            .Property(b => b.Name)
            .IsRequired()
            .HasMaxLength(200);
    
        modelBuilder.Entity<Blog>()
            .Property(b => b.Url)
            .HasColumnName("BlogUrl");
    }
        

DbContext و Cycle Life آن

DbContext یک واحد کار (Unit of Work) و یک ردیاب تغییرات (Change Tracker) است. هر نمونه از DbContext به گونه‌ای طراحی شده است که عمر کوتاهی داشته باشد (Short-Lived). توصیه می‌شود که یک نمونه DbContext برای هر عملیات تجاری یا هر درخواست وب (HTTP Request) ایجاد کرده و پس از اتمام کار، آن را dispose کنید. این کار از مشکلات مربوط به ردیابی تغییرات و مصرف حافظه جلوگیری می‌کند.

Change Tracking در EF Core

یکی از قوی‌ترین ویژگی‌های EF Core، سیستم ردیابی تغییرات آن است. هنگامی که Entities را از پایگاه داده بازیابی می‌کنید، EF Core وضعیت آن‌ها را در DbContext ردیابی می‌کند. وقتی ویژگی‌های Entities را تغییر می‌دهید، EF Core این تغییرات را تشخیص می‌دهد و هنگام فراخوانی SaveChanges()، فقط دستورات SQL لازم برای به‌روزرسانی این تغییرات را اجرا می‌کند. این سیستم شامل حالات زیر برای Entityها است:

  • **Added:** Entity جدید که هنوز به پایگاه داده ذخیره نشده است.
  • **Deleted:** Entity که قرار است از پایگاه داده حذف شود.
  • **Modified:** Entity که برخی از ویژگی‌های آن تغییر کرده است.
  • **Unchanged:** Entity که از پایگاه داده بازیابی شده و تغییری نکرده است.
  • **Detached:** Entity که توسط DbContext ردیابی نمی‌شود.

ذخیره تغییرات: SaveChanges() و SaveChangesAsync()

پس از انجام تغییرات روی Entities (افزودن، به‌روزرسانی، حذف)، باید متد SaveChanges() یا SaveChangesAsync() را فراخوانی کنید تا این تغییرات به پایگاه داده اعمال شوند. SaveChangesAsync() نسخه ناهمگام (Asynchronous) است و در برنامه‌های وب (ASP.NET Core) برای بهبود مقیاس‌پذیری و جلوگیری از مسدود شدن Threadها توصیه می‌شود.

using (var context = new MyDbContext(options))
{
    var blog = new Blog { Name = "My New Blog", Url = "http://mynewblog.com" };
    context.Blogs.Add(blog);
    await context.SaveChangesAsync(); // Add the new blog

    var existingBlog = await context.Blogs.FirstOrDefaultAsync(b => b.Name == "My New Blog");
    existingBlog.Url = "http://updatedblog.com";
    await context.SaveChangesAsync(); // Update the existing blog

    context.Blogs.Remove(existingBlog);
    await context.SaveChangesAsync(); // Delete the blog
}

روابط بین Entities

مدل‌سازی روابط بین جداول یکی از وظایف کلیدی در کار با پایگاه داده است. EF Core از انواع مختلفی از روابط پشتیبانی می‌کند:

  • **یک به یک (One-to-One):** یک موجودیت به یک موجودیت دیگر مرتبط است و بالعکس (مثلاً کاربر به پروفایل کاربر).
  • **یک به چند (One-to-Many):** یک موجودیت به چندین موجودیت دیگر مرتبط است، اما هر یک از موجودیت‌های دیگر فقط به یک موجودیت از نوع اول مرتبط است (مثلاً Blog به Post).
  • **چند به چند (Many-to-Many):** هر موجودیت می‌تواند به چندین موجودیت از نوع دیگر مرتبط باشد و بالعکس. در EF Core 5 و بالاتر، این روابط به صورت خودکار از طریق یک جدول واسط (Join Table) ایجاد می‌شوند (مثلاً Student به Course). در نسخه‌های قبلی، نیاز به تعریف صریح جدول واسط داشتید.

پیکربندی این روابط می‌تواند از طریق Data Annotations یا Fluent API انجام شود. Fluent API کنترل بیشتری را برای نام‌گذاری کلیدهای خارجی، شاخص‌ها و حذف Cascade فراهم می‌کند.

کوئری‌نویسی داده با Entity Framework Core

مهمترین بخش هر ORM، قابلیت کوئری‌نویسی آن است. EF Core از LINQ (Language Integrated Query) برای ساخت کوئری‌های قدرتمند و Type-Safe استفاده می‌کند.

LINQ to Entities: قدرت کوئری شی‌گرا

LINQ به توسعه‌دهندگان C# اجازه می‌دهد تا کوئری‌ها را با استفاده از نحو C# بنویسند، بدون اینکه نیاز به یادگیری SQL داشته باشند. EF Core این کوئری‌های LINQ را به دستورات SQL مناسب ترجمه کرده و آن‌ها را روی پایگاه داده اجرا می‌کند.

کوئری‌های ساده

// Get all blogs
var allBlogs = await context.Blogs.ToListAsync();

// Get a blog by ID
var blogById = await context.Blogs.FindAsync(1);

// Get blogs with a specific name
var programmingBlogs = await context.Blogs
                                    .Where(b => b.Name.Contains("Programming"))
                                    .OrderBy(b => b.Name)
                                    .ToListAsync();

// Select specific properties (projection)
var blogNames = await context.Blogs
                                .Select(b => b.Name)
                                .ToListAsync();

// Select into an anonymous type
var blogInfo = await context.Blogs
                                .Select(b => new { b.BlogId, b.Name, PostCount = b.Posts.Count() })
                                .ToListAsync();

ToListAsync() و FirstOrDefaultAsync() و FindAsync() متدهای ناهمگام هستند که اجرای کوئری را به پایگاه داده انجام می‌دهند. تا زمانی که این متدها یا متدهای مشابه (مانند Count(), ToArray(), First()) فراخوانی نشوند، کوئری فقط در حافظه ساخته شده و به پایگاه داده ارسال نمی‌شود (Lazy Execution).

بارگذاری داده‌های مرتبط (Related Data)

یکی از چالش‌های رایج در ORMها، مدیریت بارگذاری داده‌های مرتبط است. EF Core سه استراتژی اصلی برای این کار ارائه می‌دهد:

  • **Eager Loading (بارگذاری مشتاقانه):** با استفاده از متد Include()، داده‌های مرتبط در همان کوئری اصلی بارگذاری می‌شوند. این روش معمولاً بهترین عملکرد را دارد زیرا از مشکل N+1 Query جلوگیری می‌کند.

    // Load blogs and their posts in a single query
    var blogsWithPosts = await context.Blogs
                                        .Include(b => b.Posts)
                                        .ToListAsync();
    
    // Load multiple levels of related data
    var blogsWithPostsAndComments = await context.Blogs
                                                    .Include(b => b.Posts)
                                                        .ThenInclude(p => p.Comments)
                                                    .ToListAsync();
            
  • **Explicit Loading (بارگذاری صریح):** داده‌های مرتبط به صورت جداگانه و پس از بارگذاری موجودیت اصلی بارگذاری می‌شوند. این روش زمانی مفید است که شما نیاز به بارگذاری داده‌های مرتبط در شرایط خاصی دارید.

    var blog = await context.Blogs.SingleOrDefaultAsync(b => b.BlogId == 1);
    if (blog != null)
    {
        await context.Entry(blog).Collection(b => b.Posts).LoadAsync(); // Load all posts for this blog
        // Or to load just one specific post
        // await context.Entry(blog).Collection(b => b.Posts).Query().Where(p => p.Title.Contains("EF Core")).LoadAsync();
    }
            
  • **Lazy Loading (بارگذاری تنبل):** داده‌های مرتبط به صورت خودکار زمانی که اولین بار به آن‌ها دسترسی پیدا می‌کنید، بارگذاری می‌شوند. این روش نیاز به تعریف ویژگی‌های ناوبری به صورت virtual و نصب بسته Microsoft.EntityFrameworkCore.Proxies دارد. اگرچه راحت است، اما می‌تواند منجر به مشکل N+1 Query شود، به خصوص در حلقه‌ها، که عملکرد را به شدت تحت تأثیر قرار می‌دهد. استفاده از آن باید با احتیاط فراوان صورت گیرد.

    // In DbContextOptions configuration
    options.UseLazyLoadingProxies();
    
    // In entity class
    public virtual ICollection<Post> Posts { get; set; }
    
    // Accessing posts will automatically load them
    var blog = await context.Blogs.FirstAsync();
    foreach (var post in blog.Posts) // Posts will be loaded here
    {
        Console.WriteLine(post.Title);
    }
            

کوئری‌های No-Tracking

به صورت پیش‌فرض، EF Core اشیاء بازیابی شده را ردیابی می‌کند تا تغییرات آن‌ها را تشخیص دهد. اگر فقط قصد خواندن داده‌ها را دارید و نمی‌خواهید آن‌ها را به‌روزرسانی کنید، می‌توانید از کوئری‌های No-Tracking استفاده کنید. این کار سربار ردیابی تغییرات را حذف کرده و می‌تواند عملکرد را در سناریوهای خواندن افزایش دهد.

var blogs = await context.Blogs.AsNoTracking().ToListAsync();

کوئری‌های Raw SQL

گاهی اوقات نیاز دارید کوئری‌های SQL خام را مستقیماً اجرا کنید. EF Core این امکان را فراهم می‌کند.

  • برای بازیابی Entities:
    var blogs = await context.Blogs.FromSqlRaw("SELECT * FROM Blogs WHERE Name LIKE '{0}%'", "Pro").ToListAsync();
            
  • برای اجرای دستورات غیر-کوئری (Insert, Update, Delete):
    await context.Database.ExecuteSqlRawAsync("UPDATE Blogs SET Url = 'http://new.com' WHERE Name = 'Old Blog'");
            

ویژگی‌های پیشرفته Entity Framework Core

EF Core فراتر از CRUD و کوئری‌نویسی ساده، قابلیت‌های پیشرفته‌ای را برای سناریوهای پیچیده‌تر ارائه می‌دهد.

مدیریت همزمانی (Concurrency Handling)

در برنامه‌های چندکاربره، ممکن است دو کاربر به طور همزمان سعی در تغییر یک رکورد داشته باشند. EF Core از همزمانی خوشبینانه (Optimistic Concurrency) پشتیبانی می‌کند. در این رویکرد، هیچ قفلی در پایگاه داده اعمال نمی‌شود. در عوض، هنگامی که یک به‌روزرسانی یا حذف انجام می‌شود، EF Core بررسی می‌کند که آیا رکورد از زمان خوانده شدن اولیه توسط کاربر، توسط کاربر دیگری تغییر کرده است یا خیر. اگر تغییر کرده باشد، یک DbUpdateConcurrencyException رخ می‌دهد.

برای فعال کردن همزمانی خوشبینانه، باید یک ویژگی (Column) به عنوان “Concurrency Token” در مدل خود تعریف کنید. معمولاً از rowversion (در SQL Server) یا یک ویژگی timestamp استفاده می‌شود.

public class Blog
{
    public int BlogId { get; set; }
    public string Name { get; set; }
    public string Url { get; set; }

    [Timestamp] // Data Annotation for concurrency token
    public byte[] Timestamp { get; set; }
}
// Or using Fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Timestamp)
        .IsConcurrencyToken();
}

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

تراکنش‌ها (Transactions)

تراکنش‌ها اطمینان حاصل می‌کنند که مجموعه‌ای از عملیات پایگاه داده به صورت اتمیک (Atomic) انجام شوند؛ یعنی یا همه آن‌ها با موفقیت انجام شوند یا هیچ یک از آن‌ها. EF Core به صورت داخلی از تراکنش‌ها برای SaveChanges() استفاده می‌کند. همچنین می‌توانید تراکنش‌های خود را به صورت دستی مدیریت کنید:

using (var transaction = await context.Database.BeginTransactionAsync())
{
    try
    {
        // Perform multiple operations
        context.Blogs.Add(new Blog { Name = "Blog 1" });
        await context.SaveChangesAsync();

        context.Posts.Add(new Post { Title = "Post 1", BlogId = 1 });
        await context.SaveChangesAsync();

        await transaction.CommitAsync();
    }
    catch (Exception)
    {
        await transaction.RollbackAsync();
    }
}

وراثت (Inheritance)

EF Core از نگاشت سلسله مراتب وراثت در C# به پایگاه داده پشتیبانی می‌کند. سه استراتژی اصلی برای نگاشت وراثت وجود دارد:

  • **Table-Per-Hierarchy (TPH):** تمام کلاس‌های سلسله مراتب در یک جدول واحد ذخیره می‌شوند. یک ستون “Discriminator” برای شناسایی نوع موجودیت اضافه می‌شود. این ساده‌ترین و معمولاً کارآمدترین استراتژی است.
  • **Table-Per-Type (TPT):** هر کلاس در سلسله مراتب به یک جدول جداگانه نگاشت می‌شود. جداول فرزند از طریق کلیدهای خارجی با جدول والد مرتبط می‌شوند. این روش داده‌های تکراری کمتری دارد اما کوئری‌های پیچیده‌تری ایجاد می‌کند.
  • **Table-Per-Concrete-Type (TPC):** هر کلاس کانکریت (غیر انتزاعی) در سلسله مراتب به یک جدول جداگانه نگاشت می‌شود. جداول شامل ستون‌های خود و ستون‌های کلاس‌های والد (به صورت تکراری) هستند. این روش می‌تواند کوئری‌ها را ساده‌تر کند اما داده‌های تکراری زیادی ایجاد می‌کند. (معرفی شده در EF Core 7)

Owned Entities

Owned Entities به شما اجازه می‌دهند تا انواع پیچیده را در مدل خود بدون نیاز به نگاشت آن‌ها به جداول جداگانه، تعریف کنید. آن‌ها به عنوان بخشی از موجودیت والد خود ذخیره می‌شوند (معمولاً در همان جدول). این برای مدل‌سازی اشیاء ارزش (Value Objects) مانند آدرس‌ها یا اشیاء تاریخ‌گذاری مفید است.

public class Order
{
    public int OrderId { get; set; }
    public ShippingAddress ShippingAddress { get; set; }
}

public class ShippingAddress // Not a DbSet, it's an Owned Entity
{
    public string Street { get; set; }
    public string City { get; set; }
}

// In DbContext OnModelCreating
modelBuilder.Entity<Order>().OwnsOne(o => o.ShippingAddress);

Shadow Properties

Shadow Properties، ویژگی‌هایی هستند که بخشی از کلاس CLR شما نیستند، اما بخشی از مدل EF Core هستند و در پایگاه داده نگاشت می‌شوند. آن‌ها برای داده‌هایی که نمی‌خواهید در شیء دامنه‌ای شما نمایش داده شوند اما برای عملیات پایگاه داده ضروری هستند، مفیدند (مثلاً LastUpdatedBy یا CreatedDate که به صورت خودکار توسط EF Core پر می‌شوند).

modelBuilder.Entity<Blog>()
    .Property<DateTime>("LastUpdated"); // Define a shadow property

می‌توانید به این ویژگی‌ها از طریق APIهای EF Core دسترسی پیدا کنید: context.Entry(entity).Property("PropertyName").CurrentValue.

Value Converters

Value Converters به شما اجازه می‌دهند تا مقادیر بین انواع CLR و انواع پایگاه داده را تبدیل کنید. این برای ذخیره Enumها به عنوان رشته، یا ذخیره لیستی از رشته‌ها به عنوان یک رشته جدا شده با کاما در پایگاه داده مفید است.

public enum Status { Pending, Approved, Rejected }

public class Document
{
    public int Id { get; set; }
    public Status CurrentStatus { get; set; }
}

// In DbContext OnModelCreating
modelBuilder.Entity<Document>()
    .Property(e => e.CurrentStatus)
    .HasConversion<string>(); // Convert enum to string in DB

Global Query Filters

Global Query Filters به شما اجازه می‌دهند تا شرط‌های LINQ را به صورت گلوبال برای انواع Entity خاص تعریف کنید. این فیلترها به طور خودکار به هر کوئری که شامل آن Entity باشد، اعمال می‌شوند. این برای پیاده‌سازی Soft Delete یا فیلترهای چند مستاجری (Multi-Tenancy) بسیار مفید است.

public class TenantSpecificEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string TenantId { get; set; } // For multi-tenancy
    public bool IsDeleted { get; set; } // For soft delete
}

// In DbContext OnModelCreating
modelBuilder.Entity<TenantSpecificEntity>()
    .HasQueryFilter(e => e.TenantId == "YourCurrentTenantId" && !e.IsDeleted);

توجه داشته باشید که فیلترهای گلوبال با IgnoreQueryFilters() قابل غیرفعال کردن هستند.

Database Seeding

Database Seeding فرآیند پر کردن پایگاه داده با داده‌های اولیه (مانند داده‌های پیش‌فرض، تنظیمات، یا داده‌های تستی) است. EF Core از Seeding به عنوان بخشی از Migrationها پشتیبانی می‌کند:

// In DbContext OnModelCreating
modelBuilder.Entity<Blog>().HasData(
    new Blog { BlogId = 1, Name = "Programming Blog", Url = "http://programming.com" },
    new Blog { BlogId = 2, Name = "Lifestyle Blog", Url = "http://lifestyle.com" }
);

هر بار که Migrationها را اعمال می‌کنید، داده‌های Seeding نیز بررسی و اعمال می‌شوند.

Interceptors

Interceptors به شما اجازه می‌دهند تا منطق سفارشی را قبل یا بعد از عملیات‌های خاص EF Core (مانند ذخیره تغییرات، باز کردن اتصال، اجرای کوئری‌ها) تزریق کنید. این برای سناریوهایی مانند لاگ‌برداری، auditing، یا تغییر رفتار پیش‌فرض EF Core بسیار مفید است.

public class MyCommandInterceptor : IDbCommandInterceptor
{
    public InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
    {
        Console.WriteLine($"Executing command: {command.CommandText}");
        return result;
    }
    // ... other methods
}

// In DbContextOptions configuration
options.AddInterceptors(new MyCommandInterceptor());

بهینه‌سازی عملکرد در Entity Framework Core

عملکرد پایگاه داده اغلب گلوگاه برنامه‌های کاربردی است. بهینه‌سازی EF Core برای اطمینان از عملکرد بالا در برنامه‌های C# حیاتی است.

انتخاب استراتژی بارگذاری صحیح داده‌های مرتبط

  • **Eager Loading (با Include/ThenInclude):** معمولاً بهترین انتخاب برای بارگذاری داده‌های مرتبط است، زیرا از مشکل N+1 query جلوگیری می‌کند. مطمئن شوید که Includeهای غیرضروری اضافه نکنید، زیرا این کار می‌تواند منجر به تولید کوئری‌های SQL بسیار بزرگ و بازیابی داده‌های زیاد شود.
  • **No-Tracking Queries (با AsNoTracking()):** برای سناریوهای فقط خواندن، استفاده از AsNoTracking() ضروری است. این کار سربار ردیابی تغییرات را حذف کرده و منجر به کوئری‌های سریع‌تر می‌شود.
  • **Projections:** اگر فقط به زیرمجموعه‌ای از ستون‌ها نیاز دارید، از Select() برای Projection به یک نوع ناشناس یا یک DTO (Data Transfer Object) استفاده کنید. این کار فقط ستون‌های مورد نیاز را از پایگاه داده بازیابی می‌کند.
  • var postTitles = await context.Posts.Select(p => p.Title).ToListAsync();
        
  • **Batching Updates:** به جای ذخیره هر تغییر به صورت جداگانه (که هر کدام یک round trip به پایگاه داده است)، سعی کنید چندین تغییر را گروه بندی کرده و فقط یک بار SaveChanges() را فراخوانی کنید.
  • var postsToUpdate = await context.Posts.Where(p => p.Title.Contains("Old")).ToListAsync();
    foreach (var post in postsToUpdate)
    {
        post.Title = post.Title.Replace("Old", "New");
    }
    await context.SaveChangesAsync(); // All updates sent in one batch (or fewer)
        

فیلتر کردن و صفحه‌بندی کارآمد

همیشه قبل از بارگذاری داده‌ها در حافظه، فیلتر و صفحه‌بندی را روی پایگاه داده انجام دهید. متدهایی مانند Where()، OrderBy()، Skip()، و Take() به دستورات SQL ترجمه می‌شوند و عملیات را در سمت سرور انجام می‌دهند.

var paginatedPosts = await context.Posts
                                    .Where(p => p.BlogId == blogId)
                                    .OrderByDescending(p => p.PostId)
                                    .Skip(pageNumber * pageSize)
                                    .Take(pageSize)
                                    .ToListAsync();

ایندکس‌گذاری (Indexing)

مطمئن شوید که ستون‌هایی که در کوئری‌ها (WHERE، ORDER BY، JOIN) زیاد استفاده می‌شوند، ایندکس مناسبی در پایگاه داده دارند. EF Core می‌تواند ایندکس‌ها را از طریق Fluent API یا Data Annotations تعریف کند. ایندکس‌ها سرعت بازیابی داده‌ها را به شدت افزایش می‌دهند.

// Using Data Annotation
public class Blog
{
    public int BlogId { get; set; }
    [Index] // EF Core 6+
    public string Name { get; set; }
    public string Url { get; set; }
}

// Using Fluent API
modelBuilder.Entity<Blog>()
    .HasIndex(b => b.Name)
    .IsUnique(); // For unique index

مانیتورینگ و پروفایل‌سازی کوئری‌ها

برای تشخیص مشکلات عملکرد، مهم است که کوئری‌های SQL تولید شده توسط EF Core را مشاهده کنید. این کار می‌تواند با استفاده از Logging در ASP.NET Core، یا با استفاده از متد ToQueryString() در LINQ انجام شود.

var query = context.Blogs.Where(b => b.Name.Contains("Programming"));
Console.WriteLine(query.ToQueryString()); // See the generated SQL

ابزارهای پروفایل‌سازی پایگاه داده (مانند SQL Server Profiler) نیز می‌توانند برای تحلیل عملکرد کوئری‌ها مفید باشند.

کاهش Round Trips به پایگاه داده

هر تعامل با پایگاه داده (حتی خواندن یک رکورد) شامل یک Round Trip به سرور پایگاه داده است که هزینه‌بر است. سعی کنید تعداد Round Tripها را به حداقل برسانید:

  • داده‌ها را در کوئری‌های بزرگتر و جامع‌تر بارگذاری کنید (با احتیاط برای جلوگیری از بارگذاری بیش از حد).
  • از SaveChanges() برای گروه بندی تغییرات استفاده کنید.
  • از عملیات Bulk Insert/Update/Delete (با استفاده از کتابخانه‌های جانبی مانند EF Core.BulkExtensions) برای حجم بالای داده‌ها استفاده کنید، زیرا EF Core به صورت داخلی از آنها پشتیبانی نمی‌کند.

تست‌پذیری برنامه‌های Entity Framework Core

نوشتن تست‌های واحد (Unit Tests) و تست‌های یکپارچه‌سازی (Integration Tests) برای کدهای دسترسی به داده مهم است. EF Core قابلیت‌های مختلفی برای تسهیل تست‌پذیری فراهم می‌کند.

تست با پایگاه داده در حافظه (In-Memory Database)

EF Core یک فراهم‌کننده پایگاه داده در حافظه (Microsoft.EntityFrameworkCore.InMemory) ارائه می‌دهد که برای تست‌های واحد سریع مناسب است. این پایگاه داده در حافظه اجرا می‌شود و برای هر تست کاملاً ایزوله و پاک می‌شود.

// In your test setup
var options = new DbContextOptionsBuilder<MyDbContext>()
    .UseInMemoryDatabase(databaseName: "TestDatabase")
    .Options;

using (var context = new MyDbContext(options))
{
    // Arrange
    context.Blogs.Add(new Blog { Name = "Test Blog" });
    await context.SaveChangesAsync();

    // Act
    var blog = await context.Blogs.FirstAsync();

    // Assert
    Assert.Equal("Test Blog", blog.Name);
}

محدودیت اصلی این است که پایگاه داده در حافظه، رفتار پایگاه داده رابطه‌ای واقعی را به طور کامل شبیه‌سازی نمی‌کند (مثلاً کوئری‌های پیچیده، تراکنش‌ها، یا محدودیت‌های SQL خاص).

تست با SQLite in-memory

برای تست‌های واحد که به شباهت بیشتری با پایگاه داده واقعی نیاز دارند، می‌توانید از فراهم‌کننده SQLite در حافظه استفاده کنید. SQLite یک پایگاه داده رابطه‌ای واقعی است که می‌تواند به صورت کاملاً در حافظه اجرا شود و از بسیاری از قابلیت‌های SQL پشتیبانی می‌کند.

// In your test setup
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open(); // Important to keep connection open for in-memory DB

var options = new DbContextOptionsBuilder<MyDbContext>()
    .UseSqlite(connection)
    .Options;

using (var context = new MyDbContext(options))
{
    context.Database.EnsureCreated(); // Create schema
    // ... then write your test logic
}

Mocking DbContext و DbSet

در برخی از تست‌های واحد، ممکن است بخواهید DbContext یا DbSet را Mock کنید تا کاملاً از پایگاه داده جدا شوید. این رویکرد برای تست منطق تجاری که فقط از توابع IQueryable روی DbSet استفاده می‌کند، مناسب است. کتابخانه‌های Mocking مانند Moq می‌توانند برای این کار استفاده شوند.

این روش برای تست عملکرد‌های پایگاه داده و ترجمه LINQ به SQL مناسب نیست.

تست‌های یکپارچه‌سازی با پایگاه داده واقعی

برای اطمینان کامل از صحت کارکرد لایه دسترسی به داده، تست‌های یکپارچه‌سازی با پایگاه داده واقعی ضروری هستند. این تست‌ها پایگاه داده را با داده‌های تستی پر کرده، عملیات را اجرا کرده و سپس پایگاه داده را تمیز می‌کنند. می‌توان از Docker برای راه‌اندازی یک نمونه پایگاه داده تمیز برای هر اجرا استفاده کرد.

بهترین شیوه‌ها و دام‌های رایج در EF Core

برای توسعه برنامه‌های پایدار و با کارایی بالا با EF Core، رعایت بهترین شیوه‌ها و آگاهی از دام‌های رایج اهمیت دارد.

الگوی Unit of Work و Repository Pattern

EF Core به خودی خود پیاده‌سازی الگوی Unit of Work (از طریق DbContext) و بخشی از Repository Pattern (از طریق DbSet<T>) است. استفاده از DbContext به عنوان Unit of Work (یک تراکنش اتمیک از عملیات‌ها) و تزریق آن از طریق Dependency Injection، یک رویکرد استاندارد و مناسب است.

در مورد پیاده‌سازی Repository Pattern جداگانه (برای انتزاع بیشتر)، دیدگاه‌های مختلفی وجود دارد. برای پروژه‌های کوچک و متوسط، اغلب نیازی به لایه Repository اضافی نیست، زیرا DbSet<T> خود یک Repository ساده را فراهم می‌کند. اما در پروژه‌های بزرگ با دامنه‌های پیچیده یا نیاز به تست‌پذیری پیشرفته‌تر، یک لایه Repository سفارشی می‌تواند مفید باشد. اگر Repository Pattern را پیاده‌سازی می‌کنید، مطمئن شوید که IQueryable<T> را از Repository خود برمی‌گردانید تا قابلیت کوئری‌نویسی LINQ را از دست ندهید.

مدیریت چرخه حیات DbContext با Dependency Injection

همانطور که قبلاً اشاره شد، DbContext باید کوتاه مدت باشد. در ASP.NET Core، بهترین روش ثبت DbContext با Lifetime از نوع Scoped در Dependency Injection است. این به این معنی است که یک نمونه DbContext برای هر درخواست HTTP (یا هر Scope) ایجاد می‌شود و در پایان درخواست Dispose می‌شود. این کار از مشکلات ردیابی و مصرف حافظه جلوگیری می‌کند.

جلوگیری از مشکل N+1 Query

این مشکل زمانی رخ می‌دهد که شما یک مجموعه از Entities را بارگذاری می‌کنید و سپس در یک حلقه، داده‌های مرتبط را برای هر Entity به صورت جداگانه بارگذاری می‌کنید. این کار منجر به تعداد زیادی Round Trip غیرضروری به پایگاه داده می‌شود. همیشه از Eager Loading (Include) یا Projection (Select) برای بارگذاری کارآمد داده‌های مرتبط استفاده کنید.

مدیریت داده‌های بزرگ و Streaming

برای کار با حجم عظیمی از داده‌ها، بارگذاری همه آن‌ها در حافظه توصیه نمی‌شود. در برخی موارد، می‌توانید از AsNoTracking() و IAsyncEnumerable<T> برای Streaming داده‌ها (به جای بارگذاری کامل) استفاده کنید. این به شما اجازه می‌دهد تا داده‌ها را در حین خواندن پردازش کنید و مصرف حافظه را کاهش دهید.

await foreach (var blog in context.Blogs.AsAsyncEnumerable())
{
    // Process one blog at a time
}

امنیت: جلوگیری از SQL Injection

یکی از بزرگترین مزایای EF Core و LINQ، محافظت ذاتی در برابر SQL Injection است. هنگامی که کوئری‌های خود را با LINQ می‌نویسید و پارامترها را به صورت امن ارسال می‌کنید، EF Core به صورت خودکار پارامترها را Escaping می‌کند و از حملات SQL Injection جلوگیری می‌کند. تنها زمانی که باید نگران باشید، استفاده از FromSqlRaw یا ExecuteSqlRaw با ورودی‌های مستقیم کاربر است. در این موارد، همیشه از پارامترها استفاده کنید و ورودی کاربر را به صورت مستقیم در رشته SQL Concatenate نکنید.

پشتیبانی از Async/Await

همیشه از متدهای ناهمگام (مانند SaveChangesAsync()، ToListAsync()، FirstOrDefaultAsync()) استفاده کنید. این کار به مقیاس‌پذیری برنامه‌های شما (مخصوصاً برنامه‌های وب) کمک می‌کند، زیرا Threadهای سرور را مسدود نمی‌کند و به سرور اجازه می‌دهد تا درخواست‌های بیشتری را همزمان مدیریت کند.

مدیریت Concurrency Exceptions

هنگام پیاده‌سازی همزمانی خوشبینانه، کد شما باید بتواند DbUpdateConcurrencyException را مدیریت کند. این کار معمولاً شامل نمایش یک پیام خطا به کاربر و/یا ارائه گزینه‌هایی برای حل تعارض (مانند بازنویسی تغییرات کاربر دیگر، یا ادغام تغییرات) است.

نتیجه‌گیری

Entity Framework Core یک ORM بسیار قدرتمند و انعطاف‌پذیر برای توسعه برنامه‌های دات‌نت است. با درک صحیح مفاهیم اصلی، الگوهای کوئری‌نویسی، و قابلیت‌های پیشرفته آن، توسعه‌دهندگان می‌توانند به طور چشمگیری بهره‌وری خود را افزایش داده و کدهایی با کارایی بالا و قابل نگهداری بنویسند. از مدل‌سازی داده با Code-First و مدیریت تغییرات با Migrations گرفته تا کوئری‌نویسی پیچیده با LINQ و بهینه‌سازی عملکرد با تکنیک‌های پیشرفته، EF Core ابزاری کامل را برای تعامل با پایگاه داده‌ها فراهم می‌آورد.

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

به یاد داشته باشید که EF Core ابزاری است که با یادگیری صحیح و استفاده بهینه، می‌تواند فرآیند توسعه شما را متحول کند و به شما کمک کند تا راه‌حل‌های پایدار و مقیاس‌پذیری را ارائه دهید.

“تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”

قیمت اصلی 2.290.000 ریال بود.قیمت فعلی 1.590.000 ریال است.

"تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"

"با شرکت در این دوره جامع و کاربردی، به راحتی مهارت‌های برنامه‌نویسی پایتون را از سطح مبتدی تا پیشرفته با کمک هوش مصنوعی ChatGPT بیاموزید. این دوره، با بیش از 6 ساعت محتوای آموزشی، شما را قادر می‌سازد تا به سرعت الگوریتم‌های پیچیده را درک کرده و اپلیکیشن‌های هوشمند ایجاد کنید. مناسب برای تمامی سطوح با زیرنویس فارسی حرفه‌ای و امکان دانلود و تماشای آنلاین."

ویژگی‌های کلیدی:

بدون نیاز به تجربه قبلی برنامه‌نویسی

زیرنویس فارسی با ترجمه حرفه‌ای

۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان