Using Attributes and Metadata in C# 12

Overview

As part of C#, attributes allow developers to add metadata to their code. It can be used for a variety of purposes, including controlling serialization, providing runtime hints, or creating documentation. A number of improvements have been made to the way attributes and metadata can be utilized in C# 12, resulting in more maintainable and self-documenting code.

This article examines the advanced use of attributes in C# 12, including the new features introduced in this version, as well as practical examples and best practices.

How do Attributes work in C# 12?


The Attribute Parameters

As a result, attributes can now have parameters that are not just primitive types or strings. It is now possible to use read-only structs, enums, and other complex types as parameters. This allows you to create attributes that are more expressive and type-safe.

Attributes specific to a target

Attributes can now be more specific about where they can be applied, allowing you to define constraints based on the target, whether it be classes, methods, properties, or assemblies.

Enhancing the ability to reflect

With C# 12, reflection support for attributes has been improved, making it easier to retrieve attribute data at runtime, especially for custom attributes.

Attributes with nullable reference types

It is now possible to define attributes with nullable reference types, allowing for more precise definitions and eliminating null-related errors.

The creation of custom attributes

We can illustrate these new features by creating a custom attribute.

Defining a Custom Attribute as an example

using System.Reflection;

namespace CSharp12.AttributesAndMetadata;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)]
public class DeveloperInfoAttribute : Attribute
{
    public string DeveloperName { get; }
    public string Version { get; }
    public string? Notes { get; }

    public DeveloperInfoAttribute(string developerName, string version, string? notes = null)
    {
        DeveloperName = developerName;
        Version = version;
        Notes = notes;
    }

    public static void DisplayDeveloperInfo(MemberInfo member)
    {
        var attribute = member.GetCustomAttribute<DeveloperInfoAttribute>();
        if (attribute != null)
        {
            Console.WriteLine($"Member: {member.Name}");
            Console.WriteLine($"Developer: {attribute.DeveloperName}");
            Console.WriteLine($"Version: {attribute.Version}");
            Console.WriteLine($"Notes: {attribute.Notes}");
            Console.WriteLine();
        }
        else
        {
            Console.WriteLine($"No DeveloperInfo attribute found on {member.Name}.");
        }
    }

}

Here, we define a DeveloperInfoAttribute that can be applied to classes and methods. It includes three parameters: DeveloperName, Version, and Notes.

Applying the Attribute

Let's see how our custom attribute can be applied:

namespace CSharp12.AttributesAndMetadata;

[DeveloperInfo("Ziggy Rafiq", "0.0.1", "Initial version")]
public class ExampleClass
{
    [DeveloperInfo("Ziggy Rafiq", "0.0.2", "Fixed minor bugs")]
    public void ExampleMethod()
    {
        Console.WriteLine("Executing ExampleMethod.");
    }

}

The DeveloperInfoAttribute is applied to both a class and a method here in order to provide relevant metadata.

Attribute data retrieval at runtime

In C# 12, you can retrieve attributes using reflection. Let's create a method that displays the attributes of the SampleClass and its methods.

Attribute retrieval example

using CSharp12.AttributesAndMetadata;

Console.WriteLine("Hello from Ziggy Rafiq!");
DeveloperInfoAttribute.DisplayDeveloperInfo(typeof(ExampleClass));
DeveloperInfoAttribute.DisplayDeveloperInfo(typeof(ExampleClass).GetMethod("ExampleMethod"));

Output

As a result of running the above program, you will receive the following results:

Hello from Ziggy Rafiq!
Member: ExampleClass
Developer: Ziggy Rafiq
Version: 0.0.1
Notes: Initial version

Member: ExampleMethod
Developer: Ziggy Rafiq
Version: 0.0.2
Notes: Fixed minor bugs

Complex Types and Attribute Parameters

As of C# 12, you can now use more complex types as attribute parameters. Let's define a struct and use it as an attribute parameter.

Defining Complex Attribute Parameters

namespace CSharp12.AttributesAndMetadata;

public readonly struct VersionInfo
{
    public int Major { get; }
    public int Minor { get; }
    public int Patch { get; }

    public VersionInfo(int major, int minor, int patch)
    {
        Major = major;
        Minor = minor;
        Patch = patch;
    }
}
using System.Reflection;

namespace CSharp12.AttributesAndMetadata;

[AttributeUsage(AttributeTargets.Class)]
public class ComponentInfoAttribute : Attribute
{
    public string Name { get; }
    public string Version { get; }

    public ComponentInfoAttribute(string name, string version)
    {
        Name = name;
        Version = version;
    }

    public static void DisplayComponentInfo(Type componentType)
    {
        var attribute = componentType.GetCustomAttribute<ComponentInfoAttribute>();
        if (attribute != null)
        {
            Console.WriteLine($"Component Name: {attribute.Name}");
            // Split version into parts if needed
            var versionParts = attribute.Version.Split('.');
            if (versionParts.Length == 3)
            {
                Console.WriteLine($"Version: Major={versionParts[0]}, Minor={versionParts[1]}, Patch={versionParts[2]}");
            }

        }
    }
}

Using the Complex Parameter Attribute

Let's apply the ComponentInfoAttribute now:

namespace CSharp12.AttributesAndMetadata;

[ComponentInfo("ZiggyComponet","0.0.1")]
public class ZiggyComponent
{
}

Data Retrieval with Complex Attributes

As with the previous examples, you can retrieve this complex attribute:

ComponentInfoAttribute.DisplayComponentInfo(typeof(ZiggyComponent));

Output

This will produce the following output:

Component Name: ZiggyComponet
Version: Major=0, Minor=0, Patch=1

The best practices for using attributes

  1. Keep Attributes Lightweight: Attributes should be lightweight and not contain complex logic. Their primary function is to hold metadata.
  2. Use Meaningful Names: Choose meaningful names for our attributes to improve code readability and maintainability.
  3. Limit Usage: Utilize attributes judiciously to avoid cluttering our code with excessive metadata, which can make it harder to read and understand.
  4. Document Our Attributes: Provide XML documentation for our attributes to help other developers understand their use and purpose.
  5. Embrace Nullable Types: With nullable reference types, our attributes can clearly express optional parameters.

Summary

As a result of C# 12's significant enhancements in attribute and metadata handling, developers can write code that is easier to maintain and self-document. You can write cleaner and more expressive code by utilizing new features such as complex types as parameters, improved reflection capabilities, and enhanced targeting options.

Our applications will be more robust and reliable if you incorporate these practices not only to maintain the codebase but also to facilitate better communication among team members regarding the intent and use of various components. The attributes in C# can significantly enhance our coding experience and code quality as you explore these advanced features.

The source code for this article is available on my GitHub repository, and I would appreciate your support if you could like this article and also follow me on LinkedIn.


Similar Articles
Capgemini
Capgemini is a global leader in consulting, technology services, and digital transformation.