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

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

Совет

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

Совет

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

Add и AddAsync

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

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

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

AddRange, UpdateRange, AttachRange и RemoveRange

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

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

Примечание.

Это отличается от 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 = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(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);

context.SaveChanges();

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

Важно!

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

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

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

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

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

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

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

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

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

PropertyAccessMode Предпочтение Выбор создания сущностей Резерв Резервное создание сущностей
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);

context.SaveChanges();

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

context.SaveChanges();

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

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

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

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

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

Использование полей резервной копии, допускающих значение 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);
context.SaveChanges();

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

context.SaveChanges();

Выходные данные 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();