EF Core 8'deki Yenilikler

EF Core 8.0 (EF8), Kasım 2023'te piyasaya sürüldü.

İpucu

GitHub'dan örnek kodu indirerek örnekleri çalıştırabilir ve hata ayıklayabilirsiniz. Her bölüm, bu bölüme özgü kaynak koduna bağlanır.

EF8, .NET 8 SDK'sının derlenip çalışması için .NET 8 çalışma zamanını gerektirir. EF8 önceki .NET sürümlerinde çalışmaz ve .NET Framework'te çalışmaz.

Karmaşık Türler kullanarak nesneleri değerle

Veritabanına kaydedilen nesneler üç geniş kategoriye ayrılabilir:

  • Yapılandırılmamış ve tek bir değer tutan nesneler. Örneğin, int, Guid, string, IPAddress. Bunlar (kısmen) "ilkel türler" olarak adlandırılır.
  • Birden çok değeri barındıracak şekilde yapılandırılmış nesneler ve nesnenin kimliğinin bir anahtar değeri tarafından tanımlandığı yer. Örneğin, Blog, Post, Customer. Bunlara "varlık türleri" denir.
  • Birden çok değeri barındıracak şekilde yapılandırılmış nesneler, ancak nesnenin anahtar tanımlama kimliği yoktur. Örneğin, Address, Coordinate.

EF8'in öncesinde üçüncü nesne türünü eşlemenin iyi bir yolu yoktu. Sahip olunan türler kullanılabilir, ancak sahip olunan türler aslında varlık türleri olduğundan, anahtar değeri gizli olsa bile anahtar değerine dayalı semantiği vardır.

EF8 artık bu üçüncü nesne türünü kapsayacak şekilde "Karmaşık Türler"i destekliyor. Karmaşık tür nesneleri:

  • Anahtar değeri tarafından tanımlanmaz veya izlenmez.
  • Varlık türünün bir parçası olarak tanımlanmalıdır. (Başka bir deyişle, karmaşık türde bir DbSet türünüz olamaz.)
  • .NET değer türleri veya başvuru türleri olabilir.
  • Örnekler birden çok özellik tarafından paylaşılabilir.

Basit örnek

Örneğin, bir Address tür düşünün:

public class Address
{
    public required string Line1 { get; set; }
    public string? Line2 { get; set; }
    public required string City { get; set; }
    public required string Country { get; set; }
    public required string PostCode { get; set; }
}

Address daha sonra basit bir müşteri/sipariş modelinde üç yerde kullanılır:

public class Customer
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public required Address Address { get; set; }
    public List<Order> Orders { get; } = new();
}

public class Order
{
    public int Id { get; set; }
    public required string Contents { get; set; }
    public required Address ShippingAddress { get; set; }
    public required Address BillingAddress { get; set; }
    public Customer Customer { get; set; } = null!;
}

Şimdi adresiyle bir müşteri oluşturup kaydedelim:

var customer = new Customer
{
    Name = "Willow",
    Address = new() { Line1 = "Barking Gate", City = "Walpole St Peter", Country = "UK", PostCode = "PE14 7AV" }
};

context.Add(customer);
await context.SaveChangesAsync();

Bu, veritabanına aşağıdaki satırın eklenmesiyle sonuçlanır:

INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);

Karmaşık türlerin kendi tablolarını almadıklarına dikkat edin. Bunun yerine, bunlar tablonun sütunlarına Customers satır içi olarak kaydedilir. Bu, sahip olunan türlerin tablo paylaşma davranışıyla eşleşir.

Not

Karmaşık türlerin kendi tablolarına eşlenmesine izin vermeyeceğiz. Ancak, gelecek bir sürümde karmaşık türün tek bir sütunda JSON belgesi olarak kaydedilmesine izin vermenizi planlıyoruz. Bu sizin için önemliyse Sorun #31252'ye oy verin.

Şimdi müşteriye bir sipariş göndermek istediğimizi ve müşterinin adresini hem varsayılan fatura hem de sevkiyat adresi olarak kullanmak istediğimizi varsayalım. Bunu gerçekleştirmenin doğal yolu, nesnesinden Customer içine kopyalamaktır.AddressOrder Örneğin:

customer.Orders.Add(
    new Order { Contents = "Tesco Tasty Treats", BillingAddress = customer.Address, ShippingAddress = customer.Address, });

await context.SaveChangesAsync();

Karmaşık türlerde bu işlem beklendiği gibi çalışır ve adres tabloya Orders eklenir:

INSERT INTO [Orders] ([Contents], [CustomerId],
    [BillingAddress_City], [BillingAddress_Country], [BillingAddress_Line1], [BillingAddress_Line2], [BillingAddress_PostCode],
    [ShippingAddress_City], [ShippingAddress_Country], [ShippingAddress_Line1], [ShippingAddress_Line2], [ShippingAddress_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11);

Şu ana kadar "ama bunu sahip olunan tiplerle yapabilirim!" diyor olabilirsiniz. Ancak, sahip olunan türlerin "varlık türü" semantiği hızla yoluna çıkar. Örneğin, yukarıdaki kodun sahip olunan türlerle çalıştırılması bir uyarı kümesine ve ardından bir hataya neden olur:

warn: 8/20/2023 12:48:01.678 CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001] (Microsoft.EntityFrameworkCore.Update) 
      The same entity is being tracked as different entity types 'Order.BillingAddress#Address' and 'Customer.Address#Address' with defining navigations. If a property value changes, it will result in two store changes, which might not be the desired outcome.
warn: 8/20/2023 12:48:01.687 CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001] (Microsoft.EntityFrameworkCore.Update) 
      The same entity is being tracked as different entity types 'Order.ShippingAddress#Address' and 'Customer.Address#Address' with defining navigations. If a property value changes, it will result in two store changes, which might not be the desired outcome.
warn: 8/20/2023 12:48:01.687 CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001] (Microsoft.EntityFrameworkCore.Update)
      The same entity is being tracked as different entity types 'Order.ShippingAddress#Address' and 'Order.BillingAddress#Address' with defining navigations. If a property value changes, it will result in two store changes, which might not be the desired outcome.
fail: 8/20/2023 12:48:01.709 CoreEventId.SaveChangesFailed[10000] (Microsoft.EntityFrameworkCore.Update) 
      An exception occurred in the database while saving changes for context type 'NewInEfCore8.ComplexTypesSample+CustomerContext'.
      System.InvalidOperationException: Cannot save instance of 'Order.ShippingAddress#Address' because it is an owned entity without any reference to its owner. Owned entities can only be saved as part of an aggregate also including the owner entity.
         at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.PrepareToSave()

Bunun nedeni, varlık türünün tek bir örneğinin Address (aynı gizli anahtar değerine sahip) üç farklı varlık örneği için kullanılmasıdır. Öte yandan, karmaşık özellikler arasında aynı örneği paylaşmaya izin verilir ve bu nedenle kod karmaşık türler kullanılırken beklendiği gibi çalışır.

Karmaşık türlerin yapılandırması

Karmaşık türler modelde eşleme öznitelikleri kullanılarak veya içinde API OnModelCreatingçağrılarakComplexPropertyyapılandırılmalıdır. Karmaşık türler kural tarafından bulunmaz.

Örneğin, Address türü kullanılarak ComplexTypeAttributeyapılandırılabilir:

[ComplexType]
public class Address
{
    public required string Line1 { get; set; }
    public string? Line2 { get; set; }
    public required string City { get; set; }
    public required string Country { get; set; }
    public required string PostCode { get; set; }
}

veya içinde:OnModelCreating

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>()
        .ComplexProperty(e => e.Address);

    modelBuilder.Entity<Order>(b =>
    {
        b.ComplexProperty(e => e.BillingAddress);
        b.ComplexProperty(e => e.ShippingAddress);
    });
}

Mutability

Yukarıdaki örnekte üç yerde kullanılan örneği bulduk Address . Bu izin verilir ve karmaşık türler kullanılırken EF Core için herhangi bir soruna neden olmaz. Ancak, aynı başvuru türündeki örneklerin paylaşılması, örnekteki bir özellik değeri değiştirilirse bu değişikliğin üç kullanımda da yansıtılacağı anlamına gelir. Örneğin, yukarıdan başlayarak müşteri adresini değiştirelim Line1 ve değişiklikleri kaydedelim:

customer.Address.Line1 = "Peacock Lodge";
await context.SaveChangesAsync();

Bu, SQL Server kullanılırken veritabanında aşağıdaki güncelleştirmenin sonucunu döndürür:

UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;
UPDATE [Orders] SET [BillingAddress_Line1] = @p2, [ShippingAddress_Line1] = @p3
OUTPUT 1
WHERE [Id] = @p4;

Hepsi aynı örneği paylaştığından üç Line1 sütunun da değiştiğine dikkat edin. Bu genellikle istediğimiz şey değildir.

İpucu

Müşteri adresi değiştiğinde sipariş adreslerinin otomatik olarak değişmesi gerekiyorsa adresi bir varlık türü olarak eşlemeyi göz önünde bulundurun. Order ve Customer daha sonra bir gezinti özelliği aracılığıyla aynı adres örneğine (artık bir anahtarla tanımlanır) güvenli bir şekilde başvurabilir.

Bunun gibi sorunlarla başa çıkmanın iyi bir yolu, türü sabit hale getirmektir. Aslında, bir tür karmaşık bir tür olmak için iyi bir aday olduğunda bu değişmezlik genellikle doğaldır. Örneğin, genellikle ülkeyi aynı şekilde bırakırken yalnızca sessize almak yerine karmaşık yeni Address bir nesne sağlamak mantıklıdır.

Hem başvuru hem de değer türleri sabit hale getirilebilir. Aşağıdaki bölümlerde bazı örneklere göz atacağız.

Karmaşık türler olarak başvuru türleri

Sabit sınıf

Yukarıdaki örnekte basit, değiştirilebilir class bir kullandık. Yukarıda açıklanan yanlışlıkla mutasyonla ilgili sorunları önlemek için sınıfı sabit hale getirebiliriz. Örneğin:

public class Address
{
    public Address(string line1, string? line2, string city, string country, string postCode)
    {
        Line1 = line1;
        Line2 = line2;
        City = city;
        Country = country;
        PostCode = postCode;
    }

    public string Line1 { get; }
    public string? Line2 { get; }
    public string City { get; }
    public string Country { get; }
    public string PostCode { get; }
}

İpucu

C# 12 veya üzeri ile, bu sınıf tanımı birincil oluşturucu kullanılarak basitleştirilebilir:

public class Address(string line1, string? line2, string city, string country, string postCode)
{
    public string Line1 { get; } = line1;
    public string? Line2 { get; } = line2;
    public string City { get; } = city;
    public string Country { get; } = country;
    public string PostCode { get; } = postCode;
}

Artık mevcut bir adreste Line1 değeri değiştirmek mümkün değildir. Bunun yerine, değiştirilen değerle yeni bir örnek oluşturmamız gerekir. Örneğin:

var currentAddress = customer.Address;
customer.Address = new Address(
    "Peacock Lodge", currentAddress.Line2, currentAddress.City, currentAddress.Country, currentAddress.PostCode);

await context.SaveChangesAsync();

Bu kez çağrısı SaveChangesAsync yalnızca müşteri adresini güncelleştirir:

UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;

Address nesnesi sabit olsa ve nesnenin tamamı değiştirilmiş olsa da EF'nin tek tek özelliklerdeki değişiklikleri izlemeye devam ettiğini, dolayısıyla yalnızca değişen değerlere sahip sütunların güncelleştirildiğini unutmayın.

Sabit kayıt

C# 9, sabit nesneleri oluşturmayı ve kullanmayı kolaylaştıran kayıt türlerini kullanıma sunar. Örneğin, Address nesnesi bir kayıt türü yapılabilir:

public record Address
{
    public Address(string line1, string? line2, string city, string country, string postCode)
    {
        Line1 = line1;
        Line2 = line2;
        City = city;
        Country = country;
        PostCode = postCode;
    }

    public string Line1 { get; init; }
    public string? Line2 { get; init; }
    public string City { get; init; }
    public string Country { get; init; }
    public string PostCode { get; init; }
}

İpucu

Bu kayıt tanımı, birincil oluşturucu kullanılarak basitleştirilebilir:

public record Address(string Line1, string? Line2, string City, string Country, string PostCode);

Değiştirilebilir nesneyi değiştirmek ve çağırmak SaveChanges artık daha az kod gerektirir:

customer.Address = customer.Address with { Line1 = "Peacock Lodge" };

await context.SaveChangesAsync();

Karmaşık türler olarak değer türleri

Değiştirilebilir yapı

Basit bir değiştirilebilir değer türü karmaşık bir tür olarak kullanılabilir. Örneğin, Address C# dilinde olarak struct tanımlanabilir:

public struct Address
{
    public required string Line1 { get; set; }
    public string? Line2 { get; set; }
    public required string City { get; set; }
    public required string Country { get; set; }
    public required string PostCode { get; set; }
}

Müşteri Address nesnesini sevkiyat ve faturalama Address özelliklerine atamak, her özelliğin değerinin bir kopyasını almasına Addressneden olur çünkü değer türleri bu şekilde çalışır. Başka bir deyişle, müşteride değişiklik yapmak Address gönderim veya faturalama Address örneklerini değiştirmez, dolayısıyla değiştirilebilir yapılarda, değiştirilebilir sınıflarda gerçekleşen örnek paylaşımı sorunları aynı olmaz.

Ancak, C# dilinde genellikle değiştirilebilir yapılar önerilmez, bu nedenle kullanmadan önce çok dikkatli düşünün.

Sabit yapı

Sabit yapılar, sabit sınıflar gibi karmaşık türler kadar iyi çalışır. Örneğin, Address değiştirilmeyecek şekilde tanımlanabilir:

public readonly struct Address(string line1, string? line2, string city, string country, string postCode)
{
    public string Line1 { get; } = line1;
    public string? Line2 { get; } = line2;
    public string City { get; } = city;
    public string Country { get; } = country;
    public string PostCode { get; } = postCode;
}

Adresi değiştirme kodu artık sabit sınıf kullanılırken kullanılan kodla aynı görünür:

var currentAddress = customer.Address;
customer.Address = new Address(
    "Peacock Lodge", currentAddress.Line2, currentAddress.City, currentAddress.Country, currentAddress.PostCode);

await context.SaveChangesAsync();

Sabit yapı kaydı

C# 10, sabit yapı kayıtlarının, sabit sınıf kayıtlarıyla olduğu gibi oluşturulmasını ve bunlarla çalışmasını kolaylaştıran türler sunar struct record . Örneğin, sabit bir yapı kaydı olarak tanımlayabiliriz Address :

public readonly record struct Address(string Line1, string? Line2, string City, string Country, string PostCode);

Adresi değiştirme kodu artık sabit sınıf kaydı kullanılırken kullanılan kodla aynı görünür:

customer.Address = customer.Address with { Line1 = "Peacock Lodge" };

await context.SaveChangesAsync();

İç içe karmaşık türler

Karmaşık bir tür, diğer karmaşık türlerin özelliklerini içerebilir. Örneğin, yukarıdaki karmaşık türümüzü Address karmaşık bir türle birlikte kullanalım ve her ikisini de başka bir PhoneNumber karmaşık türün içine yerleştirelim:

public record Address(string Line1, string? Line2, string City, string Country, string PostCode);

public record PhoneNumber(int CountryCode, long Number);

public record Contact
{
    public required Address Address { get; init; }
    public required PhoneNumber HomePhone { get; init; }
    public required PhoneNumber WorkPhone { get; init; }
    public required PhoneNumber MobilePhone { get; init; }
}

Bunlar karmaşık türlerimizin semantiğine uygun olduğundan burada sabit kayıtlar kullanıyoruz, ancak karmaşık türlerin iç içe yerleştirme işlemi herhangi bir .NET türüyle yapılabilir.

Not

EF Core henüz karmaşık tür değerlerinin oluşturucu eklemesini Contact desteklemediğinden tür için birincil oluşturucu kullanmıyoruz. Bu sizin için önemliyse Sorun #31621 için oy verin.

öğesinin Customerbir özelliği olarak ekleyeceğizContact:

public class Customer
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public required Contact Contact { get; set; }
    public List<Order> Orders { get; } = new();
}

Ve PhoneNumber özelliğinin Orderözellikleri olarak:

public class Order
{
    public int Id { get; set; }
    public required string Contents { get; set; }
    public required PhoneNumber ContactPhone { get; set; }
    public required Address ShippingAddress { get; set; }
    public required Address BillingAddress { get; set; }
    public Customer Customer { get; set; } = null!;
}

İç içe karmaşık türlerin yapılandırması yeniden kullanılarak ComplexTypeAttributeelde edilebilir:

[ComplexType]
public record Address(string Line1, string? Line2, string City, string Country, string PostCode);

[ComplexType]
public record PhoneNumber(int CountryCode, long Number);

[ComplexType]
public record Contact
{
    public required Address Address { get; init; }
    public required PhoneNumber HomePhone { get; init; }
    public required PhoneNumber WorkPhone { get; init; }
    public required PhoneNumber MobilePhone { get; init; }
}

veya içinde:OnModelCreating

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>(
        b =>
        {
            b.ComplexProperty(
                e => e.Contact,
                b =>
                {
                    b.ComplexProperty(e => e.Address);
                    b.ComplexProperty(e => e.HomePhone);
                    b.ComplexProperty(e => e.WorkPhone);
                    b.ComplexProperty(e => e.MobilePhone);
                });
        });

    modelBuilder.Entity<Order>(
        b =>
        {
            b.ComplexProperty(e => e.ContactPhone);
            b.ComplexProperty(e => e.BillingAddress);
            b.ComplexProperty(e => e.ShippingAddress);
        });
}

Sorgular

Varlık türlerinde karmaşık türlerin özellikleri, varlık türünün diğer gezinti dışı özellikleri gibi kabul edilir. Bu, varlık türü yüklendiğinde her zaman yüklendiği anlamına gelir. Bu, iç içe yerleştirilmiş karmaşık tür özellikleri için de geçerlidir. Örneğin, bir müşteriyi sorgulama:

var customer = await context.Customers.FirstAsync(e => e.Id == customerId);

SQL Server kullanılırken aşağıdaki SQL'e çevrilir:

SELECT TOP(1) [c].[Id], [c].[Name], [c].[Contact_Address_City], [c].[Contact_Address_Country],
    [c].[Contact_Address_Line1], [c].[Contact_Address_Line2], [c].[Contact_Address_PostCode],
    [c].[Contact_HomePhone_CountryCode], [c].[Contact_HomePhone_Number], [c].[Contact_MobilePhone_CountryCode],
    [c].[Contact_MobilePhone_Number], [c].[Contact_WorkPhone_CountryCode], [c].[Contact_WorkPhone_Number]
FROM [Customers] AS [c]
WHERE [c].[Id] = @__customerId_0

Bu SQL'den iki öğeye dikkat edin:

  • Müşteriyi ve iç içe, ContactAddressvePhoneNumber karmaşık türleri doldurmak için her şey döndürülür.
  • Tüm karmaşık tür değerleri, varlık türü için tabloda sütun olarak depolanır. Karmaşık türler hiçbir zaman ayrı tablolara eşlenmez.

Projeksiyonlar

Karmaşık türler sorgudan yansıtılabilir. Örneğin, bir siparişten yalnızca sevkiyat adresini seçme:

var shippingAddress = await context.Orders
    .Where(e => e.Id == orderId)
    .Select(e => e.ShippingAddress)
    .SingleAsync();

Bu, SQL Server kullanılırken aşağıdakine çevrilir:

SELECT TOP(2) [o].[ShippingAddress_City], [o].[ShippingAddress_Country], [o].[ShippingAddress_Line1],
    [o].[ShippingAddress_Line2], [o].[ShippingAddress_PostCode]
FROM [Orders] AS [o]
WHERE [o].[Id] = @__orderId_0

Karmaşık tür nesnelerinin izleme için kullanılacak kimliği olmadığından karmaşık türlerin projeksiyonlarının izlenemeyeceğini unutmayın.

Koşullarda kullanma

Karmaşık türlerin üyeleri koşullarda kullanılabilir. Örneğin, belirli bir şehre giden tüm siparişleri bulma:

var city = "Walpole St Peter";
var walpoleOrders = await context.Orders.Where(e => e.ShippingAddress.City == city).ToListAsync();

Bu, SQL Server'da aşağıdaki SQL'e çevrilir:

SELECT [o].[Id], [o].[Contents], [o].[CustomerId], [o].[BillingAddress_City], [o].[BillingAddress_Country],
    [o].[BillingAddress_Line1], [o].[BillingAddress_Line2], [o].[BillingAddress_PostCode],
    [o].[ContactPhone_CountryCode], [o].[ContactPhone_Number], [o].[ShippingAddress_City],
    [o].[ShippingAddress_Country], [o].[ShippingAddress_Line1], [o].[ShippingAddress_Line2],
    [o].[ShippingAddress_PostCode]
FROM [Orders] AS [o]
WHERE [o].[ShippingAddress_City] = @__city_0

Koşullarda tam bir karmaşık tür örneği de kullanılabilir. Örneğin, belirli bir telefon numarasına sahip tüm müşterileri bulma:

var phoneNumber = new PhoneNumber(44, 7777555777);
var customersWithNumber = await context.Customers
    .Where(
        e => e.Contact.MobilePhone == phoneNumber
             || e.Contact.WorkPhone == phoneNumber
             || e.Contact.HomePhone == phoneNumber)
    .ToListAsync();

Bu, SQL Server kullanılırken aşağıdaki SQL'e çevrilir:

SELECT [c].[Id], [c].[Name], [c].[Contact_Address_City], [c].[Contact_Address_Country], [c].[Contact_Address_Line1],
     [c].[Contact_Address_Line2], [c].[Contact_Address_PostCode], [c].[Contact_HomePhone_CountryCode],
     [c].[Contact_HomePhone_Number], [c].[Contact_MobilePhone_CountryCode], [c].[Contact_MobilePhone_Number],
     [c].[Contact_WorkPhone_CountryCode], [c].[Contact_WorkPhone_Number]
FROM [Customers] AS [c]
WHERE ([c].[Contact_MobilePhone_CountryCode] = @__entity_equality_phoneNumber_0_CountryCode
    AND [c].[Contact_MobilePhone_Number] = @__entity_equality_phoneNumber_0_Number)
OR ([c].[Contact_WorkPhone_CountryCode] = @__entity_equality_phoneNumber_0_CountryCode
    AND [c].[Contact_WorkPhone_Number] = @__entity_equality_phoneNumber_0_Number)
OR ([c].[Contact_HomePhone_CountryCode] = @__entity_equality_phoneNumber_0_CountryCode
    AND [c].[Contact_HomePhone_Number] = @__entity_equality_phoneNumber_0_Number)

Eşitliğin karmaşık türün her üyesi genişletilerek gerçekleştirildiğini fark edin. Bu, kimlik anahtarı olmayan karmaşık türlerle hizalanır ve bu nedenle karmaşık tür örneği, yalnızca tüm üyeleri eşitse başka bir karmaşık tür örneğine eşit olur. Bu, kayıt türleri için .NET tarafından tanımlanan eşitlikle de hizalanır.

Karmaşık tür değerlerinin manipülasyonu

EF8, karmaşık türlerin geçerli ve özgün değerleri ve bir özellik değerinin değiştirilip değiştirilmediği gibi izleme bilgilerine erişim sağlar. API karmaşık türleri, varlık türleri için zaten kullanılan değişiklik izleme API'sinin bir uzantısıdır.

yöntemleri, ComplexProperty karmaşık bir nesnenin EntityEntry tamamı için bir girdi döndürür. Örneğin, geçerli değerini Order.BillingAddressalmak için:

var billingAddress = context.Entry(order)
    .ComplexProperty(e => e.BillingAddress)
    .CurrentValue;

Karmaşık türündeki bir özelliğe erişmek için çağrısı Property eklenebilir. Örneğin, yalnızca fatura posta kodunun geçerli değerini almak için:

var postCode = context.Entry(order)
    .ComplexProperty(e => e.BillingAddress)
    .Property(e => e.PostCode)
    .CurrentValue;

İç içe karmaşık türlere için iç içe çağrılar ComplexPropertykullanılarak erişilir. Örneğin, bir üzerinde Customeriç içe AddressContact şehir almak için:

var currentCity = context.Entry(customer)
    .ComplexProperty(e => e.Contact)
    .ComplexProperty(e => e.Address)
    .Property(e => e.City)
    .CurrentValue;

Durumu okumak ve değiştirmek için başka yöntemler de kullanılabilir. Örneğin, PropertyEntry.IsModified karmaşık bir türün özelliğini değiştirilmiş olarak ayarlamak için kullanılabilir:

context.Entry(customer)
    .ComplexProperty(e => e.Contact)
    .ComplexProperty(e => e.Address)
    .Property(e => e.PostCode)
    .IsModified = true;

Geçerli sınırlamalar

Karmaşık türler EF yığını genelinde önemli bir yatırımı temsil eden türlerdir. Bu sürümde her şeyi çalıştıramadık, ancak gelecek bir sürümde bazı boşlukları kapatmayı planlıyoruz. Bu sınırlamalardan herhangi birinin düzeltilmesi sizin için önemliyse uygun GitHub sorunlarında (👍) oy kullandığınızdan emin olun.

EF8'deki karmaşık tür sınırlamaları şunlardır:

  • Karmaşık tür koleksiyonlarını destekler. (Sorun #31237)
  • Karmaşık tür özelliklerinin null olmasını sağlar. (Sorun #31376)
  • Karmaşık tür özelliklerini JSON sütunlarıyla eşleyin. (Sorun #31252)
  • Karmaşık türler için oluşturucu ekleme. (Sorun #31621)
  • Karmaşık türler için tohum verileri desteği ekleyin. (Sorun #31254)
  • Cosmos sağlayıcısı için karmaşık tür özelliklerini eşleyin. (Sorun #31253)
  • Bellek içi veritabanı için karmaşık türler uygulayın. (Sorun #31464)

Temel koleksiyonlar

İlişkisel veritabanlarını kullanırken kalıcı bir soru, ilkel tür koleksiyonlarıyla ne yapmalıdır; diğer bir ifadeyle tamsayıların, tarih/saatlerin, dizelerin vb. listeleri veya dizileri. PostgreSQL kullanıyorsanız PostgreSQL'in yerleşik dizi türünü kullanarak bunları depolamak kolaydır. Diğer veritabanları için iki yaygın yaklaşım vardır:

  • İlkel tür değeri için bir sütun ve her değeri koleksiyonun sahibine bağlayan yabancı anahtar işlevi görmesi için başka bir sütun içeren bir tablo oluşturun.
  • İlkel koleksiyonu veritabanı tarafından işlenen bir sütun türüne seri hale getirme; örneğin, bir dizeye ve dizeden seri hale getirme.

İlk seçeneğin birçok durumda avantajları vardır. Bu bölümün sonunda bu seçeneğe hızlıca göz atacağız. Ancak, modeldeki verilerin doğal bir gösterimi değildir ve gerçekten sahip olduğunuz ilkel bir tür koleksiyonuysa, ikinci seçenek daha etkili olabilir.

Önizleme 4'den başlayarak EF8 artık serileştirme biçimi olarak JSON kullanarak ikinci seçenek için yerleşik destek içeriyor. Modern ilişkisel veritabanları JSON'ı sorgulamaya ve işlemeye yönelik yerleşik mekanizmalar içerdiğinden, JSON sütunu gerektiğinde gerçekten bu tabloyu oluşturma yükü olmadan etkili bir şekilde tablo olarak işlenebilir. Aynı mekanizmalar, JSON'un parametrelerde geçirilmesini ve ardından sorgulardaki tablo değerli parametrelere benzer şekilde kullanılmasını sağlar. Daha sonra bu konuda daha fazla bilgi edinebilirsiniz.

Temel koleksiyon özellikleri

EF Core, ilkel bir tür olan T herhangi IEnumerable<T> bir özelliği veritabanındaki bir JSON sütunuyla eşleyebilir. Bu, hem alıcı hem de ayarlayıcıya sahip genel özellikler için kurala göre yapılır. Örneğin, aşağıdaki varlık türündeki tüm özellikler kurala göre JSON sütunlarına eşlenir:

public class PrimitiveCollections
{
    public IEnumerable<int> Ints { get; set; }
    public ICollection<string> Strings { get; set; }
    public IList<DateOnly> Dates { get; set; }
    public uint[] UnsignedInts { get; set; }
    public List<bool> Booleans { get; set; }
    public List<Uri> Urls { get; set; }
}

Not

Bu bağlamda "ilkel tür" derken neyi kast ediyoruz? Temel olarak, gerekirse bir tür değer dönüştürme kullanarak veritabanı sağlayıcısının eşlemeyi bildiği bir şey. Örneğin, yukarıdaki varlık türünde , string, DateOnlyDateTimeve bool türleri intveritabanı sağlayıcısı tarafından dönüştürülmeden işlenir. SQL Server imzalanmamış int'ler veya URI'ler için yerel desteğe sahip değildir, Uri ancak uint bu türler için yerleşik değer dönüştürücüleri bulunduğundan hala ilkel türler olarak kabul edilir.

Varsayılan olarak, EF Core JSON'u tutmak için kısıtlanmamış bir Unicode dize sütun türü kullanır, çünkü bu büyük koleksiyonlarla veri kaybına karşı koruma sağlar. Ancak SQL Server gibi bazı veritabanı sistemlerinde dize için en fazla uzunluğun belirtilmesi performansı artırabilir. Bu, diğer sütun yapılandırmasıyla birlikte normal şekilde yapılabilir. Örneğin:

modelBuilder
    .Entity<PrimitiveCollections>()
    .Property(e => e.Booleans)
    .HasMaxLength(1024)
    .IsUnicode(false);

Veya eşleme özniteliklerini kullanarak:

[MaxLength(2500)]
[Unicode(false)]
public uint[] UnsignedInts { get; set; }

Varsayılan sütun yapılandırması, kural öncesi model yapılandırması kullanılarak belirli bir türün tüm özellikleri için kullanılabilir. Örneğin:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder
        .Properties<List<DateOnly>>()
        .AreUnicode(false)
        .HaveMaxLength(4000);
}

Temel koleksiyonları olan sorgular

İlkel tür koleksiyonlarından yararlanan bazı sorgulara göz atalım. Bunun için iki varlık türüne sahip basit bir modele ihtiyacımız olacak. birincisi bir İngiliz kamu evini veya "pub"ı temsil eder:

public class Pub
{
    public Pub(string name, string[] beers)
    {
        Name = name;
        Beers = beers;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public string[] Beers { get; set; }
    public List<DateOnly> DaysVisited { get; private set; } = new();
}

türü Pub iki temel koleksiyon içerir:

  • Beers barda bulunan bira markalarını temsil eden bir dizi dizedir.
  • DaysVisited pub'ın ziyaret edildiği tarihlerin listesidir.

İpucu

Gerçek bir uygulamada, bira için varlık türü oluşturmak ve biralar için bir tablo oluşturmak muhtemelen daha mantıklı olacaktır. Nasıl çalıştıklarını göstermek için burada ilkel bir koleksiyon gösteriyoruz. Ancak unutmayın, bir şeyi ilkel bir koleksiyon olarak modelleyebileceğiniz için mutlaka modellemeniz gerektiği anlamına gelmez.

İkinci varlık türü, İngiliz kırsalında bir köpek yürüyüşlerini temsil eder:

public class DogWalk
{
    public DogWalk(string name)
    {
        Name = name;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public Terrain Terrain { get; set; }
    public List<DateOnly> DaysVisited { get; private set; } = new();
    public Pub ClosestPub { get; set; } = null!;
}

public enum Terrain
{
    Forest,
    River,
    Hills,
    Village,
    Park,
    Beach,
}

gibi Pub, DogWalk ayrıca ziyaret edilen tarihlerin bir koleksiyonunu ve en yakın pub'ın bağlantısını içerir, çünkü, bilirsiniz, bazen köpeğin uzun bir yürüyüş sonrasında bir bira tabağına ihtiyacı vardır.

Bu modeli kullanarak, ilk olarak birkaç farklı araziden biriyle tüm yürüyüşleri bulmak için basit bir Contains sorgu yapacağız:

var terrains = new[] { Terrain.River, Terrain.Beach, Terrain.Park };
var walksWithTerrain = await context.Walks
    .Where(e => terrains.Contains(e.Terrain))
    .Select(e => e.Name)
    .ToListAsync();

Bu zaten EF Core'un geçerli sürümleri tarafından aranacak değerlerin satır içine alınarak çevrilmiştir. Örneğin, SQL Server kullanılırken:

SELECT [w].[Name]
FROM [Walks] AS [w]
WHERE [w].[Terrain] IN (1, 5, 4)

Ancak, bu strateji veritabanı sorgusunu önbelleğe alma ile iyi çalışmaz; Sorunla ilgili bir tartışma için .NET Blogu'nda EF8 Preview 4 Duyurusunun konusuna bakın.

Önemli

Burada değerlerin eklenmesi, SQL ekleme saldırısı olasılığı olmayacak şekilde gerçekleştirilir. Aşağıda açıklanan JSON kullanma değişikliği tamamen performansla ve güvenlikle ilgili değildir.

EF Core 8 için varsayılan değer artık arazi listesini JSON koleksiyonu içeren tek bir parametre olarak geçirmektir. Örneğin:

@__terrains_0='[1,5,4]'

Sorgu daha sonra SQL Server'da kullanır OpenJson :

SELECT [w].[Name]
FROM [Walks] AS [w]
WHERE EXISTS (
    SELECT 1
    FROM OpenJson(@__terrains_0) AS [t]
    WHERE CAST([t].[value] AS int) = [w].[Terrain])

Veya json_each SQLite'te:

SELECT "w"."Name"
FROM "Walks" AS "w"
WHERE EXISTS (
    SELECT 1
    FROM json_each(@__terrains_0) AS "t"
    WHERE "t"."value" = "w"."Terrain")

Not

OpenJson yalnızca SQL Server 2016 (uyumluluk düzeyi 130) ve sonraki sürümlerde kullanılabilir. uyumluluk düzeyini öğesinin bir parçası olarak yapılandırarak SQL Server'a daha eski bir sürüm kullandığınızı UseSqlServerbelirtebilirsiniz. Örneğin:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlServer(
            @"Data Source=(LocalDb)\MSSQLLocalDB;Database=AllTogetherNow",
            sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseCompatibilityLevel(120));

Şimdi farklı bir sorgu türü Contains deneyelim. Bu durumda, sütunda parametre koleksiyonunun değerini arayacağız. Örneğin, Heineken'i stoklayan herhangi bir pub:

var beer = "Heineken";
var pubsWithHeineken = await context.Pubs
    .Where(e => e.Beers.Contains(beer))
    .Select(e => e.Name)
    .ToListAsync();

EF7'deki Yenilikler'in mevcut belgelerinde JSON eşlemesi, sorguları ve güncelleştirmeleri hakkında ayrıntılı bilgiler sağlanır. Bu belgeler artık SQLite için de geçerlidir.

SELECT [p].[Name]
FROM [Pubs] AS [p]
WHERE EXISTS (
    SELECT 1
    FROM OpenJson([p].[Beers]) AS [b]
    WHERE [b].[value] = @__beer_0)

OpenJson artık her değerin geçirilen parametreyle eşleştirilebilmesi için JSON sütunundaki değerleri ayıklamak için kullanılır.

parametresinin kullanımını OpenJson sütunda ile OpenJson birleştirebiliriz. Örneğin, çeşitli gecikmelerden herhangi birini stoklayan pub'ları bulmak için:

var beers = new[] { "Carling", "Heineken", "Stella Artois", "Carlsberg" };
var pubsWithLager = await context.Pubs
    .Where(e => beers.Any(b => e.Beers.Contains(b)))
    .Select(e => e.Name)
    .ToListAsync();

Bu, SQL Server'da aşağıdakine çevrilir:

SELECT [p].[Name]
FROM [Pubs] AS [p]
WHERE EXISTS (
    SELECT 1
    FROM OpenJson(@__beers_0) AS [b]
    WHERE EXISTS (
        SELECT 1
        FROM OpenJson([p].[Beers]) AS [b0]
        WHERE [b0].[value] = [b].[value] OR ([b0].[value] IS NULL AND [b].[value] IS NULL)))

Buradaki @__beers_0 parametre değeridir ["Carling","Heineken","Stella Artois","Carlsberg"].

Şimdi bir tarih koleksiyonu içeren sütunu kullanan bir sorguya bakalım. Örneğin, bu yıl ziyaret edilen pub'ları bulmak için:

var thisYear = DateTime.Now.Year;
var pubsVisitedThisYear = await context.Pubs
    .Where(e => e.DaysVisited.Any(v => v.Year == thisYear))
    .Select(e => e.Name)
    .ToListAsync();

Bu, SQL Server'da aşağıdakine çevrilir:

SELECT [p].[Name]
FROM [Pubs] AS [p]
WHERE EXISTS (
    SELECT 1
    FROM OpenJson([p].[DaysVisited]) AS [d]
    WHERE DATEPART(year, CAST([d].[value] AS date)) = @__thisYear_0)

EF ilkel koleksiyonun tarih içerdiğini bildiği için sorgunun tarihe özgü işlevi DATEPART kullandığına dikkat edin. Öyle görünmeyebilir ama bu gerçekten çok önemli. EF koleksiyonda neler olduğunu bildiği için, yazılan değerleri parametreler, işlevler, diğer sütunlar vb. ile kullanmak için uygun SQL oluşturabilir.

Bu kez koleksiyondan ayıklanan tür ve proje değerlerine uygun şekilde sıralamak için tarih koleksiyonunu yeniden kullanalım. Örneğin, pub'ları ilk ziyaret edildikleri sırada ve her pub'ın ziyaret edildiği ilk ve son tarihle listeleyelim:

var pubsVisitedInOrder = await context.Pubs
    .Select(e => new
    {
        e.Name,
        FirstVisited = e.DaysVisited.OrderBy(v => v).First(),
        LastVisited = e.DaysVisited.OrderByDescending(v => v).First(),
    })
    .OrderBy(p => p.FirstVisited)
    .ToListAsync();

Bu, SQL Server'da aşağıdakine çevrilir:

SELECT [p].[Name], (
    SELECT TOP(1) CAST([d0].[value] AS date)
    FROM OpenJson([p].[DaysVisited]) AS [d0]
    ORDER BY CAST([d0].[value] AS date)) AS [FirstVisited], (
    SELECT TOP(1) CAST([d1].[value] AS date)
    FROM OpenJson([p].[DaysVisited]) AS [d1]
    ORDER BY CAST([d1].[value] AS date) DESC) AS [LastVisited]
FROM [Pubs] AS [p]
ORDER BY (
    SELECT TOP(1) CAST([d].[value] AS date)
    FROM OpenJson([p].[DaysVisited]) AS [d]
    ORDER BY CAST([d].[value] AS date))

Ve son olarak, köpeği yürüyüşe götürürken en yakın pub'ı ne sıklıkta ziyaret ediyoruz? Şimdi şunu öğrenelim:

var walksWithADrink = await context.Walks.Select(
    w => new
    {
        WalkName = w.Name,
        PubName = w.ClosestPub.Name,
        Count = w.DaysVisited.Count(v => w.ClosestPub.DaysVisited.Contains(v)),
        TotalCount = w.DaysVisited.Count
    }).ToListAsync();

Bu, SQL Server'da aşağıdakine çevrilir:

SELECT [w].[Name] AS [WalkName], [p].[Name] AS [PubName], (
    SELECT COUNT(*)
    FROM OpenJson([w].[DaysVisited]) AS [d]
    WHERE EXISTS (
        SELECT 1
        FROM OpenJson([p].[DaysVisited]) AS [d0]
        WHERE CAST([d0].[value] AS date) = CAST([d].[value] AS date) OR ([d0].[value] IS NULL AND [d].[value] IS NULL))) AS [Count], (
    SELECT COUNT(*)
    FROM OpenJson([w].[DaysVisited]) AS [d1]) AS [TotalCount]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]

Ve aşağıdaki verileri ortaya çıkar:

The Prince of Wales Feathers was visited 5 times in 8 "Ailsworth to Nene" walks.
The Prince of Wales Feathers was visited 6 times in 9 "Caster Hanglands" walks.
The Royal Oak was visited 6 times in 8 "Ferry Meadows" walks.
The White Swan was visited 7 times in 9 "Woodnewton" walks.
The Eltisley was visited 6 times in 8 "Eltisley" walks.
Farr Bay Inn was visited 7 times in 11 "Farr Beach" walks.
Farr Bay Inn was visited 7 times in 9 "Newlands" walks.

Görünüşe göre bira ve köpek yürüyüşü kazanan bir kombinasyon!

JSON belgelerindeki temel koleksiyonlar

Yukarıdaki tüm örneklerde ilkel koleksiyon sütunu JSON içerir. Ancak bu, sahip olunan varlık türünün EF7'de tanıtılan JSON belgesi içeren bir sütuna eşlenmesiyle aynı değildir. Peki ya bu JSON belgesinin kendisi ilkel bir koleksiyon içeriyorsa? Yukarıdaki tüm sorgular hala aynı şekilde çalışıyor! Örneğin, ziyaret edilen gün verilerini bir JSON belgesine eşlenmiş sahip olunan bir türe Visits taşıyacağımızı düşünün:

public class Pub
{
    public Pub(string name)
    {
        Name = name;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public BeerData Beers { get; set; } = null!;
    public Visits Visits { get; set; } = null!;
}

public class Visits
{
    public string? LocationTag { get; set; }
    public List<DateOnly> DaysVisited { get; set; } = null!;
}

İpucu

Burada gösterilen kod PrimitiveCollectionsInJsonSample.cs gelir.

Artık son sorgumuzun JSON belgesindeki verileri ayıklayan ve sorgular da dahil olmak üzere belgedeki temel koleksiyonlara veri ayıklayan bir çeşitlemesi çalıştırabiliriz:

var walksWithADrink = await context.Walks.Select(
    w => new
    {
        WalkName = w.Name,
        PubName = w.ClosestPub.Name,
        WalkLocationTag = w.Visits.LocationTag,
        PubLocationTag = w.ClosestPub.Visits.LocationTag,
        Count = w.Visits.DaysVisited.Count(v => w.ClosestPub.Visits.DaysVisited.Contains(v)),
        TotalCount = w.Visits.DaysVisited.Count
    }).ToListAsync();

Bu, SQL Server'da aşağıdakine çevrilir:

SELECT [w].[Name] AS [WalkName], [p].[Name] AS [PubName], JSON_VALUE([w].[Visits], '$.LocationTag') AS [WalkLocationTag], JSON_VALUE([p].[Visits], '$.LocationTag') AS [PubLocationTag], (
    SELECT COUNT(*)
    FROM OpenJson(JSON_VALUE([w].[Visits], '$.DaysVisited')) AS [d]
    WHERE EXISTS (
        SELECT 1
        FROM OpenJson(JSON_VALUE([p].[Visits], '$.DaysVisited')) AS [d0]
        WHERE CAST([d0].[value] AS date) = CAST([d].[value] AS date) OR ([d0].[value] IS NULL AND [d].[value] IS NULL))) AS [Count], (
    SELECT COUNT(*)
    FROM OpenJson(JSON_VALUE([w].[Visits], '$.DaysVisited')) AS [d1]) AS [TotalCount]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]

SQLite kullanırken de benzer bir sorguya:

SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", "w"."Visits" ->> 'LocationTag' AS "WalkLocationTag", "p"."Visits" ->> 'LocationTag' AS "PubLocationTag", (
    SELECT COUNT(*)
    FROM json_each("w"."Visits" ->> 'DaysVisited') AS "d"
    WHERE EXISTS (
        SELECT 1
        FROM json_each("p"."Visits" ->> 'DaysVisited') AS "d0"
        WHERE "d0"."value" = "d"."value")) AS "Count", json_array_length("w"."Visits" ->> 'DaysVisited') AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"

İpucu

SQLite EF Core'da artık işlecinin kullanıldığına ->> ve bunun sonucunda hem okunması daha kolay hem de genellikle daha yüksek performanslı sorgulara neden olduğuna dikkat edin.

İlkel koleksiyonları tabloya eşleme

Yukarıda ilkel koleksiyonlar için bir diğer seçeneğin de bunları farklı bir tabloyla eşlemek olduğunu belirtmiş olduk. Bunun için birinci sınıf desteği Sorun #25163 tarafından izlenir; sizin için önemliyse bu soruna oy verin. Bu uygulanana kadar en iyi yaklaşım ilkel için bir sarmalama türü oluşturmaktır. Örneğin, için Beerbir tür oluşturalım:

[Owned]
public class Beer
{
    public Beer(string name)
    {
        Name = name;
    }

    public string Name { get; private set; }
}

Türün yalnızca ilkel değeri kaydırdığını, birincil anahtara veya tanımlanmış yabancı anahtarlara sahip olmadığını fark edin. Bu tür daha sonra sınıfında kullanılabilir Pub :

public class Pub
{
    public Pub(string name)
    {
        Name = name;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public List<Beer> Beers { get; set; } = new();
    public List<DateOnly> DaysVisited { get; private set; } = new();
}

EF artık birincil anahtar ve yabancı anahtar sütunlarını tabloya Pubs yeniden sentezleyen bir Beer tablo oluşturur. Örneğin, SQL Server'da:

CREATE TABLE [Beer] (
    [PubId] int NOT NULL,
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Beer] PRIMARY KEY ([PubId], [Id]),
    CONSTRAINT [FK_Beer_Pubs_PubId] FOREIGN KEY ([PubId]) REFERENCES [Pubs] ([Id]) ON DELETE CASCADE

JSON sütun eşlemesine yönelik geliştirmeler

EF8, EF7'de sunulan JSON sütun eşleme desteğine yönelik iyileştirmeler içerir.

İpucu

Burada gösterilen kod JsonColumnsSample.cs gelir.

Öğe erişimini JSON dizilerine çevirme

EF8, sorguları yürütürken JSON dizilerinde dizin oluşturmayı destekler. Örneğin, aşağıdaki sorgu ilk iki güncelleştirmenin belirli bir tarihten önce yapılıp yapılmadığını denetler.

var cutoff = DateOnly.FromDateTime(DateTime.UtcNow - TimeSpan.FromDays(365));
var updatedPosts = await context.Posts
    .Where(
        p => p.Metadata!.Updates[0].UpdatedOn < cutoff
             && p.Metadata!.Updates[1].UpdatedOn < cutoff)
    .ToListAsync();

Bu, SQL Server kullanılırken aşağıdaki SQL'e çevrilir:

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) < @__cutoff_0
  AND CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) < @__cutoff_0

Not

Belirli bir gönderide güncelleştirme olmasa veya yalnızca tek bir güncelleştirme olsa bile bu sorgu başarılı olur. Böyle bir durumda, JSON_VALUE döndürür NULL ve koşul eşleşmez.

Bir dizideki öğeleri nihai sonuçlara yansıtmak için JSON dizilerine dizin oluşturma da kullanılabilir. Örneğin, aşağıdaki sorgu her gönderinin UpdatedOn ilk ve ikinci güncelleştirmelerinin tarihini oluşturur.

var postsAndRecentUpdatesNullable = await context.Posts
    .Select(p => new
    {
        p.Title,
        LatestUpdate = (DateOnly?)p.Metadata!.Updates[0].UpdatedOn,
        SecondLatestUpdate = (DateOnly?)p.Metadata.Updates[1].UpdatedOn
    })
    .ToListAsync();

Bu, SQL Server kullanılırken aşağıdaki SQL'e çevrilir:

SELECT [p].[Title],
       CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) AS [LatestUpdate],
       CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) AS [SecondLatestUpdate]
FROM [Posts] AS [p]

Yukarıda belirtildiği gibi, JSON_VALUE dizinin öğesi yoksa null döndürür. Bu, sorguda yansıtılan değeri null atanabilir DateOnlybir değerine dönüştürerek işlenir. Değeri atamanın bir alternatifi, sorgu sonuçlarını hiçbir zaman null döndürmeyecek şekilde JSON_VALUE filtrelemektir. Örneğin:

var postsAndRecentUpdates = await context.Posts
    .Where(p => p.Metadata!.Updates[0].UpdatedOn != null
                && p.Metadata!.Updates[1].UpdatedOn != null)
    .Select(p => new
    {
        p.Title,
        LatestUpdate = p.Metadata!.Updates[0].UpdatedOn,
        SecondLatestUpdate = p.Metadata.Updates[1].UpdatedOn
    })
    .ToListAsync();

Bu, SQL Server kullanılırken aşağıdaki SQL'e çevrilir:

SELECT [p].[Title],
       CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) AS [LatestUpdate],
       CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) AS [SecondLatestUpdate]
FROM [Posts] AS [p]
      WHERE (CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) IS NOT NULL)
        AND (CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) IS NOT NULL)

Sorguları eklenmiş koleksiyonlara çevirme

EF8, JSON belgesine katıştırılmış hem ilkel (yukarıda açıklanmış) hem de ilkel olmayan türlerden oluşan koleksiyonlara yönelik sorguları destekler. Örneğin, aşağıdaki sorgu, arama terimlerinin rastgele bir listesini içeren tüm gönderileri döndürür:

var searchTerms = new[] { "Search #2", "Search #3", "Search #5", "Search #8", "Search #13", "Search #21", "Search #34" };

var postsWithSearchTerms = await context.Posts
    .Where(post => post.Metadata!.TopSearches.Any(s => searchTerms.Contains(s.Term)))
    .ToListAsync();

Bu, SQL Server kullanılırken aşağıdaki SQL'e çevrilir:

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE EXISTS (
    SELECT 1
    FROM OPENJSON([p].[Metadata], '$.TopSearches') WITH (
        [Count] int '$.Count',
        [Term] nvarchar(max) '$.Term'
    ) AS [t]
    WHERE EXISTS (
        SELECT 1
        FROM OPENJSON(@__searchTerms_0) WITH ([value] nvarchar(max) '$') AS [s]
        WHERE [s].[value] = [t].[Term]))

SQLite için JSON Sütunları

EF7, Azure SQL/SQL Server kullanırken JSON sütunlarına eşleme desteği sunar. EF8 bu desteği SQLite veritabanlarına genişletir. SQL Server desteğine gelince, buna şunlar dahildir:

  • .NET türlerinden oluşturulan toplamaların SQLite sütunlarında depolanan JSON belgeleriyle eşlenmesini
  • Belgelerin öğelerine göre filtreleme ve sıralama gibi JSON sütunlarına sorgular
  • JSON belgesinin öğelerini sonuçlara yansıtan sorgular
  • JSON belgelerine yapılan değişiklikleri güncelleştirme ve kaydetme

EF7'deki Yenilikler'in mevcut belgelerinde JSON eşlemesi, sorguları ve güncelleştirmeleri hakkında ayrıntılı bilgiler sağlanır. Bu belgeler artık SQLite için de geçerlidir.

İpucu

EF7 belgelerinde gösterilen kod SQLite üzerinde çalışacak şekilde güncelleştirildi JsonColumnsSample.cs'de bulunabilir.

JSON sütunlarına sorgular

SQLite'te JSON sütunlarına yapılan json_extract sorgular işlevini kullanır. Örneğin, yukarıda başvuruda bulunan belgelerden "Chigley'deki yazarlar" sorgusu:

var authorsInChigley = await context.Authors
    .Where(author => author.Contact.Address.City == "Chigley")
    .ToListAsync();

SQLite kullanılırken aşağıdaki SQL'e çevrilir:

SELECT "a"."Id", "a"."Name", "a"."Contact"
FROM "Authors" AS "a"
WHERE json_extract("a"."Contact", '$.Address.City') = 'Chigley'

JSON sütunlarını güncelleştirme

Güncelleştirmeler için EF, SQLite üzerindeki işlevini kullanır json_set . Örneğin, bir belgedeki tek bir özelliği güncelleştirirken:

var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));

arthur.Contact.Address.Country = "United Kingdom";

await context.SaveChangesAsync();

EF aşağıdaki parametreleri oluşturur:

info: 3/10/2023 10:51:33.127 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='["United Kingdom"]' (Nullable = false) (Size = 18), @p1='4'], CommandType='Text', CommandTimeout='30']

SQLite üzerinde işlevini kullanan json_set :

UPDATE "Authors" SET "Contact" = json_set("Contact", '$.Address.Country', json_extract(@p0, '$[0]'))
WHERE "Id" = @p1
RETURNING 1;

.NET ve EF Core'da HierarchyId

Azure SQL ve SQL Server, hiyerarşik verileri depolamak için kullanılan adlı hierarchyid özel bir veri türüne sahiptir. Bu durumda, "hiyerarşik veriler", temelde her öğenin bir üst ve/veya alt öğeye sahip olabileceği bir ağaç yapısı oluşturan veriler anlamına gelir. Bu tür verilere örnek olarak şunlar verilebilir:

  • Kuruluş yapısı
  • Dosya sistemi
  • Projedeki görev kümesi
  • Dil terimlerinin taksonomisi
  • Web sayfaları arasındaki bağlantıların grafiği

Veritabanı daha sonra hiyerarşik yapısını kullanarak bu verilere karşı sorgu çalıştırabilir. Örneğin, bir sorgu belirli öğelerin üst öğelerini ve bağımlılarını bulabilir veya hiyerarşide belirli bir derinlikteki tüm öğeleri bulabilir.

.NET ve EF Core desteği

SQL Server hierarchyid türü için resmi destek yalnızca yakın zamanda modern .NET platformlarına (yani ".NET Core") sunulmuştur. Bu destek, alt düzey SQL Server'a özgü türleri getiren Microsoft.SqlServer.Types NuGet paketi biçimindedir. Bu durumda, alt düzey türü olarak adlandırılır SqlHierarchyId.

Sonraki düzeyde, varlık türlerinde kullanılması amaçlanan daha üst düzey HierarchyId bir tür içeren yeni bir Microsoft.EntityFrameworkCore.SqlServer.Abstractions paketi kullanıma sunulmuştur.

İpucu

Türü HierarchyId , .NET'in normlarına göre daha SqlHierarchyIdidiyomatiktir ve bunun yerine .NET Framework türlerinin SQL Server veritabanı altyapısında barındırıldığı şekilde modellenir. HierarchyId EF Core ile çalışacak şekilde tasarlanmıştır, ancak diğer uygulamalarda EF Core dışında da kullanılabilir. Paket Microsoft.EntityFrameworkCore.SqlServer.Abstractions başka hiçbir pakete başvurmaz ve dağıtılan uygulama boyutu ve bağımlılıkları üzerinde en az etkiye sahiptir.

HierarchyId sorgular ve güncelleştirmeler gibi EF Core işlevleri için kullanımı Için Microsoft.EntityFrameworkCore.SqlServer.HierarchyId paketi gerekir. Bu paket ve öğesini geçişli bağımlılıklar olarak getirir Microsoft.EntityFrameworkCore.SqlServer.AbstractionsMicrosoft.SqlServer.Types ve genellikle gereken tek paket de budur. Paket yüklendikten sonra, uygulamasının HierarchyId çağrısının bir parçası olarak çağrısı UseHierarchyIdUseSqlServeryapılarak kullanımı etkinleştirilir. Örneğin:

options.UseSqlServer(
    connectionString,
    x => x.UseHierarchyId());

Not

EF Core'da için resmi olmayan destekhierarchyid, EntityFrameworkCore.SqlServer.HierarchyId paketi aracılığıyla yıllardır kullanılabilir. Bu paket, topluluk ve EF ekibi arasında bir işbirliği olarak sürdürülmüştür. Artık .NET'te için hierarchyid resmi destek olduğuna göre, bu topluluk paketindeki kod, özgün katkıda bulunanların izniyle, burada açıklanan resmi paketin temelini oluşturur. Yıllar içinde @aljones, @cutig3r, @huan086, @kmataru, @mehdihaghshenas ve @vyrotek dahil olmak üzere tüm bu kişilere çok teşekkür ederiz

Modelleme hiyerarşileri

Türü HierarchyId bir varlık türünün özellikleri için kullanılabilir. Örneğin, bazı kurgusal yarımlamaların babalık aile ağacını modellemek istediğimizi varsayalım. için Halflingvarlık türünde, aile ağacındaki her yarıyı bulmak için bir HierarchyId özellik kullanılabilir.

public class Halfling
{
    public Halfling(HierarchyId pathFromPatriarch, string name, int? yearOfBirth = null)
    {
        PathFromPatriarch = pathFromPatriarch;
        Name = name;
        YearOfBirth = yearOfBirth;
    }

    public int Id { get; private set; }
    public HierarchyId PathFromPatriarch { get; set; }
    public string Name { get; set; }
    public int? YearOfBirth { get; set; }
}

İpucu

Burada ve aşağıdaki örneklerde gösterilen kod HierarchyIdSample.cs...

İpucu

İsterseniz, HierarchyId anahtar özellik türü olarak kullanmak için uygundur.

Bu durumda, aile ağacının kökü ailenin patriğiyle birlikte oluşturulur. Her yarımlama, kendi özelliği kullanılarak PathFromPatriarch ağaçta patrikten izlenebilir. SQL Server bu yollar için kompakt bir ikili biçim kullanır, ancak kodla çalışırken insan tarafından okunabilen bir dize gösterimine ve bu gösterimden ayrıştırmak yaygın bir yöntemdir. Bu gösterimde, her düzeydeki konum bir / karakterle ayrılır. Örneğin, aşağıdaki diyagramda yer alan aile ağacını göz önünde bulundurun:

Yarım aile ağacı

Bu ağaçta:

  • Balbo, tarafından /temsil edilen ağacın kökündedir.
  • Balbo'nun , , /4//2//3/, ve /5/ile /1/temsil edilen beş çocuğu vardır.
  • Balbo'nun ilk çocuğu Mungo'nun da , , /1/3//1/2/, /1/4/ve /1/5/ile /1/1/temsil edilen beş çocuğu vardır. Balbo (/1/) için öğesinin HierarchyId tüm çocukları için ön ek olduğuna dikkat edin.
  • Benzer şekilde Balbo'nun üçüncü çocuğu Ponto'nun ve /3/2/ile /3/1/ temsil edilen iki çocuğu vardır. Yine bu çocukların her biri, olarak temsil edilen HierarchyId Ponto için ön ekini /3/alır.
  • Ve ağacın aşağısı...

Aşağıdaki kod, EF Core kullanarak bu aile ağacını bir veritabanına ekler:

await AddRangeAsync(
    new Halfling(HierarchyId.Parse("/"), "Balbo", 1167),
    new Halfling(HierarchyId.Parse("/1/"), "Mungo", 1207),
    new Halfling(HierarchyId.Parse("/2/"), "Pansy", 1212),
    new Halfling(HierarchyId.Parse("/3/"), "Ponto", 1216),
    new Halfling(HierarchyId.Parse("/4/"), "Largo", 1220),
    new Halfling(HierarchyId.Parse("/5/"), "Lily", 1222),
    new Halfling(HierarchyId.Parse("/1/1/"), "Bungo", 1246),
    new Halfling(HierarchyId.Parse("/1/2/"), "Belba", 1256),
    new Halfling(HierarchyId.Parse("/1/3/"), "Longo", 1260),
    new Halfling(HierarchyId.Parse("/1/4/"), "Linda", 1262),
    new Halfling(HierarchyId.Parse("/1/5/"), "Bingo", 1264),
    new Halfling(HierarchyId.Parse("/3/1/"), "Rosa", 1256),
    new Halfling(HierarchyId.Parse("/3/2/"), "Polo"),
    new Halfling(HierarchyId.Parse("/4/1/"), "Fosco", 1264),
    new Halfling(HierarchyId.Parse("/1/1/1/"), "Bilbo", 1290),
    new Halfling(HierarchyId.Parse("/1/3/1/"), "Otho", 1310),
    new Halfling(HierarchyId.Parse("/1/5/1/"), "Falco", 1303),
    new Halfling(HierarchyId.Parse("/3/2/1/"), "Posco", 1302),
    new Halfling(HierarchyId.Parse("/3/2/2/"), "Prisca", 1306),
    new Halfling(HierarchyId.Parse("/4/1/1/"), "Dora", 1302),
    new Halfling(HierarchyId.Parse("/4/1/2/"), "Drogo", 1308),
    new Halfling(HierarchyId.Parse("/4/1/3/"), "Dudo", 1311),
    new Halfling(HierarchyId.Parse("/1/3/1/1/"), "Lotho", 1310),
    new Halfling(HierarchyId.Parse("/1/5/1/1/"), "Poppy", 1344),
    new Halfling(HierarchyId.Parse("/3/2/1/1/"), "Ponto", 1346),
    new Halfling(HierarchyId.Parse("/3/2/1/2/"), "Porto", 1348),
    new Halfling(HierarchyId.Parse("/3/2/1/3/"), "Peony", 1350),
    new Halfling(HierarchyId.Parse("/4/1/2/1/"), "Frodo", 1368),
    new Halfling(HierarchyId.Parse("/4/1/3/1/"), "Daisy", 1350),
    new Halfling(HierarchyId.Parse("/3/2/1/1/1/"), "Angelica", 1381));

await SaveChangesAsync();

İpucu

Gerekirse, ondalık değerler mevcut iki düğüm arasında yeni düğümler oluşturmak için kullanılabilir. Örneğin, /3/2.5/2/ ile /3/3/2/arasında /3/2/2/ gider.

Hiyerarşileri sorgulama

HierarchyId LINQ sorgularında kullanılabilecek çeşitli yöntemleri kullanıma sunar.

Metot Açıklama
GetAncestor(int n) Hiyerarşik ağaçta düğüm n düzeylerini alır.
GetDescendant(HierarchyId? child1, HierarchyId? child2) değerinden büyük child1 ve child2küçük bir alt düğümün değerini alır.
GetLevel() Hiyerarşik ağaçta bu düğümün düzeyini alır.
GetReparentedValue(HierarchyId? oldRoot, HierarchyId? newRoot) Bu konumdan buna eşit oldRoot bir yola newRoot sahip yeni düğümün konumunu temsil eden bir değer alır ve bunu yeni konuma etkili bir şekilde taşır.
IsDescendantOf(HierarchyId? parent) Bu düğümün alt parentöğesinin olup olmadığını belirten bir değer alır.

Ayrıca , , !=<, <=ve >>= işleçleri ==kullanılabilir.

Aşağıda, LINQ sorgularında bu yöntemleri kullanma örnekleri verilmiştir.

Ağaçta belirli bir düzeydeki varlıkları alma

Aşağıdaki sorgu, aile ağacında belirli bir düzeydeki tüm yarımları döndürmek için kullanır GetLevel :

var generation = await context.Halflings.Where(halfling => halfling.PathFromPatriarch.GetLevel() == level).ToListAsync();

Bu, aşağıdaki SQL'e çevrilir:

SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].GetLevel() = @__level_0

Bunu bir döngüde çalıştırarak her nesil için halfling'leri alacağız:

Generation 0: Balbo
Generation 1: Mungo, Pansy, Ponto, Largo, Lily
Generation 2: Bungo, Belba, Longo, Linda, Bingo, Rosa, Polo, Fosco
Generation 3: Bilbo, Otho, Falco, Posco, Prisca, Dora, Drogo, Dudo
Generation 4: Lotho, Poppy, Ponto, Porto, Peony, Frodo, Daisy
Generation 5: Angelica

Bir varlığın doğrudan atası alma

Aşağıdaki sorgu, bu yarımlamanın adı göz önünde bulundurularak bir yarılanmanın doğrudan atası bulmak için kullanır GetAncestor :

async Task<Halfling?> FindDirectAncestor(string name)
    => await context.Halflings
        .SingleOrDefaultAsync(
            ancestor => ancestor.PathFromPatriarch == context.Halflings
                .Single(descendent => descendent.Name == name).PathFromPatriarch
                .GetAncestor(1));

Bu, aşağıdaki SQL'e çevrilir:

SELECT TOP(2) [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch] = (
    SELECT TOP(1) [h0].[PathFromPatriarch]
    FROM [Halflings] AS [h0]
    WHERE [h0].[Name] = @__name_0).GetAncestor(1)

"Bilbo" yarımlama için bu sorgu çalıştırılırken "Bungo" döndürülüyor.

Bir varlığın doğrudan alt öğelerini alma

Aşağıdaki sorgu da kullanır GetAncestor, ancak bu kez bu yarımlamanın adını göz önünde bulundurarak, bir yarılanmanın doğrudan alt bileşenlerini bulmak için:

IQueryable<Halfling> FindDirectDescendents(string name)
    => context.Halflings.Where(
        descendent => descendent.PathFromPatriarch.GetAncestor(1) == context.Halflings
            .Single(ancestor => ancestor.Name == name).PathFromPatriarch);

Bu, aşağıdaki SQL'e çevrilir:

SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].GetAncestor(1) = (
    SELECT TOP(1) [h0].[PathFromPatriarch]
    FROM [Halflings] AS [h0]
    WHERE [h0].[Name] = @__name_0)

"Mungo" yarımlama için bu sorgu çalıştırılırken "Bungo", "Belba", "Longo" ve "Linda" döndürülüyor.

Bir varlığın tüm atalarını alma

GetAncestor tek bir düzeyi veya gerçekten de belirli sayıda düzeyi aramak için kullanışlıdır. Öte yandan, IsDescendantOf tüm ataları veya bağımlıları bulmak için yararlıdır. Örneğin, aşağıdaki sorgu, bu yarımlamanın adı göz önünde bulundurularak bir yarımlamanın tüm atalarını bulmak için kullanır IsDescendantOf :

IQueryable<Halfling> FindAllAncestors(string name)
    => context.Halflings.Where(
            ancestor => context.Halflings
                .Single(
                    descendent =>
                        descendent.Name == name
                        && ancestor.Id != descendent.Id)
                .PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch))
        .OrderByDescending(ancestor => ancestor.PathFromPatriarch.GetLevel());

Önemli

IsDescendantOf kendisi için true değerini döndürür. Bu nedenle yukarıdaki sorguda filtrelenmiştir.

Bu, aşağıdaki SQL'e çevrilir:

SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE (
    SELECT TOP(1) [h0].[PathFromPatriarch]
    FROM [Halflings] AS [h0]
    WHERE [h0].[Name] = @__name_0 AND [h].[Id] <> [h0].[Id]).IsDescendantOf([h].[PathFromPatriarch]) = CAST(1 AS bit)
ORDER BY [h].[PathFromPatriarch].GetLevel() DESC

"Bilbo" yarımlama için bu sorgu çalıştırılırken "Bungo", "Mungo" ve "Balbo" döndürülüyor.

Bir varlığın tüm alt öğelerini alma

Aşağıdaki sorguda da kullanılır IsDescendantOf, ancak bu kez bu yarımlamanın adı göz önüne alındığında, bir yarımlamanın tüm alt öğelerine kullanılır:

IQueryable<Halfling> FindAllDescendents(string name)
    => context.Halflings.Where(
            descendent => descendent.PathFromPatriarch.IsDescendantOf(
                context.Halflings
                    .Single(
                        ancestor =>
                            ancestor.Name == name
                            && descendent.Id != ancestor.Id)
                    .PathFromPatriarch))
        .OrderBy(descendent => descendent.PathFromPatriarch.GetLevel());

Bu, aşağıdaki SQL'e çevrilir:

SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].IsDescendantOf((
    SELECT TOP(1) [h0].[PathFromPatriarch]
    FROM [Halflings] AS [h0]
    WHERE [h0].[Name] = @__name_0 AND [h].[Id] <> [h0].[Id])) = CAST(1 AS bit)
ORDER BY [h].[PathFromPatriarch].GetLevel()

"Mungo" yarımlama için bu sorgu çalıştırılırken "Bungo", "Belba", "Longo", "Linda", "Bingo", "Bilbo", "Otho", "Falco", "Lotho" ve "Poppy" döndürülüyor.

Ortak bir ata bulma

Bu aile ağacı hakkında sorulan en yaygın sorulardan biri, "Frodo ve Bilbo'nun ortak atası kim?" sorusudur. Böyle bir sorgu yazmak için kullanabiliriz IsDescendantOf :

async Task<Halfling?> FindCommonAncestor(Halfling first, Halfling second)
    => await context.Halflings
        .Where(
            ancestor => first.PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch)
                        && second.PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch))
        .OrderByDescending(ancestor => ancestor.PathFromPatriarch.GetLevel())
        .FirstOrDefaultAsync();

Bu, aşağıdaki SQL'e çevrilir:

SELECT TOP(1) [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
FROM [Halflings] AS [h]
WHERE @__first_PathFromPatriarch_0.IsDescendantOf([h].[PathFromPatriarch]) = CAST(1 AS bit)
  AND @__second_PathFromPatriarch_1.IsDescendantOf([h].[PathFromPatriarch]) = CAST(1 AS bit)
ORDER BY [h].[PathFromPatriarch].GetLevel() DESC

Bu sorguyu "Bilbo" ve "Frodo" ile çalıştırmak, ortak atalarının "Balbo" olduğunu bildirir.

Hiyerarşileri güncelleştirme

Sütunları güncelleştirmek için normal değişiklik izleme ve SaveChanges mekanizmaları hierarchyid kullanılabilir.

Alt hiyerarşiyi yeniden üst öğe oluşturma

Örneğin, DNA testinde Longo'nun aslında Mungo'nun değil aslında Ponto'nun oğlu olduğunu ortaya çıkardığında hepimizin SR 1752 (yani "LongoGate") skandalını hatırlayacağından eminim! Bu skandaldan bir tanesi, aile ağacının yeniden yazılması gerektiğiydi. Özellikle, Longo ve tüm soyundan gelenlerin Mungo'dan Ponto'ya yeniden ebeveyn olması gerekiyordu. GetReparentedValue bunu yapmak için kullanılabilir. Örneğin, ilk "Longo" ve tüm alt öğeleri sorgulanır:

var longoAndDescendents = await context.Halflings.Where(
        descendent => descendent.PathFromPatriarch.IsDescendantOf(
            context.Halflings.Single(ancestor => ancestor.Name == "Longo").PathFromPatriarch))
    .ToListAsync();

Ardından GetReparentedValue Longo ve her alt öğesini güncelleştirmek HierarchyId için kullanılır ve ardından öğesine bir çağrı SaveChangesAsyncyapılır:

foreach (var descendent in longoAndDescendents)
{
    descendent.PathFromPatriarch
        = descendent.PathFromPatriarch.GetReparentedValue(
            mungo.PathFromPatriarch, ponto.PathFromPatriarch)!;
}

await context.SaveChangesAsync();

Bu, aşağıdaki veritabanı güncelleştirmesine neden olur:

SET NOCOUNT ON;
UPDATE [Halflings] SET [PathFromPatriarch] = @p0
OUTPUT 1
WHERE [Id] = @p1;
UPDATE [Halflings] SET [PathFromPatriarch] = @p2
OUTPUT 1
WHERE [Id] = @p3;
UPDATE [Halflings] SET [PathFromPatriarch] = @p4
OUTPUT 1
WHERE [Id] = @p5;

Şu parametreleri kullanarak:

 @p1='9',
 @p0='0x7BC0' (Nullable = false) (Size = 2) (DbType = Object),
 @p3='16',
 @p2='0x7BD6' (Nullable = false) (Size = 2) (DbType = Object),
 @p5='23',
 @p4='0x7BD6B0' (Nullable = false) (Size = 3) (DbType = Object)

Not

Özelliklerin parametre değerleri HierarchyId veritabanına kompakt, ikili biçimlerinde gönderilir.

Güncelleştirmeden sonra " Mungo" alt öğeleri sorgulanması "Bungo" döndürür, "Belba", "Linda", "Bingo", "Bilbo", "Falco" ve "Poppy", "Ponto" öğesinin alt öğelerini sorgularken "Longo", "Rosa", "Polo", "Otho", "Posco", "Prisca", "Lotho", "Ponto", "Porto", "Peony" ve "Angelica" döndürür.

Eşlenmemiş türler için ham SQL sorguları

EF7, skaler türler döndüren ham SQL sorgularını kullanıma sunar. Bu, EF8'de, EF modeline bu tür dahil edilmeden, herhangi bir mappable CLR türü döndüren ham SQL sorgularını içerecek şekilde geliştirilmiştir.

İpucu

Burada gösterilen kod RawSqlSample.cs gelir.

Eşlenmemiş türleri kullanan sorgular veya SqlQueryRawkullanılarak SqlQuery yürütülür. İlki, sorguyu parametreleştirmek için dize ilişkilendirmesini kullanır ve bu da sabit olmayan tüm değerlerin parametreleştirilmesine yardımcı olur. Örneğin, aşağıdaki veritabanı tablosunu göz önünde bulundurun:

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Content] nvarchar(max) NOT NULL,
    [PublishedOn] date NOT NULL,
    [BlogId] int NOT NULL,
);

SqlQuery bu tabloyu sorgulamak ve tablodaki sütunlara karşılık gelen özelliklere sahip bir BlogPost türün örneklerini döndürmek için kullanılabilir:

Örneğin:

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public DateOnly PublishedOn { get; set; }
    public int BlogId { get; set; }
}

Örneğin:

var start = new DateOnly(2022, 1, 1);
var end = new DateOnly(2023, 1, 1);
var postsIn2022 =
    await context.Database
        .SqlQuery<BlogPost>($"SELECT * FROM Posts as p WHERE p.PublishedOn >= {start} AND p.PublishedOn < {end}")
        .ToListAsync();

Bu sorgu parametreleştirilir ve şu şekilde yürütülür:

SELECT * FROM Posts as p WHERE p.PublishedOn >= @p0 AND p.PublishedOn < @p1

Sorgu sonuçları için kullanılan tür, parametreli oluşturucular ve eşleme öznitelikleri gibi EF Core tarafından desteklenen ortak eşleme yapılarını içerebilir. Örneğin:

public class BlogPost
{
    public BlogPost(string blogTitle, string content, DateOnly publishedOn)
    {
        BlogTitle = blogTitle;
        Content = content;
        PublishedOn = publishedOn;
    }

    public int Id { get; private set; }

    [Column("Title")]
    public string BlogTitle { get; set; }

    public string Content { get; set; }
    public DateOnly PublishedOn { get; set; }
    public int BlogId { get; set; }
}

Not

Bu şekilde kullanılan türlerde tanımlı anahtarlar yoktur ve diğer türlere ilişki olamaz. İlişkili türler modelde eşlenmelidir.

Kullanılan türün sonuç kümesindeki her değer için bir özelliği olmalıdır, ancak veritabanındaki herhangi bir tabloyla eşleşmesi gerekmez. Örneğin, aşağıdaki tür her gönderi için yalnızca bir bilgi alt kümesini temsil eder ve tablodan gelen Blogs blog adını içerir:

public class PostSummary
{
    public string BlogName { get; set; } = null!;
    public string PostTitle { get; set; } = null!;
    public DateOnly? PublishedOn { get; set; }
}

Ve daha önce olduğu gibi kullanılarak SqlQuery sorgulanabilir:


var cutoffDate = new DateOnly(2022, 1, 1);
var summaries =
    await context.Database.SqlQuery<PostSummary>(
            @$"SELECT b.Name AS BlogName, p.Title AS PostTitle, p.PublishedOn
               FROM Posts AS p
               INNER JOIN Blogs AS b ON p.BlogId = b.Id
               WHERE p.PublishedOn >= {cutoffDate}")
        .ToListAsync();

Bunun güzel özelliklerinden SqlQuery biri, LINQ kullanılarak oluşturulabilen bir IQueryable döndürmesidir. Örneğin, yukarıdaki sorguya bir 'Where' yan tümcesi eklenebilir:

var summariesIn2022 =
    await context.Database.SqlQuery<PostSummary>(
            @$"SELECT b.Name AS BlogName, p.Title AS PostTitle, p.PublishedOn
               FROM Posts AS p
               INNER JOIN Blogs AS b ON p.BlogId = b.Id")
        .Where(p => p.PublishedOn >= cutoffDate && p.PublishedOn < end)
        .ToListAsync();

Bu işlem şu şekilde yürütülür:

SELECT [n].[BlogName], [n].[PostTitle], [n].[PublishedOn]
FROM (
         SELECT b.Name AS BlogName, p.Title AS PostTitle, p.PublishedOn
         FROM Posts AS p
                  INNER JOIN Blogs AS b ON p.BlogId = b.Id
     ) AS [n]
WHERE [n].[PublishedOn] >= @__cutoffDate_1 AND [n].[PublishedOn] < @__end_2

Bu noktada, yukarıdakilerin tümünün herhangi bir SQL yazmaya gerek kalmadan TAMAMEN LINQ'te yapilabileceğini unutmamak gerekir. Bu, gibi PostSummaryeşlenmemiş bir türün örneklerini döndürmeyi içerir. Örneğin, yukarıdaki sorgu LINQ'te şu şekilde yazılabilir:

var summaries =
    await context.Posts.Select(
            p => new PostSummary
            {
                BlogName = p.Blog.Name,
                PostTitle = p.Title,
                PublishedOn = p.PublishedOn,
            })
        .Where(p => p.PublishedOn >= start && p.PublishedOn < end)
        .ToListAsync();

Bu da çok daha temiz BIR SQL'e dönüşür:

SELECT [b].[Name] AS [BlogName], [p].[Title] AS [PostTitle], [p].[PublishedOn]
FROM [Posts] AS [p]
INNER JOIN [Blogs] AS [b] ON [p].[BlogId] = [b].[Id]
WHERE [p].[PublishedOn] >= @__start_0 AND [p].[PublishedOn] < @__end_1

İpucu

ÖNCEKI örnekte sorgunun tam semantiği EF tarafından kullanılabildiğinden, EF sorgunun tamamından sorumlu olduğunda, kullanıcı tarafından sağlanan SQL üzerinden oluşturmadan daha temiz BIR SQL oluşturabilir.

Şimdiye kadar tüm sorgular doğrudan tablolarda yürütüldü. SqlQuery , EF modelinde görünüm türü eşlemeden görünümden sonuç döndürmek için de kullanılabilir. Örneğin:

var summariesFromView =
    await context.Database.SqlQuery<PostSummary>(
            @$"SELECT * FROM PostAndBlogSummariesView")
        .Where(p => p.PublishedOn >= cutoffDate && p.PublishedOn < end)
        .ToListAsync();

Benzer şekilde, SqlQuery işlevin sonuçları için de kullanılabilir:

var summariesFromFunc =
    await context.Database.SqlQuery<PostSummary>(
            @$"SELECT * FROM GetPostsPublishedAfter({cutoffDate})")
        .Where(p => p.PublishedOn < end)
        .ToListAsync();

Döndürülen IQueryable , tablo sorgusunun sonucu gibi bir görünümün veya işlevin sonucu olduğunda oluşturulabilir. Saklı yordamlar kullanılarak SqlQueryda yürütülebilir, ancak veritabanlarının çoğu bunlar üzerinde oluşturma işlemini desteklemez. Örneğin:

var summariesFromStoredProc =
    await context.Database.SqlQuery<PostSummary>(
            @$"exec GetRecentPostSummariesProc")
        .ToListAsync();

Yavaş yükleme iyileştirmeleri

İzleme olmayan sorgular için yavaş yükleme

EF8, tarafından izlenmeyen varlıklardaki gezintilerin yavaş yüklenmesi için DbContextdestek ekler. Bu, izleme olmayan sorgunun takip edilmediği sorgu tarafından döndürülen varlıklardaki gezintilerin yavaş yüklenmesiyle takip edilebileceği anlamına gelir.

İpucu

Aşağıda gösterilen gecikmeli yükleme örneklerinin kodu LazyLoadingSample.cs...

Örneğin, bloglar için izleme yok sorgusunu göz önünde bulundurun:

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

Örneğin, yavaş yükleme proxy'leri kullanılarak yavaş yükleme için yapılandırılmışsa Blog.Posts , erişim Posts veritabanından yüklenmesine neden olur:

Console.WriteLine();
Console.Write("Choose a blog: ");
if (int.TryParse(ReadLine(), out var blogId))
{
    Console.WriteLine("Posts:");
    foreach (var post in blogs[blogId - 1].Posts)
    {
        Console.WriteLine($"  {post.Title}");
    }
}

EF8 ayrıca bağlam tarafından izlenmeyen varlıklar için belirli bir gezintinin yüklenip yüklenmediğini de bildirir. Örneğin:

foreach (var blog in blogs)
{
    if (context.Entry(blog).Collection(e => e.Posts).IsLoaded)
    {
        Console.WriteLine($" Posts for blog '{blog.Name}' are loaded.");
    }
}

Bu şekilde yavaş yükleme kullanırken dikkat edilmesi gereken birkaç önemli nokta vardır:

  • Gecikmeli yükleme yalnızca varlığı sorgulamak DbContext için kullanılan atılana kadar başarılı olur.
  • Bu şekilde sorgulanan varlıklar, tarafından izlenmeseler bile , başvurularını DbContextkorur. Varlık örneklerinin ömrü uzun olacaksa bellek sızıntılarını önlemek için dikkatli olunmalıdır.
  • Varlığın durumunu EntityState.Detached açıkça ayırarak başvuruyu DbContext ve gecikmeli yüklemeyi kesecek şekilde ayarlamak artık çalışmaz.
  • Bir özelliğe zaman uyumsuz bir şekilde erişmenin hiçbir yolu olmadığından, tüm gecikmeli yüklemelerin zaman uyumlu G/Ç kullandığını unutmayın.

İzlenmeyen varlıklardan yavaş yükleme, hem gecikmeli yükleme proxy'leri hem de proxy'ler olmadan yavaş yükleme için çalışır.

İzlenmeyen varlıklardan açık yükleme

EF8, varlık veya gezinti yavaş yükleme için yapılandırılmamış olsa bile izlemesiz varlıklara gezintilerin yüklenmesini destekler. Gecikmeli yüklemeden farklı olarak, bu açık yükleme zaman uyumsuz olarak yapılabilir. Örneğin:

await context.Entry(blog).Collection(e => e.Posts).LoadAsync();

Belirli gezintiler için gecikmeli yüklemeyi geri çevirme

EF8, diğer her şey ayarlanmış olsa bile belirli gezintilerin yavaş yüklenmemesini sağlar. Örneğin, gezintiyi Post.Author gecikmeli yük olmayacak şekilde yapılandırmak için aşağıdakileri yapın:

modelBuilder
    .Entity<Post>()
    .Navigation(p => p.Author)
    .EnableLazyLoading(false);

Gecikmeli yüklemenin bu şekilde devre dışı bırakılması hem yavaş yüklenen proxy'ler hem de proxy'ler olmadan yavaş yükleme için kullanılabilir.

Yavaş yükleme proxy'leri, sanal gezinti özelliklerini geçersiz kılarak çalışır. Klasik EF6 uygulamalarında, yaygın bir hata kaynağı gezintiyi sanal yapmayı unutur, çünkü gezinti sessizce yavaş yüklenmez. Bu nedenle, bir gezinti sanal olmadığında EF Core proxy'leri varsayılan olarak oluşturur.

Bu, KLASIK EF6 davranışını kabul etmek için EF8'de değiştirilebilir, böylece gezinti yalnızca sanal olmayan bir gezinti yapılarak yavaş yüklenmez hale getirilebilir. Bu kabul etme, çağrısının UseLazyLoadingProxiesbir parçası olarak yapılandırılır. Örneğin:

optionsBuilder.UseLazyLoadingProxies(b => b.IgnoreNonVirtualNavigations());

İzlenen varlıklara erişim

İzlenen varlıkları birincil, alternatif veya yabancı anahtara göre arama

DAHILI olarak EF, izlenen varlıkları birincil, alternatif veya yabancı anahtara göre bulmak için veri yapılarını korur. Bu veri yapıları, yeni varlıklar izlendiğinde veya ilişkiler değiştiğinde ilgili varlıklar arasında verimli düzeltme için kullanılır.

EF8, uygulamaların izlenen varlıkları verimli bir şekilde arama amacıyla bu veri yapılarını kullanabilmesi için yeni genel API'ler içerir. Bu API'lere varlık türü aracılığıyla LocalView<TEntity> erişilir. Örneğin, izlenen bir varlığı birincil anahtarına göre arama yapmak için:

var blogEntry = context.Blogs.Local.FindEntry(2)!;

İpucu

Burada gösterilen kod LookupByKeySample.cs gelir.

FindEntry yöntemi izlenen varlık için öğesini döndürür EntityEntry<TEntity> veya null verilen anahtara sahip bir varlık izlenmiyorsa. üzerindeki LocalViewtüm yöntemler gibi, varlık bulunamazsa bile veritabanı hiçbir zaman sorgulanmaz. Döndürülen giriş varlığın kendisini ve izleme bilgilerini içerir. Örneğin:

Console.WriteLine($"Blog '{blogEntry.Entity.Name}' with key {blogEntry.Entity.Id} is tracked in the '{blogEntry.State}' state.");

Bir varlığı birincil anahtar dışında herhangi bir şeyle aramak için özellik adının belirtilmesi gerekir. Örneğin, alternatif bir tuşa göre aramak için:

var siteEntry = context.Websites.Local.FindEntry(nameof(Website.Uri), new Uri("https://www.bricelam.net/"))!;

Veya benzersiz bir yabancı anahtara göre aramak için:

var blogAtSiteEntry = context.Blogs.Local.FindEntry(nameof(Blog.SiteUri), new Uri("https://www.bricelam.net/"))!;

Şimdiye kadar, aramalar her zaman tek bir giriş veya nulldöndürmüştür. Ancak bazı aramalar, benzersiz olmayan bir yabancı anahtar arama gibi birden fazla giriş döndürebilir. yöntemi GetEntries bu aramalar için kullanılmalıdır. Örneğin:

var postEntries = context.Posts.Local.GetEntries(nameof(Post.BlogId), 2);

Tüm bu durumlarda, arama için kullanılan değer birincil anahtar, alternatif anahtar veya yabancı anahtar değeridir. EF, bu aramalar için iç veri yapılarını kullanır. Ancak, değere göre aramalar, herhangi bir özelliğin veya özelliklerin birleşiminin değeri için de kullanılabilir. Örneğin, arşivlenmiş tüm gönderileri bulmak için:

var archivedPostEntries = context.Posts.Local.GetEntries(nameof(Post.Archived), true);

Bu arama, izlenen Post tüm örneklerin taranması gerektirir ve bu nedenle anahtar aramalarından daha az verimli olacaktır. Ancak, genellikle kullanan ChangeTracker.Entries<TEntity>()saf sorgulardan daha hızlıdır.

Son olarak, bileşik anahtarlara, birden çok özelliğin diğer birleşimlerine veya derleme zamanında özellik türünün bilinmediği durumlarda aramalar yapmak da mümkündür. Örneğin:

var postTagEntry = context.Set<PostTag>().Local.FindEntryUntyped(new object[] { 4, "TagEF" });

Model oluşturma

Ayrımcı sütunların uzunluk üstleri

EF8'de, TPH devralma eşlemesi için kullanılan dize ayrıştırıcı sütunları artık maksimum uzunlukla yapılandırılır. Bu uzunluk, tanımlanan tüm ayrımcı değerleri kapsayan en küçük Fibonacci sayısı olarak hesaplanır. Örneğin, aşağıdaki hiyerarşiyi göz önünde bulundurun:

public abstract class Document
{
    public int Id { get; set; }
    public string Title { get; set; }
}

public abstract class Book : Document
{
    public string? Isbn { get; set; }
}

public class PaperbackEdition : Book
{
}

public class HardbackEdition : Book
{
}

public class Magazine : Document
{
    public int IssueNumber { get; set; }
}

Ayrımcı değerler için sınıf adlarını kullanma kuralıyla, buradaki olası değerler "PaperbackEdition", "HardbackEdition" ve "Magazine" şeklindedir ve bu nedenle ayırıcı sütun en fazla 21 uzunluk için yapılandırılır. Örneğin, SQL Server kullanılırken:

CREATE TABLE [Documents] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Discriminator] nvarchar(21) NOT NULL,
    [Isbn] nvarchar(max) NULL,
    [IssueNumber] int NULL,
    CONSTRAINT [PK_Documents] PRIMARY KEY ([Id]),

İpucu

Fibonacci numaraları, hiyerarşiye yeni türler eklendikçe sütun uzunluğunu değiştirmek için geçişin oluşturulma sayısını sınırlamak için kullanılır.

SQL Server'da DateOnly/TimeOnly desteklenir

DateOnly ve TimeOnly türleri .NET 6'da kullanıma sunulmuştur ve girişlerinden bu yana çeşitli veritabanı sağlayıcıları (örneğin SQLite, MySQL ve PostgreSQL) için desteklenmektedir. SQL Server için, .NET 6'yı hedefleyen bir Microsoft.Data.SqlClient paketinin son sürümü ErikEJ'nin bu türler için ADO.NET düzeyinde destek eklemesine izin verdi. Bu da ef8'de DateOnly varlık türlerinde özellikler olarak ve TimeOnly için destek yolunu açmıştı.

İpucu

DateOnlyve TimeOnly EF Core 6 ve 7'de, @ErikEJ ErikEJ.EntityFrameworkCore.SqlServer.DateOnlyTimeOnly topluluk paketi kullanılarak kullanılabilir.

Örneğin, İngiliz okulları için aşağıdaki EF modelini göz önünde bulundurun:

public class School
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public DateOnly Founded { get; set; }
    public List<Term> Terms { get; } = new();
    public List<OpeningHours> OpeningHours { get; } = new();
}

public class Term
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public DateOnly FirstDay { get; set; }
    public DateOnly LastDay { get; set; }
    public School School { get; set; } = null!;
}

[Owned]
public class OpeningHours
{
    public OpeningHours(DayOfWeek dayOfWeek, TimeOnly? opensAt, TimeOnly? closesAt)
    {
        DayOfWeek = dayOfWeek;
        OpensAt = opensAt;
        ClosesAt = closesAt;
    }

    public DayOfWeek DayOfWeek { get; private set; }
    public TimeOnly? OpensAt { get; set; }
    public TimeOnly? ClosesAt { get; set; }
}

İpucu

Burada gösterilen kod DateOnlyTimeOnlySample.cs gelir.

Not

Bu model yalnızca İngiliz okullarını temsil eder ve saatleri yerel (GMT) saat olarak depolar. Farklı saat dilimlerinin işlenmesi bu kodu önemli ölçüde karmaşık hale getirmektedir. Açılış ve kapanış saatleri yaz saati etkin olup olmamasına bağlı olarak farklı uzaklıklara sahip olduğundan, kullanımın DateTimeOffset burada yardımcı olmayacağını unutmayın.

Bu varlık türleri SQL Server kullanılırken aşağıdaki tablolarla eşlenir. Özelliklerin DateOnly sütunlaradate, özelliklerin ise sütunlara time eşlendiğini TimeOnly görebilirsiniz.

CREATE TABLE [Schools] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    [Founded] date NOT NULL,
    CONSTRAINT [PK_Schools] PRIMARY KEY ([Id]));

CREATE TABLE [OpeningHours] (
    [SchoolId] int NOT NULL,
    [Id] int NOT NULL IDENTITY,
    [DayOfWeek] int NOT NULL,
    [OpensAt] time NULL,
    [ClosesAt] time NULL,
    CONSTRAINT [PK_OpeningHours] PRIMARY KEY ([SchoolId], [Id]),
    CONSTRAINT [FK_OpeningHours_Schools_SchoolId] FOREIGN KEY ([SchoolId]) REFERENCES [Schools] ([Id]) ON DELETE CASCADE);

CREATE TABLE [Term] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    [FirstDay] date NOT NULL,
    [LastDay] date NOT NULL,
    [SchoolId] int NOT NULL,
    CONSTRAINT [PK_Term] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Term_Schools_SchoolId] FOREIGN KEY ([SchoolId]) REFERENCES [Schools] ([Id]) ON DELETE CASCADE);

Kullanan DateOnly ve TimeOnly beklenen şekilde çalışan sorgular. Örneğin, aşağıdaki LINQ sorgusu şu anda açık olan okulları bulur:

openSchools = await context.Schools
    .Where(
        s => s.Terms.Any(
                 t => t.FirstDay <= today
                      && t.LastDay >= today)
             && s.OpeningHours.Any(
                 o => o.DayOfWeek == dayOfWeek
                      && o.OpensAt < time && o.ClosesAt >= time))
    .ToListAsync();

Bu sorgu, tarafından ToQueryStringgösterildiği gibi aşağıdaki SQL'e çevrilir:

DECLARE @__today_0 date = '2023-02-07';
DECLARE @__dayOfWeek_1 int = 2;
DECLARE @__time_2 time = '19:53:40.4798052';

SELECT [s].[Id], [s].[Founded], [s].[Name], [o0].[SchoolId], [o0].[Id], [o0].[ClosesAt], [o0].[DayOfWeek], [o0].[OpensAt]
FROM [Schools] AS [s]
LEFT JOIN [OpeningHours] AS [o0] ON [s].[Id] = [o0].[SchoolId]
WHERE EXISTS (
    SELECT 1
    FROM [Term] AS [t]
    WHERE [s].[Id] = [t].[SchoolId] AND [t].[FirstDay] <= @__today_0 AND [t].[LastDay] >= @__today_0) AND EXISTS (
    SELECT 1
    FROM [OpeningHours] AS [o]
    WHERE [s].[Id] = [o].[SchoolId] AND [o].[DayOfWeek] = @__dayOfWeek_1 AND [o].[OpensAt] < @__time_2 AND [o].[ClosesAt] >= @__time_2)
ORDER BY [s].[Id], [o0].[SchoolId]

DateOnly ve TimeOnly JSON sütunlarında da kullanılabilir. Örneğin, OpeningHours JSON belgesi olarak kaydedilerek şuna benzer veriler elde edilebilir:

Sütun Değer
Id 2
Veri Akışı Adı Farr Lisesi
Yıl -ında kurulan 1964-05-01
OpeningHours
[
{ "DayOfWeek": "Sunday", "ClosesAt": null, "OpensAt": null },
{ "DayOfWeek": "Monday", "ClosesAt": "15:35:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Tuesday", "ClosesAt": "15:35:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Wednesday", "ClosesAt": "15:35:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Thursday", "ClosesAt": "15:35:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Friday", "ClosesAt": "12:50:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Saturday", "ClosesAt": null, "OpensAt": null }
]

EF8'in iki özelliğini birleştirerek artık JSON koleksiyonuna dizin ekleyerek açılış saatlerini sorgulayabiliriz. Örneğin:

openSchools = await context.Schools
    .Where(
        s => s.Terms.Any(
                 t => t.FirstDay <= today
                      && t.LastDay >= today)
             && s.OpeningHours[(int)dayOfWeek].OpensAt < time
             && s.OpeningHours[(int)dayOfWeek].ClosesAt >= time)
    .ToListAsync();

Bu sorgu, tarafından ToQueryStringgösterildiği gibi aşağıdaki SQL'e çevrilir:

DECLARE @__today_0 date = '2023-02-07';
DECLARE @__dayOfWeek_1 int = 2;
DECLARE @__time_2 time = '20:14:34.7795877';

SELECT [s].[Id], [s].[Founded], [s].[Name], [s].[OpeningHours]
FROM [Schools] AS [s]
WHERE EXISTS (
    SELECT 1
    FROM [Term] AS [t]
    WHERE [s].[Id] = [t].[SchoolId] AND [t].[FirstDay] <= @__today_0
      AND [t].[LastDay] >= @__today_0)
      AND CAST(JSON_VALUE([s].[OpeningHours],'$[' + CAST(CAST(@__dayOfWeek_1 AS int) AS nvarchar(max)) + '].OpensAt') AS time) < @__time_2
      AND CAST(JSON_VALUE([s].[OpeningHours],'$[' + CAST(CAST(@__dayOfWeek_1 AS int) AS nvarchar(max)) + '].ClosesAt') AS time) >= @__time_2

Son olarak, güncelleştirmeler ve silme işlemleri izleme ve SaveChanges ile veya ExecuteUpdate/ExecuteDelete kullanılarak gerçekleştirilebilir. Örneğin:

await context.Schools
    .Where(e => e.Terms.Any(t => t.LastDay.Year == 2022))
    .SelectMany(e => e.Terms)
    .ExecuteUpdateAsync(s => s.SetProperty(t => t.LastDay, t => t.LastDay.AddDays(1)));

Bu güncelleştirme aşağıdaki SQL'e çevrilir:

UPDATE [t0]
SET [t0].[LastDay] = DATEADD(day, CAST(1 AS int), [t0].[LastDay])
FROM [Schools] AS [s]
INNER JOIN [Term] AS [t0] ON [s].[Id] = [t0].[SchoolId]
WHERE EXISTS (
    SELECT 1
    FROM [Term] AS [t]
    WHERE [s].[Id] = [t].[SchoolId] AND DATEPART(year, [t].[LastDay]) = 2022)

Synapse ve Dynamics 365 TDS'de ters mühendislik

EF8 tersine mühendislik (var olan bir veritabanından yapı iskelesi) artık Synapse Sunucusuz SQL Havuzu ve Dynamics 365 TDS Uç Nokta veritabanlarını destekliyor.

Uyarı

Bu veritabanı sistemleri normal SQL Server ve Azure SQL veritabanlarından farklıdır. Bu farklılıklar, bu veritabanı sistemlerine karşı sorgu yazarken veya diğer işlemler gerçekleştirirken tüm EF Core işlevlerinin desteklenmediği anlamına gelir.

Matematik çevirilerinde geliştirmeler

Genel matematik arabirimleri .NET 7'de kullanıma sunulmuştur. Matematik ve float MathF'nin mevcut işlevselliğini yansıtan yeni API'ler ekleyen ve bu arabirimleri uygulayan somut türlerdouble.

EF Core 8, ve MathFiçin Math sağlayıcıların mevcut SQL çevirilerini kullanarak LINQ'deki bu genel matematik API'lerine yapılan çağrıları çevirir. Bu, artık EF sorgularınızdaki gibi Math.Sin veya double.Sin aramalar arasında seçim yapabileceğiniz anlamına gelir.

.NET 8'de ve floatüzerinde double uygulanan iki yeni genel matematik yöntemi eklemek için .NET ekibiyle birlikte çalıştık. Bunlar ayrıca EF Core 8'de SQL'e de çevrilir.

.NET SQL
DegreesToRadians RADIANS
RadiansToDegrees DEGREES

Son olarak, SQLitePCLRaw projesinde Eric Sink ile birlikte çalışarak SQLite matematik işlevlerini yerel SQLite kitaplığı derlemelerinde etkinleştirdik. Bu, EF Core SQLite sağlayıcısını yüklerken varsayılan olarak edindiğiniz yerel kitaplığı içerir. Bu, LINQ'de Acos, Acosh, Asin, Asin, Asinh, Atan, Atan2, Atanh, Ceiling, Cos, Cosh, DegreesToRadians, Exp, Floor, Log, Log2, Log10, Pow, RadiansToDegrees, Sign, Sin, Sinh, Sqrt, Tan, Tanh ve Truncate gibi birkaç yeni SQL çevirisini etkinleştirir.

Bekleyen model değişikliklerini denetleme

Son geçişten sonra model değişiklikleri yapılıp yapılmadığını denetlemek için yeni dotnet ef bir komut ekledik. Bu, sizin veya ekip arkadaşınıza geçiş eklemeyi unutmadığınızdan emin olmak için CI/CD senaryolarında yararlı olabilir.

dotnet ef migrations has-pending-model-changes

Ayrıca, yeni dbContext.Database.HasPendingModelChanges() yöntemi kullanarak uygulamanızda veya testlerde program aracılığıyla bu denetimi gerçekleştirebilirsiniz.

SQLite yapı iskelesi geliştirmeleri

SQLite yalnızca dört temel veri türünü destekler: INTEGER, REAL, TEXT ve BLOB. Daha önce bu, EF Core modelinin iskelesini oluşturmak için bir SQLite veritabanına ters mühendislik uygularken elde edilen varlık türlerinin yalnızca , , doublestringve byte[]türünde longözellikleri içereceği anlamına geliyordu. Ek .NET türleri, EF Core SQLite sağlayıcısı tarafından bunlar ile dört temel SQLite türünden biri arasında dönüştürülerek desteklenir.

EF Core 8'de artık modelde kullanılacak daha uygun bir .NET türünü belirlemek için SQLite türüne ek olarak veri biçimini ve sütun türü adını kullanıyoruz. Aşağıdaki tablolarda, ek bilgilerin modelde daha iyi özellik türlerine yol açtığı bazı durumlar gösterilmektedir.

Sütun türü adı .NET türü
BOOLEAN bayt[]bool
SMALLİNT uzunkısa
INT longint
BİGİNT uzun
DİZE byte[]string
Veri biçimi .NET türü
'0.0' dizeondalık
'1970-01-01' dateOnly dizesi
'1970-01-01 00:00:00' datetime dizesi
'00:00:00' timeSpan dizesi
'00000000-0000-0000-0000-000000000000' dizeGuid'i

Sentinel değerleri ve veritabanı varsayılanları

Veritabanları, satır eklenirken değer sağlanmazsa sütunların varsayılan değer oluşturacak şekilde yapılandırılmasına izin verir. Bu, sabitler için kullanılarak HasDefaultValue EF ile temsil edilebilir:

b.Property(e => e.Status).HasDefaultValue("Hidden");

Veya HasDefaultValueSql rastgele SQL yan tümceleri için:

b.Property(e => e.LeaseDate).HasDefaultValueSql("getutcdate()");

İpucu

Aşağıda gösterilen kod DefaultConstraintSample.cs...

EF'nin bunu kullanabilmesi için, sütun için bir değerin ne zaman ve ne zaman gönderilmeyeceğini belirlemesi gerekir. Varsayılan olarak, EF bunun için bir sentinel olarak CLR varsayılanını kullanır. Yani, yukarıdaki örneklerde veya LeaseDate değeri Status bu türler için CLR varsayılanları olduğunda, EF bunu özelliğin ayarlanmadığı anlamına gelir ve veritabanına bir değer göndermez. Bu, başvuru türleri için iyi çalışır; örneğin, özelliği Status ise stringnullEF veritabanına göndermeznull, ancak veritabanı varsayılan değerinin ("Hidden") kullanılması için herhangi bir değer içermez. Benzer şekilde, özelliği LeaseDateiçin DateTime EF clr varsayılan değerini 1/1/0001 12:00:00 AMeklemez, bunun yerine veritabanı varsayılanının kullanılması için bu değeri atlar.

Ancak, bazı durumlarda CLR varsayılan değeri eklemek için geçerli bir değerdir. EF8 bunu bir sütunun sentinel değerinin değişmesine izin vererek işler. Örneğin, veritabanı varsayılanı ile yapılandırılmış bir tamsayı sütununu göz önünde bulundurun:

b.Property(e => e.Credits).HasDefaultValueSql(10);

Bu durumda, yeni varlığın belirtilen sayıda krediyle eklenmesini istiyoruz; aksi belirtilmedikçe, bu durumda 10 kredi atanır. Bununla birlikte, sıfır kredisi olan bir kaydın eklenmesinin mümkün olmadığı anlamına gelir çünkü sıfır varsayılan CLR'dir ve bu nedenle EF'nin hiçbir değer göndermesine neden olur. EF8'de bu, özelliğin sentinel değeri sıfırdan -1olarak değiştirilerek düzeltilebilir:

b.Property(e => e.Credits).HasDefaultValueSql(10).HasSentinel(-1);

EF artık yalnızca olarak ayarlanırsa Credits-1veritabanı varsayılanını kullanır; diğer tutarlar gibi sıfır değeri eklenir.

Bunu hem varlık türüne hem de EF yapılandırmasına yansıtmak genellikle yararlı olabilir. Örneğin:

public class Person
{
    public int Id { get; set; }
    public int Credits { get; set; } = -1;
}

Bu, örnek oluşturulduğunda -1 sentinel değerinin otomatik olarak ayarlandığı anlamına gelir ve bu da özelliğin "ayarlanmadı" durumunda başladığı anlamına gelir.

İpucu

Sütunu oluştururken Migrations kullanılacak veritabanı varsayılan kısıtlamasını yapılandırmak, ancak EF'nin her zaman bir değer eklemesini istiyorsanız, özelliği oluşturulmamış olarak yapılandırın. Örneğin, b.Property(e => e.Credits).HasDefaultValueSql(10).ValueGeneratedNever();.

Boole değerleri için veritabanı varsayılanları

CLR varsayılanı (false) yalnızca iki geçerli değerden biri olduğundan Boole özellikleri bu sorunun aşırı bir biçimini sunar. Bu, veritabanı varsayılan kısıtlaması olan bir bool özelliğin yalnızca bu değer olduğunda eklenen bir değere sahip olacağı trueanlamına gelir. Veritabanı varsayılan değeri olduğunda false, bu özellik değeri falseolduğunda veritabanı varsayılanı kullanılır; yani falseolur. Aksi takdirde, özellik değeri ise truetrue eklenir. Bu nedenle, veritabanı varsayılan değeri olduğunda falseveritabanı sütunu doğru değerle sonuçlanır.

Öte yandan, veritabanı varsayılan değeri ise true, bu özellik değeri olduğunda falseveritabanı varsayılanı kullanılır, yani true! Özellik değeri olduğunda truetrue da eklenir. Bu nedenle, özellik değerinin ne olduğuna bakılmaksızın sütundaki değer her zaman veritabanında sona erer true .

EF8, bool özellikleri için sentinel değerini veritabanı varsayılan değeriyle aynı değere ayarlayarak bu sorunu düzeltir. Yukarıdaki her iki durum da veritabanı varsayılanının veya falseolmasına bakılmaksızın doğru değerin eklenmesiyle sonuçlanırtrue.

İpucu

Mevcut bir veritabanından yapı iskelesi oluştururken, EF8 çağrılara basit varsayılan değerleri HasDefaultValue ayrıştırıp ekler. (Daha önce tüm varsayılan değerler opak HasDefaultValueSql çağrılar olarak iskeleye alınıyordu.) Bu, veya false sabit veritabanı varsayılanı olan true boş değer atanamayan bool sütunlarının artık boş değer atanabilir olarak yapı iskelesi yapılmaması anlamına gelir.

Sabit listeleri için veritabanı varsayılanları

Sabit listeleri genellikle çok küçük bir geçerli değer kümesine bool sahip olduğundan ve CLR varsayılanı bu değerlerden biri olabileceğinden, numaralandırma özellikleri özelliklere benzer sorunlarla karşılaşabilir. Örneğin, şu varlık türünü ve numaralandırmasını göz önünde bulundurun:

public class Course
{
    public int Id { get; set; }
    public Level Level { get; set; }
}

public enum Level
{
    Beginner,
    Intermediate,
    Advanced,
    Unspecified
}

Ardından Level özelliği bir veritabanı varsayılanı ile yapılandırılır:

modelBuilder.Entity<Course>()
    .Property(e => e.Level)
    .HasDefaultValue(Level.Intermediate);

Bu yapılandırmayla, EF değeri olarak ayarlandığında Level.Beginnerveritabanına göndermeyi dışlar ve bunun yerine Level.Intermediate veritabanı tarafından atanır. Amaçlanan bu değildi!

Sabit listesi veritabanı varsayılan değeri olan "bilinmiyor" veya "belirtilmemiş" değeriyle tanımlandıysa sorun oluşmaz:

public enum Level
{
    Unspecified,
    Beginner,
    Intermediate,
    Advanced
}

Ancak, mevcut bir sabit listesi her zaman değiştirilemez, bu nedenle EF8'de sentinel yeniden belirtilebilir. Örneğin, özgün numaralandırmaya geri dönün:

modelBuilder.Entity<Course>()
    .Property(e => e.Level)
    .HasDefaultValue(Level.Intermediate)
    .HasSentinel(Level.Unspecified);

Şimdi Level.Beginner normal şekilde eklenir ve veritabanı varsayılanı yalnızca özellik değeri olduğunda Level.Unspecifiedkullanılır. Bunu varlık türünün kendisine yansıtmak yeniden yararlı olabilir. Örneğin:

public class Course
{
    public int Id { get; set; }
    public Level Level { get; set; } = Level.Unspecified;
}

Boş değer atanabilir bir yedekleme alanı kullanma

Yukarıda açıklanan sorunu işlemenin daha genel bir yolu, null atanamayan özellik için null atanabilir bir yedekleme alanı oluşturmaktır. Örneğin, bir bool özelliği olan aşağıdaki varlık türünü göz önünde bulundurun:

public class Account
{
    public int Id { get; set; }
    public bool IsActive { get; set; }
}

özelliğine null atanabilir bir yedekleme alanı verilebilir:

public class Account
{
    public int Id { get; set; }

    private bool? _isActive;

    public bool IsActive
    {
        get => _isActive ?? false;
        set => _isActive = value;
    }
}

Özellik ayarlayıcısı gerçekten çağrılmadığı sürece buradaki yedekleme alanı kalır.null Yani, yedekleme alanının değeri özelliğin ayarlanıp ayarlanmadığının, özelliğin CLR varsayılanından daha iyi bir göstergesidir. Ef özelliği varsayılan olarak okumak ve yazmak için yedekleme alanını kullanacağı için bu, EF ile birlikte kullanıma hazır bir şekilde çalışır.

Daha İyi ExecuteUpdate ve ExecuteDelete

ve yöntemleri tarafından ExecuteUpdateExecuteDelete oluşturulanlar gibi güncelleştirme ve silme işlemleri gerçekleştiren SQL komutlarının tek bir veritabanı tablosunu hedeflemesi gerekir. Ancak EF7'de ExecuteUpdateExecuteDelete sorgu sonunda tek bir tabloyu etkilese bile birden çok varlık türüne erişen güncelleştirmeleri desteklemedi. EF8 bu sınırlamayı kaldırır. Örneğin, sahip olunan türe sahip CustomerInfo bir Customer varlık türünü göz önünde bulundurun:

public class Customer
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public required CustomerInfo CustomerInfo { get; set; }
}

[Owned]
public class CustomerInfo
{
    public string? Tag { get; set; }
}

Bu varlık türlerinin her ikisi de tabloyla eşlenir Customers . Ancak, her iki varlık türünü de kullandığından EF7'de aşağıdaki toplu güncelleştirme başarısız olur:

await context.Customers
    .Where(e => e.Name == name)
    .ExecuteUpdateAsync(
        s => s.SetProperty(b => b.CustomerInfo.Tag, "Tagged")
            .SetProperty(b => b.Name, b => b.Name + "_Tagged"));

EF8'de bu artık Azure SQL kullanılırken aşağıdaki SQL'e çevrilir:

UPDATE [c]
SET [c].[Name] = [c].[Name] + N'_Tagged',
    [c].[CustomerInfo_Tag] = N'Tagged'
FROM [Customers] AS [c]
WHERE [c].[Name] = @__name_0

Benzer şekilde, bir Union sorgudan döndürülen örnekler, güncelleştirmelerin tümü aynı tabloyu hedeflediği sürece güncelleştirilebilir. Örneğin, bir bölgesi Franceile ve aynı zamanda, bir mağazayı bölgeyle Franceziyaret eden herhangi Customer birini güncelleştirebilirizCustomer:

await context.CustomersWithStores
    .Where(e => e.Region == "France")
    .Union(context.Stores.Where(e => e.Region == "France").SelectMany(e => e.Customers))
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.Tag, "The French Connection"));

EF8'de bu sorgu, Azure SQL kullanırken aşağıdakileri oluşturur:

UPDATE [c]
SET [c].[Tag] = N'The French Connection'
FROM [CustomersWithStores] AS [c]
INNER JOIN (
    SELECT [c0].[Id], [c0].[Name], [c0].[Region], [c0].[StoreId], [c0].[Tag]
    FROM [CustomersWithStores] AS [c0]
    WHERE [c0].[Region] = N'France'
    UNION
    SELECT [c1].[Id], [c1].[Name], [c1].[Region], [c1].[StoreId], [c1].[Tag]
    FROM [Stores] AS [s]
    INNER JOIN [CustomersWithStores] AS [c1] ON [s].[Id] = [c1].[StoreId]
    WHERE [s].[Region] = N'France'
) AS [t] ON [c].[Id] = [t].[Id]

Son örnek olarak, EF8'de tüm ExecuteUpdate güncelleştirilmiş özellikler aynı tabloya eşlendiği sürece TPT hiyerarşisindeki varlıkları güncelleştirmek için kullanılabilir. Örneğin, TPT kullanılarak eşlenen bu varlık türlerini göz önünde bulundurun:

[Table("TptSpecialCustomers")]
public class SpecialCustomerTpt : CustomerTpt
{
    public string? Note { get; set; }
}

[Table("TptCustomers")]
public class CustomerTpt
{
    public int Id { get; set; }
    public required string Name { get; set; }
}

EF8 ile Note özelliği güncelleştirilebilir:

await context.TptSpecialCustomers
    .Where(e => e.Name == name)
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.Note, "Noted"));

Name Veya özelliği güncelleştirilebilir:

await context.TptSpecialCustomers
    .Where(e => e.Name == name)
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.Name, b => b.Name + " (Noted)"));

Ancak EF8, farklı tablolara eşlendiğinden NameNote hem hem hem de özelliklerini güncelleştirme girişiminde başarısız olur. Örneğin:

await context.TptSpecialCustomers
    .Where(e => e.Name == name)
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.Note, "Noted")
        .SetProperty(b => b.Name, b => b.Name + " (Noted)"));

Aşağıdaki özel durumu oluşturur:

The LINQ expression 'DbSet<SpecialCustomerTpt>()
    .Where(s => s.Name == __name_0)
    .ExecuteUpdate(s => s.SetProperty<string>(
        propertyExpression: b => b.Note,
        valueExpression: "Noted").SetProperty<string>(
        propertyExpression: b => b.Name,
        valueExpression: b => b.Name + " (Noted)"))' could not be translated. Additional information: Multiple 'SetProperty' invocations refer to different tables ('b => b.Note' and 'b => b.Name'). A single 'ExecuteUpdate' call can only update the columns of a single table. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

Sorguların IN daha iyi kullanımı

Contains LINQ işleci bir alt sorguyla kullanıldığında, EF Core artık yerine SQL INEXISTSkullanarak daha iyi sorgular oluşturur; daha okunabilir SQL üretmenin yanı sıra, bazı durumlarda bu durum çok daha hızlı sorgulara neden olabilir. Örneğin, aşağıdaki LINQ sorgusunu göz önünde bulundurun:

var blogsWithPosts = await context.Blogs
    .Where(b => context.Posts.Select(p => p.BlogId).Contains(b.Id))
    .ToListAsync();

EF7, PostgreSQL için aşağıdakileri oluşturur:

SELECT b."Id", b."Name"
      FROM "Blogs" AS b
      WHERE EXISTS (
          SELECT 1
          FROM "Posts" AS p
          WHERE p."BlogId" = b."Id")

Alt sorgu dış Blogs tabloya (aracılığıyla b."Id") başvurdığından, bu bağıntılı bir alt sorgudur ve bu da alt sorgunun Posts tablodaki Blogs her satır için yürütülmesi gerektiği anlamına gelir. EF8'de bunun yerine aşağıdaki SQL oluşturulur:

SELECT b."Id", b."Name"
      FROM "Blogs" AS b
      WHERE b."Id" IN (
          SELECT p."BlogId"
          FROM "Posts" AS p
      )

Alt sorgu artık öğesine başvurmadığından Blogsbir kez değerlendirilebilir ve bu da çoğu veritabanı sisteminde büyük performans iyileştirmeleri sağlar. Ancak, özellikle SQL Server olmak üzere bazı veritabanı sistemleri, ilk sorguyu ikinci sorguya en iyi duruma getirerek performansın aynı olmasını sağlayabilir.

SQL Azure/SQL Server için sayısal rowversions

SQL Server otomatik iyimser eşzamanlılığı sütunlar kullanılarak rowversion işlenir. A rowversion , veritabanı, istemci ve sunucu arasında geçirilen 8 baytlık opak bir değerdir. Varsayılan olarak, SqlClient değiştirilebilir başvuru türlerinin semantik için rowversion hatalı eşleşme olmasına rağmen türleri olarak byte[]kullanıma sunarrowversion. EF8'de sütunları veya ulong özellikleriyle eşlemek rowversionlong kolaydır. Örneğin:

modelBuilder.Entity<Blog>()
    .Property(e => e.RowVersion)
    .HasConversion<byte[]>()
    .IsRowVersion();

Parantezleri ortadan kaldırma

Okunabilir SQL oluşturmak EF Core için önemli bir hedeftir. EF8'de, oluşturulan SQL gereksiz parantezlerin otomatik olarak ortadan kaldırılmasıyla daha okunabilir. Örneğin, aşağıdaki LINQ sorgusu:

await ctx.Customers  
    .Where(c => c.Id * 3 + 2 > 0 && c.FirstName != null || c.LastName != null)  
    .ToListAsync();  

EF7 kullanılırken aşağıdaki Azure SQL'e çevrilir:

SELECT [c].[Id], [c].[City], [c].[FirstName], [c].[LastName], [c].[Street]
FROM [Customers] AS [c]
WHERE ((([c].[Id] * 3) + 2) > 0 AND ([c].[FirstName] IS NOT NULL)) OR ([c].[LastName] IS NOT NULL)

EF8 kullanılırken aşağıdakiler geliştirilmiştir:

SELECT [c].[Id], [c].[City], [c].[FirstName], [c].[LastName], [c].[Street]
FROM [Customers] AS [c]
WHERE ([c].[Id] * 3 + 2 > 0 AND [c].[FirstName] IS NOT NULL) OR [c].[LastName] IS NOT NULL

RETURNING/OUTPUT yan tümcesi için özel geri çevirme

EF7, veritabanı tarafından oluşturulan sütunları geri getirmek için varsayılan SQL güncelleştirmesini kullanacak RETURNING/OUTPUT şekilde değiştirdi. Bunun nerede çalışmadığının belirlendiği bazı durumlar ve bu nedenle EF8 bu davranış için açık geri çevirmeler sunar.

Örneğin, SQL Server/Azure SQL sağlayıcısını kullanırken geri çevirmek OUTPUT için:

 modelBuilder.Entity<Customer>().ToTable(tb => tb.UseSqlOutputClause(false));

Veya SQLite sağlayıcısını kullanırken geri çevirmek RETURNING için:

 modelBuilder.Entity<Customer>().ToTable(tb => tb.UseSqlReturningClause(false));

Diğer küçük değişiklikler

Yukarıda açıklanan iyileştirmelere ek olarak, EF8'de birçok küçük değişiklik yapılmıştır. Buna aşağıdakiler dahildir: