ما الجديد في EF Core 9: الميزات التي تحتاج إلى معرفتها

· 12 دقيقة قراءة

تم شحن Entity Framework Core 9 جنبًا إلى جنب مع .NET 9 في نوفمبر 2024، وبعد قضاء قدر كبير من الوقت في العمل معه عبر العديد من المشاريع، يمكنني القول إنه أحد الإصدارات الأكثر أهمية منذ فترة. ليس لأنه يعيد اختراع العجلة، ولكن لأنه يصقل المجالات التي تسببت فيها EF Core تاريخيًا في أكبر قدر من الاحتكاك - ترجمة الاستعلامات والأداء والعمل باستخدام أنماط البيانات الحديثة.

في هذا المنشور، سأستعرض الميزات التي كان لها التأثير الأكبر على عملي اليومي. إذا كنت لا تزال تستخدم EF Core 8 (أو حتى 7)، فمن المفترض أن يمنحك هذا صورة واضحة عما ينتظرك على الجانب الآخر من الترقية.

EF Core 9 في النظام البيئي .NET 9

يستهدف EF Core 9 الإصدارين .NET 8 و.NET 9، مما يعني أنك لا تحتاج بالضرورة إلى ترقية تطبيقك بالكامل إلى .NET 9 للاستفادة من معظم هذه الميزات. ومع ذلك، فإن بعض AOT وتحسينات الأداء مقترنة بإحكام مع تغييرات وقت تشغيل .NET 9، لذلك ستحصل على أقصى استفادة منه من خلال المضي قدمًا.

يتبع الإصدار الإيقاع الفردي/الزوجي الذي أنشأته Microsoft: الإصدارات ذات الأرقام الفردية (مثل .NET 9) هي دعم قياسي على المدى (STS) مع 18 شهرًا من الدعم، في حين أن الإصدارات الزوجية (مثل .NET 8) هي دعم طويل الأجل (LTS). ضع ذلك في الاعتبار عند التخطيط للجدول الزمني للترقية.

تحسينات الترجمة في LINQ

هذا هو المكان الذي سيشعر فيه معظم المطورين بالفرق على الفور. لقد قطع EF Core 9 خطوات كبيرة في ترجمة تعبيرات LINQ إلى SQL وهو أمر منطقي بالفعل.

مجموعة أفضل بالترجمات

إذا سبق لك أن كتبت استعلام GroupBy في EF Core وانتهى الأمر بتحذيرات تقييم من جانب العميل أو SQL غريبة، فأنت تعرف الألم. يتعامل EF Core 9 مع مجموعة أوسع بكثير من سيناريوهات GroupBy مباشرة في SQL.

var salesByCategory = await context.Products
    .GroupBy(p => p.Category.Name)
    .Select(g => new
    {
        Category = g.Key,
        TotalRevenue = g.Sum(p => p.Price * p.UnitsSold),
        AveragePrice = g.Average(p => p.Price),
        ProductCount = g.Count()
    })
    .OrderByDescending(x => x.TotalRevenue)
    .ToListAsync();

في الإصدارات السابقة، كانت الاستعلامات التي تتضمن تجميعات عبر خصائص التنقل داخل GroupBy ترجع أحيانًا إلى تقييم العميل. يترجم EF Core 9 هذا الأمر بشكل واضح إلى استعلام SQL واحد باستخدام GROUP BY، SUM، AVG، و COUNT.

الإسقاطات المعقدة والاستعلامات الفرعية

حصلت الاستعلامات الفرعية المتداخلة والإسقاطات المعقدة أيضًا على ترقية جدية. النظر في شيء من هذا القبيل:

var orderSummaries = await context.Customers
    .Select(c => new CustomerSummaryDto
    {
        Name = c.FullName,
        TotalOrders = c.Orders.Count(),
        MostRecentOrder = c.Orders
            .OrderByDescending(o => o.OrderDate)
            .Select(o => new OrderBriefDto
            {
                Id = o.Id,
                Date = o.OrderDate,
                Total = o.LineItems.Sum(li => li.Quantity * li.UnitPrice)
            })
            .FirstOrDefault(),
        TopCategory = c.Orders
            .SelectMany(o => o.LineItems)
            .GroupBy(li => li.Product.Category.Name)
            .OrderByDescending(g => g.Count())
            .Select(g => g.Key)
            .FirstOrDefault()
    })
    .ToListAsync();

يمكن لـ EF Core 9 الآن ترجمة هذا التعبير بالكامل إلى SQL دون إجراء تقييم من جانب العميل. يستخدم الاستعلام الذي تم إنشاؤه استعلامات فرعية مترابطة وروابط جانبية حيثما يكون ذلك مناسبًا، كما أن خطة SQL أكثر كفاءة بكثير مما تنتجه الإصدارات السابقة.

المجموعات البدائية ذات المعلمات

أحد التحسينات البارزة في LINQ هو القدرة على تمرير مجموعات من القيم الأولية مباشرة إلى الاستعلامات:

var statusFilter = new List<string> { "Active", "Pending", "Review" };

var filteredOrders = await context.Orders
    .Where(o => statusFilter.Contains(o.Status))
    .ToListAsync();

في EF Core 8، تمت ترجمة ذلك باستخدام عبارات IN ذات القيم المضمنة، مما يعني أنه لا يمكن إعادة استخدام ذاكرة التخزين المؤقت لخطة الاستعلام عند تغيير القائمة. يقوم EF Core 9 بتحديد معلمات هذه المجموعات بشكل صحيح، وإرسالها كمعلمة منظمة. يعد هذا أمرًا مهمًا للتخزين المؤقت لخطة الاستعلام على SQL Server وPostgreSQL.## العمليات المجمعة - تنفيذ التحديث وتنفيذ الحذف

تم تقديم ExecuteUpdate وExecuteDelete في EF Core 7، لكن EF Core 9 يوسع ما يمكنك فعله بهما بطرق مفيدة.

المزيد من تعبيرات التحديث المعقدة

يمكنك الآن استخدام تعبيرات أكثر تعقيدًا في ExecuteUpdate، بما في ذلك الإشارات إلى الجداول الأخرى من خلال خصائص التنقل:

await context.Products
    .Where(p => p.Category.IsDiscontinued)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.IsAvailable, false)
        .SetProperty(p => p.DiscontinuedDate, DateTimeOffset.UtcNow)
        .SetProperty(p => p.Price, p => p.Price * 0.5m));

يؤدي هذا إلى إنشاء عبارة UPDATE واحدة مع JOIN إلى جدول الفئات - لا حاجة لتحميل الكيانات في الذاكرة، ولا يوجد حمل لتتبع التغيير.

عمليات الحذف المشروطة المجمعة باستخدام الاستعلامات الفرعية

أصبحت الآن عمليات الحذف المجمعة باستخدام مرشحات الاستعلام الفرعي مدعومة بشكل كامل:

await context.AuditLogs
    .Where(log => log.CreatedAt < DateTime.UtcNow.AddYears(-2))
    .Where(log => !context.ProtectedRecords
        .Any(pr => pr.AuditLogId == log.Id))
    .ExecuteDeleteAsync();

يُترجم هذا إلى DELETE مع استعلام فرعي NOT EXISTS، وهو بالضبط ما ستكتبه يدويًا. لم يتم تحميل أي كيانات، ولا توجد رحلات ذهابًا وإيابًا.

تحسينات عمود JSON

لقد كانت أعمدة JSON واحدة من أكثر الميزات إثارة في إصدارات EF Core الأخيرة، ويأخذها EF Core 9 إلى أبعد من ذلك.

الاستعلام داخل JSON

يمكنك الآن تصفية البيانات وعرضها من داخل أعمدة JSON مع دعم أفضل للترجمة:

public class Order
{
    public int Id { get; set; }
    public string CustomerName { get; set; }
    public ShippingAddress Address { get; set; } // Stored as JSON
    public List<OrderNote> Notes { get; set; }   // Stored as JSON
}

public class ShippingAddress
{
    public string Street { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public string ZipCode { get; set; }
    public string Country { get; set; }
}

public class OrderNote
{
    public DateTime CreatedAt { get; set; }
    public string Text { get; set; }
    public string Author { get; set; }
}

التكوين في DbContext:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Order>(builder =>
    {
        builder.OwnsOne(o => o.Address, ab =>
        {
            ab.ToJson();
        });

        builder.OwnsMany(o => o.Notes, nb =>
        {
            nb.ToJson();
        });
    });
}

يمكنك الآن الاستعلام مباشرة إلى JSON:

var newYorkOrders = await context.Orders
    .Where(o => o.Address.State == "NY")
    .OrderByDescending(o => o.Notes.Count)
    .Select(o => new
    {
        o.CustomerName,
        o.Address.City,
        LatestNote = o.Notes
            .OrderByDescending(n => n.CreatedAt)
            .Select(n => n.Text)
            .FirstOrDefault()
    })
    .ToListAsync();

يقوم EF Core 9 بإنشاء استدعاءات JSON_VALUE و JSON_QUERY مناسبة على SQL Server (أو ما يعادله على مقدمي الخدمات الآخرين)، وتغطي الترجمة نطاقًا أوسع بكثير من عمليات LINQ على عناصر JSON أكثر من ذي قبل.

تحديث خصائص JSON

كانت إحدى نقاط الاحتكاك في EF Core 8 هي أن تحديث خاصية واحدة داخل عمود JSON قد يؤدي إلى إعادة كتابة مستند JSON بالكامل. يعمل EF Core 9 على تحسين ذلك من خلال تتبع تغييرات أكثر دقة للأنواع المعينة بـ JSON، مما يؤدي إلى إنشاء المزيد من التحديثات المستهدفة عندما يكون ذلك ممكنًا.

var order = await context.Orders.FindAsync(orderId);
order.Address.ZipCode = "10001";
await context.SaveChangesAsync();

بالنسبة للموفرين المدعومين، يمكن أن يؤدي ذلك إلى إنشاء تعديل JSON أكثر استهدافًا بدلاً من إعادة كتابة الكائن الثنائي الكبير بالكامل.

الأنواع المعقدة — كائنات القيمة بدون هوية

تعد الأنواع المعقدة إحدى الميزات التي كان الممارسون في مجال التصميم المبني على المجال ينتظرونها. على عكس الأنواع المملوكة، فإن الأنواع المعقدة ليس لها هوية - فهي كائنات ذات قيمة خالصة.

[ComplexType]
public record Money(decimal Amount, string Currency);

[ComplexType]
public record DateRange(DateTime Start, DateTime End);

public class Project
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Money Budget { get; set; }
    public DateRange Timeline { get; set; }
}

يتم تخزينها كأعمدة مسطحة في الجدول الأصلي - Budget_Amount، Budget_Currency، Timeline_Start، Timeline_End - دون الحاجة إلى جدول منفصل أو أي نوع من المفاتيح.

الفرق الرئيسي عن الأنواع المملوكة: تتم مقارنة الأنواع المعقدة من حيث القيمة، وليس من خلال المرجع. يعتبر مثيلان Money بنفس Amount و Currency متساويين، بغض النظر عن الكيان الذي ينتمون إليه.

var expensiveProjects = await context.Projects
    .Where(p => p.Budget.Amount > 100_000m && p.Budget.Currency == "USD")
    .OrderByDescending(p => p.Budget.Amount)
    .ToListAsync();

ويترجم هذا مباشرة إلى التصفية على الأعمدة المسطحة - نظيفة وفعالة، وهي بالضبط ما تتوقعه.

دعم التسلسل الهرمي لخادم SQL

إذا سبق لك التعامل مع البيانات الهرمية في SQL Server - المخططات التنظيمية وأشجار الفئات وأنظمة الملفات - فأنت تعلم أن HierarchyId هو النوع المضمن لهذا الغرض. يقدم EF Core 9 دعمًا من الدرجة الأولى له.

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Title { get; set; }
    public HierarchyId PathFromCeo { get; set; }
}

يمكنك الآن الاستعلام عن العلاقات الهرمية مباشرة:

var managerId = HierarchyId.Parse("/1/3/");

// All direct and indirect reports
var allReports = await context.Employees
    .Where(e => e.PathFromCeo.IsDescendantOf(managerId))
    .Where(e => e.PathFromCeo != managerId) // exclude the manager
    .OrderBy(e => e.PathFromCeo)
    .ToListAsync();

// Direct reports only (one level down)
var directReports = await context.Employees
    .Where(e => e.PathFromCeo.GetAncestor(1) == managerId)
    .ToListAsync();

// Get an employee's depth in the hierarchy
var employeesWithDepth = await context.Employees
    .Select(e => new
    {
        e.Name,
        e.Title,
        Level = e.PathFromCeo.GetLevel()
    })
    .OrderBy(e => e.Level)
    .ToListAsync();
```كل هذه تترجم إلى أساليب `HierarchyId` الأصلية لـ SQL Server. إذا كنت تقوم بتنفيذ هياكل شجرية ذات مرجعية ذاتية للمفاتيح الخارجية وCTEs العودية، فهذا نهج أكثر نظافة.

## النماذج المجمعة ودعم AOT

سيقدر المطورون المهتمون بالأداء الاستثمار المستمر في النماذج المجمعة ودعم التجميع المسبق (AOT).

### النماذج المجمعة

تقوم النماذج المجمعة بإنشاء البيانات التعريفية للنموذج مسبقًا والتي تقوم EF Core ببنائها عادةً عند بدء التشغيل. بالنسبة للنماذج الكبيرة (فكر في مئات الكيانات)، يمكن أن يؤدي ذلك إلى تقليل وقت البدء البارد بشكل كبير.

```bash
dotnet ef dbcontext optimize --output-dir CompiledModels --namespace MyApp.CompiledModels

ثم قم بتوصيله:

builder.Services.AddDbContext<AppDbContext>(options =>
    options
        .UseSqlServer(connectionString)
        .UseModel(MyApp.CompiledModels.AppDbContextModel.Instance));

في EF Core 9، تكون النماذج المجمعة أكثر اكتمالاً - فهي تدعم المزيد من ميزات رسم الخرائط وتنتج مخرجات أصغر. بالنسبة للنموذج الذي يحتوي على حوالي 400 كيان، يمكن أن ينخفض ​​وقت بدء التشغيل من عدة ثوانٍ إلى شبه فوري.

تقدم تجميع AOT

لا يزال دعم AOT الأصلي الكامل لـ EF Core قيد التقدم، لكن EF Core 9 يحقق خطوات كبيرة. تمت إعادة هيكلة العديد من مسارات التعليمات البرمجية ذات الانعكاس الثقيل لتكون ملائمة للتشذيب، وتعد النماذج المجمعة جزءًا أساسيًا من قصة AOT. إذا كنت تستهدف سيناريوهات مثل وظائف Azure أو الخدمات الصغيرة حيث تكون البداية الباردة مهمة، فإن هذه التحسينات ذات صلة مباشرة.

تحديثات موفر قاعدة بيانات Cosmos

يستمر موفر Azure Cosmos DB في النضج باستخدام EF Core 9. بعض التحسينات الملحوظة:

التعامل مع مفتاح التقسيم

يدعم الموفر الآن مفاتيح الأقسام الهرمية ويتعامل مع عوامل تصفية مفاتيح الأقسام بشكل أكثر ذكاءً:

public class TenantDocument
{
    public string Id { get; set; }
    public string TenantId { get; set; }  // Partition key
    public string Region { get; set; }     // Sub-partition key
    public string Content { get; set; }
}
// This query now correctly uses the partition key for routing
var docs = await context.TenantDocuments
    .Where(d => d.TenantId == "tenant-42" && d.Region == "us-east")
    .Where(d => d.Content.Contains("important"))
    .ToListAsync();

تحسين ترجمة LINQ إلى NoSQL

يتم الآن ترجمة المزيد من عمليات LINQ إلى لهجة SQL الخاصة بـ Cosmos DB، بما في ذلك دعم أفضل لـ Contains، Any، وعمليات المصفوفة المتداخلة، والوظائف الرياضية. يتم الآن التعامل مع الاستعلامات التي كانت ترجع سابقًا إلى تقييم العميل من جانب الخادم.

دعم البحث عن المتجهات

يقدم EF Core 9 دعمًا مبكرًا للبحث عن تشابه المتجهات باستخدام Cosmos DB، وهو أمر مفيد إذا كنت تقوم بإنشاء تطبيقات تتكامل مع عمليات التضمين أو البحث المعتمد على الذكاء الاصطناعي:

var results = await context.Documents
    .OrderBy(d => EF.Functions.VectorDistance(d.Embedding, queryVector))
    .Take(10)
    .ToListAsync();

تحسينات الهجرة

حصلت عمليات الترحيل على بعض التحسينات في جودة الحياة مما يجعل العمل معهم أقل إيلامًا في بيئات الفريق.

الجداول الزمنية في الهجرات

تتعامل عمليات الترحيل الآن مع تكوين الجدول الزمني بشكل أكثر رشاقة، مع الدعم المناسب لأعمدة الفترة وتسمية جدول المحفوظات:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Employee>()
        .ToTable("Employees", b => b.IsTemporal(t =>
        {
            t.HasPeriodStart("ValidFrom");
            t.HasPeriodEnd("ValidTo");
            t.UseHistoryTable("EmployeeHistory");
        }));
}

نصوص عاجزة

ينتج الأمر Script-Migration (ومكافئه لواجهة سطر الأوامر) نصوصًا برمجية أفضل بشكل افتراضي، مع معالجة محسنة لحالات الحافة حول تغييرات المخطط التي تعتمد على البيانات الموجودة في حالات معينة.

حزم الهجرة

تعد حزم الترحيل، التي تجمع عمليات الترحيل الخاصة بك في ملف مستقل قابل للتنفيذ للنشر، أكثر موثوقية في EF Core 9 مع الإبلاغ عن الأخطاء بشكل أفضل ومنطق إعادة المحاولة في حالات الفشل العابر.

dotnet ef migrations bundle --self-contained -r linux-x64

ينتج عن ذلك ملف ثنائي يمكنك تشغيله في مسار CI/CD الخاص بك دون الحاجة إلى تثبيت .NET SDK على هدف النشر الخاص بك.

معايير الأداءفيما يلي بعض المعايير التقريبية من الاختبار الخاص بي. هذه من مشروع يضم ما يقرب من 200 كيان، تعمل مقابل SQL Server 2022، ويتم قياسها باستخدام BenchmarkDotNet. ستختلف أرقامك، لكن التحسينات النسبية يجب أن تكون متشابهة.

السيناريوإي أف كور 8إي أف كور 9تحسين
بناء النموذج (البداية الباردة)1,850 مللي ثانية320 مللي ثانية~5.8x أسرع (مترجمة)
استعلام بسيط (كيان واحد بواسطة PK)0.42 مللي ثانية0.38 مللي ثانية~10% أسرع
استعلام معقد (صلات + تجميع)3.1 مللي ثانية2.4 مللي ثانية~23% أسرع
تحديث مجمع (10 آلاف صف)145 مللي ثانية118 مللي ثانية~19% أسرع
استعلام عمود JSON2.8 مللي ثانية1.9 مللي ثانية~32% أسرع
حفظ التغييرات (100 كيان)48 مللي ثانية41 مللي ثانية~15% أسرع

يعد تحسين النموذج المجمّع هو الأكثر دراماتيكية، ولكن التحسينات الثابتة في جميع المجالات تضيف المزيد - خاصة في السيناريوهات عالية الإنتاجية حيث تقوم بتنفيذ آلاف الاستعلامات في الثانية.

الترقية من EF Core 8

إذا كنت تستخدم EF Core 8، فإن مسار الترقية يكون سلسًا نسبيًا. فيما يلي قائمة مرجعية:

**1. تحديث الحزم الخاصة بك: **

<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />

**2. تحقق من وجود تغييرات عاجلة. ** قائمة EF Core 9 قصيرة نسبيًا مقارنة ببعض الإصدارات السابقة. أبرزها:

  • تمت إزالة بعض واجهات برمجة التطبيقات (APIs) القديمة سابقًا
  • تغييرات في كيفية ترجمة استعلامات GroupBy معينة (تنتقل الآن إلى جانب الخادم، مما يغير السلوك إذا كنت تعتمد على تقييم العميل)
  • تغييرات طفيفة في مخرجات سقالات الهجرة

3. أعد إنشاء النماذج المجمعة إذا كنت تستخدمها. تم تغيير التنسيق، لذا لن تعمل النماذج القديمة المجمعة مع EF Core 9.

**4. قم بتشغيل مجموعة الاختبار الخاصة بك. ** انتبه بشكل خاص للاستعلامات التي تم تقييمها مسبقًا على العميل - ربما يتم تقييمها الآن على الخادم، وهو ما يكون عادةً أفضل ولكنه قد يؤدي إلى ظهور اختلافات في البيانات.

5. تحقق من استعلامات Cosmos DB إذا كنت تستخدم هذا الموفر. تعني الترجمات المحسنة أن بعض الاستعلامات سيتم تنفيذها بشكل مختلف (عادةً ما يكون أسرع)، ولكن من المفيد التحقق من تطابق النتائج.

يبدو الحد الأدنى من الترقية لمشروع نموذجي كما يلي:

dotnet add package Microsoft.EntityFrameworkCore --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.0
dotnet build
dotnet test

إذا تم تجميع كل شيء واجتياز الاختبارات، فمن المحتمل أنك في حالة جيدة. إذا واجهت مشكلات، فإن وثائق التغييرات العاجلة في EF Core 9 تحتوي على إرشادات مفصلة للترحيل لكل تغيير.

الختام

إن EF Core 9 ليس إصدارًا ثوريًا، بل هو إصدار تطوري، وهذا بالضبط ما كان يجب أن يكون عليه. تحسينات LINQ وحدها تبرر الترقية لمعظم المشاريع، والميزات مثل تحسينات أعمدة JSON، والأنواع المعقدة، وHierarchyId تدعم الأنماط المفتوحة التي كانت في السابق غير ملائمة أو مستحيلة.

إذا اضطررت إلى اختيار الميزات الثلاثة التي كان لها التأثير الأكبر على مشاريعي:

  1. المجموعات الأولية ذات المعلمات — لأن كفاءة ذاكرة التخزين المؤقت لخطة الاستعلام مهمة على نطاق واسع
  2. تحسينات عمود JSON — لأن نمط المستند العلائقي المختلط مفيد بشكل لا يصدق
  3. النماذج المجمعة — لأن وقت بدء التشغيل يؤثر بشكل مباشر على إنتاجية المطور وسرعة النشرلقد كان فريق EF Core يسير على مسار قوي منذ EF Core 5، ويواصل الإصدار 9 هذا الاتجاه. إذا كنت مشتركًا بالفعل في EF Core 8، فإن الترقية تكون منخفضة المخاطر ومكافأة عالية. إذا كنت تستخدم شيئًا أقدم، فليس هناك وقت أفضل للقيام بهذه القفزة.

برمجة سعيدة - واستعلام سعيد.