Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Petunjuk / Saran
Konten ini adalah kutipan dari eBook, Arsitektur Layanan Mikro .NET untuk Aplikasi .NET Kontainer, tersedia di .NET Docs atau sebagai PDF gratis yang dapat diunduh yang dapat dibaca secara offline.
Seperti yang dibahas di bagian sebelumnya tentang entitas dan agregat, identitas sangat mendasar untuk entitas. Namun, ada banyak objek dan entri data dalam sistem yang tidak memerlukan identitas dan pelacakan identitas, seperti objek nilai.
Objek nilai dapat mereferensikan entitas lain. Misalnya, dalam aplikasi yang menghasilkan rute yang menjelaskan cara mendapatkan dari satu titik ke titik lain, rute tersebut akan menjadi objek nilai. Ini akan menjadi rekam jepret titik pada rute tertentu, tetapi rute yang disarankan ini tidak akan memiliki identitas, meskipun secara internal mungkin merujuk ke entitas seperti City, Road, dll.
Gambar 7-13 menunjukkan objek nilai Alamat dalam agregat Pesanan.
Gambar 7-13. Objek nilai alamat dalam agregat Pesanan
Seperti yang ditunjukkan pada Gambar 7-13, entitas biasanya terdiri dari beberapa atribut. Misalnya, Order entitas dapat dimodelkan sebagai entitas dengan identitas dan terdiri secara internal dari sekumpulan atribut seperti OrderId, OrderDate, OrderItems, dll. Tetapi alamat, yang hanya bernilai kompleks yang terdiri dari negara/wilayah, jalan, kota, dll., dan tidak memiliki identitas dalam domain ini, harus dimodelkan dan diperlakukan sebagai objek nilai.
Karakteristik penting objek nilai
Ada dua karakteristik utama untuk objek nilai:
Mereka tidak memiliki identitas.
Mereka tidak dapat diubah.
Karakteristik pertama sudah dibahas. Kekekalan adalah persyaratan penting. Setelah dibuat, nilai dari objek nilai harus bersifat tidak dapat diubah. Oleh karena itu, ketika objek dibangun, Anda harus memberikan nilai yang diperlukan, tetapi Anda tidak boleh mengizinkannya untuk berubah selama masa pakai objek.
Objek nilai memungkinkan Anda melakukan trik tertentu untuk performa, berkat sifatnya yang tidak dapat diubah. Ini terutama berlaku dalam sistem di mana mungkin ada ribuan instans objek nilai, banyak di antaranya memiliki nilai yang sama. Sifat mereka yang tidak dapat diubah memungkinkan mereka untuk digunakan kembali; mereka dapat menjadi objek yang dapat dipertukarkan, karena nilainya sama dan tidak memiliki identitas. Jenis pengoptimalan ini terkadang dapat membuat perbedaan antara perangkat lunak yang berjalan lambat dan perangkat lunak dengan performa yang baik. Tentu saja, semua kasus ini tergantung pada lingkungan aplikasi dan konteks penyebaran.
Implementasi objek nilai dalam C#
Dalam hal implementasi, Anda dapat memiliki kelas dasar objek nilai yang memiliki metode utilitas dasar seperti kesetaraan berdasarkan perbandingan antara semua atribut (karena objek nilai tidak boleh didasarkan pada identitas) dan karakteristik dasar lainnya. Contoh berikut menunjukkan kelas dasar objek nilai yang digunakan dalam pemesanan layanan mikro dari eShopOnContainers.
public abstract class ValueObject
{
protected static bool EqualOperator(ValueObject left, ValueObject right)
{
if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
{
return false;
}
return ReferenceEquals(left, right) || left.Equals(right);
}
protected static bool NotEqualOperator(ValueObject left, ValueObject right)
{
return !(EqualOperator(left, right));
}
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (ValueObject)obj;
return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
// Other utility methods
}
ValueObject adalah abstract class jenis, tetapi dalam contoh ini, itu tidak membebani == operator dan != . Anda dapat memilih untuk melakukannya, menyerahkan perbandingan pada fungsi override Equals. Misalnya, pertimbangkan overload operator berikut ke jenis ValueObject:
public static bool operator ==(ValueObject one, ValueObject two)
{
return EqualOperator(one, two);
}
public static bool operator !=(ValueObject one, ValueObject two)
{
return NotEqualOperator(one, two);
}
Anda dapat menggunakan kelas ini saat menerapkan objek nilai aktual Anda, seperti halnya Address objek nilai yang ditunjukkan dalam contoh berikut:
public class Address : ValueObject
{
public String Street { get; private set; }
public String City { get; private set; }
public String State { get; private set; }
public String Country { get; private set; }
public String ZipCode { get; private set; }
public Address() { }
public Address(string street, string city, string state, string country, string zipcode)
{
Street = street;
City = city;
State = state;
Country = country;
ZipCode = zipcode;
}
protected override IEnumerable<object> GetEqualityComponents()
{
// Using a yield return statement to return each element one at a time
yield return Street;
yield return City;
yield return State;
yield return Country;
yield return ZipCode;
}
}
Implementasi Address objek nilai ini tidak memiliki identitas, dan oleh karena itu tidak ada bidang ID yang didefinisikan untuk itu, baik dalam Address definisi kelas atau ValueObject definisi kelas.
Tidak memiliki bidang ID di kelas yang akan digunakan oleh Kerangka Kerja Entitas (EF) tidak dimungkinkan sampai EF Core 2.0, yang sangat membantu menerapkan objek nilai yang lebih baik tanpa ID. Itulah penjelasan yang tepat dari bagian berikutnya.
Dapat dikatakan bahwa objek nilai, yang bersifat tidak dapat diubah, seharusnya hanya memiliki properti baca, dan itu memang benar. Namun, objek nilai biasanya diserialisasikan dan dideserialisasi untuk melalui antrean pesan, dan menjadi baca-saja menghentikan deserializer dari menetapkan nilai, jadi Anda hanya membiarkannya sebagai private set, yang cukup baca-saja agar praktis.
Semantik perbandingan objek nilai
Dua instans jenis Address dapat dibandingkan menggunakan semua metode berikut:
var one = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");
var two = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");
Console.WriteLine(EqualityComparer<Address>.Default.Equals(one, two)); // True
Console.WriteLine(object.Equals(one, two)); // True
Console.WriteLine(one.Equals(two)); // True
Console.WriteLine(one == two); // True
Ketika semua nilai sama, perbandingan dievaluasi dengan benar sebagai true. Jika Anda tidak memilih untuk meng-overload operator == dan !=, maka perbandingan terakhir one == two akan dievaluasi sebagai false. Untuk informasi selengkapnya, lihat Overload operator kesetaraan ValueObject.
Cara mempertahankan objek nilai dalam database dengan EF Core 2.0 dan yang lebih baru
Anda baru saja melihat cara menentukan objek nilai dalam model domain Anda. Tetapi bagaimana Anda benar-benar dapat mempertahankannya ke dalam database menggunakan Entity Framework Core karena biasanya menargetkan entitas dengan identitas?
Pendekatan lama dan latar belakang menggunakan EF Core 1.1
Sebagai latar belakang, batasan saat menggunakan EF Core 1.0 dan 1.1 adalah Anda tidak dapat menggunakan jenis kompleks seperti yang didefinisikan dalam EF 6.x dalam .NET Framework tradisional. Oleh karena itu, jika menggunakan EF Core 1.0 atau 1.1, Anda perlu menyimpan objek nilai Anda sebagai entitas EF dengan bidang ID. Kemudian, sehingga terlihat lebih seperti objek nilai tanpa identitas, Anda dapat menyembunyikan ID-nya sehingga Anda memperjelas bahwa identitas objek nilai tidak penting dalam model domain. Anda dapat menyembunyikan ID tersebut dengan menggunakan ID sebagai properti bayangan. Karena konfigurasi tersebut untuk menyembunyikan ID dalam model disiapkan di tingkat infrastruktur EF, konfigurasi tersebut akan menjadi agak transparan untuk model domain Anda.
Dalam versi awal eShopOnContainers (.NET Core 1.1), ID tersembunyi yang diperlukan oleh infrastruktur EF Core diimplementasikan dengan cara berikut di tingkat DbContext, menggunakan Fluent API di proyek infrastruktur. Oleh karena itu, ID disembunyikan dari sudut pandang model domain, tetapi masih ada di infrastruktur.
// Old approach with EF Core 1.1
// Fluent API within the OrderingContext:DbContext in the Infrastructure project
void ConfigureAddress(EntityTypeBuilder<Address> addressConfiguration)
{
addressConfiguration.ToTable("address", DEFAULT_SCHEMA);
addressConfiguration.Property<int>("Id") // Id is a shadow property
.IsRequired();
addressConfiguration.HasKey("Id"); // Id is a shadow property
}
Namun, penyimpanan objek nilai tersebut ke dalam basis data dilakukan seperti entitas biasa di tabel yang berbeda.
Dengan EF Core 2.0 dan yang lebih baru, ada cara baru dan lebih baik untuk mempertahankan objek nilai.
Mempertahankan objek nilai sebagai jenis entitas yang dimiliki di EF Core 2.0 dan yang lebih baru
Meskipun terdapat beberapa celah antara pola objek nilai dalam kanon di DDD dan jenis entitas yang dimiliki di EF Core, saat ini merupakan cara terbaik untuk mempertahankan objek nilai dengan EF Core 2.0 dan yang lebih baru. Anda dapat melihat batasan di akhir bagian ini.
Fitur jenis entitas yang dimiliki ditambahkan ke EF Core sejak versi 2.0.
Jenis entitas yang dimiliki memungkinkan Anda memetakan jenis yang tidak memiliki identitas mereka sendiri yang secara eksplisit didefinisikan dalam model domain dan digunakan sebagai properti, seperti objek nilai, dalam entitas Anda. Jenis entitas yang dimiliki berbagi jenis CLR yang sama dengan jenis entitas lain (artinya, itu hanya kelas biasa). Entitas yang menentukan navigasi adalah entitas pemilik. Saat mengkueri pemilik, jenis yang dimiliki disertakan secara default.
Hanya dengan melihat model domain, jenis yang dimiliki terlihat seperti tidak memiliki identitas apa pun. Namun, di bawah sampul, jenis yang dimiliki memang memiliki identitas, tetapi properti navigasi pemilik adalah bagian dari identitas ini.
Identitas suatu instans dari tipe yang dimiliki tidak sepenuhnya menjadi milik mereka sendiri. Ini terdiri dari tiga komponen:
Identitas pemilik
Properti navigasi menunjuk ke properti tersebut
Dalam hal koleksi tipe yang dimiliki, terdapat komponen independen (didukung di EF Core 2.2 dan versi yang lebih baru).
Misalnya, dalam model domain Pemesanan di eShopOnContainers, sebagai bagian dari entitas Pesanan, objek Nilai alamat diimplementasikan sebagai jenis entitas yang dimiliki dalam entitas pemilik, yang merupakan entitas Pesanan.
Address adalah jenis tanpa properti identitas yang ditentukan dalam model domain. Ini digunakan sebagai properti dari jenis Pesanan untuk menentukan alamat pengiriman untuk pesanan tertentu.
Menurut konvensi, kunci utama bayangan dibuat untuk tipe yang dimiliki dan akan dipetakan ke tabel yang sama dengan pemilik dengan menggunakan pemisahan tabel. Ini memungkinkan untuk menggunakan jenis yang dimiliki mirip dengan bagaimana jenis kompleks digunakan dalam EF6 dalam .NET Framework tradisional.
Penting untuk dicatat bahwa tipe yang dimiliki tidak pernah ditemukan oleh konvensi di EF Core, jadi Anda harus menyatakannya secara eksplisit.
Di eShopOnContainers, dalam file OrderingContext.cs, dalam OnModelCreating() metode , beberapa konfigurasi infrastruktur diterapkan. Salah satunya terkait dengan entitas Pesanan.
// Part of the OrderingContext.cs class at the Ordering.Infrastructure project
//
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new ClientRequestEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new PaymentMethodEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new OrderItemEntityTypeConfiguration());
//...Additional type configurations
}
Dalam kode berikut, infrastruktur persistensi didefinisikan untuk entitas Pesanan:
// Part of the OrderEntityTypeConfiguration.cs class
//
public void Configure(EntityTypeBuilder<Order> orderConfiguration)
{
orderConfiguration.ToTable("orders", OrderingContext.DEFAULT_SCHEMA);
orderConfiguration.HasKey(o => o.Id);
orderConfiguration.Ignore(b => b.DomainEvents);
orderConfiguration.Property(o => o.Id)
.ForSqlServerUseSequenceHiLo("orderseq", OrderingContext.DEFAULT_SCHEMA);
//Address value object persisted as owned entity in EF Core 2.0
orderConfiguration.OwnsOne(o => o.Address);
orderConfiguration.Property<DateTime>("OrderDate").IsRequired();
//...Additional validations, constraints and code...
//...
}
Dalam kode sebelumnya, orderConfiguration.OwnsOne(o => o.Address) menentukan bahwa metode Address adalah entitas yang dimiliki dari jenis Order.
Secara default, konvensi EF Core memberi nama kolom database untuk properti jenis entitas yang dimiliki sebagai EntityProperty_OwnedEntityProperty. Oleh karena itu, properti Address internal akan muncul dalam Orders tabel dengan nama Address_Street, Address_City (dan sebagainya untuk State, , Countrydan ZipCode).
Anda dapat menambahkan Property().HasColumnName() metode fasih untuk mengganti nama kolom tersebut. Dalam kasus di mana Address adalah properti publik, pemetaannya akan seperti berikut ini:
orderConfiguration.OwnsOne(p => p.Address)
.Property(p=>p.Street).HasColumnName("ShippingStreet");
orderConfiguration.OwnsOne(p => p.Address)
.Property(p=>p.City).HasColumnName("ShippingCity");
Dimungkinkan untuk menautkan metode OwnsOne dalam pemetaan berantai. Dalam contoh hipotetis berikut, OrderDetails memiliki BillingAddress dan ShippingAddress, yang keduanya adalah jenis Address. Kemudian OrderDetails dimiliki oleh jenis Order.
orderConfiguration.OwnsOne(p => p.OrderDetails, cb =>
{
cb.OwnsOne(c => c.BillingAddress);
cb.OwnsOne(c => c.ShippingAddress);
});
//...
//...
public class Order
{
public int Id { get; set; }
public OrderDetails OrderDetails { get; set; }
}
public class OrderDetails
{
public Address BillingAddress { get; set; }
public Address ShippingAddress { get; set; }
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
}
Detail tambahan tentang jenis entitas yang dimiliki
Tipe yang dimiliki ditentukan saat Anda mengonfigurasi properti navigasi ke tipe tertentu menggunakan API Fluent OwnsOne.
Definisi jenis yang dimiliki dalam model metadata kami merupakan gabungan dari: jenis pemilik, properti navigasi, dan jenis CLR dari jenis tersebut yang dimiliki.
Identitas (kunci) dari instans tipe yang dimiliki dalam stack kami adalah gabungan dari identitas tipe pemilik dan definisi tipe yang dimiliki.
Kemampuan entitas yang dimiliki
Tipe yang dimiliki dapat mereferensikan entitas lain, baik yang dimiliki (tipe milik bersarang) atau tidak dimiliki (properti navigasi referensi reguler ke entitas lain).
Anda dapat memetakan jenis CLR yang sama sebagai jenis milik yang berbeda dalam entitas pemilik yang sama melalui properti navigasi terpisah.
Pemisahan tabel disiapkan menurut konvensi, tetapi Anda dapat mengubahnya dengan memetakan tipe yang dimiliki ke tabel lain menggunakan ToTable.
Pemuatan eager dilakukan secara otomatis pada kategori terikat, artinya tidak perlu memanggil
.Include()dalam kueri.Dapat dikonfigurasi dengan atribut
[Owned], menggunakan EF Core 2.1 dan yang lebih baru.Dapat menangani kumpulan jenis yang dimiliki (menggunakan versi 2.2 dan yang lebih baru).
Batasan entitas yang dimiliki
Anda tidak dapat membuat
DbSet<T>dari tipe yang sudah dimiliki (sesuai desain).Anda tidak dapat memanggil
ModelBuilder.Entity<T>()pada tipe yang dimiliki (ini memang disengaja).Tidak ada dukungan untuk jenis opsional (yaitu, dapat diubah ke null) yang dipetakan dengan pemilik dalam tabel yang sama (yaitu, menggunakan pemisahan tabel). Ini karena pemetaan dilakukan untuk setiap properti, tidak ada sentinel terpisah untuk nilai kompleks null secara keseluruhan.
Tidak ada dukungan pemetaan warisan untuk jenis yang dimiliki, tetapi Anda harus dapat memetakan dua jenis daun dari hierarki warisan yang sama dengan jenis yang dimiliki yang berbeda. EF Core tidak akan beralasan tentang fakta bahwa mereka adalah bagian dari hierarki yang sama.
Perbedaan utama dari tipe kompleks EF6
- Pemisahan tabel bersifat opsional, yaitu dapat dipetakan ke tabel terpisah dan tetap menjadi jenis milik.
Sumber daya tambahan
Martin Fowler. Pola ValueObject
https://martinfowler.com/bliki/ValueObject.htmlEric Evans. Domain-Driven Desain: Mengatasi Kompleksitas di Jantung Perangkat Lunak. (Buku; mencakup diskusi objek nilai)
https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/Vaughn Vernon. Menerapkan Desain Domain-Driven. (Buku; mencakup diskusi objek nilai)
https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577/Jenis Entitas Yang Dimiliki
https://learn.microsoft.com/ef/core/modeling/owned-entitiesAtribut Bayangan
https://learn.microsoft.com/ef/core/modeling/shadow-propertiesJenis kompleks dan/atau objek nilai. Diskusi di repositori GitHub Inti EF (tab Masalah)
https://github.com/dotnet/efcore/issues/246ValueObject.cs. Kelas objek nilai dasar di eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/SeedWork/ValueObject.csValueObject.cs. Kelas objek nilai dasar di CSharpFunctionalExtensions.
https://github.com/vkhorikov/CSharpFunctionalExtensions/blob/master/CSharpFunctionalExtensions/ValueObject/ValueObject.csKelas alamat. Contoh kelas objek nilai di eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs