A Walkthrough C# Attributes

Introduction

If you have been using the C# language for a while now, and you are using the built-in attributes (e.g. [Serializable], [Obsolete]) but you haven’t deeply thought about it. Thus, 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 its 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

AttributesDescription
[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.

Built-in Attributes of the .NET Core

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 of System.Reflection.

When retrieving attribute values at runtime, there 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.
/*
*This test will read the Product-class at runtime to check for attributes. 
*1. Check if [Serializable] has been read. 
*2. Check if the product-class has two methods 
*3. Check if each methods does have attributes. 
*4. Check if the method GetProudctFullName is using the Obsolete attribute. 
*5. Check if the method RunOnlyOnDebugMode is using the Conditional attribute.
*/
[Fact]
public void Test_Read_Attributes()
{
    //get the Product-class
    var type = typeof(Product);

    //Get the attributes of the Product-class and we are expecting the [Serializable]
    var attribute = (SerializableAttribute)type.
                    GetCustomAttributes(typeof(SerializableAttribute), false).FirstOrDefault();

    Assert.NotNull(attribute);

    //Check if [Serializable] has been read.
    //Let's check if the type of the attribute is as expected
    Assert.IsType<SerializableAttribute>(attribute);

    //Let's get only those 2 methods that we have declared 
    //and ignore the special names (these are the auto-generated setter/getter)
    var methods = type.GetMethods(BindingFlags.Instance | 
                                    BindingFlags.Public | 
                                    BindingFlags.DeclaredOnly)
                        .Where(method => !method.IsSpecialName).ToArray();

    //Check if the product-class has two methods 
    //Let's check if the Product-class has two methods.
    Assert.True(methods.Length == 2);

    Assert.True(methods[0].Name == "GetProductFullName");
    Assert.True(methods[1].Name == "RunOnlyOnDebugMode");

    //Check if each methods does have attributes. 
    Assert.True(methods.All( method =>method.GetCustomAttributes(false).Length ==1));

    //Let's get the first method and its attribute. 
    var obsoleteAttribute = methods[0].GetCustomAttribute<ObsoleteAttribute>();

    // Check if the method GetProudctFullName is using the Obsolete attributes. 
    Assert.IsType<ObsoleteAttribute>(obsoleteAttribute);

    //Let's get the second method and its attribute. 
    var conditionalAttribute = methods[1].GetCustomAttribute<ConditionalAttribute>();

    //Check if the method RunOnlyOnDebugMode is using the Conditional attributes.
    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 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
    {
        //This is how to define a custom attributes.
    }
}
  • 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
    {
        /// <summary>
        /// These parameters will become mandatory once have you decided to use this attribute.
        /// </summary>
        /// <param name="alias"></param>
        /// <param name="color"></param>
        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
    {
        //....

        //Added an optional-parameter
        public string AlternativeName { get; set; }
    }
}

See the complete sample code below.

using System;

namespace CSharp_Attributes_Walkthrough.My_Custom_Attributes
{
    public class AliasAttribute : Attribute
    {
        /// <summary>
        /// These parameters will become mandatory once have you decided to use this attribute.
        /// </summary>
        /// <param name="alias"></param>
        /// <param name="color"></param>
        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 

        //Added an optional-parameter
        public string AlternativeName { get; set; }
    }
}

See the figure below to visualize the difference between positional and optional parameters.

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()
        {
            //get the current running instance.
            Type instanceType = this.GetType(); 

            //get the namespace of the running instance.
            string current_namespace = (instanceType.Namespace) ?? "";

            //get the alias.
            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.

ValueTarget
AttributeTargets.AllCan be applied to any entity in the application
AttributeTargets.AssemblyCan be applied to an assembly
AttributeTargets.ClassCan be applied to a class
AttributeTargets.ConstrutorCan be applied to a constructor
AttributeTargets.DelegateCan be applied to a delegate
AttributeTargets.EnumCan be applied to enumeration
AttributeTargets.EventCan be applied to an event
AttributeTargets.FieldCan be applied to a field
AttributeTargets.InterfaceCan be applied to interface
AttributeTargets.MethodCan be applied to a method
AttributeTargets.ModuleCan be applied to a module
AttributeTargets.ParameterCan be applied to a parameter
AttributeTargets.PropertyCan be applied to a property
AttributeTargets.ReturnValueCan be applied to a return value
AttributeTargets.StructCan 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!