Новые возможности EF Core 7.0

EF Core 7.0 (EF7) был выпущен в ноябре 2022 года.

Совет

Вы можете выполнить и выполнить отладку в примерах, скачав пример кода с GitHub. Каждый раздел ссылается на исходный код, относящееся к данному разделу.

EF7 предназначен для .NET 6 и поэтому может использоваться с .NET 6 (LTS) или .NET 7.

Пример модели

Многие из приведенных ниже примеров используют простую модель с блогами, записями, тегами и авторами:

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();
}

В некоторых примерах также используются агрегатные типы, которые сопоставляются различными способами в разных примерах. Существует один агрегатный тип для контактов:

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; }
}

А второй агрегатный тип для метаданных записи:

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; }
}

Совет

Пример модели можно найти в BlogsContext.cs.

Столбцы JSON

Большинство реляционных баз данных поддерживают столбцы, содержащие документы JSON. Json в этих столбцах можно детализировать с помощью запросов. Это позволяет, например, фильтрацию и сортировку по элементам документов, а также проекцию элементов из документов в результаты. Столбцы JSON позволяют реляционным базам данных принимать некоторые характеристики баз данных документов, создавая полезный гибрид между этими двумя.

EF7 содержит поддержку столбцов JSON, не зависящих от поставщика, с реализацией для SQL Server. Эта поддержка позволяет сопоставлять агрегаты, созданные из типов .NET, в документы JSON. Обычные запросы LINQ можно использовать для агрегатов, и они будут преобразованы в соответствующие конструкции запросов, необходимые для детализации JSON. EF7 также поддерживает обновление и сохранение изменений в документах JSON.

Примечание.

Поддержка SQLite для JSON планируется для публикации EF7. Поставщики PostgreSQL и Pomelo MySQL уже поддерживают столбцы JSON. Мы будем работать с авторами этих поставщиков для выравнивания поддержки JSON для всех поставщиков.

Сопоставление столбцов JSON

В EF Core агрегатные типы определяются с помощью OwnsOne и OwnsMany. Например, рассмотрим агрегатный тип из примера модели, используемой для хранения контактных данных:

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; }
}

Затем его можно использовать в типе сущности "владелец", например для хранения контактных данных автора:

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

Тип агрегата настраивается с OnModelCreating помощью OwnsOne:

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

Совет

Код, показанный здесь, поставляется из JsonColumnsSample.cs.

По умолчанию поставщики реляционных баз данных сопоставляют агрегатные типы, такие как это, с той же таблицей, что и тип сущности владения. То есть каждое свойство ContactDetails и Address классы сопоставляются со столбцом в Authors таблице.

Некоторые сохраненные авторы с контактными данными будут выглядеть следующим образом:

Авторы

Идентификатор Имя Contact_Address_Street Contact_Address_City Contact_Address_Postcode Contact_Address_Country Contact_Телефон
1 Мэдди Монтакила 1 Главный санкт Камбервик Грин CW1 5ZH Соединенное Королевство 01632 12345
2 Джереми Ликнесс (Jeremy Likness) 2 Main St Чигли CW1 5ZH Соединенное Королевство 01632 12346
3 Дэниэл Рот (Daniel Roth) 3 Main St Камбервик Грин CW1 5ZH Соединенное Королевство 01632 12347
4 Артур Викерс 15a Main St Чигли CW1 5ZH Соединенное Королевство 01632 22345
5 Брис Ламсон 4 Main St Чигли CW1 5ZH Соединенное Королевство 01632 12349

При необходимости каждый тип сущности, составляющий агрегат, можно сопоставить с собственной таблицей.

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");
                });
        });
}

Затем те же данные хранятся в трех таблицах:

Авторы

Идентификатор Имя
1 Мэдди Монтакила
2 Джереми Ликнесс (Jeremy Likness)
3 Дэниэл Рот (Daniel Roth)
4 Артур Викерс
5 Брис Ламсон

Контакты

AuthorId Для телефонов
1 01632 12345
2 01632 12346
3 01632 12347
4 01632 22345
5 01632 12349

Адреса

ContactDetailsAuthorId Улица City Индекс Country
1 1 Главный санкт Камбервик Грин CW1 5ZH Соединенное Королевство
2 2 Main St Чигли CW1 5ZH Соединенное Королевство
3 3 Main St Камбервик Грин CW1 5ZH Соединенное Королевство
4 15a Main St Чигли CW1 5ZH Соединенное Королевство
5 4 Main St Чигли CW1 5ZH Соединенное Королевство

Теперь, для интересной части. В EF7 тип агрегата ContactDetails можно сопоставить со столбцом JSON. Для этого требуется ToJson() только один вызов при настройке агрегатного типа:

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

Теперь таблица Authors будет содержать столбец JSON, ContactDetails заполненный документом JSON для каждого автора:

Авторы

Идентификатор Имя Контакт
1 Мэдди Монтакила {
  "Телефон":"01632 12345",
  "Адрес": {
    "Город":"Camberwick Green",
    "Страна":"Великобритания",
    "Postcode":"CW1 5ZH",
    "Улица":"1 Main St"
  }
}
2 Джереми Ликнесс (Jeremy Likness) {
  "Телефон":"01632 12346",
  "Адрес": {
    "Город":"Chigley",
    "Страна":"Великобритания",
    "Postcode":"CH1 5ZH",
    "Улица":"2 Main St"
  }
}
3 Дэниэл Рот (Daniel Roth) {
  "Телефон":"01632 12347",
  "Адрес": {
    "Город":"Camberwick Green",
    "Страна":"Великобритания",
    "Postcode":"CW1 5ZH",
    "Улица":"3 Main St"
  }
}
4 Артур Викерс {
  "Телефон":"01632 12348",
  "Адрес": {
    "Город":"Chigley",
    "Страна":"Великобритания",
    "Postcode":"CH1 5ZH",
    "Улица":"15a Main St"
  }
}
5 Брис Ламсон {
  "Телефон":"01632 12349",
  "Адрес": {
    "Город":"Chigley",
    "Страна":"Великобритания",
    "Postcode":"CH1 5ZH",
    "Улица":"4 Main St"
  }
}

Совет

Это использование агрегатов очень похоже на то, как документы JSON сопоставляются при использовании поставщика EF Core для Azure Cosmos DB. Столбцы JSON позволяют использовать EF Core для баз данных документов в документы, внедренные в реляционную базу данных.

Приведенные выше документы JSON очень просты, но эта возможность сопоставления также может использоваться с более сложными структурами документов. Например, рассмотрим другой агрегатный тип из модели примера, используемый для представления метаданных о записи:

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; }
}

Этот агрегатный тип содержит несколько вложенных типов и коллекций. OwnsOne Вызовы и OwnsMany используются для сопоставления этого агрегатного типа:

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));
    });

Совет

ToJson требуется только в корневом элементе агрегата для сопоставления всего агрегата с документом JSON.

Благодаря этому сопоставлению EF7 может создавать и запрашивать их в сложный документ JSON, как показано ниже.

{
  "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"
        }
      ]
    }
  ]
}

Примечание.

Сопоставление пространственных типов непосредственно с JSON еще не поддерживается. Приведенный выше документ использует double значения в качестве обходного решения. Проголосовать за пространственные типы поддержки в столбцах JSON, если это то, что вы хотите.

Примечание.

Сопоставление коллекций примитивных типов с JSON еще не поддерживается. Приведенный выше документ использует преобразователь значений для преобразования коллекции в строку, разделенную запятыми. Проголосуйте за Json: добавьте поддержку коллекции примитивных типов , если это то, что вам интересно.

Примечание.

Сопоставление собственных типов с JSON еще не поддерживается в сочетании с наследованием TPT или TPC. Проголосовать за свойства JSON поддержки с сопоставлением наследования TPT/TPC, если это то, что вам интересно.

Запросы в столбцы JSON

Запросы к столбцам JSON работают так же, как запросы к любому другому агрегатному типу в EF Core. То есть просто используйте LINQ! Ниже приведено несколько примеров.

Запрос для всех авторов, живущих в Chigley:

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

Этот запрос создает следующий SQL при использовании SQL Server:

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_VALUE получения City из документа JSON.Address

Select можно использовать для извлечения и проекта элементов из документа JSON:

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

Этот запрос создает следующий SQL:

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'

Ниже приведен пример, который делает немного больше в фильтре и проекции, а также заказы по номеру телефона в документе JSON:

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();

Этот запрос создает следующий SQL:

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 содержит коллекции, их можно проецировать в результатах:

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();

Этот запрос создает следующий SQL:

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

Примечание.

Более сложные запросы, связанные с коллекциями JSON, требуют jsonpath поддержки. Проголосуйте за запрос jsonpath в службу поддержки, если это то, что вам интересно.

Совет

Рекомендуется создавать индексы для повышения производительности запросов в документах JSON. Например, ознакомьтесь с данными Index Json при использовании SQL Server.

Обновление столбцов JSON

SaveChanges и SaveChangesAsync работайте в обычном режиме, чтобы обновить столбец JSON. Для обширных изменений весь документ будет обновлен. Например, замена большинства Contact документов для автора:

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();

В этом случае весь новый документ передается в качестве параметра:

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']

Который затем используется в UPDATE SQL:

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

Однако если изменяется только вложенный документ, EF Core будет использовать JSON_MODIFY команду для обновления только поддокумента. Например, изменение Address внутри Contact документа:

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

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

await context.SaveChangesAsync();

Создает следующие параметры:

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']

Который используется при вызове UPDATEJSON_MODIFY :

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

Наконец, если изменено только одно свойство, EF Core снова будет использовать команду "JSON_MODIFY", на этот раз для исправления только измененного значения свойства. Например:

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

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

await context.SaveChangesAsync();

Создает следующие параметры:

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']

Которые снова используются с :JSON_MODIFY

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

ExecuteUpdate и ExecuteDelete (массовые обновления)

По умолчанию EF Core отслеживает изменения сущностей, а затем отправляет обновления в базу данных при вызове SaveChanges одного из методов. Изменения отправляются только для свойств и связей, которые фактически изменились. Кроме того, отслеживаемые сущности остаются синхронизированными с изменениями, отправленными в базу данных. Этот механизм является эффективным и удобным способом отправки вставок, обновлений и удалений в базу данных общего назначения. Эти изменения также пакетируются для уменьшения количества цикловых обходов базы данных.

Однако иногда полезно выполнять команды обновления или удаления в базе данных без участия средства отслеживания изменений. EF7 включает это с новыми ExecuteUpdate и ExecuteDelete методами. Эти методы применяются к запросу LINQ и будут обновлять или удалять сущности в базе данных на основе результатов этого запроса. Многие сущности можно обновить с помощью одной команды, и сущности не загружаются в память, что означает, что это может привести к более эффективным обновлениям и удалению.

Однако помните, что:

  • Конкретные изменения, которые необходимо внести, должны быть указаны явным образом; Они не обнаруживаются автоматически EF Core.
  • Отслеживаемые сущности не будут храниться в синхронизации.
  • Дополнительные команды могут быть отправлены в правильном порядке, чтобы не нарушать ограничения базы данных. Например, удаление зависимых перед удалением субъекта.

Все это означает, что ExecuteUpdateExecuteDelete методы дополняют, а не заменяют существующий SaveChanges механизм.

Основные ExecuteDelete примеры

Совет

Приведенный здесь код поставляется из ExecuteDeleteSample.cs.

Вызов ExecuteDelete или ExecuteDeleteAsyncDbSet немедленно удаляет все сущности из DbSet базы данных. Например, чтобы удалить все Tag сущности:

await context.Tags.ExecuteDeleteAsync();

При использовании SQL Server выполняется следующий код SQL:

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

Более интересно, что запрос может содержать фильтр. Например:

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

Выполняется следующий SQL:

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

Запрос также может использовать более сложные фильтры, включая навигации по другим типам. Например, чтобы удалить теги только из старых записей блога:

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

Выполняется:

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))

Основные ExecuteUpdate примеры

Совет

Код, показанный здесь, поставляется из ExecuteUpdateSample.cs.

ExecuteUpdate и ExecuteUpdateAsync ведет себя очень похожим образом на ExecuteDelete методы. Основное различие заключается в том, что обновление требует знания о свойствах для обновления и способах их обновления. Это достигается с помощью одного или нескольких вызовов SetProperty. Например, чтобы обновить Name каждый блог:

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

Первый параметр SetProperty указывает, какое свойство необходимо обновить; в данном случае Blog.Name. Второй параметр указывает, как следует вычислять новое значение; в этом случае, принимая существующее значение и добавляя "*Featured!*". Результирующий SQL:

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

Как и при использовании ExecuteDelete, запрос можно использовать для фильтрации обновляемых сущностей. Кроме того, для обновления нескольких свойств целевой сущности можно использовать несколько вызовов SetProperty . Например, чтобы обновить все Content записи, опубликованные Title до 2022 года, выполните следующие действия:

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 + ")"));

В этом случае созданный SQL немного сложнее:

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

Наконец, опять же, как и в ExecuteDeleteслучае, фильтр может ссылаться на другие таблицы. Например, чтобы обновить все теги из старых записей:

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

Что создает:

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))

Дополнительные сведения и примеры кода см. в ExecuteUpdateExecuteDeleteразделе ExecuteUpdate и ExecuteDelete.

Наследование и несколько таблиц

ExecuteUpdate и ExecuteDelete может действовать только в одной таблице. Это имеет последствия при работе с различными стратегиями сопоставления наследования. Как правило, при использовании стратегии сопоставления TPH нет проблем, так как для изменения существует только одна таблица. Например, удаление всех FeaturedPost сущностей:

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

Создает следующий SQL при использовании сопоставления TPH:

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

В этом случае при использовании стратегии сопоставления TPC не возникает никаких проблем, так как при необходимости требуются только изменения в одной таблице:

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

Однако попытка выполнить эту попытку при использовании стратегии сопоставления TPT завершится ошибкой, так как для этого потребуется удалить строки из двух разных таблиц.

Добавление фильтра в запрос часто означает, что операция завершится ошибкой как с стратегиями TPC, так и с TPT. Это снова связано с тем, что строки могут быть удалены из нескольких таблиц. Например, запрос

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

Создает следующий SQL при использовании TPH:

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%')

Но происходит сбой при использовании TPC или TPT.

Совет

Проблема #10879 отслеживает добавление поддержки автоматической отправки нескольких команд в этих сценариях. Проголосуйте за этот вопрос, если это то, что вы хотите увидеть, реализовано.

ExecuteDelete и связи

Как упоминание выше, может потребоваться удалить или обновить зависимые сущности перед удалением субъекта связи. Например, каждый из них Post зависит от его связанного Author. Это означает, что автор не может быть удален, если запись по-прежнему ссылается на нее; это приведет к нарушению ограничения внешнего ключа в базе данных. Например, попробуйте выполнить следующее:

await context.Authors.ExecuteDeleteAsync();

Приведет к следующему исключению в SQL Server:

Microsoft.Data.SqlClient.SqlException (0x80131904): инструкция DELETE конфликтует с ограничением REFERENCE "FK_Posts_Authors_AuthorId". Конфликт произошел в базе данных "TphBlogsContext", таблица "dbo". Post,column 'AuthorId'. Выполнение данной инструкции было прервано.

Чтобы устранить эту проблему, сначала необходимо удалить записи или удалить связь между каждой записью и ее автором, установив AuthorId для свойства внешнего ключа значение NULL. Например, используя параметр удаления:

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

Совет

TagWith можно использовать для тега ExecuteDelete или ExecuteUpdate таким же образом, как и обычные запросы.

Это приводит к двум отдельным командам; первое удаление зависимых элементов:

-- Deleting posts...

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

И второй, чтобы удалить субъекты, выполните следующие действия.

-- Deleting authors...

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

Важно!

ExecuteUpdate По умолчанию несколько ExecuteDelete команд не будут содержаться в одной транзакции. Однако API транзакций DbContext можно использовать обычным способом для упаковки этих команд в транзакцию.

Совет

Отправка этих команд в одном круговом пути зависит от вопроса 10879. Проголосуйте за этот вопрос, если это то, что вы хотите увидеть, реализовано.

Настройка каскадных удалений в базе данных может быть очень полезной здесь. В нашей модели связь между Blog и Post является обязательной, что приводит к настройке каскадного удаления по соглашению. Это означает, что при удалении блога из базы данных все зависимые записи также будут удалены. Затем следует, чтобы удалить все блоги и записи, которые нам нужно удалить только блоги:

await context.Blogs.ExecuteDeleteAsync();

Это приводит к следующему SQL:

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

Что, как и при удалении блога, также приведет к удалению всех связанных записей с помощью настроенного каскадного удаления.

Быстрее SaveChanges

В EF7 производительность SaveChanges и SaveChangesAsync значительно улучшилась. В некоторых сценариях сохранение изменений в настоящее время составляет до четырех раз быстрее, чем с EF Core 6.0!

Большинство из этих улучшений происходят:

  • Выполнение меньшего количества циклов в базе данных
  • Создание более быстрого SQL

Ниже показаны некоторые примеры этих улучшений.

Примечание.

Дополнительные сведения об этих изменениях см . в статье "Объявление Entity Framework Core 7( предварительная версия 6: выпуск Performance Edition ) в блоге .NET.

Совет

Приведенный здесь код поставляется из SaveChangesPerformanceSample.cs.

Ненужные транзакции устраняются

Все современные реляционные базы данных гарантируют транзакцию для (большинства) отдельных инструкций SQL. То есть инструкция никогда не будет частично завершена, даже если возникает ошибка. EF7 не запускает явную транзакцию в этих случаях.

Например, просматривая ведение журнала для следующего вызова SaveChanges:

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

Показывает, что в EF Core 6.0 INSERT команда упаковывается командами, чтобы начать, а затем зафиксировать транзакцию:

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 обнаруживает, что транзакция не нужна здесь, поэтому удаляет следующие вызовы:

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);

При этом удаляются два циклического обхода базы данных, что может значительно повысить общую производительность, особенно если задержка вызовов базы данных высока. В типичных рабочих системах база данных не находится на том же компьютере, что и приложение. Это означает, что задержка часто относительно высока, что делает эту оптимизацию особенно эффективной в реальных производственных системах.

Улучшен SQL для простой вставки удостоверений

Приведенный выше случай вставляет одну строку с ключевым столбцом IDENTITY и другими значениями, созданными базой данных. EF7 упрощает SQL в этом случае с помощью OUTPUT INSERTED. Хотя это упрощение является недопустимым во многих других случаях, важно улучшить, так как этот тип однострочного вставки очень распространен во многих приложениях.

Вставка нескольких строк

В EF Core 6.0 подход по умолчанию для вставки нескольких строк был обусловлен ограничениями в поддержке SQL Server для таблиц с триггерами. Мы хотели убедиться, что интерфейс по умолчанию работал даже для меньшинства пользователей с триггерами в своих таблицах. Это означало, что мы не могли использовать простое OUTPUT предложение, так как в SQL Server это не работает с триггерами. Вместо этого при вставке нескольких сущностей EF Core 6.0 создал некоторые довольно свертанные SQL. Например, этот вызов:SaveChanges

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

await context.SaveChangesAsync();

Приводит к следующим действиям при запуске с SQL Server с EF Core 6.0:

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.

Важно!

Несмотря на то, что это сложно, пакетирование нескольких вставок, как это, все еще значительно быстрее, чем отправка одной команды для каждой вставки.

В EF7 вы по-прежнему можете получить этот SQL, если таблицы содержат триггеры, но в обычном случае мы создадим гораздо эффективнее, если все еще несколько сложные команды:

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;

Транзакция исчезнет, как в одном случае вставки, так как MERGE это одна инструкция, защищенная неявной транзакцией. Кроме того, временная таблица ушла, и предложение OUTPUT теперь отправляет созданные идентификаторы непосредственно клиенту. Это может быть четыре раза быстрее, чем в EF Core 6.0, в зависимости от экологических факторов, таких как задержка между приложением и базой данных.

Триггеры

Если в таблице есть триггеры, вызов SaveChanges в приведенном выше коде вызовет исключение:

Необработанное исключение. Microsoft.EntityFrameworkCore.DbUpdateException:
не удалось сохранить изменения, так как целевая таблица имеет триггеры базы данных. Настройте тип сущности соответствующим образом, см https://aka.ms/efcore-docs-sqlserver-save-changes-and-triggers . дополнительные сведения.
>--- Microsoft.Data.SqlClient.SqlException (0x80131904):
целевая таблица "BlogsWithTriggers" инструкции DML не может иметь триггеров, если инструкция содержит предложение OUTPUT без предложения INTO.

Следующий код можно использовать для информирования EF Core о том, что в таблице есть триггер:

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

ЗАТЕМ EF7 отменить изменения в EF Core 6.0 SQL при отправке команд вставки и обновления для этой таблицы.

Дополнительные сведения, включая соглашение о автоматической настройке всех сопоставленных таблиц с триггерами, см . в таблицах SQL Server с триггерами, которые теперь требуют специальной конфигурации EF Core в документации по критическим изменениям EF7.

Меньше циклов для вставки графов

Рекомендуется вставить граф сущностей, содержащих новую сущность субъекта, а также новые зависимые сущности с внешними ключами, ссылающимися на новый субъект. Например:

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

Если первичный ключ субъекта создается базой данных, то значение, заданное для внешнего ключа в зависимом, неизвестно, пока субъект не будет вставлен. EF Core создает два круговых обхода для этого-один, чтобы вставить субъект и вернуть новый первичный ключ, а второй — вставить зависимости с набором значений внешнего ключа. И так как для этого требуется две инструкции, требуется транзакция, то есть в общей сложности четыре раунда:

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.

Однако в некоторых случаях значение первичного ключа известно до вставки субъекта. Сюда входит следующее:

  • Ключевые значения, которые не создаются автоматически
  • Ключевые значения, созданные на клиенте, такие как Guid ключи
  • Ключевые значения, созданные на сервере в пакетах, например при использовании генератора значений hi-lo

В EF7 эти случаи теперь оптимизированы для одного кругового пути. Например, в приведенном выше случае в SQL Server Blog.Id первичный ключ можно настроить для использования стратегии создания hi-lo:

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

Вызов SaveChanges из выше теперь оптимизирован для одного круглого обхода для вставок.

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.

Обратите внимание, что транзакция по-прежнему необходима здесь. Это связано с тем, что вставки выполняются в две отдельные таблицы.

EF7 также использует один пакет в других случаях, когда EF Core 6.0 создаст несколько. Например, при удалении и вставке строк в ту же таблицу.

Значение SaveChanges

Как показано в некоторых примерах, сохранение результатов в базе данных может быть сложным бизнесом. Здесь, где используется что-то подобное EF Core действительно показывает свое значение. EF Core.

  • Пакетная обработка нескольких команд вставки, обновления и удаления вместе для уменьшения круговых операций
  • Сведения о том, требуется ли явная транзакция или нет
  • Определяет порядок вставки, обновления и удаления сущностей, чтобы ограничения базы данных не нарушались
  • Гарантирует, что созданные базы данных значения возвращаются эффективно и распространяются обратно в сущности.
  • Автоматически задает значения внешнего ключа с помощью значений, созданных для первичных ключей
  • Обнаружение конфликтов параллелизма

Кроме того, для многих этих случаев для различных систем баз данных требуются разные системы баз данных SQL. Поставщик базы данных EF Core работает с EF Core, чтобы обеспечить правильную и эффективную отправку команд для каждого случая.

Сопоставление наследования табличного типа (TPC)

По умолчанию в EF Core иерархия наследования типов .NET сопоставляется с одной таблицей базы данных. Это называется стратегией сопоставления таблиц на иерархию (TPH ). EF Core 5.0 представила стратегию таблицы на тип (TPT), которая поддерживает сопоставление каждого типа .NET с другой таблицей базы данных. EF7 представляет стратегию табличного типа (TPC). TPC также сопоставляет типы .NET с разными таблицами, но таким образом, чтобы устранить некоторые распространенные проблемы с производительностью стратегии TPT.

Совет

Код, показанный здесь, поставляется из TpcInheritanceSample.cs.

Совет

Команда EF продемонстрировала и подробно говорила о сопоставлении TPC в эпизоде стенда сообщества данных .NET. Как и во всех эпизодах стенда сообщества, вы можете смотреть эпизод TPC сейчас на YouTube.

Схема базы данных TPC

Стратегия TPC аналогична стратегии TPT, за исключением того, что для каждого конкретного типа в иерархии создается другая таблица, но таблицы не создаются для абстрактных типов, поэтому имя "table-per-concrete-type". Как и в случае с TPT, сама таблица указывает тип сохраненного объекта. Однако, в отличие от сопоставления TPT, каждая таблица содержит столбцы для каждого свойства в конкретном типе и его базовых типах. Схемы базы данных TPC денормализованы.

Например, рекомендуется сопоставить эту иерархию:

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 таблицы, созданные для этой иерархии, :

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]));

Обратите внимание на указанные ниже моменты.

  • Нет таблиц для Animal типов или Pet типов, так как они находятся abstract в объектной модели. Помните, что C# не разрешает экземпляры абстрактных типов, поэтому нет ситуаций, когда экземпляр абстрактного типа будет сохранен в базе данных.

  • Сопоставление свойств в базовых типах повторяется для каждого конкретного типа. Например, каждая таблица имеет столбец, и кошки и собаки имеют NameVet столбец.

  • Сохранение некоторых данных в этой базе данных приводит к следующему:

Таблица кошек

Идентификатор Имя FoodId Ветеринар EducationLevel
1 Алиса 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Пенджелли МБ A
2 Mac 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Пенджелли Дошкольного
8 Бакстер 5dc5019e-6f72-454b-d4b0-08da7aca624f Больница Ботелл Пет Bsc

Таблица собак

Идентификатор Имя FoodId Ветеринар Избранное
3 Тост 011aaf6f-d588-4fad-d4ac-08da7aca624f Пенджелли Г-н Белка

Таблица FarmAnimals

Идентификатор Имя FoodId Значение Разновидность
4 Клайд 1d495075-f527-4498-d4af-08da7aca624f 100.00 Equus africanus asinus

Таблица "Люди"

Идентификатор Имя FoodId ИзбранноеAnimalId
5 Венди 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 Артур 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 Кэти null 8

Обратите внимание, что в отличие от сопоставления TPT все сведения для одного объекта содержатся в одной таблице. В отличие от сопоставления TPH, в любой таблице нет сочетания столбцов и строк, где она никогда не используется моделью. Ниже показано, как эти характеристики могут быть важными для запросов и хранилища.

Настройка наследования TPC

Все типы в иерархии наследования должны быть явно включены в модель при сопоставлении иерархии с EF Core. Это можно сделать, создав DbSet свойства для DbContext каждого типа:

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>();

Или с помощью Entity метода в OnModelCreating:

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

Важно!

Это отличается от устаревшего поведения EF6, где производные типы сопоставленных базовых типов будут автоматически обнаруживать, если они были содержатся в той же сборке.

Для сопоставления иерархии с TPH ничего другого не требуется, так как она является стратегией по умолчанию. Однако начиная с EF7, TPH можно сделать явным путем вызова UseTphMappingStrategy базового типа иерархии:

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

Чтобы использовать TPT, измените следующее UseTptMappingStrategy:

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

Аналогичным образом UseTpcMappingStrategy используется для настройки TPC:

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

В каждом случае имя таблицы, используемое для каждого типа, берется из DbSet имени свойства в вашем DbContextили может быть настроено с помощью ToTable метода построителя или атрибута[Table].

Производительность запросов TPC

Для запросов стратегия TPC является улучшением TPT, так как она гарантирует, что сведения для данного экземпляра сущности всегда хранятся в одной таблице. Это означает, что стратегия TPC может быть полезной, если сопоставленная иерархия большая и имеет множество конкретных (обычно конечных) типов, каждый из которых имеет большое количество свойств, и где в большинстве запросов используется только небольшое подмножество типов.

SQL, созданный для трех простых запросов LINQ, можно использовать для наблюдения за тем, где TPC хорошо работает по сравнению с TPH и TPT. Эти запросы:

  1. Запрос, возвращающий сущности всех типов в иерархии:

    context.Animals.ToList();
    
  2. Запрос, возвращающий сущности из подмножества типов в иерархии:

    context.Pets.ToList();
    
  3. Запрос, возвращающий только сущности из одного конечного типа в иерархии:

    context.Cats.ToList();
    

Запросы TPH

При использовании TPH все три запроса запрашивают только одну таблицу, но с различными фильтрами в столбце дискриминатора:

  1. 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. 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, возвращающий только сущности из одного конечного типа в иерархии:

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

Все эти запросы должны выполняться хорошо, особенно с соответствующим индексом базы данных в столбце дискриминационных данных.

Запросы TPT

При использовании TPT все эти запросы требуют объединения нескольких таблиц, так как данные для любого конкретного типа разделяются по нескольким таблицам:

  1. 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. 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, возвращающий только сущности из одного конечного типа в иерархии:

    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]
    

Примечание.

EF Core использует "дискриминационный синтез" для определения таблицы, из которой исходят данные, и, следовательно, правильный тип для использования. Это работает, так как LEFT JOIN возвращает значения NULL для столбца зависимых идентификаторов (вложенных таблиц), которые не являются правильным типом. Так что для собаки будет [d].[Id] ненулевой, и все остальные (конкретные) идентификаторы будут иметь значение NULL.

Все эти запросы могут страдать от проблем с производительностью из-за соединений таблиц. Именно поэтому TPT никогда не является хорошим выбором для производительности запросов.

Запросы TPC

TPC улучшает TPT для всех этих запросов, так как количество таблиц, которые необходимо запрашивать, уменьшается. Кроме того, результаты каждой таблицы объединяются с использованием UNION ALL, что может быть значительно быстрее, чем соединение таблицы, так как не требуется выполнять сопоставление между строками или отменой дублирования строк.

  1. TPC SQL возвращает сущности всех типов в иерархии:

    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, возвращающий сущности из подмножества типов в иерархии:

    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 возвращает только сущности из одного конечного типа в иерархии:

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

Несмотря на то, что TPC лучше TPT для всех этих запросов, запросы TPH по-прежнему лучше при возврате экземпляров нескольких типов. Это одна из причин того, что TPH является стратегией по умолчанию, используемой EF Core.

Как показано в SQL для запроса #3, TPC действительно преуспевает при запросе сущностей одного конечного типа. Запрос использует только одну таблицу и не требует фильтрации.

Вставка и обновление TPC

TPC также хорошо работает при сохранении новой сущности, так как для этого требуется вставить только одну строку в одну таблицу. Это также верно для TPH. При использовании TPT строки должны быть вставлены во многие таблицы, которые будут работать менее хорошо.

То же самое часто верно для обновлений, хотя в этом случае все обновляемые столбцы находятся в одной таблице, даже для TPT, разница может не быть значительной.

Рекомендации по пространству

TPT и TPC могут использовать меньше хранилища, чем TPH, если существует множество подтипов со многими свойствами, которые часто не используются. Это связано с тем, что каждая строка в таблице TPH должна храниться NULL для каждого из этих неиспользуемых свойств. На практике это редко возникает, но стоит учитывать при хранении больших объемов данных с этими характеристиками.

Совет

Если ваша система базы данных поддерживает ее (например, SQL Server), попробуйте использовать "разреженные столбцы" для столбцов TPH, которые редко заполняются.

Создание ключей

Выбранная стратегия сопоставления наследования имеет последствия для создания и управления значениями первичного ключа. Ключи в TPH просты, так как каждый экземпляр сущности представлен одной строкой в одной таблице. Можно использовать любое поколение значений ключа, и никаких дополнительных ограничений не требуется.

Для стратегии TPT всегда есть строка в таблице, сопоставленной с базовым типом иерархии. Любое поколение ключей можно использовать в этой строке, а ключи для других таблиц связаны с этой таблицей с помощью ограничений внешнего ключа.

Вещи немного сложнее для TPC. Во-первых, важно понимать, что EF Core требует, чтобы все сущности в иерархии имели уникальное значение ключа, даже если сущности имеют разные типы. Таким образом, используя нашу примерную модель, собака не может иметь то же значение ключа идентификатора, что и Cat. Во-вторых, в отличие от TPT, не существует общей таблицы, которая может выступать в качестве одного места, где живут ключевые значения и могут быть созданы. Это означает, что не удается использовать простой Identity столбец.

Для баз данных, поддерживающих последовательности, значения ключей можно создать с помощью одной последовательности, на которую ссылается ограничение по умолчанию для каждой таблицы. Это стратегия, используемая в таблицах TPC выше, где каждая таблица имеет следующую команду:

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

AnimalSequence — это последовательность баз данных, созданная EF Core. Эта стратегия используется по умолчанию для иерархий TPC при использовании поставщика базы данных EF Core для SQL Server. Поставщики баз данных для других баз данных, поддерживающих последовательности, должны иметь аналогичный по умолчанию. Другие стратегии создания ключей, использующие последовательности, такие как шаблоны Hi-Lo, также могут использоваться с TPC.

Хотя стандартные столбцы удостоверений не будут работать с TPC, можно использовать столбцы удостоверений, если каждая таблица настроена с соответствующим начальным и добавочным значением, чтобы значения, созданные для каждой таблицы, никогда не будут конфликтовать. Например:

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 не поддерживает последовательности или начальное или добавочное значение удостоверения, поэтому создание целочисленных значений ключей не поддерживается при использовании SQLite со стратегией TPC. Однако создание на стороне клиента или глобально уникальные ключи, например ключи GUID, поддерживаются в любой базе данных, включая SQLite.

Ограничения внешнего ключа

Стратегия сопоставления TPC создает денормализованную схему SQL- это одна из причин, почему некоторые пуристы базы данных против него. Например, рассмотрим столбец FavoriteAnimalIdвнешнего ключа. Значение в этом столбце должно соответствовать значению первичного ключа некоторых животных. Это можно применить в базе данных с простым ограничением FK при использовании TPH или TPT. Например:

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

Но при использовании TPC первичный ключ для животного хранится в таблице для конкретного типа этого животного. Например, первичный ключ кошки хранится в Cats.Id столбце, а первичный ключ собаки хранится в Dogs.Id столбце и т. д. Это означает, что для этой связи невозможно создать ограничение FK.

На практике это не проблема, если приложение не пытается вставить недопустимые данные. Например, если все данные вставляются EF Core и используют навигации для связи сущностей, то гарантируется, что столбец FK будет содержать допустимое значение PK в любое время.

Сводка и руководство

В итоге TPC — это хорошая стратегия сопоставления, используемая, когда код будет в основном запрашивать сущности одного конечного типа. Это связано с тем, что требования к хранилищу меньше, и нет дискриминационных столбцов, которые могут потребовать индекс. Вставки и обновления также эффективны.

Это говорится, что TPH обычно хорошо подходит для большинства приложений и является хорошим по умолчанию для широкого спектра сценариев, поэтому не добавляйте сложность TPC, если вы не нуждаетесь в нем. В частности, если ваш код будет в основном запрашивать сущности многих типов, например писать запросы к базовому типу, а затем опираться на TPH по TPC.

Используйте TPT, только если это ограничено внешними факторами.

Пользовательские шаблоны обратной инженерии

Теперь можно настроить шаблонный код при обратном проектировании модели EF из базы данных. Начало работы с добавлением шаблонов по умолчанию в проект:

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

Затем можно настроить шаблоны и автоматически использовать dotnet ef dbcontext scaffold их Scaffold-DbContext.

Дополнительные сведения см . в разделе "Пользовательские шаблоны обратной инженерии".

Совет

Команда EF продемонстрировала и подробно говорила о шаблонах обратной инженерии в эпизоде стенда сообщества данных .NET. Как и во всех эпизодах стенда сообщества, вы можете смотреть эпизод шаблонов T4 теперь на YouTube.

Соглашения о сборке моделей

EF Core использует "модель" метаданных для описания того, как типы сущностей приложения сопоставляются с базовой базой данных. Эта модель построена с помощью набора около 60 "соглашений". Затем модель, созданная по соглашениям, можно настроить с помощью атрибутов сопоставления (ака "заметки данных") и /или вызовов DbModelBuilder API в OnModelCreating.

Начиная с EF7 приложения теперь могут удалять или заменять любые из этих соглашений, а также добавлять новые соглашения. Соглашения о построении моделей — это эффективный способ управления конфигурацией модели, но может быть сложным и трудным для правильного получения. Во многих случаях существующую конфигурацию модели предварительного соглашения можно использовать вместо того, чтобы легко указать общую конфигурацию свойств и типов.

Изменения в соглашениях, используемых a, DbContext выполняются путем переопределения DbContext.ConfigureConventions метода. Например:

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

Совет

Чтобы найти все встроенные соглашения о сборке моделей, найдите каждый класс, реализующий IConvention интерфейс.

Совет

Код, показанный здесь, поставляется из ModelBuildingConventionsSample.cs.

Удаление существующего соглашения

Иногда одно из встроенных соглашений может не подходить для приложения, в этом случае его можно удалить.

Пример. Не создавайте индексы для столбцов внешнего ключа

Обычно имеет смысл создавать индексы для столбцов внешнего ключа (FK), поэтому для этого используется встроенное соглашение. ForeignKeyIndexConvention Просмотр представления отладки модели для Post типа сущности с связями Blog с иAuthor, можно увидеть, что создаются два индекса— один для FK, а другой — для BlogIdAuthorId FK.

  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

Однако индексы имеют дополнительные затраты, и , как показано здесь, они не всегда могут быть подходящими для всех столбцов FK. Для этого ForeignKeyIndexConvention можно удалить при создании модели:

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

Глядя на представление отладки модели Post сейчас, мы видим, что индексы на FK не созданы:

  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

Если это необходимо, индексы по-прежнему могут быть явно созданы для внешних ключевых столбцов, либо с помощью IndexAttribute:

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

Или с конфигурацией в OnModelCreating:

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

Глядя на Post тип сущности снова, он теперь содержит BlogId индекс, но не AuthorId индекс:

  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

Совет

Если модель не использует атрибуты сопоставления (заметки к данным aka) для настройки, все соглашения, заканчивающиеся AttributeConvention , можно безопасно удалить, чтобы ускорить построение модели.

Добавление нового соглашения

Удаление существующих соглашений — это начало, но как насчет добавления совершенно новых соглашений по созданию моделей? EF7 также поддерживает это!

Пример: ограничение длины дискриминационных свойств

Стратегия сопоставления наследования таблиц для каждой иерархии требует от дискриминационных столбцов, чтобы указать, какой тип представлен в любой строке. По умолчанию EF использует несвязанный строковый столбец для дискриминатора, который гарантирует, что она будет работать на любую дискриминационную длину. Однако ограничение максимальной длины дискриминационных строк может обеспечить более эффективное хранение и запросы. Давайте создадим новое соглашение, которое сделает это.

Соглашения о сборке моделей EF Core активируются на основе изменений, внесенных в модель по мере его создания. Благодаря этому модель обновляется так, как выполняется явная конфигурация, применяются атрибуты сопоставления и выполняются другие соглашения. Для участия в этом каждом соглашении реализуется один или несколько интерфейсов, определяющих, когда будет активировано соглашение. Например, соглашение, реализующее IEntityTypeAddedConvention , будет активировано при добавлении нового типа сущности в модель. Аналогичным образом, соглашение, реализующее оба IForeignKeyAddedConvention и IKeyAddedConvention будет активировано всякий раз, когда ключ или внешний ключ добавляются в модель.

Зная, какие интерфейсы для реализации могут быть сложными, так как конфигурация, сделанная в модель, может быть изменена или удалена в более позднюю точку. Например, ключ может быть создан по соглашению, но затем позже заменен при явной настройке другого ключа.

Давайте сделаем это немного более конкретным, сделав первую попытку реализовать соглашение о дискриминационных длинах:

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);
        }
    }
}

Это соглашение реализует IEntityTypeBaseTypeChangedConvention, что означает, что он будет активирован при изменении сопоставленной иерархии наследования для типа сущности. Затем соглашение находит и настраивает строковое дискриминационное свойство для иерархии.

Затем это соглашение используется путем вызова Add в ConfigureConventions:

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

Совет

Вместо прямого добавления экземпляра соглашения Add метод принимает фабрику для создания экземпляров соглашения. Это позволяет соглашению использовать зависимости от внутреннего поставщика услуг EF Core. Так как это соглашение не имеет зависимостей, параметр поставщика услуг называется _, указывая, что он никогда не используется.

Создание модели и просмотр Post типа сущности показывает, что это работало - дискриминационные свойства теперь настроены на максимальную длину 24:

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

Но что произойдет, если мы сейчас явно настраиваем другое дискриминационное свойство? Например:

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

Глядя на представление отладки модели, мы обнаружили, что длина дискриминации больше не настроена!

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

Это связано с тем, что дискриминационная собственность, которую мы настроили в нашем соглашении, позже была удалена при добавлении пользовательской дискриминации. Мы могли бы попытаться исправить это, реализуя другой интерфейс в нашем соглашении, чтобы реагировать на дискриминационные изменения, но выяснить, какой интерфейс реализовать не просто.

К счастью, существует другой способ приблизиться к этому, что делает вещи гораздо проще. Много времени, это не имеет значения, как выглядит модель во время его сборки, если окончательная модель правильна. Кроме того, конфигурация, которую мы хотим применить, часто не требует активации других соглашений для реагирования. Поэтому наше соглашение может реализовать IModelFinalizingConvention. Соглашения о завершении модели выполняются после завершения всей другой сборки модели и поэтому имеют доступ к окончательному состоянию модели. Соглашение о завершении модели обычно выполняет итерацию по всему элементу модели, настраивая элементы модели по мере ее выполнения. Таким образом, в этом случае мы найдем каждую дискриминацию в модели и настроим ее:

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);
            }
        }
    }
}

После создания модели с помощью этого нового соглашения мы обнаружили, что длина дискриминатора теперь настроена правильно, даже если она была настроена:

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

Просто для удовольствия, давайте пойдем дальше и настройте максимальную длину, чтобы быть длиной самой длинной дискриминационной ценности.

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);
            }
        }
    }
}

Теперь максимальная длина дискриминационных столбцов составляет 8, что является длиной "Функция", самой длинной дискриминационным значением в использовании.

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

Совет

Возможно, вам интересно, следует ли также создать индекс для дискриминационных столбцов. На сайте GitHub обсуждается эта тема. Короткий ответ заключается в том, что иногда индекс может быть полезным, но в большинстве случаев он, вероятно, не будет. Поэтому рекомендуется создавать соответствующие индексы здесь по мере необходимости, а не всегда иметь соглашение. Но если вы не согласны с этим, то приведенные выше соглашения можно легко изменить, чтобы создать индекс, а также.

Пример: длина по умолчанию для всех строковых свойств

Давайте рассмотрим другой пример, в котором можно использовать соглашение о завершении. На этот раз задав максимальную длину по умолчанию для любого строкового свойства, как показано на сайте GitHub. Соглашение выглядит довольно похоже на предыдущий пример:

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);
        }
    }
}

Это соглашение довольно просто. Он находит каждое строковое свойство в модели и задает максимальную длину 512. В представлении отладки в свойствах Postмы видим, что все свойства строки теперь имеют максимальную длину 512.

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 Но свойство, вероятно, должно позволить более 512 символов, или все наши посты будут довольно короткими! Это можно сделать, не изменив наше соглашение, явно настроив максимальную длину только для этого свойства с помощью атрибута сопоставления:

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

Или с кодом в OnModelCreating:

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

Теперь все свойства имеют максимальную длину 512, за исключением Content того, что было явно настроено с 4000:

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)

Поэтому почему наша конвенция не переопределяет явно настроенную максимальную длину? Ответ заключается в том, что EF Core отслеживает, как была создана каждая часть конфигурации. Это представлено перечислением ConfigurationSource . Различные типы конфигурации:

  • Explicit: элемент модели был явно настроен в OnModelCreating
  • DataAnnotation: элемент модели был настроен с помощью атрибута сопоставления (заметка к данным aka) в типе CLR
  • Convention: элемент модели был настроен соглашением о сборке модели

Соглашения никогда не переопределяют конфигурацию, помеченную как DataAnnotation или Explicit. Это достигается с помощью построителя соглашений, например, IConventionPropertyBuilderполученного Builder из свойства. Например:

property.Builder.HasMaxLength(512);

Вызов HasMaxLength построителя соглашений будет задавать только максимальную длину , если она еще не настроена атрибутом сопоставления или в OnModelCreating.

Такие методы построителя также имеют второй параметр: fromDataAnnotation Задайте для этого true значение, если соглашение делает конфигурацию от имени атрибута сопоставления. Например:

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

Это задает ConfigurationSourceDataAnnotationзначение , что означает, что значение теперь может быть переопределено явным сопоставлением, OnModelCreatingно не с помощью соглашений атрибутов, не относящихся к сопоставлению.

Наконец, прежде чем оставить этот пример, что произойдет, если мы используем оба MaxStringLengthConvention и DiscriminatorLengthConvention3 одновременно? Ответ заключается в том, что он зависит от того, какой порядок они добавляются, так как соглашения о завершении модели выполняются в том порядке, в котором они добавляются. Таким образом, если MaxStringLengthConvention добавляется последний, он будет выполняться последней, и он установит максимальную длину дискриминационных свойств 512. Таким образом, в этом случае лучше добавить DiscriminatorLengthConvention3 последнее, чтобы можно было переопределить максимальную длину по умолчанию только для дискриминационных свойств, оставляя все остальные свойства строки как 512.

Замена существующего соглашения

Иногда вместо того, чтобы полностью удалить существующее соглашение, мы хотим заменить его на соглашение, которое делает в основном то же самое, но с измененным поведением. Это полезно, так как существующее соглашение уже реализует необходимые интерфейсы, чтобы соответствующим образом активироваться.

Пример: сопоставление свойств opt-in

EF Core сопоставляет все общедоступные свойства чтения и записи по соглашению. Это может быть не подходит для способа определения типов сущностей. Чтобы изменить это, можно заменить PropertyDiscoveryConvention собственную реализацию, которая не сопоставляет любое свойство, если оно не сопоставляется явным образом или OnModelCreating не отмечено новым атрибутом:Persist

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

Вот новое соглашение:

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;
            }
        }
    }
}

Совет

При замене встроенного соглашения новая реализация соглашения должна наследоваться от существующего класса соглашения. Обратите внимание, что некоторые соглашения имеют реляционные или поставщики реализации, в этом случае новая реализация соглашения должна наследоваться от наиболее конкретного существующего класса соглашения для используемого поставщика базы данных.

Затем соглашение регистрируется с помощью Replace метода в ConfigureConventions:

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

Совет

Это случай, когда существующее соглашение имеет зависимости, представленные ProviderConventionSetBuilderDependencies объектом зависимостей. Они получены от внутреннего поставщика услуг, используя GetRequiredService и передаваемые конструктору соглашения.

Это соглашение работает путем получения всех доступных для чтения свойств и полей из заданного типа сущности. Если член имеет атрибут [Persist], он сопоставляется с помощью вызова:

entityTypeBuilder.Property(memberInfo);

С другой стороны, если элемент является свойством, которое иначе было сопоставлено, то он исключается из модели с помощью:

entityTypeBuilder.Ignore(propertyInfo.Name);

Обратите внимание, что это соглашение позволяет сопоставлять поля (в дополнение к свойствам) до тех пор, пока они помечены с помощью [Persist]. Это означает, что в модели можно использовать закрытые поля в качестве скрытых ключей.

Рассмотрим следующие типы сущностей:

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; }
}

Модель, созданная из этих типов сущностей:

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

Обратите внимание, что обычно было бы сопоставлено, IsClean но так как оно не отмечено [Perist] (предположительно, потому что чистота не является постоянным свойством прачечной), он в настоящее время рассматривается как не сопоставленное свойство.

Совет

Это соглашение не может быть реализовано как соглашение о завершении модели, так как сопоставление свойства активирует множество других соглашений для выполнения для дальнейшей настройки сопоставленного свойства.

Сопоставление хранимых процедур

По умолчанию EF Core создает команды вставки, обновления и удаления, которые работают непосредственно с таблицами или обновляемыми представлениями. EF7 предоставляет поддержку сопоставления этих команд с хранимыми процедурами.

Совет

EF Core всегда поддерживает запросы с помощью хранимых процедур. Новая поддержка в EF7 явно связана с использованием хранимых процедур для вставок, обновлений и удалений.

Важно!

Поддержка сопоставления хранимых процедур не означает, что рекомендуется использовать хранимые процедуры.

Хранимые процедуры сопоставляются с OnModelCreating использованием InsertUsingStoredProcedure, UpdateUsingStoredProcedureи DeleteUsingStoredProcedure. Например, чтобы сопоставить хранимые процедуры с типом Person сущности:

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();
        });

Эта конфигурация сопоставляется со следующими хранимыми процедурами при использовании SQL Server:

Для вставок

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

Для обновлений

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

Для удаления

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

Совет

Хранимые процедуры не должны использоваться для каждого типа модели или для всех операций с заданным типом. Например, если указан только DeleteUsingStoredProcedure для данного типа, EF Core создаст SQL как обычный для операций вставки и обновления и использует хранимую процедуру только для удаления.

Первым аргументом, переданным каждому методу, является имя хранимой процедуры. Это может быть опущено, в этом случае EF Core будет использовать имя таблицы, добавленное с "_Insert", "_Update" или "_Delete". Таким образом, в приведенном выше примере, так как таблица называется "Люди", имена хранимых процедур можно удалить без изменений в функциональных возможностях.

Второй аргумент — это построитель, используемый для настройки входных и выходных данных хранимой процедуры, включая параметры, возвращаемые значения и столбцы результатов.

Параметры

Параметры должны быть добавлены в построитель в том же порядке, что и в определении хранимой процедуры.

Примечание.

Параметры можно назвать, но EF Core всегда вызывает хранимые процедуры с помощью позиционных аргументов, а не именованных аргументов. Проголосуйте за разрешение настройки сопоставления sproc для использования имен параметров для вызова , если вызов по имени — это то, что вам интересно.

Первый аргумент для каждого метода построителя параметров указывает свойство в модели, к которой привязан параметр. Это может быть лямбда-выражение:

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

Или строка, которая особенно полезна при сопоставлении теневых свойств:

storedProcedureBuilder.HasParameter("Name");

Параметры по умолчанию настроены для входных данных. Параметры output или output можно настроить с помощью вложенного построителя. Например:

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

Существует три различных метода построителя для различных вариантов параметров:

  • HasParameter указывает обычный параметр, привязанный к текущему значению данного свойства.
  • HasOriginalValueParameter указывает параметр, привязанный к исходному значению данного свойства. Исходное значение — это значение, которое свойство имело при запросе из базы данных, если известно. Если это значение не известно, то вместо этого используется текущее значение. Исходные параметры значения полезны для маркеров параллелизма.
  • HasRowsAffectedParameter указывает параметр, используемый для возврата количества строк, затронутых хранимой процедурой.

Совет

Исходные параметры значения должны использоваться для ключевых значений в хранимых процедурах update и delete. Это гарантирует, что правильная строка будет обновлена в будущих версиях EF Core, поддерживающих изменяемые значения ключей.

Возвращаемые значения

EF Core поддерживает три механизма возврата значений из хранимых процедур:

  • Выходные параметры, как показано выше.
  • Столбцы результатов, которые задаются с помощью HasResultColumn метода построителя.
  • Возвращаемое значение, которое ограничено возвратом количества затронутых строк и указывается с помощью HasRowsAffectedReturnValue метода построителя.

Значения, возвращаемые из хранимых процедур, часто используются для создания, по умолчанию или вычисляемых значений, таких как Identity ключ или вычисляемый столбец. Например, следующая конфигурация указывает четыре столбца результатов:

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);
        });

Они используются для возврата:

  • Созданное значение ключа для Id свойства.
  • Значение по умолчанию, созданное базой данных для FirstRecordedOn свойства.
  • Вычисляемое значение, созданное базой данных для RetrievedOn свойства.
  • Автоматически созданный rowversion маркер параллелизма для RowVersion свойства.

Эта конфигурация сопоставляется со следующей хранимой процедурой при использовании SQL Server:

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

Оптимистическая блокировка

Оптимистическая параллелизм работает так же, как и с хранимыми процедурами. Хранимая процедура должна:

  • Используйте маркер параллелизма в WHERE предложении, чтобы убедиться, что строка обновляется только в том случае, если он имеет допустимый маркер. Значение, используемое для маркера параллелизма, обычно, но не обязательно, исходное значение свойства токена параллелизма.
  • Возвращает количество затронутых строк, чтобы EF Core могли сравнить это с ожидаемым числом затронутых строк и вызвать исключение DbUpdateConcurrencyException , если значения не совпадают.

Например, следующая хранимая процедура SQL Server использует маркер автоматического rowversion параллелизма:

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

Это настроено в EF Core с помощью:

.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();
    });

Обратите внимание на указанные ниже моменты.

  • Используется исходное значение маркера RowVersion параллелизма.
  • Хранимая процедура использует WHERE предложение, чтобы убедиться, что строка обновляется только в том случае, если исходное RowVersion значение совпадает.
  • Новое созданное значение для нее RowVersion вставляется во временную таблицу.
  • Количество затронутых строк (@@ROWCOUNT) и созданное RowVersion значение возвращаются.

Сопоставление иерархий наследования с хранимыми процедурами

EF Core требует, чтобы хранимые процедуры следовали макету таблицы для типов в иерархии. Это означает следующее.

  • Иерархия, сопоставленная с помощью TPH, должна иметь одну вставку, обновление и/или удаление хранимой процедуры, предназначенной для одной сопоставленной таблицы. Хранимые процедуры вставки и обновления должны иметь параметр для дискриминационных значений.
  • Иерархия, сопоставленная с помощью TPT, должна иметь хранимую процедуру вставки, обновления и/или удаления для каждого типа, включая абстрактные типы. EF Core будет выполнять несколько вызовов при необходимости для обновления, вставки и удаления строк во всех таблицах.
  • Иерархия, сопоставленная с помощью TPC, должна иметь хранимую процедуру вставки, обновления и/или удаления для каждого конкретного типа, но не абстрактных типов.

Примечание.

Если вы хотите использовать одну хранимую процедуру для конкретного типа независимо от стратегии сопоставления, то голосуйте за поддержку, используя один sproc для каждого конкретного типа независимо от стратегии сопоставления наследования.

Сопоставление принадлежащих типов с хранимыми процедурами

Настройка хранимых процедур для принадлежащих типов выполняется в построителе вложенных типов. Например:

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();
            });
    });

Примечание.

В настоящее время хранимые процедуры для вставки, обновления и удаления поддерживают только собственные типы, необходимо сопоставить с отдельными таблицами. То есть собственный тип не может быть представлен столбцами в таблице владельца. Проголосуйте за добавление поддержки разделения таблицы на сопоставление sproc CUD, если это ограничение, которое вы хотите удалить.

Сопоставление сущностей соединения "многие ко многим" с хранимыми процедурами

Конфигурацию хранимых процедур соединения "многие ко многим" можно выполнять как часть конфигурации "многие ко многим". Например:

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();
                            });
                });
    });

Новые и улучшенные перехватчики и события

Перехватчики EF Core обеспечивают перехват, изменение и/или подавление операций EF Core. EF Core также включает традиционные событияи ведение журнала .NET.

EF7 включает следующие улучшения для перехватчиков:

Кроме того, EF7 включает новые традиционные события .NET для:

В следующих разделах показаны некоторые примеры использования этих новых возможностей перехвата.

Простые действия по созданию сущностей

Совет

Приведенный здесь код поставляется из SimpleMaterializationSample.cs.

Новое IMaterializationInterceptor поддерживает перехват до и после создания экземпляра сущности, а также до и после инициализации свойств этого экземпляра. Перехватчик может изменять или заменять экземпляр сущности в каждой точке. Позволяет:

  • Задание несопоставленных свойств или вызов методов, необходимых для проверки, вычисляемых значений или флагов.
  • Использование фабрики для создания экземпляров.
  • Создание другого экземпляра сущности, отличного от EF, обычно создает, например экземпляр из кэша или прокси-типа.
  • Внедрение служб в экземпляр сущности.

Например, предположим, что мы хотим отслеживать время, когда сущность была получена из базы данных, возможно, чтобы ее можно было отобразить для пользователя, редактируемого данными. Для этого сначала мы определим интерфейс:

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

Использование интерфейса распространено с перехватчиками, так как позволяет одному перехватчику работать с различными типами сущностей. Например:

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; }
}

Обратите внимание, что атрибут используется для указания того, что [NotMapped] это свойство используется только при работе с сущностью и не должно сохраняться в базе данных.

Затем перехватчик должен реализовать соответствующий метод IMaterializationInterceptor и задать время, полученное:

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

        return instance;
    }
}

Экземпляр этого перехватчика регистрируется при настройке DbContext:

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");
}

Совет

Этот перехватчик является бессерверным, что является общим, поэтому создается и совместно используется один экземпляр между всеми DbContext экземплярами.

Теперь, когда Customer запросы из базы данных запрашиваются, Retrieved свойство будет задано автоматически. Например:

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()}'");
}

Создает выходные данные:

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

Внедрение служб в сущности

Совет

Приведенный здесь код поставляется из InjectLoggerSample.cs.

EF Core уже имеет встроенную поддержку внедрения некоторых специальных служб в экземпляры контекста; Например, см. сведения об отложенной загрузке без прокси-серверов, которые работают путем внедрения ILazyLoader службы.

Можно IMaterializationInterceptor использовать для обобщения этого в любой службе. В следующем примере показано, как внедрить сущности ILogger таким образом, чтобы они могли выполнять собственное ведение журнала.

Примечание.

Внедрение служб в сущности связывает эти типы сущностей в внедренные службы, которые некоторые люди считают анти-шаблоном.

Как и раньше, интерфейс используется для определения того, что можно сделать.

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

И типы сущностей, которые будут выполнять журнал, должны реализовать этот интерфейс. Например:

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; }
}

На этот раз перехватчик должен реализовать IMaterializationInterceptor.InitializedInstance, который вызывается после создания каждого экземпляра сущности, и его значения свойств инициализированы. Перехватчик получает ILogger из контекста и инициализирует 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;
    }
}

На этот раз новый экземпляр перехватчика используется для каждого экземпляра, так как ILogger полученный может изменяться на DbContext экземплярDbContext, а ILogger кэшируется на перехватчике:

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

Теперь, когда Customer.PhoneNumber изменения изменяются, это изменение будет записано в журнал приложения. Например:

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

Перехват дерева выражений LINQ

Совет

Приведенный здесь код поставляется из QueryInterceptionSample.cs.

EF Core использует запросы .NET LINQ. Обычно это предполагает использование компилятора C#, VB или F# для создания дерева выражений, которое затем преобразуется EF Core в соответствующий SQL. Например, рассмотрим метод, который возвращает страницу клиентов:

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();
}

Совет

Этот запрос использует EF.Property метод, чтобы указать свойство для сортировки по. Это позволяет приложению динамически передавать имя свойства, разрешая сортировку по любому свойству типа сущности. Помните, что сортировка по неиндексированных столбцам может быть медленной.

Это будет работать нормально, если свойство, используемое для сортировки, всегда возвращает стабильное упорядочение. Но это не всегда может быть делом. Например, приведенный выше запрос LINQ создает следующее в SQLite при заказе по Customer.City:

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

Если есть несколько клиентов с Cityодинаковым, порядок этого запроса не является стабильным. Это может привести к отсутствующим или повторяющимся результатам, так как пользовательские страницы через данные.

Распространенный способ устранения этой проблемы — выполнить вторичную сортировку по первичному ключу. Однако вместо того, чтобы вручную добавлять это в каждый запрос, EF7 позволяет перехватывать дерево выражений запроса, где можно динамически добавлять вторичную упорядочение. Чтобы упростить это, мы снова будем использовать интерфейс, на этот раз для любой сущности с целым числом первичный ключ:

public interface IHasIntKey
{
    int Id { get; }
}

Этот интерфейс реализуется типами сущностей:

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; }
}

Затем нам нужен перехватчик, реализующий 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);
        }
    }
}

Это, вероятно, выглядит довольно сложно- и это! Работа с деревьями выражений обычно не легко. Давайте рассмотрим, что происходит:

  • По сути, перехватчик инкапсулирует объект ExpressionVisitor. Посетитель переопределяет VisitMethodCall, который будет вызываться всякий раз при вызове метода в дереве выражений запроса.

  • Посетитель проверка, является ли это вызовом OrderBy метода, который мы заинтересованы.

  • Если это так, посетитель дальше проверка, если вызов универсального метода предназначен для типа, реализующего наш IHasIntKey интерфейс.

  • На этом этапе мы знаем, что вызов метода имеет форму OrderBy(e => ...). Мы извлекаем лямбда-выражение из этого вызова и получаем параметр, используемый в этом выражении, т. е e.

  • Теперь мы создадим новый MethodCallExpression с помощью Expression.Call метода построителя. В этом случае вызывается ThenBy(e => e.Id)метод. Мы создадим это с помощью параметра, извлеченного выше, и доступа к Id свойству IHasIntKey интерфейса.

  • Входные данные в этот вызов являются исходными OrderBy(e => ...), поэтому конечный результат является выражением для OrderBy(e => ...).ThenBy(e => e.Id).

  • Это измененное выражение возвращается от посетителя, что означает, что запрос LINQ теперь был соответствующим образом изменен для включения ThenBy вызова.

  • EF Core продолжает и компилирует это выражение запроса в соответствующий SQL для используемой базы данных.

Этот перехватчик регистрируется так же, как и в первом примере. GetPageOfCustomers Выполнение теперь создает следующий SQL:

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

Теперь это всегда будет производить стабильный заказ, даже если есть несколько клиентов с одинаковыми City.

Наконец-то. Это много кода, чтобы сделать простое изменение запроса. И еще хуже, это может даже не работать для всех запросов. Известно, что трудно написать посетителя выражения, который распознает все фигуры запроса, которые он должен, и ни один из них не должен. Например, это, скорее всего, не будет работать, если заказ выполняется в подзапросе.

Это приводит нас к критической точке о перехватчиках- всегда спрашивайте себя, есть ли более легкий способ делать то, что вы хотите. Перехватчики мощны, но легко получить вещи неправильно. Они, как говорится, простой способ стрелять себя в ногу.

Например, представьте, что вместо этого мы изменили наш GetPageOfCustomers метод следующим образом:

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();
}

В этом случае ThenBy просто добавляется в запрос. Да, это может потребоваться сделать отдельно для каждого запроса, но это просто, легко понять и всегда будет работать.

Оптимистическая перехватка параллелизма

Совет

Код, показанный здесь, поставляется из ОптимистичногоConcurrencyInterceptionSample.cs.

EF Core поддерживает шаблон оптимистического параллелизма, проверка, что количество строк, фактически затронутых обновлением или удалением, совпадает с числом строк, которые будут затронуты. Это часто связано с токеном параллелизма; То есть значение столбца, которое будет соответствовать только ожидаемому значению, если строка не была обновлена после считывания ожидаемого значения.

EF сигнализирует о нарушении оптимистического параллелизма, вызывая DbUpdateConcurrencyExceptionисключение. В EF7 ISaveChangesInterceptor есть новые методы ThrowingConcurrencyException , ThrowingConcurrencyExceptionAsync которые вызываются до DbUpdateConcurrencyException создания. Эти точки перехвата позволяют подавлять исключение, возможно, в сочетании с асинхронными изменениями базы данных для устранения нарушения.

Например, если два запроса пытаются удалить одну и ту же сущность почти одновременно, то второй удаление может завершиться ошибкой, так как строка в базе данных больше не существует. Это может быть тонким результатом является то, что сущность была удалена в любом случае. Следующий перехватчик демонстрирует, как это можно сделать:

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));
}

Есть несколько вещей стоит отметить об этом перехватчике:

  • Реализованы синхронные и асинхронные методы перехвата. Это важно, если приложение может вызвать либоSaveChanges.SaveChangesAsync Однако если весь код приложения асинхронен, необходимо реализовать только ThrowingConcurrencyExceptionAsync его. Аналогичным образом, если приложение никогда не использует асинхронные методы базы данных, необходимо реализовать только ThrowingConcurrencyException их. Обычно это верно для всех перехватчиков с синхронными и асинхронными методами. (Возможно, стоит реализовать метод, который приложение не использует для создания, просто в случае, если в коде синхронизации или асинхронного кода возникает ошибка.)
  • Перехватчик имеет доступ к EntityEntry объектам для сохраненных сущностей. В этом случае это используется для проверка, происходит ли нарушение параллелизма для операции удаления.
  • Если приложение использует поставщик реляционной базы данных, ConcurrencyExceptionEventData объект можно привести к объекту RelationalConcurrencyExceptionEventData . Это предоставляет дополнительные реляционные сведения о выполняемой операции базы данных. В этом случае реляционный текст команды выводится в консоль.
  • Возвращаясь InterceptionResult.Suppress() , ef Core сообщает EF Core, чтобы подавить действие, что это было предпринять в этом случае, бросая DbUpdateConcurrencyException. Эта возможность изменить поведение EF Core, а не просто наблюдать за тем, что делает EF Core, является одной из самых мощных функций перехватчиков.

Отложенная инициализация строка подключения

Совет

Код, показанный здесь, поставляется из Lazy Подключение ionStringSample.cs.

строки Подключение ion часто являются статическими ресурсами, считываемыми из файла конфигурации. Их можно легко передать в UseSqlServer или аналогичное при настройке DbContext. Однако иногда строка подключения могут изменяться для каждого экземпляра контекста. Например, каждый клиент в мультитенантной системе может иметь разные строка подключения.

EF7 упрощает обработку динамических подключений и строка подключения с помощью усовершенствованийIDbConnectionInterceptor. Это начинается с возможности настройки DbContext без каких-либо строка подключения. Например:

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

Затем можно реализовать один из IDbConnectionInterceptor методов, чтобы настроить подключение перед его использованием. ConnectionOpeningAsyncявляется хорошим выбором, так как он может выполнять асинхронную операцию для получения строка подключения, поиска маркера доступа и т. д. Например, представьте, что служба область в текущий запрос, который понимает текущий клиент:

services.AddScoped<ITenantConnectionStringFactory, TestTenantConnectionStringFactory>();

Предупреждение

Выполнение асинхронного поиска для строка подключения, маркера доступа или аналогичного при каждом необходимости может быть очень медленным. Рассмотрите возможность кэширования этих вещей и периодически обновляйте кэшированную строку или маркер. Например, маркеры доступа часто можно использовать в течение значительного периода времени перед обновлением.

Это можно внедрить в каждый DbContext экземпляр с помощью внедрения конструктора:

public class CustomerContext : DbContext
{
    private readonly ITenantConnectionStringFactory _connectionStringFactory;

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

    // ...
}

Затем эта служба используется при создании реализации перехватчика для контекста:

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

Наконец, перехватчик использует эту службу для асинхронного получения строка подключения и при первом использовании подключения.

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;
    }
}

Примечание.

Строка подключения получается только при первом использовании подключения. После этого строка подключения, хранящиеся на нейDbConnection, будут использоваться без поиска новых строка подключения.

Совет

Этот перехватчик переопределяет асинхронный метод, который необходимо вызвать, так как служба для получения строка подключения должна вызываться из асинхронного ConnectionOpening пути кода.

Ведение журнала статистики запросов SQL Server

Совет

Приведенный здесь код поставляется из QueryStatisticsLoggerSample.cs.

Наконец, создадим два перехватчика, которые работают вместе для отправки статистики запросов SQL Server в журнал приложений. Чтобы создать статистику, нам нужно IDbCommandInterceptor сделать два действия.

Во-первых, перехватчик будет выполнять команды префикса, SET STATISTICS IO ONс помощью которых SQL Server отправляет статистику клиенту после использования результирующий набор:

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);
}

Во-вторых, перехватчик реализует новый DataReaderClosingAsync метод, который вызывается после DbDataReader завершения использования результатов, но до его закрытия. Когда SQL Server отправляет статистику, она помещает их во второй результат на читатель, поэтому на этом этапе перехватчик считывает этот результат путем вызова NextResultAsync , который заполняет статистику подключения.

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

    return result;
}

Второй перехватчик необходим для получения статистики из подключения и их записи в средство ведения журнала приложения. Для этого мы будем использовать IDbConnectionInterceptorновый ConnectionCreated метод. ConnectionCreated вызывается сразу после создания подключения EF Core, поэтому его можно использовать для выполнения дополнительной настройки этого подключения. В этом случае перехватчик получает ILogger и затем перехватывает 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;
}

Важно!

ConnectionCreated Методы ConnectionCreating вызываются только при создании DbConnectionEF Core. Они не будут вызываться, если приложение создает DbConnection и передает его в EF Core.

При выполнении кода, использующего эти перехватчики, отображаются статистические данные запросов SQL Server в журнале:

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.

Улучшения запросов

EF7 содержит много улучшений в переводе запросов LINQ.

GroupBy в качестве конечного оператора

Совет

Приведенный здесь код поставляется из GroupByFinalOperatorSample.cs.

EF7 поддерживает использование GroupBy в качестве конечного оператора в запросе. Например, следующий LINQ-запрос:

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

Преобразуется в следующий SQL при использовании SQL Server:

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

Примечание.

Этот тип GroupBy не преобразуется непосредственно в SQL, поэтому EF Core выполняет группировку по возвращаемым результатам. Однако это не приводит к передаче дополнительных данных с сервера.

GroupJoin в качестве конечного оператора

Совет

Приведенный здесь код поставляется из GroupJoinFinalOperatorSample.cs.

EF7 поддерживает использование GroupJoin в качестве конечного оператора в запросе. Например, следующий LINQ-запрос:

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

Преобразуется в следующий SQL при использовании SQL Server:

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

Совет

Код, показанный здесь, поставляется из GroupByEntityTypeSample.cs.

EF7 поддерживает группирование по типу сущности. Например, следующий LINQ-запрос:

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

Преобразуется в следующий SQL при использовании SQLite:

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]

Помните, что группирование по уникальному свойству, например первичному ключу, всегда будет более эффективным, чем группирование по типу сущности. Однако группирование по типам сущностей можно использовать как для типов ключевых, так и без ключей.

Кроме того, группирование по типу сущности с первичным ключом всегда приводит к одной группе на экземпляр сущности, так как каждая сущность должна иметь уникальное значение ключа. Иногда стоит переключить источник запроса, чтобы группироваться не обязательно. Например, следующий запрос возвращает те же результаты, что и предыдущий запрос:

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

Этот запрос преобразуется в следующий SQL при использовании SQLite:

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

Вложенные запросы не ссылаются на несгруппированные столбцы из внешнего запроса

Совет

Приведенный здесь код поставляется из UngroupedColumnsQuerySample.cs.

В EF Core 6.0 GROUP BY предложение будет ссылать на столбцы во внешнем запросе, который завершается ошибкой с некоторыми базами данных и неэффективным в других. Рассмотрим, например, следующий запрос :

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)
            };

В EF Core 6.0 в SQL Server это было преобразовано в:

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 перевод выполняется следующим образом:

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]

Коллекции только для чтения можно использовать для Contains

Совет

Код, показанный здесь, поставляется из ReadOnlySetQuerySample.cs.

EF7 поддерживает использование, Contains когда элементы для поиска содержатся в IReadOnlySet или IReadOnlyCollectionили IReadOnlyList. Например, следующий LINQ-запрос:

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

Преобразуется в следующий SQL при использовании SQL Server:

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))

Переводы для агрегатных функций

EF7 обеспечивает лучшую расширяемость для поставщиков для перевода агрегатных функций. Эта и другая работа в этой области привели к созданию нескольких новых переводов между поставщиками, в том числе:

Примечание.

Агрегатные функции, которые работают с IEnumerable аргументами, обычно переводятся только в GroupBy запросах. Проголосуйте за типы пространственных типов поддержки в столбцах JSON, если вы хотите удалить это ограничение.

Статистические функции строк

Совет

Приведенный здесь код поставляется из StringAggregateFunctionsSample.cs.

Запросы, использующие Join и Concat теперь преобразуются при необходимости. Например:

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

Этот запрос преобразуется в следующее при использовании SQL Server:

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]

В сочетании с другими строковыми функциями эти переводы позволяют выполнять некоторые сложные операции со строками на сервере. Например:

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) + "' "))
            });

Этот запрос преобразуется в следующее при использовании SQL Server:

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]

Пространственные агрегатные функции

Совет

Приведенный здесь код поставляется из SpatialAggregateFunctionsSample.cs.

Теперь поставщики баз данных, поддерживающие NetTopologySuite, могут переводить следующие пространственные агрегатные функции:

Совет

Эти переводы были реализованы командой sql Server и SQLite. Для других поставщиков обратитесь в службу поддержки поставщика, чтобы добавить поддержку, если она была реализована для этого поставщика.

Например:

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)) });

Этот запрос преобразуется в следующий SQL при использовании SQL Server:

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]

Статистические агрегатные функции

Совет

Код, показанный здесь, поставляется из статистических данныхAggregateFunctionsSample.cs.

Переводы SQL Server реализованы для следующих статистических функций:

Совет

Эти переводы были реализованы командой sql Server. Для других поставщиков обратитесь в службу поддержки поставщика, чтобы добавить поддержку, если она была реализована для этого поставщика.

Например:

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))
        });

Этот запрос преобразуется в следующий SQL при использовании SQL Server:

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]

Перевод string.IndexOf

Совет

Приведенный здесь код поставляется из MiscellaneousTranslationsSample.cs.

EF7 теперь преобразуется String.IndexOf в запросы LINQ. Например:

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

Этот запрос преобразуется в следующий SQL при использовании SQL Server:

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

GetType Перевод типов сущностей

Совет

Приведенный здесь код поставляется из MiscellaneousTranslationsSample.cs.

EF7 теперь преобразуется Object.GetType() в запросы LINQ. Например:

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

Этот запрос преобразуется в следующий SQL при использовании SQL Server с наследованием TPH:

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'

Обратите внимание, что этот запрос возвращает только Post экземпляры, которые на самом деле являются типом Post, а не производными типами. Это отличается от запроса, использующего is или OfTypeвозвращающего экземпляры любых производных типов. Например, рассмотрим запрос:

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

Который преобразуется в другой SQL:

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

И возвратит оба PostFeaturedPost и сущности.

Поддержка AT TIME ZONE

Совет

Приведенный здесь код поставляется из MiscellaneousTranslationsSample.cs.

EF7 представляет новые AtTimeZone функции для DateTime и DateTimeOffset. Эти функции преобразуются в AT TIME ZONE предложения в созданном SQL. Например:

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"),
        });

Этот запрос преобразуется в следующий SQL при использовании SQL Server:

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]

Совет

Эти переводы были реализованы командой sql Server. Для других поставщиков обратитесь в службу поддержки поставщика, чтобы добавить поддержку, если она была реализована для этого поставщика.

Отфильтрованное включение при скрытых навигациях

Совет

Приведенный здесь код поставляется из MiscellaneousTranslationsSample.cs.

Теперь методы Include можно использовать с EF.Property. Это позволяет фильтровать и упорядочить даже для свойств частной навигации или частных навигаций, представленных полями. Например:

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

Это соответствует следующей записи:

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

Но не требуется Blog.Posts быть общедоступным.

При использовании SQL Server оба запроса выше преобразуют:

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]

Перевод Cosmos для Regex.IsMatch

Совет

Код, показанный здесь, поставляется из CosmosQueriesSample.cs.

EF7 поддерживает использование Regex.IsMatch в запросах LINQ к Azure Cosmos DB. Например:

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

Преобразуется в следующий SQL:

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

Усовершенствования API DbContext и поведения

EF7 содержит различные небольшие улучшения для DbContext и связанных классов.

Совет

Код для примеров в этом разделе поставляется из DbContextApiSample.cs.

Подавитель для неинициализированных свойств DbSet

Общедоступные, наборные DbSet свойства для объекта DbContext автоматически инициализированы EF Core при DbContext создании. Например, рассмотрим следующее DbContext определение:

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

Свойство Blogs будет присвоено DbSet<Blog> экземпляру в рамках создания экземпляра DbContext . Это позволяет использовать контекст для запросов без каких-либо дополнительных шагов.

Однако после введения ссылочных типов, допускающих значение NULL C#, компилятор теперь предупреждает, что свойство Blogs , не допускающее значение NULL, не инициализировано:

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

Это нефиксное предупреждение; Свойство имеет значение, отличное от NULL, по EF Core. Кроме того, объявление свойства как допускающего значение NULL приведет к тому, что предупреждение уйдет, но это не рекомендуется, так как, концептуально, свойство не может иметь значение NULL и никогда не будет иметь значение NULL.

EF7 содержит средство DiagnosticSuppressor для DbSet свойств, DbContext которое останавливает компилятора от создания этого предупреждения.

Совет

Этот шаблон был создан в дни, когда автоматические свойства C# были очень ограничены. С помощью современного C# рекомендуется сделать автоматически доступные только для чтения свойства, а затем явно инициализировать их в DbContext конструкторе или получить кэшированный DbSet экземпляр из контекста при необходимости. Например, public DbSet<Blog> Blogs => Set<Blog>().

Различает отмену от сбоя в журналах

Иногда приложение явно отменяет запрос или другую операцию базы данных. Обычно это делается с помощью переданного CancellationToken метода, выполняющего операцию.

В EF Core 6 события регистрируются при отмене операции так же, как и при сбое операции по какой-либо другой причине. EF7 представляет новые события журнала специально для отмененных операций базы данных. Эти новые события по умолчанию регистрируются на Debug уровне. В следующей таблице показаны соответствующие события и их уровни журнала по умолчанию:

Мероприятие Description Уровень ведения журнала по умолчанию
CoreEventId.QueryIterationFailed Произошла ошибка при обработке результатов запроса. LogLevel.Error
CoreEventId.SaveChangesFailed Произошла ошибка при попытке сохранить изменения в базе данных. LogLevel.Error
RelationalEventId.CommandError Произошла ошибка во время выполнения команды базы данных. LogLevel.Error
CoreEventId.QueryCanceled Запрос был отменен. LogLevel.Debug
CoreEventId.SaveChangesCanceled Команда базы данных была отменена при попытке сохранить изменения. LogLevel.Debug
RelationalEventId.CommandCanceled DbCommand Выполнение отменено. LogLevel.Debug

Примечание.

Отмена обнаруживается путем просмотра исключения, а не проверка маркера отмены. Это означает, что отмены, не активированные с помощью маркера отмены, по-прежнему будут обнаружены и вошедированы таким образом.

Новые IProperty и INavigation перегрузки для EntityEntry методов

Код, работающий с моделью EF, часто содержит IPropertyINavigation или представляет свойства или метаданные навигации. Затем EntityEntry используется для получения значения свойства или навигации или запроса его состояния. Однако до EF7 это требует передачи имени свойства или навигации методам метода EntityEntry, который затем повторно подстановки IProperty или INavigation. В EF7 IPropertyINavigation вместо этого можно передать напрямую, избегая дополнительного поиска.

Например, рассмотрим метод для поиска всех братьев и сестер заданной сущности:

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));
}

Этот метод находит родительский элемент заданной сущности, а затем передает обратному INavigationCollection методу родительской записи. Затем эти метаданные используются для возврата всех братьев и сестер данного родительского элемента. Ниже приведен пример его использования:


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}'");
}

И выходные данные:

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 для типов сущностей общего типа

EF Core может использовать один и тот же тип СРЕДЫ CLR для нескольких различных типов сущностей. Они называются типами сущностей общего типа и часто используются для сопоставления типа словаря с парами "ключ-значение", используемыми для свойств типа сущности. Например, BuildMetadata тип сущности можно определить без определения выделенного типа СРЕДЫ CLR:

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");
    });

Обратите внимание, что тип сущности общего типа должен быть назван . В данном случае это имя BuildMetadata. Затем эти типы сущностей получают доступ с помощью DbSet типа сущности, полученного с помощью имени. Например:

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

Это DbSet можно использовать для отслеживания экземпляров сущностей:

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" }
    });

И выполнение запросов:

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

Теперь в EF7 также есть Entry метод, на DbSet котором можно использовать для получения состояния экземпляра, даже если он еще не отслеживается. Например:

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

ContextInitialized теперь регистрируется как Debug

В EF7 ContextInitialized событие регистрируется на Debug уровне. Например:

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

В предыдущих выпусках он был зарегистрирован на Information уровне. Например:

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

При необходимости уровень журнала можно изменить на Information:

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

IEntityEntryGraphIterator является общедоступным для использования

В EF7 IEntityEntryGraphIterator служба может использоваться приложениями. Эта служба используется внутренне при обнаружении графа сущностей для отслеживания, а также путем TrackGraph. Ниже приведен пример, который выполняет итерацию по всем сущностям, доступным из некоторых начальных сущностей:

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();

Примечание.

  • Итератор останавливает обход из заданного узла при возврате falseделегата обратного вызова. В этом примере отслеживается посещаемые сущности и возвращается false , когда сущность уже посетила. Это предотвращает бесконечные циклы, полученные из циклов в графе.
  • Объект EntityEntryGraphNode<TState> позволяет передавать состояние, не записывая его в делегат.
  • Для каждого узла, отличного от первого, узел был обнаружен и навигация, обнаруженная с помощью обратного вызова, передается обратному вызову.

Усовершенствования сборки моделей

EF7 содержит множество небольших улучшений в построении моделей.

Совет

Код для примеров в этом разделе поставляется из ModelBuildingSample.cs.

Индексы могут быть по возрастанию или убыванию

По умолчанию EF Core создает индексы по возрастанию. EF7 также поддерживает создание убывящих индексов. Например:

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

Или, используя Index атрибут сопоставления:

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

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

Это редко полезно для индексов по одному столбцу, так как база данных может использовать один и тот же индекс для упорядочивания в обоих направлениях. Однако это не относится к составным индексам по нескольким столбцам, где порядок для каждого столбца может быть важным. EF Core поддерживает это, позволяя нескольким столбцам иметь разные упорядочения для каждого столбца. Например:

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

Или с помощью атрибута сопоставления:

[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();
}

Это приводит к следующему SQL при использовании SQL Server:

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

Наконец, можно создать несколько индексов по одному упорядоченному набору столбцов, предоставив имена индексов. Например:

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);

Или, используя атрибуты сопоставления:

[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();
}

При этом создается следующий SQL на SQL Server:

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);

Атрибут сопоставления для составных ключей

EF7 представляет новый атрибут сопоставления (ака "заметка к данным") для указания свойства первичного ключа или свойств любого типа сущности. В отличие от System.ComponentModel.DataAnnotations.KeyAttributeэтого, PrimaryKeyAttribute класс типа сущности помещается вместо свойства ключа. Например:

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

Это делает его естественным для определения составных ключей:

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

Определение индекса класса также означает, что его можно использовать для указания частных свойств или полей в качестве ключей, даже если они обычно игнорируются при создании модели EF. Например:

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

DeleteBehavior Атрибут сопоставления

EF7 представляет атрибут сопоставления (ака "заметка к данным"), чтобы указать DeleteBehavior связь. Например, необходимые связи создаются по DeleteBehavior.Cascade умолчанию. Это можно изменить по DeleteBehavior.NoAction умолчанию с помощью DeleteBehaviorAttribute:

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

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

Это приведет к отключению каскадных удалений для связи "Блог-записи".

Свойства, сопоставленные с различными именами столбцов

Некоторые шаблоны сопоставления приводят к тому, что одно и то же свойство CLR сопоставляется с столбцом в каждой из нескольких разных таблиц. EF7 позволяет этим столбцам иметь разные имена. Например, рассмотрим простую иерархию наследования:

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 эти типы будут сопоставлены с тремя таблицами. Однако столбец первичного ключа в каждой таблице может иметь другое имя. Например:

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 позволяет настроить это сопоставление с помощью вложенного построителя таблиц:

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"));

При сопоставлении Breed наследования TPC свойство также можно сопоставить с различными именами столбцов в разных таблицах. Например, рассмотрим следующие таблицы TPC:

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 поддерживает это сопоставление таблиц:

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");
        });

Однонаправленные связи "многие ко многим"

EF7 поддерживает связи "многие ко многим", где одна сторона или другая не имеет свойства навигации. Например, рассмотрим Post и Tag типы:

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!;
}

Обратите внимание, что Post тип имеет свойство навигации для списка тегов, но тип Tag не имеет свойства навигации для записей. В EF7 это по-прежнему можно настроить как связь "многие ко многим", что позволяет использовать один и тот же Tag объект для многих разных записей. Например:

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

Это приводит к сопоставлению соответствующей таблицы соединения:

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
);

И связь может использоваться как многие ко многим в обычном режиме. Например, вставка некоторых записей, которые совместно используют различные теги из общего набора:

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();

Разбиение сущностей

Разделение сущностей сопоставляет один тип сущности с несколькими таблицами. Например, рассмотрим базу данных с тремя таблицами, в которые хранятся данные клиента:

  • Таблица для сведений о клиенте Customers
  • PhoneNumbers Таблица для номера телефона клиента
  • Addresses Таблица для адреса клиента

Ниже приведены определения для этих таблиц в SQL Server:

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
);

Каждая из этих таблиц обычно сопоставляется с собственным типом сущности с связями между типами. Однако если все три таблицы всегда используются вместе, то их можно сопоставить с одним типом сущности. Например:

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; }
}

Это достигается в EF7 путем вызова SplitToTable каждого разделения в типе сущности. Например, следующий код разбивает Customer тип сущности на CustomersPhoneNumbersAddresses и таблицы, показанные выше:

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);
                });
    });

Обратите внимание, что при необходимости для каждой таблицы можно указать разные имена столбцов первичного ключа.

Строки UTF-8 SQL Server

Строки Юникода SQL Server, представленные типами nchar данных, nvarchar хранятся как UTF-16. Кроме того, charvarchar типы данных используются для хранения строк, отличных от Юникода, с поддержкой различных символьных наборов.

Начиная с SQL Server 2019, charvarchar типы данных можно использовать для хранения строк Юникода с кодировкой UTF-8 . Достигается путем задания одного из параметров сортировки UTF-8. Например, следующий код настраивает строку SQL Server UTF-8 переменной длины для столбца CommentText :

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

Эта конфигурация создает следующее определение столбца SQL Server:

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])
);

Темпоральные таблицы поддерживают принадлежащие сущности

Сопоставление темпоральных таблиц EF Core SQL Server было улучшено в EF7 для поддержки общего доступа к таблицам. В частности, сопоставление по умолчанию для отдельных сущностей использует общий доступ к таблицам.

Например, рассмотрим тип Employee сущности владельца и его собственный тип EmployeeInfoсущности:

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; }
}

Если эти типы сопоставляются с той же таблицей, то в EF7 эта таблица может быть сделана темпоральной таблицей:

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");
            }));

Примечание.

Чтобы упростить эту настройку, можно отслеживать проблему #29303. Проголосуйте за этот вопрос, если это то, что вы хотите увидеть, реализовано.

Улучшенное создание значений

EF7 включает два существенных улучшения автоматического создания значений для ключевых свойств.

Совет

Код для примеров в этом разделе поставляется из ValueGenerationSample.cs.

Создание значений для защищенных типов DDD

В дизайне на основе домена (DDD) защищенные ключи могут повысить безопасность типов свойств ключей. Это достигается путем упаковки типа ключа в другой тип, который зависит от использования ключа. Например, следующий код определяет ProductId тип ключей продукта и CategoryId тип для ключей категорий.

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; }
}

Затем они используются в Product типах сущностей и Category в следующих типах:

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();
}

Это делает невозможным случайно назначить идентификатор категории продукту или наоборот.

Предупреждение

Как и во многих понятиях DDDD, эта улучшенная безопасность типов приходит за счет дополнительной сложности кода. Стоит рассмотреть вопрос о том, следует ли, например, назначить идентификатор продукта категории то, что когда-либо произойдет. Сохранение простых вещей может быть в целом более полезным для базы кода.

Защищенные типы ключей, показанные здесь, оба значения ключей оболочки int , что означает, что целочисленные значения будут использоваться в сопоставленных таблицах базы данных. Это достигается путем определения преобразователей значений для типов:

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))
    {
    }
}

Примечание.

В этом коде используются struct типы. Это означает, что они имеют соответствующую семантику типа значений для использования в качестве ключей. Если class типы используются вместо этого, они должны переопределить семантику равенства или также указать средство сравнения значений.

В EF7 типы ключей на основе преобразователей значений могут использовать автоматически созданные значения ключей до тех пор, пока базовый тип поддерживает это. Это настроено обычным способом с помощью ValueGeneratedOnAdd:

modelBuilder.Entity<Product>().Property(product => product.Id).ValueGeneratedOnAdd();
modelBuilder.Entity<Category>().Property(category => category.Id).ValueGeneratedOnAdd();

По умолчанию это приводит к столбцам IDENTITY при использовании с SQL Server:

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);

Которые используются обычным способом для создания ключевых значений при вставке сущностей:

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

EF Core поддерживает создание ключевых значений с помощью столбцов SQL Server IDENTITY или шаблона Hi-Lo на основе блоков ключей, созданных последовательностью баз данных. EF7 предоставляет поддержку последовательности баз данных, подключенной к ограничению по умолчанию ключа. В самой простой форме это просто требует, чтобы EF Core использовал последовательность для свойства ключа:

modelBuilder.Entity<Product>().Property(product => product.Id).UseSequence();

Это приводит к определению последовательности в базе данных:

CREATE SEQUENCE [ProductSequence] START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE;

Который затем используется в ограничении по умолчанию для ключевого столбца:

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);

Примечание.

Эта форма создания ключей используется по умолчанию для созданных ключей в иерархиях типов сущностей с помощью стратегии сопоставления TPC.

При необходимости последовательность может быть присвоена другому имени и схеме. Например:

modelBuilder
    .Entity<Product>()
    .Property(product => product.Id)
    .UseSequence("ProductsSequence", "northwind");

Дальнейшая настройка последовательности формируется путем явной настройки в модели. Например:

modelBuilder
    .HasSequence<int>("ProductsSequence", "northwind")
    .StartsAt(1000)
    .IncrementsBy(2);

Улучшения средств миграции

EF7 включает два существенных улучшения при использовании средств командной строки ef Core Migrations.

UseSqlServer etc. Accept null

Очень распространено чтение строка подключения из файла конфигурации, а затем передача этого строка подключения UseSqlServerв , UseSqliteили эквивалентный метод для другого поставщика. Например:

services.AddDbContext<BloggingContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("BloggingDatabase")));

При применении миграции также часто передается строка подключения. Например:

dotnet ef database update --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"

Или при использовании пакета миграций.

./bundle.exe --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb"

В этом случае, даже если строка подключения чтение из конфигурации не используется, код запуска приложения по-прежнему пытается прочитать его из конфигурации и передать его UseSqlServerв . Если конфигурация недоступна, это приведет к UseSqlServerпередаче null в . В EF7 это разрешено, если строка подключения в конечном итоге устанавливается позже, например путем передачи --connection в средство командной строки.

Примечание.

Это изменение было внесено и UseSqlServerUseSqlite. Для других поставщиков обратитесь к обработчику поставщика, чтобы внести эквивалентные изменения, если он еще не был выполнен для этого поставщика.

Обнаружение выполнения средств

EF Core запускает код приложения при dotnet-efиспользовании команд PowerShell . Иногда может потребоваться обнаружить эту ситуацию, чтобы предотвратить выполнение неуместного кода во время разработки. Например, код, который автоматически применяет миграции при запуске, вероятно, не должен делать это во время разработки. В EF7 это можно обнаружить с помощью флага EF.IsDesignTime :

if (!EF.IsDesignTime)
{
    await context.Database.MigrateAsync();
}

EF Core задает IsDesignTime значение true , когда код приложения выполняется от имени средств.

Улучшения производительности для прокси-серверов

EF Core поддерживает динамически созданные прокси-серверы для отложенной загрузки и отслеживания изменений. EF7 содержит два улучшения производительности при использовании этих прокси-серверов:

  • Теперь типы прокси-сервера создаются лениво. Это означает, что начальное время сборки модели при использовании прокси-серверов может быть значительно быстрее с EF7, чем в EF Core 6.0.
  • Теперь прокси-серверы можно использовать с скомпилированных моделей.

Ниже приведены некоторые результаты производительности для модели с 449 типами сущностей, 6390 свойствами и 720 связями.

Сценарий Метод Среднее значение Ошибка StdDev
EF Core 6.0 без прокси-серверов TimeToFirstQuery 1.085 s 0.0083 s 0.0167 s
EF Core 6.0 с прокси-серверами отслеживания изменений TimeToFirstQuery 13.01 s 0.2040 s 0.4110 s
EF Core 7.0 без прокси-серверов TimeToFirstQuery 1.442 s 0.0134 s 0.0272 s
EF Core 7.0 с прокси-серверами отслеживания изменений TimeToFirstQuery 1.446 s 0.0160 s 0.0323 s
EF Core 7.0 с прокси-серверами отслеживания изменений и скомпилированной моделью TimeToFirstQuery 0.162 s 0.0062 s 0.0125 s

Таким образом, в этом случае модель с прокси-серверами отслеживания изменений может быть готова к выполнению первого запроса 80 раз быстрее в EF7, чем было возможно с EF Core 6.0.

Привязка данных Windows Forms первого класса

Команда Windows Forms в значительной степени улучшила интерфейс конструктора Visual Studio. Сюда входят новые возможности привязки данных, которые хорошо интегрируются с EF Core.

Кратко, новый интерфейс предоставляет Visual Studio U.I. для создания ObjectDataSource:

Choose Category data source type

Затем его можно привязать к EF Core DbSet с помощью простого кода:

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;
    }
}

См. статью "Начало работы с Windows Forms " для полного пошагового руководства и примера примера приложения WinForms.