Friday, November 19, 2021

A Walkthrough of C# Attributes

Introduction

 
If you have been using the C# language for a while now,  you are probably 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 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 they provide additional information which gives the developers a clue about what to expect with the behavior of a class and its properties and/or methods within the 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.
  1. using System;  
  2. using System.Linq;  
  3. using System.Reflection;  
  4. using Xunit;  
  5. using Xunit.Abstractions;  
  6.   
  7. namespace CSharp_Attributes_Walkthrough {  
  8.     public class UnitTest_Csharp_Attributes {  
  9.         private readonly ITestOutputHelper _output;  
  10.   
  11.         private readonly string assemblyFullName = "System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e";  
  12.   
  13.         public UnitTest_Csharp_Attributes (ITestOutputHelper output) {  
  14.             this._output = output;  
  15.         }  
  16.   
  17.         [Fact]  
  18.         public void Test_GetAll_BuiltIn_Attributes () {  
  19.             var assembly = Assembly.Load (assemblyFullName);  
  20.   
  21.             var attributes = assembly  
  22.                 .DefinedTypes  
  23.                 .Where (type =>  
  24.                     type  
  25.                     .IsSubclassOf (typeof (Attribute)));  
  26.   
  27.             foreach (var attribute in attributes) {  
  28.                   
  29.                 string attr = attribute  
  30.                     .Name  
  31.                     .Replace ("Attribute""");  
  32.   
  33.                 this._output  
  34.                     .WriteLine ("Attribute: {0} and Usage: [{1}]", attribute.Name, attr);  
  35.             }  
  36.         }  
  37.     }  
  38. }  
Output
 
 

Reading Attributes At Runtime

 
Now, that we have answered what attributes are, the commonly used ones, and 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.
  1. using System;  
  2. using System.Diagnostics;  
  3.   
  4. namespace CSharp_Attributes_Walkthrough.My_Custom_Attributes  
  5. {  
  6.     [Serializable]  
  7.     public class Product  
  8.     {  
  9.         public string Name { getset; }  
  10.         public string Code { getset; }  
  11.   
  12.         [Obsolete("This method is already obselete. Use the ProductFullName instead.")]  
  13.         public string GetProductFullName()  
  14.         {  
  15.             return $"{this.Name} {this.Code}";  
  16.         }  
  17.   
  18.         [Conditional("DEBUG")]  
  19.         public void RunOnlyOnDebugMode()  
  20.         {  
  21.   
  22.         }  
  23.     }  
  24. }  
The Product-class is quite easy to understand. With the example below, we want to check the following,
  • Check if the Product-class has a [Serializable] attribute.
  • Check if the Product-class has two methods
  • Check if each method has  attributes.
  • Check if the method GetProductFullName is using the [Obsolete] attribute.
  • Check if the method RunOnlyDebugMode is using the [Conditional] attribute.
  1. /* 
  2. *This test will read the Product-class at runtime to check for attributes.  
  3. *1. Check if [Serializable] has been read.  
  4. *2. Check if the product-class has two methods  
  5. *3. Check if each method does have attributes.  
  6. *4. Check if the method GetProudctFullName is using the Obsolete attribute.  
  7. *5. Check if the method RunOnlyOnDebugMode is using the Conditional attribute. 
  8. */  
  9. [Fact]  
  10. public void Test_Read_Attributes()  
  11. {  
  12.     //get the Product-class  
  13.     var type = typeof(Product);  
  14.   
  15.     //Get the attributes of the Product-class and we are expecting the [Serializable]  
  16.     var attribute = (SerializableAttribute)type.  
  17.                     GetCustomAttributes(typeof(SerializableAttribute), false).FirstOrDefault();  
  18.   
  19.     Assert.NotNull(attribute);  
  20.   
  21.     //Check if [Serializable] has been read.  
  22.     //Let's check if the type of the attribute is as expected  
  23.     Assert.IsType<SerializableAttribute>(attribute);  
  24.   
  25.     //Let's get only those 2 methods that we have declared   
  26.     //and ignore the special names (these are the auto-generated setter/getter)  
  27.     var methods = type.GetMethods(BindingFlags.Instance |   
  28.                                     BindingFlags.Public |   
  29.                                     BindingFlags.DeclaredOnly)  
  30.                         .Where(method => !method.IsSpecialName).ToArray();  
  31.   
  32.     //Check if the product-class has two methods   
  33.     //Let's check if the Product-class has two methods.  
  34.     Assert.True(methods.Length == 2);  
  35.   
  36.     Assert.True(methods[0].Name == "GetProductFullName");  
  37.     Assert.True(methods[1].Name == "RunOnlyOnDebugMode");  
  38.   
  39.     //Check if each method does have attributes.   
  40.     Assert.True(methods.All( method =>method.GetCustomAttributes(false).Length ==1));  
  41.   
  42.     //Let's get the first method and its attribute.   
  43.     var obsoleteAttribute = methods[0].GetCustomAttribute<ObsoleteAttribute>();  
  44.   
  45.     // Check if the method GetProudctFullName is using the Obsolete attributes.   
  46.     Assert.IsType<ObsoleteAttribute>(obsoleteAttribute);  
  47.   
  48.     //Let's get the second method and its attribute.   
  49.     var conditionalAttribute = methods[1].GetCustomAttribute<ConditionalAttribute>();  
  50.   
  51.     //Check if the method RunOnlyOnDebugMode is using the Conditional attributes.  
  52.     Assert.IsType<ConditionalAttribute>(conditionalAttribute);  
  53. }  
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.
    1. using System;  
    2.   
    3. namespace CSharp_Attributes_Walkthrough.My_Custom_Attributes  
    4. {  
    5.     public class AliasAttribute : Attribute  
    6.     {  
    7.         //This is how to define a custom attributes.  
    8.     }  
    9. }  
  • Positional parameters
    If you have any parameters within the constructor of your custom attribute, it will become the mandatory positional parameter.
    1. using System;  
    2.   
    3. namespace CSharp_Attributes_Walkthrough.My_Custom_Attributes  
    4. {  
    5.     public class AliasAttribute : Attribute  
    6.     {  
    7.         /// <summary>  
    8.         /// These parameters will become mandatory once have you decided to use this attribute.  
    9.         /// </summary>  
    10.         /// <param name="alias"></param>  
    11.         /// <param name="color"></param>  
    12.         public AliasAttribute(string alias, ConsoleColor color)  
    13.         {  
    14.             this.Alias = alias;  
    15.             this.Color = color;  
    16.         }  
    17.   
    18.         public string  Alias { getprivate set; }  
    19.         public ConsoleColor Color { getprivate set; }  
    20.     }  
    21. }  
  • Optional parameters
    These are the public fields and public writeable properties of the class which derives from the System.Attribute.
    1. using CSharp_Attributes_Walkthrough.My_Custom_Attributes;  
    2. using System;  
    3.   
    4. namespace CSharp_Attributes_Walkthrough.My_Custom_Attributes  
    5. {  
    6.     public class AliasAttribute : Attribute  
    7.     {  
    8.         //....  
    9.   
    10.         //Added an optional-parameter  
    11.         public string AlternativeName { getset; }  
    12.     }  
    13. }  
See the complete sample code below.
  1. using System;  
  2.   
  3. namespace CSharp_Attributes_Walkthrough.My_Custom_Attributes  
  4. {  
  5.     public class AliasAttribute : Attribute  
  6.     {  
  7.         /// <summary>  
  8.         /// These parameters will become mandatory once have you decided to use this attribute.  
  9.         /// </summary>  
  10.         /// <param name="alias"></param>  
  11.         /// <param name="color"></param>  
  12.         public AliasAttribute(string alias, ConsoleColor color)  
  13.         {  
  14.             this.Alias = alias;  
  15.             this.Color = color;  
  16.         }  
  17.  
  18.         #region Positional-Parameters  
  19.         public string Alias { getprivate set; }  
  20.         public ConsoleColor Color { getprivate set; }  
  21.         #endregion   
  22.   
  23.         //Added an optional-parameter  
  24.         public string AlternativeName { getset; }  
  25.     }  
  26. }  
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,
  1. using System;  
  2. using System.Linq;  
  3.   
  4. namespace CSharp_Attributes_Walkthrough.My_Custom_Attributes  
  5. {  
  6.     [Alias("Filipino_Customers", ConsoleColor.Yellow)]  
  7.     public class Customer  
  8.     {  
  9.         [Alias("Fname", ConsoleColor.White, AlternativeName = "Customer_FirstName")]  
  10.         public string Firstname { getset; }  
  11.   
  12.         [Alias("Lname", ConsoleColor.White, AlternativeName = "Customer_LastName")]  
  13.         public string LastName { getset; }  
  14.   
  15.         public override string ToString()  
  16.         {  
  17.             //get the current running instance.  
  18.             Type instanceType = this.GetType();   
  19.   
  20.             //get the namespace of the running instance.  
  21.             string current_namespace = (instanceType.Namespace) ?? "";  
  22.   
  23.             //get the alias.  
  24.             string alias = (this.GetType().GetCustomAttributes(false).FirstOrDefault() as AliasAttribute)?.Alias;  
  25.   
  26.             return $"{current_namespace}.{alias}";  
  27.         }  
  28.     }  
  29. }  
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.
  1. using CSharp_Attributes_Walkthrough.My_Custom_Attributes;  
  2. using System;  
  3.   
  4. namespace Implementing_Csharp_Attributes_101  
  5. {  
  6.     class Program  
  7.     {  
  8.         static void Main(string[] args)  
  9.         {  
  10.             var customer = new Customer { Firstname = "Jin Vincent" , LastName = "Necesario" };  
  11.             
  12.             var aliasAttributeType = customer.GetType();  
  13.   
  14.             var attribute = aliasAttributeType.GetCustomAttributes(typeof(AliasAttribute), false);  
  15.   
  16.             Console.ForegroundColor = ((AliasAttribute)attribute[0]).Color;  
  17.   
  18.             Console.WriteLine(customer.ToString());  
  19.   
  20.             Console.ReadLine();  
  21.         }  
  22.     }  
  23. }  
Output
 
 
 

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.All Can be applied to any entity in the application.
AttributeTargets.AssemblyCan be applied to an assembly
AttributeTargets.ClassCan be applied to a class
AttributeTargets.ConstructorCan 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. 
  1. [Fact]  
  2. public void Test_GetAll_AttributeTargets()  
  3. {  
  4.     var targets = Enum.GetNames(typeof(AttributeTargets));  
  5.   
  6.     foreach (var target in targets)  
  7.     {  
  8.         this._output.WriteLine($"AttributeTargets.{target}");  
  9.     }  
  10.   
  11. }  

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 much as I have enjoyed writing it. Stay tuned for more and don't forget to download the attached project-source code.

No comments:

Post a Comment

No String Argument Constructor/Factory Method to Deserialize From String Value

  In this short article, we will cover in-depth the   JsonMappingException: no String-argument constructor/factory method to deserialize fro...