KembaliKembali ke artikel

Memahami Attribute di C#: Metadata yang Membuat Kode Lebih Fleksibel dan Powerful

11 Agustus 2026

1 jam baca

4 pembaca

Memahami Attribute di C#: Metadata yang Membuat Kode Lebih Fleksibel dan Powerful

Attribute di C# bukan sekadar syntax dengan tanda kurung siku. Di balik [HttpGet], [Required], atau [Key] terdapat mekanisme metadata yang memungkinkan compiler, runtime, framework, dan tooling memahami informasi tambahan yang melekat pada kode.

C#Advanced C#

Bagian 1: Pendahuluan

Dalam pengembangan aplikasi C#, kode seperti berikut cukup sering ditemui:

C#

[HttpGet]
public IActionResult GetUsers() { ... }

Sekilas, [HttpGet] terlihat seperti informasi tambahan yang ditempatkan di atas method. Namun, bagaimana ASP.NET Core bisa mengetahui bahwa method tersebut harus menangani HTTP GET?

Method tersebut tidak memanggil fungsi khusus untuk mendaftarkan dirinya sebagai handler GET. Tidak ada logic di dalam method yang secara eksplisit menyatakan hal tersebut. Informasinya hanya ditulis dalam bentuk [HttpGet], lalu ASP.NET Core dapat membacanya dan menggunakannya.

Hal serupa juga dapat ditemukan di berbagai bagian lain dalam ekosistem .NET:

  • Bagaimana sebuah class dapat memiliki informasi tambahan tanpa perlu menambahkan property baru?

  • Bagaimana Entity Framework dapat mengetahui bahwa sebuah property merupakan primary key hanya dengan menambahkan [Key]?

  • Bagaimana JsonSerializer dapat mengetahui bahwa property FirstName perlu ditulis sebagai first_name dalam JSON?

Pertanyaan-pertanyaan tersebut memiliki satu konsep yang sama sebagai dasar jawabannya: metadata.

Apa itu Metadata?

Secara sederhana, metadata adalah informasi tentang sesuatu. Dalam konteks pemrograman, metadata dapat berupa informasi tambahan mengenai kode, seperti class, method, property, parameter, atau elemen program lainnya.

Metadata bukan merupakan logic utama dari program. Namun, informasi tersebut dapat dibaca dan dimanfaatkan oleh berbagai komponen, seperti compiler, tooling, library, maupun framework.

Misalnya, kode berikut:

C#

[HttpGet]
public IActionResult GetUsers() { ... }

GetUsers() berisi logic untuk menangani suatu proses. Sementara itu, [HttpGet] memberikan informasi tambahan mengenai bagaimana method tersebut seharusnya digunakan oleh ASP.NET Core.

Dengan kata lain, [HttpGet] tidak menjalankan proses HTTP GET secara langsung. Attribute tersebut memberikan informasi yang nantinya dapat dibaca oleh ASP.NET Core.

Attribute sebagai Salah Satu Cara Menambahkan Metadata

Salah satu cara yang umum digunakan untuk menambahkan metadata di C# adalah melalui Attribute. Bentuknya biasanya mudah dikenali karena menggunakan tanda kurung siku:

C#

[HttpGet]
[Obsolete]
[Serializable]

Attribute dapat ditempatkan pada berbagai bagian kode, misalnya class, method, property, parameter, dan beberapa elemen program lainnya.

Contohnya:

C#

[Obsolete]
public void OldMethod()
{
}

Pada contoh tersebut, [Obsolete] memberikan informasi tambahan bahwa OldMethod() sudah tidak dianjurkan untuk digunakan.

Contoh lain dapat ditemukan pada ASP.NET Core:

C#

[HttpGet]
public IActionResult GetUsers()
{
// ...
}

[HttpGet] memberikan metadata kepada ASP.NET Core bahwa method tersebut berkaitan dengan HTTP GET.

Hal penting yang perlu dipahami adalah bahwa Attribute bukan sekadar "teks yang ditempel" di atas kode. Di balik sintaks tersebut terdapat sebuah object yang dapat dibaca melalui mekanisme tertentu di .NET.

Mengapa Attribute Penting?

Attribute digunakan di banyak bagian dalam ekosistem .NET. Beberapa framework dan library memanfaatkannya untuk memberikan informasi tambahan pada kode.

Contohnya:

  • ASP.NET Core menggunakan Attribute untuk routing, authorization, filtering, dan berbagai kebutuhan lainnya.

  • Entity Framework Core dapat menggunakan Attribute seperti [Key] dan [Required] untuk memberikan konfigurasi pada model.

  • System.Text.Json menyediakan Attribute seperti [JsonPropertyName] untuk mengatur proses serialisasi dan deserialisasi JSON.

  • xUnit menggunakan Attribute seperti [Fact] untuk menandai method sebagai test.

  • .NET compiler juga mengenali Attribute tertentu, seperti [Obsolete], dan dapat memberikan warning ketika Attribute tersebut digunakan.

Karena itu, memahami Attribute tidak hanya membantu memahami sintaks [Something] yang sering terlihat dalam kode C#. Lebih jauh lagi, konsep ini dapat membantu menjelaskan bagaimana framework dapat membaca informasi dari kode dan mengambil keputusan berdasarkan informasi tersebut.

Artikel ini akan membahas Attribute secara bertahap. Pembahasan dimulai dari konsep dasar Attribute, cara membuat Custom Attribute, hingga bagaimana Attribute dibaca menggunakan Reflection. Setelah itu, pembahasan akan dilanjutkan dengan melihat bagaimana konsep yang sama digunakan oleh berbagai framework besar di ekosistem .NET.

Bagian 2: Apa Itu Attribute?

Secara sederhana, Attribute adalah mekanisme di C# untuk menambahkan metadata pada elemen kode, seperti class, method, property, parameter, atau bahkan assembly secara keseluruhan.

Metadata dapat dipahami sebagai informasi tentang kode. Informasi ini memberikan keterangan tambahan mengenai suatu elemen kode tanpa menjadi bagian dari logic utama elemen tersebut.

Sebagai gambaran sederhana, sebuah buku memiliki isi yang menjadi bagian utama buku tersebut. Di luar isi tersebut, terdapat informasi lain seperti judul, penulis, dan tahun terbit.

Plain Text

Buku
├── Isi
├── Judul
├── Penulis
└── Tahun terbit

Judul, penulis, dan tahun terbit tidak mengubah isi buku. Namun, informasi tersebut memberikan konteks tambahan mengenai buku. Konsep yang kurang lebih sama dapat ditemukan pada kode:

Plain Text

Class / Method
├── Logic
└── Metadata
└── Attribute

Logic menentukan apa yang dilakukan oleh kode, sedangkan Attribute memberikan informasi tambahan mengenai kode tersebut.

Contohnya:

C#

[Obsolete]
public void OldMethod()
{
// ...
}

OldMethod() tetap memiliki logic yang sama. [Obsolete] tidak mengubah isi method tersebut, tetapi memberikan informasi bahwa method tersebut sudah tidak dianjurkan untuk digunakan.

Attribute Disimpan sebagai Metadata

Ketika project C# dikompilasi, informasi mengenai Attribute akan menjadi bagian dari metadata assembly. Metadata tersebut kemudian dapat dibaca kembali oleh berbagai mekanisme di .NET, termasuk saat aplikasi sedang berjalan.

Inilah salah satu alasan mengapa sebuah program dapat mengetahui bahwa suatu method memiliki Attribute tertentu meskipun informasi tersebut tidak ditulis sebagai property atau field biasa di dalam class.

Sebagai contoh:

C#

[Obsolete]
public void OldMethod()
{
}

Setelah program dikompilasi, informasi mengenai [Obsolete] tetap tersedia dalam metadata assembly. Kode lain dapat membaca informasi tersebut ketika diperlukan. Salah satu mekanisme yang paling umum digunakan untuk membaca informasi semacam ini adalah Reflection. Reflection akan dibahas lebih lanjut pada bagian berikutnya.

Attribute Bukan Logic Program

Attribute juga perlu dibedakan dari logic program.

Misalnya:

C#

[Obsolete]
public void OldMethod()
{
Console.WriteLine("Hello");
}

[Obsolete] tidak membuat OldMethod() menjalankan instruksi tambahan ketika method tersebut dipanggil. Attribute hanya memberikan informasi tambahan mengenai method tersebut. Hal yang sama berlaku untuk banyak Attribute lainnya. Attribute pada dasarnya berisi informasi yang dapat digunakan oleh sistem lain.

Attribute Bersifat Pasif

Attribute tidak melakukan sesuatu dengan sendirinya.

Sebagai contoh:

C#

[HttpGet]
public IActionResult GetUsers()
{
// ...
}

Menambahkan [HttpGet] tidak secara langsung membuat method tersebut menerima HTTP GET request.

ASP.NET Core memiliki mekanisme sendiri untuk menemukan endpoint, membaca Attribute yang terdapat pada method tersebut, lalu menggunakan informasi tersebut untuk menentukan bagaimana endpoint tersebut harus diperlakukan.

Jadi, ada beberapa bagian yang perlu dibedakan:

Plain Text

[HttpGet]
Metadata
Dibaca oleh mekanisme ASP.NET Core
Digunakan untuk menentukan routing

Attribute menyediakan informasinya. Kode lain yang membaca informasi tersebutlah yang menentukan bagaimana informasi itu digunakan.

Attribute sebagai Wadah Informasi

Karena itu, Attribute dapat dipandang sebagai wadah informasi tambahan yang ditempelkan pada kode.

Misalnya:

C#

[HttpGet]
public IActionResult GetUsers()
{
// ...
}

Secara sederhana, dapat dibayangkan bahwa method tersebut memiliki informasi tambahan seperti:

Plain Text

Method: GetUsers
Metadata:
HTTP Method = GET

Kemudian, framework yang membutuhkan informasi tersebut dapat membacanya dan mengambil keputusan berdasarkan metadata tersebut.

Cara pembacaan dan pemanfaatannya dapat berbeda-beda tergantung pada framework atau library yang digunakan. Pada banyak kasus, Reflection menjadi salah satu mekanisme penting yang memungkinkan informasi tersebut ditemukan saat runtime.

Dengan cara pandang ini, Attribute dapat dianggap sebagai label yang membawa informasi tambahan tentang kode. Label tersebut tidak menjalankan logic utama program, tetapi dapat menjadi sangat berguna ketika ada bagian lain dari aplikasi yang membacanya dan mengetahui cara memanfaatkannya.

Bagian 3: Kenapa Attribute Dibutuhkan?

Setelah memahami apa itu Attribute, muncul pertanyaan yang cukup wajar: mengapa perlu menggunakan Attribute? Bukankah informasi yang sama sebenarnya bisa disimpan atau ditentukan menggunakan logic biasa?

Secara teknis, memang bisa. Namun, Attribute memberikan cara yang lebih jelas untuk menyatakan informasi tentang kode tanpa mencampurkannya dengan logic yang menjalankan proses.

Contoh Sederhana

Misalnya, sebuah framework perlu mengetahui property mana yang digunakan sebagai primary key. Salah satu pendekatan yang mungkin digunakan tanpa Attribute adalah membuat konvensi berdasarkan nama property:

C#

if (property.Name == "Id")
{
// Anggap property ini sebagai primary key
}

Selama semua class menggunakan property bernama Id, pendekatan tersebut dapat berjalan dengan baik. Namun, pendekatan seperti ini memiliki keterbatasan. Bagaimana jika sebuah class menggunakan nama UserId?

C#

public class User
{
public int UserId { get; set; }
}

Logic sebelumnya tidak akan mengenali UserId sebagai primary key karena hanya mencari property dengan nama Id. Masalah lainnya adalah informasi penting menjadi tersembunyi di dalam logic.

Ketika melihat class tersebut:

C#

public class User
{
public int UserId { get; set; }
}

Tidak ada informasi yang secara langsung menunjukkan bahwa UserId memiliki peran khusus. Untuk mengetahuinya, perlu mencari kode lain yang memproses class tersebut.

Menyatakan Informasi Secara Eksplisit

Attribute memberikan pendekatan yang berbeda.

Sebagai ilustrasi:

C#

public class User
{
[PrimaryKey]
public int UserId { get; set; }
}

Sekarang informasi mengenai primary key berada langsung pada property yang bersangkutan.

Ketika melihat UserId, dapat langsung terlihat bahwa property tersebut memiliki peran khusus:

Plain Text

UserId
└── [PrimaryKey]
└── Property ini adalah primary key

Tidak perlu mengetahui bagaimana framework memproses property tersebut terlebih dahulu untuk memahami maksud deklarasinya. Inilah salah satu kekuatan utama Attribute: informasi dapat dinyatakan secara deklaratif di tempat informasi tersebut berlaku.

Beberapa Keuntungan Attribute

1. Lebih mudah dibaca

Attribute membuat metadata terlihat langsung pada elemen kode yang berkaitan.

Misalnya:

C#

[Required]
public string Name { get; set; }

Informasi bahwa Name wajib diisi dapat terlihat langsung dari deklarasinya.

Bandingkan dengan aturan yang hanya terdapat di tempat lain:

C#

if (property.Name == "Name")
{
// ...
}

Pembaca kode perlu mengetahui terlebih dahulu bahwa terdapat logic tertentu yang memperlakukan Name secara khusus.

Dengan Attribute, informasi tersebut lebih dekat dengan sumbernya.

2. Tidak bergantung pada nama

Attribute memungkinkan suatu elemen kode memiliki peran tertentu tanpa harus mengikuti nama tertentu.

Misalnya:

C#

[PrimaryKey]
public int UserId { get; set; }

Primary key tidak harus bernama Id. Nama UserId, CustomerNumber, atau nama lainnya tetap dapat digunakan selama mekanisme yang membaca Attribute memahami [PrimaryKey].

Dengan demikian, nama dan peran dapat dipisahkan.

3. Lebih mudah dipelihara

Ketika aturan perlu diubah, Attribute dapat menjadi tempat yang jelas untuk menyatakan perubahan tersebut.

Misalnya:

C#

[Required]
public string Email { get; set; }

Kemudian aturan validasi berubah dan property tersebut tidak lagi wajib diisi. Perubahannya dapat dilakukan pada deklarasi metadata tersebut, tanpa harus mencari berbagai logic yang mungkin bergantung pada nama property. Tentu saja, tingkat kemudahannya tetap bergantung pada bagaimana framework atau aplikasi dirancang.

4. Mengurangi ketergantungan pada konvensi tersembunyi

Konvensi penamaan sebenarnya bukan sesuatu yang buruk. Banyak framework justru menggunakan convention over configuration untuk mengurangi konfigurasi yang tidak perlu.

Misalnya, framework dapat memiliki aturan:

Plain Text

Property bernama "Id"
Dianggap sebagai primary key

Pendekatan ini sederhana dan nyaman selama konvensinya cukup jelas dan tidak perlu banyak pengecualian. Masalah mulai muncul ketika aturan menjadi semakin kompleks atau tidak lagi dapat diwakili hanya dengan nama.

Attribute dapat digunakan ketika informasi tersebut perlu dinyatakan secara lebih eksplisit:

Plain Text

[PrimaryKey]
Property ini adalah primary key

Dengan demikian, Attribute dan convention sebenarnya bukan dua pendekatan yang harus selalu dipertentangkan. Convention cocok untuk aturan umum yang sederhana, sedangkan Attribute berguna ketika suatu informasi perlu dinyatakan secara eksplisit pada elemen kode tertentu.

Attribute dan Pendekatan Deklaratif

Di sinilah konsep deklaratif menjadi penting. Dengan pendekatan imperatif, kode menjelaskan bagaimana suatu proses dilakukan:

C#

if (property.Name == "Id")
{
// lakukan sesuatu
}

Sementara itu, Attribute dapat digunakan untuk menyatakan apa arti atau karakteristik dari suatu elemen:

C#

[PrimaryKey]
public int UserId { get; set; }

Attribute tidak menjelaskan bagaimana primary key tersebut diproses. Attribute hanya menyatakan bahwa property tersebut memiliki karakteristik tertentu. Bagian lain dari sistem kemudian dapat membaca informasi tersebut dan menentukan apa yang harus dilakukan.

Gambaran sederhananya:

Plain Text

Attribute
│ menyatakan informasi
[PrimaryKey]
public int UserId { get; set; }
│ dibaca oleh framework
Framework menentukan perilaku

Pola seperti ini banyak digunakan oleh framework di ekosistem .NET. Developer cukup memberikan informasi yang diperlukan melalui deklarasi Attribute, sedangkan framework menangani proses di belakangnya.

Karena itu, Attribute bukan sekadar cara untuk menambahkan sintaks [Something] pada kode. Attribute merupakan salah satu cara untuk memisahkan informasi tentang kode dari logic yang menjalankan kode tersebut, sekaligus membuat informasi tersebut dapat dibaca oleh sistem lain.

Bagian 4: Anatomi dari sebuah Attribute

Setelah memahami apa itu Attribute dan alasan penggunaannya, bagian ini akan melihat bentuk Attribute secara lebih dekat.

Beberapa contoh Attribute yang mungkin sudah sering ditemui:

C#

[Obsolete]
public void OldMethod() { }
[Serializable]
public class UserData { }
[AttributeUsage(AttributeTargets.Class)]
public class MyCustomAttribute : Attribute { }
[HttpGet]
public IActionResult Index() { }

Sekilas, semuanya terlihat memiliki pola yang sama:

Plain Text

[Attribute]
Elemen kode

Nama Attribute ditulis di dalam tanda kurung siku [ ], kemudian ditempatkan pada elemen kode yang ingin diberi metadata. Tanda kurung siku tersebut merupakan syntax khusus C# untuk Attribute. Compiler mengenalinya sebagai deklarasi metadata, bukan sebagai statement biasa yang dieksekusi seperti pemanggilan method atau operasi if. Metadata tersebut kemudian menjadi bagian dari informasi yang tersedia pada hasil kompilasi dan dapat dibaca oleh komponen lain, termasuk saat aplikasi berjalan.

Attribute Sebenarnya Adalah Sebuah Class

Ada satu hal yang mungkin terlihat sedikit membingungkan ketika pertama kali mempelajari Attribute.

Misalnya terdapat kode:

C#

[Obsolete]
public void OldMethod()
{
}

Dari syntax tersebut terlihat seolah-olah Obsolete adalah sesuatu yang khusus disediakan oleh compiler.

Sebenarnya, Obsolete adalah sebuah Attribute class yang bernama:

C#

ObsoleteAttribute

Secara konsep, bentuk sederhananya dapat dibayangkan seperti:

C#

public class ObsoleteAttribute : Attribute
{
// ...
}

Jadi, Attribute yang ditulis menggunakan:

C#

[Obsolete]

sebenarnya merujuk pada class ObsoleteAttribute.

Hal yang sama berlaku untuk Custom Attribute. Misalnya:

C#

public class PrimaryKeyAttribute : Attribute
{
}

Attribute tersebut dapat digunakan seperti:

C#

[PrimaryKey]
public int UserId { get; set; }

Dengan kata lain, syntax [PrimaryKey] sebenarnya merupakan cara khusus C# untuk menggunakan sebuah class Attribute.

Akhiran Attribute Boleh Dihilangkan

C# menyediakan kemudahan dalam penamaan Attribute. Jika nama class Attribute diakhiri dengan Attribute, akhiran tersebut boleh dihilangkan ketika Attribute digunakan.

Misalnya:

C#

[Obsolete]

dan:

C#

[ObsoleteAttribute]

keduanya merujuk pada ObsoleteAttribute.

Hal yang sama berlaku pada Custom Attribute:

C#

public class PrimaryKeyAttribute : Attribute
{
}

Attribute tersebut dapat digunakan dengan:

C#

[PrimaryKey]

atau:

C#

[PrimaryKeyAttribute]

Keduanya valid.

Secara sederhana, dapat dibayangkan bahwa ketika compiler menemukan:

C#

[PrimaryKey]

compiler dapat mencocokkannya dengan class:

C#

PrimaryKeyAttribute

jika class tersebut tersedia dan memenuhi aturan Attribute.

Akhiran Attribute dapat dihilangkan hanya ketika digunakan dalam syntax Attribute. Nama class-nya sendiri tetap PrimaryKeyAttribute.

Kenapa Konvensi Ini Ada?

Konvensi tersebut terutama dibuat untuk membuat kode lebih ringkas dan mudah dibaca.

Bandingkan:

C#

[HttpGet]
[Authorize]
[Obsolete]

dengan:

C#

[HttpGetAttribute]
[AuthorizeAttribute]
[ObsoleteAttribute]

Keduanya valid jika class Attribute yang sesuai tersedia, tetapi bentuk pertama biasanya lebih nyaman dibaca. Konvensi ini berlaku baik untuk Attribute bawaan .NET maupun Custom Attribute yang dibuat dalam aplikasi sendiri.

Attribute Harus Berasal dari System.Attribute

Agar sebuah class dapat digunakan sebagai Attribute, class tersebut harus mewarisi System.Attribute, baik secara langsung maupun melalui inheritance.

Contoh paling sederhana:

C#

public class PrimaryKeyAttribute : Attribute
{
}

Setelah itu, Attribute tersebut dapat digunakan:

C#

public class User
{
[PrimaryKey]
public int UserId { get; set; }
}

Strukturnya dapat digambarkan seperti ini:

Plain Text

System.Attribute
PrimaryKeyAttribute
[PrimaryKey]

Di sini terlihat bahwa Attribute sebenarnya bukan jenis object yang sepenuhnya berbeda dari class biasa. Attribute adalah class yang mengikuti aturan khusus agar dapat digunakan sebagai metadata. Karena itu, memahami inheritance dari System.Attribute menjadi langkah penting sebelum mulai membuat Custom Attribute.

Bagian 5: Attribute Adalah Class

Salah satu konsep penting sebelum membuat Custom Attribute adalah memahami bahwa Attribute pada dasarnya adalah sebuah class. Tidak ada mekanisme khusus yang membuat [Obsolete] atau [Serializable] menjadi sesuatu yang sepenuhnya berbeda dari konsep class dalam C#. Keduanya berasal dari class yang mewarisi System.Attribute.

Sebagai gambaran sederhana:

C#

public class ObsoleteAttribute : Attribute
{
public string Message { get; }
public ObsoleteAttribute(string message)
{
Message = message;
}
}

Kemudian Attribute tersebut dapat digunakan seperti:

C#

[Obsolete("Gunakan method versi baru")]
public void OldMethod()
{
}

Di sini ObsoleteAttribute adalah sebuah class, sedangkan [Obsolete(...)] adalah syntax Attribute yang digunakan untuk memberikan metadata pada OldMethod().

Inheritance dari System.Attribute

Setiap class yang dimaksudkan untuk digunakan sebagai Attribute harus mewarisi System.Attribute, baik secara langsung maupun melalui class turunan lainnya. Secara sederhana, hubungan inheritance-nya dapat digambarkan seperti ini:

Plain Text

System.Attribute
├── ObsoleteAttribute
├── SerializableAttribute
└── AuthorAttribute

Contoh sederhananya:

C#

// Class dasar dari .NET
public abstract class Attribute
{
}
// Attribute bawaan
public class ObsoleteAttribute : Attribute
{
}
public class SerializableAttribute : Attribute
{
}
// Custom Attribute
public class AuthorAttribute : Attribute
{
}

Dengan mewarisi System.Attribute, sebuah class dapat dikenali sebagai class yang dapat digunakan dalam syntax Attribute.

Attribute Tetap Mengikuti Konsep Class

Karena Attribute merupakan class, konsep OOP yang umum juga dapat ditemukan di dalamnya. Sebuah Attribute dapat memiliki:

  • constructor

  • property

  • field

  • method

Contohnya:

C#

public class AuthorAttribute : Attribute
{
public string Name { get; }
public AuthorAttribute(string name)
{
Name = name;
}
}

Attribute tersebut kemudian dapat digunakan:

C#

[Author("Budi")]
public class UserService
{
}

String "Budi" pada contoh tersebut bukan sekadar teks yang ditempelkan pada class. Nilai tersebut merupakan argumen yang diberikan kepada constructor AuthorAttribute.

Secara konsep, deklarasi:

C#

[Author("Budi")]

berhubungan dengan:

C#

new AuthorAttribute("Budi")

Namun, keduanya tidak digunakan dengan cara yang sama. new AuthorAttribute("Budi") adalah pembuatan object secara langsung dalam logic program, sedangkan [Author("Budi")] adalah deklarasi metadata pada kode.

Lalu Apa yang Terjadi Saat Compile?

Bagian ini perlu sedikit diperhatikan karena terdapat perbedaan antara source code, metadata assembly, dan object Attribute saat runtime.

Ketika compiler menemukan:

C#

[Author("Budi")]
public class UserService
{
}

compiler tidak sekadar menganggapnya sebagai pemanggilan constructor biasa.

Compiler memproses deklarasi tersebut dan menyimpan informasi Attribute ke dalam metadata hasil kompilasi, termasuk informasi mengenai:

  • Attribute class yang digunakan

  • constructor yang digunakan

  • argumen yang diberikan

  • target tempat Attribute tersebut dipasang

Informasi tersebut menjadi bagian dari assembly dan dapat dibaca kembali nantinya.

Jadi, secara sederhana:

Plain Text

Source Code
│ compile
Assembly
├── IL
├── Type information
└── Attribute metadata

Pada tahap ini, lebih tepat membayangkan bahwa informasi untuk Attribute disimpan dalam metadata assembly, bukan bahwa object Attribute sudah dibuat dan disimpan sebagai object hidup di dalam assembly.

Kapan Object Attribute Dibuat?

Object Attribute dapat dibuat ketika metadata tersebut dibaca kembali, misalnya menggunakan Reflection.

Misalnya terdapat:

C#

[Author("Budi")]
public class UserService
{
}

Kemudian kode lain menggunakan Reflection untuk mengambil Attribute:

C#

var attribute = typeof(UserService)
.GetCustomAttributes(typeof(AuthorAttribute), inherit: false)
.FirstOrDefault();

Pada proses tersebut, .NET dapat membuat instance AuthorAttribute berdasarkan informasi yang tersimpan di metadata.

Hasilnya dapat digunakan seperti object biasa:

C#

if (attribute is AuthorAttribute author)
{
Console.WriteLine(author.Name);
}

Secara sederhana, alurnya menjadi:

Plain Text

[Author("Budi")]
│ compile
Metadata dalam Assembly
│ Reflection
AuthorAttribute instance
author.Name
"Budi"

Di sinilah konsep Attribute mulai terlihat lebih jelas. Syntax [Author("Budi")] memang terlihat sederhana, tetapi di baliknya terdapat beberapa tahap: compiler menyimpan informasi tersebut ke dalam metadata, kemudian runtime dapat membaca metadata tersebut dan membuat instance Attribute ketika diperlukan.

Dengan memahami hal ini, syntax seperti:

C#

[Obsolete("Gunakan CalculateV2()")]

tidak lagi terlihat seperti sesuatu yang misterius.

Terdapat sebuah class bernama ObsoleteAttribute, terdapat constructor yang menerima parameter, dan informasi mengenai penggunaan Attribute tersebut disimpan sebagai metadata dalam assembly. Reflection kemudian menjadi salah satu mekanisme yang memungkinkan metadata tersebut dibaca kembali saat runtime.

Bagian 6: Attribute Bawaan .NET

Sebelum membuat Custom Attribute sendiri, ada baiknya melihat beberapa Attribute yang sudah tersedia di .NET. Attribute-Attribute ini dapat ditemukan di berbagai jenis aplikasi dan library. Masing-masing memiliki tujuan yang berbeda, tetapi pola dasarnya tetap sama: memberikan informasi tambahan pada elemen kode yang kemudian digunakan oleh compiler, runtime, atau library tertentu.

Obsolete

C#

[Obsolete]
public void OldMethod()
{
}

ObsoleteAttribute digunakan untuk menandai suatu elemen kode sebagai sudah tidak dianjurkan untuk digunakan.

Misalnya:

C#

[Obsolete]
public void OldMethod()
{
}
public void NewMethod()
{
}

Ketika OldMethod() digunakan dari kode lain, compiler dapat memberikan warning bahwa method tersebut sudah tidak digunakan lagi.

Pesan yang lebih informatif juga dapat diberikan:

C#

[Obsolete("Gunakan NewMethod() sebagai gantinya.")]
public void OldMethod()
{
}

Bahkan, Attribute ini dapat dikonfigurasi agar penggunaannya menghasilkan error:

C#

[Obsolete("Gunakan NewMethod() sebagai gantinya.", true)]
public void OldMethod()
{
}

Dalam contoh tersebut, true menunjukkan bahwa penggunaan OldMethod() harus dianggap sebagai error oleh compiler. Menariknya, Obsolete merupakan contoh Attribute yang diproses oleh compiler. Artinya, efeknya dapat terlihat bahkan sebelum aplikasi dijalankan.

Serializable

C#

[Serializable]
public class UserData
{
}

SerializableAttribute digunakan untuk menandai sebuah class sebagai dapat diserialisasi oleh mekanisme tertentu yang mendukung Attribute tersebut. Attribute ini terutama berkaitan dengan mekanisme serialisasi lama di .NET, termasuk BinaryFormatter.

Perlu diperhatikan bahwa [Serializable] bukan berarti object tersebut otomatis berubah menjadi JSON, XML, atau format tertentu hanya karena Attribute tersebut ditambahkan. Attribute ini hanya memberikan metadata bahwa type tersebut mendukung mekanisme serialisasi tertentu.

Pada .NET modern, BinaryFormatter sendiri sudah tidak direkomendasikan untuk digunakan karena memiliki masalah keamanan. Karena itu, contoh [Serializable] lebih tepat dipahami sebagai bagian penting dari sejarah dan mekanisme serialisasi klasik .NET, bukan sebagai pendekatan yang biasanya dipilih untuk serialisasi data pada aplikasi baru.

Conditional

C#

[Conditional("DEBUG")]
public void LogDebugInfo()
{
}

ConditionalAttribute memiliki perilaku yang cukup berbeda dibanding Attribute seperti Obsolete. Attribute ini dapat digunakan untuk menentukan apakah pemanggilan sebuah method akan disertakan oleh compiler berdasarkan apakah suatu conditional compilation symbol tersedia.

Contohnya:

C#

[Conditional("DEBUG")]
public void LogDebugInfo()
{
Console.WriteLine("Debug information");
}

Kemudian:

C#

LogDebugInfo();

Jika symbol DEBUG tersedia, pemanggilan method tersebut akan dipertahankan dalam hasil kompilasi. Jika DEBUG tidak tersedia, pemanggilan tersebut dapat dihilangkan oleh compiler. Hal penting yang perlu diperhatikan adalah bahwa ConditionalAttribute berlaku pada pemanggilan method, bukan membuat method tersebut menghilang dari assembly.

Jadi, secara sederhana:

Plain Text

DEBUG tersedia
└── pemanggilan LogDebugInfo() dipertahankan
DEBUG tidak tersedia
└── pemanggilan LogDebugInfo() dihilangkan

Attribute ini sering berguna untuk kebutuhan seperti logging atau debugging yang hanya diperlukan pada build tertentu.

Flags

C#

[Flags]
public enum Permission
{
None = 0,
Read = 1,
Write = 2,
Execute = 4
}

FlagsAttribute digunakan untuk menunjukkan bahwa sebuah enum dirancang agar nilai-nilainya dapat dikombinasikan menggunakan operasi bitwise.

Contohnya:

C#

var permission = Permission.Read | Permission.Write;

Sekarang permission merepresentasikan dua permission sekaligus:

Plain Text

Read
+
Write

Karena setiap nilai menggunakan bit yang berbeda:

Plain Text

Read = 001
Write = 010
Execute = 100

kombinasi:

Plain Text

Read | Write
001 | 010
-----------
011

dapat merepresentasikan dua nilai sekaligus.

[Flags] sendiri tidak mengubah cara operator | bekerja. Attribute tersebut terutama memberikan informasi mengenai maksud penggunaan enum dan juga memengaruhi representasi string enum tertentu, misalnya ketika dipanggil melalui ToString().

CLSCompliant

C#

[CLSCompliant(true)]
public class PublicLibrary
{
}

CLSCompliantAttribute berkaitan dengan Common Language Specification (CLS).

.NET mendukung banyak bahasa pemrograman, seperti C#, Visual Basic, dan F#. Bahasa-bahasa tersebut dapat menghasilkan assembly yang menggunakan runtime dan type system .NET yang sama. Namun, setiap bahasa dapat memiliki fitur atau aturan yang berbeda. CLS menyediakan sekumpulan aturan umum agar public API sebuah library dapat digunakan dengan lebih mudah oleh berbagai bahasa .NET.

Misalnya, sebuah library dapat menandai API tertentu sebagai CLS-compliant:

C#

[CLSCompliant(true)]
public class PublicLibrary
{
}

Compiler kemudian dapat membantu mendeteksi penggunaan fitur tertentu yang tidak sesuai dengan aturan CLS pada API tersebut. Dengan demikian, Attribute ini bukan berarti runtime melakukan pemeriksaan kompatibilitas setiap kali class digunakan. Fokus utamanya adalah memberikan informasi kepada compiler mengenai kepatuhan terhadap CLS.

ThreadStatic

C#

public class Counter
{
[ThreadStatic]
public static int Value;
}

ThreadStaticAttribute digunakan pada static field untuk menunjukkan bahwa field tersebut memiliki nilai yang berbeda pada setiap thread. Tanpa [ThreadStatic], sebuah static field pada dasarnya memiliki satu storage yang dapat diakses oleh berbagai thread:

Plain Text

Thread 1 ──┐
Thread 2 ──┼──> static Value
Thread 3 ──┘

Dengan [ThreadStatic], setiap thread memiliki storage-nya sendiri:

Plain Text

Thread 1 ──> Value = 10
Thread 2 ──> Value = 20
Thread 3 ──> Value = 30

Contohnya:

C#

public class Counter
{
[ThreadStatic]
public static int Value;
}

Jika thread pertama mengubah Value menjadi 10, perubahan tersebut tidak otomatis terlihat sebagai nilai 10 bagi thread kedua. Perlu diperhatikan bahwa ThreadStatic memiliki beberapa aturan penting, terutama mengenai inisialisasi field dan penggunaan static field. Karena itu, Attribute ini lebih cocok dipahami sebagai mekanisme runtime yang berkaitan dengan thread-local storage, bukan sekadar label biasa.

Attribute Tidak Selalu Diproses dengan Cara yang Sama

Keenam contoh tersebut menunjukkan satu hal penting: Attribute merupakan mekanisme metadata, tetapi pihak yang menggunakan metadata tersebut dapat berbeda-beda.

Misalnya:

Plain Text

[Obsolete]
└── digunakan oleh compiler

Plain Text

[Conditional]
└── digunakan oleh compiler

Plain Text

[Flags]
└── memberikan informasi mengenai penggunaan enum

Plain Text

[ThreadStatic]
└── digunakan oleh runtime

Sementara Attribute lain dapat dibaca dan digunakan oleh library atau framework tertentu. Karena itu, tidak tepat jika semua Attribute dianggap bekerja dengan mekanisme yang sama. Ada Attribute yang memengaruhi proses kompilasi, ada yang digunakan oleh runtime, dan ada pula yang menjadi metadata yang kemudian dibaca oleh library atau framework.

Namun, semuanya memiliki fondasi yang sama:

Plain Text

Attribute
Metadata
Compiler / Runtime / Library / Framework
Perilaku tertentu

Attribute sendiri tidak secara otomatis menjalankan logic aplikasi. Attribute menyediakan informasi. Komponen yang memahami Attribute tersebutlah yang menentukan bagaimana informasi tersebut digunakan.

Pemahaman ini akan menjadi semakin penting ketika mulai membuat Custom Attribute. Dengan Custom Attribute, mekanisme yang sama dapat digunakan untuk membuat metadata sendiri dan kemudian membangun kode yang membaca metadata tersebut menggunakan Reflection.

Bagian 7: Attribute pada Property

Salah satu tempat yang paling sering digunakan untuk Attribute adalah property pada sebuah class.

Contohnya:

C#

public class UserDto
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Column("email_address")]
public string Email { get; set; }
[JsonPropertyName("first_name")]
public string FirstName { get; set; }
}

Masing-masing Attribute memberikan informasi yang berbeda mengenai property tempat Attribute tersebut ditempatkan.

  • [Key] memberikan informasi bahwa property tersebut digunakan sebagai primary key.

  • [Required] memberikan informasi bahwa property tersebut wajib memiliki nilai dalam konteks validasi yang mendukung Attribute tersebut.

  • [Column("email_address")] memberikan informasi bahwa property Email dipetakan ke kolom database bernama email_address.

  • [JsonPropertyName("first_name")] memberikan informasi bahwa FirstName menggunakan nama first_name ketika diproses oleh System.Text.Json.

Dari contoh tersebut terlihat bahwa satu class dapat memiliki Attribute dari berbagai library:

Plain Text

UserDto
├── [Key]
├── [Required]
├── [Column(...)]
└── [JsonPropertyName(...)]

Menariknya, masing-masing Attribute dapat digunakan oleh komponen yang berbeda.

Attribute dan Entity Framework

[Key] dan [Column] merupakan contoh Attribute yang umum digunakan dalam Entity Framework Core.

Misalnya:

C#

public class User
{
[Key]
public int UserId { get; set; }
[Column("email_address")]
public string Email { get; set; }
}

Ketika Entity Framework Core membangun model dari class tersebut, informasi mengenai property perlu dikumpulkan.

Secara sederhana, prosesnya dapat dibayangkan seperti:

Plain Text

User class
Periksa property
├── UserId
│ └── [Key]
└── Email
└── [Column("email_address")]
Model Entity Framework
Mapping ke database

Entity Framework dapat menggunakan Reflection dan mekanisme metadata .NET untuk mengetahui Attribute yang terdapat pada property. Jika menemukan [Key], informasi tersebut dapat digunakan dalam konfigurasi model untuk menentukan primary key.

Jika menemukan:

C#

[Column("email_address")]
public string Email { get; set; }

Entity Framework dapat menggunakan email_address sebagai nama kolom ketika melakukan mapping ke database.

Dengan demikian, Attribute berfungsi sebagai sumber informasi, sedangkan Entity Framework adalah salah satu pihak yang membaca dan menggunakan informasi tersebut.

Attribute dan System.Text.Json

Pola yang sama juga dapat ditemukan pada System.Text.Json.

Contohnya:

C#

public class User
{
[JsonPropertyName("first_name")]
public string FirstName { get; set; }
}

Ketika object tersebut diserialisasi, JsonSerializer dapat menggunakan informasi dari [JsonPropertyName] untuk menentukan nama property dalam JSON.

Misalnya:

C#

var user = new User
{
FirstName = "Budi"
};

Hasil serialisasinya dapat berupa:

JSON

{
"first_name": "Budi"
}

Tanpa Attribute tersebut, nama yang digunakan dapat mengikuti konfigurasi naming policy yang sedang diterapkan oleh JsonSerializer.

Secara sederhana:

Plain Text

C# Property
├── Name: FirstName
└── [JsonPropertyName("first_name")]
JsonSerializer
JSON property: first_name

Sekali lagi, [JsonPropertyName] tidak melakukan proses serialisasi dengan sendirinya. Attribute hanya menyediakan informasi. JsonSerializer membaca informasi tersebut dan menggunakannya ketika melakukan serialisasi atau deserialisasi.

Satu Mekanisme, Banyak Pengguna

Dari dua contoh tersebut terlihat pola yang cukup menarik. Entity Framework dan System.Text.Json merupakan library yang berbeda dan memiliki tujuan yang berbeda pula.

Namun, keduanya dapat menggunakan mekanisme metadata yang sama:

Plain Text

C# Code
Attribute
Metadata
/ \
/ \
▼ ▼
Entity Framework JsonSerializer
│ │
▼ ▼
Database Mapping JSON Mapping

Entity Framework tidak perlu mengetahui bagaimana JsonPropertyName bekerja. Sebaliknya, System.Text.Json juga tidak perlu mengetahui bagaimana [Key] digunakan oleh Entity Framework. Masing-masing library cukup membaca Attribute yang memang menjadi tanggung jawabnya. Hal ini membuat mekanisme Attribute cukup fleksibel. Sebuah class dapat memiliki metadata dari berbagai library sekaligus, dan masing-masing library dapat mengambil informasi yang relevan bagi kebutuhannya.

Pola yang Sama di Banyak Tempat

Pada tahap ini, pola penggunaan Attribute mulai terlihat:

Plain Text

Attribute ditempatkan pada kode
Informasi menjadi bagian dari metadata
Library / framework membaca metadata
Informasi digunakan untuk menentukan perilaku

Pada property, informasi tersebut dapat digunakan untuk mapping database atau JSON. Pada method, Attribute dapat digunakan untuk memberikan informasi mengenai routing, authorization, testing, dan berbagai kebutuhan lainnya. Pada class, Attribute dapat memberikan informasi mengenai konfigurasi atau karakteristik tertentu dari type tersebut. Bahkan, Attribute juga dapat ditempatkan pada assembly untuk memberikan informasi pada tingkat yang lebih luas.

Jadi, property hanyalah salah satu tempat Attribute dapat digunakan. Mekanisme dasarnya tetap sama: Attribute menyediakan metadata, kemudian komponen lain yang memahami metadata tersebut memanfaatkannya.

Bagian 8: Attribute pada Method

Selain property, method juga merupakan salah satu tempat yang sangat umum untuk menggunakan Attribute. Beberapa contohnya dapat ditemukan dalam ASP.NET Core maupun framework testing seperti xUnit:

C#

[HttpGet]
public IActionResult GetAllUsers()
{
// ...
}

C#

[Fact]
public void Addition_ShouldReturnCorrectSum()
{
// ...
}

C#

[Theory]
[InlineData(1, 2, 3)]
public void Addition_WithMultipleInputs(
int a,
int b,
int expected)
{
// ...
}

Masing-masing Attribute memberikan informasi yang berbeda. Pada ASP.NET Core, [HttpGet] memberikan informasi bahwa method tersebut dapat digunakan untuk menangani HTTP GET. Pada xUnit, [Fact] menandai method sebagai test case yang dapat dijalankan oleh test runner. Sementara itu, [Theory] menandai test yang dapat dijalankan menggunakan beberapa kombinasi data. [InlineData] kemudian menyediakan data yang digunakan oleh test tersebut.

Bagaimana Framework Menemukan Method Tersebut?

Pertanyaan yang menarik adalah: bagaimana framework mengetahui method mana yang harus diperlakukan secara khusus? Sebuah assembly dapat memiliki banyak class dan setiap class dapat memiliki banyak method.

Misalnya:

Plain Text

Assembly
├── UserService
│ ├── GetUsers()
│ ├── CreateUser()
│ └── DeleteUser()
├── OrderService
│ ├── GetOrders()
│ └── CreateOrder()
└── UserTests
├── TestUserCreation()
├── TestUserDeletion()
└── TestUserValidation()

Tidak semua method tersebut memiliki tujuan yang sama. Test runner hanya perlu menemukan method yang merupakan test. ASP.NET Core hanya perlu menemukan method yang dapat menjadi endpoint. Di sinilah metadata dari Attribute menjadi berguna.

Secara umum, framework dapat menggunakan Reflection untuk memeriksa type dan method yang tersedia, kemudian melihat Attribute yang melekat pada masing-masing method.

Contoh pada Test Runner

Sebagai gambaran sederhana, proses yang dilakukan test runner dapat dibayangkan seperti berikut:

Plain Text

Assembly Test
Cari class
Cari method
Periksa Attribute
├── [Fact] ──────► Test Case
├── [Theory] ────► Test Case + Data
└── Tidak ada ───► Abaikan

Secara konseptual, langkahnya kurang lebih seperti ini:

  1. Test runner menemukan assembly yang berisi test.

  2. Assembly tersebut diperiksa untuk menemukan type yang relevan.

  3. Method pada type tersebut diperiksa.

  4. Attribute pada setiap method diperiksa.

  5. Method dengan [Fact] atau [Theory] dikenali sebagai test.

  6. Test runner kemudian membangun informasi yang diperlukan untuk menjalankan test tersebut.

  7. Hasil setiap test dilaporkan.

Implementasi sebenarnya tentu lebih kompleks. Test runner perlu menangani berbagai hal lain seperti lifecycle, dependency, data test, exception, parallel execution, dan sebagainya. Namun, pola dasarnya tetap dapat dipahami melalui metadata Attribute.

Contoh pada ASP.NET Core

ASP.NET Core menggunakan pola yang serupa, tetapi tujuannya berbeda.

Misalnya terdapat controller:

C#

public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult GetAllUsers()
{
// ...
}
[HttpPost]
public IActionResult CreateUser()
{
// ...
}
}

Dari dua method tersebut, ASP.NET Core perlu mengetahui bahwa:

Plain Text

GetAllUsers()
└── [HttpGet]
CreateUser()
└── [HttpPost]

memiliki aturan HTTP yang berbeda.

Informasi tersebut kemudian digunakan bersama informasi routing lainnya untuk membangun endpoint aplikasi.

Secara sederhana:

Plain Text

Controller
Method
├── [HttpGet]
└── [HttpPost]
Endpoint metadata
Routing system
HTTP Request

Ketika sebuah request HTTP masuk, routing system dapat menentukan endpoint yang sesuai berdasarkan informasi seperti URL, HTTP method, dan metadata endpoint yang telah dibangun sebelumnya.

Perlu dicatat bahwa ASP.NET Core tidak harus melakukan Reflection dari awal setiap kali request masuk. Framework dapat melakukan proses discovery dan membangun representasi endpoint ketika aplikasi sedang menyiapkan routing. Jadi, Reflection merupakan salah satu mekanisme yang dapat digunakan dalam proses menemukan metadata, bukan berarti seluruh assembly dipindai ulang untuk setiap request.

Attribute sebagai Penanda

Dari contoh xUnit dan ASP.NET Core, terlihat satu pola yang sama:

Plain Text

Method
└── Attribute
Informasi tambahan
Framework membaca informasi
Menentukan perlakuan terhadap method

Method tidak perlu ditulis dengan bentuk khusus agar framework dapat mengenalinya.

Misalnya, method berikut tetap merupakan method C# biasa:

C#

[HttpGet]
public IActionResult GetAllUsers()
{
return Ok();
}

Yang memberikan informasi tambahan adalah [HttpGet].

Framework kemudian memiliki mekanisme sendiri untuk mencari dan membaca informasi tersebut.

Attribute Tidak Menentukan Logic Method

Hal penting lainnya adalah Attribute tidak mengubah logic method secara langsung.

Misalnya:

C#

[HttpGet]
public IActionResult GetAllUsers()
{
return Ok(users);
}

[HttpGet] tidak membuat GetAllUsers() menjalankan proses HTTP GET di dalam method tersebut.

Method tetap hanya berisi logic:

C#

return Ok(users);

Sementara [HttpGet] memberikan informasi mengenai kapan dan melalui mekanisme apa method tersebut dapat dipanggil oleh framework.

Dengan demikian, dapat dibedakan:

Plain Text

Method
└── Menentukan apa yang dilakukan
Attribute
└── Memberikan informasi tentang method
Framework
└── Membaca informasi dan menentukan bagaimana method digunakan

Pola inilah yang membuat Attribute sangat berguna dalam framework. Satu method dapat tetap ditulis sebagai method biasa, tetapi dengan menambahkan metadata, framework dapat memperlakukannya sebagai endpoint HTTP, test case, handler, atau elemen khusus lainnya.

Pada titik ini, pola yang sebelumnya terlihat pada property mulai terlihat kembali pada method. Attribute tetap berfungsi sebagai metadata; yang berubah hanyalah elemen kode yang diberi metadata.

Bagian 9: Attribute pada Class

Selain property dan method, Attribute juga dapat ditempatkan langsung pada class. Contohnya cukup mudah ditemukan dalam aplikasi ASP.NET Core:

C#

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[Authorize]
public IActionResult GetProfile()
{
// ...
}
}

Contoh lainnya:

C#

[Serializable]
public class UserSession
{
public string Token { get; set; }
}

Pada contoh pertama, [ApiController] dan [Route] ditempatkan pada class UsersController, sedangkan [Authorize] ditempatkan pada method. Masing-masing Attribute memberikan informasi yang berbeda kepada ASP.NET Core.

Attribute pada Class dan Cakupannya

Perbedaan penting antara Attribute pada class dengan Attribute pada method atau property adalah cakupan konteksnya.

Misalnya:

C#

public class User
{
[Required]
public string Name { get; set; }
}

[Required] berada pada property Name, sehingga informasi tersebut berkaitan dengan property tersebut.

Sementara:

C#

[SomeAttribute]
public class User
{
}

Attribute berada pada level class. Framework yang membaca Attribute tersebut dapat menggunakan informasinya untuk menentukan bagaimana User harus diperlakukan. Namun, perlu diperhatikan bahwa Attribute pada class tidak otomatis berlaku pada semua isi class. Cakupannya ditentukan oleh kode yang membaca Attribute tersebut.

Misalnya, sebuah framework dapat memilih untuk menggunakan Attribute pada class sebagai konfigurasi untuk seluruh method di dalamnya. Framework lain mungkin hanya menggunakan Attribute tersebut untuk menentukan karakteristik class itu sendiri.

Jadi, lebih tepat jika dipahami seperti ini:

Plain Text

Attribute pada Property
└── informasi tentang property
Attribute pada Method
└── informasi tentang method
Attribute pada Class
└── informasi tentang class
└── framework dapat memilih
bagaimana informasi tersebut
diterapkan

Contoh [ApiController]

Dalam ASP.NET Core, salah satu contoh yang mudah ditemui adalah [ApiController]:

C#

[ApiController]
public class UsersController : ControllerBase
{
}

Attribute tersebut memberikan informasi kepada ASP.NET Core bahwa controller tersebut merupakan API controller. Informasi tersebut kemudian digunakan oleh framework untuk mengaktifkan sejumlah perilaku yang berkaitan dengan API controller, seperti automatic model validation dan inference tertentu dalam binding parameter.

Dengan demikian, Attribute tersebut tidak berisi logic untuk melakukan validasi atau membaca request body. [ApiController] hanya memberikan metadata. ASP.NET Core kemudian membaca metadata tersebut dan menerapkan perilaku yang sesuai.

Contoh [Route]

Attribute lain yang sering ditempatkan pada class adalah [Route]:

C#

[Route("api/[controller]")]
public class UsersController : ControllerBase
{
}

Dalam konteks routing ASP.NET Core, route pada class dapat menjadi bagian dari template URL yang digunakan oleh endpoint di dalam controller.

Misalnya:

C#

[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult GetUsers()
{
// ...
}
}

Secara sederhana dapat dibayangkan:

Plain Text

Controller
└── [Route("api/[controller]")]
Route dasar
+
[HttpGet]
Endpoint

Dengan cara ini, Attribute pada class dapat memberikan informasi yang kemudian digunakan bersama Attribute pada method.

[Authorize] pada Class

[Authorize] juga dapat ditempatkan pada level class:

C#

[Authorize]
public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult GetProfile()
{
// ...
}
[HttpGet("settings")]
public IActionResult GetSettings()
{
// ...
}
}

Dalam konteks ASP.NET Core, penggunaan [Authorize] pada controller berarti authorization requirement tersebut berlaku terhadap action di dalam controller tersebut. Dengan demikian, beberapa endpoint dapat memiliki aturan yang sama tanpa perlu menuliskan Attribute yang sama pada setiap method. Jika terdapat action tertentu yang memang harus dapat diakses tanpa authentication, [AllowAnonymous] dapat digunakan pada action tersebut.

Contohnya:

C#

[Authorize]
public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult GetProfile()
{
// Membutuhkan authorization
}
[AllowAnonymous]
[HttpGet("public")]
public IActionResult GetPublicProfile()
{
// Tidak memerlukan authorization
}
}

Di sini terlihat bahwa Attribute pada class dapat memberikan aturan yang berlaku lebih luas, sementara Attribute pada method dapat memberikan konfigurasi yang lebih spesifik.

Class sebagai Konteks yang Lebih Luas

Contoh tersebut menunjukkan pola yang cukup umum:

Plain Text

Class
├── Method A
├── Method B
└── Method C

Attribute pada class memberikan informasi pada konteks yang lebih luas:

Plain Text

[Authorize]
Class
├── Method A ← berlaku
├── Method B ← berlaku
└── Method C ← berlaku

Kemudian method tertentu dapat memiliki metadata tambahan:

Plain Text

[Authorize]
Class
├── Method A ← authorize
├── Method B ← authorize
└── [AllowAnonymous]
Method C ← pengecualian

Namun, sekali lagi, perilaku tersebut bukan merupakan aturan universal dari C#. C# hanya menyediakan kemampuan untuk menempelkan Attribute pada elemen tertentu. Bagaimana Attribute tersebut memengaruhi elemen lain ditentukan oleh framework atau kode yang membacanya.

Attribute Memiliki Target yang Berbeda

Attribute dalam C# dapat ditempatkan pada berbagai elemen program, tergantung pada Attribute tersebut mengizinkan target apa saja.

Beberapa target yang umum antara lain:

Plain Text

Assembly
└── informasi tingkat assembly
Class
└── informasi tentang type
Method
└── informasi tentang method
Property
└── informasi tentang property
Parameter
└── informasi tentang parameter

Bahkan terdapat target lain seperti field, event, constructor, return value, dan generic parameter. Jadi, posisi Attribute bukan sekadar masalah penempatan syntax. Posisi tersebut menentukan elemen mana yang memiliki metadata tersebut. Hal ini juga berkaitan dengan AttributeUsage, yang dapat digunakan untuk menentukan pada target mana sebuah Custom Attribute boleh digunakan.

Pembahasan mengenai AttributeUsage akan menjadi lebih menarik ketika mulai membuat Custom Attribute sendiri. Sebelum sampai ke sana, masih ada satu tingkat yang perlu diperhatikan: assembly. Jika class, method, dan property mewakili bagian-bagian di dalam sebuah program, assembly dapat dipandang sebagai konteks yang mencakup keseluruhan hasil kompilasi.

Bagian 10: Attribute pada Assembly

Setelah membahas Attribute pada property, method, dan class, masih ada satu level yang memiliki cakupan lebih luas, yaitu assembly. Jika Attribute pada property memberikan informasi tentang satu property, Attribute pada method memberikan informasi tentang satu method, dan Attribute pada class memberikan informasi tentang satu class, maka Assembly Attribute memberikan informasi tentang keseluruhan assembly.

Dalam konteks .NET, assembly merupakan hasil kompilasi aplikasi atau library yang umumnya berbentuk file .dll atau .exe.

Contoh sederhana Assembly Attribute:

C#

[assembly: CLSCompliant(true)]

Ada bagian yang terlihat berbeda dari Attribute yang sebelumnya dibahas:

Plain Text

[assembly: ...]
└── assembly

assembly: disebut attribute target specifier.

Specifier ini digunakan untuk memberi tahu compiler bahwa Attribute tersebut ditujukan pada assembly, bukan pada class, method, property, atau elemen kode lain di sekitarnya.

Tanpa target tersebut, compiler akan menentukan target berdasarkan konteks tempat Attribute ditulis.

Dengan menuliskan:

C#

[assembly: CLSCompliant(true)]

targetnya dinyatakan secara eksplisit:

Plain Text

CLSCompliant
Assembly
├── Class A
├── Class B
├── Class C
└── ...

Di Mana Assembly Attribute Diletakkan?

Secara historis, Assembly Attribute sering ditempatkan dalam file bernama:

Plain Text

Properties/AssemblyInfo.cs

Misalnya:

C#

using System.Reflection;
[assembly: AssemblyTitle("MyApplication")]
[assembly: AssemblyDescription("Aplikasi contoh")]
[assembly: AssemblyVersion("1.0.0.0")]

Namun, Attribute tersebut sebenarnya tidak harus berada di AssemblyInfo.cs. Selama syntax-nya valid dan berada dalam project yang sama, Assembly Attribute dapat ditulis di file .cs lainnya. Pada project .NET modern, banyak informasi assembly bahkan dapat dikonfigurasi langsung melalui file project .csproj.

Contohnya:

xml

<PropertyGroup>
<AssemblyTitle>MyApplication</AssemblyTitle>
<AssemblyDescription>Aplikasi contoh</AssemblyDescription>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
</PropertyGroup>

MSBuild kemudian dapat menghasilkan Assembly Attribute yang sesuai ketika project dikompilasi. Karena itu, AssemblyInfo.cs lebih tepat dipahami sebagai salah satu tempat untuk menuliskan Assembly Attribute, bukan tempat yang wajib digunakan.

Metadata tentang Assembly

Assembly Attribute biasanya digunakan untuk menyimpan informasi yang memang berkaitan dengan assembly secara keseluruhan.

Misalnya:

C#

[assembly: AssemblyTitle("MyApplication")]
[assembly: AssemblyDescription("Aplikasi contoh")]
[assembly: AssemblyVersion("1.0.0.0")]

Informasi tersebut memberikan metadata seperti:

Plain Text

Assembly
├── Title → MyApplication
├── Description → Aplikasi contoh
└── Version → 1.0.0.0

Metadata tersebut dapat dibaca oleh berbagai tools atau aplikasi. Misalnya, informasi versi assembly dapat digunakan oleh sistem untuk mengetahui versi library yang sedang digunakan.

Metadata assembly juga dapat dibaca secara programatis menggunakan Reflection:

C#

var assembly = typeof(MyApplication).Assembly;
var name = assembly.GetName();
Console.WriteLine(name.Name);
Console.WriteLine(name.Version);

Selain informasi yang berkaitan dengan identitas assembly, Attribute pada level assembly juga dapat digunakan untuk kebutuhan tertentu yang memang cakupannya berada pada level assembly.

Memilih Level Attribute yang Tepat

Sampai titik ini, beberapa level penempatan Attribute sudah terlihat:

Plain Text

Assembly
└── Class
├── Property
└── Method

Pemilihan level sebaiknya mengikuti cakupan informasi yang ingin diberikan.

Jika informasi hanya berkaitan dengan satu property:

C#

[Required]
public string Name { get; set; }

Attribute ditempatkan pada property.

Jika informasi hanya berkaitan dengan satu method:

C#

[HttpGet]
public IActionResult GetUsers()
{
// ...
}

Attribute ditempatkan pada method.

Jika informasi berkaitan dengan sebuah class:

C#

[ApiController]
public class UsersController : ControllerBase
{
}

Attribute ditempatkan pada class.

Sedangkan jika informasi berkaitan dengan keseluruhan assembly:

C#

[assembly: CLSCompliant(true)]

Attribute ditempatkan pada assembly.

Dengan cara ini, metadata berada sedekat mungkin dengan scope yang memang membutuhkannya.

Attribute Target Tidak Sama dengan Pengaruhnya

Ada satu hal penting yang perlu dibedakan. Target Attribute menentukan elemen tempat metadata tersebut ditempelkan. Namun, seberapa luas metadata tersebut memengaruhi perilaku aplikasi bergantung pada kode yang membacanya.

Misalnya, [Authorize] pada class dapat digunakan ASP.NET Core sebagai aturan yang berlaku pada action di dalam controller. Ini bukan karena C# secara otomatis menyebarkan Attribute dari class ke semua method. Framework-lah yang menentukan bagaimana Attribute tersebut ditafsirkan. Hal yang sama berlaku pada Assembly Attribute. Menempatkan Attribute pada assembly tidak berarti semua class di dalam assembly otomatis memiliki Attribute tersebut.

Secara sederhana:

Plain Text

Attribute Target
Menentukan tempat metadata ditempelkan
Attribute Consumer
Menentukan bagaimana metadata digunakan

C# menyediakan mekanisme untuk menyimpan metadata. Compiler, runtime, atau framework kemudian dapat membaca metadata tersebut dan menentukan perilakunya masing-masing.

Dengan demikian, seluruh level utama yang telah dibahas dapat dirangkum sebagai berikut:

Plain Text

Assembly
├── Class
│ │
│ ├── Property
│ │
│ └── Method
└── ...

Attribute dapat digunakan pada berbagai bagian tersebut selama Attribute yang digunakan memang mengizinkan target tersebut.

Setelah memahami di mana Attribute dapat ditempatkan, pembahasan berikutnya akan beralih pada pertanyaan lain yang tidak kalah penting: bagaimana Attribute dapat membawa data?

Contohnya:

C#

[Obsolete("Gunakan CalculateV2()")]

Teks "Gunakan CalculateV2()" bukan sekadar bagian dari syntax. Nilai tersebut dikirim sebagai argument dan akan menjadi bagian dari informasi yang dibawa oleh Attribute.

Untuk memahami mekanismenya, pembahasan berikutnya akan melihat constructor pada Attribute dan bagaimana parameter dapat diberikan ketika Attribute digunakan.

Bagian 11: Constructor pada Attribute

Pada bagian sebelumnya sudah dibahas bahwa Attribute pada dasarnya adalah sebuah class. Karena itu, Attribute juga dapat memiliki constructor, sama seperti class pada umumnya. Constructor inilah yang memungkinkan sebuah Attribute menerima data ketika digunakan.

Contoh yang cukup familiar adalah:

C#

[Obsolete("Gunakan CalculateV2()")]
public int Calculate(int a, int b)
{
return a + b;
}

Teks:

Plain Text

"Gunakan CalculateV2()"

yang berada di dalam tanda kurung bukan sekadar teks bebas. Nilai tersebut merupakan argument yang diberikan kepada constructor ObsoleteAttribute.

Constructor pada Attribute

Secara sederhana, ObsoleteAttribute dapat dibayangkan memiliki definisi seperti berikut:

C#

public class ObsoleteAttribute : Attribute
{
public string Message { get; }
public ObsoleteAttribute(string message)
{
Message = message;
}
}

Dengan constructor tersebut, Attribute dapat menerima sebuah string ketika digunakan:

C#

[Obsolete("Gunakan CalculateV2()")]

Secara konsep, hubungan keduanya dapat dibayangkan seperti:

Plain Text

[Obsolete("Gunakan CalculateV2()")]
ObsoleteAttribute(string message)
message = "Gunakan CalculateV2()"

Jadi, Attribute dapat membawa informasi tambahan melalui parameter constructor.

Apakah Ini Sama dengan new?

Dari sudut pandang konsep, syntax:

C#

[Obsolete("Gunakan CalculateV2()")]

dapat dibandingkan dengan:

C#

new ObsoleteAttribute("Gunakan CalculateV2()");

Keduanya sama-sama berkaitan dengan constructor ObsoleteAttribute.

Namun, keduanya bukan operasi yang sama dalam source code.

Ketika menggunakan:

C#

new ObsoleteAttribute("Gunakan CalculateV2()");

object dibuat secara eksplisit sebagai bagian dari logic program.

Sedangkan ketika menggunakan:

C#

[Obsolete("Gunakan CalculateV2()")]

compiler memperlakukan syntax tersebut sebagai deklarasi Attribute dan menyimpan informasi yang diperlukan ke dalam metadata assembly.

Ketika metadata tersebut kemudian dibaca, misalnya melalui Reflection, .NET dapat membuat instance Attribute berdasarkan informasi yang tersimpan.

Secara sederhana:

Plain Text

[Obsolete("...")]
│ compile
Metadata dalam Assembly
│ Reflection
ObsoleteAttribute instance

Perbedaan ini penting karena membantu membedakan antara deklarasi metadata dan pembuatan object secara langsung dalam logic program.

Attribute Dapat Memiliki Beberapa Constructor

Karena Attribute adalah class, Attribute juga dapat memiliki lebih dari satu constructor. Ini sama seperti class biasa yang mendukung constructor overloading.

ObsoleteAttribute, misalnya, menyediakan beberapa bentuk penggunaan:

C#

[Obsolete]

C#

[Obsolete("Gunakan CalculateV2()")]

C#

[Obsolete("Gunakan CalculateV2()", true)]

Ketiganya menggunakan constructor yang berbeda.

Secara konseptual:

Plain Text

[Obsolete]
└── ObsoleteAttribute()
[Obsolete("Gunakan CalculateV2()")]
└── ObsoleteAttribute(string)
[Obsolete("Gunakan CalculateV2()", true)]
└── ObsoleteAttribute(string, bool)

Pada bentuk pertama, tidak ada argument yang diberikan. Pada bentuk kedua, terdapat satu argument berupa string yang digunakan sebagai pesan. Pada bentuk ketiga, terdapat dua argument:

C#

"Gunakan CalculateV2()"
true

Argument bool tersebut menentukan apakah penggunaan elemen yang ditandai sebagai obsolete harus dianggap sebagai error oleh compiler.

Positional Parameter

Argument yang diberikan melalui constructor Attribute disebut positional parameter karena nilainya ditentukan berdasarkan posisi.

Misalnya:

C#

[Obsolete("Gunakan CalculateV2()", true)]

dapat dipahami sebagai:

Plain Text

Parameter pertama → "Gunakan CalculateV2()"
Parameter kedua → true

Urutannya penting.

Jika constructor didefinisikan seperti:

C#

public MyAttribute(string message, bool isError)
{
}

maka:

C#

[My("Pesan", true)]

berarti:

Plain Text

message = "Pesan"
isError = true

Sedangkan membalik urutannya:

C#

[My(true, "Pesan")]

tidak valid jika tidak terdapat constructor yang menerima parameter dengan urutan tersebut.

Constructor Bukan Satu-satunya Cara Memberikan Data

Constructor memang dapat digunakan untuk memberikan data kepada Attribute, tetapi bukan satu-satunya cara. Attribute juga dapat memiliki property atau field yang nilainya dapat ditentukan ketika Attribute digunakan.

Misalnya:

C#

public class AuthorAttribute : Attribute
{
public string Name { get; }
public string Role { get; set; }
public AuthorAttribute(string name)
{
Name = name;
}
}

Attribute tersebut dapat digunakan seperti:

C#

[Author("Budi", Role = "Developer")]
public class UserService
{
}

Di sini terdapat dua cara memberikan nilai:

Plain Text

"Budi"
└── constructor parameter
Role = "Developer"
└── named parameter

Jadi, terdapat dua konsep yang perlu dibedakan:

Positional parameter

Nilai diberikan melalui constructor:

C#

[Author("Budi")]

Named parameter

Nilai diberikan melalui property atau field:

C#

[Author("Budi", Role = "Developer")]

Keduanya dapat digunakan bersama.

Pembahasan mengenai named parameter akan dilanjutkan pada bagian berikutnya, termasuk aturan mengenai property dan field seperti apa yang dapat digunakan sebagai named parameter pada Attribute.

Bagian 12: Named Parameter

Pada bagian sebelumnya sudah dibahas positional parameter, yaitu nilai yang diberikan kepada Attribute melalui constructor dan ditentukan berdasarkan urutan parameternya. Selain positional parameter, Attribute juga memiliki mekanisme lain untuk menerima nilai, yaitu named parameter. Named parameter memungkinkan property atau field tertentu pada Attribute diisi secara langsung ketika Attribute digunakan.

Contoh Named Parameter

Perhatikan contoh berikut:

C#

[Obsolete(
"Deprecated",
IsError = true)]
public void OldMethod()
{
}

Pada contoh tersebut terdapat dua bentuk pemberian nilai:

Plain Text

"Deprecated"
└── positional parameter
IsError = true
└── named parameter

"Deprecated" dikirim ke constructor ObsoleteAttribute.

Sementara IsError = true digunakan untuk mengatur property IsError pada Attribute. Dengan kata lain, named parameter tidak membutuhkan constructor khusus yang menerima IsError sebagai parameter.

Contoh Custom Attribute

Agar perbedaannya lebih jelas, perhatikan Custom Attribute berikut:

C#

public class InfoAttribute : Attribute
{
public string Description { get; }
public string Author { get; set; }
public InfoAttribute(string description)
{
Description = description;
}
}

Class tersebut memiliki dua bagian penting:

Plain Text

Description
└── diisi melalui constructor
Author
└── diisi melalui named parameter

Constructor hanya menerima description:

C#

public InfoAttribute(string description)
{
Description = description;
}

Sementara Author merupakan public property yang dapat diisi setelah Attribute dibuat:

C#

public string Author { get; set; }

Attribute tersebut kemudian dapat digunakan seperti ini:

C#

[Info(
"Method ini menghitung total harga",
Author = "Tim Backend")]
public decimal CalculateTotal()
{
// ...
}

Nilai "Method ini menghitung total harga" merupakan positional parameter.

Sementara:

C#

Author = "Tim Backend"

merupakan named parameter.

Bagaimana Membayangkannya?

Secara konsep, penggunaan tersebut dapat dibayangkan mirip dengan object initializer:

C#

new InfoAttribute("Method ini menghitung total harga")
{
Author = "Tim Backend"
};

Namun, perlu diingat bahwa syntax Attribute bukan sekadar object initializer yang ditulis langsung di source code. Compiler memproses syntax Attribute sebagai metadata dan menyimpan informasi yang diperlukan dalam metadata assembly.

Object initializer di atas hanya digunakan sebagai analogi untuk memahami dari mana nilai Author berasal.

Secara sederhana:

Plain Text

[Info("Method ini menghitung total harga",
Author = "Tim Backend")]
├── constructor
└── property

Positional dan Named Parameter

Perbedaan keduanya dapat dirangkum sebagai berikut:

Positional ParameterNamed Parameter
Sumber nilaiConstructorProperty / field
Cara penulisanBerdasarkan posisiMenggunakan nama
Wajib?Bergantung pada constructorUmumnya opsional
UrutanMengikuti urutan parameter constructorTidak bergantung pada urutan
Contoh"Description"Author = "Budi"

Misalnya:

C#

[Info("Method untuk menghitung harga")]

hanya memberikan positional parameter.

Sedangkan:

C#

[Info(
"Method untuk menghitung harga",
Author = "Tim Backend")]

menggunakan keduanya.

Named Parameter Harus Menggunakan Property atau Field yang Valid

Tidak semua member dapat digunakan sebagai named parameter. Member tersebut harus berupa public property atau public field yang dapat diisi.

Contohnya:

C#

public class InfoAttribute : Attribute
{
public string Description { get; }
public string Author { get; set; }
public string Category;
public InfoAttribute(string description)
{
Description = description;
}
}

Author dapat digunakan sebagai named parameter karena merupakan public property yang memiliki setter:

C#

[Info("Perhitungan harga", Author = "Backend")]

Category juga dapat digunakan karena merupakan public field:

C#

[Info("Perhitungan harga", Category = "Finance")]

Sebaliknya, property yang hanya memiliki getter tidak dapat digunakan untuk menerima nilai:

C#

public string Description { get; }

Property tersebut bersifat read-only dari luar class. Nilainya harus diberikan melalui constructor.

Karena itu, desain seperti berikut cukup umum:

C#

public class InfoAttribute : Attribute
{
// Wajib
public string Description { get; }
// Opsional
public string Author { get; set; }
public InfoAttribute(string description)
{
Description = description;
}
}

Penggunaannya:

C#

[Info("Method untuk menghitung harga")]

atau:

C#

[Info(
"Method untuk menghitung harga",
Author = "Tim Backend")]

Dengan desain seperti ini, Description menjadi informasi yang harus tersedia ketika Attribute dibuat, sedangkan Author menjadi informasi tambahan yang hanya perlu diberikan jika memang diperlukan.

Kapan Menggunakan Positional dan Named Parameter?

Kombinasi keduanya dapat membantu menentukan mana informasi yang bersifat wajib dan mana yang bersifat opsional.

Misalnya sebuah Attribute membutuhkan nama sebagai informasi utama:

C#

public class AuthorAttribute : Attribute
{
public string Name { get; }
public string Role { get; set; }
public string Team { get; set; }
public AuthorAttribute(string name)
{
Name = name;
}
}

Penggunaannya dapat dibuat seperti:

C#

[Author("Budi")]

atau:

C#

[Author(
"Budi",
Role = "Software Engineer",
Team = "Backend")]

Dalam desain tersebut:

Plain Text

Name
└── informasi utama
→ constructor
Role
└── informasi tambahan
→ named parameter
Team
└── informasi tambahan
→ named parameter

Pendekatan ini membuat Custom Attribute dapat memiliki API yang cukup jelas: informasi penting diberikan melalui constructor, sedangkan informasi tambahan diberikan hanya ketika diperlukan. Dengan memahami positional dan named parameter, sudah ada cukup bekal untuk mulai membuat Custom Attribute sendiri.

Bagian 13: Membuat Custom Attribute

Sampai tahap ini, beberapa konsep penting sudah dibahas:

  • Attribute pada dasarnya adalah class.

  • Custom Attribute mewarisi System.Attribute.

  • Attribute dapat memiliki constructor.

  • Constructor dapat menerima positional parameter.

  • Property atau field tertentu dapat digunakan sebagai named parameter.

Konsep-konsep tersebut menjadi dasar untuk membuat Custom Attribute sendiri. Sebagai contoh, akan dibuat AuthorAttribute, yaitu Attribute sederhana yang digunakan untuk mencatat siapa yang menulis sebuah class, method, atau elemen kode lainnya. Informasi seperti ini dapat dimanfaatkan untuk berbagai kebutuhan, misalnya dokumentasi, audit, atau tooling internal.

Langkah 1 — Membuat Class yang Mewarisi Attribute

Langkah pertama adalah membuat class yang mewarisi System.Attribute:

C#

public class AuthorAttribute : Attribute
{
}

Inilah bagian yang membedakan Custom Attribute dari class biasa. Dengan mewarisi Attribute, AuthorAttribute dikenali sebagai class yang dapat digunakan sebagai Attribute:

C#

[Author]
public class ReportGenerator
{
}

Tanpa pewarisan tersebut, AuthorAttribute hanyalah class biasa dan tidak dapat digunakan dengan syntax Attribute seperti di atas.

Langkah 2 — Menambahkan Property untuk Menyimpan Data

Tujuan AuthorAttribute adalah menyimpan nama penulis.

Karena itu, diperlukan property untuk menyimpan informasi tersebut:

C#

public class AuthorAttribute : Attribute
{
public string Name { get; }
}

Property Name dibuat read-only:

C#

public string Name { get; }

Artinya, nilai Name dapat dibaca dari luar class, tetapi tidak dapat diubah melalui assignment biasa setelah object terbentuk.

Desain seperti ini cocok untuk informasi yang dianggap sebagai bagian utama dari identitas Attribute.

Langkah 3 — Menambahkan Constructor

Property Name sekarang sudah tersedia, tetapi belum ada cara untuk mengisinya.

Constructor dapat digunakan untuk tujuan tersebut:

C#

public class AuthorAttribute : Attribute
{
public string Name { get; }
public AuthorAttribute(string name)
{
Name = name;
}
}

Dengan constructor tersebut, AuthorAttribute membutuhkan sebuah string ketika dibuat.

Hal ini memungkinkan penggunaan seperti:

C#

[Author("John")]
public class ReportGenerator
{
}

Nilai:

Plain Text

"John"

diberikan kepada parameter name pada constructor.

Secara sederhana:

Plain Text

[Author("John")]
AuthorAttribute(string name)
Name = "John"

Dengan demikian, Name menjadi positional parameter dari Attribute tersebut.

Langkah 4 — Menggunakan Custom Attribute

Setelah class selesai dibuat, Attribute dapat digunakan pada elemen kode:

C#

[Author("John")]
public class ReportGenerator
{
[Author("Jane")]
public void GenerateReport()
{
Console.WriteLine("Laporan berhasil dibuat.");
}
}

Pada contoh tersebut, AuthorAttribute digunakan pada dua level:

Plain Text

ReportGenerator
├── [Author("John")]
└── GenerateReport()
└── [Author("Jane")]

Ini menunjukkan bahwa sebuah Custom Attribute dapat digunakan pada beberapa jenis elemen kode, selama target tersebut diizinkan oleh Attribute. Sampai tahap ini, belum ada pembatasan yang secara eksplisit menentukan apakah AuthorAttribute hanya boleh digunakan pada class, method, property, atau elemen lainnya. Pembatasan tersebut dapat dibuat menggunakan AttributeUsage, yang akan dibahas pada bagian berikutnya.

Langkah 5 — Menambahkan Named Parameter

AuthorAttribute saat ini hanya menyimpan nama penulis.

Agar lebih berguna, dapat ditambahkan informasi tambahan, misalnya tanggal review:

C#

public class AuthorAttribute : Attribute
{
public string Name { get; }
public string ReviewDate { get; set; }
public AuthorAttribute(string name)
{
Name = name;
}
}

Name tetap menjadi property read-only yang diisi melalui constructor.

Sementara ReviewDate memiliki setter sehingga dapat digunakan sebagai named parameter.

Contoh penggunaannya:

C#

[Author(
"John",
ReviewDate = "2025-01-10")]
public class ReportGenerator
{
}

Terdapat dua jenis parameter dalam penggunaan tersebut:

Plain Text

"John"
└── positional parameter
→ constructor
ReviewDate = "2025-01-10"
└── named parameter
→ property

Dengan desain ini, nama penulis menjadi informasi utama yang diberikan melalui constructor, sedangkan tanggal review menjadi informasi tambahan yang bersifat opsional.

Attribute Belum Melakukan Apa-Apa

Pada tahap ini, AuthorAttribute sudah dapat digunakan:

C#

[Author("John")]
public class ReportGenerator
{
}

Namun, ada satu hal penting yang perlu diperhatikan.

Attribute tersebut belum melakukan tindakan apa pun.

Misalnya, penggunaan:

C#

[Author("John")]

tidak otomatis membuat:

Plain Text

John

muncul di console.

Attribute hanya menyimpan informasi:

Plain Text

ReportGenerator
└── AuthorAttribute
├── Name = "John"
└── ReviewDate = ...

Agar informasi tersebut dapat digunakan, harus ada kode lain yang membaca Attribute tersebut. Salah satu mekanisme utama yang digunakan untuk membaca metadata tersebut adalah Reflection. Misalnya, kode lain nantinya dapat mencari AuthorAttribute pada ReportGenerator, kemudian membaca nilai:

C#

attribute.Name

atau:

C#

attribute.ReviewDate

Barulah informasi tersebut dapat dimanfaatkan untuk kebutuhan tertentu.

Pemisahan ini merupakan konsep penting:

Plain Text

Custom Attribute
│ menyimpan
Metadata
│ dibaca oleh
Reflection / Framework / Tooling
Perilaku atau informasi yang dibutuhkan

Jadi, ada dua tahap yang berbeda:

  1. Mendeklarasikan metadata menggunakan Attribute.

  2. Membaca dan memanfaatkan metadata menggunakan kode lain.

Tahap pertama sudah selesai dengan pembuatan AuthorAttribute. Tahap kedua akan mulai terlihat ketika membahas Reflection. Namun, sebelum masuk ke Reflection, masih ada satu hal yang perlu diperbaiki dari AuthorAttribute: saat ini Attribute tersebut dapat dipasang pada berbagai target tanpa aturan yang jelas. Bagaimana jika AuthorAttribute memang hanya dimaksudkan untuk class dan method?

Di sinilah AttributeUsage berperan. Dengan AttributeUsage, target yang diperbolehkan, apakah Attribute dapat digunakan berulang kali, dan bagaimana Attribute dapat diwariskan dapat ditentukan secara eksplisit.

Bagian 14: AttributeUsage

Pada bagian sebelumnya, AuthorAttribute sudah berhasil dibuat dan dapat digunakan pada elemen kode seperti class dan method. Namun, ada satu hal yang masih belum diatur: di mana Attribute tersebut boleh digunakan?

Tanpa aturan tambahan, sebuah Custom Attribute dapat memiliki target yang lebih luas daripada yang sebenarnya dibutuhkan.

Misalnya, AuthorAttribute dirancang untuk mencatat penulis sebuah class atau method. Akan tetapi, tanpa pembatasan, penggunaan seperti berikut juga dapat dimungkinkan:

C#

public class Report
{
[Author("John")]
public string Name { get; set; }
}

Padahal, jika informasi author memang hanya dimaksudkan untuk class dan method, penggunaan pada property seharusnya tidak diperbolehkan. Di sinilah AttributeUsage berperan.

Apa Itu AttributeUsage?

AttributeUsage adalah Attribute bawaan .NET yang digunakan untuk menentukan aturan penggunaan Custom Attribute. Dengan kata lain, AttributeUsage dapat dipandang sebagai:

Attribute untuk mengatur Attribute.

Contohnya:

C#

[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorAttribute : Attribute
{
public string Name { get; }
public AuthorAttribute(string name)
{
Name = name;
}
}

Sekarang AuthorAttribute hanya boleh digunakan pada:

  • class

  • method

Contoh yang valid:

C#

[Author("John")]
public class ReportGenerator
{
[Author("Jane")]
public void GenerateReport()
{
}
}

Sedangkan penggunaan pada property:

C#

public class Report
{
[Author("John")]
public string Name { get; set; }
}

akan ditolak oleh compiler karena AttributeTargets.Property tidak termasuk dalam target yang diizinkan.

Pembatasan seperti ini berguna karena kesalahan dapat diketahui sejak proses kompilasi, bukan setelah aplikasi berjalan.

AttributeTargets

Property pertama yang paling penting pada AttributeUsage adalah AttributeTargets. Property ini menentukan elemen kode mana yang boleh diberi Custom Attribute.

Beberapa nilai yang umum digunakan antara lain:

C#

AttributeTargets.Class
AttributeTargets.Method
AttributeTargets.Property
AttributeTargets.Field
AttributeTargets.Parameter
AttributeTargets.Assembly
AttributeTargets.All

Misalnya, jika sebuah Attribute hanya boleh digunakan pada method:

C#

[AttributeUsage(AttributeTargets.Method)]
public class AuditAttribute : Attribute
{
}

Maka penggunaan berikut valid:

C#

[Audit]
public void CreateUser()
{
}

Tetapi penggunaan pada class:

C#

[Audit]
public class UserService
{
}

tidak diperbolehkan.

Menggabungkan Beberapa Target

Sebuah Attribute tidak harus dibatasi hanya pada satu target.

Beberapa target dapat digabungkan menggunakan operator |:

C#

[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorAttribute : Attribute
{
}

Operator | di sini adalah bitwise OR.

Hal ini dapat digunakan karena nilai-nilai dalam AttributeTargets dirancang sebagai flag yang dapat dikombinasikan.

Secara sederhana:

Plain Text

Class
├── OR ──► Class | Method
Method

Hasilnya berarti:

Plain Text

AuthorAttribute
├── boleh pada Class
└── boleh pada Method

Cara yang sama dapat digunakan untuk target lainnya:

C#

[AttributeUsage(
AttributeTargets.Class |
AttributeTargets.Method |
AttributeTargets.Property)]
public class InfoAttribute : Attribute
{
}

Attribute tersebut sekarang dapat digunakan pada ketiga target tersebut.

AllowMultiple

Selain menentukan lokasi pemasangan, AttributeUsage juga dapat menentukan apakah Attribute yang sama boleh dipasang lebih dari satu kali pada elemen yang sama. Pengaturannya dilakukan melalui property AllowMultiple.

Nilai default-nya adalah:

C#

false

Artinya, secara default sebuah Attribute hanya boleh digunakan satu kali pada elemen yang sama.

Misalnya:

C#

[AttributeUsage(AttributeTargets.Method)]
public class TagAttribute : Attribute
{
public string Value { get; }
public TagAttribute(string value)
{
Value = value;
}
}

Penggunaan berikut:

C#

[Tag("penting")]
public void ProcessData()
{
}

valid.

Namun jika ditulis dua kali:

C#

[Tag("penting")]
[Tag("eksperimental")]
public void ProcessData()
{
}

akan menghasilkan compile error karena AllowMultiple secara default bernilai false.

Jika memang sebuah Attribute dirancang agar dapat digunakan berkali-kali, AllowMultiple dapat diaktifkan:

C#

[AttributeUsage(
AttributeTargets.Method,
AllowMultiple = true)]
public class TagAttribute : Attribute
{
public string Value { get; }
public TagAttribute(string value)
{
Value = value;
}
}

Sekarang penggunaan berikut diperbolehkan:

C#

[Tag("penting")]
[Tag("eksperimental")]
public void ProcessData()
{
}

Hal ini berguna untuk Attribute yang memang dapat memiliki beberapa nilai pada elemen yang sama.

Misalnya:

Plain Text

Method
├── [Tag("penting")]
└── [Tag("eksperimental")]

Satu method dapat memiliki lebih dari satu metadata Tag.

Inherited

Property ketiga adalah Inherited. Property ini menentukan apakah Attribute pada class dapat diwariskan ke class turunannya.

Nilai default-nya adalah:

C#

true

Misalnya terdapat Custom Attribute:

C#

[AttributeUsage(
AttributeTargets.Class,
Inherited = true)]
public class ModuleAttribute : Attribute
{
public string Name { get; }
public ModuleAttribute(string name)
{
Name = name;
}
}

Kemudian Attribute tersebut digunakan pada base class:

C#

[Module("Reporting")]
public class BaseReport
{
}
public class MonthlyReport : BaseReport
{
}

Karena Inherited = true, Attribute tersebut dapat dianggap diwarisi oleh MonthlyReport ketika diperiksa menggunakan mekanisme Reflection yang memperhitungkan inheritance.

Secara konseptual:

Plain Text

BaseReport
└── [Module("Reporting")]
│ inheritance
MonthlyReport
└── [Module("Reporting")]

Sebaliknya, jika Inherited diatur menjadi false:

C#

[AttributeUsage(
AttributeTargets.Class,
Inherited = false)]
public class ModuleAttribute : Attribute
{
public string Name { get; }
public ModuleAttribute(string name)
{
Name = name;
}
}

maka Attribute tersebut tidak diwariskan ke class turunannya.

Plain Text

BaseReport
└── [Module("Reporting")]
MonthlyReport
└── tidak mewarisi ModuleAttribute

Perlu diperhatikan bahwa Inherited berkaitan dengan mekanisme inheritance Attribute ketika metadata diperiksa. C# tidak menyalin Attribute tersebut ke source code class turunannya.

Dengan kata lain, MonthlyReport tidak tiba-tiba memiliki deklarasi:

C#

[Module("Reporting")]

di dalam source code.

Reflection-lah yang dapat memperhitungkan aturan inheritance tersebut ketika mencari Attribute.

Tiga Pertanyaan Utama AttributeUsage

Dengan tiga property tersebut, AttributeUsage sebenarnya menjawab tiga pertanyaan penting:

Plain Text

AttributeUsage
├── AttributeTargets
│ └── Di mana Attribute boleh digunakan?
├── AllowMultiple
│ └── Boleh digunakan lebih dari sekali?
└── Inherited
└── Apakah Attribute diwariskan?

Misalnya:

C#

[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Method,
AllowMultiple = true,
Inherited = false)]
public class AuthorAttribute : Attribute
{
public string Name { get; }
public AuthorAttribute(string name)
{
Name = name;
}
}

Definisi tersebut dapat dibaca sebagai:

Plain Text

AuthorAttribute
├── Target
│ ├── Class
│ └── Method
├── Multiple
│ └── Ya
└── Inherited
└── Tidak

Dengan begitu, aturan penggunaan Attribute dapat terlihat langsung dari definisinya.

Mengapa AttributeUsage Penting?

AttributeUsage bukan hanya masalah membatasi syntax. Ia juga membantu membuat maksud sebuah Custom Attribute menjadi lebih jelas.

Bandingkan dua definisi berikut:

C#

public class AuthorAttribute : Attribute
{
}

dengan:

C#

[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorAttribute : Attribute
{
}

Pada definisi kedua, tanpa melihat penggunaan Attribute di seluruh project pun sudah dapat diketahui bahwa AuthorAttribute memang dirancang untuk class dan method. Informasi tersebut menjadi bagian dari kontrak Custom Attribute. Selain itu, pembatasan target membantu mencegah kesalahan sejak compile time:

Plain Text

Custom Attribute
AttributeUsage
├── Target valid?
│ │
│ ├── Ya ──► Kompilasi
│ │
│ └── Tidak ──► Compile error
├── Multiple diperbolehkan?
└── Inheritance diperbolehkan?

Dengan demikian, AttributeUsage melengkapi proses pembuatan Custom Attribute. Sampai tahap ini sudah tersedia seluruh fondasi untuk membuat Attribute yang terstruktur:

Plain Text

System.Attribute
Custom Attribute
├── Constructor
│ └── Positional parameter
├── Property / Field
│ └── Named parameter
└── AttributeUsage
├── Target
├── AllowMultiple
└── Inherited

Namun, seluruh informasi tersebut masih bersifat metadata. Pertanyaan berikutnya menjadi lebih menarik: bagaimana kode program dapat mengambil kembali Attribute yang sudah ditempelkan pada class, method, atau property?

Bagian 15: Reflection

Sampai tahap ini, Attribute sudah dibahas dari berbagai sisi: apa itu Attribute, bagaimana membuat Custom Attribute, bagaimana memberikan parameter, hingga bagaimana membatasi penggunaannya dengan AttributeUsage.

Namun, masih ada satu pertanyaan penting:

Bagaimana informasi yang disimpan oleh Attribute bisa dibaca kembali oleh program?

Attribute sendiri bersifat pasif. Menuliskan:

C#

[Author("John")]
public class ReportGenerator
{
}

tidak otomatis membuat informasi "John" digunakan oleh aplikasi.

Diperlukan mekanisme yang dapat menemukan class tersebut, memeriksa Attribute yang menempel padanya, lalu membaca nilai yang tersimpan di dalam Attribute. Dalam .NET, salah satu mekanisme utama untuk melakukan hal tersebut adalah Reflection.

Apa Itu Reflection?

Secara sederhana, Reflection adalah kemampuan program .NET untuk memeriksa struktur dan metadata program saat runtime. Dengan Reflection, sebuah program dapat mencari informasi seperti:

Plain Text

Assembly
├── Class apa saja yang tersedia?
└── Class
├── Property apa saja?
├── Method apa saja?
├── Constructor apa saja?
└── Attribute apa saja?

Program bahkan dapat melakukan hal tersebut tanpa harus mengetahui seluruh informasi tersebut secara eksplisit ketika source code ditulis.

Misalnya, terdapat class:

C#

public class ReportGenerator
{
public string Name { get; set; }
public void Generate()
{
}
}

Dengan Reflection, struktur tersebut dapat diperiksa saat aplikasi berjalan:

Plain Text

ReportGenerator
├── Property
│ └── Name
└── Method
└── Generate()

Jika class tersebut memiliki Attribute:

C#

[Author("John")]
public class ReportGenerator
{
}

Reflection juga dapat digunakan untuk menemukan AuthorAttribute tersebut.

Pembahasan mengenai cara kerja Reflection, konsep metadata, serta contoh implementasinya dapat ditemukan pada artikel Memahami Reflection di C#: Cara Kerja, Penggunaan, dan Contoh Implementasi.

typeof()

Salah satu cara paling sederhana untuk mulai menggunakan Reflection adalah typeof().

typeof() digunakan untuk mendapatkan objek Type berdasarkan tipe yang ditulis secara eksplisit dalam source code.

Contohnya:

C#

Type type = typeof(ReportGenerator);

Di sini ReportGenerator sudah diketahui ketika source code ditulis.

Hasil dari typeof(ReportGenerator) adalah objek bertipe:

C#

System.Type

Objek tersebut kemudian dapat digunakan untuk memeriksa informasi tentang ReportGenerator.

Misalnya:

C#

Console.WriteLine(type.Name);

akan menghasilkan nama type:

Plain Text

ReportGenerator

Dari objek Type inilah eksplorasi Reflection dapat dilanjutkan.

GetType()

Cara lain untuk mendapatkan Type adalah menggunakan GetType() pada object.

Misalnya:

C#

ReportGenerator report = new ReportGenerator();
Type type = report.GetType();

Perbedaannya dengan typeof() dapat dilihat seperti ini:

Plain Text

typeof(ReportGenerator)
└── Type diketahui dari source code
report.GetType()
└── Type diperoleh dari object saat runtime

GetType() sangat berguna ketika object yang sedang diperiksa baru diketahui tipe konkretnya saat program berjalan.

Contohnya:

C#

object value = new ReportGenerator();
Type type = value.GetType();

Walaupun variabel value dideklarasikan sebagai object, saat runtime object yang sebenarnya adalah ReportGenerator. GetType() dapat menemukan tipe runtime tersebut.

typeof() vs GetType()

Keduanya menghasilkan objek Type, tetapi sumber informasinya berbeda.

C#

Type type1 = typeof(ReportGenerator);
ReportGenerator report = new ReportGenerator();
Type type2 = report.GetType();

Secara sederhana:

Plain Text

typeof()
└── "Tipe ini sudah diketahui."
GetType()
└── "Tipe object ini diketahui saat runtime."

Keduanya kemudian dapat digunakan untuk melakukan eksplorasi menggunakan Reflection.

Type

Type merupakan salah satu class terpenting dalam Reflection. Objek Type merepresentasikan informasi tentang sebuah tipe, termasuk class, struct, interface, enum, dan tipe lainnya.

Misalnya:

C#

Type type = typeof(ReportGenerator);

Dari type, berbagai informasi dapat diperiksa.

Nama type:

C#

type.Name

Namespace:

C#

type.Namespace

Property:

C#

type.GetProperties()

Method:

C#

type.GetMethods()

Constructor:

C#

type.GetConstructors()

Attribute:

C#

type.GetCustomAttributes()

Dengan demikian, Type dapat dianggap sebagai pintu masuk utama untuk mengeksplorasi sebuah tipe melalui Reflection.

MemberInfo

Sebuah class tidak hanya memiliki method. Sebuah type dapat memiliki berbagai jenis member, seperti:

Plain Text

Type
├── Method
├── Property
├── Field
├── Constructor
├── Event
└── ...

.NET menyediakan class dasar bernama MemberInfo untuk merepresentasikan informasi mengenai member-member tersebut.

Beberapa class yang lebih spesifik mewarisi MemberInfo, misalnya:

Plain Text

MemberInfo
├── MethodInfo
├── PropertyInfo
├── FieldInfo
├── ConstructorInfo
└── EventInfo

Karena itu, MemberInfo dapat dipandang sebagai representasi umum dari sebuah member, sedangkan class seperti MethodInfo dan PropertyInfo menyediakan informasi yang lebih spesifik.

PropertyInfo

Jika ingin memeriksa property dari sebuah class, PropertyInfo dapat digunakan.

Misalnya:

C#

Type type = typeof(ReportGenerator);
PropertyInfo[] properties = type.GetProperties();

Jika class-nya:

C#

public class ReportGenerator
{
public string Name { get; set; }
public int Total { get; set; }
}

maka GetProperties() akan menghasilkan informasi mengenai:

Plain Text

PropertyInfo[]
├── Name
└── Total

Setiap PropertyInfo dapat diperiksa lebih lanjut.

Misalnya:

C#

foreach (PropertyInfo property in properties)
{
Console.WriteLine(property.Name);
}

Hasilnya:

Plain Text

Name
Total

PropertyInfo juga dapat digunakan untuk memeriksa Attribute yang menempel pada property tersebut.

Misalnya:

C#

property.GetCustomAttributes();

Dengan demikian, pola yang sebelumnya dibahas mulai terlihat:

Plain Text

Type
└── PropertyInfo
└── Attribute

MethodInfo

MethodInfo memiliki konsep yang serupa, tetapi digunakan untuk merepresentasikan method.

Contohnya:

C#

Type type = typeof(ReportGenerator);
MethodInfo[] methods = type.GetMethods();

Jika class memiliki:

C#

public class ReportGenerator
{
public void Generate()
{
}
public void Cancel()
{
}
}

maka Reflection dapat menemukan:

Plain Text

MethodInfo[]
├── Generate()
└── Cancel()

Setiap MethodInfo dapat memberikan informasi seperti:

  • nama method

  • return type

  • parameter

  • accessibility

  • Attribute yang menempel

  • dan informasi lainnya

Contohnya:

C#

foreach (MethodInfo method in methods)
{
Console.WriteLine(method.Name);
}

Selain membaca informasi, MethodInfo juga dapat digunakan untuk memanggil method secara dinamis melalui Reflection. Namun, kemampuan tersebut bukan fokus utama pembahasan Attribute. Yang lebih penting untuk saat ini adalah memahami bahwa MethodInfo dapat menjadi titik untuk menemukan Attribute yang menempel pada sebuah method.

Menghubungkan Semuanya

Sampai di sini, beberapa komponen utama Reflection sudah diperkenalkan:

Plain Text

typeof() / GetType()
Type
├── GetProperties()
│ │
│ ▼
│ PropertyInfo
└── GetMethods()
MethodInfo

Kemudian dari PropertyInfo, MethodInfo, atau Type, Attribute dapat diperiksa:

Plain Text

Type
├── Attribute pada Class
├── PropertyInfo
│ └── Attribute pada Property
└── MethodInfo
└── Attribute pada Method

Inilah yang menjadi dasar bagaimana metadata Attribute dapat dibaca oleh program.

Contoh Sederhana

Misalnya terdapat Custom Attribute:

C#

[AttributeUsage(AttributeTargets.Class)]
public class AuthorAttribute : Attribute
{
public string Name { get; }
public AuthorAttribute(string name)
{
Name = name;
}
}

Kemudian digunakan pada class:

C#

[Author("John")]
public class ReportGenerator
{
}

Reflection dapat digunakan untuk mendapatkan Type:

C#

Type type = typeof(ReportGenerator);

Kemudian Attribute dapat dicari:

C#

var attributes = type.GetCustomAttributes();

Dari sini program dapat menemukan AuthorAttribute dan membaca nilai yang tersimpan di dalamnya.

Alurnya dapat digambarkan seperti ini:

Plain Text

ReportGenerator
│ typeof()
Type
│ GetCustomAttributes()
AuthorAttribute
Name = "John"

Inilah bagian yang sebelumnya belum terlihat.

Saat Attribute ditulis:

C#

[Author("John")]

informasinya disimpan sebagai metadata.

Saat Reflection digunakan:

C#

type.GetCustomAttributes()

metadata tersebut dapat ditemukan dan digunakan kembali oleh program.

Jadi, hubungan antara Attribute dan Reflection dapat diringkas menjadi:

Plain Text

Attribute
│ menyimpan
Metadata
│ dibaca melalui
Reflection
Type / MethodInfo / PropertyInfo
Attribute
Data dapat digunakan

Dengan memahami alur ini, Attribute tidak lagi terlihat seperti syntax khusus yang bekerja secara ajaib. Attribute hanyalah cara untuk menyatakan metadata, sedangkan Reflection menyediakan salah satu mekanisme untuk menemukan dan membaca metadata tersebut.

Bagian 16: Membaca Attribute dengan Reflection

Pada bagian sebelumnya, Reflection sudah diperkenalkan sebagai mekanisme yang memungkinkan program memeriksa struktur dan metadata sebuah type saat runtime. Sekarang saatnya melihat proses tersebut secara langsung.

Ada tiga operasi yang paling sering digunakan ketika berhadapan dengan Attribute:

Plain Text

IsDefined()
GetCustomAttribute()
GetCustomAttributes()

Ketiganya memiliki tujuan yang sedikit berbeda.

IsDefined()

IsDefined() digunakan ketika hanya perlu mengetahui apakah sebuah elemen memiliki Attribute tertentu.

Hasilnya berupa bool:

C#

true

atau:

C#

false

Contohnya:

C#

bool hasAuthor =
Attribute.IsDefined(
type,
typeof(AuthorAttribute));

Jika ReportGenerator memiliki AuthorAttribute, hasilnya adalah true.

Cara berpikirnya sederhana:

Plain Text

"Apakah Attribute ini ada?"
├── Ya → true
└── Tidak → false

Method ini cocok ketika data yang tersimpan di dalam Attribute tidak diperlukan.

GetCustomAttribute()

Jika bukan hanya keberadaannya yang diperlukan, tetapi juga isi Attribute, GetCustomAttribute() dapat digunakan.

Contohnya:

C#

AuthorAttribute? attribute =
type.GetCustomAttribute<AuthorAttribute>();

Jika Attribute ditemukan, method tersebut mengembalikan instance AuthorAttribute. Jika tidak ditemukan, hasilnya adalah null.

Dengan demikian:

Plain Text

IsDefined()
└── Hanya menjawab: ada atau tidak?
GetCustomAttribute()
└── Mengambil: Attribute beserta datanya

Karena menggunakan generic version, hasilnya langsung bertipe AuthorAttribute. Tidak diperlukan cast manual dari Attribute.

GetCustomAttributes()

Jika sebuah elemen dapat memiliki beberapa Attribute dan seluruhnya perlu diperiksa, GetCustomAttributes() dapat digunakan.

Contohnya:

C#

IEnumerable<Attribute> attributes =
type.GetCustomAttributes();

Atau jika hanya ingin mencari jenis Attribute tertentu:

C#

IEnumerable<AuthorAttribute> authors =
type.GetCustomAttributes<AuthorAttribute>();

Method ini mengembalikan kumpulan Attribute, bukan satu instance.

Hal ini menjadi penting ketika sebuah Attribute menggunakan:

C#

AllowMultiple = true

Misalnya:

C#

[Tag("penting")]
[Tag("eksperimental")]
public void ProcessData()
{
}

GetCustomAttributes<TagAttribute>() dapat digunakan untuk mengambil kedua Attribute tersebut.

Perbedaan Ketiganya

Secara sederhana, ketiga operasi tersebut dapat dibedakan seperti ini:

MethodTujuanHasil
IsDefined()Mengecek keberadaan Attributebool
GetCustomAttribute<T>()Mengambil satu AttributeT?
GetCustomAttributes<T>()Mengambil semua Attribute sejenisKumpulan T

Pemilihannya bergantung pada kebutuhan.

Jika hanya diperlukan pengecekan:

C#

if (type.IsDefined(typeof(AuthorAttribute)))
{
// ...
}

Jika hanya ada satu Attribute yang ingin diambil:

C#

AuthorAttribute? author =
type.GetCustomAttribute<AuthorAttribute>();

Jika terdapat kemungkinan beberapa Attribute:

C#

IEnumerable<AuthorAttribute> authors =
type.GetCustomAttributes<AuthorAttribute>();

Contoh Lengkap

Sekarang seluruh konsep tersebut dapat digabungkan dalam satu contoh.

C#

using System;
using System.Reflection;
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorAttribute : Attribute
{
public string Name { get; }
public string? ReviewDate { get; set; }
public AuthorAttribute(string name)
{
Name = name;
}
}
[Author("John", ReviewDate = "2025-01-10")]
public class ReportGenerator
{
[Author("Jane")]
public void GenerateReport()
{
Console.WriteLine("Laporan berhasil dibuat.");
}
}
public class Program
{
public static void Main()
{
Type type = typeof(ReportGenerator);
// 1. Mengecek apakah class memiliki AuthorAttribute
bool hasAuthor =
type.IsDefined(typeof(AuthorAttribute));
Console.WriteLine(
$"Punya AuthorAttribute? {hasAuthor}");
// 2. Mengambil AuthorAttribute dari class
AuthorAttribute? classAttribute =
type.GetCustomAttribute<AuthorAttribute>();
Console.WriteLine(
$"Penulis class: {classAttribute?.Name}, " +
$"Review: {classAttribute?.ReviewDate}");
// 3. Mengambil AuthorAttribute dari method
MethodInfo? method =
type.GetMethod("GenerateReport");
AuthorAttribute? methodAttribute =
method?.GetCustomAttribute<AuthorAttribute>();
Console.WriteLine(
$"Penulis method: {methodAttribute?.Name}");
}
}

Output-nya:

Plain Text

Punya AuthorAttribute? True
Penulis class: John, Review: 2025-01-10
Penulis method: Jane

Membaca Attribute pada Class

Perhatikan bagian pertama:

C#

Type type = typeof(ReportGenerator);

Baris tersebut mendapatkan objek Type yang merepresentasikan ReportGenerator.

Karena Attribute dipasang pada class:

C#

[Author("John", ReviewDate = "2025-01-10")]
public class ReportGenerator
{
}

Attribute dapat dicari langsung dari objek Type:

C#

AuthorAttribute? classAttribute =
type.GetCustomAttribute<AuthorAttribute>();

Hasilnya adalah instance AuthorAttribute yang berasal dari deklarasi pada ReportGenerator.

Nilainya dapat dibaca:

C#

classAttribute?.Name

dan:

C#

classAttribute?.ReviewDate

Hasilnya:

Plain Text

Name → John
ReviewDate → 2025-01-10

Membaca Attribute pada Method

Attribute pada method berada di lokasi yang berbeda:

C#

[Author("Jane")]
public void GenerateReport()
{
}

Karena itu, Type saja belum cukup.

Pertama, method perlu ditemukan:

C#

MethodInfo? method =
type.GetMethod("GenerateReport");

Sekarang sudah tersedia objek MethodInfo yang merepresentasikan GenerateReport.

Attribute dapat dicari dari objek tersebut:

C#

AuthorAttribute? methodAttribute =
method?.GetCustomAttribute<AuthorAttribute>();

Hasilnya adalah Attribute yang dipasang pada method:

Plain Text

GenerateReport()
└── [Author("Jane")]

Sehingga:

C#

methodAttribute?.Name

menghasilkan:

Plain Text

Jane

Attribute pada Class dan Method Adalah Instance yang Berbeda

Ada satu detail penting dari contoh ini.

Terdapat dua penggunaan AuthorAttribute:

C#

[Author("John")]
public class ReportGenerator
{
[Author("Jane")]
public void GenerateReport()
{
}
}

Walaupun keduanya menggunakan class Attribute yang sama, keduanya merupakan instance metadata yang berbeda.

Secara konseptual:

Plain Text

ReportGenerator
└── AuthorAttribute
└── Name = "John"
GenerateReport()
└── AuthorAttribute
└── Name = "Jane"

Karena itu:

C#

classAttribute.Name

menghasilkan:

Plain Text

John

sedangkan:

C#

methodAttribute.Name

menghasilkan:

Plain Text

Jane

Nilai keduanya tidak saling memengaruhi.

Dari Metadata Menjadi Logic Program

Pada titik ini, siklus Attribute sudah dapat terlihat secara lengkap.

1. Attribute didefinisikan

C#

public class AuthorAttribute : Attribute
{
public string Name { get; }
public AuthorAttribute(string name)
{
Name = name;
}
}

2. Attribute dipasang

C#

[Author("John")]
public class ReportGenerator
{
}

3. Compiler menyimpan informasi tersebut sebagai metadata

Plain Text

ReportGenerator
└── AuthorAttribute
└── Name = "John"

4. Reflection mencari metadata

C#

Type type = typeof(ReportGenerator);
AuthorAttribute? attribute =
type.GetCustomAttribute<AuthorAttribute>();

5. Program menggunakan informasi tersebut

C#

Console.WriteLine(attribute?.Name);

Hasilnya:

Plain Text

John

Keseluruhan proses dapat diringkas menjadi:

Plain Text

Source Code
│ [Author("John")]
Attribute Metadata
│ Reflection
AuthorAttribute
│ Name
"John"
Logic Program

Di sinilah hubungan antara Attribute dan Reflection menjadi jelas. Attribute menyediakan cara untuk menyatakan informasi secara deklaratif, sedangkan Reflection menyediakan mekanisme untuk menemukan dan membaca informasi tersebut saat runtime.

Pola sederhana ini ternyata menjadi fondasi bagi banyak framework di ekosistem .NET. ASP.NET Core, Entity Framework, library serialization, framework testing, dan berbagai library lainnya dapat menggunakan pola yang serupa untuk membaca metadata dan menentukan perilaku aplikasi.

Bagian: 17: Studi Kasus Sederhana

Setelah memahami cara membuat Attribute dan membacanya menggunakan Reflection, konsep-konsep tersebut dapat digabungkan menjadi sebuah contoh yang lebih menyerupai penggunaan nyata.

Studi kasus kali ini akan membuat mini framework sederhana yang dapat:

  1. menerima sebuah class,

  2. membaca Attribute yang terpasang pada class tersebut,

  3. membaca Attribute pada setiap property,

  4. kemudian menghasilkan informasi berdasarkan metadata yang ditemukan.

Contoh ini memang jauh lebih sederhana dibanding framework sebenarnya, tetapi pola dasarnya sama: framework tidak perlu mengetahui aturan setiap class secara hard-coded karena informasi tersebut dinyatakan melalui Attribute.

Skenario

Misalkan terdapat model ProductModel dengan informasi berikut:

  • class memiliki route tertentu,

  • class memiliki informasi author,

  • setiap property memiliki label yang lebih ramah untuk ditampilkan,

  • beberapa property wajib diisi,

  • beberapa property bersifat opsional.

Untuk menyimpan informasi tersebut, akan dibuat empat Custom Attribute:

Plain Text

[Route]
[Author]
[Display]
[Required]

Mendefinisikan Custom Attribute

Langkah pertama adalah membuat keempat Custom Attribute tersebut.

C#

using System;
using System.Reflection;
[AttributeUsage(AttributeTargets.Class)]
public class RouteAttribute : Attribute
{
public string Path { get; }
public RouteAttribute(string path)
{
Path = path;
}
}
[AttributeUsage(AttributeTargets.Property)]
public class DisplayAttribute : Attribute
{
public string Label { get; }
public DisplayAttribute(string label)
{
Label = label;
}
}
[AttributeUsage(AttributeTargets.Class)]
public class AuthorAttribute : Attribute
{
public string Name { get; }
public AuthorAttribute(string name)
{
Name = name;
}
}
[AttributeUsage(AttributeTargets.Property)]
public class RequiredAttribute : Attribute
{
}

Masing-masing Attribute memiliki tujuan yang berbeda:

AttributeTargetFungsi
RouteAttributeClassMenyimpan informasi route
AuthorAttributeClassMenyimpan informasi author
DisplayAttributePropertyMenyimpan label property
RequiredAttributePropertyMenandai property sebagai wajib

Perhatikan bahwa RequiredAttribute tidak membutuhkan property atau constructor parameter.

Keberadaan Attribute itu sendiri sudah cukup sebagai penanda:

C#

[Required]
public string Name { get; set; }

Dalam kasus seperti ini, Attribute berfungsi seperti sebuah flag:

Plain Text

[Required]
└── Property ini wajib

Menerapkan Attribute pada Model

Setelah Custom Attribute tersedia, metadata dapat ditempelkan pada ProductModel:

C#

[Route("api/products")]
[Author("Tim Backend")]
public class ProductModel
{
[Display("Nama Produk")]
[Required]
public string Name { get; set; }
[Display("Harga")]
[Required]
public decimal Price { get; set; }
[Display("Deskripsi")]
public string Description { get; set; }
}

Sekarang informasi tentang model tersebut sudah dinyatakan langsung pada source code.

Jika dibaca secara konseptual:

Plain Text

ProductModel
├── [Route("api/products")]
├── [Author("Tim Backend")]
├── Name
│ ├── [Display("Nama Produk")]
│ └── [Required]
├── Price
│ ├── [Display("Harga")]
│ └── [Required]
└── Description
└── [Display("Deskripsi")]

Ada hal menarik di sini.

ProductModel tidak memiliki property bernama Route, Author, atau Required. Informasi tersebut berada di luar struktur data utama dan disimpan sebagai metadata melalui Attribute.

Membuat ModelInspector

Sekarang diperlukan kode yang membaca metadata tersebut.

Buat sebuah class bernama ModelInspector:

C#

class ModelInspector
{
public static void Inspect(Type type)
{
Console.WriteLine($"Memeriksa: {type.Name}");
Console.WriteLine(new string('-', 30));
// Membaca Attribute di level class
RouteAttribute? route =
type.GetCustomAttribute<RouteAttribute>();
AuthorAttribute? author =
type.GetCustomAttribute<AuthorAttribute>();
if (route != null)
{
Console.WriteLine($"Route: {route.Path}");
}
if (author != null)
{
Console.WriteLine($"Author: {author.Name}");
}
Console.WriteLine();
Console.WriteLine("Daftar Property:");
// Membaca Attribute pada setiap property
foreach (PropertyInfo property in type.GetProperties())
{
DisplayAttribute? display =
property.GetCustomAttribute<DisplayAttribute>();
bool isRequired =
property.IsDefined(typeof(RequiredAttribute));
string label =
display?.Label ?? property.Name;
string requiredMark =
isRequired ? "(wajib)" : "(opsional)";
Console.WriteLine(
$"- {property.Name} => \"{label}\" {requiredMark}");
}
}
}

Ada beberapa hal penting yang terjadi di dalam method Inspect().

1. Menerima Type

Method tidak menerima ProductModel secara langsung.

Sebaliknya, method menerima:

C#

Type type

Dengan desain seperti ini, ModelInspector tidak terikat pada satu model tertentu.

Artinya, method yang sama nantinya dapat digunakan untuk:

C#

ModelInspector.Inspect(typeof(ProductModel));

atau:

C#

ModelInspector.Inspect(typeof(OrderModel));

atau class model lainnya.

Inilah salah satu ide penting dalam framework: logic pemeriksaan dibuat generik, sedangkan informasi spesifik diberikan melalui metadata.

2. Membaca Attribute pada Class

Pertama, ModelInspector mencari RouteAttribute:

C#

RouteAttribute? route =
type.GetCustomAttribute<RouteAttribute>();

Jika ditemukan, nilai Path dapat dibaca:

C#

route.Path

Pada ProductModel, nilainya adalah:

Plain Text

api/products

Hal yang sama dilakukan untuk AuthorAttribute:

C#

AuthorAttribute? author =
type.GetCustomAttribute<AuthorAttribute>();

Kemudian:

C#

author.Name

menghasilkan:

Plain Text

Tim Backend

Sampai titik ini, alurnya masih sama seperti contoh Reflection sebelumnya:

Plain Text

ProductModel
Type
├── GetCustomAttribute<RouteAttribute>()
│ └── Path
└── GetCustomAttribute<AuthorAttribute>()
└── Name

3. Memeriksa Setiap Property

Setelah metadata pada class selesai diperiksa, ModelInspector mengambil seluruh property:

C#

foreach (PropertyInfo property in type.GetProperties())
{
// ...
}

GetProperties() menghasilkan kumpulan PropertyInfo.

Untuk ProductModel, kurang lebih hasilnya:

Plain Text

PropertyInfo[]
├── Name
├── Price
└── Description

Setiap property kemudian diperiksa satu per satu.

4. Membaca DisplayAttribute

Untuk mendapatkan label yang lebih ramah, program mencari DisplayAttribute:

C#

DisplayAttribute? display =
property.GetCustomAttribute<DisplayAttribute>();

Misalnya ketika property sedang menunjuk ke:

C#

Name

Reflection menemukan:

C#

[Display("Nama Produk")]

Kemudian:

C#

display?.Label

menghasilkan:

Plain Text

Nama Produk

Jika DisplayAttribute tidak ditemukan, digunakan nama property asli:

C#

string label =
display?.Label ?? property.Name;

Jadi jika sebuah property tidak memiliki:

C#

[Display(...)]

program tetap dapat menghasilkan label dengan menggunakan nama property.

5. Memeriksa RequiredAttribute

RequiredAttribute sedikit berbeda. Program tidak membutuhkan data apa pun dari Attribute tersebut. Yang diperlukan hanya jawaban:

Apakah Attribute ini ada?

Karena itu, IsDefined() merupakan pilihan yang tepat:

C#

bool isRequired =
property.IsDefined(typeof(RequiredAttribute));

Untuk property Name:

Plain Text

[Required]
public string Name { get; set; }

hasilnya:

Plain Text

true

Sedangkan untuk Description:

C#

[Display("Deskripsi")]
public string Description { get; set; }

tidak ada RequiredAttribute, sehingga hasilnya:

Plain Text

false

Nilai tersebut kemudian digunakan untuk menentukan apakah property ditampilkan sebagai wajib atau opsional:

C#

string requiredMark =
isRequired ? "(wajib)" : "(opsional)";

6. Menjalankan ModelInspector

Sekarang tinggal memberikan Type dari ProductModel:

C#

class Program
{
static void Main()
{
ModelInspector.Inspect(
typeof(ProductModel));
}
}

Karena ModelInspector tidak mengetahui apa pun secara khusus tentang ProductModel, seluruh informasi yang dibutuhkan diperoleh melalui Reflection.

Output

Jika program dijalankan, hasilnya:

Plain Text

Memeriksa: ProductModel
------------------------------
Route: api/products
Author: Tim Backend
Daftar Property:
- Name => "Nama Produk" (wajib)
- Price => "Harga" (wajib)
- Description => "Deskripsi" (opsional)

Perhatikan bahwa tidak ada kode seperti:

C#

if (type == typeof(ProductModel))
{
// ...
}

atau:

C#

if (property.Name == "Name")
{
// ...
}

ModelInspector tidak perlu mengetahui nama model maupun nama property tertentu.

Ia hanya mengetahui aturan umum:

Plain Text

Jika ada RouteAttribute
→ baca Path
Jika ada AuthorAttribute
→ baca Name
Jika ada DisplayAttribute
→ gunakan Label
Jika ada RequiredAttribute
→ tandai sebagai wajib

Inilah yang membuat pendekatan berbasis metadata menjadi menarik.

Menambahkan Model Baru

Misalnya suatu saat ditambahkan model lain:

C#

[Route("api/orders")]
[Author("Tim Sales")]
public class OrderModel
{
[Display("Nomor Pesanan")]
[Required]
public string OrderNumber { get; set; }
[Display("Catatan")]
public string Note { get; set; }
}

Tidak perlu mengubah ModelInspector.

Cukup jalankan:

C#

ModelInspector.Inspect(typeof(OrderModel));

Program yang sama dapat membaca metadata dari model baru tersebut.

Secara konseptual:

Plain Text

ProductModel ──────┐
OrderModel ────────┼──► ModelInspector
│ │
CustomerModel ─────┘ ▼
Reflection
Attribute Metadata

ModelInspector bertindak sebagai mini framework yang memahami aturan Attribute, bukan detail masing-masing model.

Pola yang Sebenarnya Sedang Dibangun

Studi kasus ini terlihat sederhana, tetapi sebenarnya sudah memperlihatkan pola yang sangat penting dalam framework:

Plain Text

Source Code
Attribute Metadata
Reflection
Model Inspector
Program Behavior

Informasi yang sebelumnya harus ditulis dalam konfigurasi atau logic khusus sekarang dapat dinyatakan langsung pada elemen yang bersangkutan.

Misalnya:

C#

[Required]
public string Name { get; set; }

lebih bersifat deklaratif daripada membuat konfigurasi terpisah seperti:

C#

validationRules.Add(
typeof(ProductModel),
"Name",
required: true);

Pada pendekatan Attribute, aturan ditempatkan dekat dengan elemen yang diberi aturan.

Dari Mini Framework ke Framework Sebenarnya

Tentu saja framework nyata jauh lebih kompleks daripada ModelInspector. ASP.NET Core, Entity Framework, xUnit, dan library lainnya memiliki sistem internal yang jauh lebih besar dan tidak selalu hanya mengandalkan Attribute. Banyak framework juga menggunakan convention, konfigurasi programmatic, source generator, dependency injection, dan mekanisme lainnya.

Namun, pola dasarnya tetap mudah dikenali:

Plain Text

Metadata
Reflection / mekanisme metadata lainnya
Framework membaca informasi
Framework menentukan perilaku

Sebagai contoh, ketika sebuah method diberi:

C#

[HttpGet]
public IActionResult GetProducts()
{
// ...
}

framework dapat menggunakan informasi tersebut sebagai salah satu bagian dari proses menentukan endpoint.

Ketika sebuah property diberi:

C#

[Required]
public string Name { get; set; }

framework validasi dapat menggunakan metadata tersebut sebagai salah satu sumber aturan validasi.

Ketika property diberi:

C#

[Column("product_name")]
public string Name { get; set; }

ORM dapat menggunakan informasi tersebut sebagai bagian dari pemetaan antara property C# dan kolom database. Jadi, studi kasus ModelInspector bukan sekadar contoh buatan. Ia merupakan versi kecil dari pola yang banyak ditemukan dalam framework:

Kode mendeklarasikan metadata, kemudian mekanisme lain membaca metadata tersebut dan menentukan perilaku berdasarkan informasi yang ditemukan.

Dengan pola ini sudah dipahami secara langsung, pembahasan berikutnya dapat beralih dari mini framework ke contoh nyata di ekosistem .NET, dimulai dari ASP.NET Core dan [HttpGet].

Bagian 18: Bagaimana ASP.NET Core Menggunakan Attribute

Setelah melihat pola penggunaan Attribute melalui studi kasus sederhana, bagian ini dapat digunakan untuk melihat bagaimana pola yang sama diterapkan dalam framework yang lebih kompleks, yaitu ASP.NET Core.

Pembahasan ini tidak perlu masuk sampai ke detail source code internal ASP.NET Core. Yang lebih penting adalah memahami alur berpikir framework ketika menemukan Attribute pada controller dan method.

Beberapa Attribute yang cukup sering ditemui dalam ASP.NET Core antara lain:

C#

[HttpGet]
[HttpPost]
[Route]
[Authorize]
[FromBody]

Masing-masing memberikan informasi yang berbeda.

  • [HttpGet] menunjukkan bahwa sebuah action berkaitan dengan HTTP GET.

  • [HttpPost] menunjukkan bahwa sebuah action berkaitan dengan HTTP POST.

  • [Route] menentukan pola URL yang digunakan untuk mencapai controller atau action.

  • [Authorize] memberikan metadata bahwa endpoint membutuhkan otorisasi.

  • [FromBody] memberi tahu model binding bahwa nilai parameter perlu diambil dari body request.

Sekilas, kode seperti ini terlihat sangat sederhana:

C#

[HttpGet]
public IActionResult GetUsers()
{
// ...
}

Namun sebenarnya ada proses yang cukup panjang sebelum GetUsers() akhirnya dipanggil.

1. ASP.NET Core Mengenali Controller

Saat aplikasi ASP.NET Core dibangun dan dijalankan, framework perlu mengetahui endpoint apa saja yang tersedia.

Salah satu sumber informasi tersebut adalah controller.

Contohnya:

C#

[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult GetUsers()
{
// ...
}
}

Framework perlu menemukan bahwa UsersController merupakan controller dan kemudian memeriksa action yang ada di dalamnya.

Dalam penjelasan sederhana, proses ini dapat dibayangkan seperti:

Plain Text

Assembly
Cari controller
UsersController

Namun, penting untuk meluruskan satu hal: ASP.NET Core tidak sekadar mencari class yang memiliki [ApiController]. Controller dapat dikenali melalui beberapa mekanisme dan convention yang lebih luas. Pewarisan dari ControllerBase, Controller, nama class, serta konfigurasi MVC semuanya dapat berperan dalam proses discovery. Jadi, [ApiController] lebih tepat dipahami sebagai metadata yang mengaktifkan perilaku API tertentu, bukan satu-satunya syarat agar sebuah class dikenali sebagai controller.

2. Framework Memeriksa Action

Setelah menemukan controller, framework perlu mengetahui method mana yang dapat menjadi action.

Misalnya:

C#

public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult GetUsers()
{
// ...
}
[HttpPost]
public IActionResult CreateUser()
{
// ...
}
}

Di sini terdapat dua method:

Plain Text

UsersController
├── GetUsers()
│ └── [HttpGet]
└── CreateUser()
└── [HttpPost]

Framework kemudian mengumpulkan informasi tentang method-method tersebut, termasuk Attribute yang melekat padanya.

Secara konseptual, proses ini mirip dengan:

C#

MethodInfo[] methods = controllerType.GetMethods();
foreach (MethodInfo method in methods)
{
// Periksa metadata method
}

Implementasi sebenarnya tentu jauh lebih kompleks dan menggunakan berbagai mekanisme internal ASP.NET Core, tetapi pola dasarnya dapat dibayangkan seperti itu.

3. [HttpGet] Menjadi Metadata Endpoint

Ketika framework menemukan:

C#

[HttpGet]
public IActionResult GetUsers()
{
// ...
}

HttpGetAttribute memberikan informasi bahwa action tersebut memiliki constraint terhadap HTTP method GET.

Secara konseptual:

Plain Text

GetUsers()
└── HttpGetAttribute
HTTP GET

Attribute tersebut bukan yang menerima request HTTP. HttpGetAttribute juga tidak menjalankan method GetUsers(). Ia hanya menyediakan metadata yang kemudian digunakan oleh bagian lain dari ASP.NET Core.

4. Route dan HTTP Method Digabungkan

Misalnya controller memiliki:

C#

[Route("api/users")]
public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult GetUsers()
{
// ...
}
[HttpPost]
public IActionResult CreateUser()
{
// ...
}
}

Dari metadata tersebut, framework dapat membangun informasi endpoint seperti:

Plain Text

HTTP GET /api/users → GetUsers()
HTTP POST /api/users → CreateUser()

Jika terdapat route tambahan pada action:

C#

[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
// ...
}

maka informasi endpoint-nya dapat menjadi:

Plain Text

HTTP GET /api/users/{id} → GetUser(int id)

Dengan demikian, [Route] dan [HttpGet] tidak bekerja sendirian. Informasi dari beberapa Attribute dan metadata endpoint digabungkan untuk membentuk konfigurasi routing.

5. Endpoint Data Dibangun

Pada tahap berikutnya, ASP.NET Core membentuk representasi internal dari endpoint-endpoint yang tersedia.

Secara konseptual, hasilnya dapat dibayangkan seperti:

Plain Text

Endpoint
├── HTTP Method: GET
├── Route: /api/users
├── Controller: UsersController
└── Action: GetUsers

Dan endpoint lainnya:

Plain Text

Endpoint
├── HTTP Method: POST
├── Route: /api/users
├── Controller: UsersController
└── Action: CreateUser

Struktur internal sebenarnya jauh lebih kaya daripada contoh di atas. Endpoint dapat memiliki metadata tambahan untuk authorization, filters, model binding, response information, dan berbagai kebutuhan lainnya.

Namun gambaran sederhananya cukup untuk memahami ide utamanya:

Attribute digunakan sebagai salah satu sumber informasi untuk membentuk metadata endpoint.

6. Request HTTP Masuk

Sekarang misalkan terdapat request:

http

GET /api/users

ASP.NET Core tidak perlu melakukan Reflection terhadap seluruh controller dari awal setiap kali request datang. Informasi endpoint sudah disiapkan sebelumnya.

Request tersebut kemudian melewati pipeline dan routing mencari endpoint yang sesuai berdasarkan informasi seperti:

Plain Text

HTTP Method = GET
Path = /api/users

Hasil pencocokan:

Plain Text

GET /api/users
GetUsers()

Setelah endpoint ditemukan, framework dapat melanjutkan ke tahap berikutnya.

7. Attribute Lain Ikut Berperan

Tidak hanya routing yang menggunakan metadata.

Misalnya terdapat:

C#

[Authorize]
[HttpGet]
public IActionResult GetUsers()
{
// ...
}

Sekarang action tersebut memiliki lebih dari satu informasi:

Plain Text

GetUsers()
├── [HttpGet]
│ └── HTTP method
└── [Authorize]
└── Authorization requirement

Routing menggunakan informasi HTTP method untuk membantu menentukan endpoint. Sementara itu, authorization middleware atau mekanisme authorization ASP.NET Core menggunakan metadata authorization untuk menentukan apakah request boleh mengakses endpoint tersebut. Dengan kata lain, Attribute yang sama-sama menempel pada sebuah method dapat dimanfaatkan oleh bagian framework yang berbeda.

8. Model Binding

Attribute juga dapat digunakan untuk memberi petunjuk mengenai sumber parameter.

Misalnya:

C#

[HttpPost]
public IActionResult CreateUser(
[FromBody] CreateUserRequest request)
{
// ...
}

[FromBody] memberikan metadata bahwa request perlu diambil dari HTTP request body.

Jika request berisi JSON:

JSON

{
"name": "John",
"email": "john@example.com"
}

ASP.NET Core kemudian menggunakan model binding dan input formatter untuk membaca body tersebut dan menghasilkan object CreateUserRequest.

Secara sederhana:

Plain Text

HTTP Request
├── Route
├── Query String
├── Headers
└── Body
Model Binding
CreateUserRequest
CreateUser(...)

[FromBody] menjadi salah satu informasi yang membantu framework menentukan bagaimana parameter tersebut harus diproses.

9. Gambaran Keseluruhan

Jika seluruh proses disederhanakan, alurnya kurang lebih seperti ini:

Plain Text

Source Code
┌─────────────────────┐
│ UsersController │
│ │
│ [Route("api/users")]│
│ │
│ [HttpGet] │
│ GetUsers() │
└──────────┬──────────┘
Metadata
ASP.NET Core
┌──────────┴──────────┐
│ │
▼ ▼
Routing Authorization
│ │
└──────────┬──────────┘
Endpoint
GetUsers()

Gambaran tersebut memperlihatkan peran Attribute dengan lebih jelas. Attribute bukan bagian yang secara langsung menerima request. Attribute juga bukan kode yang secara otomatis memanggil method. Attribute hanya menyediakan informasi yang dapat digunakan oleh mekanisme framework.

10. Apakah ASP.NET Core Selalu Menggunakan Reflection?

Ada satu penyederhanaan yang perlu diperhatikan. Pada contoh ModelInspector sebelumnya, Reflection digunakan secara langsung:

C#

type.GetProperties();
method.GetCustomAttribute<AuthorAttribute>();

Pada ASP.NET Core, mekanismenya jauh lebih kompleks.

Framework memang menggunakan reflection dalam berbagai tahap discovery dan pemeriksaan metadata, tetapi tidak berarti setiap request melakukan:

Plain Text

Reflection
Cari controller
Cari method
Cari [HttpGet]
Panggil method

untuk setiap request.

Pendekatan seperti itu akan terlalu mahal jika dilakukan berulang kali. Sebaliknya, ASP.NET Core melakukan proses discovery dan membangun informasi endpoint terlebih dahulu. Hasilnya kemudian digunakan oleh routing dan komponen lain ketika request masuk.

Secara sederhana:

Plain Text

Saat aplikasi dibangun
──────────────────────
Controller
Metadata
Discovery
Endpoint Metadata
Disimpan
Saat request masuk
──────────────────
HTTP Request
Routing
Endpoint yang sudah tersedia
Authorization / Binding / Filters
Action

Jadi, Reflection lebih tepat dipahami sebagai salah satu mekanisme yang membantu framework memahami struktur dan metadata aplikasi, bukan sesuatu yang harus dijalankan dari awal pada setiap request.

11. Mengapa [HttpGet] Terlihat Seperti "Sihir"?

Kembali ke pertanyaan yang muncul di awal artikel:

C#

[HttpGet]
public IActionResult GetUsers()
{
// ...
}

Mengapa ASP.NET Core tahu bahwa method tersebut menangani HTTP GET? Sekarang jawabannya menjadi lebih jelas. Bukan karena [HttpGet] secara ajaib menjalankan method tersebut.

Alurnya kurang lebih:

Plain Text

[HttpGet]
Metadata pada method
ASP.NET Core membaca metadata
Endpoint metadata dibentuk
Routing mengetahui constraint HTTP GET
GET /api/users
Endpoint cocok
GetUsers()

Attribute hanyalah salah satu bagian dari rangkaian mekanisme tersebut. Hal yang sama berlaku pada banyak Attribute lain dalam ekosistem .NET. [Authorize], [FromBody], [Route], [Required], dan berbagai Attribute lainnya memberikan informasi yang kemudian dimanfaatkan oleh komponen yang memahami metadata tersebut.

Inilah alasan Attribute menjadi sangat berguna dalam framework: kode dapat menyatakan maksudnya secara deklaratif, sementara framework yang bertugas menerjemahkan metadata tersebut menjadi perilaku.

Dengan demikian, syntax:

C#

[HttpGet]
public IActionResult GetUsers()
{
// ...
}

sebenarnya merupakan bentuk deklarasi.

Attribute menyatakan:

Method ini memiliki metadata yang berkaitan dengan HTTP GET.

Kemudian framework mengambil informasi tersebut dan menggunakannya sebagai bagian dari proses membangun endpoint dan menangani request.

Pola yang sama akan terlihat kembali pada pembahasan berikutnya, ketika melihat bagaimana Entity Framework Core menggunakan Attribute untuk memetakan class dan property C# ke struktur database.

Bagian 19: Bagaimana Entity Framework Menggunakan Attribute

Setelah membahas ASP.NET Core, bagian ini membahas contoh kedua: bagaimana Entity Framework memanfaatkan Attribute untuk membangun struktur database dari class C# biasa, pendekatan yang dikenal sebagai Code First.

Beberapa Attribute yang umum ditemui dalam konteks Entity Framework:

C#

[Key]
[Table]
[Column]
[NotMapped]
[DatabaseGenerated]

[Key] menandai property yang berperan sebagai primary key suatu tabel. [Table] menentukan nama tabel yang digunakan di database, jika berbeda dari nama class. [Column] menentukan nama kolom di database untuk suatu property, jika berbeda dari nama property tersebut. [NotMapped] menandai bahwa suatu property sengaja tidak perlu dipetakan ke kolom database sama sekali. [DatabaseGenerated] menentukan bagaimana nilai suatu kolom dihasilkan, misalnya secara otomatis oleh database (seperti kolom auto-increment).

Contoh penerapan pada sebuah class model:

C#

[Table("products")]
public class Product
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Column("product_name")]
public string Name { get; set; }
public decimal Price { get; set; }
[NotMapped]
public string DisplayText => $"{Name} - {Price:C}";
}

Class Product di atas, tanpa perlu satu baris kode SQL pun, sudah membawa cukup informasi bagi Entity Framework untuk mengetahui bahwa data ini disimpan di tabel bernama products, kolom primary key-nya bernama Id dan nilainya dihasilkan otomatis oleh database, kolom Name sebenarnya bernama product_name di database, kolom Price menggunakan nama yang sama seperti nama property-nya, dan property DisplayText sama sekali tidak perlu dipetakan ke kolom apa pun karena nilainya hanya dihitung on-the-fly.

Alur kerja Entity Framework dalam memanfaatkan informasi ini, secara garis besar, terjadi pada tahap yang disebut model building, yang berlangsung sekali saat aplikasi pertama kali menginisialisasi DbContext:

  1. Pemindaian seluruh DbSet yang terdaftar di dalam DbContext, untuk menemukan class model mana saja yang perlu dipetakan ke database.

  2. Pemindaian setiap property pada class model tersebut menggunakan Reflection, mencari Attribute seperti [Key], [Column], atau [NotMapped].

  3. Pembentukan model metadata internal, yang menyimpan pemetaan lengkap antara class C# dan struktur tabel database — nama tabel, nama kolom, tipe data, primary key, dan relasi antar tabel.

  4. Penerjemahan query LINQ menjadi SQL, dengan memanfaatkan model metadata yang sudah dibangun sebelumnya untuk menerjemahkan operasi seperti dbContext.Products.Where(p => p.Price > 100) menjadi perintah SQL yang sesuai dengan struktur tabel sebenarnya.

Pola ini sekali lagi mengikuti alur yang sama seperti yang sudah dipraktikkan pada studi kasus ModelInspector sebelumnya: memindai class, membaca Attribute yang menempel pada property-nya, lalu menyusun sebuah struktur data internal berdasarkan hasil pembacaan tersebut. Yang membedakan Entity Framework adalah struktur data yang dihasilkan dipakai lebih jauh lagi, untuk menerjemahkan kode C# menjadi perintah SQL yang bisa dijalankan langsung ke database.

Bagian 20: Bagaimana xUnit Menggunakan Attribute

Contoh ketiga membahas konteks yang cukup berbeda dari dua contoh sebelumnya: pengujian otomatis, melalui framework testing populer di ekosistem .NET, xUnit.

Tiga Attribute yang paling sering ditemui saat menulis unit test dengan xUnit:

C#

[Fact]
[Theory]
[InlineData]

[Fact] menandai sebuah method sebagai satu unit test tunggal, yang dijalankan tanpa parameter apa pun. [Theory] menandai sebuah method sebagai unit test yang dijalankan berulang kali dengan kumpulan data yang berbeda-beda. [InlineData] menyuplai satu set data untuk setiap kali eksekusi [Theory], dan bisa dipasang berkali-kali pada method yang sama untuk menjalankan beberapa skenario pengujian sekaligus.

Contoh penerapannya:

C#

public class CalculatorTests
{
[Fact]
public void Add_ShouldReturnCorrectSum()
{
var result = Calculator.Add(2, 3);
Assert.Equal(5, result);
}
[Theory]
[InlineData(1, 2, 3)]
[InlineData(-1, 1, 0)]
[InlineData(0, 0, 0)]
public void Add_WithMultipleInputs_ShouldReturnCorrectSum(int a, int b, int expected)
{
var result = Calculator.Add(a, b);
Assert.Equal(expected, result);
}
}

Method Add_ShouldReturnCorrectSum hanya dijalankan satu kali, sesuai dengan sifat [Fact]. Sementara Add_WithMultipleInputs_ShouldReturnCorrectSum dijalankan sebanyak tiga kali, masing-masing dengan set data yang berbeda dari ketiga [InlineData] yang dipasang.

Alur kerja xUnit dalam menjalankan test-test ini, secara garis besar, dilakukan oleh sebuah test runner — baik itu dijalankan melalui IDE, command line, maupun pipeline CI/CD, dan mengikuti tahapan berikut:

  1. Memindai assembly hasil kompilasi test project, mencari seluruh class publik yang tersedia di dalamnya.

  2. Memindai setiap method dalam class tersebut, mencari method yang memiliki [Fact] atau [Theory] menggunakan Reflection.

  3. Untuk method dengan [Fact], method tersebut langsung dicatat sebagai satu test case yang siap dijalankan.

  4. Untuk method dengan [Theory], test runner memeriksa seluruh [InlineData] yang menempel pada method yang sama, lalu membuat satu test case terpisah untuk setiap set data yang ditemukan.

  5. Menjalankan seluruh test case yang sudah tercatat, satu per satu, sambil mencatat apakah masing-masing test case berhasil (pass) atau gagal (fail), berdasarkan hasil eksekusi assertion seperti Assert.Equal() di dalamnya.

  6. Melaporkan hasil keseluruhan, biasanya dalam bentuk ringkasan jumlah test yang berhasil, gagal, dan dilewati.

Pola yang muncul di sini sekali lagi konsisten dengan dua contoh sebelumnya: Reflection dipakai untuk memindai dan menemukan method yang relevan berdasarkan Attribute yang menempel padanya, lalu hasil pemindaian tersebut dipakai untuk mengambil tindakan. Dalam kasus ini, menjalankan test dan melaporkan hasilnya, bukan membangun routing table seperti ASP.NET Core atau model database seperti Entity Framework.

Bagian 21: Bagaimana System.Text.Json Menggunakan Attribute

Contoh terakhir dari rangkaian studi kasus ini membahas System.Text.Json, library bawaan .NET untuk melakukan serialisasi dan deserialisasi JSON, yang sempat disinggung sejak bagian awal artikel ini.

Tiga Attribute yang paling umum ditemui dalam konteks ini:

C#

[JsonIgnore]
[JsonPropertyName]
[JsonInclude]

[JsonIgnore] menandai bahwa suatu property harus dilewati sepenuhnya saat proses serialisasi maupun deserialisasi, sehingga tidak akan pernah muncul di hasil JSON. [JsonPropertyName] menentukan nama field JSON yang dipakai untuk suatu property, jika berbeda dari nama property itu sendiri. [JsonInclude] menandai bahwa suatu field atau property non-public tetap perlu disertakan dalam proses serialisasi, meskipun secara default System.Text.Json hanya memproses property public.

Contoh penerapannya:

C#

public class UserProfile
{
[JsonPropertyName("first_name")]
public string FirstName { get; set; }
[JsonPropertyName("last_name")]
public string LastName { get; set; }
[JsonIgnore]
public string PasswordHash { get; set; }
[JsonInclude]
internal int LoginCount;
}

Ketika instance dari UserProfile diserialisasi menggunakan JsonSerializer.Serialize(), hasilnya kurang lebih akan terlihat seperti berikut:

JSON

{
"first_name": "Budi",
"last_name": "Santoso",
"LoginCount": 3
}

Property FirstName dan LastName muncul dengan nama sesuai yang ditentukan oleh [JsonPropertyName], bukan nama aslinya. Property PasswordHash sama sekali tidak muncul karena ditandai [JsonIgnore], sebuah pertimbangan keamanan yang penting agar data sensitif tidak ikut terekspos saat objek diserialisasi. Field LoginCount, meskipun bersifat internal dan bukan property public, tetap muncul karena ditandai [JsonInclude].

Alur kerja JsonSerializer dalam memanfaatkan Attribute ini terjadi setiap kali proses serialisasi atau deserialisasi dijalankan, mengikuti tahapan berikut:

  1. Memindai seluruh property (dan field yang relevan) dari tipe objek yang sedang diproses, menggunakan Reflection.

  2. Untuk setiap property, memeriksa keberadaan [JsonIgnore]. Jika ditemukan, property tersebut langsung dilewati dari proses serialisasi maupun deserialisasi.

  3. Memeriksa keberadaan [JsonPropertyName]. Jika ditemukan, nama yang tertulis pada Attribute tersebut dipakai sebagai nama field JSON, menggantikan nama property aslinya.

  4. Memeriksa keberadaan [JsonInclude] pada field atau property non-public, untuk menentukan apakah elemen tersebut perlu diikutsertakan meskipun secara default seharusnya dilewati.

  5. Membentuk hasil akhir berupa teks JSON saat serialisasi, atau berupa instance object yang sudah terisi saat deserialisasi, berdasarkan seluruh informasi yang dikumpulkan dari langkah-langkah sebelumnya.

Dengan ini, keempat contoh nyata sudah selesai dibahas: ASP.NET Core memanfaatkan Attribute untuk routing dan otorisasi, Entity Framework memanfaatkannya untuk pemetaan struktur database, xUnit memanfaatkannya untuk menemukan dan menjalankan test, dan System.Text.Json memanfaatkannya untuk mengatur proses serialisasi. Keempatnya, meskipun menyelesaikan masalah yang sangat berbeda, sama-sama bertumpu pada satu pola dasar yang sudah dipelajari sejak awal artikel ini: Attribute sebagai metadata pasif, dan Reflection sebagai mekanisme yang menghidupkannya.

Bagian 22: Kapan Sebaiknya Menggunakan Attribute?

Setelah melihat bagaimana Attribute digunakan oleh berbagai library dan framework, ada satu pertanyaan praktis yang penting untuk dibahas:

Kapan Attribute merupakan pilihan yang tepat, dan kapan sebaiknya menggunakan pendekatan lain?

Attribute memang sangat berguna, tetapi bukan berarti setiap konfigurasi atau perilaku dalam aplikasi perlu dinyatakan menggunakan Attribute. Pemilihan pendekatan sebaiknya disesuaikan dengan jenis informasi yang ingin disampaikan.

Gunakan Attribute ketika:

1. Menambahkan metadata

Ini adalah penggunaan Attribute yang paling dasar. Jika sebuah elemen kode membutuhkan informasi tambahan yang bersifat deskriptif, Attribute biasanya menjadi pilihan yang baik.

Contohnya:

C#

[Column("email_address")]
public string Email { get; set; }

Attribute tersebut tidak mengubah cara property Email bekerja. Ia hanya memberikan informasi tambahan mengenai bagaimana property tersebut dipetakan ke database.

Contoh lainnya:

C#

[JsonPropertyName("first_name")]
public string FirstName { get; set; }

Informasi seperti nama kolom database, nama field JSON, atau penanda validasi merupakan contoh metadata yang cocok ditempatkan sebagai Attribute.

2. Konfigurasi secara deklaratif

Attribute sangat cocok ketika sebuah konfigurasi dapat dinyatakan langsung pada elemen yang bersangkutan.

Misalnya:

C#

[Required]
public string Name { get; set; }

Dari kode tersebut, aturan yang berlaku untuk Name dapat langsung terlihat.

Bandingkan dengan konfigurasi yang ditempatkan jauh dari class:

C#

validationRules.Add(
typeof(User),
nameof(User.Name),
required: true);

Kedua pendekatan sama-sama mungkin digunakan, tetapi Attribute membuat hubungan antara aturan dan elemen yang diatur menjadi lebih jelas.

Pendekatan seperti ini disebut declarative configuration: kode menyatakan apa yang berlaku, sementara mekanisme lain menentukan bagaimana aturan tersebut dijalankan.

3. Memberikan petunjuk kepada framework atau library

Attribute sangat berguna ketika ada framework atau library yang perlu mendapatkan informasi tambahan dari kode.

Contohnya:

C#

[HttpGet]
public IActionResult GetUsers()
{
// ...
}

atau:

C#

[Key]
public int Id { get; set; }

Dalam kedua contoh tersebut, Attribute memberikan petunjuk kepada framework tentang bagaimana elemen tersebut perlu diperlakukan. Method GetUsers() tidak menjalankan proses routing hanya karena memiliki [HttpGet]. Begitu pula property Id tidak otomatis menjadi primary key hanya karena memiliki [Key]. Framework-lah yang membaca metadata tersebut dan menggunakannya dalam prosesnya sendiri.

4. Mengurangi konfigurasi yang tersebar

Tanpa Attribute, konfigurasi untuk setiap elemen kode sering kali perlu ditempatkan di lokasi lain. Misalnya, informasi tentang beberapa property mungkin harus dikumpulkan dalam sebuah konfigurasi terpisah:

Plain Text

User
├── Id → Primary Key
├── Name → Required
└── Email → Column "email_address"

Dengan Attribute, informasi tersebut dapat ditempatkan langsung pada elemen yang bersangkutan:

C#

public class User
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Column("email_address")]
public string Email { get; set; }
}

Hal ini dapat membuat kode lebih mudah ditelusuri karena metadata berada dekat dengan elemen yang dijelaskannya. Namun, ini bukan berarti Attribute selalu lebih baik daripada konfigurasi terpisah. Ketika konfigurasi menjadi sangat banyak atau membutuhkan aturan yang kompleks, pendekatan lain justru bisa lebih sesuai.

Hindari Attribute ketika:

1. Menyimpan business logic

Attribute sebaiknya menyimpan informasi, bukan menjadi tempat untuk menjalankan business logic.

Misalnya, kebutuhan seperti:

Plain Text

Hitung harga
→ cek diskon
→ panggil service
→ cek database
→ tentukan hasil

tidak cocok ditempatkan di dalam Attribute.

Attribute seperti:

C#

[CalculateDiscount]
public decimal Price { get; set; }

mungkin terlihat menarik, tetapi Attribute itu sendiri tidak menjelaskan bagaimana proses tersebut harus dijalankan. Untuk kebutuhan seperti ini, service, domain logic, atau komponen aplikasi lainnya biasanya lebih sesuai.

2. Menyimpan data yang berubah saat runtime

Attribute merupakan bagian dari metadata yang ditentukan dari source code dan tersedia melalui metadata assembly. Karena itu, Attribute cocok untuk informasi yang relatif statis.

Misalnya:

C#

[Author("John")]
public class ReportGenerator
{
}

Nama "John" merupakan bagian dari deklarasi kode.

Sebaliknya, data seperti:

Plain Text

Nama pengguna yang sedang login
Jumlah saldo saat ini
Harga berdasarkan kondisi pasar
Status koneksi
Waktu terakhir request

bersifat dinamis dan bergantung pada kondisi runtime.

Data semacam itu tidak cocok dijadikan Attribute.

3. Menggantikan Dependency Injection

Attribute bukan mekanisme dependency injection.

Misalnya sebuah class membutuhkan:

C#

IEmailService
ILogger
IPaymentService

kebutuhan tersebut sebaiknya ditangani melalui dependency injection:

C#

public class OrderService
{
private readonly IEmailService _emailService;
public OrderService(IEmailService emailService)
{
_emailService = emailService;
}
}

Attribute tidak dirancang untuk menyediakan instance service atau mengatur lifetime sebuah object.

Walaupun framework tertentu dapat menghubungkan Attribute dengan mekanisme dependency injection, hubungan tersebut terjadi karena framework memiliki logic tambahan. Attribute-nya sendiri tetap hanya menyediakan metadata.

4. Membuat Attribute terlalu kompleks

Custom Attribute sebaiknya relatif sederhana.

Contoh yang mudah dipahami:

C#

[Route("api/products")]

atau:

C#

[Author("John")]

Informasinya jelas dan mudah dibaca.

Sebaliknya, jika sebuah Attribute mulai memiliki banyak property dan aturan:

C#

[Something(
A = "...",
B = "...",
C = "...",
D = "...",
E = "...")]

kemudian constructor-nya juga melakukan berbagai pemeriksaan dan pengambilan keputusan, hal tersebut patut dievaluasi kembali.

Semakin kompleks sebuah Attribute, semakin sulit pula memahami apa yang sebenarnya dikonfigurasikan hanya dengan melihat deklarasinya.

Jika kebutuhan sudah berkembang menjadi konfigurasi yang besar, fluent API, options pattern, atau konfigurasi terpisah mungkin lebih sesuai.

Attribute Menjawab "Apa", Bukan "Bagaimana"

Salah satu cara sederhana untuk menentukan apakah Attribute cocok digunakan adalah dengan melihat jenis pertanyaan yang ingin dijawab.

Attribute biasanya cocok untuk informasi seperti:

Plain Text

Apa nama kolomnya?
Apa route-nya?
Apa perannya?
Apakah property ini wajib?
Apakah method ini merupakan endpoint GET?
Apakah class ini memiliki karakteristik tertentu?

Dengan kata lain, Attribute cenderung menjawab:

"Apa informasi atau aturan yang melekat pada elemen ini?"

Sedangkan kebutuhan seperti:

Plain Text

Bagaimana proses ini dijalankan?
Bagaimana keputusan bisnis dibuat?
Bagaimana data dihitung?
Bagaimana service dipanggil?
Bagaimana transaksi diproses?

lebih cocok ditangani oleh logic program biasa.

Perbedaannya dapat digambarkan secara sederhana:

Plain Text

Attribute
└── Menjelaskan metadata
├── Apa perannya?
├── Apa namanya?
├── Aturan apa yang melekat?
└── Bagaimana framework harus memperlakukannya?
Program Logic
└── Menjalankan proses
├── Bagaimana data diproses?
├── Bagaimana keputusan dibuat?
├── Service apa yang dipanggil?
└── Bagaimana hasil diperoleh?

Batas ini tidak selalu mutlak. Framework dapat menggunakan Attribute sebagai pemicu untuk menjalankan proses yang cukup kompleks. Namun kompleksitas tersebut seharusnya berada pada kode yang membaca dan memproses metadata, bukan pada Attribute itu sendiri.

Dengan cara pandang ini, Attribute dapat ditempatkan pada perannya yang tepat: sebagai cara deklaratif untuk menyampaikan informasi kepada compiler, runtime, framework, atau tooling.

Bagian 23: Kesalahan yang Sering Terjadi

Setelah membahas kapan Attribute sebaiknya digunakan dan kapan sebaiknya dihindari, ada baiknya juga mengenali beberapa kesalahan pemahaman yang paling sering muncul, khususnya bagi yang baru pertama kali mempelajari Attribute.

Mengira Attribute langsung menjalankan kode. Ini adalah kesalahpahaman paling umum, dan sudah ditekankan berkali-kali sejak awal artikel. Menulis [HttpGet] di atas sebuah method tidak membuat apa pun terjadi secara otomatis pada saat itu juga. Attribute hanyalah metadata pasif, ia baru "berarti" sesuatu ketika ada kode lain yang secara eksplisit membacanya dan mengambil tindakan berdasarkan apa yang ditemukan.

Tidak memahami bahwa Reflection diperlukan. Berkaitan erat dengan poin sebelumnya, kesalahan ini muncul ketika seseorang membuat Custom Attribute, memasangnya pada elemen kode, lalu bingung mengapa "tidak terjadi apa-apa". Seperti yang sudah dipraktikkan pada studi kasus ModelInspector, Attribute yang sudah dibuat dan dipasang tetap membutuhkan kode Reflection yang secara aktif membacanya sebelum informasi di dalamnya bisa dimanfaatkan.

Menaruh terlalu banyak informasi pada Attribute. Attribute yang memiliki terlalu banyak property, atau menyimpan struktur data yang kompleks, biasanya adalah tanda bahwa kebutuhan sudah melampaui apa yang seharusnya ditangani oleh Attribute. Semakin banyak informasi yang ditumpuk, semakin sulit Attribute tersebut dibaca dan dipahami maksudnya hanya dengan melihat penggunaannya.

Menggunakan Attribute untuk kebutuhan yang lebih cocok diselesaikan dengan konfigurasi atau kode biasa. Kadang ada dorongan untuk menggunakan Attribute semata-mata karena terlihat "elegan" atau "modern", padahal kebutuhan yang ada sebenarnya lebih sesuai diselesaikan dengan pendekatan lain, misalnya file konfigurasi eksternal untuk pengaturan yang sering berubah, atau method biasa untuk logic yang kompleks. Attribute bukan solusi untuk semua masalah, dan memaksakan penggunaannya justru bisa membuat kode lebih sulit dipahami, bukan lebih mudah.

Mengabaikan dampak performa jika Reflection digunakan secara berlebihan. Reflection, meskipun sangat berguna, memiliki overhead performa dibanding pemanggilan kode secara langsung. Memanggil GetCustomAttribute() atau method Reflection lainnya secara berulang-ulang di jalur kode yang sering dieksekusi, misalnya di dalam loop yang berjalan ribuan kali, atau pada setiap request yang masuk, bisa berdampak nyata terhadap performa aplikasi. Framework besar seperti ASP.NET Core dan Entity Framework mengatasi ini dengan melakukan pemindaian Attribute hanya sekali di awal (biasanya saat aplikasi start), lalu menyimpan hasilnya dalam struktur data yang bisa dipakai berulang kali tanpa perlu memindai ulang.

Bagian 24: Ringkasan

Attribute merupakan salah satu fondasi penting dalam ekosistem .NET. Banyak framework populer memanfaatkan metadata untuk menghadirkan API yang sederhana namun tetap fleksibel dengan cukup menulis satu baris seperti [HttpGet] atau [Key], dan seluruh kompleksitas di baliknya ditangani oleh framework tanpa perlu ditulis secara manual berulang-ulang.

Setelah memahami cara membuat dan membaca Attribute menggunakan Reflection, akan jauh lebih mudah memahami bagaimana berbagai framework bekerja di balik layar, serta membangun library sendiri yang lebih extensible, mengikuti pola yang sama yang sudah dipraktikkan oleh ASP.NET Core, Entity Framework, xUnit, dan System.Text.Json sepanjang artikel ini.

Daftar isi

Bagian 1: Pendahuluan
Bagian 19: Bagaimana Entity Framework Menggunakan Attribute

Baca juga

Artikel sebelumnya

Artikel berikutnya

Dibuat pelan-pelan dengan rasa hangat dari Kuningan.

© 2026 Rifky Haekal Al-Fadillah