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
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);
- }
- }
- }
- }
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.
- 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 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.
- /*
- *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 method 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 method 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.
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();
- }
- }
- }
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.
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.Constructor | 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 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