7 min read read

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.

Desk with a cup of coffee

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
1
class Person
2
{
3
[Obsolete("GetAge is deprecated, please use GetBirthDate instead.​")]
4
public int GetAge()
5
{
6
/​/​ Omitted code for brevity
7
}
8
9
public DateTime GetBirthDate()
10
{
11
/​/​ Omitted code for brevity
12
}
13
}
14

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.

Extending the

Creating a custom attribute is as simple as creating a class that extends the Attribute class.

Positional parameters

Next, we can add positional parameters by adding a constructor to our attribute. Parameters can be made optional.

Optional Parameters

We can also add (optional) named parameters by adding a public property.

AttributeUsage

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.

AllowMultiple and Inherited

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

WarningAttribute.cs
1
class WarningAttribute : Attribute
2
{
3
4
}
5
UseAttribute.cs
1
[Warning]
2
class ExpensiveCalculations
3
{
4
5
}
6

Extending the

Creating a custom attribute is as simple as creating a class that extends the Attribute class.

WarningAttribute.cs
1
class WarningAttribute : Attribute
2
{
3
4
}
5
UseAttribute.cs
1
[Warning]
2
class ExpensiveCalculations
3
{
4
5
}
6

Positional parameters

Next, we can add positional parameters by adding a constructor to our attribute. Parameters can be made optional.

WarningAttribute.cs
1
class WarningAttribute : Attribute
2
{
3
private readonly string _​message;
4
private readonly WarningType _​type;
5
6
public WarningAttribute(string message, WarningType type = WarningType.Normal)
7
{
8
_​message = message;
9
_​type = type;
10
}
11
}
12
UseAttribute.cs
1
[Warning("Prefer 'CheapCalculations'")]
2
class ExpensiveCalculations
3
{
4
5
}
6

Optional Parameters

We can also add (optional) named parameters by adding a public property.

WarningAttribute.cs
1
class WarningAttribute : Attribute
2
{
3
public string Author { get; set; }
4
5
private readonly string _​message;
6
private readonly WarningType _​type;
7
8
public WarningAttribute(string message, WarningType type = WarningType.Normal)
9
{
10
_​message = message;
11
_​type = type;
12
}
13
}
14
UseAttribute.cs
1
[Warning("Prefer 'CheapCalculations'", Author = "Lars Volkers")]
2
class ExpensiveCalculations
3
{
4
5
}
6

AttributeUsage

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.

WarningAttribute.cs
1
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Field)]
2
class WarningAttribute : Attribute
3
{
4
public string Author { get; set; }
5
6
private readonly string _​message;
7
private readonly WarningType _​type;
8
9
public WarningAttribute(string message, WarningType type = WarningType.Normal)
10
{
11
_​message = message;
12
_​type = type;
13
}
14
}
15
UseAttribute.cs
1
class ExpensiveCalculations
2
{
3
[Warning("Prefer 'CheapCalculations.​RecursiveLoop'", Author = "Lars Volkers")]
4
public int[] RecursiveLoop(int[] arr)
5
{
6
/​/​ Omitted code for brevity
7
}
8
}
9

AllowMultiple and Inherited

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

WarningAttribute.cs
1
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
2
class WarningAttribute : Attribute
3
{
4
private readonly string _​message;
5
private readonly WarningType _​type;
6
7
public WarningAttribute(string message, WarningType type = WarningType.Normal)
8
{
9
_​message = message;
10
_​type = type;
11
}
12
}
13
UseAttribute.cs
1
[Warning("Prefer 'CheapCalculations'")]
2
[Warning("Class is resource intensive")]
3
class ExpensiveCalculations
4
{
5
6
}
7

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 Customers 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.

1
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
2
public class AlcoholRestrictionAttribute : ValidationAttribute
3
{
4
5
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
6
{
7
if (value is Order order)
8
{
9
var restrictedAge = GetAgeRestrictionByCountry(order.Country);
10
Customer customer = order.Buyer;
11
if (customer.Age < restrictedAge)
12
{
13
return new ValidationResult($"We only sell alcohol above age {restrictedAge} for country {order.Country}");
14
}
15
}
16
}
17
18
private int GetAgeRestrictionByCountry(string country)
19
{
20
return country switch
21
{
22
"USA" => 21,
23
"Netherlands" => 18,
24
/​/​ .​.​.​etc
25
_ => throw new ArgumentOutOfRangeException($"Value {country} for argument {nameof(country)} not supported")
26
};
27
}
28
}
29

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:

OrderValidator.cs
1
/​/​.​.​.
2
Type orderType = typeof(Order);
3
Type attributeType = typeof(AlcoholRestrictionAttribute);
4
object myAttribute = Attribute.GetCustomAttribute (orderType, attributeType);
5
if(myAttribute is AlcoholRestrictionAttribute restrictionAttribute)
6
{
7
/​/​Our attribute is found
8
}
9
/​/​.​.​.

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 AttributeTargets.

AttributeByTarget.cs
1
2
/​/​[AttributeUsage(AttributeTargets.​Class)]
3
var classAttribute = Attribute.GetCustomAttribute(classType, attributeType);
4
5
/​/​[AttributeUsage(AttributeTargets.​Constructor)]
6
var constructorTypes = classType.GetConstructors();
7
var constructorAttributes = constructorTypes
8
.Select(ctor => Attribute.GetCustomAttribute(ctor, attributeType));
9
10
/​/​[AttributeUsage(AttributeTargets.​Parameters)]
11
var methodType = classType.GetMethod("MyMethod");
12
var parameterTypes = methodType.GetParameters();
13
var parameterAttributes = parameterTypes
14
.Select(parm => Attribute.GetCustomAttribute(parm, attributeType));
15
16

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.

1
public class Dog : IOmnivore
2
{
3
void IAnimal.Eat(Food food)
4
{
5
/​/​ Omitted code for brevity
6
}
7
}
8
9
interface IAnimal
10
{
11
void Eat(Food food);
12
}
13
14
interface IHerbivore : IAnimal { }
15
interface ICarnivore : IAnimal { }
16
interface IOmnivore : IAnimal { }
17

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.

1
public static class ApplyPropertyExtensions
2
{
3
/​/​using our attribute in a generic extension method
4
public static void ApplyTo<T, U>(this T thisObject, U otherObject)
5
where T : class
6
where U : class
7
{
8
var thisType = typeof(T);
9
var otherType = typeof(U);
10
11
var otherProperties = otherType.GetProperties();
12
foreach (var property in otherProperties)
13
{
14
var ourAttribute = Attribute.GetCustomAttribute(property, typeof(FromPropertyAttribute));
15
if (ourAttribute is not FromPropertyAttribute fromPropertyAttribute || fromPropertyAttribute.OtherType != thisType)
16
{
17
continue;/​/​ move to next property if types don't match or attribute is not found
18
}
19
20
var thisProperty = thisType.GetProperty(fromPropertyAttribute.PropertyName);
21
if (thisProperty == null)
22
{
23
continue;/​/​ move to next property if property name is not found
24
}
25
26
var propertyValue = thisProperty.GetValue(thisObject);
27
if (fromPropertyAttribute.Converter != null)
28
{
29
var converter = (IPropertyConverter)Activator.CreateInstance(fromPropertyAttribute.Converter); /​/​create our converter
30
var convertedValue = converter.Convert(propertyValue);
31
property.SetValue(otherObject, convertedValue);
32
continue;
33
}
34
property.SetValue(otherObject, propertyValue);
35
}
36
}
37
}
38

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:

NotGenericAttribute.cs
1
public class NotGenericAttribute : Attribute
2
{
3
public Type CustomConverter { get; }
4
5
private string GetMessage() =>
6
$"Converter should implement {nameof(IConverter)}, but it doesn't";
7
8
public NotGenericAttribute(Type customConverter)
9
{
10
if (customConverter.GetInterface(nameof(IConverter)) == null)
11
{
12
/​/​this exception is only thrown at runtime
13
/​/​there are no compiler errors when customConverter doesn't implement IConverter
14
throw new ArgumentException(GetMessage(), nameof(customConverter));
15
}
16
CustomConverter = customConverter;
17
}
18
}
19

To:

GenericAttribute.cs
1
public class GenericAttribute<T> : Attribute
2
where T : IConverter
3
/​/​compiler shows error when customConverter doesn't implement IConverter
4
{
5
public T CustomConverter { get; }
6
7
public GenericAttribute(T customConverter)
8
{
9
/​/​no type checks needed
10
CustomConverter = customConverter;
11
}
12
}
13

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.

What to read next:

I really hope you enjoyed this article. If you did, you might want to check out some of these articles I've written on similar topics.