Tipe Kompleks

Objek yang disimpan ke database dapat dibagi menjadi tiga kategori luas:

  • Objek yang tidak terstruktur dan menyimpan satu nilai. Misalnya, int, Guid, string, IPAddress. Ini (agak longgar) yang disebut jenis primitif.
  • Objek yang disusun untuk menyimpan beberapa nilai, dan di mana identitas objek ditentukan oleh nilai kunci. Misalnya, Blog, Post, Customer. Ini disebut jenis entitas.
  • Objek yang disusun untuk menyimpan beberapa nilai, tetapi objek tidak memiliki kunci yang menentukan identitasnya. Misalnya, Address, Coordinate, Money. Ini disebut objek nilai, dan EF Core memetakannya sebagai jenis kompleks.

Jenis kompleks mengelompokkan beberapa properti ke dalam satu jenis .NET yang terkandung dalam jenis entitas; tidak memiliki identitasnya sendiri dan tidak dapat dilacak atau dikueri secara independen. Ini membuat jenis kompleks sebagai cara alami untuk memodelkan objek nilai.

Tip

Anda dapat menjalankan dan men-debug ke dalam proyek sampel lengkap untuk artikel ini di GitHub.

Note

Jenis kompleks diperkenalkan di EF Core 8, dan telah diperluas secara substansial dalam rilis selanjutnya. Fitur diannotasikan di bawah ini dengan versi yang memperkenalkannya.

Jenis kompleks vs. jenis entitas yang dimiliki

Sebelum jenis kompleks ada, jenis entitas yang dimiliki adalah cara yang disarankan untuk memodelkan objek tanpa properti kunci. Namun, jenis yang dimiliki masih merupakan jenis entitas di belakang layar: mereka memiliki kunci dan identitas tersembunyi, dan oleh karena itu beroperasi dengan semantik referensi. Hal ini menyebabkan sejumlah titik gesekan yang dirancang untuk dipecahkan oleh jenis kompleks.

Perbedaan utamanya adalah:

Aspek Jenis entitas yang dimiliki Jenis kompleks
Identity Memiliki kunci dan identitas tersembunyi Tidak ada identitas; dibandingkan dengan nilai
Berbagi instans Instans yang sama tidak dapat dirujuk dua kali Instans yang sama dapat ditetapkan ke beberapa properti
Semantik penugasan Semantik referensi Semantik nilai (properti disalin)
Jenis .NET Jenis referensi saja Jenis referensi atau nilai
Pemetaan tabel Tabel sendiri, pemisahan tabel, atau JSON Tabel kontainer (pemisahan tabel) atau JSON
Navigations Dapat berisi navigasi ke entitas lain Tidak dapat memuat navigasi
Pembaruan massal (ExecuteUpdate) Tidak didukung Dukungan

Misalnya, menetapkan alamat penagihan pelanggan agar sama dengan alamat pengiriman mereka gagal dengan jenis entitas yang dimiliki, karena instans entitas yang sama tidak dapat direferensikan lebih dari sekali:

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

Karena jenis kompleks memiliki semantik nilai, penugasan yang sama hanya menyalin properti, dan berfungsi seperti yang diharapkan. Demikian pula, membandingkan dua nilai kompleks dalam kueri LINQ membandingkan kontennya, sedangkan membandingkan dua entitas yang dimiliki membandingkan identitas mereka.

Untuk alasan ini, jenis kompleks umumnya adalah pilihan yang lebih baik untuk memodelkan objek nilai dengan pemisahan tabel atau pemetaan JSON. Pengguna yang saat ini menggunakan jenis entitas yang dimiliki untuk skenario ini didorong untuk mempertimbangkan untuk beralih ke jenis kompleks.

Contoh sederhana

Address Pertimbangkan jenis yang menyimpan beberapa nilai terkait tetapi tidak memiliki identitasnya sendiri:

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 kemudian dapat digunakan di beberapa tempat di seluruh model pelanggan/pesanan:

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

Membuat dan menyimpan pelanggan berfungsi seperti biasa:

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

Pada database relasional, jenis kompleks tidak mendapatkan tabelnya sendiri. Sebagai gantinya, propertinya disimpan sebaris sebagai kolom tambahan pada tabel entitas yang berisi (ini dikenal sebagai pemisahan tabel):

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

Karena jenis kompleks memiliki semantik nilai, instans yang sama Address dapat dibagikan di beberapa properti tanpa masalah:

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

Mengonfigurasi jenis kompleks

Tidak seperti kebanyakan jenis entitas, jenis kompleks tidak ditemukan berdasarkan konvensi. Anda harus mengonfigurasinya secara eksplisit, baik dengan menganotasi jenis dengan ComplexTypeAttribute, atau dengan memanggil ComplexProperty Api Fasih untuk setiap properti yang harus dipetakan OnModelCreating sebagai jenis kompleks:

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

Mengonfigurasi faset properti jenis kompleks

Penyusun berlapis Property dapat digunakan untuk mengonfigurasi properti skalar dari jenis kompleks, sama seperti properti jenis entitas - misalnya, untuk mengatur nama kolom atau panjang maksimum:

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

Dimulai dengan EF Core 11, Anda dapat mengonfigurasi properti yang bersarang di dalam jenis kompleks secara langsung dengan menautkan akses anggota di lambda, tanpa terlebih dahulu mendapatkan pembuat jenis kompleks:

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

Jenis referensi dan nilai

Jenis kompleks dapat berupa jenis referensi .NET (atau classrecord) atau jenis nilai (structatau record struct, yang diperkenalkan dalam EF Core 10).

Mutabilitas

Karena instans jenis referensi dapat dibagikan oleh beberapa properti, bermutasi salah satu propertinya mengubah nilai di mana pun instans digunakan. Ini biasanya tidak apa yang Anda inginkan. Cara yang baik untuk menghindarinya - dan kecocokan alami untuk objek nilai - adalah dengan membuat jenis kompleks tidak dapat diubah, sehingga mengubah nilai memerlukan pembuatan instans baru. Jenis Address yang digunakan di seluruh artikel ini tidak dapat recorddiubah ; mengubah alamat oleh karena itu dilakukan dengan with ekspresi:

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

Meskipun seluruh instans baru Address ditetapkan, EF masih melacak perubahan di tingkat properti individual, jadi hanya kolom yang nilainya benar-benar diubah diperbarui:

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

Kekekalan dapat diekspresikan dengan properti yang tidak dapat class diubah (hanya init atau baca-saja), record, , readonly structatau readonly record struct. Jenis nilai (struct) memiliki semantik salinan, jadi menetapkannya selalu menyalin nilai dan menghindari masalah berbagi yang tidak disengaja, bahkan ketika dapat diubah - tetapi struktur yang dapat diubah umumnya tidak disarankan dalam C#, sehingga bentuk yang tidak dapat diubah masih direkomendasikan.

Tip

Jika beberapa entitas benar-benar harus mengamati alamat yang sama dan memperbarui bersama-sama ketika berubah, maka model alamat sebagai jenis entitas dengan identitasnya sendiri dan mereferensikannya melalui navigasi, daripada menggunakan jenis kompleks.

Jenis kompleks berlapis

Jenis kompleks dapat berisi properti dari jenis kompleks lainnya, memungkinkan Anda untuk membangun objek terstruktur ke kedalaman apa pun. Misalnya, Contact jenis kompleks mungkin berisi satu Address dan satu atau beberapa PhoneNumber jenis kompleks:

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

Saat dipetakan melalui pemisahan tabel, kolom jenis kompleks berlapis diawali dengan jalur lengkap ke properti (misalnya, Contact_HomePhone_Number).

Jenis kompleks opsional

Secara default, properti kompleks diperlukan: properti CLR harus selalu memiliki nilai, dan memetakan ke kolom yang tidak dapat diubah ke null. Dimulai dengan EF Core 10, properti kompleks dapat dibuat opsional dengan mendeklarasikannya sebagai nullable:

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

Properti kompleks opsional yang null menghasilkan NULL nilai di semua kolomnya.

Note

Jenis kompleks opsional saat ini memerlukan setidaknya satu properti yang diperlukan untuk didefinisikan pada jenis kompleks. Ini karena EF membutuhkan setidaknya satu kolom yang tidak dapat diubah ke null untuk membedakan null nilai kompleks dari nilai kompleks yang propertinya semuanya adalah null.

Jika jenis kompleks tidak memiliki properti yang diperlukan sendiri, Anda dapat mengonfigurasi properti diskriminator. Meskipun EF Core belum mendukung pewarisan untuk jenis kompleks, diskriminator dibuat sebagai properti bayangan yang diperlukan secara default, yang memenuhi persyaratan di atas:

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

Kumpulan jenis kompleks

Dimulai dengan EF Core 10, properti dapat menyimpan koleksi jenis kompleks. Pada database relasional, koleksi kompleks harus dipetakan ke satu kolom JSON menggunakan ToJson - tidak dapat dipetakan ke tabel lain:

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

Setiap elemen koleksi disimpan sebagai objek JSON di dalam array, dan seluruh koleksi memetakan ke satu kolom:

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

Note

Kumpulan jenis nilai (struct) saat ini tidak didukung; gunakan jenis referensi (class atau record) untuk elemen koleksi kompleks.

Memetakan jenis kompleks ke JSON

Selain pemisahan tabel, EF Core 10 memungkinkan pemetaan properti kompleks (non-koleksi) ke satu kolom JSON dengan ToJson:

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

Setiap nilai kompleks kemudian diserialisasikan ke dalam satu kolom JSON daripada tersebar di beberapa kolom:

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

Pada SQL Server 2025 dan Azure SQL, EF menggunakan jenis data aslijson secara default; pada database lain dan versi SQL Server yang lebih lama, JSON disimpan dalam kolom teks. Anda dapat mengambil alih jenis kolom dengan HasColumnType jika diperlukan.

Tidak seperti pemisahan tabel, pemetaan JSON memungkinkan koleksi dalam jenis yang dipetakan , dan memungkinkan Anda mengkueri dan memperbarui properti individual di dalam dokumen sama seperti properti lainnya. Nilai di dalam kolom JSON juga dapat diperbarui secara efisien secara massal dengan ExecuteUpdateAsync.

Kunci dan indeks pada properti jenis kompleks

Dimulai dengan EF Core 11, kunci dan indeks dapat menargetkan properti skalar yang bersarang di dalam jenis kompleks non-koleksi. Ini dapat dilakukan dengan lambda:

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

Jalur yang sama dapat dikonfigurasi berdasarkan nama, menggunakan . untuk menavigasi ke properti kompleks:

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

Untuk penyedia relasional, indeks juga dapat menargetkan jalur di dalam tipe kompleks yang dipetakan ke dalam kolom JSON. Jalur koleksi kompleks digunakan [] untuk merujuk ke semua elemen, atau pengindeks numerik untuk elemen tertentu:

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

Pengindeksan ke dalam koleksi kompleks yang dipetakan JSON memerlukan database yang mendukung indeks JSON, seperti SQL Server 2025.

Untuk informasi selengkapnya, lihat Kunci dan Indeks dan batasan.

Jenis kompleks dengan pewarisan entitas

Dimulai dengan EF Core 11, jenis kompleks dan kolom JSON dapat digunakan pada jenis entitas yang menggunakan TPT (table-per-type) atau TPC (table-per-concrete-type). Ini memungkinkan Anda menggabungkan fleksibilitas strategi warisan ini dengan kekuatan pemodelan jenis kompleks.

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

Pelacakan perubahan

EF Core melacak perubahan pada properti individual dari jenis kompleks, sehingga hanya kolom yang terpengaruh yang diperbarui saat Anda memanggil SaveChanges. Anda dapat memeriksa dan memanipulasi status pelacakan ini melalui pelacak perubahan.

Gunakan EntityEntry.ComplexProperty untuk menjangkau properti kompleks, lalu telusuri properti skalarnya:

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 mencerminkan API entitasEntityEntry: Anda dapat membaca dan mengatur CurrentValue, memeriksa dan mengatur IsModified, dan menavigasi ke properti kompleks berlapis lebih lanjut atau koleksi kompleks. Properti kompleks juga diekspos melalui API nilai properti entitas (CurrentValues/OriginalValues).

Mengkueri tipe kompleks

Anggota jenis kompleks dapat digunakan dalam kueri LINQ seperti properti entitas itu sendiri - Anda dapat memfilternya, memproyeksikannya, dan mengurutkannya:

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

Karena jenis kompleks memiliki semantik nilai, Anda juga dapat membandingkan seluruh nilai kompleks dalam kueri, dan EF akan membandingkan semua propertinya:

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

Untuk jenis kompleks yang dipetakan ke JSON, EF Core 11 menambahkan EF.Functions.JsonPathExists, yang memeriksa apakah jalur JSON tertentu ada dalam dokumen:

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

Limitations

Jenis kompleks dirancang untuk memodelkan objek nilai, dan sengaja tidak mendukung setiap kemampuan jenis entitas. Batasan utamanya adalah:

  • Tidak ada identitas atau pelacakan mereka sendiri. Jenis kompleks hanya dapat ada sebagai bagian dari entitas; Anda tidak dapat memiliki DbSet<T> jenis kompleks, atau melacak atau mengkuerinya secara independen.
  • Tidak ada navigasi. Jenis kompleks tidak boleh berisi properti navigasi ke jenis entitas.
  • Tidak ada tabel terpisah. Pada database relasional, jenis kompleks selalu disimpan dalam tabel kontainernya (melalui pemisahan tabel) atau di kolom JSON - tidak pernah dalam tabelnya sendiri.
  • Koleksi memerlukan JSON. Pada penyedia relasional, koleksi kompleks harus dipetakan ke JSON dengan ToJson; mereka tidak dapat dipetakan melalui pemisahan tabel.
  • Kumpulan jenis nilai tidak didukung. Elemen koleksi kompleks harus berupa jenis referensi.
  • Jenis kompleks opsional memerlukan properti yang diperlukan. Jenis kompleks opsional (nullable) harus menentukan setidaknya satu properti yang diperlukan.

Dukungan jenis kompleks terus diperluas di seluruh rilis; lihat halaman apa yang baru untuk penambahan terbaru.