8 minutes 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
Person.cs
PersonController.cs
Job.cs

_13
class Person
_13
{
_13
[Obsolete("GetAge is deprecated, please use GetBirthDate instead.")]
_13
public int GetAge()
_13
{
_13
// Omitted code for brevity
_13
}
_13
_13
public DateTime GetBirthDate()
_13
{
_13
// Omitted code for brevity
_13
}
_13
}

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

WarningAttribute.cs

_10
class WarningAttribute : Attribute
_10
{
_10
_10
}

UseAttribute.cs

_10
[Warning]
_10
class ExpensiveCalculations
_10
{
_10
_10
}

WarningAttribute.cs

_10
class WarningAttribute : Attribute
_10
{
_10
_10
}

UseAttribute.cs

_10
[Warning]
_10
class ExpensiveCalculations
_10
{
_10
_10
}

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

WarningAttribute.cs

_10
class WarningAttribute : Attribute
_10
{
_10
_10
}

UseAttribute.cs

_10
[Warning]
_10
class ExpensiveCalculations
_10
{
_10
_10
}

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

WarningAttribute.cs

_11
class WarningAttribute : Attribute
_11
{
_11
private readonly string _message;
_11
private readonly WarningType _type;
_11
_11
public WarningAttribute(string message, WarningType type = WarningType.Normal)
_11
{
_11
_message = message;
_11
_type = type;
_11
}
_11
}

UseAttribute.cs

_10
[Warning("Prefer 'CheapCalculations'")]
_10
class ExpensiveCalculations
_10
{
_10
_10
}

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

WarningAttribute.cs

_13
class WarningAttribute : Attribute
_13
{
_13
public string Author { get; set; }
_13
_13
private readonly string _message;
_13
private readonly WarningType _type;
_13
_13
public WarningAttribute(string message, WarningType type = WarningType.Normal)
_13
{
_13
_message = message;
_13
_type = type;
_13
}
_13
}

UseAttribute.cs

_10
[Warning("Prefer 'CheapCalculations'", Author = "Lars Volkers")]
_10
class ExpensiveCalculations
_10
{
_10
_10
}

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

_14
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Field)]
_14
class WarningAttribute : Attribute
_14
{
_14
public string Author { get; set; }
_14
_14
private readonly string _message;
_14
private readonly WarningType _type;
_14
_14
public WarningAttribute(string message, WarningType type = WarningType.Normal)
_14
{
_14
_message = message;
_14
_type = type;
_14
}
_14
}

UseAttribute.cs

_10
class ExpensiveCalculations
_10
{
_10
[Warning("Prefer 'CheapCalculations.RecursiveLoop'", Author = "Lars Volkers")]
_10
public int[] RecursiveLoop(int[] arr)
_10
{
_10
// Omitted code for brevity
_10
}
_10
}

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

_12
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
_12
class WarningAttribute : Attribute
_12
{
_12
private readonly string _message;
_12
private readonly WarningType _type;
_12
_12
public WarningAttribute(string message, WarningType type = WarningType.Normal)
_12
{
_12
_message = message;
_12
_type = type;
_12
}
_12
}

UseAttribute.cs

_10
[Warning("Prefer 'CheapCalculations'")]
_10
[Warning("Class is resource intensive")]
_10
class ExpensiveCalculations
_10
{
_10
_10
}

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.

AlcoholRestrictionAttribute.cs
Order.cs
Customer.cs

_28
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
_28
public class AlcoholRestrictionAttribute : ValidationAttribute
_28
{
_28
_28
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
_28
{
_28
if (value is Order order)
_28
{
_28
var restrictedAge = GetAgeRestrictionByCountry(order.Country);
_28
Customer customer = order.Buyer;
_28
if (customer.Age < restrictedAge)
_28
{
_28
return new ValidationResult($"We only sell alcohol above age {restrictedAge} for country {order.Country}");
_28
}
_28
}
_28
}
_28
_28
private int GetAgeRestrictionByCountry(string country)
_28
{
_28
return country switch
_28
{
_28
"USA" => 21,
_28
"Netherlands" => 18,
_28
// ...etc
_28
_ => throw new ArgumentOutOfRangeException($"Value {country} for argument {nameof(country)} not supported")
_28
};
_28
}
_28
}

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

_10
//...
_10
Type orderType = typeof(Order);
_10
Type attributeType = typeof(AlcoholRestrictionAttribute);
_10
object myAttribute = Attribute.GetCustomAttribute (orderType, attributeType);
_10
if(myAttribute is AlcoholRestrictionAttribute restrictionAttribute)
_10
{
_10
//Our attribute is found
_10
}
_10
//...

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

_14
_14
//[AttributeUsage(AttributeTargets.Class)]
_14
var classAttribute = Attribute.GetCustomAttribute(classType, attributeType);
_14
_14
//[AttributeUsage(AttributeTargets.Constructor)]
_14
var constructorTypes = classType.GetConstructors();
_14
var constructorAttributes = constructorTypes
_14
.Select(ctor => Attribute.GetCustomAttribute(ctor, attributeType));
_14
_14
//[AttributeUsage(AttributeTargets.Parameters)]
_14
var methodType = classType.GetMethod("MyMethod");
_14
var parameterTypes = methodType.GetParameters();
_14
var parameterAttributes = parameterTypes
_14
.Select(parm => Attribute.GetCustomAttribute(parm, attributeType));

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.

BadExample.cs
CorrectExample.cs

_16
public class Dog : IOmnivore
_16
{
_16
void IAnimal.Eat(Food food)
_16
{
_16
// Omitted code for brevity
_16
}
_16
}
_16
_16
interface IAnimal
_16
{
_16
void Eat(Food food);
_16
}
_16
_16
interface IHerbivore : IAnimal { }
_16
interface ICarnivore : IAnimal { }
_16
interface IOmnivore : IAnimal { }

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.

ApplyPropertyExtensions.cs
FromPropertyAttribute.cs
Order.cs
Customer.cs

_37
public static class ApplyPropertyExtensions
_37
{
_37
//using our attribute in a generic extension method
_37
public static void ApplyTo<T, U>(this T thisObject, U otherObject)
_37
where T : class
_37
where U : class
_37
{
_37
var thisType = typeof(T);
_37
var otherType = typeof(U);
_37
_37
var otherProperties = otherType.GetProperties();
_37
foreach (var property in otherProperties)
_37
{
_37
var ourAttribute = Attribute.GetCustomAttribute(property, typeof(FromPropertyAttribute));
_37
if (ourAttribute is not FromPropertyAttribute fromPropertyAttribute || fromPropertyAttribute.OtherType != thisType)
_37
{
_37
continue;// move to next property if types don't match or attribute is not found
_37
}
_37
_37
var thisProperty = thisType.GetProperty(fromPropertyAttribute.PropertyName);
_37
if (thisProperty == null)
_37
{
_37
continue;// move to next property if property name is not found
_37
}
_37
_37
var propertyValue = thisProperty.GetValue(thisObject);
_37
if (fromPropertyAttribute.Converter != null)
_37
{
_37
var converter = (IPropertyConverter)Activator.CreateInstance(fromPropertyAttribute.Converter); //create our converter
_37
var convertedValue = converter.Convert(propertyValue);
_37
property.SetValue(otherObject, convertedValue);
_37
continue;
_37
}
_37
property.SetValue(otherObject, propertyValue);
_37
}
_37
}
_37
}

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

_18
public class NotGenericAttribute : Attribute
_18
{
_18
public Type CustomConverter { get; }
_18
_18
private string GetMessage() =>
_18
$"Converter should implement {nameof(IConverter)}, but it doesn't";
_18
_18
public NotGenericAttribute(Type customConverter)
_18
{
_18
if (customConverter.GetInterface(nameof(IConverter)) == null)
_18
{
_18
//this exception is only thrown at runtime
_18
//there are no compiler errors when customConverter doesn't implement IConverter
_18
throw new ArgumentException(GetMessage(), nameof(customConverter));
_18
}
_18
CustomConverter = customConverter;
_18
}
_18
}

To:

GenericAttribute.cs

_12
public class GenericAttribute<T> : Attribute
_12
where T : IConverter
_12
//compiler shows error when customConverter doesn't implement IConverter
_12
{
_12
public T CustomConverter { get; }
_12
_12
public GenericAttribute(T customConverter)
_12
{
_12
//no type checks needed
_12
CustomConverter = customConverter;
_12
}
_12
}

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.