Share via


EF Core 7.0'daki Yenilikler

EF Core 7.0 (EF7), Kasım 2022'de piyasaya sürüldü.

Bahşiş

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.

EF7 , .NET 6'yı hedefler ve bu nedenle .NET 6 (LTS) veya .NET 7 ile kullanılabilir.

Örnek model

Aşağıdaki örneklerin birçoğu bloglar, gönderiler, etiketler ve yazarlarla basit bir model kullanır:

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

    public int Id { get; private set; }
    public string Name { get; set; }
    public List<Post> Posts { get; } = new();
}

public class Post
{
    public Post(string title, string content, DateTime publishedOn)
    {
        Title = title;
        Content = content;
        PublishedOn = publishedOn;
    }

    public int Id { get; private set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime PublishedOn { get; set; }
    public Blog Blog { get; set; } = null!;
    public List<Tag> Tags { get; } = new();
    public Author? Author { get; set; }
    public PostMetadata? Metadata { get; set; }
}

public class FeaturedPost : Post
{
    public FeaturedPost(string title, string content, DateTime publishedOn, string promoText)
        : base(title, content, publishedOn)
    {
        PromoText = promoText;
    }

    public string PromoText { get; set; }
}

public class Tag
{
    public Tag(string id, string text)
    {
        Id = id;
        Text = text;
    }

    public string Id { get; private set; }
    public string Text { get; set; }
    public List<Post> Posts { get; } = new();
}

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

    public int Id { get; private set; }
    public string Name { get; set; }
    public ContactDetails Contact { get; set; } = null!;
    public List<Post> Posts { get; } = new();
}

Örneklerden bazıları, farklı örneklerde farklı şekillerde eşlenen toplama türlerini de kullanır. Kişiler için bir toplama türü vardır:

public class ContactDetails
{
    public Address Address { get; set; } = null!;
    public string? Phone { get; set; }
}

public class Address
{
    public Address(string street, string city, string postcode, string country)
    {
        Street = street;
        City = city;
        Postcode = postcode;
        Country = country;
    }

    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
    public string Country { get; set; }
}

Gönderi meta verileri için ikinci bir toplama türü:

public class PostMetadata
{
    public PostMetadata(int views)
    {
        Views = views;
    }

    public int Views { get; set; }
    public List<SearchTerm> TopSearches { get; } = new();
    public List<Visits> TopGeographies { get; } = new();
    public List<PostUpdate> Updates { get; } = new();
}

public class SearchTerm
{
    public SearchTerm(string term, int count)
    {
        Term = term;
        Count = count;
    }

    public string Term { get; private set; }
    public int Count { get; private set; }
}

public class Visits
{
    public Visits(double latitude, double longitude, int count)
    {
        Latitude = latitude;
        Longitude = longitude;
        Count = count;
    }

    public double Latitude { get; private set; }
    public double Longitude { get; private set; }
    public int Count { get; private set; }
    public List<string>? Browsers { get; set; }
}

public class PostUpdate
{
    public PostUpdate(IPAddress postedFrom, DateTime updatedOn)
    {
        PostedFrom = postedFrom;
        UpdatedOn = updatedOn;
    }

    public IPAddress PostedFrom { get; private set; }
    public string? UpdatedBy { get; init; }
    public DateTime UpdatedOn { get; private set; }
    public List<Commit> Commits { get; } = new();
}

public class Commit
{
    public Commit(DateTime committedOn, string comment)
    {
        CommittedOn = committedOn;
        Comment = comment;
    }

    public DateTime CommittedOn { get; private set; }
    public string Comment { get; set; }
}

Bahşiş

Örnek model BlogsContext.cs içinde bulunabilir.

JSON Sütunları

İlişkisel veritabanlarının çoğu JSON belgeleri içeren sütunları destekler. Bu sütunlardaki JSON dosyasında sorgularla detaya gidebilirsiniz. Bu, örneğin, belgelerin öğelerine göre filtreleme ve sıralamanın yanı sıra belgelerin dışından öğelerin sonuçlara yansıtılmasını sağlar. JSON sütunları ilişkisel veritabanlarının belge veritabanlarının bazı özelliklerini almasına olanak tanıyarak ikisi arasında kullanışlı bir karma oluşturur.

EF7, SQL Server uygulamasıyla birlikte JSON sütunları için sağlayıcıdan bağımsız destek içerir. Bu destek, .NET türlerinden oluşturulan toplamaların JSON belgelerine eşlenmesini sağlar. Normal LINQ sorguları toplamalarda kullanılabilir ve bunlar JSON'da detaya gitmek için gereken uygun sorgu yapılarına çevrilir. EF7 ayrıca JSON belgelerinin güncelleştirilmesini ve kaydedilmesini de destekler.

Dekont

EF7 sonrası için JSON için SQLite desteği planlanıyor. PostgreSQL ve Pomelo MySQL sağlayıcıları JSON sütunları için zaten bazı destek içerir. JSON desteğini tüm sağlayıcılarda uyumlu hale getirmek için bu sağlayıcıların yazarlarıyla birlikte çalışacağız.

JSON sütunlarına eşleme

EF Core'da toplama türleri ve OwnsManykullanılarak OwnsOne tanımlanır. Örneğin, kişi bilgilerini depolamak için kullanılan örnek modelimizin toplama türünü göz önünde bulundurun:

public class ContactDetails
{
    public Address Address { get; set; } = null!;
    public string? Phone { get; set; }
}

public class Address
{
    public Address(string street, string city, string postcode, string country)
    {
        Street = street;
        City = city;
        Postcode = postcode;
        Country = country;
    }

    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
    public string Country { get; set; }
}

Bu daha sonra bir yazarın kişi ayrıntılarını depolamak için "sahip" varlık türünde kullanılabilir:

public class Author
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ContactDetails Contact { get; set; }
}

Toplama türü kullanılarak OwnsOneyapılandırılırOnModelCreating:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Author>().OwnsOne(
        author => author.Contact, ownedNavigationBuilder =>
        {
            ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
        });
}

Bahşiş

Burada gösterilen kod JsonColumnsSample.cs dosyasından gelir.

varsayılan olarak, ilişkisel veritabanı sağlayıcıları bunun gibi toplama türlerini sahip olan varlık türüyle aynı tabloyla eşler. Diğer bir ifadeyle, ve Address sınıflarının ContactDetails her özelliği tablodaki bir sütuna Authors eşlenir.

Kişi ayrıntıları olan bazı kayıtlı yazarlar şöyle görünür:

Yazar

Id Ad Contact_Address_Street Contact_Address_City Contact_Address_Postcode Contact_Address_Country Contact_Telefon
Kategori 1 Maddy Montaquila 1 Ana Cadde Camberwick Yeşili CW1 5ZH Birleşik Krallık 01632 12345
2 Jeremy Likness 2 Ana Cadde Kıkırdak CW1 5ZH Birleşik Krallık 01632 12346
3 Daniel Roth 3 Ana Cadde Camberwick Yeşili CW1 5ZH Birleşik Krallık 01632 12347
4 Arthur Vickers 15a Ana Cadde Kıkırdak CW1 5ZH Birleşik Krallık 01632 22345
5 Brice Lambson 4 Ana Cadde Kıkırdak CW1 5ZH Birleşik Krallık 01632 12349

İsterseniz, toplamayı oluşturan her varlık türü kendi tablosuna eşlenebilir:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Author>().OwnsOne(
        author => author.Contact, ownedNavigationBuilder =>
        {
            ownedNavigationBuilder.ToTable("Contacts");
            ownedNavigationBuilder.OwnsOne(
                contactDetails => contactDetails.Address, ownedOwnedNavigationBuilder =>
                {
                    ownedOwnedNavigationBuilder.ToTable("Addresses");
                });
        });
}

Daha sonra aynı veriler üç tabloda depolanır:

Yazar

Id Ad
Kategori 1 Maddy Montaquila
2 Jeremy Likness
3 Daniel Roth
4 Arthur Vickers
5 Brice Lambson

İlgili Kişiler

AuthorId Telefon
Kategori 1 01632 12345
2 01632 12346
3 01632 12347
4 01632 22345
5 01632 12349

Adresler

ContactDetailsAuthorId Sokak City Posta kodu Ülke
Kategori 1 1 Ana Cadde Camberwick Yeşili CW1 5ZH Birleşik Krallık
2 2 Ana Cadde Kıkırdak CW1 5ZH Birleşik Krallık
3 3 Ana Cadde Camberwick Yeşili CW1 5ZH Birleşik Krallık
4 15a Ana Cadde Kıkırdak CW1 5ZH Birleşik Krallık
5 4 Ana Cadde Kıkırdak CW1 5ZH Birleşik Krallık

Şimdi, ilginç kısmı için. EF7'de toplama ContactDetails türü bir JSON sütununa eşlenebilir. Bu, toplama türünü yapılandırırken yalnızca bir çağrı ToJson() gerektirir:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Author>().OwnsOne(
        author => author.Contact, ownedNavigationBuilder =>
        {
            ownedNavigationBuilder.ToJson();
            ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
        });
}

Tabloda Authors artık her yazar için ContactDetails bir JSON belgesiyle doldurulan bir JSON sütunu yer alır:

Yazar

Id Ad Başvurun
Kategori 1 Maddy Montaquila {
  "Telefon":"01632 12345",
  "Adres": {
    "City":"Camberwick Green",
    "Ülke":"UK",
    "Postcode":"CW1 5ZH",
    "Street":"1 Main St"
  }
}
2 Jeremy Likness {
  "Telefon":"01632 12346",
  "Adres": {
    "Şehir":"Chigley",
    "Ülke":"UK",
    "Postcode":"CH1 5ZH",
    "Street":"2 Main St"
  }
}
3 Daniel Roth {
  "Telefon":"01632 12347",
  "Adres": {
    "City":"Camberwick Green",
    "Ülke":"UK",
    "Postcode":"CW1 5ZH",
    "Street":"3 Main St"
  }
}
4 Arthur Vickers {
  "Telefon":"01632 12348",
  "Adres": {
    "Şehir":"Chigley",
    "Ülke":"UK",
    "Postcode":"CH1 5ZH",
    "Street":"15a Main St"
  }
}
5 Brice Lambson {
  "Telefon":"01632 12349",
  "Adres": {
    "Şehir":"Chigley",
    "Ülke":"UK",
    "Postcode":"CH1 5ZH",
    "Street":"4 Main St"
  }
}

Bahşiş

Toplamaların bu kullanımı, Azure Cosmos DB için EF Core sağlayıcısı kullanılırken JSON belgelerinin eşlenme yöntemine çok benzer. JSON sütunları, ef core kullanarak belge veritabanlarını ilişkisel bir veritabanına katıştırılmış belgelere getirme olanağı sunar.

Yukarıda gösterilen JSON belgeleri çok basittir, ancak bu eşleme özelliği daha karmaşık belge yapılarıyla da kullanılabilir. Örneğin, örnek modelimizin bir gönderi hakkındaki meta verileri temsil etmek için kullanılan başka bir toplama türünü göz önünde bulundurun:

public class PostMetadata
{
    public PostMetadata(int views)
    {
        Views = views;
    }

    public int Views { get; set; }
    public List<SearchTerm> TopSearches { get; } = new();
    public List<Visits> TopGeographies { get; } = new();
    public List<PostUpdate> Updates { get; } = new();
}

public class SearchTerm
{
    public SearchTerm(string term, int count)
    {
        Term = term;
        Count = count;
    }

    public string Term { get; private set; }
    public int Count { get; private set; }
}

public class Visits
{
    public Visits(double latitude, double longitude, int count)
    {
        Latitude = latitude;
        Longitude = longitude;
        Count = count;
    }

    public double Latitude { get; private set; }
    public double Longitude { get; private set; }
    public int Count { get; private set; }
    public List<string>? Browsers { get; set; }
}

public class PostUpdate
{
    public PostUpdate(IPAddress postedFrom, DateTime updatedOn)
    {
        PostedFrom = postedFrom;
        UpdatedOn = updatedOn;
    }

    public IPAddress PostedFrom { get; private set; }
    public string? UpdatedBy { get; init; }
    public DateTime UpdatedOn { get; private set; }
    public List<Commit> Commits { get; } = new();
}

public class Commit
{
    public Commit(DateTime committedOn, string comment)
    {
        CommittedOn = committedOn;
        Comment = comment;
    }

    public DateTime CommittedOn { get; private set; }
    public string Comment { get; set; }
}

Bu toplama türü birkaç iç içe tür ve koleksiyon içerir. OwnsOne ve OwnsMany çağrıları bu toplama türünü eşlemek için kullanılır:

modelBuilder.Entity<Post>().OwnsOne(
    post => post.Metadata, ownedNavigationBuilder =>
    {
        ownedNavigationBuilder.ToJson();
        ownedNavigationBuilder.OwnsMany(metadata => metadata.TopSearches);
        ownedNavigationBuilder.OwnsMany(metadata => metadata.TopGeographies);
        ownedNavigationBuilder.OwnsMany(
            metadata => metadata.Updates,
            ownedOwnedNavigationBuilder => ownedOwnedNavigationBuilder.OwnsMany(update => update.Commits));
    });

Bahşiş

ToJson yalnızca toplamanın tamamını bir JSON belgesine eşlemek için toplama kökünde gereklidir.

Bu eşlemeyle EF7, aşağıdaki gibi karmaşık bir JSON belgesi oluşturabilir ve sorgulayabilir:

{
  "Views": 5085,
  "TopGeographies": [
    {
      "Browsers": "Firefox, Netscape",
      "Count": 924,
      "Latitude": 110.793,
      "Longitude": 39.2431
    },
    {
      "Browsers": "Firefox, Netscape",
      "Count": 885,
      "Latitude": 133.793,
      "Longitude": 45.2431
    }
  ],
  "TopSearches": [
    {
      "Count": 9359,
      "Term": "Search #1"
    }
  ],
  "Updates": [
    {
      "PostedFrom": "127.0.0.1",
      "UpdatedBy": "Admin",
      "UpdatedOn": "1996-02-17T19:24:29.5429092Z",
      "Commits": []
    },
    {
      "PostedFrom": "127.0.0.1",
      "UpdatedBy": "Admin",
      "UpdatedOn": "2019-11-24T19:24:29.5429093Z",
      "Commits": [
        {
          "Comment": "Commit #1",
          "CommittedOn": "2022-08-21T00:00:00+01:00"
        }
      ]
    },
    {
      "PostedFrom": "127.0.0.1",
      "UpdatedBy": "Admin",
      "UpdatedOn": "1997-05-28T19:24:29.5429097Z",
      "Commits": [
        {
          "Comment": "Commit #1",
          "CommittedOn": "2022-08-21T00:00:00+01:00"
        },
        {
          "Comment": "Commit #2",
          "CommittedOn": "2022-08-21T00:00:00+01:00"
        }
      ]
    }
  ]
}

Dekont

Uzamsal türleri doğrudan JSON'a eşleme henüz desteklenmiyor. Yukarıdaki belgede geçici çözüm olarak değerler kullanılır double . İlgilendiğiniz bir konuysa JSON sütunlarında Uzamsal türleri destekleme için oy verin.

Dekont

İlkel tür koleksiyonlarının JSON ile eşlenmesi henüz desteklenmiyor. Yukarıdaki belge, koleksiyonu virgülle ayrılmış bir dizeye dönüştürmek için bir değer dönüştürücüsü kullanır. Json için oy verin: bu ilgilendiğiniz bir şeyse ilkel türlerin toplanması için destek ekleyin.

Dekont

Sahip olunan türlerin JSON'a eşlenmesi henüz TPT veya TPC devralma ile birlikte desteklenmiyor. İlgilendiğiniz bir şeyse TPT/TPC devralma eşlemesi ile JSON özelliklerini destekleme için oy verin.

JSON sütunlarına sorgular

JSON sütunlarına yapılan sorgular, EF Core'daki diğer toplama türlerinde sorgulamayla aynı şekilde çalışır. Yani, LINQ kullanın! İşte bazı örnekler.

Chigley'de yaşayan tüm yazarlar için bir sorgu:

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

Bu sorgu, SQL Server kullanılırken aşağıdaki SQL'i oluşturur:

SELECT [a].[Id], [a].[Name], JSON_QUERY([a].[Contact],'$')
FROM [Authors] AS [a]
WHERE CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley'

JSON belgesinin içinden almak CityAddress için kullanımına JSON_VALUE dikkat edin.

Select JSON belgesinden öğeleri ayıklamak ve yansıtmak için kullanılabilir:

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

Bu sorgu aşağıdaki SQL'i oluşturur:

SELECT CAST(JSON_VALUE([a].[Contact],'$.Address.Postcode') AS nvarchar(max))
FROM [Authors] AS [a]
WHERE CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley'

Filtre ve projeksiyonda biraz daha fazlasının yanı sıra JSON belgesindeki telefon numarasına göre sipariş veren bir örnek aşağıda verilmişti:

var orderedAddresses = await context.Authors
    .Where(
        author => (author.Contact.Address.City == "Chigley"
                   && author.Contact.Phone != null)
                  || author.Name.StartsWith("D"))
    .OrderBy(author => author.Contact.Phone)
    .Select(
        author => author.Name + " (" + author.Contact.Address.Street
                  + ", " + author.Contact.Address.City
                  + " " + author.Contact.Address.Postcode + ")")
    .ToListAsync();

Bu sorgu aşağıdaki SQL'i oluşturur:

SELECT (((((([a].[Name] + N' (') + CAST(JSON_VALUE([a].[Contact],'$.Address.Street') AS nvarchar(max))) + N', ') + CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max))) + N' ') + CAST(JSON_VALUE([a].[Contact],'$.Address.Postcode') AS nvarchar(max))) + N')'
FROM [Authors] AS [a]
WHERE (CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley' AND CAST(JSON_VALUE([a].[Contact],'$.Phone') AS nvarchar(max)) IS NOT NULL) OR ([a].[Name] LIKE N'D%')
ORDER BY CAST(JSON_VALUE([a].[Contact],'$.Phone') AS nvarchar(max))

JSON belgesi koleksiyonlar içerdiğinde, bunlar sonuçlarda yansıtılabilir:

var postsWithViews = await context.Posts.Where(post => post.Metadata!.Views > 3000)
    .AsNoTracking()
    .Select(
        post => new
        {
            post.Author!.Name, post.Metadata!.Views, Searches = post.Metadata.TopSearches, Commits = post.Metadata.Updates
        })
    .ToListAsync();

Bu sorgu aşağıdaki SQL'i oluşturur:

SELECT [a].[Name], CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int), JSON_QUERY([p].[Metadata],'$.TopSearches'), [p].[Id], JSON_QUERY([p].[Metadata],'$.Updates')
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
WHERE CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int) > 3000

Dekont

JSON koleksiyonları içeren daha karmaşık sorgular için destek gerekir jsonpath . İlgilendiğiniz bir şey varsa Destek jsonpath sorgulaması için oy verin.

Bahşiş

JSON belgelerinde sorgu performansını geliştirmek için dizinler oluşturmayı göz önünde bulundurun. Örneğin, bkz . SQL Server kullanırken Json verilerini dizine ekleme.

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

SaveChanges ve SaveChangesAsync bir JSON sütununda güncelleştirmeler yapmak için normal şekilde çalışın. Kapsamlı değişiklikler için belgenin tamamı güncelleştirilir. Örneğin, bir yazar için belgenin Contact büyük bölümünü değiştirme:

var jeremy = await context.Authors.SingleAsync(author => author.Name.StartsWith("Jeremy"));

jeremy.Contact = new() { Address = new("2 Riverside", "Trimbridge", "TB1 5ZS", "UK"), Phone = "01632 88346" };

await context.SaveChangesAsync();

Bu durumda, yeni belgenin tamamı parametre olarak geçirilir:

info: 8/30/2022 20:21:24.392 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[@p0='{"Phone":"01632 88346","Address":{"City":"Trimbridge","Country":"UK","Postcode":"TB1 5ZS","Street":"2 Riverside"}}' (Nullable = false) (Size = 114), @p1='2'], CommandType='Text', CommandTimeout='30']

Daha sonra SQL'de UPDATE kullanılır:

UPDATE [Authors] SET [Contact] = @p0
OUTPUT 1
WHERE [Id] = @p1;

Ancak, yalnızca bir alt belge değiştirilirse EF Core yalnızca alt belgeyi güncelleştirmek için bir JSON_MODIFY komut kullanır. Örneğin, belgenin Contact içini Address değiştirme:

var brice = await context.Authors.SingleAsync(author => author.Name.StartsWith("Brice"));

brice.Contact.Address = new("4 Riverside", "Trimbridge", "TB1 5ZS", "UK");

await context.SaveChangesAsync();

Aşağıdaki parametreleri oluşturur:

info: 10/2/2022 15:51:15.895 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[@p0='{"City":"Trimbridge","Country":"UK","Postcode":"TB1 5ZS","Street":"4 Riverside"}' (Nullable = false) (Size = 80), @p1='5'], CommandType='Text', CommandTimeout='30']

Bir çağrı aracılığıyla JSON_MODIFY içinde UPDATE kullanılan:

UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address', JSON_QUERY(@p0))
OUTPUT 1
WHERE [Id] = @p1;

Son olarak, yalnızca tek bir özellik değiştirilirse EF Core bu kez yalnızca değiştirilen özellik değerine düzeltme eki uygulamak için bir "JSON_MODIFY" komutu kullanır. Örnek:

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

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

await context.SaveChangesAsync();

Aşağıdaki parametreleri oluşturur:

info: 10/2/2022 15:54:05.112 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (2ms) [Parameters=[@p0='["United Kingdom"]' (Nullable = false) (Size = 18), @p1='4'], CommandType='Text', CommandTimeout='30']

Bu da bir JSON_MODIFYile yeniden kullanılır:

UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address.Country', JSON_VALUE(@p0, '$[0]'))
OUTPUT 1
WHERE [Id] = @p1;

ExecuteUpdate ve ExecuteDelete (Toplu güncelleştirmeler)

Varsayılan olarak, EF Core varlıklardaki değişiklikleri izler ve yöntemlerden biri SaveChanges çağrıldığında veritabanına güncelleştirmeler gönderir. Değişiklikler yalnızca gerçekten değişmiş özellikler ve ilişkiler için gönderilir. Ayrıca, izlenen varlıklar veritabanına gönderilen değişikliklerle eşitlenmiş durumda kalır. Bu mekanizma, veritabanına genel amaçlı eklemeler, güncelleştirmeler ve silmeler göndermenin verimli ve kullanışlı bir yoludur. Bu değişiklikler, veritabanı gidiş dönüş sayısını azaltmak için de toplu olarak oluşturulur.

Ancak bazen değişiklik izleyicisini dahil etmeden veritabanında güncelleştirme veya silme komutları yürütmek yararlı olabilir. EF7, bunu yeni ExecuteUpdate ve ExecuteDelete yöntemleriyle etkinleştirir. Bu yöntemler bir LINQ sorgusuna uygulanır ve veritabanındaki varlıkları bu sorgunun sonuçlarına göre güncelleştirir veya siler. Birçok varlık tek bir komutla güncelleştirilebilir ve varlıklar belleğe yüklenmez, bu da daha verimli güncelleştirmelere ve silmelere neden olabileceği anlamına gelir.

Ancak şunları unutmayın:

  • Yapılacak belirli değişiklikler açıkça belirtilmelidir; EF Core tarafından otomatik olarak algılanmaz.
  • İzlenen varlıklar eşitlenmez.
  • Veritabanı kısıtlamalarını ihlal etmemesi için ek komutların doğru sırada gönderilmesi gerekebilir. Örneğin, bir sorumlu silinemeden önce bağımlıları silme.

Tüm bunlar, ve ExecuteDelete yöntemlerinin ExecuteUpdate mevcut SaveChanges mekanizmayı değiştirmek yerine tamamlayacakları anlamına gelir.

Temel ExecuteDelete örnekler

Bahşiş

Burada gösterilen kod ExecuteDeleteSample.cs dosyasından gelir.

veya ExecuteDeleteExecuteDeleteAsync çağrısı, DbSet veritabanındaki DbSet tüm varlıklarını hemen siler. Örneğin, tüm Tag varlıkları silmek için:

await context.Tags.ExecuteDeleteAsync();

Bu, SQL Server kullanılırken aşağıdaki SQL'i yürütür:

DELETE FROM [t]
FROM [Tags] AS [t]

Daha da ilginç olan sorgu bir filtre içerebilir. Örnek:

await context.Tags.Where(t => t.Text.Contains(".NET")).ExecuteDeleteAsync();

Bu işlem aşağıdaki SQL'i yürütür:

DELETE FROM [t]
FROM [Tags] AS [t]
WHERE [t].[Text] LIKE N'%.NET%'

Sorgu, diğer türlerdeki gezintiler de dahil olmak üzere daha karmaşık filtreler de kullanabilir. Örneğin, etiketleri yalnızca eski blog gönderilerinden silmek için:

await context.Tags.Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022)).ExecuteDeleteAsync();

Yürütülür:

DELETE FROM [t]
FROM [Tags] AS [t]
WHERE NOT EXISTS (
    SELECT 1
    FROM [PostTag] AS [p]
    INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id]
    WHERE [t].[Id] = [p].[TagsId] AND NOT (DATEPART(year, [p0].[PublishedOn]) < 2022))

Temel ExecuteUpdate örnekler

Bahşiş

Burada gösterilen kod ExecuteUpdateSample.cs dosyasından gelir.

ExecuteUpdate ve ExecuteUpdateAsync yöntemlere çok benzer bir şekilde davranın ExecuteDelete . Temel fark, bir güncelleştirmenin hangi özelliklerin güncelleştirildiğini ve bunların nasıl güncelleştirildiğini bilmeyi gerektirmesidir. Bu, öğesine SetPropertyyapılan bir veya daha fazla çağrı kullanılarak elde edilir. Örneğin, her blogun Name güncelleştirmesini yapmak için:

await context.Blogs.ExecuteUpdateAsync(
    s => s.SetProperty(b => b.Name, b => b.Name + " *Featured!*"));

öğesinin ilk parametresi hangi özelliğin SetProperty güncelleştirilecek olduğunu belirtir; bu durumda, Blog.Name. İkinci parametre, yeni değerin nasıl hesaplanması gerektiğini belirtir; bu durumda, mevcut değeri alıp ekleyerek "*Featured!*". Sonuçta elde edilen SQL şöyledir:

UPDATE [b]
SET [b].[Name] = [b].[Name] + N' *Featured!*'
FROM [Blogs] AS [b]

gibi ExecuteDeletesorgu da güncelleştirilen varlıkları filtrelemek için kullanılabilir. Ayrıca, hedef varlıkta birden fazla özelliği güncelleştirmek için birden çok çağrısı SetProperty kullanılabilir. Örneğin, Title 2022'ye kadar yayımlanan tüm gönderilerin ve Content güncelleştirmeleri için:

await context.Posts
    .Where(p => p.PublishedOn.Year < 2022)
    .ExecuteUpdateAsync(s => s
        .SetProperty(b => b.Title, b => b.Title + " (" + b.PublishedOn.Year + ")")
        .SetProperty(b => b.Content, b => b.Content + " ( This content was published in " + b.PublishedOn.Year + ")"));

Bu durumda oluşturulan SQL biraz daha karmaşıktır:

UPDATE [p]
SET [p].[Content] = (([p].[Content] + N' ( This content was published in ') + COALESCE(CAST(DATEPART(year, [p].[PublishedOn]) AS nvarchar(max)), N'')) + N')',
    [p].[Title] = (([p].[Title] + N' (') + COALESCE(CAST(DATEPART(year, [p].[PublishedOn]) AS nvarchar(max)), N'')) + N')'
FROM [Posts] AS [p]
WHERE DATEPART(year, [p].[PublishedOn]) < 2022

Son olarak, yine ile ExecuteDeleteolduğu gibi filtre diğer tablolara başvurabilir. Örneğin, eski gönderilerden tüm etiketleri güncelleştirmek için:

await context.Tags
    .Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022))
    .ExecuteUpdateAsync(s => s.SetProperty(t => t.Text, t => t.Text + " (old)"));

Oluşturan:

UPDATE [t]
SET [t].[Text] = [t].[Text] + N' (old)'
FROM [Tags] AS [t]
WHERE NOT EXISTS (
    SELECT 1
    FROM [PostTag] AS [p]
    INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id]
    WHERE [t].[Id] = [p].[TagsId] AND NOT (DATEPART(year, [p0].[PublishedOn]) < 2022))

ve üzerinde ExecuteUpdate daha fazla bilgi ve kod örnekleri için bkz. ExecuteUpdate ve ExecuteDelete.ExecuteDelete

Devralma ve birden çok tablo

ExecuteUpdate ve ExecuteDelete yalnızca tek bir tabloda işlem yapabilir. Bunun, farklı devralma eşleme stratejileriyle çalışırken etkileri vardır. Genellikle, değiştirebileceğiniz tek bir tablo olduğundan, TPH eşleme stratejisi kullanılırken herhangi bir sorun olmaz. Örneğin, tüm FeaturedPost varlıkları silme:

await context.Set<FeaturedPost>().ExecuteDeleteAsync();

TPH eşlemesi kullanılırken aşağıdaki SQL'i oluşturur:

DELETE FROM [p]
FROM [Posts] AS [p]
WHERE [p].[Discriminator] = N'FeaturedPost'

TPC eşleme stratejisi kullanılırken de bu durumla ilgili herhangi bir sorun yoktur, çünkü tek bir tabloda yalnızca değişiklik yapılması gerekir:

DELETE FROM [f]
FROM [FeaturedPosts] AS [f]

Ancak, TPT eşleme stratejisi kullanılırken bu girişim başarısız olur çünkü iki farklı tablodan satırların silinmesini gerektirir.

Sorguya filtre eklemek genellikle işlemin hem TPC hem de TPT stratejileriyle başarısız olacağı anlamına gelir. Bunun nedeni, satırların birden çok tablodan silinmesi gerekebileceğidir. Örneğin, bu sorgu:

await context.Posts.Where(p => p.Author!.Name.StartsWith("Arthur")).ExecuteDeleteAsync();

TPH kullanırken aşağıdaki SQL'i oluşturur:

DELETE FROM [p]
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
WHERE [a].[Name] IS NOT NULL AND ([a].[Name] LIKE N'Arthur%')

Ancak TPC veya TPT kullanılırken başarısız olur.

Bahşiş

Sorun #10879 , bu senaryolarda birden çok komutu otomatik olarak göndermek için destek eklemeyi izler. Uygulandığını görmek istediğiniz bir sorunsa bu sorun için oy verin.

ExecuteDelete ve ilişkiler

Yukarıda belirtildiği gibi, bir ilişkinin sorumlusunun silinebilmesi için önce bağımlı varlıkların silinmesi veya güncelleştirilmesi gerekebilir. Örneğin, her Post biri ilişkili Authoröğesine bağımlıdır. Bu, bir gönderi hala başvuruda bulunsa yazarın silinemeyeceği anlamına gelir; bunu yaptığınızda veritabanındaki yabancı anahtar kısıtlaması ihlal edilecek. Örneğin, bunu deneme:

await context.Authors.ExecuteDeleteAsync();

SQL Server'da aşağıdaki özel durumla sonuçlanır:

Microsoft.Data.SqlClient.SqlException (0x80131904): DELETE deyimi "FK_Posts_Authors_AuthorId" BAŞVURU kısıtlamasıyla çakıştı. Çakışma "TphBlogsContext" veritabanında, "dbo" tablosunda oluştu. Postlar", 'AuthorId' sütunu. Deyim sonlandırıldı.

Bunu düzeltmek için önce gönderileri silmemiz veya yabancı anahtar özelliğini null olarak ayarlayarak AuthorId her gönderi ile yazarı arasındaki ilişkiyi kesmemiz gerekir. Örneğin, silme seçeneğini kullanarak:

await context.Posts.TagWith("Deleting posts...").ExecuteDeleteAsync();
await context.Authors.TagWith("Deleting authors...").ExecuteDeleteAsync();

Bahşiş

TagWith normal sorguları etiketlemek ExecuteDelete için veya ExecuteUpdate aynı şekilde kullanılabilir.

Bu, iki ayrı komutla sonuç verir; bağımlıları ilk silecek olan:

-- Deleting posts...

DELETE FROM [p]
FROM [Posts] AS [p]

İkinci olarak da sorumluları silin:

-- Deleting authors...

DELETE FROM [a]
FROM [Authors] AS [a]

Önemli

Birden çok ExecuteDelete ve ExecuteUpdate komutu varsayılan olarak tek bir işlemde yer almayacaktır. Ancak DbContext işlem API'leri , bu komutları bir işlemde sarmalamanın normal yöntemiyle kullanılabilir.

Bahşiş

Bu komutları tek bir gidiş dönüşte göndermek Sorun #10879'a bağlıdır. Uygulandığını görmek istediğiniz bir sorunsa bu sorun için oy verin.

Veritabanında art arda silmeleri yapılandırmak burada çok yararlı olabilir. Modelimizde ve arasındaki BlogPost ilişki gereklidir ve bu da EF Core'un kurala göre art arda silme yapılandırmasına neden olur. Bu, bir blog veritabanından silindiğinde tüm bağımlı gönderilerinin de silineceği anlamına gelir. Ardından tüm blogları ve gönderileri silmek için yalnızca blogları silmemiz gerekir:

await context.Blogs.ExecuteDeleteAsync();

Bu, aşağıdaki SQL'e neden olur:

DELETE FROM [b]
FROM [Blogs] AS [b]

Bu, bir blogu silerken, yapılandırılan art arda silme işlemiyle ilgili tüm gönderilerin silinmesine de neden olur.

Daha Hızlı SaveChanges

EF7'de ve SaveChangesAsync performansı SaveChanges önemli ölçüde geliştirilmiştir. Bazı senaryolarda, değişiklikleri kaydetmek artık EF Core 6.0'dan dört kat daha hızlıdır!

Bu iyileştirmelerin çoğu şunlardan gelir:

  • Veritabanına daha az gidiş dönüş gerçekleştirme
  • Daha hızlı SQL oluşturma

Bu iyileştirmelerin bazı örnekleri aşağıda gösterilmiştir.

Dekont

Bu değişikliklerin ayrıntılı bir şekilde tartışılması için .NET Blogu'ndaki Entity Framework Core 7 Preview 6: Performance Edition Duyuruları bölümüne bakın.

Bahşiş

Burada gösterilen kod SaveChangesPerformanceSample.cs dosyasından gelir.

Gereksiz işlemler ortadan kaldırılıyor

Tüm modern ilişkisel veritabanları, tek sql deyimlerinin (çoğu) işlemselliğini garanti eder. Yani, bir hata oluşsa bile deyimi hiçbir zaman yalnızca kısmen tamamlanmaz. EF7, bu durumlarda açık bir işlem başlatmaktan kaçınıyor.

Örneğin, aşağıdaki çağrısı için günlüğe SaveChangesbakarak:

await context.AddAsync(new Blog { Name = "MyBlog" });
await context.SaveChangesAsync();

EF Core 6.0'da komutun INSERT bir işlemi başlatmak ve işlemek için komutlar tarafından sarmalandığını gösterir:

dbug: 9/29/2022 11:43:09.196 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 9/29/2022 11:43:09.265 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (27ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Name])
      VALUES (@p0);
      SELECT [Id]
      FROM [Blogs]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
dbug: 9/29/2022 11:43:09.297 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

EF7, işlemin burada gerekli olmadığını algılar ve bu nedenle şu çağrıları kaldırır:

info: 9/29/2022 11:42:34.776 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (25ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Name])
      OUTPUT INSERTED.[Id]
      VALUES (@p0);

Bu, özellikle veritabanına yapılan çağrıların gecikme süresi yüksek olduğunda genel performansta büyük fark yaratabilecek iki veritabanı gidiş dönüşlerini kaldırır. Tipik üretim sistemlerinde veritabanı, uygulamayla aynı makinede birlikte bulunmaz. Bu da gecikme süresinin genellikle nispeten yüksek olduğu ve bu iyileştirmenin özellikle gerçek dünyadaki üretim sistemlerinde etkili olduğu anlamına gelir.

Basit Kimlik ekleme için geliştirilmiş SQL

Yukarıdaki örnekte anahtar sütunu olan ve veritabanı tarafından oluşturulan başka değer içermeyen tek bir IDENTITY satır eklenir. EF7, kullanarak OUTPUT INSERTEDBU durumda SQL'i basitleştirir. Bu basitleştirme diğer birçok durum için geçerli olmasa da, bu tür tek satırlı ekleme birçok uygulamada çok yaygın olduğu için geliştirmek yine de önemlidir.

Birden çok satır ekleme

EF Core 6.0'da, birden çok satır eklemeye yönelik varsayılan yaklaşım, tetikleyicileri olan tablolar için SQL Server desteğindeki sınırlamalar tarafından yönlendirildi. Tablolarında tetikleyicileri olan kullanıcıların azınlığı için bile varsayılan deneyimin çalıştığından emin olmak istedik. Bu, SQL Server'da tetikleyicilerle çalışmadığından basit OUTPUT bir yan tümce kullanamadığımız anlamına geliyordu. Bunun yerine, birden çok varlık eklerken EF Core 6.0 bazı oldukça karışık SQL oluşturdu. Örneğin, bu çağrısı:SaveChanges

for (var i = 0; i < 4; i++)
{
    await context.AddAsync(new Blog { Name = "Foo" + i });
}

await context.SaveChangesAsync();

EF Core 6.0 ile SQL Server'da çalıştırıldığında aşağıdaki eylemlerle sonuçılır:

dbug: 9/30/2022 17:19:51.919 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 9/30/2022 17:19:51.993 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (27ms) [Parameters=[@p0='Foo0' (Nullable = false) (Size = 4000), @p1='Foo1' (Nullable = false) (Size = 4000), @p2='Foo2' (Nullable = false) (Size = 4000), @p3='Foo3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]);
      MERGE [Blogs] USING (
      VALUES (@p0, 0),
      (@p1, 1),
      (@p2, 2),
      (@p3, 3)) AS i ([Name], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([Name])
      VALUES (i.[Name])
      OUTPUT INSERTED.[Id], i._Position
      INTO @inserted0;

      SELECT [i].[Id] FROM @inserted0 i
      ORDER BY [i].[_Position];
dbug: 9/30/2022 17:19:52.023 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

Önemli

Bu karmaşık olsa da, bunun gibi birden çok eklemeyi toplu olarak çalıştırmak, her ekleme için tek bir komut göndermekten önemli ölçüde daha hızlıdır.

EF7'de, tablolarınız tetikleyici içeriyorsa bu SQL'i almaya devam edebilirsiniz, ancak genel durum için artık biraz karmaşıksa çok daha verimli komutlar oluşturuyoruz:

info: 9/30/2022 17:40:37.612 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (4ms) [Parameters=[@p0='Foo0' (Nullable = false) (Size = 4000), @p1='Foo1' (Nullable = false) (Size = 4000), @p2='Foo2' (Nullable = false) (Size = 4000), @p3='Foo3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      MERGE [Blogs] USING (
      VALUES (@p0, 0),
      (@p1, 1),
      (@p2, 2),
      (@p3, 3)) AS i ([Name], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([Name])
      VALUES (i.[Name])
      OUTPUT INSERTED.[Id], i._Position;

Tek bir ekleme durumunda olduğu gibi işlem, örtük bir işlem tarafından korunan tek bir deyim olduğundan MERGE yok olur. Ayrıca geçici tablo gitti ve OUTPUT yan tümcesi artık oluşturulan kimlikleri doğrudan istemciye geri gönderiyor. Bu, uygulama ve veritabanı arasındaki gecikme süresi gibi çevresel faktörlere bağlı olarak EF Core 6.0'dan dört kat daha hızlı olabilir.

Tetikleyiciler

Tabloda tetikleyiciler varsa yukarıdaki SaveChanges kodda çağrısı bir özel durum oluşturur:

İşlenmeyen özel durum. Microsoft.EntityFrameworkCore.DbUpdateException:
Hedef tabloda veritabanı tetikleyicileri olduğundan değişiklikler kaydedilemedi. Lütfen varlık türünüzü uygun şekilde yapılandırın, daha fazla bilgi için bkz https://aka.ms/efcore-docs-sqlserver-save-changes-and-triggers .
>--- Microsoft.Data.SqlClient.SqlException (0x80131904):
DML deyiminin 'BlogsWithTriggers' hedef tablosunda, deyimi INTO yan tümcesi olmayan bir OUTPUT yan tümcesi içeriyorsa etkin tetikleyici olamaz.

Aşağıdaki kod, EF Core'a tablonun tetikleyicisi olduğunu bildirmek için kullanılabilir:

modelBuilder
    .Entity<BlogWithTrigger>()
    .ToTable(tb => tb.HasTrigger("TRG_InsertUpdateBlog"));

EF7, bu tablo için ekleme ve güncelleştirme komutları gönderirken EF Core 6.0 SQL'e geri döner.

Tüm eşlenen tabloları tetikleyicilerle otomatik olarak yapılandırma kuralı da dahil olmak üzere daha fazla bilgi için EF7 hataya neden olan değişiklikler belgelerinde tetikleyicileri olan SQL Server tabloları artık özel EF Core yapılandırması gerektiriyor bölümüne bakın.

Grafik eklemek için daha az gidiş dönüş

Yeni bir asıl varlık ve yeni sorumluya başvuran yabancı anahtarlara sahip yeni bağımlı varlıklar içeren varlıkların grafiğini eklemeyi göz önünde bulundurun. Örnek:

await context.AddAsync(
    new Blog { Name = "MyBlog", Posts = { new() { Title = "My first post" }, new() { Title = "My second post" } } });
await context.SaveChangesAsync();

Sorumlunun birincil anahtarı veritabanı tarafından oluşturulursa, sorumlu eklenene kadar bağımlıdaki yabancı anahtar için ayarlanacağı değer bilinmez. EF Core bunun için iki gidiş dönüş oluşturur: biri sorumluyu eklemek ve yeni birincil anahtarı geri almak için, diğeri ise yabancı anahtar değeri kümesine sahip bağımlıları eklemek için. Bunun için iki deyim olduğundan bir işlem gereklidir, yani toplam dört gidiş dönüş vardır:

dbug: 10/1/2022 13:12:02.517 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 10/1/2022 13:12:02.517 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Name])
      OUTPUT INSERTED.[Id]
      VALUES (@p0);
info: 10/1/2022 13:12:02.529 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (5ms) [Parameters=[@p1='6', @p2='My first post' (Nullable = false) (Size = 4000), @p3='6', @p4='My second post' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      MERGE [Post] USING (
      VALUES (@p1, @p2, 0),
      (@p3, @p4, 1)) AS i ([BlogId], [Title], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([BlogId], [Title])
      VALUES (i.[BlogId], i.[Title])
      OUTPUT INSERTED.[Id], i._Position;
dbug: 10/1/2022 13:12:02.531 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

Ancak, bazı durumlarda, birincil anahtar değeri sorumlu eklenmeden önce bilinir. Buna aşağıdakiler dahildir:

  • Otomatik olarak oluşturulmayan anahtar değerleri
  • İstemcide oluşturulan anahtar değerleri( anahtarlar gibi Guid )
  • Bir hi-lo değer oluşturucu kullanılırken olduğu gibi sunucuda toplu olarak oluşturulan anahtar değerleri

EF7'de bu durumlar artık tek gidiş dönüş olarak iyileştirilmiştir. Örneğin, SQL Server'da yukarıdaki örnekte birincil Blog.Id anahtar hi-lo oluşturma stratejisini kullanacak şekilde yapılandırılabilir:

modelBuilder.Entity<Blog>().Property(e => e.Id).UseHiLo();
modelBuilder.Entity<Post>().Property(e => e.Id).UseHiLo();

SaveChanges Yukarıdaki çağrı artık eklemeler için tek bir gidiş dönüş için iyileştirilmiştir.

dbug: 10/1/2022 21:51:55.805 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Began transaction with isolation level 'ReadCommitted'.
info: 10/1/2022 21:51:55.806 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='9', @p1='MyBlog' (Nullable = false) (Size = 4000), @p2='10', @p3='9', @p4='My first post' (Nullable = false) (Size = 4000), @p5='11', @p6='9', @p7='My second post' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Blogs] ([Id], [Name])
      VALUES (@p0, @p1);
      INSERT INTO [Posts] ([Id], [BlogId], [Title])
      VALUES (@p2, @p3, @p4),
      (@p5, @p6, @p7);
dbug: 10/1/2022 21:51:55.807 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction)
      Committed transaction.

Burada bir işlemin hala gerekli olduğuna dikkat edin. Bunun nedeni, eklemelerin iki ayrı tabloya dönüştürülmesidir.

EF7, EF Core 6.0'ın birden fazla oluşturacağı diğer durumlarda da tek bir toplu iş kullanır. Örneğin, aynı tabloya satır silerken ve eklerken.

SaveChanges değeri

Buradaki örneklerden bazılarının gösterdiği gibi, sonuçları veritabanına kaydetmek karmaşık bir iş olabilir. EF Core gibi bir şeyin kullanılması değerinin gerçekten gösterildiği yerdir. EF Core:

  • Gidiş dönüşleri azaltmak için birden çok ekleme, güncelleştirme ve silme komutlarını birlikte toplu olarak işler
  • Açık bir işlem gerekip gerekmediğini anlar
  • Veritabanı kısıtlamalarının ihlal edilmemesi için varlıkların hangi sırada ekleneceğini, güncelleştirileceğini ve silineceğini belirler
  • Veritabanı tarafından oluşturulan değerlerin verimli bir şekilde döndürülmesini ve varlıklara geri yayılmasını sağlar
  • Birincil anahtarlar için oluşturulan değerleri kullanarak yabancı anahtar değerlerini otomatik olarak ayarlar
  • Eşzamanlılık çakışmalarını algılama

Buna ek olarak, farklı veritabanı sistemleri bu durumların çoğu için farklı SQL gerektirir. EF Core veritabanı sağlayıcısı, her olay için doğru ve verimli komutların gönderilmesini sağlamak için EF Core ile birlikte çalışır.

Somut tür başına tablo (TPC) devralma eşlemesi

VARSAYıLAN olarak EF Core, .NET türlerinin devralma hiyerarşisini tek bir veritabanı tablosuyla eşler. Bu, hiyerarşi başına tablo (TPH) eşleme stratejisi olarak bilinir. EF Core 5.0, her .NET türünü farklı bir veritabanı tablosuna eşlemeyi destekleyen tür başına tablo (TPT) stratejisini kullanıma sunar. EF7, beton başına tablo türü (TPC) stratejisini tanıtır. TPC ayrıca .NET türlerini farklı tablolarla eşler, ancak TPT stratejisiyle ilgili bazı yaygın performans sorunlarını giderecek şekilde.

Bahşiş

Burada gösterilen kod TpcInheritanceSample.cs dosyasından gelir.

Bahşiş

EF Ekibi, .NET Veri Topluluğu Standup'ın bir bölümünde TPC eşlemesi hakkında ayrıntılı bir şekilde bilgi verdi ve konuştu. Tüm Topluluk Standup bölümlerinde olduğu gibi, TPC bölümünü artık YouTube'da izleyebilirsiniz.

TPC veritabanı şeması

TPC stratejisi TPT stratejisine benzer, ancak hiyerarşideki her somut tür için farklı bir tablo oluşturulur, ancak tablolar soyut türler için oluşturulmaz; bu nedenle "somut tür başına tablo" adıdır. TPT'de olduğu gibi, tablonun kendisi kaydedilen nesnenin türünü gösterir. Ancak, TPT eşlemesinin aksine, her tablo somut türdeki ve temel türlerindeki her özelliğin sütunlarını içerir. TPC veritabanı şemaları normal dışıdır.

Örneğin, şu hiyerarşiyi eşlemeyi göz önünde bulundurun:

public abstract class Animal
{
    protected Animal(string name)
    {
        Name = name;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public abstract string Species { get; }

    public Food? Food { get; set; }
}

public abstract class Pet : Animal
{
    protected Pet(string name)
        : base(name)
    {
    }

    public string? Vet { get; set; }

    public ICollection<Human> Humans { get; } = new List<Human>();
}

public class FarmAnimal : Animal
{
    public FarmAnimal(string name, string species)
        : base(name)
    {
        Species = species;
    }

    public override string Species { get; }

    [Precision(18, 2)]
    public decimal Value { get; set; }

    public override string ToString()
        => $"Farm animal '{Name}' ({Species}/{Id}) worth {Value:C} eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Cat : Pet
{
    public Cat(string name, string educationLevel)
        : base(name)
    {
        EducationLevel = educationLevel;
    }

    public string EducationLevel { get; set; }
    public override string Species => "Felis catus";

    public override string ToString()
        => $"Cat '{Name}' ({Species}/{Id}) with education '{EducationLevel}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Dog : Pet
{
    public Dog(string name, string favoriteToy)
        : base(name)
    {
        FavoriteToy = favoriteToy;
    }

    public string FavoriteToy { get; set; }
    public override string Species => "Canis familiaris";

    public override string ToString()
        => $"Dog '{Name}' ({Species}/{Id}) with favorite toy '{FavoriteToy}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Human : Animal
{
    public Human(string name)
        : base(name)
    {
    }

    public override string Species => "Homo sapiens";

    public Animal? FavoriteAnimal { get; set; }
    public ICollection<Pet> Pets { get; } = new List<Pet>();

    public override string ToString()
        => $"Human '{Name}' ({Species}/{Id}) with favorite animal '{FavoriteAnimal?.Name ?? "<Unknown>"}'" +
           $" eats {Food?.ToString() ?? "<Unknown>"}";
}

SQL Server kullanılırken, bu hiyerarşi için oluşturulan tablolar şunlardır:

CREATE TABLE [Cats] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Vet] nvarchar(max) NULL,
    [EducationLevel] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]));

CREATE TABLE [Dogs] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Vet] nvarchar(max) NULL,
    [FavoriteToy] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]));

CREATE TABLE [FarmAnimals] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Value] decimal(18,2) NOT NULL,
    [Species] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]));

CREATE TABLE [Humans] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [FavoriteAnimalId] int NULL,
    CONSTRAINT [PK_Humans] PRIMARY KEY ([Id]));

Şu noktalara dikkat edin:

  • Nesne modelinde Animal olduğundan veya Pet türleri abstract için tablo yoktur. C# öğesinin soyut türlerin örneklerine izin vermediğini ve bu nedenle soyut tür örneğinin veritabanına kaydedileceği bir durum olmadığını unutmayın.

  • Temel türlerdeki özelliklerin eşlemesi her beton türü için yinelenir. Örneğin, her tablonun bir Name sütunu vardır ve hem Cats hem de Dogs'un bir Vet sütunu vardır.

  • Bazı verilerin bu veritabanına kaydedilmesi aşağıdaki sonuçlara neden olur:

Cats tablosu

Id Ad FoodId Veteriner EducationLevel
Kategori 1 Alice 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly MBA
2 Mac 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly Okul öncesi
8 Baxter 5dc5019e-6f72-454b-d4b0-08da7aca624f Bothell Evcil Hayvan Hastanesi Bsc

Köpekler masası

Id Ad FoodId Veteriner FavoriteToy
3 Tost 011aaf6f-d588-4fad-d4ac-08da7aca624f Pengelly Bay Sincap

FarmAnimals tablosu

Id Ad FoodId Değer Türler
4 Clyde 1d495075-f527-4498-d4af-08da7aca624f 100.00 Equus africanus asinus

İnsanlar tablosu

Id Ad FoodId FavoriteAnimalId
5 Wendy 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 Arthur 59b495d4-0414-46bf-d4ad-08da7aca624f Kategori 1
9 Katie boş 8

TPT eşlemesinin aksine, tek bir nesnenin tüm bilgilerinin tek bir tabloda yer aldığına dikkat edin. Ayrıca, TPH eşlemesinin aksine, model tarafından hiçbir zaman kullanılmayan herhangi bir tabloda sütun ve satır birleşimi yoktur. Aşağıda bu özelliklerin sorgular ve depolama için nasıl önemli olabileceğini göreceğiz.

TPC devralmayı yapılandırma

Devralma hiyerarşisindeki tüm türler, hiyerarşiYI EF Core ile eşlerken modele açıkça dahil edilmelidir. Bu, her tür için cihazınızda DbContext özellikler oluşturularak DbSet yapılabilir:

public DbSet<Animal> Animals => Set<Animal>();
public DbSet<Pet> Pets => Set<Pet>();
public DbSet<FarmAnimal> FarmAnimals => Set<FarmAnimal>();
public DbSet<Cat> Cats => Set<Cat>();
public DbSet<Dog> Dogs => Set<Dog>();
public DbSet<Human> Humans => Set<Human>();

Veya içindeki yöntemini kullanarakEntity:OnModelCreating

modelBuilder.Entity<Animal>();
modelBuilder.Entity<Pet>();
modelBuilder.Entity<Cat>();
modelBuilder.Entity<Dog>();
modelBuilder.Entity<FarmAnimal>();
modelBuilder.Entity<Human>();

Önemli

Bu, türetilmiş eşlenmiş temel tür türlerinin aynı derlemede yer alıyorsa otomatik olarak bulunabileceği eski EF6 davranışından farklıdır.

Varsayılan strateji olduğundan hiyerarşiyi TPH olarak eşlemek için başka hiçbir şey yapılması gerekmez. Ancak EF7'den başlayarak TPH, hiyerarşinin temel türüne çağrılarak UseTphMappingStrategy açık hale getirilebilir:

modelBuilder.Entity<Animal>().UseTphMappingStrategy();

Bunun yerine TPT kullanmak için bunu olarak UseTptMappingStrategydeğiştirin:

modelBuilder.Entity<Animal>().UseTptMappingStrategy();

Benzer şekilde, UseTpcMappingStrategy TPC'yi yapılandırmak için kullanılır:

modelBuilder.Entity<Animal>().UseTpcMappingStrategy();

Her durumda, her tür için kullanılan tablo adı , üzerindeki özellik adından DbSet alınır veya oluşturucu yöntemi veya [Table] özniteliği kullanılarak ToTable yapılandırılabilir.DbContext

TPC sorgu performansı

Sorgular için TPC stratejisi, belirli bir varlık örneğine ilişkin bilgilerin her zaman tek bir tabloda depolanmasını sağladığından TPT'ye göre bir geliştirmedir. Bu, eşlenen hiyerarşi büyük olduğunda ve her biri çok sayıda özelliğe sahip ve çoğu sorguda yalnızca küçük bir tür alt kümesinin kullanıldığı birçok somut (genellikle yaprak) türe sahip olduğunda TPC stratejisinin yararlı olabileceği anlamına gelir.

Üç basit LINQ sorgusu için oluşturulan SQL, TPH ve TPT ile karşılaştırıldığında TPC'nin nerede iyi çalıştığını gözlemlemek için kullanılabilir. Bu sorgular şunlardır:

  1. Hiyerarşideki tüm türlerin varlıklarını döndüren sorgu:

    context.Animals.ToList();
    
  2. Hiyerarşideki türlerin bir alt kümesindeki varlıkları döndüren sorgu:

    context.Pets.ToList();
    
  3. Hiyerarşideki tek bir yaprak türünden yalnızca varlıkları döndüren sorgu:

    context.Cats.ToList();
    

TPH sorguları

TPH kullanırken, üç sorgu da yalnızca tek bir tabloyu sorgular, ancak ayrımcı sütununda farklı filtreler vardır:

  1. Hiyerarşideki tüm türlerin varlıklarını döndüren TPH SQL:

    SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Species], [a].[Value], [a].[FavoriteAnimalId], [a].[Vet], [a].[EducationLevel], [a].[FavoriteToy]
    FROM [Animals] AS [a]
    
  2. Hiyerarşideki türlerin bir alt kümesinden varlıkları döndüren TPH SQL:

    SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Vet], [a].[EducationLevel], [a].[FavoriteToy]
    FROM [Animals] AS [a]
    WHERE [a].[Discriminator] IN (N'Cat', N'Dog')
    
  3. TPH SQL, hiyerarşideki tek bir yaprak türünden yalnızca varlıkları döndürmektedir:

    SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Vet], [a].[EducationLevel]
    FROM [Animals] AS [a]
    WHERE [a].[Discriminator] = N'Cat'
    

Tüm bu sorgular, özellikle ayırıcı sütununda uygun bir veritabanı diziniyle iyi performans göstermelidir.

TPT sorguları

TPT kullanılırken, belirli bir somut türe ait veriler birçok tabloya bölündüğünden, bu sorguların tümü birden çok tabloyu birleştirmeyi gerektirir:

  1. Hiyerarşideki tüm türlerin varlıklarını döndüren TPT SQL:

    SELECT [a].[Id], [a].[FoodId], [a].[Name], [f].[Species], [f].[Value], [h].[FavoriteAnimalId], [p].[Vet], [c].[EducationLevel], [d].[FavoriteToy], CASE
        WHEN [d].[Id] IS NOT NULL THEN N'Dog'
        WHEN [c].[Id] IS NOT NULL THEN N'Cat'
        WHEN [h].[Id] IS NOT NULL THEN N'Human'
        WHEN [f].[Id] IS NOT NULL THEN N'FarmAnimal'
    END AS [Discriminator]
    FROM [Animals] AS [a]
    LEFT JOIN [FarmAnimals] AS [f] ON [a].[Id] = [f].[Id]
    LEFT JOIN [Humans] AS [h] ON [a].[Id] = [h].[Id]
    LEFT JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
    LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
    LEFT JOIN [Dogs] AS [d] ON [a].[Id] = [d].[Id]
    
  2. Hiyerarşideki türlerin bir alt kümesinden varlıkları döndüren TPT SQL:

    SELECT [a].[Id], [a].[FoodId], [a].[Name], [p].[Vet], [c].[EducationLevel], [d].[FavoriteToy], CASE
        WHEN [d].[Id] IS NOT NULL THEN N'Dog'
        WHEN [c].[Id] IS NOT NULL THEN N'Cat'
    END AS [Discriminator]
    FROM [Animals] AS [a]
    INNER JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
    LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
    LEFT JOIN [Dogs] AS [d] ON [a].[Id] = [d].[Id]
    
  3. TPT SQL, hiyerarşideki tek bir yaprak türünden yalnızca varlıkları döndürmektedir:

    SELECT [a].[Id], [a].[FoodId], [a].[Name], [p].[Vet], [c].[EducationLevel]
    FROM [Animals] AS [a]
    INNER JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id]
    INNER JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id]
    

Dekont

EF Core, verilerin hangi tablodan geldiğini ve bu nedenle kullanılacak doğru türü belirlemek için "ayrıştırıcı sentezi" kullanır. Bunun nedeni, LEFT JOIN'in doğru tür olmayan bağımlı kimlik sütunu ("alt tablolar") için null değer döndürmesidir. Bu nedenle bir [d].[Id] köpek için null olmayan ve diğer tüm (somut) kimlikler null olacaktır.

Bu sorguların tümü, tablo birleştirmeleri nedeniyle performans sorunlarıyla karşılaşabilir. Bu nedenle TPT, sorgu performansı için hiçbir zaman iyi bir seçim değildir.

TPC sorguları

Sorgulanması gereken tablo sayısı azaldığından, TPC tüm bu sorgular için TPT'ye göre geliştirilir. Buna ek olarak, her tablodan elde edilen sonuçlar kullanılarak UNION ALLbirleştirilir. Bu, satırlar arasında herhangi bir eşleştirme gerçekleştirmesi veya satırların yinelenenleri kaldırması gerekmediğinden, tablo birleşiminden çok daha hızlı olabilir.

  1. TPC SQL hiyerarşideki tüm türlerin varlıklarını döndüren:

    SELECT [f].[Id], [f].[FoodId], [f].[Name], [f].[Species], [f].[Value], NULL AS [FavoriteAnimalId], NULL AS [Vet], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'FarmAnimal' AS [Discriminator]
    FROM [FarmAnimals] AS [f]
    UNION ALL
    SELECT [h].[Id], [h].[FoodId], [h].[Name], NULL AS [Species], NULL AS [Value], [h].[FavoriteAnimalId], NULL AS [Vet], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'Human' AS [Discriminator]
    FROM [Humans] AS [h]
    UNION ALL
    SELECT [c].[Id], [c].[FoodId], [c].[Name], NULL AS [Species], NULL AS [Value], NULL AS [FavoriteAnimalId], [c].[Vet], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
    FROM [Cats] AS [c]
    UNION ALL
    SELECT [d].[Id], [d].[FoodId], [d].[Name], NULL AS [Species], NULL AS [Value], NULL AS [FavoriteAnimalId], [d].[Vet], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
    FROM [Dogs] AS [d]
    
  2. TPC SQL, hiyerarşideki türlerin bir alt kümesinden varlık döndüren:

    SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator]
    FROM [Cats] AS [c]
    UNION ALL
    SELECT [d].[Id], [d].[FoodId], [d].[Name], [d].[Vet], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator]
    FROM [Dogs] AS [d]
    
  3. TPC SQL, hiyerarşideki tek bir yaprak türünden yalnızca varlıkları döndürmektedir:

    SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel]
    FROM [Cats] AS [c]
    

TPC, bu sorguların tümü için TPT'den daha iyi olsa da, birden çok türün örnekleri döndürülürken TPH sorguları hala daha iyidir. Bu, TPH'nin EF Core tarafından kullanılan varsayılan strateji olmasının nedenlerinden biridir.

3. sorgu için SQL'de gösterildiği gibi, TPC tek bir yaprak türündeki varlıkları sorgularken gerçekten çok başarılı olur. Sorgu yalnızca tek bir tablo kullanır ve filtreleme gerektirmez.

TPC eklemeleri ve güncelleştirmeleri

TPC, yeni bir varlığı kaydederken de iyi performans gösterir, çünkü bunun için tek bir tabloya yalnızca tek bir satır eklenmesi gerekir. Bu, TPH için de geçerlidir. TPT ile, daha az iyi performans gösteren birçok tabloya satır eklenmelidir.

Aynı durum genellikle güncelleştirmeler için de geçerlidir, ancak bu durumda güncelleştirilen tüm sütunlar TPT için bile aynı tablodaysa fark önemli olmayabilir.

Alanla ilgili dikkat edilmesi gerekenler

Hem TPT hem de TPC, genellikle kullanılmayan birçok özelliğe sahip çok sayıda alt tür olduğunda TPH'den daha az depolama alanı kullanabilir. Bunun nedeni, TPH tablosundaki her satırın bu kullanılmayan özelliklerin her biri için bir NULL depolaması gerekir. Pratikte, bu nadiren bir sorundur, ancak bu özelliklere sahip büyük miktarda veriyi depolarken göz önünde bulundurmaya değer.

Bahşiş

Veritabanı sisteminiz destekliyorsa (örneğin SQL Server), nadiren doldurulacak TPH sütunları için "seyrek sütunlar" kullanmayı göz önünde bulundurun.

Anahtar oluşturma

Seçilen devralma eşleme stratejisi, birincil anahtar değerlerinin nasıl oluşturulup yönetildiğinin sonuçlarına neden olur. TPH'deki anahtarlar kolaydır çünkü her varlık örneği tek bir tabloda tek bir satırla temsil edilir. Herhangi bir tür anahtar değeri oluşturma kullanılabilir ve ek kısıtlama gerekmez.

TPT stratejisi için, tabloda her zaman hiyerarşinin temel türüne eşlenmiş bir satır vardır. Bu satırda herhangi bir tür anahtar oluşturma kullanılabilir ve diğer tabloların anahtarları yabancı anahtar kısıtlamaları kullanılarak bu tabloya bağlanır.

TPC için işler biraz daha karmaşık hale geliyor. İlk olarak, EF Core'un, varlıkların farklı türleri olsa bile bir hiyerarşideki tüm varlıkların benzersiz bir anahtar değerine sahip olmasını gerektirdiğini anlamak önemlidir. Bu nedenle, örnek modelimizi kullanarak bir Köpek, Kedi ile aynı Kimlik anahtarı değerine sahip olamaz. İkincisi, TPT'nin aksine, anahtar değerlerin yaşadığı ve oluşturulabileceği tek bir yer olarak davranabilen ortak bir tablo yoktur. Bu, basit Identity bir sütunun kullanılamayacağı anlamına gelir.

Sıraları destekleyen veritabanları için anahtar değerleri, her tablo için varsayılan kısıtlamada başvurulan tek bir sıra kullanılarak oluşturulabilir. Bu, yukarıda gösterilen TPC tablolarında kullanılan ve her tablonun aşağıdakilere sahip olduğu stratejidir:

[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])

AnimalSequence , EF Core tarafından oluşturulan bir veritabanı dizisidir. Bu strateji, SQL Server için EF Core veritabanı sağlayıcısı kullanılırken varsayılan olarak TPC hiyerarşileri için kullanılır. Sıraları destekleyen diğer veritabanları için veritabanı sağlayıcıları da benzer bir varsayılan değere sahip olmalıdır. Hi-Lo desenleri gibi dizileri kullanan diğer anahtar oluşturma stratejileri de TPC ile kullanılabilir.

Standart Kimlik sütunları TPC ile çalışmaz, ancak her tablo uygun bir tohumla yapılandırılırsa ve her tablo için oluşturulan değerler hiçbir zaman çakışmayacak şekilde artırılırsa Kimlik sütunlarını kullanmak mümkündür. Örnek:

modelBuilder.Entity<Cat>().ToTable("Cats", tb => tb.Property(e => e.Id).UseIdentityColumn(1, 4));
modelBuilder.Entity<Dog>().ToTable("Dogs", tb => tb.Property(e => e.Id).UseIdentityColumn(2, 4));
modelBuilder.Entity<FarmAnimal>().ToTable("FarmAnimals", tb => tb.Property(e => e.Id).UseIdentityColumn(3, 4));
modelBuilder.Entity<Human>().ToTable("Humans", tb => tb.Property(e => e.Id).UseIdentityColumn(4, 4));

SQLite dizileri veya Kimlik çekirdek/artışını desteklemez ve bu nedenle TPC stratejisiyle SQLite kullanılırken tamsayı anahtar değeri oluşturma desteklenmez. Ancak, istemci tarafı oluşturma veya genel olarak benzersiz anahtarlar (örneğin, GUID anahtarları) SQLite dahil olmak üzere tüm veritabanlarında desteklenir.

Yabancı anahtar kısıtlamaları

TPC eşleme stratejisi, normal olmayan bir SQL şeması oluşturur. Bu, bazı veritabanı temizleyicilerinin buna karşı olmasının bir nedenidir. Örneğin, yabancı anahtar sütununu FavoriteAnimalIdgöz önünde bulundurun. Bu sütundaki değer, bir hayvanın birincil anahtar değeriyle eşleşmelidir. Bu, TPH veya TPT kullanılırken veritabanında basit bir FK kısıtlaması ile zorunlu kılınabilir. Örnek:

CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])

Ancak TPC kullanırken, bir hayvanın birincil anahtarı, o hayvanın beton türü için tabloda depolanır. Örneğin, bir kedinin Cats.Id birincil anahtarı sütunda depolanırken, köpeğin birincil anahtarı sütunda Dogs.Id depolanır ve bu şekilde devam edilir. Bu, bu ilişki için bir FK kısıtlaması oluşturulamayacağı anlamına gelir.

Uygulamada, uygulama geçersiz veri eklemeye çalışmadığı sürece bu bir sorun değildir. Örneğin, tüm veriler EF Core tarafından eklenirse ve varlıkları ilişkilendirmek için gezintileri kullanırsa, FK sütununun her zaman geçerli PK değeri içermesi garanti edilir.

Özet ve rehberlik

Özetle, TPC, kodunuz çoğunlukla tek bir yaprak türündeki varlıkları sorguladığında kullanmak için iyi bir eşleme stratejisidir. Bunun nedeni depolama gereksinimlerinin daha küçük olması ve dizin gerektirebilecek ayrıştırıcı sütun olmamasıdır. Eklemeler ve güncelleştirmeler de verimlidir.

Bununla birlikte, TPH çoğu uygulama için genellikle uygundur ve çok çeşitli senaryolar için iyi bir varsayılandır, bu nedenle ihtiyacınız yoksa TPC'nin karmaşıklığını eklemeyin. Özel olarak, kodunuz çoğunlukla temel türe göre sorgu yazma gibi birçok türdeki varlıkları sorgulayacaksa, TPC üzerinden TPH'ye doğru eğilin.

TPT'yi yalnızca dış etmenler tarafından kısıtlanmışsa kullanın.

Özel Tersine Mühendislik Şablonları

Artık veritabanından ef modelini tersine mühendislik yaparken iskelesi oluşturulmuş kodu özelleştirebilirsiniz. Projenize varsayılan şablonları ekleyerek başlayın:

dotnet new install Microsoft.EntityFrameworkCore.Templates
dotnet new ef-templates

Şablonlar daha sonra özelleştirilebilir ve ve Scaffold-DbContexttarafından dotnet ef dbcontext scaffold otomatik olarak kullanılır.

Daha fazla ayrıntı için bkz . Özel Tersine Mühendislik Şablonları.

Bahşiş

EF Ekibi, .NET Veri Topluluğu Standup'ın bir bölümünde tersine mühendislik şablonlarını ayrıntılı bir şekilde gösterdi ve konuştu. Tüm Topluluk Standup bölümlerinde olduğu gibi, T4 şablonları bölümünü artık YouTube'da izleyebilirsiniz.

Model oluşturma kuralları

EF Core, uygulamanın varlık türlerinin temel alınan veritabanına nasıl eşlenmiş olduğunu açıklamak için bir meta veri "modeli" kullanır. Bu model yaklaşık 60 "kural" kümesi kullanılarak oluşturulur. Kurallar tarafından oluşturulan model daha sonra eşleme öznitelikleri ("veri ek açıklamaları" olarak adlandırılır) ve/veya içindekiOnModelCreating API çağrıları DbModelBuilder kullanılarak özelleştirilebilir.

EF7'den başlayarak, uygulamalar artık bu kuralları kaldırabilir veya değiştirebilir ve yeni kurallar ekleyebilir. Model oluşturma kuralları, model yapılandırmasını denetlemenin güçlü bir yoludur, ancak karmaşık ve düzeltilmesi zor olabilir. Çoğu durumda, özellikler ve türler için ortak bir yapılandırmayı kolayca belirtmek için mevcut kural öncesi model yapılandırması kullanılabilir.

tarafından DbContext kullanılan kurallarda değişiklikler yöntemi geçersiz kılınarak DbContext.ConfigureConventions yapılır. Örnek:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}

Bahşiş

Tüm yerleşik model oluşturma kurallarını bulmak için arabirimini uygulayan IConvention her sınıfı arayın.

Bahşiş

Burada gösterilen kod ModelBuildingConventionsSample.cs dosyasından gelir.

Mevcut bir kuralı kaldırma

Bazen yerleşik kurallardan biri uygulamanız için uygun olmayabilir ve bu durumda kaldırılabilir.

Örnek: Yabancı anahtar sütunları için dizin oluşturma

Genellikle yabancı anahtar (FK) sütunları için dizinler oluşturmak mantıklıdır ve bu nedenle bunun için yerleşik bir kural vardır: ForeignKeyIndexConvention. ve Authorile ilişkileri olan bir Post varlık türünün model hata ayıklama görünümüne Blog baktığımızda biri FK, diğeri de FK için BlogId olmak üzere iki dizinin oluşturulduğunu AuthorId görebiliriz.

  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      AuthorId (no field, int?) Shadow FK Index
      BlogId (no field, int) Shadow Required FK Index
    Navigations:
      Author (Author) ToPrincipal Author Inverse: Posts
      Blog (Blog) ToPrincipal Blog Inverse: Posts
    Keys:
      Id PK
    Foreign keys:
      Post {'AuthorId'} -> Author {'Id'} ToDependent: Posts ToPrincipal: Author ClientSetNull
      Post {'BlogId'} -> Blog {'Id'} ToDependent: Posts ToPrincipal: Blog Cascade
    Indexes:
      AuthorId
      BlogId

Ancak dizinlerin ek yükü vardır ve burada istendiği gibi, bunları tüm FK sütunları için oluşturmak her zaman uygun olmayabilir. Bunu başarmak için, ForeignKeyIndexConvention modeli oluştururken kaldırılabilir:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}

Modelin şimdilik hata ayıklama görünümüne Post baktığımızda, FK'lerdeki dizinlerin oluşturulmadığını görüyoruz:

  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      AuthorId (no field, int?) Shadow FK
      BlogId (no field, int) Shadow Required FK
    Navigations:
      Author (Author) ToPrincipal Author Inverse: Posts
      Blog (Blog) ToPrincipal Blog Inverse: Posts
    Keys:
      Id PK
    Foreign keys:
      Post {'AuthorId'} -> Author {'Id'} ToDependent: Posts ToPrincipal: Author ClientSetNull
      Post {'BlogId'} -> Blog {'Id'} ToDependent: Posts ToPrincipal: Blog Cascade

İstendiğinde, dizinler yine de yabancı anahtar sütunları IndexAttributeiçin kullanılarak açıkça oluşturulabilir:

[Index("BlogId")]
public class Post
{
    // ...
}

Veya içinde yapılandırma ile OnModelCreating:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>(entityTypeBuilder => entityTypeBuilder.HasIndex("BlogId"));
}

Varlık türüne Post yeniden baktığınızda artık dizini içeriyor ancak dizini içermiyor BlogIdAuthorId :

  EntityType: Post
    Properties:
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      AuthorId (no field, int?) Shadow FK
      BlogId (no field, int) Shadow Required FK Index
    Navigations:
      Author (Author) ToPrincipal Author Inverse: Posts
      Blog (Blog) ToPrincipal Blog Inverse: Posts
    Keys:
      Id PK
    Foreign keys:
      Post {'AuthorId'} -> Author {'Id'} ToDependent: Posts ToPrincipal: Author ClientSetNull
      Post {'BlogId'} -> Blog {'Id'} ToDependent: Posts ToPrincipal: Blog Cascade
    Indexes:
      BlogId

Bahşiş

Modeliniz yapılandırma için eşleme özniteliklerini (yani veri ek açıklamalarını) kullanmıyorsa, model oluşturmayı hızlandırmak için ile AttributeConvention biten tüm kurallar güvenle kaldırılabilir.

Yeni kural ekleme

Mevcut kuralları kaldırmak bir başlangıçtır, ancak tamamen yeni model oluşturma kuralları eklemeye ne dersin? EF7 bunu da destekliyor!

Örnek: Ayrımcı özelliklerin kısıt uzunluğu

Hiyerarşi başına tablo devralma eşleme stratejisi , belirli bir satırda hangi türün gösterileceğini belirtmek için ayrıştırıcı sütun gerektirir. Varsayılan olarak, EF ayırıcı için herhangi bir ayrımcı uzunluğu için çalışmasını sağlayan ilişkisiz bir dize sütunu kullanır. Ancak, ayrımcı dizelerin uzunluk üst sınırının kısıtlanması, depolama ve sorguların daha verimli olmasını sağlayabilir. Şimdi bunu yapacak yeni bir kural oluşturalım.

EF Core model oluşturma kuralları, model oluşturulurken modelde yapılan değişikliklere bağlı olarak tetiklenir. Bu, açık yapılandırma yapıldıkçe, eşleme öznitelikleri uygulandıkça ve diğer kurallar çalıştırıldığında modelin güncel kalmasını sağlar. Buna katılmak için her kural, kuralın ne zaman tetikleneceğini belirleyen bir veya daha fazla arabirim uygular. Örneğin, modele yeni bir varlık türü eklendiğinde uygulayan IEntityTypeAddedConvention bir kural tetiklenir. Benzer şekilde, hem uygulayan hem IKeyAddedConvention de IForeignKeyAddedConvention modele bir anahtar veya yabancı anahtar eklendiğinde tetiklenen bir kural.

Modele bir noktada yapılan yapılandırma daha sonraki bir noktada değiştirilebileceği veya kaldırılabildiği için uygulanacak arabirimleri bilmek karmaşık olabilir. Örneğin, bir anahtar kural tarafından oluşturulabilir, ancak daha sonra farklı bir anahtar açıkça yapılandırıldığında değiştirilir.

Ayırıcı uzunluk kuralını uygulamaya yönelik ilk denemeyi yaparak bunu biraz daha somut hale getirelim:

public class DiscriminatorLengthConvention1 : IEntityTypeBaseTypeChangedConvention
{
    public void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        var discriminatorProperty = entityTypeBuilder.Metadata.FindDiscriminatorProperty();
        if (discriminatorProperty != null
            && discriminatorProperty.ClrType == typeof(string))
        {
            discriminatorProperty.Builder.HasMaxLength(24);
        }
    }
}

Bu kural, bir varlık türü için eşlenmiş devralma hiyerarşisi her değiştirildiğinde tetikleneceği anlamına gelen öğesini uygular IEntityTypeBaseTypeChangedConvention. Kural daha sonra hiyerarşi için dize ayırıcı özelliğini bulur ve yapılandırılır.

Bu kural daha sonra içinde ConfigureConventionsçağrılarak Add kullanılır:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Add(_ =>  new DiscriminatorLengthConvention1());
}

Bahşiş

yöntemi, kuralın bir örneğini doğrudan eklemek yerine kuralın Add örneklerini oluşturmak için bir fabrikayı kabul eder. Bu, kuralın EF Core iç hizmet sağlayıcısından bağımlılıkları kullanmasına olanak tanır. Bu kuralın bağımlılıkları olmadığından, hizmet sağlayıcısı parametresi hiçbir zaman kullanılmadığını belirten olarak adlandırılır _.

Modeli oluşturmak ve varlık türüne Post bakmak bunun işe yaradığını gösterir; ayırıcı özelliği artık en fazla 24 uzunlukla yapılandırılır:

 Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)

Ancak şimdi açıkça farklı bir ayrımcı özelliği yapılandırırsak ne olur? Örnek:

modelBuilder.Entity<Post>()
    .HasDiscriminator<string>("PostTypeDiscriminator")
    .HasValue<Post>("Post")
    .HasValue<FeaturedPost>("Featured");

Modelin hata ayıklama görünümüne baktığımızda, ayrıştırıcı uzunluğunun artık yapılandırılmadığını fark ediyoruz!

 PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw

Bunun nedeni, kuralımızda yapılandırdığımız ayırıcı özelliğinin daha sonra özel ayrıştırıcı eklendiğinde kaldırılmış olmasıdır. Ayrıştırıcı değişikliklere tepki vermek için kuralımızda başka bir arabirim uygulayarak bunu düzeltmeye çalışabiliriz, ancak hangi arabirimi uygulayacağımızı bulmak kolay değildir.

Neyse ki, bu yaklaşımın işleri çok daha kolay hale getiren farklı bir yolu vardır. Çoğu zaman, son model doğru olduğu sürece modelin oluşturulurken nasıl göründüğü önemli değildir. Buna ek olarak, uygulamak istediğimiz yapılandırmanın genellikle tepki vermek için başka kuralları tetiklemesi gerekmez. Bu nedenle, kuralımız uygulamasını uygulayabilir IModelFinalizingConvention. Model sonlandırma kuralları, diğer tüm model oluşturma işlemleri tamamlandıktan sonra çalıştırılır ve bu nedenle modelin son durumuna erişim elde edilir. Model sonlandırma kuralı genellikle model öğelerini yapılandıran modelin tamamı üzerinde yinelenir. Bu durumda modeldeki tüm ayrımcıları bulup yapılandıracağız:

public class DiscriminatorLengthConvention2 : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()
                     .Where(entityType => entityType.BaseType == null))
        {
            var discriminatorProperty = entityType.FindDiscriminatorProperty();
            if (discriminatorProperty != null
                && discriminatorProperty.ClrType == typeof(string))
            {
                discriminatorProperty.Builder.HasMaxLength(24);
            }
        }
    }
}

Modeli bu yeni kuralla derledikten sonra, ayrıştırıcı uzunluğunun özelleştirildiği halde doğru yapılandırıldığını fark ettik:

PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(24)

Yalnızca eğlence için bir adım ileri gidelim ve maksimum uzunluğu en uzun ayırıcı değerin uzunluğu olacak şekilde yapılandıralım.

public class DiscriminatorLengthConvention3 : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()
                     .Where(entityType => entityType.BaseType == null))
        {
            var discriminatorProperty = entityType.FindDiscriminatorProperty();
            if (discriminatorProperty != null
                && discriminatorProperty.ClrType == typeof(string))
            {
                var maxDiscriminatorValueLength =
                    entityType.GetDerivedTypesInclusive().Select(e => ((string)e.GetDiscriminatorValue()!).Length).Max();

                discriminatorProperty.Builder.HasMaxLength(maxDiscriminatorValueLength);
            }
        }
    }
}

Artık en uzun ayırıcı sütun uzunluğu 8'dir ve bu da kullanımdaki en uzun ayrıştırıcı değeri olan "Öne Çıkan" uzunluğudur.

PostTypeDiscriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(8)

Bahşiş

Kuralın ayırıcı sütun için de bir dizin oluşturması gerekip gerekmediğini merak ediyor olabilirsiniz. GitHub'da bu konu hakkında bir tartışma vardır. Kısa bir yanıt, bazen bir dizinin yararlı olabileceği, ancak çoğu zaman büyük olasılıkla işe yaramayacağıdır. Bu nedenle, her zaman bunu yapmak için bir kurala sahip olmak yerine burada gerektiği gibi uygun dizinler oluşturmak en iyisidir. Ancak buna katılmıyorsanız yukarıdaki kural da kolayca değiştirilip bir dizin oluşturulabilir.

Örnek: Tüm dize özellikleri için varsayılan uzunluk

Bu kez GitHub'da istendiği gibi herhangi bir dize özelliği için varsayılan en uzun uzunluğu ayarlayan bir sonlandırma kuralının kullanılabildiği başka bir örneğe göz atalım. Kural önceki örneğe oldukça benzer:

public class MaxStringLengthConvention : IModelFinalizingConvention
{
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
    {
        foreach (var property in modelBuilder.Metadata.GetEntityTypes()
                     .SelectMany(
                         entityType => entityType.GetDeclaredProperties()
                             .Where(
                                 property => property.ClrType == typeof(string))))
        {
            property.Builder.HasMaxLength(512);
        }
    }
}

Bu kural oldukça basittir. Modeldeki her dize özelliğini bulur ve maksimum uzunluğunu 512 olarak ayarlar. özelliklerinde Posthata ayıklama görünümüne baktığımızda, tüm dize özelliklerinin artık en fazla 512 uzunluğuna sahip olduğunu görüyoruz.

EntityType: Post
  Properties:
    Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    AuthorId (no field, int?) Shadow FK Index
    BlogId (no field, int) Shadow Required FK Index
    Content (string) Required MaxLength(512)
    Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(512)
    PublishedOn (DateTime) Required
    Title (string) Required MaxLength(512)

Content Ancak özellik muhtemelen 512'den fazla karaktere izin vermelidir, aksi takdirde tüm gönderilerimiz oldukça kısa olacaktır! Bu, eşleme özniteliği kullanılarak yalnızca bu özellik için en uzun uzunluğu açıkça yapılandırarak kuralımızı değiştirmeden yapılabilir:

[MaxLength(4000)]
public string Content { get; set; }

Veya içindeki OnModelCreatingkodla:

modelBuilder.Entity<Post>()
    .Property(post => post.Content)
    .HasMaxLength(4000);

Artık 4000 ile açıkça yapılandırılan özellikler dışında Content tüm özelliklerin uzunluğu en fazla 512 olur:

EntityType: Post
  Properties:
    Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    AuthorId (no field, int?) Shadow FK Index
    BlogId (no field, int) Shadow Required FK Index
    Content (string) Required MaxLength(4000)
    Discriminator (no field, string) Shadow Required AfterSave:Throw MaxLength(512)
    PublishedOn (DateTime) Required
    Title (string) Required MaxLength(512)

Peki kuralımız neden açıkça yapılandırılan maksimum uzunluğu geçersiz kılmadı? Yanıt, EF Core'un her yapılandırma parçasının nasıl yapıldığını izlemesidir. Bu, sabit listesi tarafından ConfigurationSource temsil edilir. Farklı yapılandırma türleri şunlardır:

  • Explicit: Model öğesi açıkça içinde yapılandırıldı OnModelCreating
  • DataAnnotation: Model öğesi CLR türünde eşleme özniteliği (veri ek açıklaması) kullanılarak yapılandırıldı
  • Convention: Model öğesi bir model oluşturma kuralı tarafından yapılandırıldı

Kurallar, veya Explicitolarak DataAnnotation işaretlenmiş yapılandırmayı hiçbir zaman geçersiz kılmaz. Bu, örneğin özelliğinden Builder elde edilen bir "kural oluşturucusu" IConventionPropertyBuilderkullanılarak elde edilir. Örnek:

property.Builder.HasMaxLength(512);

Kural oluşturucusunda çağrılmasıHasMaxLength, yalnızca bir eşleme özniteliği tarafından veya içinde OnModelCreatingyapılandırılmamışsa maksimum uzunluğu ayarlar.

Bunun gibi oluşturucu yöntemlerinin ikinci bir parametresi de vardır: fromDataAnnotation. Kural bir eşleme özniteliği adına yapılandırma yapıyorsa bunu true olarak ayarlayın. Örnek:

property.Builder.HasMaxLength(512, fromDataAnnotation: true);

Bu, değerini olarak ConfigurationSourceDataAnnotationayarlar. Bu, değerin artık üzerinde OnModelCreatingaçık eşleme yoluyla geçersiz kılınabileceği, ancak eşleme dışı öznitelik kuralları tarafından geçersiz kılınabileceği anlamına gelir.

Son olarak, bu örnekten MaxStringLengthConvention ayrılmadan önce hem hem DiscriminatorLengthConvention3 de aynı anda kullanırsak ne olur? Bunun yanıtı, model sonlandırma kuralları eklendikleri sırayla çalıştırıldığından, bunların hangi sırayla eklendiğine bağlı olmasıdır. Bu nedenle, en son eklenirse MaxStringLengthConvention , en son çalışır ve ayırıcı özelliğinin maksimum uzunluğunu 512 olarak ayarlar. Bu nedenle, bu durumda, diğer tüm dize özelliklerini 512 olarak bırakırken yalnızca ayırıcı özellikleri için varsayılan maksimum uzunluğu geçersiz kılabilmesi için last eklemek DiscriminatorLengthConvention3 daha iyidir.

Mevcut bir kuralı değiştirme

Bazen mevcut bir kuralı tamamen kaldırmak yerine temelde aynı şeyi ancak değişen davranışı olan bir kuralla değiştirmek istiyoruz. Mevcut kural, uygun şekilde tetiklenmesi için gereken arabirimleri zaten uygulayacağından bu yararlı olur.

Örnek: Özellik eşlemeyi kabul etme

EF Core, tüm genel okuma-yazma özelliklerini kurala göre eşler. Bu , varlık türlerinizin tanımlanma biçimi için uygun olmayabilir. Bunu değiştirmek için, açıkça ile eşlenmediği OnModelCreating veya adlı Persistyeni bir öznitelikle işaretlenmediği sürece öğesini herhangi bir özelliği eşlemeyen kendi uygulamamızla değiştirebilirizPropertyDiscoveryConvention:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public sealed class PersistAttribute : Attribute
{
}

Yeni kural şu şekildedir:

public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
    public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
        : base(dependencies)
    {
    }

    public override void ProcessEntityTypeAdded(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionContext<IConventionEntityTypeBuilder> context)
        => Process(entityTypeBuilder);

    public override void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        if ((newBaseType == null
             || oldBaseType != null)
            && entityTypeBuilder.Metadata.BaseType == newBaseType)
        {
            Process(entityTypeBuilder);
        }
    }

    private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
    {
        foreach (var memberInfo in GetRuntimeMembers())
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                entityTypeBuilder.Property(memberInfo);
            }
            else if (memberInfo is PropertyInfo propertyInfo
                     && Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
            {
                entityTypeBuilder.Ignore(propertyInfo.Name);
            }
        }

        IEnumerable<MemberInfo> GetRuntimeMembers()
        {
            var clrType = entityTypeBuilder.Metadata.ClrType;

            foreach (var property in clrType.GetRuntimeProperties()
                         .Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
            {
                yield return property;
            }

            foreach (var property in clrType.GetRuntimeFields())
            {
                yield return property;
            }
        }
    }
}

Bahşiş

Yerleşik bir kuralı değiştirirken, yeni kural uygulaması mevcut kural sınıfından devralmalıdır. Bazı kuralların ilişkisel veya sağlayıcıya özgü uygulamaları olduğunu unutmayın. Bu durumda, yeni kural uygulaması kullanımdaki veritabanı sağlayıcısı için en belirli mevcut kural sınıfından devralmalıdır.

Kural daha sonra içindeki ConfigureConventionsyöntemi kullanılarak Replace kaydedilir:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Conventions.Replace<PropertyDiscoveryConvention>(
        serviceProvider => new AttributeBasedPropertyDiscoveryConvention(
            serviceProvider.GetRequiredService<ProviderConventionSetBuilderDependencies>()));
}

Bahşiş

Bu durum, mevcut kuralın bağımlılık nesnesiyle ProviderConventionSetBuilderDependencies temsil edilen bağımlılıklara sahip olduğu bir durumdur. Bunlar kullanılarak GetRequiredService iç hizmet sağlayıcısından alınır ve kural oluşturucusna geçirilir.

Bu kural, verilen varlık türünden tüm okunabilir özellikleri ve alanları alarak çalışır. Üye ile [Persist]ilişkilendirildiyse, çağrılarak eşlenir:

entityTypeBuilder.Property(memberInfo);

Öte yandan, üye başka türlü eşlenmiş bir özellikse, aşağıdakiler kullanılarak modelden dışlanır:

entityTypeBuilder.Ignore(propertyInfo.Name);

Bu kuralın, ile [Persist]işaretlendikleri sürece alanların eşlenmesine (özelliklere ek olarak) izin verdiğine dikkat edin. Bu, özel alanları modelde gizli anahtarlar olarak kullanabileceğimiz anlamına gelir.

Örneğin, aşağıdaki varlık türlerini göz önünde bulundurun:

public class LaundryBasket
{
    [Persist]
    [Key]
    private readonly int _id;

    [Persist]
    public int TenantId { get; init; }

    public bool IsClean { get; set; }

    public List<Garment> Garments { get; } = new();
}

public class Garment
{
    public Garment(string name, string color)
    {
        Name = name;
        Color = color;
    }

    [Persist]
    [Key]
    private readonly int _id;

    [Persist]
    public int TenantId { get; init; }

    [Persist]
    public string Name { get; }

    [Persist]
    public string Color { get; }

    public bool IsClean { get; set; }

    public LaundryBasket? Basket { get; set; }
}

Bu varlık türlerinden oluşturulan model:

Model:
  EntityType: Garment
    Properties:
      _id (_id, int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      Basket_id (no field, int?) Shadow FK Index
      Color (string) Required
      Name (string) Required
      TenantId (int) Required
    Navigations:
      Basket (LaundryBasket) ToPrincipal LaundryBasket Inverse: Garments
    Keys:
      _id PK
    Foreign keys:
      Garment {'Basket_id'} -> LaundryBasket {'_id'} ToDependent: Garments ToPrincipal: Basket ClientSetNull
    Indexes:
      Basket_id
  EntityType: LaundryBasket
    Properties:
      _id (_id, int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      TenantId (int) Required
    Navigations:
      Garments (List<Garment>) Collection ToDependent Garment Inverse: Basket
    Keys:
      _id PK

Normalde IsClean eşlenmiş olabileceğine dikkat edin, ancak ile [Perist] işaretlenmediğinden (muhtemelen temizlik çamaşırların kalıcı bir özelliği olmadığından), artık eşlenmemiş bir özellik olarak kabul edilir.

Bahşiş

Bir özelliği eşlemek eşlenen özelliği daha fazla yapılandırmak için başka birçok kuralın çalıştırılmasını tetiklediğinden, bu kural model sonlandırma kuralı olarak uygulanamadı.

Saklı yordam eşlemesi

Varsayılan olarak, EF Core doğrudan tablolar veya güncelleştirilebilir görünümlerle çalışan ekleme, güncelleştirme ve silme komutları oluşturur. EF7, bu komutların saklı yordamlara eşlemini destekler.

Bahşiş

EF Core saklı yordamlar aracılığıyla sorgulamayı her zaman desteklemiştir. EF7'deki yeni destek açıkça eklemeler, güncelleştirmeler ve silmeler için saklı yordamları kullanma hakkındadır.

Önemli

Saklı yordam eşleme desteği, saklı yordamların önerileceği anlamına gelmez.

Saklı yordamlar , ve UpdateUsingStoredProcedureDeleteUsingStoredProcedurekullanılarak InsertUsingStoredProcedureile OnModelCreating eşlenir. Örneğin, bir Person varlık türü için saklı yordamları eşlemek için:

modelBuilder.Entity<Person>()
    .InsertUsingStoredProcedure(
        "People_Insert",
        storedProcedureBuilder =>
        {
            storedProcedureBuilder.HasParameter(a => a.Name);
            storedProcedureBuilder.HasResultColumn(a => a.Id);
        })
    .UpdateUsingStoredProcedure(
        "People_Update",
        storedProcedureBuilder =>
        {
            storedProcedureBuilder.HasOriginalValueParameter(person => person.Id);
            storedProcedureBuilder.HasOriginalValueParameter(person => person.Name);
            storedProcedureBuilder.HasParameter(person => person.Name);
            storedProcedureBuilder.HasRowsAffectedResultColumn();
        })
    .DeleteUsingStoredProcedure(
        "People_Delete",
        storedProcedureBuilder =>
        {
            storedProcedureBuilder.HasOriginalValueParameter(person => person.Id);
            storedProcedureBuilder.HasOriginalValueParameter(person => person.Name);
            storedProcedureBuilder.HasRowsAffectedResultColumn();
        });

Bu yapılandırma, SQL Server kullanırken aşağıdaki saklı yordamlarla eşler:

Eklemeler için

CREATE PROCEDURE [dbo].[People_Insert]
    @Name [nvarchar](max)
AS
BEGIN
      INSERT INTO [People] ([Name])
      OUTPUT INSERTED.[Id]
      VALUES (@Name);
END

Güncelleştirmeler için

CREATE PROCEDURE [dbo].[People_Update]
    @Id [int],
    @Name_Original [nvarchar](max),
    @Name [nvarchar](max)
AS
BEGIN
    UPDATE [People] SET [Name] = @Name
    WHERE [Id] = @Id AND [Name] = @Name_Original
    SELECT @@ROWCOUNT
END

Silme işlemleri için

CREATE PROCEDURE [dbo].[People_Delete]
    @Id [int],
    @Name_Original [nvarchar](max)
AS
BEGIN
    DELETE FROM [People]
    OUTPUT 1
    WHERE [Id] = @Id AND [Name] = @Name_Original;
END

Bahşiş

Saklı yordamların modeldeki her tür için veya belirli bir türdeki tüm işlemler için kullanılması gerekmez. Örneğin, yalnızca DeleteUsingStoredProcedure belirli bir tür için belirtilmişse, EF Core ekleme ve güncelleştirme işlemleri için normal SQL oluşturur ve yalnızca silme işlemleri için saklı yordamı kullanır.

Her yönteme geçirilen ilk bağımsız değişken saklı yordam adıdır. Bu atlanabilir, bu durumda EF Core "_Insert", "_Update" veya "_Delete" ile eklenen tablo adını kullanır. Bu nedenle yukarıdaki örnekte tablo "Kişiler" olarak adlandırıldığından saklı yordam adları işlev değişikliği yapılmadan kaldırılabilir.

İkinci bağımsız değişken, parametreler, dönüş değerleri ve sonuç sütunları da dahil olmak üzere saklı yordamın giriş ve çıkışını yapılandırmak için kullanılan bir oluşturucudur.

Parametreler

Parametreler, saklı yordam tanımında göründükleri sırada oluşturucuya eklenmelidir.

Dekont

Parametreler adlandırılabilir, ancak EF Core her zaman adlandırılmış bağımsız değişkenler yerine konumsal bağımsız değişkenler kullanarak saklı yordamları çağırır. Adla çağırmak ilginizi çekiyorsa, çağırma için parametre adlarını kullanmak üzere sproc eşlemesini yapılandırmaya izin ver için oy verin.

Her parametre oluşturucu yönteminin ilk bağımsız değişkeni, parametrenin bağlı olduğu modeldeki özelliği belirtir. Bu bir lambda ifadesi olabilir:

storedProcedureBuilder.HasParameter(a => a.Name);

Veya özellikle gölge özellikleri eşlerken yararlı olan bir dize:

storedProcedureBuilder.HasParameter("Name");

Parametreler varsayılan olarak "giriş" için yapılandırılır. "Output" veya "input/output" parametreleri iç içe oluşturucu kullanılarak yapılandırılabilir. Örnek:

storedProcedureBuilder.HasParameter(
    document => document.RetrievedOn, 
    parameterBuilder => parameterBuilder.IsOutput());

Farklı parametre türleri için üç farklı oluşturucu yöntemi vardır:

  • HasParameter verilen özelliğin geçerli değerine bağlı normal bir parametre belirtir.
  • HasOriginalValueParameter verilen özelliğin özgün değerine bağlı bir parametre belirtir. Özgün değer, biliniyorsa, özelliğin veritabanından sorgulandığında sahip olduğu değerdir. Bu değer bilinmiyorsa, bunun yerine geçerli değer kullanılır. Özgün değer parametreleri eşzamanlılık belirteçleri için kullanışlıdır.
  • HasRowsAffectedParameter saklı yordamdan etkilenen satır sayısını döndürmek için kullanılan bir parametreyi belirtir.

Bahşiş

"Update" ve "delete" saklı yordamlarındaki anahtar değerleri için özgün değer parametreleri kullanılmalıdır. Bu, değiştirilebilir anahtar değerlerini destekleyen EF Core'un gelecek sürümlerinde doğru satırın güncelleştirilmesini sağlar.

Değer döndürme

EF Core, saklı yordamlardan değer döndürmek için üç mekanizmayı destekler:

  • Yukarıda gösterildiği gibi çıkış parametreleri.
  • Oluşturucu yöntemi kullanılarak HasResultColumn belirtilen sonuç sütunları.
  • Döndürülen değer, etkilenen satır sayısını döndürmeyle sınırlıdır ve oluşturucu yöntemi kullanılarak HasRowsAffectedReturnValue belirtilir.

Saklı yordamlardan döndürülen değerler genellikle anahtar veya hesaplanan sütun gibi Identity oluşturulan, varsayılan veya hesaplanan değerler için kullanılır. Örneğin, aşağıdaki yapılandırma dört sonuç sütunu belirtir:

entityTypeBuilder.InsertUsingStoredProcedure(
        storedProcedureBuilder =>
        {
            storedProcedureBuilder.HasParameter(document => document.Title);
            storedProcedureBuilder.HasResultColumn(document => document.Id);
            storedProcedureBuilder.HasResultColumn(document => document.FirstRecordedOn);
            storedProcedureBuilder.HasResultColumn(document => document.RetrievedOn);
            storedProcedureBuilder.HasResultColumn(document => document.RowVersion);
        });

Bunlar şunları döndürmek için kullanılır:

  • Özelliği için Id oluşturulan anahtar değeri.
  • Özelliği için FirstRecordedOn veritabanı tarafından oluşturulan varsayılan değer.
  • özelliği için RetrievedOn veritabanı tarafından oluşturulan hesaplanan değer.
  • Özelliği için otomatik olarak oluşturulan rowversion eşzamanlılık belirteci RowVersion .

Bu yapılandırma, SQL Server kullanırken aşağıdaki saklı yordamla eşler:

CREATE PROCEDURE [dbo].[Documents_Insert]
    @Title [nvarchar](max)
AS
BEGIN
    INSERT INTO [Documents] ([Title])
    OUTPUT INSERTED.[Id], INSERTED.[FirstRecordedOn], INSERTED.[RetrievedOn], INSERTED.[RowVersion]
    VALUES (@Title);
END

İyimser eşzamanlılık

İyimser eşzamanlılık saklı yordamlarda olduğu gibi çalışır. Saklı yordam şu şekilde olmalıdır:

  • Satırın yalnızca geçerli bir belirteci varsa güncelleştirildiğinden emin olmak için yan WHERE tümcesinde eşzamanlılık belirteci kullanın. Eşzamanlılık belirteci için kullanılan değer genellikle eşzamanlılık belirteci özelliğinin özgün değeridir ancak olması gerekmez.
  • ETKILENEN satır sayısını döndürerek EF Core'un bunu etkilenen beklenen satır sayısıyla karşılaştırabilmesini ve değerler eşleşmiyorsa bir DbUpdateConcurrencyException atabilmesini sağlayın.

Örneğin, aşağıdaki SQL Server saklı yordamı otomatik eşzamanlılık rowversion belirteci kullanır:

CREATE PROCEDURE [dbo].[Documents_Update]
    @Id [int],
    @RowVersion_Original [rowversion],
    @Title [nvarchar](max),
    @RowVersion [rowversion] OUT
AS
BEGIN
    DECLARE @TempTable table ([RowVersion] varbinary(8));
    UPDATE [Documents] SET
        [Title] = @Title
    OUTPUT INSERTED.[RowVersion] INTO @TempTable
    WHERE [Id] = @Id AND [RowVersion] = @RowVersion_Original
    SELECT @@ROWCOUNT;
    SELECT @RowVersion = [RowVersion] FROM @TempTable;
END

Bu, EF Core'da aşağıdakiler kullanılarak yapılandırılır:

.UpdateUsingStoredProcedure(
    storedProcedureBuilder =>
    {
        storedProcedureBuilder.HasOriginalValueParameter(document => document.Id);
        storedProcedureBuilder.HasOriginalValueParameter(document => document.RowVersion);
        storedProcedureBuilder.HasParameter(document => document.Title);
        storedProcedureBuilder.HasParameter(document => document.RowVersion, parameterBuilder => parameterBuilder.IsOutput());
        storedProcedureBuilder.HasRowsAffectedResultColumn();
    });

Şu noktalara dikkat edin:

  • Eşzamanlılık belirtecinin RowVersion özgün değeri kullanılır.
  • Saklı yordam, satırın yalnızca özgün değer eşleşiyorsa RowVersion güncelleştirildiğinden emin olmak için bir WHERE yan tümcesi kullanır.
  • için RowVersion oluşturulan yeni değer geçici bir tabloya eklenir.
  • Etkilenen (@@ROWCOUNT) satır sayısı ve oluşturulan RowVersion değer döndürülür.

Devralma hiyerarşilerini saklı yordamlara eşleme

EF Core, saklı yordamların hiyerarşideki türler için tablo düzenini izlemesini gerektirir. Bu şu anlama gelir:

  • TPH kullanılarak eşlenen hiyerarşide, eşlenen tek tabloyu hedefleyen tek bir ekleme, güncelleştirme ve/veya silme saklı yordamı olmalıdır. Ekleme ve güncelleştirme saklı yordamlarının ayrıştırıcı değeri için bir parametresi olmalıdır.
  • TPT kullanılarak eşlenen hiyerarşi, soyut türler de dahil olmak üzere her tür için bir ekleme, güncelleştirme ve/veya silme yordamına sahip olmalıdır. EF Core, tüm tablolardaki satırları güncelleştirmek, eklemek ve silmek için gerektiğinde birden çok çağrı yapar.
  • TPC kullanılarak eşlenen hiyerarşinin her somut tür için ekleme, güncelleştirme ve/veya silme saklı yordamı olmalıdır, ancak soyut türler olmamalıdır.

Dekont

Eşleme stratejisinden bağımsız olarak somut tür başına tek bir saklı yordam kullanmak ilgilendiğiniz bir şeyse devralma eşleme stratejisinden bağımsız olarak somut tür başına tek bir sproc kullanarak Destek'e oy verin.

Sahip olunan türleri saklı yordamlarla eşleme

Sahip olunan türler için saklı yordamların yapılandırması, iç içe sahip olunan tür oluşturucusunda yapılır. Örnek:

modelBuilder.Entity<Person>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.OwnsOne(
            author => author.Contact,
            ownedNavigationBuilder =>
            {
                ownedNavigationBuilder.ToTable("Contacts");
                ownedNavigationBuilder
                    .InsertUsingStoredProcedure(
                        storedProcedureBuilder =>
                        {
                            storedProcedureBuilder.HasParameter("PersonId");
                            storedProcedureBuilder.HasParameter(contactDetails => contactDetails.Phone);
                        })
                    .UpdateUsingStoredProcedure(
                        storedProcedureBuilder =>
                        {
                            storedProcedureBuilder.HasOriginalValueParameter("PersonId");
                            storedProcedureBuilder.HasParameter(contactDetails => contactDetails.Phone);
                            storedProcedureBuilder.HasRowsAffectedResultColumn();
                        })
                    .DeleteUsingStoredProcedure(
                        storedProcedureBuilder =>
                        {
                            storedProcedureBuilder.HasOriginalValueParameter("PersonId");
                            storedProcedureBuilder.HasRowsAffectedResultColumn();
            });
    });

Dekont

Şu anda yalnızca sahip olunan ekleme, güncelleştirme ve silme desteği türleri için saklı yordamlar ayrı tablolara eşlenmelidir. Başka bir ifadeyle, sahip olunan tür sahip tablosundaki sütunlar tarafından temsil edilemez. Kaldırıldığını görmek istediğiniz bir sınırlamaysa CUD sproc eşlemesine "tablo" bölme desteği ekleme için oy verin.

Çoka çok birleştirme varlıklarını saklı yordamlarla eşleme

Çoka çok birleştirme varlıklarının saklı yordamlarının yapılandırılması, çoka çok yapılandırmasının bir parçası olarak gerçekleştirilebilir. Örnek:

modelBuilder.Entity<Book>(
    entityTypeBuilder =>
    {
        entityTypeBuilder
            .HasMany(document => document.Authors)
            .WithMany(author => author.PublishedWorks)
            .UsingEntity<Dictionary<string, object>>(
                "BookPerson",
                builder => builder.HasOne<Person>().WithMany().OnDelete(DeleteBehavior.Cascade),
                builder => builder.HasOne<Book>().WithMany().OnDelete(DeleteBehavior.ClientCascade),
                joinTypeBuilder =>
                {
                    joinTypeBuilder
                        .InsertUsingStoredProcedure(
                            storedProcedureBuilder =>
                            {
                                storedProcedureBuilder.HasParameter("AuthorsId");
                                storedProcedureBuilder.HasParameter("PublishedWorksId");
                            })
                        .DeleteUsingStoredProcedure(
                            storedProcedureBuilder =>
                            {
                                storedProcedureBuilder.HasOriginalValueParameter("AuthorsId");
                                storedProcedureBuilder.HasOriginalValueParameter("PublishedWorksId");
                                storedProcedureBuilder.HasRowsAffectedResultColumn();
                            });
                });
    });

Yeni ve geliştirilmiş kesme çizgileri ve olaylar

EF Core kesme makineleri EF Core işlemlerinin kesilmesini, değiştirilmesini ve/veya gizlenmesini sağlar. EF Core ayrıca geleneksel .NET olaylarını ve günlüğe kaydetmeyi de içerir.

EF7, kesiciler için aşağıdaki geliştirmeleri içerir:

Buna ek olarak, EF7 aşağıdakiler için yeni geleneksel .NET olayları içerir:

Aşağıdaki bölümlerde, bu yeni kesme noktası özelliklerini kullanmanın bazı örnekleri gösterilmektedir.

Varlık oluşturmayla ilgili basit eylemler

Bahşiş

Burada gösterilen kod SimpleMaterializationSample.cs dosyasından gelir.

Yeni IMaterializationInterceptor , bir varlık örneği oluşturulmadan önce ve sonra ve bu örneğin özellikleri başlatılmadan önce ve sonra kesmeyi destekler. Kesme noktası her noktada varlık örneğini değiştirebilir veya değiştirebilir. Bu, aşağıdakileri sağlar:

  • Doğrulama, hesaplanan değerler veya bayraklar için gereken eşlenmemiş özellikleri veya çağırma yöntemlerini ayarlama.
  • Örnek oluşturmak için fabrika kullanma.
  • EF'den farklı bir varlık örneği oluşturmak normalde önbellekten veya ara sunucu türünde bir örnek gibi bir örnek oluşturur.
  • Bir varlık örneğine hizmet ekleme.

Örneğin, bir varlığın veritabanından alındığı zamanı takip etmek istediğimizi ve bu sayede verileri düzenleyen bir kullanıcıya görüntülenebileceğini düşünün. Bunu başarmak için önce bir arabirim tanımlayacağız:

public interface IHasRetrieved
{
    DateTime Retrieved { get; set; }
}

Bir arabirim kullanmak, aynı kesme noktasının birçok farklı varlık türüyle çalışmasına izin verdiğinden, kesişenlerle ortaktır. Örnek:

public class Customer : IHasRetrieved
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? PhoneNumber { get; set; }

    [NotMapped]
    public DateTime Retrieved { get; set; }
}

özniteliğinin [NotMapped] bu özelliğin yalnızca varlıkla çalışırken kullanıldığını ve veritabanında kalıcı olmaması gerektiğini belirtmek için kullanıldığına dikkat edin.

Kesme noktası daha sonra uygun yöntemi IMaterializationInterceptor uygulamalı ve alınan zamanı ayarlamalıdır:

public class SetRetrievedInterceptor : IMaterializationInterceptor
{
    public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
    {
        if (instance is IHasRetrieved hasRetrieved)
        {
            hasRetrieved.Retrieved = DateTime.UtcNow;
        }

        return instance;
    }
}

bu kesme noktasının bir örneği, yapılandırılırken DbContextkaydedilir:

public class CustomerContext : DbContext
{
    private static readonly SetRetrievedInterceptor _setRetrievedInterceptor = new();

    public DbSet<Customer> Customers
        => Set<Customer>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .AddInterceptors(_setRetrievedInterceptor)
            .UseSqlite("Data Source = customers.db");
}

Bahşiş

Bu kesme noktası durum bilgisi olmadığından ortaktır, bu nedenle tek bir örnek oluşturulur ve tüm DbContext örnekler arasında paylaşılır.

Artık veritabanından bir Customer sorgulandığında özelliği Retrieved otomatik olarak ayarlanır. Örnek:

await using (var context = new CustomerContext())
{
    var customer = await context.Customers.SingleAsync(e => e.Name == "Alice");
    Console.WriteLine($"Customer '{customer.Name}' was retrieved at '{customer.Retrieved.ToLocalTime()}'");
}

Çıktı oluşturur:

Customer 'Alice' was retrieved at '9/22/2022 5:25:54 PM'

Varlıklara hizmet ekleme

Bahşiş

Burada gösterilen kod InjectLoggerSample.cs dosyasından gelir.

EF Core, bağlam örneklerine bazı özel hizmetler eklemek için yerleşik desteğe zaten sahiptir; örneğin, hizmeti ekleyerek ILazyLoader çalışan proxy'ler olmadan yavaş yükleme bölümüne bakın.

IMaterializationInterceptor bunu herhangi bir hizmette genelleştirmek için kullanılabilir. Aşağıdaki örnek, varlıkların kendi günlüklerini gerçekleştirebilecekleri şekilde nasıl eklendiğini ILogger gösterir.

Dekont

Varlıklara hizmet eklemek, bu varlık türlerini eklenen hizmetlerle eşleştirir ve bu da bazı kişilerin deseni önleme olarak kabul eder.

Daha önce olduğu gibi, neler yapılabileceğini tanımlamak için bir arabirim kullanılır.

public interface IHasLogger
{
    ILogger? Logger { get; set; }
}

Ve günlüğe kaydedecek varlık türlerinin bu arabirimi uygulaması gerekir. Örnek:

public class Customer : IHasLogger
{
    private string? _phoneNumber;

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

    public string? PhoneNumber
    {
        get => _phoneNumber;
        set
        {
            Logger?.LogInformation(1, $"Updating phone number for '{Name}' from '{_phoneNumber}' to '{value}'.");

            _phoneNumber = value;
        }
    }

    [NotMapped]
    public ILogger? Logger { get; set; }
}

Bu kez, kesme noktasının her varlık örneği oluşturulduktan ve özellik değerleri başlatıldıktan sonra çağrılan uygulamasını IMaterializationInterceptor.InitializedInstancegerekir. Kesme noktası, bağlamdan bir ILogger alır ve onunla başlatır IHasLogger.Logger :

public class LoggerInjectionInterceptor : IMaterializationInterceptor
{
    private ILogger? _logger;

    public object InitializedInstance(MaterializationInterceptionData materializationData, object instance)
    {
        if (instance is IHasLogger hasLogger)
        {
            _logger ??= materializationData.Context.GetService<ILoggerFactory>().CreateLogger("CustomersLogger");
            hasLogger.Logger = _logger;
        }

        return instance;
    }
}

Bu kez her örnek için DbContext yeni bir kesme noktası örneği kullanılır, çünkü ILogger elde edilen örnek her örnek için DbContext değişebilir ve ILogger kesme noktası üzerinde önbelleğe alınır:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.AddInterceptors(new LoggerInjectionInterceptor());

Şimdi, Customer.PhoneNumber her değiştirildiğinde, bu değişiklik uygulamanın günlüğüne kaydedilir. Örnek:

info: CustomersLogger[1]
      Updating phone number for 'Alice' from '+1 515 555 0123' to '+1 515 555 0125'.

LINQ ifade ağacı kesme noktası

Bahşiş

Burada gösterilen kod QueryInterceptionSample.cs dosyasından gelir.

EF Core, .NET LINQ sorgularını kullanır. Bu genellikle EF Core tarafından uygun SQL'e çevrilen bir ifade ağacı oluşturmak için C#, VB veya F# derleyicisini kullanmayı içerir. Örneğin, bir müşteri sayfası döndüren bir yöntem düşünün:

Task<List<Customer>> GetPageOfCustomers(string sortProperty, int page)
{
    using var context = new CustomerContext();

    return context.Customers
        .OrderBy(e => EF.Property<object>(e, sortProperty))
        .Skip(page * 20).Take(20).ToListAsync();
}

Bahşiş

Bu sorgu, EF.Property sıralama ölçütü olarak özelliğini belirtmek için yöntemini kullanır. Bu, uygulamanın özellik adını dinamik olarak geçirmesini sağlar ve varlık türünün herhangi bir özelliğine göre sıralamaya izin verir. Dizine alınamayan sütunlara göre sıralamanın yavaş olabileceğini unutmayın.

Sıralama için kullanılan özellik her zaman kararlı bir sıralama döndürdüğü sürece bu işlem düzgün çalışır. Ama her zaman böyle olmayabilir. Örneğin, yukarıdaki LINQ sorgusu tarafından sıralandığında Customer.CitySQLite'te aşağıdakileri oluşturur:

SELECT "c"."Id", "c"."City", "c"."Name", "c"."PhoneNumber"
FROM "Customers" AS "c"
ORDER BY "c"."City"
LIMIT @__p_1 OFFSET @__p_0

aynı Cityolan birden çok müşteri varsa, bu sorgunun sıralaması kararlı değildir. Bu, kullanıcı veriler arasında sayfa oluştururken eksik veya yinelenen sonuçlara yol açabilir.

Bu sorunu düzeltmenin yaygın bir yolu, birincil anahtara göre ikincil bir sıralama gerçekleştirmektir. Ancak EF7, bunu her sorguya el ile eklemek yerine, ikincil sıralamanın dinamik olarak eklenebildiği sorgu ifade ağacının kesilmesine izin verir. Bunu kolaylaştırmak için, bu kez tamsayı birincil anahtarı olan herhangi bir varlık için bir arabirim kullanacağız:

public interface IHasIntKey
{
    int Id { get; }
}

Bu arabirim, ilgilendiğim varlık türleri tarafından uygulanır:

public class Customer : IHasIntKey
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? City { get; set; }
    public string? PhoneNumber { get; set; }
}

Ardından uygulayan bir kesiciye ihtiyacımız var IQueryExpressionInterceptor

public class KeyOrderingExpressionInterceptor : IQueryExpressionInterceptor
{
    public Expression QueryCompilationStarting(Expression queryExpression, QueryExpressionEventData eventData)
        => new KeyOrderingExpressionVisitor().Visit(queryExpression);

    private class KeyOrderingExpressionVisitor : ExpressionVisitor
    {
        private static readonly MethodInfo ThenByMethod
            = typeof(Queryable).GetMethods()
                .Single(m => m.Name == nameof(Queryable.ThenBy) && m.GetParameters().Length == 2);

        protected override Expression VisitMethodCall(MethodCallExpression? methodCallExpression)
        {
            var methodInfo = methodCallExpression!.Method;
            if (methodInfo.DeclaringType == typeof(Queryable)
                && methodInfo.Name == nameof(Queryable.OrderBy)
                && methodInfo.GetParameters().Length == 2)
            {
                var sourceType = methodCallExpression.Type.GetGenericArguments()[0];
                if (typeof(IHasIntKey).IsAssignableFrom(sourceType))
                {
                    var lambdaExpression = (LambdaExpression)((UnaryExpression)methodCallExpression.Arguments[1]).Operand;
                    var entityParameterExpression = lambdaExpression.Parameters[0];

                    return Expression.Call(
                        ThenByMethod.MakeGenericMethod(
                            sourceType,
                            typeof(int)),
                        base.VisitMethodCall(methodCallExpression),
                        Expression.Lambda(
                            typeof(Func<,>).MakeGenericType(entityParameterExpression.Type, typeof(int)),
                            Expression.Property(entityParameterExpression, nameof(IHasIntKey.Id)),
                            entityParameterExpression));
                }
            }

            return base.VisitMethodCall(methodCallExpression);
        }
    }
}

Bu muhtemelen oldukça karmaşık görünüyor ve öyle! İfade ağaçlarıyla çalışmak genellikle kolay değildir. Şimdi neler olduğuna bakalım:

  • Temel olarak, kesme noktası bir ExpressionVisitorkapsüller. Ziyaretçi, sorgu ifade ağacında bir yönteme çağrı olduğunda çağrılan öğesini geçersiz kılar VisitMethodCall.

  • Ziyaretçi, bunun ilgilendiğimiz yönteme OrderBy yapılan bir çağrı olup olmadığını denetler.

  • Bu durumda, ziyaretçi genel yöntem çağrısının arabirimimizi IHasIntKey uygulayan bir tür olup olmadığını daha fazla denetler.

  • Bu noktada yöntem çağrısının biçiminde OrderBy(e => ...)olduğunu biliyoruz. Bu çağrıdan lambda ifadesini ayıklar ve bu ifadede kullanılan parametresini (yani , e) alırız.

  • Şimdi oluşturucu yöntemini kullanarak Expression.Call yeni MethodCallExpression bir derleme yapıyoruz. Bu durumda, çağrılan yöntem şeklindedir ThenBy(e => e.Id). Bunu, yukarıda ayıklanan parametresini ve arabiriminin IdIHasIntKey özelliğine bir özellik erişimi kullanarak oluştururuz.

  • Bu çağrının girişi özgün OrderBy(e => ...)öğesidir ve sonuç için OrderBy(e => ...).ThenBy(e => e.Id)bir ifadedir.

  • Bu değiştirilen ifade ziyaretçiden döndürülür; başka bir deyişle LINQ sorgusu artık bir ThenBy çağrı içerecek şekilde uygun şekilde değiştirilmiştir.

  • EF Core devam eder ve bu sorgu ifadesini kullanılan veritabanı için uygun SQL'de derler.

Bu kesme noktası, ilk örnektekiyle aynı şekilde kaydedilir. Yürütme GetPageOfCustomers işlemi şimdi aşağıdaki SQL'i oluşturur:

SELECT "c"."Id", "c"."City", "c"."Name", "c"."PhoneNumber"
FROM "Customers" AS "c"
ORDER BY "c"."City", "c"."Id"
LIMIT @__p_1 OFFSET @__p_0

Bu, aynı Cityolan birden çok müşteri olsa bile artık her zaman kararlı bir sıralama üretecektir.

Vay! Bu, sorguda basit bir değişiklik yapmak için çok fazla kod sağlar. Daha da kötüsü, tüm sorgular için çalışmayabilir. Olması gereken tüm sorgu şekillerini tanıyan ve olmaması gerekenlerin hiçbirini tanımayan bir ifade ziyaretçisi yazmak oldukça zordur. Örneğin, sıralama bir alt sorguda yapılırsa bu büyük olasılıkla işe yaramaz.

Bu, bizi kesme noktalarıyla ilgili kritik bir noktaya getirir; her zaman kendinize istediğinizi yapmanın daha kolay bir yolu olup olmadığını sorun. Kesiciler güçlü olsa da, işleri yanlış yapmak kolaydır. Onlar, dedikleri gibi, kendini ayağından vurmanın kolay bir yolu.

Örneğin, bunun yerine yöntemimizi GetPageOfCustomers şu şekilde değiştirip değiştirmediğimize bakın:

Task<List<Customer>> GetPageOfCustomers2(string sortProperty, int page)
{
    using var context = new CustomerContext();

    return context.Customers
        .OrderBy(e => EF.Property<object>(e, sortProperty))
        .ThenBy(e => e.Id)
        .Skip(page * 20).Take(20).ToListAsync();
}

Bu durumda, ThenBy basitçe sorguya eklenir. Evet, her sorgu için ayrı ayrı yapılması gerekebilir, ancak basit, anlaşılması kolaydır ve her zaman çalışır.

İyimser eşzamanlılık kesme

Bahşiş

Burada gösterilen kod, OptimisticConcurrencyInterceptionSample.cs dosyasından gelir.

EF Core, bir güncelleştirmeden veya silmeden gerçekten etkilenen satır sayısının, etkilenmesi beklenen satır sayısıyla aynı olup olmadığını denetleyerek iyimser eşzamanlılık düzenini destekler. Bu genellikle eşzamanlılık belirteci ile birlikte kullanılır; başka bir ifadeyle, yalnızca beklenen değer okunduktan sonra satır güncelleştirilmediyse beklenen değeriyle eşleşecek bir sütun değeridir.

EF, bir DbUpdateConcurrencyExceptionoluşturarak iyimser eşzamanlılık ihlaline işaret eder. EF7'de, ISaveChangesInterceptor yeni yöntemleri ThrowingConcurrencyException vardır ve ThrowingConcurrencyExceptionAsync bu yöntemler atmadan önce DbUpdateConcurrencyException çağrılır. Bu kesme noktaları, ihlali çözmek için büyük olasılıkla zaman uyumsuz veritabanı değişiklikleriyle birlikte özel durumun gizlenmesine olanak sağlar.

Örneğin, iki istek aynı varlığı hemen hemen aynı anda silmeyi denerse, veritabanındaki satır artık mevcut olmadığından ikinci silme işlemi başarısız olabilir. Bu sorun olmayabilir; sonuçta varlığın yine de silinmiş olması gerekir. Aşağıdaki kesme noktası bunun nasıl yapılabilmesini gösterir:

public class SuppressDeleteConcurrencyInterceptor : ISaveChangesInterceptor
{
    public InterceptionResult ThrowingConcurrencyException(
        ConcurrencyExceptionEventData eventData,
        InterceptionResult result)
    {
        if (eventData.Entries.All(e => e.State == EntityState.Deleted))
        {
            Console.WriteLine("Suppressing Concurrency violation for command:");
            Console.WriteLine(((RelationalConcurrencyExceptionEventData)eventData).Command.CommandText);

            return InterceptionResult.Suppress();
        }

        return result;
    }

    public ValueTask<InterceptionResult> ThrowingConcurrencyExceptionAsync(
        ConcurrencyExceptionEventData eventData,
        InterceptionResult result,
        CancellationToken cancellationToken = default)
        => new(ThrowingConcurrencyException(eventData, result));
}

Bu kesme noktası hakkında dikkate değer birkaç şey vardır:

  • Hem zaman uyumlu hem de zaman uyumsuz kesme yöntemleri uygulanır. Uygulama veya SaveChangesAsyncçağrısı SaveChanges yapabilirse bu önemlidir. Ancak, tüm uygulama kodu zaman uyumsuzsa, yalnızca ThrowingConcurrencyExceptionAsync uygulanması gerekir. Benzer şekilde, uygulama hiçbir zaman zaman uyumsuz veritabanı yöntemlerini kullanmıyorsa, yalnızca ThrowingConcurrencyException uygulanması gerekir. Bu genellikle eşitleme ve zaman uyumsuz yöntemlere sahip tüm kesme makineleri için geçerlidir. (Bazı eşitleme/zaman uyumsuz kodların ürpermesi durumunda uygulamanızın oluşturmak için kullanmadığı yöntemi uygulamak faydalı olabilir.)
  • Kesme noktasının, kaydedilen varlıklar için nesnelere EntityEntry erişimi vardır. Bu durumda, silme işlemi için eşzamanlılık ihlali olup olmadığını denetlemek için kullanılır.
  • Uygulama ilişkisel veritabanı sağlayıcısı kullanıyorsa, ConcurrencyExceptionEventData nesne bir RelationalConcurrencyExceptionEventData nesneye yayınlanabilir. Bu, gerçekleştirilen veritabanı işlemi hakkında ilişkisel olarak ek bilgiler sağlar. Bu durumda, ilişkisel komut metni konsola yazdırılır.
  • Döndüren InterceptionResult.Suppress() , EF Core'a yapmak üzere olduğu eylemi gizlemesini bildirir; bu durumda, DbUpdateConcurrencyExceptionoluşturur. EF Core'un davranışını değiştirmek, EF Core'un ne yaptığını gözlemlemek yerine, kesme noktası avcılarının en güçlü özelliklerinden biridir.

bağlantı dizesi yavaş başlatma

Bahşiş

Burada gösterilen kod Lazy Bağlan ionStringSample.cs dosyasından gelir.

Bağlan ion dizeleri genellikle bir yapılandırma dosyasından okunan statik varlıklardır. Bunlar, yapılandırırken kolayca veya UseSqlServer benzer bir DbContextşekilde geçirilebilir. Ancak, bazen bağlantı dizesi her bağlam örneği için değişebilir. Örneğin, çok kiracılı bir sistemdeki her kiracının farklı bir bağlantı dizesi olabilir.

EF7, geliştirmeleri aracılığıyla dinamik bağlantıları ve bağlantı dizesi işlemeyi IDbConnectionInterceptorkolaylaştırır. Bu, herhangi bir bağlantı dizesi olmadan öğesini DbContext yapılandırma özelliğiyle başlar. Örnek:

services.AddDbContext<CustomerContext>(
    b => b.UseSqlServer());

Bu yöntemlerden IDbConnectionInterceptor biri, kullanılmadan önce bağlantıyı yapılandırmak için uygulanabilir. ConnectionOpeningAsyncbağlantı dizesi almak, erişim belirteci bulmak vb. için zaman uyumsuz bir işlem gerçekleştirebildiğinden iyi bir seçimdir. Örneğin, kapsamı geçerli kiracıyı anlayan geçerli istek kapsamında bir hizmet düşünün:

services.AddScoped<ITenantConnectionStringFactory, TestTenantConnectionStringFactory>();

Uyarı

Bir bağlantı dizesi, erişim belirteci veya benzer bir zaman uyumsuz aramanın her gerektiğinde gerçekleştirilmesi çok yavaş olabilir. Bu öğeleri önbelleğe almayı ve yalnızca önbelleğe alınan dizeyi veya belirteci düzenli aralıklarla yenilemeyi göz önünde bulundurun. Örneğin, erişim belirteçleri genellikle yenilenmesi gerekmeden önce önemli bir süre kullanılabilir.

Bu, oluşturucu eklemesi kullanılarak her DbContext örneğe eklenebilir:

public class CustomerContext : DbContext
{
    private readonly ITenantConnectionStringFactory _connectionStringFactory;

    public CustomerContext(
        DbContextOptions<CustomerContext> options,
        ITenantConnectionStringFactory connectionStringFactory)
        : base(options)
    {
        _connectionStringFactory = connectionStringFactory;
    }

    // ...
}

Bu hizmet daha sonra bağlam için kesme noktası uygulaması oluşturulurken kullanılır:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.AddInterceptors(
        new ConnectionStringInitializationInterceptor(_connectionStringFactory));

Son olarak, kesme noktası bağlantı dizesi zaman uyumsuz olarak almak ve bağlantı ilk kez kullanıldığında ayarlamak için bu hizmeti kullanır:

public class ConnectionStringInitializationInterceptor : DbConnectionInterceptor
{
    private readonly IClientConnectionStringFactory _connectionStringFactory;

    public ConnectionStringInitializationInterceptor(IClientConnectionStringFactory connectionStringFactory)
    {
        _connectionStringFactory = connectionStringFactory;
    }

    public override InterceptionResult ConnectionOpening(
        DbConnection connection,
        ConnectionEventData eventData,
        InterceptionResult result)
        => throw new NotSupportedException("Synchronous connections not supported.");

    public override async ValueTask<InterceptionResult> ConnectionOpeningAsync(
        DbConnection connection, ConnectionEventData eventData, InterceptionResult result,
        CancellationToken cancellationToken = new())
    {
        if (string.IsNullOrEmpty(connection.ConnectionString))
        {
            connection.ConnectionString = (await _connectionStringFactory.GetConnectionStringAsync(cancellationToken));
        }

        return result;
    }
}

Dekont

bağlantı dizesi yalnızca ilk kez bağlantı kullanıldığında elde edilir. Bundan sonra, üzerinde DbConnection depolanan bağlantı dizesi yeni bir bağlantı dizesi aramadan kullanılır.

Bahşiş

Bu kesme noktası, bağlantı dizesi almak için hizmetin zaman uyumsuz bir kod yolundan çağrılması gerektiğinden, oluşturulacak zaman ConnectionOpening uyumsuz yöntemi geçersiz kılar.

SQL Server sorgu istatistiklerini günlüğe kaydetme

Bahşiş

Burada gösterilen kod QueryStatisticsLoggerSample.cs dosyasından gelir.

Son olarak, sql server sorgu istatistiklerini uygulama günlüğüne göndermek için birlikte çalışan iki kesme noktası oluşturalım. İstatistikleri oluşturmak için iki şey yapmamız gerekir IDbCommandInterceptor .

İlk olarak, kesme noktası komutları ile SET STATISTICS IO ONön ekleyecek ve sql server'a bir sonuç kümesi tüketildikten sonra istemciye istatistik göndermesini söyler:

public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
    DbCommand command,
    CommandEventData eventData,
    InterceptionResult<DbDataReader> result,
    CancellationToken cancellationToken = default)
{
    command.CommandText = "SET STATISTICS IO ON;" + Environment.NewLine + command.CommandText;

    return new(result);
}

İkinci olarak, kesme noktası, sonuçların tüketilmesi tamamlandıktan sonra DbDataReader ancak kapatılmadan önce çağrılan yeni DataReaderClosingAsync yöntemi uygular. SQL Server istatistik gönderirken, bunları okuyucuda ikinci bir sonuca yerleştirir, bu nedenle bu noktada kesme noktası, bağlantıda istatistikleri dolduran çağrı NextResultAsync yaparak sonucu okur.

public override async ValueTask<InterceptionResult> DataReaderClosingAsync(
    DbCommand command,
    DataReaderClosingEventData eventData,
    InterceptionResult result)
{
    await eventData.DataReader.NextResultAsync();

    return result;
}

İkinci kesme noktası, bağlantıdan istatistikleri almak ve bunları uygulamanın günlükçüsine yazmak için gereklidir. Bunun için yeni ConnectionCreated yöntemi uygulayan bir IDbConnectionInterceptorkullanacağız. ConnectionCreated , EF Core bir bağlantı oluşturduktan hemen sonra çağrılır ve bu nedenle bu bağlantının ek yapılandırmasını gerçekleştirmek için kullanılabilir. Bu durumda, kesme noktası bir ILogger alır ve sonra iletileri günlüğe kaydetmek için olaya takılır SqlConnection.InfoMessage .

public override DbConnection ConnectionCreated(ConnectionCreatedEventData eventData, DbConnection result)
{
    var logger = eventData.Context!.GetService<ILoggerFactory>().CreateLogger("InfoMessageLogger");
    ((SqlConnection)eventData.Connection).InfoMessage += (_, args) =>
    {
        logger.LogInformation(1, args.Message);
    };
    return result;
}

Önemli

ConnectionCreating ve ConnectionCreated yöntemleri yalnızca EF Core bir DbConnectionoluşturduğunda çağrılır. Uygulama oluşturur DbConnection ve EF Core'a geçirirse çağrılmaz.

Bu kesicileri kullanan bazı kodlar çalıştırılırken günlükte SQL Server sorgu istatistikleri gösterilir:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (4ms) [Parameters=[@p0='?' (Size = 4000), @p1='?' (Size = 4000), @p2='?' (Size = 4000), @p3='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET STATISTICS IO ON;
      SET IMPLICIT_TRANSACTIONS OFF;
      SET NOCOUNT ON;
      MERGE [Customers] USING (
      VALUES (@p0, @p1, 0),
      (@p2, @p3, 1)) AS i ([Name], [PhoneNumber], _Position) ON 1=0
      WHEN NOT MATCHED THEN
      INSERT ([Name], [PhoneNumber])
      VALUES (i.[Name], i.[PhoneNumber])
      OUTPUT INSERTED.[Id], i._Position;
info: InfoMessageLogger[1]
      Table 'Customers'. Scan count 0, logical reads 5, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SET STATISTICS IO ON;
      SELECT TOP(2) [c].[Id], [c].[Name], [c].[PhoneNumber]
      FROM [Customers] AS [c]
      WHERE [c].[Name] = N'Alice'
info: InfoMessageLogger[1]
      Table 'Customers'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.

Sorgu geliştirmeleri

EF7, LINQ sorgularının çevirisinde birçok geliştirme içerir.

Son işleç olarak GroupBy

Bahşiş

Burada gösterilen kod GroupByFinalOperatorSample.cs dosyasından gelir.

EF7, sorguda son işleç olarak kullanmayı GroupBy destekler. Örneğin, bu LINQ sorgusu:

var query = context.Books.GroupBy(s => s.Price);

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

SELECT [b].[Price], [b].[Id], [b].[AuthorId]
FROM [Books] AS [b]
ORDER BY [b].[Price]

Dekont

Bu tür GroupBy doğrudan SQL'e çevrilmediğinden EF Core, döndürülen sonuçlarda gruplandırma yapar. Ancak bu, sunucudan herhangi bir ek veri aktarılmasına neden olmaz.

Son işleç olarak GroupJoin

Bahşiş

Burada gösterilen kod GroupJoinFinalOperatorSample.cs dosyasından gelir.

EF7, sorguda son işleç olarak kullanmayı GroupJoin destekler. Örneğin, bu LINQ sorgusu:

var query = context.Customers.GroupJoin(
    context.Orders, c => c.Id, o => o.CustomerId, (c, os) => new { Customer = c, Orders = os });

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

SELECT [c].[Id], [c].[Name], [t].[Id], [t].[Amount], [t].[CustomerId]
FROM [Customers] AS [c]
OUTER APPLY (
    SELECT [o].[Id], [o].[Amount], [o].[CustomerId]
    FROM [Orders] AS [o]
    WHERE [c].[Id] = [o].[CustomerId]
) AS [t]
ORDER BY [c].[Id]

GroupBy varlık türü

Bahşiş

Burada gösterilen kod GroupByEntityTypeSample.cs dosyasından gelir.

EF7, varlık türüne göre gruplandırma işlemini destekler. Örneğin, bu LINQ sorgusu:

var query = context.Books
    .GroupBy(s => s.Author)
    .Select(s => new { Author = s.Key, MaxPrice = s.Max(p => p.Price) });

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

SELECT [a].[Id], [a].[Name], MAX([b].[Price]) AS [MaxPrice]
FROM [Books] AS [b]
INNER JOIN [Author] AS [a] ON [b].[AuthorId] = [a].[Id]
GROUP BY [a].[Id], [a].[Name]

Birincil anahtar gibi benzersiz bir özelliğe göre gruplandırma işleminin her zaman varlık türüne göre gruplandırmaktan daha verimli olacağını unutmayın. Ancak, varlık türlerine göre gruplandırma hem anahtarlı hem de anahtarsız varlık türleri için kullanılabilir.

Ayrıca, her varlığın benzersiz bir anahtar değerine sahip olması gerektiğinden, birincil anahtara sahip bir varlık türüne göre gruplandırma her zaman varlık örneği başına bir grupla sonuçlanır. Bazen içinde gruplandırma gerekli olmayacak şekilde sorgunun kaynağını değiştirmeye değer. Örneğin, aşağıdaki sorgu önceki sorguyla aynı sonuçları döndürür:

var query = context.Authors
    .Select(a => new { Author = a, MaxPrice = a.Books.Max(b => b.Price) });

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

SELECT [a].[Id], [a].[Name], (
    SELECT MAX([b].[Price])
    FROM [Books] AS [b]
    WHERE [a].[Id] = [b].[AuthorId]) AS [MaxPrice]
FROM [Authors] AS [a]

Alt sorgular dış sorgudaki gruplandırılmamış sütunlara başvuramaz

Bahşiş

Burada gösterilen kod UngroupedColumnsQuerySample.cs dosyasından gelir.

EF Core 6.0'da yan GROUP BY tümcesi dış sorgudaki sütunlara başvurarak bazı veritabanlarında başarısız olur ve bazılarında verimsiz olur. Örneğin, aşağıdaki sorguyu göz önünde bulundurun:

var query = from s in (from i in context.Invoices
                       group i by i.History.Month
                       into g
                       select new { Month = g.Key, Total = g.Sum(p => p.Amount), })
            select new
            {
                s.Month, s.Total, Payment = context.Payments.Where(p => p.History.Month == s.Month).Sum(p => p.Amount)
            };

SQL Server'da EF Core 6.0'da bu, şu şekilde çevrildi:

SELECT DATEPART(month, [i].[History]) AS [Month], COALESCE(SUM([i].[Amount]), 0.0) AS [Total], (
    SELECT COALESCE(SUM([p].[Amount]), 0.0)
    FROM [Payments] AS [p]
    WHERE DATEPART(month, [p].[History]) = DATEPART(month, [i].[History])) AS [Payment]
FROM [Invoices] AS [i]
GROUP BY DATEPART(month, [i].[History])

EF7'de çeviri şu şekildedir:

SELECT [t].[Key] AS [Month], COALESCE(SUM([t].[Amount]), 0.0) AS [Total], (
    SELECT COALESCE(SUM([p].[Amount]), 0.0)
    FROM [Payments] AS [p]
    WHERE DATEPART(month, [p].[History]) = [t].[Key]) AS [Payment]
FROM (
    SELECT [i].[Amount], DATEPART(month, [i].[History]) AS [Key]
    FROM [Invoices] AS [i]
) AS [t]
GROUP BY [t].[Key]

Salt okunur koleksiyonlar için kullanılabilir Contains

Bahşiş

Burada gösterilen kod ReadOnlySetQuerySample.cs dosyasından gelir.

EF7, aranacak öğeler veya IReadOnlySet veya IReadOnlyCollectionIReadOnlyListiçinde bulunduğunda kullanmayı Contains destekler. Örneğin, bu LINQ sorgusu:

IReadOnlySet<int> searchIds = new HashSet<int> { 1, 3, 5 };
var query = context.Customers.Where(p => p.Orders.Any(l => searchIds.Contains(l.Id)));

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

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
    SELECT 1
    FROM [Orders] AS [o]
    WHERE [c].[Id] = [o].[Customer1Id] AND [o].[Id] IN (1, 3, 5))

Toplama işlevleri için çeviriler

EF7, sağlayıcıların toplama işlevlerini çevirmesi için daha iyi genişletilebilirlik sağlar. Bu ve bu alandaki diğer çalışmalar, sağlayıcılar arasında aşağıdakiler de dahil olmak üzere birkaç yeni çeviriye neden oldu:

Dekont

Bağımsız değişken üzerinde IEnumerable işlem gören toplama işlevleri genellikle yalnızca sorgularda GroupBy çevrilir. Bu sınırlamanın kaldırılmasını istiyorsanız JSON sütunlarında Uzamsal türleri destekleme için oy verin.

Dize toplama işlevleri

Bahşiş

Burada gösterilen kod StringAggregateFunctionsSample.cs dosyasından gelir.

ve Concat kullanan Join sorgular artık uygun olduğunda çevriliyor. Örnek:

var query = context.Posts
    .GroupBy(post => post.Author)
    .Select(grouping => new { Author = grouping.Key, Books = string.Join("|", grouping.Select(post => post.Title)) });

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

SELECT [a].[Id], [a].[Name], COALESCE(STRING_AGG([p].[Title], N'|'), N'') AS [Books]
FROM [Posts] AS [p]
LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
GROUP BY [a].[Id], [a].[Name]

Diğer dize işlevleriyle birleştirildiğinde, bu çeviriler sunucuda bazı karmaşık dize düzenlemelerine olanak tanır. Örnek:

var query = context.Posts
    .GroupBy(post => post.Author!.Name)
    .Select(
        grouping =>
            new
            {
                PostAuthor = grouping.Key,
                Blogs = string.Concat(
                    grouping
                        .Select(post => post.Blog.Name)
                        .Distinct()
                        .Select(postName => "'" + postName + "' ")),
                ContentSummaries = string.Join(
                    " | ",
                    grouping
                        .Where(post => post.Content.Length >= 10)
                        .Select(post => "'" + post.Content.Substring(0, 10) + "' "))
            });

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

SELECT [t].[Name], (N'''' + [t0].[Name]) + N''' ', [t0].[Name], [t].[c]
FROM (
    SELECT [a].[Name], COALESCE(STRING_AGG(CASE
        WHEN CAST(LEN([p].[Content]) AS int) >= 10 THEN COALESCE((N'''' + COALESCE(SUBSTRING([p].[Content], 0 + 1, 10), N'')) + N''' ', N'')
    END, N' | '), N'') AS [c]
    FROM [Posts] AS [p]
    LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id]
    GROUP BY [a].[Name]
) AS [t]
OUTER APPLY (
    SELECT DISTINCT [b].[Name]
    FROM [Posts] AS [p0]
    LEFT JOIN [Authors] AS [a0] ON [p0].[AuthorId] = [a0].[Id]
    INNER JOIN [Blogs] AS [b] ON [p0].[BlogId] = [b].[Id]
    WHERE [t].[Name] = [a0].[Name] OR ([t].[Name] IS NULL AND [a0].[Name] IS NULL)
) AS [t0]
ORDER BY [t].[Name]

Uzamsal toplama işlevleri

Bahşiş

Burada gösterilen kod SpatialAggregateFunctionsSample.cs dosyasından gelir.

Artık NetTopologySuite'ı destekleyen veritabanı sağlayıcılarının aşağıdaki uzamsal toplama işlevlerini çevirmesi mümkündür:

Bahşiş

Bu çeviriler SQL Server ve SQLite için ekip tarafından uygulanmıştır. Diğer sağlayıcılar için, sağlayıcı için uygulandıysa destek eklemek için sağlayıcı bakımcısına başvurun.

Örnek:

var query = context.Caches
    .Where(cache => cache.Location.X < -90)
    .GroupBy(cache => cache.Owner)
    .Select(
        grouping => new { Id = grouping.Key, Combined = GeometryCombiner.Combine(grouping.Select(cache => cache.Location)) });

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

SELECT [c].[Owner] AS [Id], geography::CollectionAggregate([c].[Location]) AS [Combined]
FROM [Caches] AS [c]
WHERE [c].[Location].Long < -90.0E0
GROUP BY [c].[Owner]

İstatistiksel toplama işlevleri

Bahşiş

Burada gösterilen kod StatisticalAggregateFunctionsSample.cs dosyasından gelir.

SQL Server çevirileri aşağıdaki istatistiksel işlevler için uygulanmıştır:

Bahşiş

Bu çeviriler SQL Server için ekip tarafından uygulanmıştır. Diğer sağlayıcılar için, sağlayıcı için uygulandıysa destek eklemek için sağlayıcı bakımcısına başvurun.

Örnek:

var query = context.Downloads
    .GroupBy(download => download.Uploader.Id)
    .Select(
        grouping => new
        {
            Author = grouping.Key,
            TotalCost = grouping.Sum(d => d.DownloadCount),
            AverageViews = grouping.Average(d => d.DownloadCount),
            VariancePopulation = EF.Functions.VariancePopulation(grouping.Select(d => d.DownloadCount)),
            VarianceSample = EF.Functions.VarianceSample(grouping.Select(d => d.DownloadCount)),
            StandardDeviationPopulation = EF.Functions.StandardDeviationPopulation(grouping.Select(d => d.DownloadCount)),
            StandardDeviationSample = EF.Functions.StandardDeviationSample(grouping.Select(d => d.DownloadCount))
        });

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

SELECT [u].[Id] AS [Author], COALESCE(SUM([d].[DownloadCount]), 0) AS [TotalCost], AVG(CAST([d].[DownloadCount] AS float)) AS [AverageViews], VARP([d].[DownloadCount]) AS [VariancePopulation], VAR([d].[DownloadCount]) AS [VarianceSample], STDEVP([d].[DownloadCount]) AS [StandardDeviationPopulation], STDEV([d].[DownloadCount]) AS [StandardDeviationSample]
FROM [Downloads] AS [d]
INNER JOIN [Uploader] AS [u] ON [d].[UploaderId] = [u].[Id]
GROUP BY [u].[Id]

Çevirisi string.IndexOf

Bahşiş

Burada gösterilen kod, MiscellaneousTranslationsSample.cs dosyasından gelir.

EF7 artık LINQ sorgularında çevrilmektedir String.IndexOf . Örnek:

var query = context.Posts
    .Select(post => new { post.Title, IndexOfEntity = post.Content.IndexOf("Entity") })
    .Where(post => post.IndexOfEntity > 0);

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

SELECT [p].[Title], CAST(CHARINDEX(N'Entity', [p].[Content]) AS int) - 1 AS [IndexOfEntity]
FROM [Posts] AS [p]
WHERE (CAST(CHARINDEX(N'Entity', [p].[Content]) AS int) - 1) > 0

Varlık türleri için çevirisi GetType

Bahşiş

Burada gösterilen kod, MiscellaneousTranslationsSample.cs dosyasından gelir.

EF7 artık LINQ sorgularında çevrilmektedir Object.GetType() . Örnek:

var query = context.Posts.Where(post => post.GetType() == typeof(Post));

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

SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
FROM [Posts] AS [p]
WHERE [p].[Discriminator] = N'Post'

Bu sorgunun türetilmiş türlerin değil, yalnızca Post türünde Postörnekleri döndürdüğüne dikkat edin. Bu, veya OfTypekullanan is ve türetilmiş türlerin örneklerini de döndürecek bir sorgudan farklıdır. Örneğin, şu sorguyu göz önünde bulundurun:

var query = context.Posts.OfType<Post>();

Bu, farklı SQL'e çevrilir:

      SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
      FROM [Posts] AS [p]

Ve hem hem FeaturedPost de Post varlıkları döndürür.

Için destek AT TIME ZONE

Bahşiş

Burada gösterilen kod, MiscellaneousTranslationsSample.cs dosyasından gelir.

EF7, ve DateTimeOffsetiçin DateTime yeni AtTimeZone işlevler sağlar. Bu işlevler, oluşturulan SQL'deki yan tümcelere çevrilir AT TIME ZONE . Örnek:

var query = context.Posts
    .Select(
        post => new
        {
            post.Title,
            PacificTime = EF.Functions.AtTimeZone(post.PublishedOn, "Pacific Standard Time"),
            UkTime = EF.Functions.AtTimeZone(post.PublishedOn, "GMT Standard Time"),
        });

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

SELECT [p].[Title], [p].[PublishedOn] AT TIME ZONE 'Pacific Standard Time' AS [PacificTime], [p].[PublishedOn] AT TIME ZONE 'GMT Standard Time' AS [UkTime]
FROM [Posts] AS [p]

Bahşiş

Bu çeviriler SQL Server için ekip tarafından uygulanmıştır. Diğer sağlayıcılar için, sağlayıcı için uygulandıysa destek eklemek için sağlayıcı bakımcısına başvurun.

Filtrelenmiş Gizli gezintilere ekle

Bahşiş

Burada gösterilen kod, MiscellaneousTranslationsSample.cs dosyasından gelir.

Include yöntemleri artık ile EF.Propertykullanılabilir. Bu, özel gezinti özellikleri veya alanlar tarafından temsil edilen özel gezintiler için bile filtrelemeye ve sıralamaya olanak tanır. Örnek:

var query = context.Blogs.Include(
    blog => EF.Property<ICollection<Post>>(blog, "Posts")
        .Where(post => post.Content.Contains(".NET"))
        .OrderBy(post => post.Title));

Bu, şuna eşdeğerdir:

var query = context.Blogs.Include(
    blog => Posts
        .Where(post => post.Content.Contains(".NET"))
        .OrderBy(post => post.Title));

Ancak genel olarak erişilebilir olmasını gerektirmez Blog.Posts .

SQL Server kullanırken, yukarıdaki her iki sorgu da şu şekilde çevrilir:

SELECT [b].[Id], [b].[Name], [t].[Id], [t].[AuthorId], [t].[BlogId], [t].[Content], [t].[Discriminator], [t].[PublishedOn], [t].[Title], [t].[PromoText]
FROM [Blogs] AS [b]
LEFT JOIN (
    SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText]
    FROM [Posts] AS [p]
    WHERE [p].[Content] LIKE N'%.NET%'
) AS [t] ON [b].[Id] = [t].[BlogId]
ORDER BY [b].[Id], [t].[Title]

için Cosmos çevirisi Regex.IsMatch

Bahşiş

Burada gösterilen kod CosmosQueriesSample.cs dosyasından gelir.

EF7, Azure Cosmos DB'ye yönelik LINQ sorgularında kullanmayı Regex.IsMatch destekler. Örnek:

var containsInnerT = await context.Triangles
    .Where(o => Regex.IsMatch(o.Name, "[a-z]t[a-z]", RegexOptions.IgnoreCase))
    .ToListAsync();

Aşağıdaki SQL'e çevrilir:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND RegexMatch(c["Name"], "[a-z]t[a-z]", "i"))

DbContext API ve davranış geliştirmeleri

EF7, ve ilgili sınıflar için DbContext çeşitli küçük iyileştirmeler içerir.

Bahşiş

Bu bölümdeki örneklerin kodu DbContextApiSample.cs dosyasından alınıyor.

Başlatılmamış DbSet özellikleri için suppressor

üzerinde genel, ayarlanabilir DbSet özellikler DbContext , oluşturulurken DbContext EF Core tarafından otomatik olarak başlatılır. Örneğin, aşağıdaki DbContext tanımı göz önünde bulundurun:

public class SomeDbContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
}

özelliği, Blogs örneği oluşturmanın DbContext bir parçası olarak bir DbSet<Blog> örneğe ayarlanır. Bu, bağlamın ek adımlar olmadan sorgular için kullanılmasını sağlar.

Ancak, C# null atanabilir başvuru türlerinin kullanıma sunulmasının ardından, derleyici artık null atanamayan özelliğin Blogs başlatılmadığı konusunda uyarır:

[CS8618] Non-nullable property 'Blogs' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Bu sahte bir uyarıdır; özelliği EF Core tarafından null olmayan bir değere ayarlanır. Ayrıca, özelliğin null atanabilir olarak bildirilmesi uyarının gitmesini sağlar, ancak kavramsal olarak özellik null atanamaz ve hiçbir zaman null olmadığından bu iyi bir fikir değildir.

EF7, derleyicinin bu uyarıyı oluşturmasını durduran bir DbContext üzerindeki özellikler için DbSet DiagnosticSuppressor içerir.

Bahşiş

Bu desen, C# otomatik özelliklerinin çok sınırlı olduğu günlerde ortaya çıkmıştır. Modern C# ile otomatik özellikleri salt okunur yapmayı ve sonra bunları oluşturucuda açıkça başlatmayı DbContext veya gerektiğinde önbelleğe alınmış DbSet örneği bağlamdan almayı göz önünde bulundurun. Örneğin, public DbSet<Blog> Blogs => Set<Blog>().

İptali günlüklerdeki hatadan ayırt etme

Bazen bir uygulama bir sorguyu veya başka bir veritabanı işlemini açıkça iptal eder. Bu genellikle işlemi gerçekleştiren yöntemine geçirilen bir CancellationToken kullanılarak yapılır.

EF Core 6'da, bir işlem iptal edildiğinde günlüğe kaydedilen olaylar, işlem başka bir nedenle başarısız olduğunda günlüğe kaydedilen olaylarla aynıdır. EF7, iptal edilen veritabanı işlemleri için özel olarak yeni günlük olaylarını tanıtır. Bu yeni olaylar varsayılan olarak düzeyinde günlüğe Debug kaydedilir. Aşağıdaki tabloda ilgili olaylar ve bunların varsayılan günlük düzeyleri gösterilmektedir:

Olay Tanım Varsayılan günlük düzeyi
CoreEventId.QueryIterationFailed Sorgu sonuçları işlenirken bir hata oluştu. LogLevel.Error
CoreEventId.SaveChangesFailed Değişiklikleri veritabanına kaydetmeye çalışırken bir hata oluştu. LogLevel.Error
RelationalEventId.CommandError Veritabanı komutu yürütülürken bir hata oluştu. LogLevel.Error
CoreEventId.QueryCanceled Sorgu iptal edildi. LogLevel.Debug
CoreEventId.SaveChangesCanceled Değişiklikleri kaydetmeye çalışılırken veritabanı komutu iptal edildi. LogLevel.Debug
RelationalEventId.CommandCanceled yürütmesi DbCommand iptal edildi. LogLevel.Debug

Dekont

İptal belirtecini denetlemek yerine özel duruma bakılarak iptal algılanır. Bu, iptal belirteci aracılığıyla tetiklenmeyen iptallerin yine de algılanacağı ve bu şekilde günlüğe kaydedileceği anlamına gelir.

Yöntemler için EntityEntry yeni IProperty ve INavigation aşırı yüklemeler

EF modeliyle çalışan kodun genellikle bir IProperty veya INavigation temsil eden özelliği veya gezinti meta verileri olur. Ardından bir EntityEntry özelliği/gezinti değerini almak veya durumunu sorgulamak için kullanılır. Ancak, EF7'nin öncesinde, bu işlem özelliğin adını veya gezintiyi yöntemine EntityEntrygeçirerek veya INavigationöğesini yeniden ararIProperty. EF7'de, IProperty ek aramadan kaçınarak veya INavigation doğrudan geçirilebilir.

Örneğin, belirli bir varlığın tüm eşdüzeylerini bulmak için bir yöntem düşünün:

public static IEnumerable<TEntity> FindSiblings<TEntity>(
    this DbContext context, TEntity entity, string navigationToParent)
    where TEntity : class
{
    var parentEntry = context.Entry(entity).Reference(navigationToParent);

    return context.Entry(parentEntry.CurrentValue!)
        .Collection(parentEntry.Metadata.Inverse!)
        .CurrentValue!
        .OfType<TEntity>()
        .Where(e => !ReferenceEquals(e, entity));
}

Bu yöntem, belirli bir varlığın üst öğesini bulur ve ardından üst girişin Collection yöntemine tersini INavigation geçirir. Bu meta veriler daha sonra verilen üst öğesinin tüm eşdüzeylerini döndürmek için kullanılır. Kullanım örneği aşağıda verilmiştir:


Console.WriteLine($"Siblings to {post.Id}: '{post.Title}' are...");
foreach (var sibling in context.FindSiblings(post, nameof(post.Blog)))
{
    Console.WriteLine($"    {sibling.Id}: '{sibling.Title}'");
}

Ve çıkış:

Siblings to 1: 'Announcing Entity Framework 7 Preview 7: Interceptors!' are...
    5: 'Productivity comes to .NET MAUI in Visual Studio 2022'
    6: 'Announcing .NET 7 Preview 7'
    7: 'ASP.NET Core updates in .NET 7 Preview 7'

EntityEntry paylaşılan tür varlık türleri için

EF Core, birden çok farklı varlık türü için aynı CLR türünü kullanabilir. Bunlar "paylaşılan tür varlık türleri" olarak bilinir ve genellikle bir sözlük türünü varlık türünün özellikleri için kullanılan anahtar/değer çiftleriyle eşlemek için kullanılır. Örneğin, bir BuildMetadata varlık türü ayrılmış bir CLR türü tanımlamadan tanımlanabilir:

modelBuilder.SharedTypeEntity<Dictionary<string, object>>(
    "BuildMetadata", b =>
    {
        b.IndexerProperty<int>("Id");
        b.IndexerProperty<string>("Tag");
        b.IndexerProperty<Version>("Version");
        b.IndexerProperty<string>("Hash");
        b.IndexerProperty<bool>("Prerelease");
    });

Paylaşılan tür varlık türünün adlandırılması gerektiğine dikkat edin. Bu durumda, adı şeklindedir BuildMetadata. Bu varlık türlerine daha sonra ad kullanılarak elde edilen varlık türü için bir DbSet kullanılarak erişilir. Örnek:

public DbSet<Dictionary<string, object>> BuildMetadata
    => Set<Dictionary<string, object>>("BuildMetadata");

Bu DbSet , varlık örneklerini izlemek için kullanılabilir:

await context.BuildMetadata.AddAsync(
    new Dictionary<string, object>
    {
        { "Tag", "v7.0.0-rc.1.22426.7" },
        { "Version", new Version(7, 0, 0) },
        { "Prerelease", true },
        { "Hash", "dc0f3e8ef10eb1464b27f0fd4704f53c01226036" }
    });

Ve sorguları yürüt:

var builds = await context.BuildMetadata
    .Where(metadata => !EF.Property<bool>(metadata, "Prerelease"))
    .OrderBy(metadata => EF.Property<string>(metadata, "Tag"))
    .ToListAsync();

Şimdi EF7'de, henüz izlenmese bile bir örneğin durumunu elde etmek için kullanılabilecek bir yöntem DbSet de Entry vardır. Örnek:

var state = context.BuildMetadata.Entry(build).State;

ContextInitialized şu anda şu şekilde günlüğe kaydedilir: Debug

EF7'de olay ContextInitialized düzeyinde günlüğe Debug kaydedilir. Örnek:

dbug: 10/7/2022 12:27:52.379 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
      Entity Framework Core 7.0.0 initialized 'BlogsContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:7.0.0' with options: SensitiveDataLoggingEnabled using NetTopologySuite

Önceki sürümlerde, düzeyinde günlüğe Information kaydedildi. Örnek:

info: 10/7/2022 12:30:34.757 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
      Entity Framework Core 7.0.0 initialized 'BlogsContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:7.0.0' with options: SensitiveDataLoggingEnabled using NetTopologySuite

İsterseniz günlük düzeyi olarak Informationdeğiştirilebilir:

optionsBuilder.ConfigureWarnings(
    builder =>
    {
        builder.Log((CoreEventId.ContextInitialized, LogLevel.Information));
    });

IEntityEntryGraphIterator genel kullanıma açık

EF7'de hizmet IEntityEntryGraphIterator uygulamalar tarafından kullanılabilir. Bu, hem izlenip hem de tarafından TrackGraphizlenen varlıkların grafiğini keşfederken dahili olarak kullanılan hizmettir. Aşağıda, bir başlangıç varlığından ulaşılabilen tüm varlıklar üzerinde yinelemeye yönelik bir örnek verilmiştir:

var blogEntry = context.ChangeTracker.Entries<Blog>().First();
var found = new HashSet<object>();
var iterator = context.GetService<IEntityEntryGraphIterator>();
iterator.TraverseGraph(new EntityEntryGraphNode<HashSet<object>>(blogEntry, found, null, null), node =>
{
    if (node.NodeState.Contains(node.Entry.Entity))
    {
        return false;
    }

    Console.Write($"Found with '{node.Entry.Entity.GetType().Name}'");

    if (node.InboundNavigation != null)
    {
        Console.Write($" by traversing '{node.InboundNavigation.Name}' from '{node.SourceEntry!.Entity.GetType().Name}'");
    }

    Console.WriteLine();

    node.NodeState.Add(node.Entry.Entity);

    return true;
});

Console.WriteLine();
Console.WriteLine($"Finished iterating. Found {found.Count} entities.");
Console.WriteLine();

Not:

  • Geri çağırma temsilcisi döndürdüğünde falseyineleyici belirli bir düğümden geçişi durdurur. Bu örnek, ziyaret edilen varlıkları izler ve varlık zaten ziyaret edildiğinde döndürür false . Bu, grafikteki döngülerden kaynaklanan sonsuz döngüleri önler.
  • nesnesi, EntityEntryGraphNode<TState> durumun temsilciye yakalanmadan geçirilmesini sağlar.
  • İlk düğüm dışında ziyaret edilen her düğüm için, bulunduğu düğüm ve aracılığıyla bulunduğu gezinti geri çağırmaya geçirilir.

Model oluşturma geliştirmeleri

EF7, model oluşturmada çeşitli küçük geliştirmeler içerir.

Bahşiş

Bu bölümdeki örneklerin kodu ModelBuildingSample.cs dosyasından alınıyor.

Dizinler artan veya azalan olabilir

Varsayılan olarak EF Core artan dizinler oluşturur. EF7, azalan dizinlerin oluşturulmasını da destekler. Örnek:

modelBuilder
    .Entity<Post>()
    .HasIndex(post => post.Title)
    .IsDescending();

Veya eşleme özniteliğini Index kullanarak:

[Index(nameof(Title), AllDescending = true)]
public class Post
{
    public int Id { get; set; }

    [MaxLength(64)]
    public string? Title { get; set; }
}

Veritabanı her iki yönde de sıralama için aynı dizini kullanabileceğinden, bu özellik tek bir sütundaki dizinler için nadiren yararlıdır. Ancak, her sütundaki sıranın önemli olabileceği birden çok sütundaki bileşik dizinler için bu durum geçerli değildir. EF Core, birden çok sütunun her sütun için tanımlanmış farklı sıralamaya sahip olmasını sağlayarak bunu destekler. Örnek:

modelBuilder
    .Entity<Blog>()
    .HasIndex(blog => new { blog.Name, blog.Owner })
    .IsDescending(false, true);

Veya eşleme özniteliğini kullanarak:

[Index(nameof(Name), nameof(Owner), IsDescending = new[] { false, true })]
public class Blog
{
    public int Id { get; set; }

    [MaxLength(64)]
    public string? Name { get; set; }

    [MaxLength(64)]
    public string? Owner { get; set; }

    public List<Post> Posts { get; } = new();
}

Bu, SQL Server kullanılırken aşağıdaki SQL'e neden olur:

CREATE INDEX [IX_Blogs_Name_Owner] ON [Blogs] ([Name], [Owner] DESC);

Son olarak, dizin adları verilerek aynı sıralı sütun kümesi üzerinde birden çok dizin oluşturulabilir. Örnek:

modelBuilder
    .Entity<Blog>()
    .HasIndex(blog => new { blog.Name, blog.Owner }, "IX_Blogs_Name_Owner_1")
    .IsDescending(false, true);

modelBuilder
    .Entity<Blog>()
    .HasIndex(blog => new { blog.Name, blog.Owner }, "IX_Blogs_Name_Owner_2")
    .IsDescending(true, true);

Veya eşleme özniteliklerini kullanarak:

[Index(nameof(Name), nameof(Owner), IsDescending = new[] { false, true }, Name = "IX_Blogs_Name_Owner_1")]
[Index(nameof(Name), nameof(Owner), IsDescending = new[] { true, true }, Name = "IX_Blogs_Name_Owner_2")]
public class Blog
{
    public int Id { get; set; }

    [MaxLength(64)]
    public string? Name { get; set; }

    [MaxLength(64)]
    public string? Owner { get; set; }

    public List<Post> Posts { get; } = new();
}

Bu, SQL Server'da aşağıdaki SQL'i oluşturur:

CREATE INDEX [IX_Blogs_Name_Owner_1] ON [Blogs] ([Name], [Owner] DESC);
CREATE INDEX [IX_Blogs_Name_Owner_2] ON [Blogs] ([Name] DESC, [Owner] DESC);

Bileşik anahtarlar için eşleme özniteliği

EF7, herhangi bir varlık türünün birincil anahtar özelliğini veya özelliklerini belirtmek için yeni bir eşleme özniteliği ("veri ek açıklaması") sunar. aksine System.ComponentModel.DataAnnotations.KeyAttribute, PrimaryKeyAttribute anahtar özelliği yerine varlık türü sınıfına yerleştirilir. Örnek:

[PrimaryKey(nameof(PostKey))]
public class Post
{
    public int PostKey { get; set; }
}

Bu, bileşik anahtarları tanımlamak için doğal bir uyum sağlar:

[PrimaryKey(nameof(PostId), nameof(CommentId))]
public class Comment
{
    public int PostId { get; set; }
    public int CommentId { get; set; }
    public string CommentText { get; set; } = null!;
}

Sınıfında dizini tanımlamak, özel özellikleri veya alanları anahtar olarak belirtmek için de kullanılabileceğini, ancak EF modelini oluştururken genellikle yoksayılacağını gösterir. Örnek:

[PrimaryKey(nameof(_id))]
public class Tag
{
    private readonly int _id;
}

DeleteBehavior eşleme özniteliği

EF7, bir ilişkinin belirtilmesi DeleteBehavior için bir eşleme özniteliği ("veri ek açıklaması") sunar. Örneğin, gerekli ilişkiler varsayılan olarak ile DeleteBehavior.Cascade oluşturulur. Bu, kullanılarak DeleteBehaviorAttributevarsayılan olarak olarak DeleteBehavior.NoAction değiştirilebilir:

public class Post
{
    public int Id { get; set; }
    public string? Title { get; set; }

    [DeleteBehavior(DeleteBehavior.NoAction)]
    public Blog Blog { get; set; } = null!;
}

Bu, Blog Gönderileri ilişkisi için art arda silmeleri devre dışı bırakır.

Farklı sütun adlarına eşlenen özellikler

Bazı eşleme desenleri, aynı CLR özelliğinin birden çok farklı tablonun her birindeki bir sütuna eşlenmesine neden olur. EF7, bu sütunların farklı adlara sahip olmasını sağlar. Örneğin, basit bir devralma hiyerarşisini göz önünde bulundurun:

public abstract class Animal
{
    public int Id { get; set; }
    public string Breed { get; set; } = null!;
}

public class Cat : Animal
{
    public string? EducationalLevel { get; set; }
}

public class Dog : Animal
{
    public string? FavoriteToy { get; set; }
}

TPT devralma eşleme stratejisiyle bu türler üç tabloya eşlenir. Ancak, her tablodaki birincil anahtar sütunu farklı bir ada sahip olabilir. Örnek:

CREATE TABLE [Animals] (
    [Id] int NOT NULL IDENTITY,
    [Breed] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])
);

CREATE TABLE [Cats] (
    [CatId] int NOT NULL,
    [EducationalLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId]),
    CONSTRAINT [FK_Cats_Animals_CatId] FOREIGN KEY ([CatId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Dogs] (
    [DogId] int NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId]),
    CONSTRAINT [FK_Dogs_Animals_DogId] FOREIGN KEY ([DogId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);

EF7, bu eşlemenin iç içe tablo oluşturucusu kullanılarak yapılandırılmasına izin verir:

modelBuilder.Entity<Animal>().ToTable("Animals");

modelBuilder.Entity<Cat>()
    .ToTable(
        "Cats",
        tableBuilder => tableBuilder.Property(cat => cat.Id).HasColumnName("CatId"));

modelBuilder.Entity<Dog>()
    .ToTable(
        "Dogs",
        tableBuilder => tableBuilder.Property(dog => dog.Id).HasColumnName("DogId"));

TPC devralma eşlemesi ile Breed özelliği, farklı tablolardaki farklı sütun adlarına da eşlenebilir. Örneğin, aşağıdaki TPC tablolarını göz önünde bulundurun:

CREATE TABLE [Cats] (
    [CatId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [CatBreed] nvarchar(max) NOT NULL,
    [EducationalLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId])
);

CREATE TABLE [Dogs] (
    [DogId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [DogBreed] nvarchar(max) NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId])
);

EF7 şu tablo eşlemesini destekler:

modelBuilder.Entity<Animal>().UseTpcMappingStrategy();

modelBuilder.Entity<Cat>()
    .ToTable(
        "Cats",
        builder =>
        {
            builder.Property(cat => cat.Id).HasColumnName("CatId");
            builder.Property(cat => cat.Breed).HasColumnName("CatBreed");
        });

modelBuilder.Entity<Dog>()
    .ToTable(
        "Dogs",
        builder =>
        {
            builder.Property(dog => dog.Id).HasColumnName("DogId");
            builder.Property(dog => dog.Breed).HasColumnName("DogBreed");
        });

Tek yönlü çoka çok ilişkileri

EF7, bir tarafın veya diğerinin gezinti özelliğine sahip olmadığı çoka çok ilişkileri destekler. Örneğin, ve Tag türlerini göz önünde bulundurunPost:

public class Post
{
    public int Id { get; set; }
    public string? Title { get; set; }
    public Blog Blog { get; set; } = null!;
    public List<Tag> Tags { get; } = new();
}
public class Tag
{
    public int Id { get; set; }
    public string TagName { get; set; } = null!;
}

Türün Post etiket listesi için gezinti özelliği olduğuna, ancak türün Tag gönderiler için gezinti özelliğine sahip olduğuna dikkat edin. EF7'de bu, çoka çok ilişki olarak yapılandırılabilir ve aynı Tag nesnenin birçok farklı gönderi için kullanılmasına olanak sağlar. Örnek:

modelBuilder
    .Entity<Post>()
    .HasMany(post => post.Tags)
    .WithMany();

Bu, uygun birleştirme tablosuna eşlemeyle sonuç olur:

CREATE TABLE [Tags] (
    [Id] int NOT NULL IDENTITY,
    [TagName] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Tags] PRIMARY KEY ([Id])
);

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(64) NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id])
);

CREATE TABLE [PostTag] (
    [PostId] int NOT NULL,
    [TagsId] int NOT NULL,
    CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostId], [TagsId]),
    CONSTRAINT [FK_PostTag_Posts_PostId] FOREIGN KEY ([PostId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tags_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE
);

Ve ilişki normal şekilde çoka çok olarak kullanılabilir. Örneğin, ortak bir kümeden çeşitli etiketleri paylaşan bazı gönderiler ekleme:

var tags = new Tag[] { new() { TagName = "Tag1" }, new() { TagName = "Tag2" }, new() { TagName = "Tag2" }, };

await context.AddRangeAsync(new Blog { Posts =
{
    new Post { Tags = { tags[0], tags[1] } },
    new Post { Tags = { tags[1], tags[0], tags[2] } },
    new Post()
} });

await context.SaveChangesAsync();

Varlık bölme

Varlık bölme tek bir varlık türünü birden çok tabloya eşler. Örneğin, müşteri verilerini barındıran üç tablo içeren bir veritabanı düşünün:

  • Customers Müşteri bilgileri tablosu
  • PhoneNumbers Müşterinin telefon numarası için bir tablo
  • Addresses Müşterinin adresi için bir tablo

SQL Server'da bu tabloların tanımları şunlardır:

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
    
CREATE TABLE [PhoneNumbers] (
    [CustomerId] int NOT NULL,
    [PhoneNumber] nvarchar(max) NULL,
    CONSTRAINT [PK_PhoneNumbers] PRIMARY KEY ([CustomerId]),
    CONSTRAINT [FK_PhoneNumbers_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Addresses] (
    [CustomerId] int NOT NULL,
    [Street] nvarchar(max) NOT NULL,
    [City] nvarchar(max) NOT NULL,
    [PostCode] nvarchar(max) NULL,
    [Country] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Addresses] PRIMARY KEY ([CustomerId]),
    CONSTRAINT [FK_Addresses_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

Bu tabloların her biri genellikle türler arasındaki ilişkilerle kendi varlık türlerine eşlenir. Ancak, üç tablonun hepsi her zaman birlikte kullanılıyorsa, bunların tümünü tek bir varlık türüne eşlemek daha kullanışlı olabilir. Örnek:

public class Customer
{
    public Customer(string name, string street, string city, string? postCode, string country)
    {
        Name = name;
        Street = street;
        City = city;
        PostCode = postCode;
        Country = country;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public string? PhoneNumber { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string? PostCode { get; set; }
    public string Country { get; set; }
}

Bu, EF7'de varlık türündeki her bölme için çağrılarak SplitToTable güncelleştirilir. Örneğin, aşağıdaki kod varlık türünü Customersyukarıda gösterilen , PhoneNumbersve Addresses tablolarına bölerCustomer:

modelBuilder.Entity<Customer>(
    entityBuilder =>
    {
        entityBuilder
            .ToTable("Customers")
            .SplitToTable(
                "PhoneNumbers",
                tableBuilder =>
                {
                    tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
                    tableBuilder.Property(customer => customer.PhoneNumber);
                })
            .SplitToTable(
                "Addresses",
                tableBuilder =>
                {
                    tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
                    tableBuilder.Property(customer => customer.Street);
                    tableBuilder.Property(customer => customer.City);
                    tableBuilder.Property(customer => customer.PostCode);
                    tableBuilder.Property(customer => customer.Country);
                });
    });

Ayrıca, gerekirse tabloların her biri için farklı birincil anahtar sütun adlarının belirtilebildiğine de dikkat edin.

SQL Server UTF-8 dizeleri

ve nvarchar veri türleri tarafından nchar temsil edilen SQL Server Unicode dizeleri UTF-16 olarak depolanır. Ayrıca ve varchar veri türleri,charçeşitli karakter kümelerini destekleyen Unicode olmayan dizeleri depolamak için kullanılır.

SQL Server 2019'dan başlayarak ve varchar veri türleri bunun char yerine UTF-8 kodlamalı Unicode dizeleri depolamak için kullanılabilir. , UTF-8 harmanlamalarından biri ayarlanarak elde edilir. Örneğin, aşağıdaki kod sütun için değişken uzunlukta bir SQL Server UTF-8 dizesi yapılandırıyor CommentText :

modelBuilder
    .Entity<Comment>()
    .Property(comment => comment.CommentText)
    .HasColumnType("varchar(max)")
    .UseCollation("LATIN1_GENERAL_100_CI_AS_SC_UTF8");

Bu yapılandırma aşağıdaki SQL Server sütun tanımını oluşturur:

CREATE TABLE [Comment] (
    [PostId] int NOT NULL,
    [CommentId] int NOT NULL,
    [CommentText] varchar(max) COLLATE LATIN1_GENERAL_100_CI_AS_SC_UTF8 NOT NULL,
    CONSTRAINT [PK_Comment] PRIMARY KEY ([PostId], [CommentId])
);

Zamana bağlı tablolar sahip olunan varlıkları destekler

EF Core SQL Server zamana bağlı tablo eşlemesi, EF7'de tablo paylaşımını destekleyecek şekilde geliştirilmiştir. En önemlisi, sahip olunan tek varlıklar için varsayılan eşleme tablo paylaşımını kullanır.

Örneğin, sahip varlık türünü Employee ve sahip olduğu varlık türünü EmployeeInfogöz önünde bulundurun:

public class Employee
{
    public Guid EmployeeId { get; set; }
    public string Name { get; set; } = null!;

    public EmployeeInfo Info { get; set; } = null!;
}

public class EmployeeInfo
{
    public string Position { get; set; } = null!;
    public string Department { get; set; } = null!;
    public string? Address { get; set; }
    public decimal? AnnualSalary { get; set; }
}

Bu türler aynı tabloya eşlenmişse EF7'de bu tablo zamana bağlı bir tablo yapılabilir:

modelBuilder
    .Entity<Employee>()
    .ToTable(
        "Employees",
        tableBuilder =>
        {
            tableBuilder.IsTemporal();
            tableBuilder.Property<DateTime>("PeriodStart").HasColumnName("PeriodStart");
            tableBuilder.Property<DateTime>("PeriodEnd").HasColumnName("PeriodEnd");
        })
    .OwnsOne(
        employee => employee.Info,
        ownedBuilder => ownedBuilder.ToTable(
            "Employees",
            tableBuilder =>
            {
                tableBuilder.IsTemporal();
                tableBuilder.Property<DateTime>("PeriodStart").HasColumnName("PeriodStart");
                tableBuilder.Property<DateTime>("PeriodEnd").HasColumnName("PeriodEnd");
            }));

Dekont

Bu yapılandırmayı kolaylaştırma işlemi Sorun #29303 tarafından izlenir. Uygulandığını görmek istediğiniz bir sorunsa bu sorun için oy verin.

Geliştirilmiş değer oluşturma

EF7, anahtar özellikleri için değerlerin otomatik olarak oluşturulmasında iki önemli geliştirme içerir.

Bahşiş

Bu bölümdeki örneklerin kodu ValueGenerationSample.cs dosyasından gelir.

DDD korumalı türler için değer oluşturma

Etki alanı odaklı tasarımda (DDD), "korunan anahtarlar" anahtar özelliklerinin tür güvenliğini iyileştirebilir. Bu, anahtar türü, anahtarın kullanımına özgü başka bir türe kaydırılarak elde edilir. Örneğin, aşağıdaki kod ürün anahtarları için bir ProductId tür ve kategori anahtarları için bir CategoryId tür tanımlar.

public readonly struct ProductId
{
    public ProductId(int value) => Value = value;
    public int Value { get; }
}

public readonly struct CategoryId
{
    public CategoryId(int value) => Value = value;
    public int Value { get; }
}

Bunlar daha sonra ve Category varlık türlerinde Product kullanılır:

public class Product
{
    public Product(string name) => Name = name;
    public ProductId Id { get; set; }
    public string Name { get; set; }
    public CategoryId CategoryId { get; set; }
    public Category Category { get; set; } = null!;
}

public class Category
{
    public Category(string name) => Name = name;
    public CategoryId Id { get; set; }
    public string Name { get; set; }
    public List<Product> Products { get; } = new();
}

Bu, bir kategorinin kimliğinin yanlışlıkla bir ürüne atanmaması (veya tam tersi) olanaksız hale gelir.

Uyarı

Birçok DDD kavramında olduğu gibi, bu geliştirilmiş tür güvenliği de ek kod karmaşıklığından kaynaklanmıştır. Örneğin, bir kategoriye ürün kimliği atamanın olası bir şey olup olmadığını göz önünde bulundurmaya değer. İşleri basit tutmak, kod tabanı için genel olarak daha yararlı olabilir.

Burada gösterilen korunan anahtar türleri hem anahtar değerlerini sarmalar int , diğer bir deyişle eşlenen veritabanı tablolarında tamsayı değerleri kullanılır. Bu, türler için değer dönüştürücüleri tanımlanarak elde edilir:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<ProductId>().HaveConversion<ProductIdConverter>();
    configurationBuilder.Properties<CategoryId>().HaveConversion<CategoryIdConverter>();
}

private class ProductIdConverter : ValueConverter<ProductId, int>
{
    public ProductIdConverter()
        : base(v => v.Value, v => new(v))
    {
    }
}

private class CategoryIdConverter : ValueConverter<CategoryId, int>
{
    public CategoryIdConverter()
        : base(v => v.Value, v => new(v))
    {
    }
}

Dekont

Buradaki kod türleri kullanır struct . Bu, anahtar olarak kullanmak için uygun değer türü semantiğine sahip oldukları anlamına gelir. Türler bunun yerine kullanılırsa class , eşitlik semantiğini geçersiz kılmaları veya ayrıca bir değer karşılaştırıcı belirtmeleri gerekir.

EF7'de, değer dönüştürücülerini temel alan anahtar türleri, temel alınan tür bunu desteklediği sürece otomatik olarak oluşturulan anahtar değerlerini kullanabilir. Bu, kullanılarak ValueGeneratedOnAddnormal şekilde yapılandırılır:

modelBuilder.Entity<Product>().Property(product => product.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<Category>().Property(category => category.Id).ValueGeneratedOnAdd();

Varsayılan olarak bu, SQL Server ile kullanıldığında sütunlara IDENTITY neden olur:

CREATE TABLE [Categories] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Categories] PRIMARY KEY ([Id]));

CREATE TABLE [Products] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    [CategoryId] int NOT NULL,
    CONSTRAINT [PK_Products] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Products_Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [Categories] ([Id]) ON DELETE CASCADE);

Varlıklar eklenirken anahtar değerleri oluşturmak için normal şekilde kullanılır:

MERGE [Categories] USING (
VALUES (@p0, 0),
(@p1, 1)) AS i ([Name], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name])
VALUES (i.[Name])
OUTPUT INSERTED.[Id], i._Position;

SQL Server için sıra tabanlı anahtar oluşturma

EF Core, SQL Server sütunlarını kullanarak anahtar değeri oluşturmayı veya veritabanı dizisi tarafından oluşturulan anahtar bloklarını temel alan bir Hi-Lo desenini destekler.IDENTITY EF7, anahtarın sütun varsayılan kısıtlamasına eklenmiş bir veritabanı dizisi için destek sağlar. En basit haliyle, bunun için EF Core'a anahtar özelliği için bir dizi kullanmasını söylemek gerekir:

modelBuilder.Entity<Product>().Property(product => product.Id).UseSequence();

Bu, veritabanında bir sıranın tanımlanmasına neden olur:

CREATE SEQUENCE [ProductSequence] START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE;

Daha sonra anahtar sütunu varsayılan kısıtlamasında kullanılır:

CREATE TABLE [Products] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [ProductSequence]),
    [Name] nvarchar(max) NOT NULL,
    [CategoryId] int NOT NULL,
    CONSTRAINT [PK_Products] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Products_Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [Categories] ([Id]) ON DELETE CASCADE);

Dekont

Bu anahtar oluşturma biçimi, TPC eşleme stratejisi kullanılarak varlık türü hiyerarşilerinde oluşturulan anahtarlar için varsayılan olarak kullanılır.

İsterseniz, diziye farklı bir ad ve şema verilebilir. Örnek:

modelBuilder
    .Entity<Product>()
    .Property(product => product.Id)
    .UseSequence("ProductsSequence", "northwind");

Dizinin daha fazla yapılandırması, modelde açıkça yapılandırılarak oluşturulur. Örnek:

modelBuilder
    .HasSequence<int>("ProductsSequence", "northwind")
    .StartsAt(1000)
    .IncrementsBy(2);

Geçiş araçları geliştirmeleri

EF7, EF Core Migrations komut satırı araçlarını kullanırken iki önemli geliştirme içerir.

UseSqlServer vb. null kabul et

Yapılandırma dosyasından bir bağlantı dizesi okumak ve ardından bu bağlantı dizesi , UseSqliteveya başka bir sağlayıcı için eşdeğer yönteme UseSqlServergeçirmek çok yaygındır. Örnek:

services.AddDbContext<BloggingContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("BloggingDatabase")));

Geçişler uygulanırken bağlantı dizesi geçirmek de yaygın bir durumdur. Örnek:

dotnet ef database update --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"

Veya Migrations paketi kullanırken.

./bundle.exe --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"

Bu durumda, yapılandırmadan okuma bağlantı dizesi kullanılmasa da, uygulama başlangıç kodu yapılandırmadan okumaya ve öğesine geçirmeye UseSqlServerçalışır. Yapılandırma kullanılamıyorsa, bu işlem null değerinin öğesine geçirilmesiyle UseSqlServersonuçlanır. EF7'de, bağlantı dizesi daha sonra ayarlandığı sürece, örneğin komut satırı aracına geçirilerek --connection buna izin verilir.

Dekont

Bu değişiklik ve UseSqliteiçin UseSqlServer yapılmıştır. Diğer sağlayıcılar için, sağlayıcı için henüz yapılmamışsa eşdeğer bir değişiklik yapmak üzere sağlayıcı bakımcısına başvurun.

Araçların ne zaman çalıştığını algılama

VEYA PowerShell komutları kullanılırken dotnet-ef EF Core uygulama kodunu çalıştırır. Bazen uygunsuz kodun tasarım zamanında yürütülmesini önlemek için bu durumu algılamak gerekebilir. Örneğin, başlangıçta geçişleri otomatik olarak uygulayan kodun bunu tasarım zamanında yapmaması gerekir. EF7'de bu, bayrağı kullanılarak EF.IsDesignTime algılanabilir:

if (!EF.IsDesignTime)
{
    await context.Database.MigrateAsync();
}

EF Core, uygulama kodunun IsDesignTimetrue araçlar adına ne zaman çalıştırılacağına ayarlar.

Proxy'ler için performans geliştirmeleri

EF Core, yavaş yükleme ve değişiklik izleme için dinamik olarak oluşturulan proxy'leri destekler. EF7, bu proxy'leri kullanırken iki performans geliştirmesi içerir:

  • Proxy türleri artık lazily oluşturulur. Bu, ara sunucuları kullanırken ilk model oluşturma süresinin EF7 ile EF Core 6.0'dan çok daha hızlı olabileceği anlamına gelir.
  • Proxy'ler artık derlenmiş modellerle kullanılabilir.

449 varlık türü, 6390 özellik ve 720 ilişki içeren bir model için bazı performans sonuçları aşağıdadır.

Senaryo Metot Ortalama Hata Stdsapma
Proxy'ler olmadan EF Core 6.0 TimeToFirstQuery 1,085 sn 0,0083 sn 0,0167 sn
Değişiklik izleme proxy'leri ile EF Core 6.0 TimeToFirstQuery 13.01 sn 0,2040 sn 0,4110 sn
Proxy'ler olmadan EF Core 7.0 TimeToFirstQuery 1,442 sn 0,0134 sn 0,0272 sn
Değişiklik izleme proxy'leri ile EF Core 7.0 TimeToFirstQuery 1,446 sn 0,0160 sn 0,0323 sn
Değişiklik izleme proxy'leri ve derlenmiş model içeren EF Core 7.0 TimeToFirstQuery 0,162 sn 0,0062 sn 0,0125 sn

Bu durumda değişiklik izleme proxy'lerine sahip bir model, EF7'de EF Core 6.0 ile mümkün olandan 80 kat daha hızlı bir şekilde ilk sorguyu yürütmeye hazır olabilir.

Birinci sınıf Windows Forms veri bağlama

Windows Forms ekibi, Visual Studio Tasarım Aracı deneyiminde bazı harika geliştirmeler yapıyor. Bu, EF Core ile iyi tümleşen veri bağlamaya yönelik yeni deneyimler içerir.

Kısaca, yeni deneyim bir ObjectDataSourceoluşturmak için Visual Studio U.I. sağlar:

Choose Category data source type

Bu işlem daha sonra basit bir kodla EF Core'a DbSet bağlanabilir:

public partial class MainForm : Form
{
    private ProductsContext? dbContext;

    public MainForm()
    {
        InitializeComponent();
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        this.dbContext = new ProductsContext();

        this.dbContext.Categories.Load();
        this.categoryBindingSource.DataSource = dbContext.Categories.Local.ToBindingList();
    }

    protected override void OnClosing(CancelEventArgs e)
    {
        base.OnClosing(e);

        this.dbContext?.Dispose();
        this.dbContext = null;
    }
}

Eksiksiz bir izlenecek yol ve indirilebilir WinForms örnek uygulaması için bkz. Windows Forms ile Çalışmaya Başlama.