devxlogo

Get Personal with C# Custom Attributes

Get Personal with C# Custom Attributes

henever I bump into one of my Java friends, he invariably tells me C# is no better than Java. I generally reply that one of my favorite features of C# is custom attributes. This tends to cut the Java versus C# debate short, since it’s clear that custom attributes are very useful and dearly missed in Java. Indeed, while attributes may seem like a relatively insignificant element of C# grammar, they can dramatically ease your development. This article describes what custom attributes are, the different kind of attributes, and creative ways to use them.

Custom Attributes Defined
In source code-based programming, you annotate your code using comments. This is just fine because whoever reuses your code will see these annotations. However, in component-based programming, the metadata replaces the source file as the method of reuse. Custom attributes are extensions of the metadata that survive compilation and are therefore visible by the client even if he doesn’t have access to the source code.

Custom attributes differ from regular code. While regular code describes what the execution should do, attributes state something about the software itself.

Using Custom Attributes
When it is clear from the syntax what the attribute applies to, you just apply it between brackets before the element to annotate. For example, the following code fragment declares four attributes at the class, field, method and parameter level:

[AttributeForTheClass]class SomeClass{	[AttributeForTheField]	int SomeField;	[AttributeForTheMethod]	int SomeMethod(		[AttributeForTheParameter]int someParam)	{		...	}}

When the C# syntax does not clearly present the element to annotate, you need to add the name of the target in the attribute usage. The name of the target is one of the following:

Target name

Description

assembly

The attribute applies to the entire assembly.

module

The attribute applies to the entire module.

return

The attribute applies to the return value of the method, property set branch or indexer set branch.

value

The attribute applies to the implicit “value” parameter of the property or indexer.

Table 1: Attribute Targets.

The next code fragment first declares two global attributes–one for the entire assembly and one for the entire module. The return value of SomeMethod also has a custom attribute. Note that without the “return:” target indicator, the attribute would apply to the method itself.

[assembly: AttributeForTheEntireAssembly][module: AttributeForTheEntireModule]class SomeClass{	[return: AttributeForTheReturnValue]	int SomeMethod(){...}	int SomeProperty	{		[return: AttributeForTheReturnValue]		get		{			return ...;		}		[value: AttributeForTheValue]		set		{			... = value;		}	}}

This same principle applies to the get methods of properties. The property set method’s “value” implicit parameter has a custom attribute. Again, without the “value:” target indicator, the attribute would apply to the set method itself.

Attributes themselves implement as regular .NET classes. In particular, they are able to have members. Differentiate attributes from other .NET classes by deriving them from System.Attribute.

Different Flavors of Custom Attributes
While the syntax is always the same, attributes are compiled into a wide variety of representations. The following sections will explore the three flavors of attributes: user attributes, run time attributes, and compile time attributes.

User Attributes
User attributes are the simplest form of attributes. Their meaning depends entirely on the intent of the user. Consider the following code snippet:

class Programmer{	[Currency ("Japanese Yen")]	public int Salary = 1000000;}class CurrencyAttribute: System.Attribute{	public CurrencyAttribute (string currencyName)	{		Name = currencyName;	}	public readonly string Name;}

Given the exchange rate between dollar and yen, it’s clear that this attribute is crucial. Remove it, and the Programmer’s salary may be ambiguous. The class CurrencyAttribute defines the currency attribute itself. A nice thing about custom attributes is that they have all the power of classes. For example, a real-life attribute class might have an exchange rate.

But who would possibly know what this attribute means? Neither the compiler nor the runtime have any concept of currency. It’s your job to discover the attributes through the GetCustomAttribute method and do something meaningful with them. The following code first gets the FieldInfo metadata element and then queries its attributes through GetCustomAttributes:

FieldInfo field = typeof (Programmer).GetField ("Salary");object [] attributes = field.GetCustomAttributes (	typeof (CurrencyAttribute), false);if (attributes != null && attributes.Length > 0){	CurrencyAttribute currency = (CurrencyAttribute)attributes[0];	Console.WriteLine ("The currency for Programmer is " + currency.Name);}

Runtime Attributes
Runtime attributes regroup all attributes that influence the behavior of the runtime. The Just In Time (JIT) compiler looks for these attributes to customize the machine specific assembler it produces. Here are two examples how to use them:

The Layout of Structures
To the surprise of most C++ programmers, when the JIT compiler sees a declaration for a struct, it is not required to respect the order of the fields in memory. Take, for example, this struct:

struct MyStruct{	public int X;	public string Y;}

The JIT compiler may decide to put Y before X in memory to simplify the job of the garbage collector. This is not desirable when you need to pass instances of this struct outside .NET. The StructLayout attribute indicates the memory layout of a C# struct. In the following code, the struct is marked as sequential, so the JIT compiler respects the X and Y order.

[StructLayout (LayoutKind.Sequential)]struct MyStruct{	public int X;	[MarshalAs (UnmanagedType.BStr)]	public string Y;}

In addition, you can use MarshalAs to specify the corresponding unmanaged representation of fields and parameters.

Imperative and Declarative Security
While component based software is a huge step forward for software productivity, it can be a security concern because it makes it difficult to know whether assemblies are performing dangerous operations behind your back. This is particularly true when you obtain components from the Internet.

.NET resolves this problem by demanding permissions before performing any sensitive operation. Demanding permission involves ensuring that all callers on the stack have permission. You can do this imperatively or declaratively.

Imperative simply means writing code as usual. To perform some sensitive operation, you would first create a corresponding permission and demand it:

public void SomeMethod(){	new SomePermission().Demand();	// perform some sensitive operation}

This solution has two drawbacks. First, it is fairly verbose. Second, it is difficult for the client to know about the permission demand because it is buried in IL.

To avoid these issues, specify the permission requirements declaratively. Instead of invoking Demand, add a permission attribute before the method to secure.

[SomePermissionAttribute(SecurityAction.Demand)]public void OtherMethod(){	// perform some sensitive operation}

Tools like permview.exe will help you discover such security annotations.

When the JIT compiler comes across this permission attribute, it injects the call to Demand at the beginning of the method call.

Author’s Note: The “Attribute” suffix can be omitted in the attribute usage:

[SomePermission(SecurityAction.Demand)] // Note: no "Attribute" suffixpublic void OtherMethod(){	// perform some sensitive operation}

The C# compiler detects that SomePermission does not derive from Attribute and substitutes it for SomePermissionAttribute.

The following table summarizes the runtime attributes.

Attribute Name

Description

System.Serializable

The type is serializable.

System.Runtime.InteropServices.DllImportAttribute

Specifies the DLL name an other information for extern methods.

System.Runtime.InteropServices.MarshalAsAttribute

Specifies how the field or attribute should be marshaled.

System.Runtime.InteropServices.ComImportAttribute

Specifies that the type was previously defined in COM.

System.Runtime.InteropServices.StructLayoutAttribute

Specifies the layout of the structure in memory.

System.Runtime.InteropServices.FieldOffsetAttribute

Specifies the memory offset of a field in a struct.

System.Runtime.InteropServices.GuidAttribute

Specifies the Guid of a COM class or interface.

System.Runtime.InteropServices.InAttribute

Indicates that the parameter is “in”.

System.Security.Permissions.CodeAccessSecurityAttribute

?

Specifies a code access permission.

System.Security.Permissions.SecurityPermissionAttribute

?

Specifies a code access permission.

Table 2: Summary of runtime attributes.

Compile Time Attributes
These attributes regroup attributes that participate in the compilation. There are two categories of compile time attributes: attributes that generate errors or warnings and attributes that change the compiled code in some way. This section examines two compile time attributes supported by C# out of the box: Obsolete and Conditional.

Obsolete
This attribute is used to indicate that a type or member should not be used anymore. The compiler then yields a message each time it detects a member or type marked as obsolete.

void Test (){	SomeOldMethod();}[Obsolete("Use something else", /*IsError*/ false)]void SomeOldMethod(){}

Compile the preceding code and you should see the following warning:

c:...: warning CS0618:'...SomeOldMethod()' is obsolete: 'Use something else'

This kind of message is helpful for code maintenance, plus the attribute gives you a nice way to extend the language without polluting the grammar with unnecessary “obsolete” keywords.

Conditional
This attribute tells the C# compiler that method calls should be discarded unless you apply a specified preprocessing identifier to the calling entity.

void Test (){	WriteSomeDebugInfo();}[System.Diagnostics.Conditional("DEBUG")]void WriteSomeDebugInfo(){ //... do something here}

In debug mode, the compiler preserves the call to WriteSomeDebugInfo because the “DEBUG” preprocessor identifier is applied. In release mode however, the call is removed. This approach is superior to existing C++ preprocessor techniques because it works across compiled assemblies.

Attribute name

Description

System.ObsoleteAttribute

?

A warning or an error will be raised each time this element is used

System.Diagnostics.ConditionalAttribute

?

The compiler will discard any call to the method unless the preprocessor identifier is defined

System.CLSCompliantAttribute

?

Raises an error if the element is not CLS compliant

System.AttributeUsageAttribute

?

Specifies where the attribute can be applied

Table 3: Compile time Attributes supported by Microsoft.

Extensible C#
eXtensible C# (XC#) brings the idea of extensibility to a new level. XC# is both fully compatible with C# and extensible. In addition to all C# language features, XC# also supports arbitrary custom compile time attributes. This means that XC# allows you to go beyond the limited set of compile time attributes supported by C#. XC# comes with compile time attributes for obfuscation, code coverage, declarative assertions, and code verification at the same time that it understands your own compile time attribute. The rest of this article will focus on one of the XC# applications: declarative assertion attributes.

Imperative and Declarative Assertions
Remember imperative and declarative security? The same concept can be applied to assertions.In a nutshell, assertions are Boolean expressions that should always be true. They are incredibly useful in specifying and debugging code.

However, one common mistake is to confuse assertions with exceptions. Exceptions model an abnormal termination, which can occur in correct code and be caught. On the other hand, assertions should never arise; an assertion violation indicates a bug.

Class Debug in the System.Diagnostics namespace provides many variants of the Assert method. Suppose I want to write the hash code of some parameter:

void WriteHashCode (object o){	Debug.Assert(o != null);	Console.WriteLine(o.GetHashCode());}

Note that I Assert that o is not null before getting the hash code. Without the call to Assert, passing null to WriteHashCode would result in a NullReferenceException.

XC# goes further by supporting declarative assertions:

[Requires ("o != null")]void WriteHashCode2 (object o){	Console.WriteLine(o.GetHashCode());}

In this case, the XC# compiler adds a call to Assert at the beginning of the method. Because this technique can be fairly verbose, XC# comes with a short form for widespread assertions.

void WriteHashCode3 ([NotNull] object o){	Console.WriteLine(o.GetHashCode());}

Notice that in this case, the assertion applies to the parameter itself.

Besides being easier to write than imperative assertions, declarative assertions have two benefits.

First, declarative assertions are part of the metadata. A client could enumerate the assertions of a given method and do something interesting with them.

Second, declarative assertions are inherited. Suppose for example that the WriteHashCode method implements an interface method. In this case, it is more judicious to declare the assertion at the interface level.

interface IHashCodeWriter{	void WriteHashCode ([NotNull] object o);}

Because the interface method does not have any code, no call to Assert is generated at this point. However, the implementer does not need to declare the assertion, the compiler will do it automatically.

class ConsoleHashCodeWriter: IHashCodeWriter{	public void WriteHashCode (object o)	{		// Call to Debug.Assert automatically generated here		Console.WriteLine(o);	}}

Flexibility and Extensibility
Custom attributes provide a powerful way to communicate something about your component. Custom attributes can be user attributes, runtime attributes, or compile time attributes depending on the element of the system they interact with. The main point about attributes is extensibility. Not only can you apply attributes to your code but you can also define your own attribute with your own specific semantic. For example, some use attributes to specify tests. In fact, the limit is your imagination.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist