Introduction to Attributes in C#

Rasheed K Mozaffar - Sep 9 - - Dev Community

Hello There! πŸ‘‹πŸ»

If you've spent time working with C# and ASP.NET Core, you've likely come across a powerful feature known as Attributes. This feature enables developers to enrich and extend their code with metadata in a pretty neat way. Attributes are incredibly useful when building tools, source generators, or injecting additional behaviors into already existing code without changing the core logic.

That's the topic I have for you today, so if that sounds exciting to you, let's get the show going!

What is an Attribute?

I'll start by giving you some context on what attributes are, and how they're used, before we dive into more details.

An Attribute in C# is a normal class that is derived from System.Attribute that can add metadata to code you already have, this metadata can be a lot of things, which we'll discuss later on as we look into attributes in more depth and when we create our own custom ones (Yes we will do that here).

In case you haven't seen an attribute in a C# codebase before, these are some examples:


[Serializable]
[Obsolete]
[Route] // ASP.NET Core
[Required] // ASP.NET Core
[HttpGet] // ASP.NET Core
[HttpPost] // ASP.NET Core
...

Enter fullscreen mode Exit fullscreen mode

Each one of these represents a different attribute , each of which performs a separate augmentation to your code.

How Do We Use Attributes?

In C#, the use of attributes is done through annotation, we say that we annotate a member or a program element with attributes. The attributes are then used internally at compile-time or runtime to extend, modify or add behavior to the annotated code.

Let's look at a code sample:


[Serializable]
public class Person(string FirstName, string LastName)
{
    [NonSerialized]
    private DateTime dateOfBirth;

    [Obsolete("This method is depricated, use GetInitialsWithConcat instead")]
    public string GetInitials()
    {
        return FirstName[0] + "" + LastName[0];
    }

    public string GetInitialsWithConcat()
    {
        return string.Concat(FirstName[0], LastName[0]);
    }
}

Enter fullscreen mode Exit fullscreen mode

In this code, I've used 3 different attributes, [Serializable], [Obsolete], [NonSerialized], let me explain what each one does.

[Serializable]

When this attribute is applied to a class, it means that this class can be serialized, if you're not familiar, serialization is the process of taking an object, and transforming it into a format that can be transmitted, like JSON, XML or Binary.
This attribute means that instances can be serialized, however, the actual serialization is actually performed by another library or framework, such as JsonSerializer from System.Text.Json and so on.

[NonSerialized]

This attribute marks a member as not part of the serialization process, in the code I provided, the private field dateOfBirth is annotated with that attribute so that during serialization, this field is ignored.

[Obsolete]

This attribute allows you to display warnings at compile-time for you or your fellow developers about a piece of code that is no longer used, but you don't want to take it out of the codebase, so you annotate it with [Obsolete], and add a message to clarify to the other developers that hey, you should use this instead, and the like.

Now that you have an idea as to what's an attribute, and how do we use them, let's take a look at how we can create our own now.

Creating a Custom Attribute

You'll be surprised by how easy it is to build an attribute and start using it in your code, all you have to do is a create a class, derive it from System.Attribute, and add some parameters to it if you like.

The example attribute I'll be creating isn't something super useful, but it'll be enough to demonstrate how attributes are created, and what are the steps you need to take in order to make them function and actually augment your code.

Coding the [PropDisplayName] Attribute

We'll create an attribute that can be used on properties so that we can assign them a different display name that's more human readable.

For this example, I'll be using a console application to demonstrate the output, but feel free to follow along with any project type you like, the logic and concepts discussed apply everywhere.

Let's write some code

Create a new class and name it PropDisplayNameAttribute, and make it derive from Attribute, like this:


public class PropDisplayNameAttribute : Attribute
{

}

Enter fullscreen mode Exit fullscreen mode

It's a common naming convention to use the Attribute postfix when creating an attribute class. However, when using the attribute in your code, you can omit the postfix, In our case, we can use our custom attribute using the following syntax [PropDisplayName("Identifier")]

Now when creating attributes, we need to specify the targets of that attribute, in other words, the program elements that the attribute will be used for, like will that attribute be used on Methods, Classes, Properties and so on. How do we do that? Well, wait for it...

We use an attribute to annotate our custom attribute class! I bet you didn't see that one coming ;)

Add these lines to make our attribute class complete:


[AttributeUsage(AttributeTargets.Property)]
public class PropDisplayNameAttribute : Attribute
{
    public string DisplayName { get; private set; }

    public PropDisplayNameAttribute(string displayName)
    {
        DisplayName = displayName;
    }
}

Enter fullscreen mode Exit fullscreen mode

Let me break down the changes:

  • The [AttributeUsage] attribute is used to annotate a custom attribute class to define the targets of the attribute. One attribute can be applied to multiple targets, if you want to specify more than one, you can separate them with the | operator, like this: [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Constructor)]. That means an attribute can be applied to properties, fields, and constructors.
  • The DisplayName property is going to be used to provide a value that we can later read and make use of.
  • The constructor is what will allow us to pass down a value for the DisplayName property when we annotate the properties later on.

And that's really it! We just built a custom attribute.

How to Use a Custom Attribute

Now our attribute can be used on properties, but in reality, it doesn't do anything, an attribute alone can't do much if any at all. To make this recipe work, we need to add a second ingredient, whisk them together, and that is when our attribute, will come to life.

Enter Reflection! The wizard that will allow us to bring our [PropDisplayName] attribute to the light.

Reflection is a vast concept, and it's definitely something I'll cover later on in a dedicated post, but for now, I'm going to assume that you know what Reflection is, but I'll also explain the code I'll be writing just so that it makes sense to you.

Inside Program.cs, add the following code:


static List<string> GetDisplayNamesPropertyValues<T>()
{
    var typeProperties = typeof(T).GetProperties();
    var output = new List<string>();

    foreach (var property in typeProperties)
    {
        var displayNameAttribute = property.GetCustomAttribute<PropDisplayNameAttribute>();
        var displayName = displayNameAttribute is not null ? displayNameAttribute.DisplayName : property.Name;

        output.Add(displayName);
    }

    return output;
}

Enter fullscreen mode Exit fullscreen mode

The code creates a generic method and it uses reflection to first get all the properties of the provided type, then it iterates over each property and it gets the PropDisplayNameAttribute, it then checks if that attribute is null, it just reads the property's name, otherwise it gets the value from the attribute by reading the DisplayName property. At the end, it builds the output list of strings and returns it once done iterating the properties.

To test that, just add the following code to Program.cs:


foreach (var name in GetDisplayNamesPropertyValues<Person>())
{
    Console.WriteLine(name);
}


public class Person
{
    [PropDisplayName("First Name")]
    public string? FirstName { get; set; }

    [PropDisplayName("Last Name")]
    public string? LastName { get; set; }

    [PropDisplayName("Date of Birth")]
    public DateTime DateOfBirth { get; set; }
}

Enter fullscreen mode Exit fullscreen mode

If you run the code, you should see the names provided inside the attribute constructor printed for each property just as you would expect.

How is That Useful?

For the attribute we just created, you could actually take it a step further, and use it for example to generate dynamic components in your application based on the data models solely by decorating the properties of a data model with this attribute. Assuming you're using Blazor, you can create a dynamic table component, that takes a data type, and it reads the names of the properties, and it generates the appropriate columns based on that, which means you will have do design the table once, and be able to dynamically generate it for any data model you like. I'm sure you can also elaborate more on that idea to make it more useful, but you get the point!

Conclusion

If you made it this far, then massive congratulations to you! You've just learned what attributes are, what they do, how to use them, and how to build your own custom attributes and make them functional using Reflection. There's still way more to them than what I've discussed here, but the point was to give you an introduction that will teach you the basics and get you up to speed on this amazing feature of C#. Now you're all set to step up and start thinking of something cool, like a tool, source generator, or a package that you can build with the knowledge you've just gained!

Thanks for Reading! πŸ‘‹πŸ»

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .