Here, we’re going to explore the basics of attributes, what are the common ones, how to create and read attributes.
Introduction
If you have been using the C# language for a while now, you are using the built-in attributes (e.g., [Serializable], [Obsolete]
), but you haven’t deeply thought about it? In this post, we’re going to explore the basics of attributes, what are the common ones, how to create and read attributes. What’s exciting is you will see how to get the built-in attributes using System.Reflection
. OK, then let’s get started.
Background
This is a bit off-topic but it's good to share. Do you know that? When we chill and read books, more often than not our mind starts to wonder. And, it happened to me when I was reading a C# book, I started to wonder about, how I can get those built-in attributes via System.Reflection
. Thus, this article came to life.
What are Attributes?
Attributes are important, and it provides additional information which gives the developers a clue. Especially on what to expect, with the behavior of a class and class’ properties and/or methods within your application.
In short, attributes are like adjectives that describe a type, assembly, module, method and so on.
Things to Remember about Attributes
- Attributes are classes derived from
System.Attribute
- Attributes can have parameters
- Attributes can omit the
Attribute
portion of the attribute name when using the attribute in code. The framework will handle the attribute correctly either way.
Types Of Attributes
Intrinsic Attributes
These attributes are also known as predefined or built-in attributes. The .NET Framework/.NET Core provides hundreds or even thousands of built-in attributes. Most of them are specialized, but we will try to extract them programmatically and discuss some of the most commonly known ones.
Commonly Known Built-in Attributes
Attributes | Description |
[Obsolete] | System.ObsoleteAttribute
Helps you identify obsolete bits of your code’s application. |
[Conditional] | System.Diagnostics.ConditionalAttribute
Gives you the ability to perform conditional compilation. |
[Serializable] | System.SerializableAttribute
Shows that a class can be serialized. |
[NonSerialized] | System.NonSerializedAttribute
Shows that a field of a serializable class shouldn’t be serialized. |
[DLLImport] | System.DllImportAttribute
Shows that a method is exposed by an unmanaged dynamic-link library (DLL) as a static entry point. |
Extract Built-in Types Via Reflection Using C#
As promised, we are going to see how to extract the built-in attributes using C#. See the sample code below:
using System;
using System.Linq;
using System.Reflection;
using Xunit;
using Xunit.Abstractions;
namespace CSharp_Attributes_Walkthrough {
public class UnitTest_Csharp_Attributes {
private readonly ITestOutputHelper _output;
private readonly string assemblyFullName = "System.Private.CoreLib,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e";
public UnitTest_Csharp_Attributes (ITestOutputHelper output) {
this._output = output;
}
[Fact]
public void Test_GetAll_BuiltIn_Attributes () {
var assembly = Assembly.Load (assemblyFullName);
var attributes = assembly
.DefinedTypes
.Where (type =>
type
.IsSubclassOf (typeof (Attribute)));
foreach (var attribute in attributes) {
string attr = attribute
.Name
.Replace ("Attribute", "");
this._output
.WriteLine ("Attribute: {0} and Usage: [{1}]", attribute.Name, attr);
}
}
}
}
See the output below:
Reading Attributes at Runtime
Now, that we have answered what attributes are, what are the commonly used ones, and the how-to extract the built-in attributes via System.Reflection
, let us see how we can read these attributes at runtime, of course using System.Reflection
.
When retrieving attribute values at runtime, there are two ways for us to retrieve values.
- Use the
GetCustomAttributes()
method, this returns an array containing all of the attributes of the specified type. You can use this when you aren’t sure which attributes apply to a particular type, you can iterate through this array.
- Use the
GetCustomAttribute()
method, this returns the details of the particular attribute that you want.
OK, then let’s get into an example.
Let us first try to create a class and label it with some random attributes.
using System;
using System.Diagnostics;
namespace CSharp_Attributes_Walkthrough.My_Custom_Attributes
{
[Serializable]
public class Product
{
public string Name { get; set; }
public string Code { get; set; }
[Obsolete("This method is already obselete. Use the ProductFullName instead.")]
public string GetProductFullName()
{
return $"{this.Name} {this.Code}";
}
[Conditional("DEBUG")]
public void RunOnlyOnDebugMode()
{
}
}
}
The Product
-class is quite easy to understand. With the example below, we want to check the following:
- Check if the
Product
-class does have a [Serializable]
attribute. - Check if the
Product
-class has two methods. - Check if each method does have attributes.
- Check if the method
GetProductFullName
is using the [Obsolete]
attribute. - Check if the method
RunOnlyDebugMode
is using the [Conditional]
attribute.
[Fact]
public void Test_Read_Attributes()
{
var type = typeof(Product);
var attribute = (SerializableAttribute)type.
GetCustomAttributes(typeof(SerializableAttribute), false).FirstOrDefault();
Assert.NotNull(attribute);
Assert.IsType<SerializableAttribute>(attribute);
var methods = type.GetMethods(BindingFlags.Instance |
BindingFlags.Public |
BindingFlags.DeclaredOnly)
.Where(method => !method.IsSpecialName).ToArray();
Assert.True(methods.Length == 2);
Assert.True(methods[0].Name == "GetProductFullName");
Assert.True(methods[1].Name == "RunOnlyOnDebugMode");
Assert.True(methods.All( method =>method.GetCustomAttributes(false).Length ==1));
var obsoleteAttribute = methods[0].GetCustomAttribute<ObsoleteAttribute>();
Assert.IsType<ObsoleteAttribute>(obsoleteAttribute);
var conditionalAttribute = methods[1].GetCustomAttribute<ConditionalAttribute>();
Assert.IsType<ConditionalAttribute>(conditionalAttribute);
}
Hopefully, you have enjoyed the example above. Let’s get into the custom-attributes then.
Custom Attributes
The built-in attributes are useful and important, but for the most part, they have specific uses. Moreover, if you think you need an attribute for some reason that didn’t contemplate the built-in ones, you can create your own.
Creating Custom Attributes
In this section, you will see how we can create custom attributes and what are the things we need to remember when creating one.
- To create a custom attribute, you define a class that derives from
System.Attribute
.
using System;
namespace CSharp_Attributes_Walkthrough.My_Custom_Attributes
{
public class AliasAttribute : Attribute
{
}
}
- Positional parameters – if you have any parameters within the constructor of your custom attribute, it will become the mandatory positional parameter.
using System;
namespace CSharp_Attributes_Walkthrough.My_Custom_Attributes
{
public class AliasAttribute : Attribute
{
public AliasAttribute(string alias, ConsoleColor color)
{
this.Alias = alias;
this.Color = color;
}
public string Alias { get; private set; }
public ConsoleColor Color { get; private set; }
}
}
- Optional parameters – These are the
public
fields and public
writeable properties of the class which derives from the System.Attribute
.
using CSharp_Attributes_Walkthrough.My_Custom_Attributes;
using System;
namespace CSharp_Attributes_Walkthrough.My_Custom_Attributes
{
public class AliasAttribute : Attribute
{
public string AlternativeName { get; set; }
}
}
See the complete sample code below:
using System;
namespace CSharp_Attributes_Walkthrough.My_Custom_Attributes
{
public class AliasAttribute : Attribute
{
public AliasAttribute(string alias, ConsoleColor color)
{
this.Alias = alias;
this.Color = color;
}
#region Positional-Parameters
public string Alias { get; private set; }
public ConsoleColor Color { get; private set; }
#endregion
public string AlternativeName { get; set; }
}
}
See the figure below to visualize the difference between positional and optional parameters.
Now that we have created a custom attribute, let us try using it in a class.
Apply Custom Attribute in a Class
using System;
using System.Linq;
namespace CSharp_Attributes_Walkthrough.My_Custom_Attributes
{
[Alias("Filipino_Customers", ConsoleColor.Yellow)]
public class Customer
{
[Alias("Fname", ConsoleColor.White, AlternativeName = "Customer_FirstName")]
public string Firstname { get; set; }
[Alias("Lname", ConsoleColor.White, AlternativeName = "Customer_LastName")]
public string LastName { get; set; }
public override string ToString()
{
Type instanceType = this.GetType();
string current_namespace = (instanceType.Namespace) ?? "";
string alias = (this.GetType().GetCustomAttributes(false).FirstOrDefault()
as AliasAttribute)?.Alias;
return $"{current_namespace}.{alias}";
}
}
}
The main meat of the example is the ToString()
method, which by default returns the fully-qualified name of the type. However; what we have done here, is that we have overridden the ToString()
method to return the fully-qualified name and the alias-name of the attribute.
Let us now try to call the ToString()
method and see what does it returns. See the example below with the output:
using CSharp_Attributes_Walkthrough.My_Custom_Attributes;
using System;
namespace Implementing_Csharp_Attributes_101
{
class Program
{
static void Main(string[] args)
{
var customer = new Customer { Firstname = "Jin Vincent" , LastName = "Necesario" };
var aliasAttributeType = customer.GetType();
var attribute =
aliasAttributeType.GetCustomAttributes(typeof(AliasAttribute), false);
Console.ForegroundColor = ((AliasAttribute)attribute[0]).Color;
Console.WriteLine(customer.ToString());
Console.ReadLine();
}
}
}
Limiting Attributes Usage
By default, you can apply a custom-attribute to any entity within your application-code. As a result, when you created a custom-attribute, it can be applied to a class, method, a private field, property, struct, and so on. However, if you want to limit your custom-attributes to appearing only on certain types of entities. You can use the AttributeUsage
attribute to control to which entities it can be applied.
Value | Target |
AttributeTargets.All | Can be applied to any entity in the application |
AttributeTargets.Assembly | Can be applied to an assembly |
AttributeTargets.Class | Can be applied to a class |
AttributeTargets.Construtor | Can be applied to a constructor |
AttributeTargets.Delegate | Can be applied to a delegate |
AttributeTargets.Enum | Can be applied to enumeration |
AttributeTargets.Event | Can be applied to an event |
AttributeTargets.Field | Can be applied to a field |
AttributeTargets.Interface | Can be applied to interface |
AttributeTargets.Method | Can be applied to a method |
AttributeTargets.Module | Can be applied to a module |
AttributeTargets.Parameter | Can be applied to a parameter |
AttributeTargets.Property | Can be applied to a property |
AttributeTargets.ReturnValue | Can be applied to a return value |
AttributeTargets.Struct | Can be applied to a structure |
If you are wondering, how we can get those AttributeTargets
at runtime? See the example below:
[Fact]
public void Test_GetAll_AttributeTargets()
{
var targets = Enum.GetNames(typeof(AttributeTargets));
foreach (var target in targets)
{
this._output.WriteLine($"AttributeTargets.{target}");
}
}
Summary
In this post, we have discussed the following:
- What are attributes?
- Things to Remember about Attributes
- Types Of Attributes
- Intrinsic Attributes
- Commonly Known Built-in Attributes
- Extract Built-in Types via Reflection Using C#
- Reading Attributes at Runtime
- Custom Attributes
- Creating Custom Attributes
- Apply Custom Attribute in a Class
- Limiting Attributes Usage
I hope you have enjoyed this article, as I have enjoyed writing it. Stay tuned for more. Until next time, happy programming!
Lastly, you can download the sample code
here.