A 10 minute introduction to C# Attributes
C# Attributes are awesome! But since you clicked this post, something tells me you don’t agree (yet!). When I was a junior dev, not understanding Attributes, I simply dismissed them as something used by some frameworks.
However, now that I’ve used some and built some myself, attributes can really get me excited! You know what’s coming next, right? In this article, I’ll be introducing attributes, showing you how others use them, and what awesomeness you can create yourself with attributes. Enjoy!
C# attributes in a nutshell
Let’s start with the one thing that confused me the most: Attributes do NOT add behavior.You can use attributes to decorate all sorts of elements with custom metadata. None of this has any use until you add it.
The metadata added by Attributes can either be used during compile time with Source Generators or queried with reflection during runtime. This metadata can then be used by frameworks or your own generic code to decide how something should be handled.
Common attributes you might encounter are:
- informing the compiler an element is no longer in use
- tell the API framework what role a user needs to access an endpoint
- control how the debugger displays an object, property, or field
The most important thing to remember is that Attributes do not add behavior, but instead inform other features how they should behave.
My first custom attribute
Before we start exploring the awesomeness of C# attributes, let’s make a custom attribute ourselves. Definitely feel free to skip this section if this is not your first custom attribute.
Creating a custom attribute is as simple as creating a class that extends the Attribute
class.
Next, we can add positional parameters by adding a constructor to our attribute. Parameters can be made optional.
We can also add (optional) named parameters by adding a public property.
The last step is telling the compiler where our attribute can be used. We do this by adding the AttributeUsage
attribute to our class and defining the AttributeTargets
.
We can also allow the attribute to be specified more than one time with the named parameter AllowMultiple
. And we can specify whether derived classes can inherit the attribute with the named parameter Inherited
Creating a custom attribute is as simple as creating a class that extends the Attribute
class.
Next, we can add positional parameters by adding a constructor to our attribute. Parameters can be made optional.
We can also add (optional) named parameters by adding a public property.
The last step is telling the compiler where our attribute can be used. We do this by adding the AttributeUsage
attribute to our class and defining the AttributeTargets
.
We can also allow the attribute to be specified more than one time with the named parameter AllowMultiple
. And we can specify whether derived classes can inherit the attribute with the named parameter Inherited
Tell frameworks and libraries what to do
The most common attributes are the ones that have been predefined by Microsoft for particular tasks and are documented in MSDN. Frameworks and libraries often use attributes to let users determine how a framework or library should behave.
When serializing objects to JSON with either Newtonsoft’s Json.NET or Microsoft’s System.Text.Json you can use attributes to inform how an object should be serialized:
We also slowly start to see the awesomeness of custom attributes when you extend the ValidationAttribute
. This way you can add your own attributes which can be used by the model validation. Let’s see how we actually do that:
This time we extend ValidationAttribute
instead of the default Attribute
. With ValidationAttribute
we can override the IsValid
for our own validation logic. Since our AlcoholRestrictionAttribute
only makes sense when we're validating an Order
, we check if we are actually validating an Order
object. Next, we check if the Customer
s age is larger than the minimum age in the Country
where the Order
is placed. If that's not the case, we return a user-friendly error message.
Custom attributes for fun and profit
Now that we’ve seen how to create our own attributes and how frameworks and libraries use attributes, let’s see how we can use them ourselves.
Accessing your attributes works the same, whether you’re using them during runtime or in your source generators. In both cases, we need to use Reflection to query the metadata on the types we’re working with.
Actually retrieving an attribute is as simple as:
Now that we have our attribute instance we can use it like any other instance. The difficulty isn't how to retrieve our attribute. But depending on the AttributeTarget
of our custom attribute it might be a bit harder to get the right type through reflection. Below are some examples how to find different AttributeTarget
s.
But, why?
We’ve seen how to make our own attributes, how others use them, and how we can use them ourselves. But why would we want to use them?
Have you ever created several empty derived interfaces just to identify groups of types? This is where you should use attributes. Attributes are the preffered way to identify groups of types. They are also more flexible if you need complex criteria when grouping types.
Attributes are also awesome for when you need per-class customization in otherwise generic code. Let’s say you’re writing some custom code to generically map your DTOs to your domain models. Reflection to the rescue! But you don’t want both to have the exact named properties (or worse, use some weird naming conventions). This is where attributes really shine. With custom attributes, you can instruct your generic code on what properties should map to each other.
Finally, attributes are a great way to add opt-in functionality to your Source Generators. I’ll explore this option more in-depth in a future article. But for now, let’s just say you can combine the two examples above. Custom attributes can inform your Source Generator for which element it should run, while also allowing you to add extra information for how it should run.
Attributes, Generics, C#11
I hope you’re now just as excited about Attributes as I am!? However, C#11 makes attributes even more awesome by adding support for generics.
Previously, when you needed some —Type info— you had to use typeof
like in the previous example.
I don’t know about you, but I do not like the looks of that. The code not only feels convoluted, but we also miss out on any generic awesomeness.
Luckily, with C#11 we finally get support for generics. Allowing us to write our generic attributes as [MyCustomAttribute<SomeClass>]
instead of [MyCustomAttribute(typeof(SomeClass))
And I can already hear you think sarcastically; “wow, the syntax changed a little bit for the better…”
But that’s not everything, when using generics instead of a System.Type
parameter we can finally constrain the type passed to our attribute. Meaning, C#11 can help us go from:
To:
What do I think
You might have picked up a hint or two that I think attributes can be pretty awesome! And I really hope I convinced you as well.
Attributes allow me to write cleaner and smarter code that can be used in more than one situation. By adding declarative metadata where needed, I remove the necessity for complex conventions or opaque tricks.
I’m still finding new ways to use attributes. But I’m glad I took a deep dive into attributes after Sonar started screaming at me with:
Empty interfaces are usually used as a marker or a way to identify groups of types. The preferred way to achieve this is to use custom attributes.
Please come back to this article in the future. I plan on showing off Attributes together with Source generators. And I might add some cool snippets or examples if I encounter more uses for Attributes.