Using Attributes in C#
Attributes provide a way of associating information with code in a declarative way. They can also provide a reusable element that can be applied to a variety of targets.
Consider the [Obsolete]
attribute. It can be applied to classes, structs, methods, constructors, and more. It declares that the element is obsolete. It's then up to the C# compiler to look for this attribute, and do some action in response.
In this tutorial, you'll be introduced to how to add attributes to your code, how to create and use your own attributes, and how to use some attributes that are built into .NET Core.
Prerequisites
You’ll need to setup your machine to run .NET core. You can find the installation instructions on the .NET Core page. You can run this application on Windows, Ubuntu Linux, macOS or in a Docker container. You’ll need to install your favorite code editor. The descriptions below use Visual Studio Code which is an open source, cross platform editor. However, you can use whatever tools you are comfortable with.
Create the Application
Now that you've installed all the tools, create a new .NET Core application. To use the command line generator, execute the following command in your favorite shell:
dotnet new console
This command will create barebones .NET core project files. You will need to execute dotnet restore
to restore the dependencies needed to compile this project.
Note
Starting with .NET Core 2.0 SDK, you don't have to run dotnet restore
because it's run implicitly by all commands that require a restore to occur, such as dotnet new
, dotnet build
and dotnet run
. It's still a valid command in certain scenarios where doing an explicit restore makes sense, such as continuous integration builds in Azure DevOps Services or in build systems that need to explicitly control the time at which the restore occurs.
To execute the program, use dotnet run
. You should see "Hello, World" output to the console.
How to add attributes to code
In C#, attributes are classes that inherit from the Attribute
base class. Any class that inherits from Attribute
can be used as a sort of "tag" on other pieces of code. For instance, there is an attribute called ObsoleteAttribute
. This is used to signal that code is obsolete and shouldn't be used anymore. You can place this attribute on a class, for instance, by using square brackets.
[Obsolete]
public class MyClass
{
}
Note that while the class is called ObsoleteAttribute
, it's only necessary to use [Obsolete]
in the code. This is a convention that C# follows. You can use the full name [ObsoleteAttribute]
if you choose.
When marking a class obsolete, it's a good idea to provide some information as to why it's obsolete, and/or what to use instead. Do this by passing a string parameter to the Obsolete attribute.
[Obsolete("ThisClass is obsolete. Use ThisClass2 instead.")]
public class ThisClass
{
}
The string is being passed as an argument to an ObsoleteAttribute
constructor, just as if you were writing var attr = new ObsoleteAttribute("some string")
.
Parameters to an attribute constructor are limited to simple types/literals: bool, int, double, string, Type, enums, etc
and arrays of those types. You can not use an expression or a variable. You are free to use positional or named parameters.
How to create your own attribute
Creating an attribute is as simple as inheriting from the Attribute
base class.
public class MySpecialAttribute : Attribute
{
}
With the above, I can now use [MySpecial]
(or [MySpecialAttribute]
) as an attribute elsewhere in the code base.
[MySpecial]
public class SomeOtherClass
{
}
Attributes in the .NET base class library like ObsoleteAttribute
trigger certain behaviors within the compiler. However, any attribute you create acts only as metadata, and doesn't result in any code within the attribute class being executed. It's up to you to act on that metadata elsewhere in your code (more on that later in the tutorial).
There is a 'gotcha' here to watch out for. As mentioned above, only certain types are allowed to be passed as arguments when using attributes. However, when creating an attribute type, the C# compiler won't stop you from creating those parameters. In the below example, I've created an attribute with a constructor that compiles just fine.
public class GotchaAttribute : Attribute
{
public GotchaAttribute(Foo myClass, string str) {
}
}
However, you will be unable to use this constructor with attribute syntax.
[Gotcha(new Foo(), "test")] // does not compile
public class AttributeFail
{
}
The above will cause a compiler error like Attribute constructor parameter 'myClass' has type 'Foo', which is not a valid attribute parameter type
How to restrict attribute usage
Attributes can be used on a number of "targets". The above examples show them on classes, but they can also be used on:
- Assembly
- Class
- Constructor
- Delegate
- Enum
- Event
- Field
- GenericParameter
- Interface
- Method
- Module
- Parameter
- Property
- ReturnValue
- Struct
When you create an attribute class, by default, C# will allow you to use that attribute on any of the possible attribute targets. If you want to restrict your attribute to certain targets, you can do so by using the AttributeUsageAttribute
on your attribute class. That's right, an attribute on an attribute!
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class MyAttributeForClassAndStructOnly : Attribute
{
}
If you attempt to put the above attribute on something that's not a class or a struct, you will get a compiler error like Attribute 'MyAttributeForClassAndStructOnly' is not valid on this declaration type. It is only valid on 'class, struct' declarations
public class Foo
{
// if the below attribute was uncommented, it would cause a compiler error
// [MyAttributeForClassAndStructOnly]
public Foo()
{ }
}
How to use attributes attached to a code element
Attributes act as metadata. Without some outward force, they won't actually do anything.
To find and act on attributes, Reflection is generally needed. I won't cover Reflection in-depth in this tutorial, but the basic idea is that Reflection allows you to write code in C# that examines other code.
For instance, you can use Reflection to get information about a class:
TypeInfo typeInfo = typeof(MyClass).GetTypeInfo();
Console.WriteLine("The assembly qualified name of MyClass is " + typeInfo.AssemblyQualifiedName);
That will print out something like: The assembly qualified name of MyClass is ConsoleApplication.MyClass, attributes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Once you have a TypeInfo
object (or a MemberInfo
, FieldInfo
, etc), you can use the GetCustomAttributes
method. This will return a collection of Attribute
objects. You can also use GetCustomAttribute
and specify an Attribute type.
Here's an example of using GetCustomAttributes
on a MemberInfo
instance for MyClass
(which we saw earlier has an [Obsolete]
attribute on it).
var attrs = typeInfo.GetCustomAttributes();
foreach(var attr in attrs)
Console.WriteLine("Attribute on MyClass: " + attr.GetType().Name);
That will print to console: Attribute on MyClass: ObsoleteAttribute
. Try adding other attributes to MyClass
.
It's important to note that these Attribute
objects are instantiated lazily. That is, they won't be instantiated until you use GetCustomAttribute
or GetCustomAttributes
. They are also instantiated each time. Calling GetCustomAttributes
twice in a row will return two different instances of ObsoleteAttribute
.
Common attributes in the base class library (BCL)
Attributes are used by many tools and frameworks. NUnit uses attributes like [Test]
and [TestFixture]
that are used by the NUnit test runner. ASP.NET MVC uses attributes like [Authorize]
and provides an action filter framework to perform cross-cutting concerns on MVC actions. PostSharp uses the attribute syntax to allow aspect-oriented programming in C#.
Here are a few notable attributes built into the .NET Core base class libraries:
-
[Obsolete]
. This one was used in the above examples, and it lives in theSystem
namespace. It is useful to provide declarative documentation about a changing code base. A message can be provided in the form of a string, and another boolean parameter can be used to escalate from a compiler warning to a compiler error. -
[Conditional]
. This attribute is in theSystem.Diagnostics
namespace. This attribute can be applied to methods (or attribute classes). You must pass a string to the constructor. If that string matches a#define
directive, then any calls to that method (but not the method itself) will be removed by the C# compiler. Typically this is used for debugging (diagnostics) purposes. -
[CallerMemberName]
. This attribute can be used on parameters, and lives in theSystem.Runtime.CompilerServices
namespace. This is an attribute that is used to inject the name of the method that is calling another method. This is typically used as a way to eliminate 'magic strings' when implementing INotifyPropertyChanged in various UI frameworks. As an example:
public class MyUIClass : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string _name;
public string Name
{
get { return _name;}
set
{
if (value != _name)
{
_name = value;
RaisePropertyChanged(); // notice that "Name" is not needed here explicitly
}
}
}
}
In the above code, you don't have to have a literal "Name"
string. This can help prevent typo-related bugs and also makes for smoother refactoring/renaming.