Сложные типы

Объекты, сохраненные в базе данных, можно разделить на три широкие категории:

  • Объекты, неструктурированные и удерживающие одно значение. Например, int, Guid, string. IPAddress Это (несколько слабо) называется примитивными типами.
  • Объекты, структурированные для хранения нескольких значений, и где удостоверение объекта определяется значением ключа. Например Blog, Post, Customer. Они называются типами сущностей.
  • Объекты, структурированные для хранения нескольких значений, но у объекта нет ключа, определяющего его удостоверение. Например Address, Coordinate, Money. Эти объекты называются объектами значений, а EF Core сопоставляют их как сложные типы.

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

Подсказка

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

Note

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

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

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

Основные различия:

Аспект Типы зависимых сущностей Сложные типы
Identity Использование скрытого ключа и удостоверения Нет удостоверения; сравнивается по значению
Общий доступ к экземплярам Один и тот же экземпляр нельзя ссылаться дважды Один и тот же экземпляр можно назначить нескольким свойствам.
Семантика назначения Семантика ссылок Семантика значений (свойства копируются)
Тип .NET Только ссылочные типы Ссылочные или типы значений
Сопоставление таблиц Собственная таблица, разделение таблиц или JSON Таблица контейнера (разделение таблиц) или JSON
Navigations Может содержать навигации по другим сущностям Не удается содержать навигации
Массовое обновление (ExecuteUpdate) Не поддерживаются Поддерживается

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

var customer = await context.Customers.SingleAsync(c => c.Id == someId);
customer.BillingAddress = customer.ShippingAddress;
await context.SaveChangesAsync(); // Throws with owned entity types

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

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

Простой пример

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

public record Address
{
    public required string Line1 { get; init; }
    public string? Line2 { get; init; }
    public required string City { get; init; }
    public required string Country { get; init; }
    public required string PostCode { get; init; }
}

Address затем можно использовать в нескольких местах в модели клиентов и заказов:

public class Customer
{
    public int Id { get; set; }
    public required string Name { get; set; }

    // A required (non-nullable) complex property.
    public required Address Address { get; set; }

    // An optional (nullable) complex property.
    public Address? SecondaryAddress { get; set; }

    public List<Order> Orders { get; } = new();
}

public class Order
{
    public int Id { get; set; }
    public required string Contents { get; set; }
    public required Address ShippingAddress { get; set; }
    public required Address BillingAddress { get; set; }
    public Customer Customer { get; set; } = null!;
}

Создание и сохранение клиента работает как обычно:

var customer = new Customer
{
    Name = "Willow",
    Address = new Address
    {
        Line1 = "Barking Gate",
        City = "Walpole St Peter",
        Country = "UK",
        PostCode = "PE14 7AV"
    }
};

context.Add(customer);
await context.SaveChangesAsync();

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

INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);

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

// The same Address instance can be assigned to multiple complex properties.
customer.Orders.Add(
    new Order
    {
        Contents = "Tesco Tasty Treats",
        BillingAddress = customer.Address,
        ShippingAddress = customer.Address
    });

await context.SaveChangesAsync();

Настройка сложных типов

В отличие от большинства типов сущностей, сложные типы не обнаруживаются по соглашению. Их необходимо настроить явно либо путем аннотации типа с ComplexTypeAttributeпомощью или вызова ComplexProperty API Fluent в OnModelCreating каждом свойстве, которое должно быть сопоставлено как сложный тип:

[ComplexType]
public record Address
{
    public required string Line1 { get; init; }
    public string? Line2 { get; init; }
    public required string City { get; init; }
    public required string Country { get; init; }
    public required string PostCode { get; init; }
}

Настройка аспектов свойств сложного типа

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

modelBuilder.Entity<Order>()
    .ComplexProperty(
        o => o.ShippingAddress,
        b =>
        {
            b.Property(a => a.Line1).HasColumnName("ShipsToStreet").HasMaxLength(100);
            b.Property(a => a.City).HasColumnName("ShipsToCity");
        });

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

// EF Core 11 allows configuring a complex-type property directly by chaining
// member access, without first obtaining the complex-type builder.
modelBuilder.Entity<Customer>()
    .Property(c => c.Address.Line1)
    .HasMaxLength(200);

Типы ссылок и значений

Сложный тип может быть .NET ссылочным типом (a class илиrecord) или типом значения (a struct или record struct, представленным в EF Core 10).

Изменяемость

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

// Address is an immutable record, so create a new instance to change a value.
customer.Address = customer.Address with { Line1 = "Peacock Lodge" };

await context.SaveChangesAsync();

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

UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;

Неизменяемость может быть выражена с неизменяемыми (неизменяемыми или неизменяемыми class свойствами только для чтения), a, a recordreadonly structили areadonly record struct. Типы значений (struct) имеют семантику копирования, поэтому назначение их всегда копирует значения и избегает проблемы случайного совместного доступа, даже если изменяемые структуры , но изменяемые структуры обычно не рекомендуется в C#, поэтому неизменяемая форма по-прежнему рекомендуется.

Подсказка

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

Вложенные сложные типы

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

public record Address(string Line1, string? Line2, string City, string Country, string PostCode);

public record PhoneNumber(int CountryCode, long Number);

public record Contact
{
    public required Address Address { get; init; }
    public required PhoneNumber HomePhone { get; init; }
    public required PhoneNumber WorkPhone { get; init; }
}

При сопоставлении с разделением таблицы столбцы вложенного сложного типа префиксируются полным путем к свойству (например, Contact_HomePhone_Number).

Необязательные сложные типы

По умолчанию требуется сложное свойство: свойство CLR должно всегда иметь значение, и оно сопоставляется со столбцами, не допускаемыми значением NULL. Начиная с EF Core 10, сложное свойство можно сделать необязательным, объявив его как допускающий значение NULL:

public class Customer
{
    public int Id { get; set; }
    public required string Name { get; set; }

    // A required (non-nullable) complex property.
    public required Address Address { get; set; }

    // An optional (nullable) complex property.
    public Address? SecondaryAddress { get; set; }

    public List<Order> Orders { get; } = new();
}

public class Order
{
    public int Id { get; set; }
    public required string Contents { get; set; }
    public required Address ShippingAddress { get; set; }
    public required Address BillingAddress { get; set; }
    public Customer Customer { get; set; } = null!;
}

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

Note

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

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

// GeoLocation has no required property, so configure a discriminator. EF creates
// it as a required shadow property, which satisfies the requirement that an
// optional complex type have at least one required property.
modelBuilder.Entity<Place>()
    .ComplexProperty(p => p.Location, b => b.HasDiscriminator());

Коллекции сложных типов

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

public class Distributor
{
    public int Id { get; set; }
    public required string Name { get; set; }

    // A collection of complex types, mapped to a single JSON column.
    public List<Address> ShippingCenters { get; set; } = new();
}
// Collections of complex types must be mapped to JSON on relational providers.
modelBuilder.Entity<Distributor>()
    .ComplexCollection(d => d.ShippingCenters, b => b.ToJson());

Каждый элемент коллекции хранится в виде объекта JSON внутри массива, а вся коллекция сопоставляется с одним столбцом:

CREATE TABLE [Distributors] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    [ShippingCenters] json NOT NULL,
    CONSTRAINT [PK_Distributors] PRIMARY KEY ([Id])
);

Note

Коллекции типов значений (struct) в настоящее время не поддерживаются; используйте ссылочный тип (class или record) для сложных элементов коллекции.

Сопоставление сложных типов с JSON

Помимо разделения таблиц EF Core 10 позволяет сопоставлять сложное свойство (не коллекции) с одним столбцом JSON с ToJson:

modelBuilder.Entity<Customer>(b =>
{
    b.ComplexProperty(c => c.Address, c => c.ToJson());
    b.ComplexProperty(c => c.SecondaryAddress, c => c.ToJson());
});

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

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    [Address] json NOT NULL,
    [SecondaryAddress] json NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);

В SQL Server 2025 и Azure SQL EF использует собственный json тип данных по умолчанию; в других базах данных и более старых версиях SQL Server JSON хранится в текстовом столбце. При необходимости можно переопределить тип HasColumnType столбца.

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

Ключи и индексы для свойств сложного типа

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

// EF Core 11 allows keys and indexes to target scalar properties nested
// inside non-collection complex types.
modelBuilder.Entity<Customer>()
    .HasIndex(c => c.Address.PostCode);

Те же пути можно настроить по имени, используя . для перехода к сложному свойству:

modelBuilder.Entity<Customer>()
    .HasIndex("Address.PostCode");

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

// Index a scalar inside every element of a JSON-mapped complex collection.
// Requires a provider/database with JSON index support, such as SQL Server 2025.
modelBuilder.Entity<Distributor>()
    .HasIndex("ShippingCenters[].City");

Note

Для индексирования в сопоставленной с JSON сложной коллекции требуется база данных, поддерживающая индексы JSON, например SQL Server 2025.

Дополнительные сведения см. в разделе "Ключи " и " Индексы" и "Ограничения".

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

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

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Animal>()
        .UseTptMappingStrategy()
        .ComplexProperty(a => a.Details);
}

Отслеживание изменений

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

Используйте EntityEntry.ComplexProperty для достижения сложного свойства, а затем детализировать его скалярные свойства:

var addressEntry = context.Entry(customer).ComplexProperty(c => c.Address);
Console.WriteLine($"City is currently: {addressEntry.Property(a => a.City).CurrentValue}");
Console.WriteLine($"Address was modified: {addressEntry.Property(a => a.City).IsModified}");

ComplexPropertyEntry API зеркально отражает API сущностиEntityEntry: вы можете читать и задавать, проверять и задаватьCurrentValueIsModified, а также переходить к дополнительным вложенным сложным свойствам или сложным коллекциям. Сложные свойства также предоставляются через API значений свойств сущности (CurrentValues/OriginalValues).

Запросы сложных типов

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

// Filter and project members of a complex property.
var ukCities = await context.Customers
    .Where(c => c.Address.Country == "UK")
    .Select(c => c.Address.City)
    .ToListAsync();

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

var ordersToHomeAddress = await context.Orders
    .Where(o => o.ShippingAddress == o.BillingAddress)
    .ToListAsync();

Для сложных типов, сопоставленных с JSON, ef Core 11 добавляет EF.Functions.JsonPathExists, что проверяет, существует ли в документе заданный путь JSON:

var withPostCode = await context.Customers
    .Where(c => EF.Functions.JsonPathExists(c.Address, "$.PostCode"))
    .ToListAsync();

Limitations

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

  • Нет удостоверений или отслеживания своих собственных. Сложный тип может существовать только как часть сущности; У вас не может быть DbSet<T> сложный тип, а также не отслеживать или запрашивать его независимо.
  • Навигации отсутствуют. Сложный тип не может содержать свойства навигации для типов сущностей.
  • Нет отдельной таблицы. В реляционных базах данных сложный тип всегда хранится в таблице контейнера (с помощью разделения таблиц) или в столбце JSON — никогда не в собственной таблице.
  • Для коллекций требуется JSON. При реляционных поставщиках сложные коллекции должны быть сопоставлены с ToJsonJSON; их нельзя сопоставить с помощью разделения таблиц.
  • Коллекции типов значений не поддерживаются. Сложные элементы коллекции должны быть ссылочными типами.
  • Необязательные сложные типы требуют обязательного свойства. Необязательный сложный тип (nullable) должен определять по крайней мере одно обязательное свойство.

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