Разрешение удостоверений в EF Core

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

Совет

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

Совет

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

Введение

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

using var context = new BlogsContext();

var blogA = context.Blogs.Single(e => e.Id == 1);
var blogB = new Blog { Id = 1, Name = ".NET Blog (All new!)" };

try
{
    context.Update(blogB); // This will throw
}
catch (Exception e)
{
    Console.WriteLine($"{e.GetType().FullName}: {e.Message}");
}

Выполнение этого кода приводит к следующему исключению:

System.InvalidOperationException: невозможно отслеживать экземпляр типа сущности "Blog", так как другой экземпляр с ключевым значением "{Id: 1}" уже отслеживается. При присоединении существующих сущностей убедитесь, что присоединен только один экземпляр сущности с заданным значением ключа.

ДЛЯ EF Core требуется один экземпляр, так как:

  • Значения свойств могут отличаться между несколькими экземплярами. При обновлении базы данных EF Core необходимо знать, какие значения свойств следует использовать.
  • Связи с другими сущностями могут отличаться между несколькими экземплярами. Например, "blogA" может быть связан с другой коллекцией записей, отличных от "blogB".

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

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

Каждая из этих ситуаций рассматривается в следующих разделах.

Обновление сущности

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

Обновление вызовов

Часто сущность для обновления не приходит из запроса на DbContext, который мы будем использовать для SaveChanges. Например, в веб-приложении экземпляр сущности может быть создан из сведений в запросе POST. Самый простой способ обработки этого — использовать DbContext.Update или DbSet<TEntity>.Update. Например:

public static void UpdateFromHttpPost1(Blog blog)
{
    using var context = new BlogsContext();

    context.Update(blog);

    context.SaveChanges();
}

В данном случае:

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

Затем запрос применяет изменения

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

public static void UpdateFromHttpPost2(Blog blog)
{
    using var context = new BlogsContext();

    var trackedBlog = context.Blogs.Find(blog.Id);

    trackedBlog.Name = blog.Name;
    trackedBlog.Summary = blog.Summary;

    context.SaveChanges();
}

В данном случае:

  • Отслеживается только один экземпляр сущности; тот, который возвращается из базы данных запросом Find .
  • Update, Attachи т. д. не используются.
  • В базе данных обновляются только значения свойств, которые на самом деле изменились.
  • Выполняется два круговых пути базы данных.

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

public static void UpdateFromHttpPost3(Blog blog)
{
    using var context = new BlogsContext();

    var trackedBlog = context.Blogs.Find(blog.Id);

    context.Entry(trackedBlog).CurrentValues.SetValues(blog);

    context.SaveChanges();
}

SetValues принимает различные типы объектов, включая объекты передачи данных (DTOs) с именами свойств, которые соответствуют свойствам типа сущности. Например:

public static void UpdateFromHttpPost4(BlogDto dto)
{
    using var context = new BlogsContext();

    var trackedBlog = context.Blogs.Find(dto.Id);

    context.Entry(trackedBlog).CurrentValues.SetValues(dto);

    context.SaveChanges();
}

Или словарь с записями name/value для значений свойств:

public static void UpdateFromHttpPost5(Dictionary<string, object> propertyValues)
{
    using var context = new BlogsContext();

    var trackedBlog = context.Blogs.Find(propertyValues["Id"]);

    context.Entry(trackedBlog).CurrentValues.SetValues(propertyValues);

    context.SaveChanges();
}

Дополнительные сведения о работе со значениями свойств см. в разделе Accessing отслеживаемых сущностей .

Использование исходных значений

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

public static void UpdateFromHttpPost6(Blog blog, Dictionary<string, object> originalValues)
{
    using var context = new BlogsContext();

    context.Attach(blog);
    context.Entry(blog).OriginalValues.SetValues(originalValues);

    context.SaveChanges();
}

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

В данном случае:

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

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

Совет

Хотя этот подход имеет привлекательные характеристики, он требует отправки исходных значений сущности в веб-клиент и из него. Тщательно рассмотрите, стоит ли эта дополнительная сложность стоит преимуществ; для многих приложений один из более простых подходов является более прагматичным.

Присоединение сериализованного графа

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

Графы без дубликатов

Прежде чем идти дальше, важно признать, что:

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

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

using var context = new BlogsContext();

var blogs = context.Blogs.Include(e => e.Posts).ToList();

var serialized = JsonConvert.SerializeObject(
    blogs,
    new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, Formatting = Formatting.Indented });

Console.WriteLine(serialized);

Код JSON, созданный из этого кода:

[
  {
    "Id": 1,
    "Name": ".NET Blog",
    "Summary": "Posts about .NET",
    "Posts": [
      {
        "Id": 1,
        "Title": "Announcing the Release of EF Core 5.0",
        "Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
        "BlogId": 1
      },
      {
        "Id": 2,
        "Title": "Announcing F# 5",
        "Content": "F# 5 is the latest version of F#, the functional programming language...",
        "BlogId": 1
      }
    ]
  },
  {
    "Id": 2,
    "Name": "Visual Studio Blog",
    "Summary": "Posts about Visual Studio",
    "Posts": [
      {
        "Id": 3,
        "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...",
        "BlogId": 2
      },
      {
        "Id": 4,
        "Title": "Database Profiling with Visual Studio",
        "Content": "Examine when database queries were executed and measure how long the take using...",
        "BlogId": 2
      }
    ]
  }
]

Обратите внимание, что в JSON отсутствуют повторяющиеся блоги или записи. Это означает, что простые вызовы будут работать для Update обновления этих сущностей в базе данных:

public static void UpdateBlogsFromJson(string json)
{
    using var context = new BlogsContext();

    var blogs = JsonConvert.DeserializeObject<List<Blog>>(json);

    foreach (var blog in blogs)
    {
        context.Update(blog);
    }

    context.SaveChanges();
}

Обработка дубликатов

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

using var context = new BlogsContext();

var posts = context.Posts.Include(e => e.Blog).ToList();

var serialized = JsonConvert.SerializeObject(
    posts,
    new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, Formatting = Formatting.Indented });

Console.WriteLine(serialized);

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

[
  {
    "Id": 1,
    "Title": "Announcing the Release of EF Core 5.0",
    "Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
    "BlogId": 1,
    "Blog": {
      "Id": 1,
      "Name": ".NET Blog",
      "Summary": "Posts about .NET",
      "Posts": [
        {
          "Id": 2,
          "Title": "Announcing F# 5",
          "Content": "F# 5 is the latest version of F#, the functional programming language...",
          "BlogId": 1
        }
      ]
    }
  },
  {
    "Id": 2,
    "Title": "Announcing F# 5",
    "Content": "F# 5 is the latest version of F#, the functional programming language...",
    "BlogId": 1,
    "Blog": {
      "Id": 1,
      "Name": ".NET Blog",
      "Summary": "Posts about .NET",
      "Posts": [
        {
          "Id": 1,
          "Title": "Announcing the Release of EF Core 5.0",
          "Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
          "BlogId": 1
        }
      ]
    }
  },
  {
    "Id": 3,
    "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...",
    "BlogId": 2,
    "Blog": {
      "Id": 2,
      "Name": "Visual Studio Blog",
      "Summary": "Posts about Visual Studio",
      "Posts": [
        {
          "Id": 4,
          "Title": "Database Profiling with Visual Studio",
          "Content": "Examine when database queries were executed and measure how long the take using...",
          "BlogId": 2
        }
      ]
    }
  },
  {
    "Id": 4,
    "Title": "Database Profiling with Visual Studio",
    "Content": "Examine when database queries were executed and measure how long the take using...",
    "BlogId": 2,
    "Blog": {
      "Id": 2,
      "Name": "Visual Studio Blog",
      "Summary": "Posts about Visual Studio",
      "Posts": [
        {
          "Id": 3,
          "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...",
          "BlogId": 2
        }
      ]
    }
  }
]

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

System.InvalidOperationException: невозможно отслеживать экземпляр типа сущности Post, так как другой экземпляр с ключевым значением "{Id: 2}" уже отслеживается. При присоединении существующих сущностей убедитесь, что присоединен только один экземпляр сущности с заданным значением ключа.

Это можно исправить двумя способами:

  • Использование параметров сериализации JSON, которые сохраняют ссылки
  • Выполнение разрешения удостоверений во время отслеживания графа

Сохранение ссылок

Json.NET предоставляет PreserveReferencesHandling возможность обработки этого. Например:

var serialized = JsonConvert.SerializeObject(
    posts,
    new JsonSerializerSettings
    {
        PreserveReferencesHandling = PreserveReferencesHandling.All, Formatting = Formatting.Indented
    });

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

{
  "$id": "1",
  "$values": [
    {
      "$id": "2",
      "Id": 1,
      "Title": "Announcing the Release of EF Core 5.0",
      "Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
      "BlogId": 1,
      "Blog": {
        "$id": "3",
        "Id": 1,
        "Name": ".NET Blog",
        "Summary": "Posts about .NET",
        "Posts": [
          {
            "$ref": "2"
          },
          {
            "$id": "4",
            "Id": 2,
            "Title": "Announcing F# 5",
            "Content": "F# 5 is the latest version of F#, the functional programming language...",
            "BlogId": 1,
            "Blog": {
              "$ref": "3"
            }
          }
        ]
      }
    },
    {
      "$ref": "4"
    },
    {
      "$id": "5",
      "Id": 3,
      "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...",
      "BlogId": 2,
      "Blog": {
        "$id": "6",
        "Id": 2,
        "Name": "Visual Studio Blog",
        "Summary": "Posts about Visual Studio",
        "Posts": [
          {
            "$ref": "5"
          },
          {
            "$id": "7",
            "Id": 4,
            "Title": "Database Profiling with Visual Studio",
            "Content": "Examine when database queries were executed and measure how long the take using...",
            "BlogId": 2,
            "Blog": {
              "$ref": "6"
            }
          }
        ]
      }
    },
    {
      "$ref": "7"
    }
  ]
}

Обратите внимание, что этот КОД JSON заменил дубликаты ссылками, такими как "$ref": "5" ссылки на уже существующий экземпляр в графе. Этот граф можно снова отслеживать с помощью простых вызовов Update, как показано выше.

Поддержка System.Text.Json библиотек базовых классов .NET (BCL) имеет аналогичный параметр, который создает тот же результат. Например:

var serialized = JsonSerializer.Serialize(
    posts, new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, WriteIndented = true });

Разрешение дубликатов

Если невозможно исключить дубликаты в процессе сериализации, то ChangeTracker.TrackGraph предоставляет способ их обработки. TrackGraph работает так Add, Attach и Update за исключением того, что он создает обратный вызов для каждого экземпляра сущности перед отслеживанием. Этот обратный вызов можно использовать для отслеживания сущности или его пропуска. Например:

public static void UpdatePostsFromJsonWithIdentityResolution(string json)
{
    using var context = new BlogsContext();

    var posts = JsonConvert.DeserializeObject<List<Post>>(json);

    foreach (var post in posts)
    {
        context.ChangeTracker.TrackGraph(
            post, node =>
            {
                var keyValue = node.Entry.Property("Id").CurrentValue;
                var entityType = node.Entry.Metadata;

                var existingEntity = node.Entry.Context.ChangeTracker.Entries()
                    .FirstOrDefault(
                        e => Equals(e.Metadata, entityType)
                             && Equals(e.Property("Id").CurrentValue, keyValue));

                if (existingEntity == null)
                {
                    Console.WriteLine($"Tracking {entityType.DisplayName()} entity with key value {keyValue}");

                    node.Entry.State = EntityState.Modified;
                }
                else
                {
                    Console.WriteLine($"Discarding duplicate {entityType.DisplayName()} entity with key value {keyValue}");
                }
            });
    }

    context.SaveChanges();
}

Для каждой сущности в графе этот код:

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

Выходные данные выполнения этого кода:

Tracking EntityType: Post entity with key value 1
Tracking EntityType: Blog entity with key value 1
Tracking EntityType: Post entity with key value 2
Discarding duplicate EntityType: Post entity with key value 2
Tracking EntityType: Post entity with key value 3
Tracking EntityType: Blog entity with key value 2
Tracking EntityType: Post entity with key value 4
Discarding duplicate EntityType: Post entity with key value 4

Внимание

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

Примечание.

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

Не удалось задать значения ключей

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

public class Pet
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    public string Name { get; set; }
}

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

using var context = new BlogsContext();

context.Add(new Pet { Name = "Smokey" });

try
{
    context.Add(new Pet { Name = "Clippy" }); // This will throw
}
catch (Exception e)
{
    Console.WriteLine($"{e.GetType().FullName}: {e.Message}");
}

Этот код вызовет следующее:

System.InvalidOperationException: невозможно отслеживать экземпляр типа сущности Pet, так как другой экземпляр с ключевым значением "{Id: 0}" уже отслеживается. При присоединении существующих сущностей убедитесь, что присоединен только один экземпляр сущности с заданным значением ключа.

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

Перепользование одного экземпляра DbContext

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

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

Разрешение удостоверений и запросы

Разрешение удостоверений происходит автоматически, когда сущности отслеживаются из запроса. Это означает, что если экземпляр сущности с заданным значением ключа уже отслеживается, то вместо создания нового экземпляра используется существующий отслеживаемый экземпляр. Это имеет важное следствие: если данные изменились в базе данных, это не будет отражено в результатах запроса. Это хорошая причина для использования нового экземпляра DbContext для каждой единицы работы, как описано в dbContext Initialization и Configuration, и подробно описано в Отслеживание изменений в EF Core.

Внимание

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

Reload() или GetDatabaseValues() можно использовать при обновлении отслеживаемых сущностей с помощью последних данных из базы данных. Дополнительные сведения см. в разделе "Доступ к отслеживаемых сущностям ".

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

Совет

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

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

Запросы без отслеживания могут быть вынуждены выполнять разрешение удостоверений с помощью AsNoTrackingWithIdentityResolution<TEntity>(IQueryable<TEntity>). Затем запрос будет отслеживать возвращаемые экземпляры (без отслеживания их обычным образом) и гарантировать, что в результатах запроса не создаются дубликаты.

Переопределение равенства объектов

EF Core использует эталонное равенство при сравнении экземпляров сущностей. Это так, даже если типы сущностей переопределяют Object.Equals(Object) или изменяют равенство объектов. Однако есть одно место, где переопределение равенства может повлиять на поведение EF Core: когда навигации по коллекциям используют переопределенное равенство вместо ссылочного равенства, а следовательно, сообщать о нескольких экземплярах одинаково.

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

public sealed class ReferenceEqualityComparer : IEqualityComparer<object>
{
    private ReferenceEqualityComparer()
    {
    }

    public static ReferenceEqualityComparer Instance { get; } = new ReferenceEqualityComparer();

    bool IEqualityComparer<object>.Equals(object x, object y) => x == y;

    int IEqualityComparer<object>.GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
}

(Начиная с .NET 5, это включается в BCL как ReferenceEqualityComparer.)

Затем этот средство сравнения можно использовать при создании навигаций по коллекции. Например:

public ICollection<Order> Orders { get; set; }
    = new HashSet<Order>(ReferenceEqualityComparer.Instance);

Сравнение ключевых свойств

Помимо сравнения равенства, необходимо также упорядочить ключевые значения. Это важно для предотвращения взаимоблокировок при обновлении нескольких сущностей в одном вызове SaveChanges. Все типы, используемые для первичных, альтернативных или внешних ключевых свойств, а также тех, которые используются для уникальных индексов, должны реализовывать IComparable<T> и IEquatable<T>. Типы обычно используются в качестве ключей (int, Guid, string и т. д.) уже поддерживают эти интерфейсы. Пользовательские типы ключей могут добавлять эти интерфейсы.