Дополнительные функции отслеживания изменений

В этом документе рассматриваются другие функции и сценарии, связанные с отслеживанием изменений.

Подсказка

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

Подсказка

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

Add и AddAsync

Entity Framework Core (EF Core) предоставляет асинхронные методы при использовании этого метода, что может привести к взаимодействию с базой данных. Синхронные методы также предоставляются для предотвращения накладных расходов при использовании баз данных, которые не поддерживают высокопроизводительный асинхронный доступ.

DbContext.Add и DbSet<TEntity>.Add обычно не обращаются к базе данных, так как эти методы по сути просто запускают отслеживание сущностей. Однако некоторые формы создания значений могут получить доступ к базе данных, чтобы создать значение ключа. Единственный генератор значений, который делает это и поставляется с EF Core — это HiLoValueGenerator<TValue>. Использование этого генератора является редким; Он никогда не настраивается по умолчанию. Это означает, что подавляющее большинство приложений должно использовать Add, а не AddAsync.

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

AddRange, UpdateRange, AttachRange и RemoveRange

DbSet<TEntity> и DbContext предоставляют альтернативные версии Add, Update, Attach и Remove, которые допускают несколько экземпляров в одном вызове. Эти методы: AddRange, UpdateRange, AttachRange и RemoveRange соответственно.

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

Замечание

Это отличается от EF6, где AddRange и Add автоматически вызывали DetectChanges, но множественные вызовы Add приводили к многократному вызову DetectChanges вместо одного. Это сделало AddRange более эффективным в EF6. В EF Core ни один из этих методов автоматически не вызывает DetectChanges.

Методы DbContext и DbSet

Многие методы, в том числе Add, UpdateAttachи Remove, имеют реализации для обоих DbSet<TEntity> и DbContext. Эти методы имеют точно то же поведение для обычных типов сущностей. Это связано с тем, что тип CLR сущности отображается на один и только один тип сущности в модели EF Core. Таким образом, тип CLR полностью определяет, где сущность вписывается в модель, и поэтому DbSet для использования можно определить неявно.

Исключением из этого правила является использование типов сущностей общего типа, которые в основном используются для сущностей соединения "многие ко многим". При использовании типа сущности общего типа необходимо сначала создать DbSet для используемого типа модели EF Core. Такие методы, как Add, UpdateAttachи Remove затем можно использовать в DbSet без неоднозначности относительно того, какой тип модели EF Core используется.

Типы сущностей общего типа используются по умолчанию для сущностей соединения в отношениях "многие ко многим". Тип сущности общего типа также можно явно настроить для использования в отношениях «многие ко многим». Например, приведенный ниже код настраивает Dictionary<string, int> как тип сущности соединения.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .SharedTypeEntity<Dictionary<string, int>>(
            "PostTag",
            b =>
            {
                b.IndexerProperty<int>("TagId");
                b.IndexerProperty<int>("PostId");
            });

    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<Dictionary<string, int>>(
            "PostTag",
            j => j.HasOne<Tag>().WithMany(),
            j => j.HasOne<Post>().WithMany());
}

Изменение внешних ключей и навигаций показывает, как связать две сущности путем отслеживания нового экземпляра сущности соединения. Приведенный ниже код делает это для типа сущности общего типа, используемого для Dictionary<string, int> сущности соединения:

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

var joinEntitySet = context.Set<Dictionary<string, int>>("PostTag");
var joinEntity = new Dictionary<string, int> { ["PostId"] = post.Id, ["TagId"] = tag.Id };
joinEntitySet.Add(joinEntity);

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

await context.SaveChangesAsync();

Обратите внимание, что DbContext.Set<TEntity>(String) используется для создания DbSet для типа сущности PostTag . Затем этот dbSet можно использовать для вызова Add с новым экземпляром сущности соединения.

Это важно

Тип CLR, используемый для соединяемых типов сущностей по соглашению, может измениться в будущих выпусках, чтобы повысить производительность. Не зависите от какого-либо конкретного типа сущности соединения, если оно не было явно сконфигурировано, как это сделано для Dictionary<string, int> в коде выше.

Доступ к свойствам и полям

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

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

Режимы доступа к свойствам Field и PreferField приведут к тому, что EF Core получит доступ к значению свойства через его базовое поле. Аналогичным образом, Property и PreferProperty приведут к тому, что EF Core будет получать доступ к значению свойства через его геттер и сеттер.

Если используются Field или Property, и EF Core не может получить доступ к значению через поле или геттер/сеттер свойства соответственно, то EF Core вызовет исключение. Это гарантирует, что EF Core всегда использует доступ к полю или свойству, когда вы думаете, что это так.

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

FieldDuringConstruction и PreferFieldDuringConstruction настройте EF Core для использования полей резервного копирования только при создании экземпляров сущностей. Это позволяет выполнять запросы без побочных эффектов от методов 'getter' и 'setter', а последующие изменения свойств EF Core будут вызывать эти побочные эффекты.

Различные режимы доступа к свойствам приведены в следующей таблице:

РежимДоступаКСвойству Предпочтение Сущности, создающие предпочтения Резервный вариант Резервное создание сущностей
Field Поле Поле Выбрасывает исключение Выбрасывает исключение
Property Недвижимость Недвижимость Бросает исключение Выбрасывает
PreferField Поле Поле Недвижимость Недвижимость
PreferProperty Недвижимость Недвижимость Поле Поле
FieldDuringConstruction Недвижимость Поле Поле Вызывает исключение
PreferFieldDuringConstruction Недвижимость Поле Поле Недвижимость

Временные значения

EF Core создает временные значения ключей при отслеживании новых сущностей, которые будут иметь реальные значения ключей, созданные базой данных при вызове SaveChanges. Сведения об использовании этих временных значений см. в разделе "Отслеживание изменений" в EF Core .

Доступ к временным значениям

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

using var context = new BlogsContext();

var blog = new Blog { Name = ".NET Blog" };

context.Add(blog);

Console.WriteLine($"Blog.Id set on entity is {blog.Id}");
Console.WriteLine($"Blog.Id tracked by EF is {context.Entry(blog).Property(e => e.Id).CurrentValue}");

Выходные данные из этого кода:

Blog.Id set on entity is 0
Blog.Id tracked by EF is -2147482643

PropertyEntry.IsTemporary можно использовать для проверки временных значений.

Управление временными значениями

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

var blogs = new List<Blog> { new Blog { Id = -1, Name = ".NET Blog" }, new Blog { Id = -2, Name = "Visual Studio Blog" } };

var posts = new List<Post>
{
    new Post
    {
        Id = -1,
        BlogId = -1,
        Title = "Announcing the Release of EF Core 5.0",
        Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
    },
    new Post
    {
        Id = -2,
        BlogId = -2,
        Title = "Disassembly improvements for optimized managed debugging",
        Content = "If you are focused on squeezing out the last bits of performance for your .NET service or..."
    }
};

using var context = new BlogsContext();

foreach (var blog in blogs)
{
    context.Add(blog).Property(e => e.Id).IsTemporary = true;
}

foreach (var post in posts)
{
    context.Add(post).Property(e => e.Id).IsTemporary = true;
}

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

await context.SaveChangesAsync();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

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

  • Отрицательные числа используются в качестве временных значений ключей; это не обязательно, но является общим соглашением, чтобы предотвратить конфликты ключей.
  • Свойству Post.BlogId FK присваивается такое же отрицательное значение, как и у PK связанного блога.
  • Значения PK помечаются как временные посредством установки IsTemporary при отслеживании каждой сущности. Это необходимо, так как любое ключевое значение, предоставленное приложением, считается реальным значением ключа.

Просмотр представления отладки отслеживания изменений перед вызовом SaveChanges показывает, что значения PK помечены как временные и записи связаны с правильными блогами, включая исправление навигаций:

Blog {Id: -2} Added
  Id: -2 PK Temporary
  Name: 'Visual Studio Blog'
  Posts: [{Id: -2}]
Blog {Id: -1} Added
  Id: -1 PK Temporary
  Name: '.NET Blog'
  Posts: [{Id: -1}]
Post {Id: -2} Added
  Id: -2 PK Temporary
  BlogId: -2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: -2}
  Tags: []
Post {Id: -1} Added
  Id: -1 PK Temporary
  BlogId: -1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: -1}

После вызова SaveChangesэти временные значения были заменены реальными значениями, созданными базой данных:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Posts: [{Id: 2}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []

Работа со значениями по умолчанию

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

public class Token
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime ValidFrom { get; set; }
}

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

modelBuilder
    .Entity<Token>()
    .Property(e => e.ValidFrom)
    .HasDefaultValueSql("CURRENT_TIMESTAMP");

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

using var context = new BlogsContext();

context.AddRange(
    new Token { Name = "A" },
    new Token { Name = "B", ValidFrom = new DateTime(1111, 11, 11, 11, 11, 11) });

await context.SaveChangesAsync();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

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

Token {Id: 1} Unchanged
  Id: 1 PK
  Name: 'A'
  ValidFrom: '12/30/2020 6:36:06 PM'
Token {Id: 2} Unchanged
  Id: 2 PK
  Name: 'B'
  ValidFrom: '11/11/1111 11:11:11 AM'

Замечание

Для использования значений базы данных по умолчанию требуется, чтобы столбец базы данных был настроен на ограничение значения по умолчанию. Это выполняется автоматически миграциями EF Core при использовании HasDefaultValueSql или HasDefaultValue. Не забудьте создать ограничение по умолчанию для столбца другим способом, если миграция EF Core не используется.

Использование свойств, допускающих значение NULL

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

public class Foo1
{
    public int Id { get; set; }
    public int Count { get; set; }
}

Где это свойство настроено для базы данных по умолчанию –1:

modelBuilder
    .Entity<Foo1>()
    .Property(e => e.Count)
    .HasDefaultValue(-1);

Цель заключается в том, что значение по умолчанию -1 будет использоваться всякий раз, когда явное значение не задано. Однако, если значение равно 0 (значение CLR по умолчанию для целых чисел), EF Core не отличает это от отсутствия значения, что означает невозможность вставки значения 0 для этого свойства. Рассмотрим пример.

using var context = new BlogsContext();

var fooA = new Foo1 { Count = 10 };
var fooB = new Foo1 { Count = 0 };
var fooC = new Foo1();

context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);

Обратите внимание, что экземпляр, в котором Count явно задано значение 0, по-прежнему получает значение по умолчанию из базы данных, что не является тем, что мы намеревались. Простой способ справиться с этим — сделать свойство Count nullable.

public class Foo2
{
    public int Id { get; set; }
    public int? Count { get; set; }
}

Это делает значение CLR по умолчанию равным null вместо 0, что означает, что 0 теперь будет вставлено при явной установке.

using var context = new BlogsContext();

var fooA = new Foo2 { Count = 10 };
var fooB = new Foo2 { Count = 0 };
var fooC = new Foo2();

context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

Использование полей резервной копии, допускающих значение NULL

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

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

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

    private int? _count;

    public int Count
    {
        get => _count ?? -1;
        set => _count = value;
    }
}

Это позволяет вставить значение по умолчанию CLR (0), если свойство явно имеет значение 0, а не требуется предоставлять свойство в качестве значения NULL в модели домена. Рассмотрим пример.

using var context = new BlogsContext();

var fooA = new Foo3 { Count = 10 };
var fooB = new Foo3 { Count = 0 };
var fooC = new Foo3();

context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

Поля данных, допускающие значение NULL для булевых свойств

Этот шаблон особенно полезен при использовании логических свойств с созданными магазином значениями по умолчанию. Так как значение по умолчанию CLR для bool равно "false", это означает, что "false" нельзя вставить явно, используя обычный шаблон. Например, рассмотрим тип сущности User :

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }

    private bool? _isAuthorized;

    public bool IsAuthorized
    {
        get => _isAuthorized ?? true;
        set => _isAuthorized = value;
    }
}

Свойство IsAuthorized настроено со значением по умолчанию базы данных true:

modelBuilder
    .Entity<User>()
    .Property(e => e.IsAuthorized)
    .HasDefaultValue(true);

Свойство IsAuthorized можно задать явным образом "true" или "false" перед вставкой, или можно оставить неустановленным, в этом случае будет использоваться база данных по умолчанию:

using var context = new BlogsContext();

var userA = new User { Name = "Mac" };
var userB = new User { Name = "Alice", IsAuthorized = true };
var userC = new User { Name = "Baxter", IsAuthorized = false }; // Always deny Baxter access!

context.AddRange(userA, userB, userC);

await context.SaveChangesAsync();

Выходные данные SaveChanges при использовании SQLite показывают, что база данных по умолчанию используется для Mac, а явные значения задаются для Alice и Baxter:

-- Executed DbCommand (0ms) [Parameters=[@p0='Mac' (Size = 3)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("Name")
VALUES (@p0);
SELECT "Id", "IsAuthorized"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='True' (DbType = String), @p1='Alice' (Size = 5)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='False' (DbType = String), @p1='Baxter' (Size = 6)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Значение по умолчанию схемы используется только

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

modelBuilder
    .Entity<Bar>()
    .Property(e => e.Count)
    .HasDefaultValue(-1)
    .ValueGeneratedNever();