Jika Anda suka atau menggunakan proyek ini, silakan berikan bintang. Terima kasih!
Vogen ( diucapkan "VoH Jen" ) adalah generator dan penganalisa sumber. Ini mengubah primitif Anda (ints, desimal dll.) Menjadi objek nilai yang mewakili konsep domain (customerid, accountghanance dll.)
Ini menambahkan kesalahan kompilasi C# baru untuk membantu menghentikan pembuatan objek nilai yang tidak valid.
Readme ini mencakup beberapa fungsionalitas. Silakan lihat wiki untuk informasi yang lebih rinci, seperti memulai, tutorial, dan bagaimana-untuk.
Generator sumber menghasilkan konsep domain yang sangat diketik. Anda memberikan ini:
[ ValueObject < int > ]
public partial struct CustomerId ;... dan vogen menghasilkan sumber yang mirip dengan ini:
public partial struct CustomerId :
System . IEquatable < CustomerId > ,
System . IComparable < CustomerId > , System . IComparable
{
private readonly int _value ;
public readonly int Value => _value ;
public CustomerId ( ) {
throw new Vogen . ValueObjectValidationException (
"Validation skipped by attempting to use the default constructor..." ) ;
}
private CustomerId ( int value ) => _value = value ;
public static CustomerId From ( int value ) {
CustomerId instance = new CustomerId ( value ) ;
return instance ;
}
public readonly bool Equals ( CustomerId other ) .. .
public readonly bool Equals ( int primitive ) .. .
public readonly override bool Equals ( object obj ) .. .
public static bool operator == ( CustomerId left , CustomerId right ) .. .
public static bool operator != ( CustomerId left , CustomerId right ) .. .
public static bool operator == ( CustomerId left , int right ) .. .
public static bool operator != ( CustomerId left , int right ) .. .
public static bool operator == ( int left , CustomerId right ) .. .
public static bool operator != ( int left , CustomerId right ) .. .
public readonly override int GetHashCode ( ) .. .
public readonly override string ToString ( ) .. .
} Anda kemudian menggunakan CustomerId alih -alih int di domain Anda dengan pengetahuan penuh bahwa itu valid dan aman untuk digunakan:
CustomerId customerId = CustomerId . From ( 123 ) ;
SendInvoice ( customerId ) ;
.. .
public void SendInvoice ( CustomerId customerId ) { .. . } int adalah tipe default untuk objek nilai. Secara umum merupakan ide yang baik untuk secara eksplisit menyatakan setiap jenis untuk kejelasan. Anda juga dapat - secara individu atau global - mengkonfigurasinya menjadi tipe lain. Lihat bagian Konfigurasi nanti di dokumen.
Inilah beberapa contoh lainnya:
[ ValueObject < decimal > ]
public partial struct AccountBalance ;
[ ValueObject < string > ]
public partial class LegalEntityName ;Tujuan utama vogen adalah untuk memastikan validitas objek nilai Anda , penganalisa kode membantu Anda untuk menghindari kesalahan yang mungkin membuat Anda dengan objek nilai yang tidak diwujudkan dalam domain Anda.
Ini melakukan ini dengan menambahkan kendala baru dalam bentuk kesalahan kompilasi C# baru . Ada beberapa cara Anda bisa berakhir dengan objek nilai yang tidak diinisialisasi. Salah satu caranya adalah dengan memberikan konstruktor tipe Anda. Memberikan konstruktor Anda sendiri dapat berarti bahwa Anda lupa untuk menetapkan nilai, jadi Vogen tidak memungkinkan Anda memiliki konstruktor yang ditentukan pengguna :
[ ValueObject ]
public partial struct CustomerId
{
// Vogen deliberately generates this so that you can't create your own:
// error CS0111: Type 'CustomerId' already defines a member called 'CustomerId'
// with the same parameter type
public CustomerId ( ) { }
// error VOG008: Cannot have user defined constructors,
// please use the From method for creation.
public CustomerId ( int value ) { }
}Selain itu, vogen akan menemukan masalah saat membuat atau mengonsumsi objek nilai:
// catches object creation expressions
var c = new CustomerId ( ) ; // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited
CustomerId c = default ; // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.
var c = default ( CustomerId ) ; // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.
var c = GetCustomerId ( ) ; // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited
var c = Activator . CreateInstance < CustomerId > ( ) ; // error VOG025: Type 'CustomerId' cannot be constructed via Reflection as it is prohibited.
var c = Activator . CreateInstance ( < CustomerId > ) ; // error VOG025: Type 'MyVo' cannot be constructed via Reflection as
it is prohibited
// catches lambda expressions
Func < CustomerId > f = ( ) => default ; // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.
// catches method / local function return expressions
CustomerId GetCustomerId ( ) => default ; // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.
CustomerId GetCustomerId ( ) => new CustomerId ( ) ; // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited
CustomerId GetCustomerId ( ) => new ( ) ; // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited
// catches argument / parameter expressions
Task < CustomerId > t = Task . FromResult < CustomerId > ( new ( ) ) ; // error VOG010: Type 'CustomerId' cannot be constructed with 'new' as it is prohibited
void Process ( CustomerId customerId = default ) { } // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited. Salah satu tujuan utama dari proyek ini adalah untuk mencapai kecepatan dan kinerja memori yang hampir sama dengan menggunakan primitif secara langsung . Dengan kata lain, jika primitif decimal Anda mewakili saldo akun, maka ada overhead yang sangat rendah untuk menggunakan objek Nilai AccountBalance sebagai gantinya. Silakan lihat metrik kinerja di bawah ini.
Vogen adalah paket Nuget. Instal dengan:
dotnet add package Vogen
Saat ditambahkan ke proyek Anda, generator sumber menghasilkan pembungkus untuk primitif Anda dan penganalisa kode akan memberi tahu Anda jika Anda mencoba membuat objek nilai yang tidak valid.
Pikirkan tentang konsep domain Anda dan bagaimana Anda menggunakan primitif untuk mewakili mereka, misalnya bukannya:
public void HandlePayment ( int customerId , int accountId , decimal paymentAmount )... punya ini:
public void HandlePayment ( CustomerId customerId , AccountId accountId , PaymentAmount paymentAmount )Sederhana membuat tipe seperti ini:
[ ValueObject ]
public partial struct CustomerId ;
[ ValueObject ]
public partial struct AccountId ;
[ ValueObject < decimal > ]
public partial struct PaymentAmount ; Generator sumber menghasilkan objek nilai. Objek nilai membantu memerangi obsesi primitif dengan membungkus primitif sederhana seperti int , string , double dll dalam tipe yang sangat diketik.
Obsesi primitif (alias stringtyped) berarti terobsesi dengan primitif. Ini adalah bau kode yang menurunkan kualitas perangkat lunak.
" Obsesi primitif menggunakan tipe data primitif untuk mewakili ide domain " #
Beberapa contoh:
int age - kita akan memiliki Age age . Age mungkin memiliki validasi bahwa itu tidak bisa negatifstring postcode - kami memiliki Postcode postcode . Postcode mungkin memiliki validasi pada format teksGenerator Sumber Dipendek. Pendapat membantu memastikan konsistensi. Pendapatnya adalah:
From , misalnya Age.From(12)Age.From(12) == Age.From(12) )Validate yang mengembalikan hasil ValidationValidation.Ok ValueObjectValidationException Adalah umum untuk mewakili ide -ide domain sebagai primitif, tetapi primitif mungkin tidak dapat sepenuhnya menggambarkan ide domain.
Untuk menggunakan objek nilai alih -alih primitif, kami hanya bertukar kode seperti ini:
public class CustomerInfo {
private int _id ;
public CustomerInfo ( int id ) => _id = id ;
}.. untuk ini:
public class CustomerInfo {
private CustomerId _id ;
public CustomerInfo ( CustomerId id ) => _id = id ;
} Ada posting blog di sini yang menggambarkannya, tetapi untuk meringkas:
Obsesi primitif terobsesi dengan cara yang tampaknya nyaman sehingga primitif, seperti
intsdanstrings, memungkinkan kita untuk mewakili objek dan ide domain.
Ini adalah ini :
int customerId = 42Ada apa dengan itu?
ID pelanggan kemungkinan tidak dapat sepenuhnya diwakili oleh int . int bisa negatif atau nol, tetapi tidak mungkin ID pelanggan bisa. Jadi, kami memiliki kendala pada ID pelanggan. Kami tidak dapat mewakili atau menegakkan kendala tersebut pada int .
Jadi, kami perlu validasi untuk memastikan kendala ID pelanggan terpenuhi. Karena itu ada di int , kami tidak dapat memastikan apakah itu telah diperiksa sebelumnya, jadi kami perlu memeriksanya setiap kali kami menggunakannya. Karena ini primitif, seseorang mungkin telah mengubah nilainya, jadi bahkan jika kita 100% yakin kita sudah memeriksanya sebelumnya, itu masih perlu memeriksa lagi.
Sejauh ini, kami telah menggunakan sebagai contoh, ID pelanggan nilai 42 . Dalam C#, mungkin tidak mengherankan bahwa " 42 == 42 " ( saya belum memeriksanya di JavaScript! ). Tapi, di domain kita, haruskah 42 selalu sama dengan 42 ? Mungkin tidak jika Anda membandingkan ID pemasok 42 dengan ID pelanggan 42 ! Tapi primitif tidak akan membantu Anda di sini (ingat, 42 == 42 !).
( 42 == 42 ) // true
( SuppliedId . From ( 42 ) == SupplierId . From ( 42 ) ) // true
( SuppliedId . From ( 42 ) == VendorId . From ( 42 ) ) // compilation error Tetapi kadang -kadang, kita perlu menunjukkan bahwa objek nilai tidak valid atau belum ditetapkan. Kami tidak ingin siapa pun di luar objek melakukan ini karena dapat digunakan secara tidak sengaja. Adalah umum untuk memiliki contoh Unspecified , misalnya
public class Person {
public Age Age { get ; } = Age . Unspecified ;
} Kita dapat melakukannya dengan atribut Instance :
[ ValueObject ]
[ Instance ( "Unspecified" , - 1 ) ]
public readonly partial struct Age {
public static Validation Validate ( int value ) =>
value > 0 ? Validation . Ok : Validation . Invalid ( "Must be greater than zero." ) ;
} Ini menghasilkan public static Age Unspecified = new Age(-1); . Konstruktor bersifat private , jadi hanya jenis ini yang dapat (sengaja) membuat instance yang tidak valid .
Sekarang, ketika kita menggunakan Age , validasi kita menjadi lebih jelas:
public void Process ( Person person ) {
if ( person . Age == Age . Unspecified ) {
// age not specified.
}
}Kami juga dapat menentukan properti instance lainnya:
[ ValueObject < float > ]
[ Instance ( "Freezing" , 0 ) ]
[ Instance ( "Boiling" , 100 ) ]
public readonly partial struct Celsius {
public static Validation Validate ( float value ) =>
value >= - 273 ? Validation . Ok : Validation . Invalid ( "Cannot be colder than absolute zero" ) ;
} Setiap objek nilai dapat memiliki konfigurasi opsional sendiri. Konfigurasi termasuk:
Jika salah satu dari mereka di atas tidak ditentukan, maka konfigurasi global disimpulkan. Sepertinya ini:
[ assembly : VogenDefaults (
underlyingType : typeof ( int ) ,
conversions : Conversions . Default ,
throws : typeof ( ValueObjectValidationException ) ) ]Itu lagi opsional. Jika mereka tidak ditentukan, maka mereka default untuk:
typeof(int)Conversions.Default ( TypeConverter dan System.Text.Json )typeof(ValueObjectValidationException)Ada beberapa peringatan analisis kode untuk konfigurasi yang tidak valid, termasuk:
System.Exception (Untuk menjalankan ini sendiri: dotnet run -c Release --framework net9.0 -- --job short --filter * di folder Vogen.Benchmarks )
Seperti yang disebutkan sebelumnya, tujuan vogen adalah untuk mencapai kinerja yang sangat mirip dibandingkan dengan menggunakan primitif itu sendiri. Berikut adalah tolok ukur yang membandingkan penggunaan objek nilai yang divalidasi dengan jenis int vs yang mendasarinya menggunakan yang secara national ( secara primitif ?)
BenchmarkDotNet =v0.13.2, OS =Windows 11 (10.0.22621.1194)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK =7.0.102
[Host] : .NET 7.0.2 (7.0.222.60605), X64 RyuJIT AVX2
ShortRun : .NET 7.0.2 (7.0.222.60605), X64 RyuJIT AVX2
Job =ShortRun IterationCount =3 LaunchCount =1
WarmupCount =3 | Metode | Berarti | Kesalahan | Stddev | Perbandingan | RasioSd | Gen0 | Dialokasikan |
|---|---|---|---|---|---|---|---|
| Menggunakan secara intistif | 14.55 ns | 1.443 ns | 0,079 ns | 1.00 | 0,00 | - | - |
| Menggunakan ValueObjectStruct | 14.88 ns | 3.639 ns | 0,199 ns | 1.02 | 0,02 | - | - |
Tidak ada perbedaan yang dapat dilihat antara menggunakan Int asli dan struct VO; Keduanya hampir sama dalam hal kecepatan dan memori.
Skenario paling umum berikutnya adalah menggunakan kelas VO untuk mewakili String asli. Hasil ini adalah:
BenchmarkDotNet =v0.13.2, OS =Windows 11 (10.0.22621.1194)
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK =7.0.102
[Host] : .NET 7.0.2 (7.0.222.60605), X64 RyuJIT AVX2
ShortRun : .NET 7.0.2 (7.0.222.60605), X64 RyuJIT AVX2
Job =ShortRun IterationCount =3 LaunchCount =1
WarmupCount =3 | Metode | Berarti | Kesalahan | Stddev | Perbandingan | RasioSd | Gen0 | Dialokasikan | Rasio Alloc |
|---|---|---|---|---|---|---|---|---|
| Usingstringnative | 151.8 ns | 32.19 | 1.76 | 1.00 | 0,00 | 0,0153 | 256 b | 1.00 |
| Menggunakan ValueObjectStruct | 184.8 ns | 12.19 | 0.67 | 1.22 | 0,02 | 0,0153 | 256 b | 1.00 |
Ada sejumlah kecil overhead kinerja, tetapi pengukuran ini sangat kecil. Tidak ada overhead memori.
Secara default, masing -masing VO dihiasi dengan serializer TypeConverter dan System.Text.Json (STJ). Ada konverter/serializer lain untuk:
Mereka dikendalikan oleh Conversions enum. Berikut ini memiliki serializer untuk NSJ dan STJ:
[ ValueObject < float > ( conversions : Conversions . NewtonsoftJson | Conversions . SystemTextJson ) ]
public readonly partial struct Celsius ; Jika Anda tidak ingin konversi, maka tentukan Conversions.None .
Jika Anda menginginkan konversi Anda sendiri, sekali lagi tentukan tidak ada, dan terapkan sendiri, sama seperti jenis lainnya. Tetapi ketahuilah bahwa bahkan serializer akan mendapatkan kesalahan kompilasi yang sama untuk new dan default saat mencoba membuat VOS.
Jika Anda ingin menggunakan necis, ingatlah untuk mendaftarkannya - sesuatu seperti ini:
SqlMapper . AddTypeHandler ( new Customer . DapperTypeHandler ( ) ) ;Lihat folder contoh untuk informasi lebih lanjut.
Berikut ini adalah kutipan dari halaman FAQ lengkap di wiki.
Ya, ada di sini: https://stevedunn.github.io/vogen/vogen.html
Generator sumber adalah .NET Standard 2.0. Kode yang dihasilkannya mendukung semua versi bahasa C# dari 6.0 dan dan seterusnya
Jika Anda menggunakan generator dalam proyek .NET Framework dan menggunakan proyek gaya lama (yang sebelum proyek 'SDK Style'), maka Anda harus melakukan beberapa hal secara berbeda:
PackageReference di file .csproj: < ItemGroup >
< PackageReference Include = " Vogen " Version = " [LATEST_VERSION_HERE - E.G. 1.0.18] " PrivateAssets = " all " />
</ ItemGroup >latest (atau apa pun 8 atau lebih): <PropertyGroup>
+ <LangVersion>latest</LangVersion>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
Objek nilai istilah mewakili objek kecil yang kesetaraan didasarkan pada nilai dan bukan identitas. Dari Wikipedia
Dalam ilmu komputer, objek nilai adalah objek kecil yang mewakili entitas sederhana yang kesetaraannya tidak didasarkan pada identitas: yaitu dua objek nilai sama ketika mereka memiliki nilai yang sama, tidak harus menjadi objek yang sama.
Di DDD, objek nilai adalah (sekali lagi, dari Wikipedia)
... Objek nilai adalah objek yang tidak dapat diubah yang berisi atribut tetapi tidak memiliki identitas konseptual
public record struct CustomerId(int Value); ? Itu tidak memberi Anda validasi. Untuk memvalidasi Value , Anda tidak dapat menggunakan sintaks steno (konstruktor primer). Jadi Anda harus melakukannya:
public record struct CustomerId
{
public CustomerId ( int value ) {
if ( value <= 0 ) throw new Exception ( .. . )
}
} Anda mungkin juga menyediakan konstruktor lain yang mungkin tidak memvalidasi data, sehingga memungkinkan data yang tidak valid ke dalam domain Anda . Konstruktor lain mungkin tidak melempar pengecualian, atau mungkin melemparkan pengecualian yang berbeda. Salah satu pendapat dalam vogen adalah bahwa setiap data tidak valid yang diberikan kepada objek nilai melempar ValueObjectValidationException .
Anda juga dapat menggunakan default(CustomerId) untuk menghindari validasi. Dalam vogen, ada analisis yang menangkap ini dan gagal dalam build, misalnya:
// error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.
CustomerId c = default ;
// error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.
var c2 = default ( CustomerId ) ; Ya. Secara default, masing -masing VO dihiasi dengan serializer TypeConverter dan System.Text.Json (STJ). Ada konverter/serial lainnya untuk:
Ya, meskipun ada pertimbangan tertentu. Silakan lihat halaman Efcore di wiki, tetapi TL; DR adalah:
[ValueObject<string>(conversions: Conversions.EfCoreValueConverter)] dan Anda perlu memberi tahu EFCore untuk menggunakan konverter itu dalam metode OnModelCreating , misalnya: builder . Entity < SomeEntity > ( b =>
{
b . Property ( e => e . Name ) . HasConversion ( new Name . EfCoreValueConverter ( ) ) ;
} ) ;Anda bisa, tetapi untuk memastikan konsistensi di seluruh domain Anda, Anda harus memvalidasi di mana -mana . Dan hukum dangkal mengatakan bahwa itu tidak mungkin:
⚖️ Hukum dangkal "Ketika hal -hal perlu diubah dan n> 1, dangkal akan menemukan paling banyak n - 1 dari hal -hal ini."
Secara konkret: "Ketika 5 hal perlu diubah, dangkal akan menemukan paling banyak, 4 dari hal -hal ini."
struct , dapatkah saya melarang penggunaan CustomerId customerId = default(CustomerId); ?Ya . Analisis menghasilkan kesalahan kompilasi.
struct , dapatkah saya melarang penggunaan CustomerId customerId = new(CustomerId); ?Ya . Analisis menghasilkan kesalahan kompilasi.
TIDAK . Konstruktor tanpa parameter dihasilkan secara otomatis, dan konstruktor yang mengambil nilai yang mendasarinya juga dihasilkan secara otomatis.
Jika Anda menambahkan konstruktor lebih lanjut, maka Anda akan mendapatkan kesalahan kompilasi dari generator kode, misalnya
[ ValueObject < int > ) ]
public partial struct CustomerId {
// Vogen already generates this as a private constructor:
// error CS0111: Type 'CustomerId' already defines a member called 'CustomerId' with the same parameter type
public CustomerId ( ) { }
// error VOG008: Cannot have user defined constructors, please use the From method for creation.
public CustomerId ( int value ) { }
}Anda bisa , tetapi Anda akan mendapatkan peringatan kompiler CS0282-tidak ada pemesanan yang ditentukan antara bidang dalam beberapa deklarasi kelas parsial atau 'tipe'
Saya ingin menormalkan/membersihkan nilai yang digunakan, misalnya, memotong input. Apakah ini mungkin?
Ya, tambahkan metode normalizeInput, misalnya
private static string NormalizeInput ( string input ) => input . Trim ( ) ;Lihat Wiki untuk informasi lebih lanjut.
Akan lebih bagus jika itu, tetapi saat ini tidak. Saya menulis sebuah artikel tentang hal itu, tetapi secara ringkas, ada proposal bahasa lama yang berfokus pada tipe nilai yang tidak dapat diatasi. Memiliki tipe nilai yang tidak dapat didefault adalah langkah pertama yang hebat, tetapi juga akan berguna untuk memiliki sesuatu dalam bahasa untuk menegakkan validasi. Jadi saya menambahkan proposal bahasa untuk catatan invarian.
Salah satu tanggapan dalam proposal mengatakan bahwa tim bahasa memutuskan bahwa kebijakan validasi tidak boleh menjadi bagian dari C#, tetapi disediakan oleh generator sumber.
StronglyTypedId Ini lebih terfokus pada ID. Vogen lebih terfokus pada 'konsep domain' dan kendala yang terkait dengan konsep -konsep tersebut.
Stringtyped ini adalah upaya pertama saya dan tidak dihasilkan sumber. Ada overhead memori karena jenis dasarnya adalah kelas. Juga tidak ada analisis. Sekarang ditandai sebagai sudah usang mendukung vogen.
Nilai mirip dengan StrededTyped - Non Source -Engontated dan No Analyzer. Ini juga lebih santai dan memungkinkan tipe 'yang mendasari' komposit.
ValueObjectGenerator mirip dengan vogen, tetapi kurang fokus pada validasi dan tidak ada penganalisa kode.
Jenis apa pun dapat dibungkus. Serialisasi dan tipe konversi memiliki implementasi untuk:
rangkaian
int
panjang
pendek
byte
float (single)
desimal
dobel
Datetime
DateNy
Timeonly
DatetimeOffset
GUID
bool
Untuk jenis lain, konversi jenis dan serializer generik diterapkan. Jika Anda menyediakan konverter Anda sendiri untuk konversi jenis dan serialisasi, maka tentukan None untuk konverter dan hias tipe Anda dengan atribut untuk tipe Anda sendiri, misalnya
[ ValueObject < SpecialPrimitive > ( conversions : Conversions . None ) ]
[ System . Text . Json . Serialization . JsonConverter ( typeof ( SpecialPrimitiveJsonConverter ) ) ]
[ System . ComponentModel . TypeConverter ( typeof ( SpecialPrimitiveTypeConverter ) ) ]
public partial struct SpecialMeasurement ; Ya, dengan menentukan jenis pengecualian baik di atribut ValueObject , atau secara global, dengan VogenConfiguration .
TimeOnly yang mengatakan bahwa DateTime tidak dapat dikonversi ke TimeOnly - apa yang harus saya lakukan? LINQ2DB 4.0 atau lebih besar mendukung DateOnly dan TimeOnly . Vogen menghasilkan konverter nilai untuk linq2db; Untuk DateOnly , itu hanya berfungsi, tetapi untuk `TimeAnly, Anda perlu menambahkan ini ke aplikasi Anda:
MappingSchema.Default.SetConverter<DateTime, TimeOnly>(dt => TimeOnly.FromDateTime(dt));
Ya. Tambahkan ketergantungan ke protobuf-net dan atur atribut pengganti:
[ ValueObject < string > ]
[ ProtoContract ( Surrogate = typeof ( string ) ) ]
public partial class BoxId {
//...
}Tipe BoxID sekarang akan diserialisasi sebagai string di semua pesan/panggilan GRPC. Jika seseorang menghasilkan file .proto untuk aplikasi lain dari kode C#, file proto akan menyertakan jenis pengganti sebagai jenis. Terima kasih untuk @Domasm untuk informasi ini .
Terima kasih kepada semua orang yang telah berkontribusi!
Saya mengambil banyak inspirasi dari Andrew Lock's Strongytypedid.
Saya juga mendapat beberapa ide hebat dari Gérald Barré's meziantou.analyzer